指针也是一个变量,只不过它存储的值跟别的类型不同,它存放的是另外一个变量的内存直接地址。
变量的地址
访问变量的地址是通过 &
符和 %p
来实现的。
int a = 5;
printf("%p \n",&a); //%p 内存地址的输出符, & 访问地址运算符
复制代码
指针声明
指针声明一般形式是
类型 * 指针名;
复制代码
指针存放的是一个变量的地址,所以指针的类型跟它存放的变量类型相同。即
int *p1; //一个整型指针
char *p2; //一个字符型指针
float *p3; //一个浮点型指针
复制代码
赋值
对指针赋值,与之前提到的取变量地址类似,在指针初始化和初始化之后赋值都是可以的
int a = 5; // &a = 0x7ffeefbff5ac
int *p;
p = &a; // p = 0x7ffeefbff5ac ,
int *r = &a; // r = 0x7ffeefbff5ac
复制代码
上面的代码有写到,0x7ffeefbff5ac
,这是一个代表内存地址的十六进制数,所有的指针内容都是如此的格式,与指针类型无关。
空指针
初始化指针的时候,如果没有确切的地址可以赋值,我们可以使用 NULL
值对指针初始化,产生了一个 空指针。
int *p = NULL; // 此时 p = 0x0
复制代码
使用
使用指针的时候我们一般用 *
运算符,来返回指针指向的内存地址存放的变量的值,可能有点绕口,直接看代码
int a = 5;
int *p = &a;
printf("%p \n",p); // 结果是: 0x7ffeefbff5ac
printf("%d \n",*p); // 结果是:5
复制代码
这个地方要小心注意的是,不能看到 a
跟 *p
的值一样,就直接使用 *p
,这个是 严重错误 的。
int a = 5;
int *p;
*p = 5; // 不能直接用 *p 赋值
复制代码
因为 p
也是一个变量,这种使用方式就是在 p
都未初始化时,直接用 *
操作符,对 p
进行了访问,不能访问未初始化的指针(未初始化 不等于 空指针), 这是指针的特殊性(别的类型如int
、float
等,不初始化时系统会默认给初始化,指针不会)。
指针与数组
将数组对指针赋值时,也是 类型 * 指针名
的形式。
int a[10];
int *p = a;
复制代码
如上图的将一个 int
数组赋值给 int *p
。使用数组时,单独的数组名相当于数组第一个元素的地址,即&a[0]
。
指针的偏移
指针的偏移指的是指针存储的地址的偏移,单位是指针类型的字节长度。int *p
,指针 p
存储的地址,偏移单位为一个int
长度的字节。char *p
,指针 p
存储的地址,偏移单位为一个char
长度的字节。
...
偏移和数组
int a[5] = {1,2,3,4,5};
int *p = a;
printf("%p --- %d \n",p,*p); // 0x7ffeefbff5a0 --- 1
p++;
printf("%p --- %d \n",p,*p); // 0x7ffeefbff5a4 --- 2
//一个 int 占4个字节
char b[3] = {'A','c','n'};
char *q = b;
printf("%p --- %c \n",q,*q); // 0x7ffeefbff5ad --- A
q++;
printf("%p --- %c \n",q,*q); // 0x7ffeefbff5ae --- c
//一个 char 占1个字节
复制代码
地址偏移取值,常常用的是 *(p + n)
,也可以用 p[n]
,用数组来类比
int a[5] = {1,2,3,4,5};
int *p = a;
//取第2个元素,就是首元素地址偏移一个单位之后的地址所指向的值
// a[1] -> *(&a[0] + 1) -> *(a + 1) -> *(p + 1) -> p[1]
a[1] == p[1] //两者值相等
复制代码
通过上述演变,发现数组跟指针很类似,但是它们不是同一东西,因为 数组是地址常量,指针是地址变量 。 如上面的 int a[5]
, a
就是一个地址常量,它就是数组的首地址,它不可变。 而 int *p
,p
是一个地址变量,它的值可以随意修改。
常量指针 和 指针常量
常量指针,是指针指向的内容是常量,指针指向的内容不可通过指针修改(* p = 4
,错误的),指针的指向可以修改。形式一般为
const int *p;
int const *q;
复制代码
使用方面
int a = 3;
const int *p = &a;
*p = 4 ; //通过指针修改值,错误的
a = 4; //变量自行修改,合法的
int b = 4;
p = &b; //指针的指向可以修改,
复制代码
指针常量,是指针本身是一个常量,指向的地址不可修改,但是指针指向内容可通过指针修改。
int * const p;
复制代码
int a = 3;
int b = 4;
int *const p = &a;
p = &b ; //指针的指向不可修改,错误的
*p = 4 ; //指针指向的内容可通过 *p 修改
复制代码
还有一种就是指向常量的常量指针,即 const int * const p
。不能修改指针指向,也不能通过指针修改指向的值。
int a = 5;
int b = 4;
const int * const p = &a;
*p = 6; //不可通过 *p 修改指向内容的值
p = &b; //不可修改指针指向
复制代码
但是可以通过另外一种方式,指向另外的一个指针
int a = 5;
int *b = &a;
const int * const p = b;
printf("%d \n",*p); // 5
*b = 2;
printf("%d \n",*p); // 2
复制代码
复杂类型
有些时候,会将其他类型和指针进行结和,形成新的类型,如
int *p[10];
int (*p)[10];
复制代码
p
离谁最近就是什么类型。按照符号右结合的原则,第一个 int *p[10]
应该是
// 用一个A来代替 p[10] ,p离 [10] 最近
int *p[10] -> int *(p[10]) -> int *A
复制代码
这样可以得出,A 的类型(即 p[10]
的类型)是一个整型指针,p[10]
很明显这是一个数组声明的结构。所以 int *p[10]
是一个数组,数组元素是整型指针。
下面看看第二个 int (*p)[10]
,()
优先级比较高,先将其看成一个整体
//用一个A代替 *p ,p 离 * 最近
int (*p)[10] -> int A[10]
//可以理解为
int A[10];
*p = A;
p = &A;
复制代码
A 的类型(即 *p
的类型)是一个数组, *p
这是一个指针访问方式,指向了 A( 一个整形数组)。则 int (*p)[10]
是一个指针,指向的是一个整型数组。
当然跟 整型数值指针 略有不同
int a[10];
int *p = a; //p 是指向 数组首元素 a 地址的指针
int (*q)[10] = &a; //q 是指向数组 a[10] 地址的指针
sizeof(*p); //这是一个 int 的字节长,因为 *p 是一个 int 变量
sizeof(*q); //这是一个 int[10] 的字节长,*q 是一个 int 数组
复制代码
可能 int (*p)[10]
这种类型,多用于二维数组传参定义形参的时候把。
还有其他复杂类型,比如
int **p;
复制代码
二级指针,指向另外一个整型指针。可以用来改变它指向的 整形指针的指向,如
int a,b;
int *p = &a;
int **q = &p; //此时 *q == p
*q = &b; //此时 *q == p == &b, 即 p 的指向变化成了 int b
复制代码
和
int *p(float)
复制代码
一个函数,参数为 float
类型,返回值为 指向整型的指针 int *
。
int (*p)(float)
复制代码
这是一个指针,指向的是一个函数,函数的参数为 float
类型,返回值为 int
类型。这个就是我们常数的 函数指针 。可以直接将方法名赋值给指针,函数名是函数的入口地址,与数组类似。
int func(float) { return 1;}
复制代码
int (*p)(float) ; //或者 int (*p)(float) = func
p = func;
复制代码
练习一下,
int (*(*p[10])(int))(int);
复制代码
一步步拆解,还是右结合原则得到的 p[10]
,说明这是一个数组。
int (*(*p[10])(int))(int) //然后将 `p[10]` 用一个符号代替,
⬇️⬇️⬇️
int (*(*A)(int))(int) //结合 *A ,说明数组里面的元素是 某种指针
⬇️⬇️⬇️
int (*B(int))(int) //B 替换 *A,结合 *B(int),说明指针指向的是一个函数,函数参数为 int 类型,返回值为另外一个指针
⬇️⬇️⬇️
int C(int) //C 替换 (*B(int)) ,得到了一个参数为 int ,返回值为 int 的函数。
复制代码
整理下来:这是一个数组,数组里面的元素是一种指针A,指向了一种参数为一个 int
,返回值为一个指针B的函数,指针B指向了一种参数为一个 int
类型,返回值也是一个 int
类型的函数。
它的大小的是多少?80个字节,因为这是一个元素为指针的长度为10的数组,而指针的大小占8个字节,不论指针指向何种类型。