C语言——指针
一、指针定义
1、指针
指针是一个变量,指针存储的是数据在内存中的首地址
/* 定义方式一 */
int a = 1, *p;
p = &a; // & 表示取a的地址,p中存储的是a的地址,p指向a
printf("%d",*p) // 1 ,*p表示p所指向存储单元中的内容
/* 定义方式二 */
int a = 1;
int *p = &a;
2、指针变量与普通变量对比
int *p; // p与 * 结合,说明p是一个指针变量,然后还是int类型,说明p所指向的数据的类型时int类型
/* P是一个由返回int数据的指针所组成的数组 */
int *p[10]; // p先与[]结合,然后p[]再与*结合,说明数组中的元素是指针类型,int说明数组每个元素(指针类型)指向的数据是int类型,P是一个由返回int数据的指针所组成的数组
/* P是一个指向由整型数据组成的数组的指针 */
int (*p)[3]; // 首先从P处开始,先与*结合,说明P是一个指针然后再与[]结合,说明指针所指向的内容是一个数组,然后再与int结合,说明数组里的元素是整型的,所以P是一个指向由整型数据组成的数组的指针
int p(int); // 从P处起,先与()结合,说明P是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
/* P是一个指向有一个整型参数且返回类型为整型的函数的指针 */
int (*p)(int); // 从P处开始,先与指针结合,说明P是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int结合,说明函数有一个int型的参数,再与最外层的int结合,说明函数的返回类型是整型,所以P是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P开始,先与()结合,说明P是一个函数,然后进入()里面,与int结合,说明函数有一个整型变量参数,然后再与外面的 * 结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与 * 结合,说明数组里的元素是指针,然后再与int结合,说明指针指向的内容是整型数据。所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
3、空指针:
int *p;
p = NULL;
// 等价于 p = '/0';
// 等价于 p = 0;
4、运算符 & 和 *
& 是取地址址运算符,* 是间接运算符
&a 表示取a的地址
*p 表示指针p所指向地址中的内容
二、指针四大方面
要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区
1、指针的类型
/* 把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型 */
int *p; // 指针的类型是int*
char *p; // 指针的类型是char*
int **p; // 指针的类型是int**
int (*p)[3]; // 指针的类型是int(*)[3]
int *(*p)[4]; // 指针的类型是int*(*)[4]
2、指针所指向的类型
当通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
/* 从语法上看,只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型 */
int *p; // 指针所指向的类型是int
char *p; // 指针所指向的的类型是char
int **p; // 指针所指向的的类型是int*
int (*p)[3]; // 指针所指向的的类型是int()[3]
int *(*p)[4]; // 指针所指向的的类型是int*()[4]
3、指针的值——或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为 sizeof (指针所指向的类型) 的一片内存区。
一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址
4、指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数 sizeof (指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。
指针类型为 int的指针,只能指向 int类型的变量
三、指针的算术运算
指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以存储单元为单位
例一:
char a[20];
int *p = (int *)a;
p++;
指针p 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针p被加了1,编译器是这样处理的:它是向右移动了一个存储单元,即这里把指针p 的值加上了 sizeof(int),在32 位程序中,是被加上了4,因为在32 位程序中,int 占4 个字节。由于地址是用字节做单位的,故p 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来p 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。
一个存储单元的大小 = 指针所指向存储单元的值的类型
例二:
int array[20] = {0};
int *p = array;
for(i=0;i<20;i++)
{
(*p)++;
p++;
}
每一次循环,*p表示指针指向的存储单元中的值,第7行p指向的存储单元的数值加1。第8行表示p指针加1,即指针向高位移动一个存储单元。所以每次循环结束,指针就向后移动一个存储单元。
总结:
两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面,不多说了。
四、指针表达式
1、定义
一个表达式的结果是一个指针,那么这个表达式就叫指针表式。
2、++ 和 *
++ 与 * 的优先级相同,所以当两个运算符同时出现,没有括号时,遵循自右向左运算
int *p;
*p++; // 指针先向后移动一个存储单元,新的地址上的值
*++p; // 指针先向后移动一个存储单元,新的地址上的值
++*p; // 指针指向的存储单元中的值,进行加1
3、指针改变数据,将指针指向的内存单元上的值改变;而普通的改变值,只是将a临时指向新的一块内存区域。
五、数组与指针
数组的数组名其实可以看作一个指针,数组名表示数组的首地址。
int arr[10],*p;
p = arr; // arr == &arr[0], arr+1 <==> p+1 <==> &(arr[1])
1、利用指针给一维数组赋值
for(p=arr; p-arr<10; p++)
{
scanf("%d",p); // 每次循环完,指针p就指向数组的下一个位置
}
// 等价于
for(int i=0; i<10; i++)
{
scanf("%d",&(arr[i]));
}
2、引用一维数组元素
- arr[i]
- *(p+i)
- *(arr+i)
3、被调用函数的数组形参
- int *a
- int a[]
- int a[M]
int fun(int *a){}
// 等价于
int fun(int a[]){}
// 等价于
int fun(int a[M]){}
main(){
int arr[10] = {1,2,3,4,5,6,7,8};
fun(arr); // 数组arr的0索引地址,作为数组的首地址;也可可以是 &(arr[4]) 索引4号作为首地址传到fun函数
return 0;
}
3、二维数组
3.1、二维数组的定义
类型名 数组名[ 常量表达式1 ][ 常量表达式2 ]
int a[2][2]
二维数组可以看成是矩阵(或表格),常量表达式1可以看成矩阵(表格)的行数,常量表达式2可以看成矩阵(表格)的列数。
二维数组可以看成一个一维数组a[0],a[1],数组中的元素又是一个个一维数组a[0][0],a[0][1]和a[1][0],a[1][1]
在内存中,二维数组站一系列连续的存储单元。存放的顺序是“ 按行存放 ”
3.2、二维数组的初始化
3.2.1、赋初值个数与数组元素个数相同
int a[4][3] = {{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
3.2.2、一行所赋初值个数与数组每行元素不同
系统自动给该行后面的元素补充初值0,但是不能跳过每行前面的元素给后面的元素赋初值
int a[4][3] = {{1,2,3},{1},{1,3},{1}}
// 等价于
int a[4][3] = {{1,2,3},{1,0,0},{1,3,0},{1,0,0}}
3.3.3、赋初值行数少于数组的行数
系统自动给不足行赋初值0
int a[4][3] = {{1,2,3},{1}}
// 等价于
int a[4][3] = {{1,2,3},{1,0,0},{0,0,0},{0,0,0}}
3.3.4、赋初值时省略花括号
系统按照元素在内存中排列的顺序,将{}中的元素一一对应地赋给各个元素,若数据不足,后面的元素自动赋初值0
int a[4][3] = {1,2,3,4,5,6,7}
// 等价于
int a[4][3] = {{1,2,3},{4,5,6},{7,0,0},{0,0,0}}
3.4、通过赋初值定义二维数组的大小
在一维数组中可以省略常量表达式,通过赋初值个数来确定数组的大小。
==> 二维数组也可以省略常量表达式,但是 只能省略常量表达式1
int a[][3] = {{1,2,3},{1},{2,3}}
// 等价于
int a[3][3] = {{1,2,3},{1},{2,3}}
int a[][3] = {1,2,3,4,5,6,7}
// 等价于
int a[3][3] = {{1,2,3},{4,5,6},{7,0,0}}
3.5、二维数组与指针
3.5.1、二维数组的名字
在二维数组中,数组名也是一个存放地址的指针,它的值是二维数组中第一个元素的地址。a与a[0]的值相 同,a+1与a[1]的值相同,a+2与a[2]的值相同,他们分别表示第一行、第二行、第三行的首地址。
===> 二维数组的名字是一个行指针,表示一行的首地址
3.5.2、二维数组元素的地址
// 三种方式等价
&a[i][j]
a[i]+j
*(a+i)+j
3.5.3、引用二维数组元素
- 通过地址引用二维数组元素
int a[3][4]
a[i][j] <==> *(a[i]+j) <==> *(*(a+i)+j)
- 通过建立指针数组引用二维数组元素
int *p[3],a[3][2]
*(p[i]+j) <==> *(*(p+i)+j) <==> (*(p+i))[j] <==> p[i][j]
- 通过建立一个行指针引用二维数组元素
int a[3][2],(*p)[2]
p[i][j] <==> *(p[i]+j) <==> *(*(p+i)+j) <==> (*(p+i))[j]
3.5.4、二维数组名与指针数组作为实参
-
二维数组名作为实参
当二维数组名作为实参时,形参必须是一个行指针变量
main(){
int s[M][N];
fun(s);
}
// 形参的三种书写形式
fun(int (*a)[N])
fun(int a[][N])
fun(int a[M][N])
- 指针数组作为实参
当指针数组名作为实参时,形参必须是一个指向指针的指针
main(){
int s[M][N], *ps[M];
for(int i=0;i<M;i++)ps[i]=s[i];
func(ps)
}
// 形参的三种书写形式
func(int *a[M])
func(int *a[])
func(int **a)