指针是 C 语言最重要的概念之一,也是最难理解的概念之一。
指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。
字符*
表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char*
表示一个指向字符的指针,float*
表示一个指向float
类型的值的指针。
int* intPtr;
上面示例声明了一个变量intPtr
,它是一个指针,指向的内存地址存放的是一个整数。
星号*
可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。
int *intPtr;
int * intPtr;
int* intPtr;
本书使用星号紧跟在类型关键字后面的写法(即int* intPtr;
),因为这样可以体现,指针变量就是一个普通变量,只不过它的值是内存地址而已。
这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。
// 正确
int * foo, * bar;
// 错误
int* foo, bar;
上面示例中,第二行的执行结果是,foo
是整数指针变量,而bar
是整数变量,即*
只对第一个变量生效。
一个指针指向的可能还是指针,这时就要用两个星号**
表示。
int** foo;
上面示例表示变量foo
是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。
&
运算符用来取出一个变量所在的内存地址。
int x = 1;
printf("x's address is %p\n", &x);
上面示例中,x
是一个整数变量,&x
就是x
的值所在的内存地址。printf()
的%p
是内存地址的占位符,可以打印出内存地址。
*
这个符号在=
号左边表示定义指针变量,在=
号右边表示解引用,用来取出指针变量所指向的内存地址里面的值。
void increment(int* p) {
*p = *p + 1;
}
上面示例中,函数increment()
的参数是一个整数指针p
。函数体里面,*p
就表示指针p
所指向的那个值。对*p
赋值,就表示改变指针所指向的那个地址里面的值。
上面函数的作用是将参数值加1
。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。
变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。
&
运算符与*
运算符互为逆运算,下面的表达式总是成立。
int i = 5;
if (i == *(&i)) // 正确
指针变量的大小取决于地址的大小。
printf("%llu\n", sizeof(char)); // 1
printf("%llu\n", sizeof(char*)); // 8
printf("%llu\n", sizeof(int)); // 4
printf("%llu\n", sizeof(int*)); // 8
注意:指针变量的大小和指针所指向内容的类型无关,只要指针类型的变量,在相同平台下,大小都是相同的。
声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存空间里面的值是随机的,也就是说,指针变量指向的值是随机的。这时一定不能去读写指针变量指向的地址,因为那个地址是随机地址,很可能会导致严重后果。
int* p;
*p = 1; // 错误
上面的代码是错的,因为p
指向的那个地址是随机的,向这个随机地址里面写入1
,会导致意想不到的结果。
正确做法是指针变量声明后,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的初始化。
int* p;
int i;
p = &i;
*p = 13;
上面示例中,p
是指针变量,声明这个变量后,p
会指向一个随机的内存地址。这时要将它指向一个已经分配好的内存地址,上例就是再声明一个整数变量i
,编译器会为i
分配内存地址,然后让p
指向i
的内存地址(p = &i;
)。完成初始化之后,就可以对p
指向的内存地址进行赋值了(*p = 13;
)。
为了防止读写未初始化的指针变量,可以养成习惯,将未初始化的指针变量设为NULL
。
int* p = NULL;
NULL
在 C 语言中是一个常量,表示地址为0
的内存空间,这个地址是无法使用的,读写该地址会报错。
指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。
(1)指针与整数值的加减运算
指针与整数值的运算,表示指针的移动。
#include <stdio.h>
int main()
{
int n = 10;
int* pi = &n;
char* pc = (char*)&n;
printf("&n = %p\n", &n); // 000000894037F754
printf("&pi = %p\n", &pi); // 000000894037F778
printf("pi = %p\n", pi); // 000000894037F754
printf("pi+1 = %p\n", pi + 1); // 000000894037F758 指针向高位移动4个字节
printf("&pc = %p\n", &pc); // 000000894037F798
printf("pc = %p\n", pc); // 000000894037F754
printf("pc+1 = %p\n", pc + 1); // 000000894037F755 指针向高位移动1个字节
}
上面的示例中,n
是一个指针。
由于n
本身是整数类型(int
),跟pc
的类型(char*
)并不兼容,所以强制使用类型投射,将n
转成char*
。你可能以为pi + 1
等于pc + 1
,那是不对的。
原因是pi + 1
表示指针向内存地址的高位移动4个字节,而pc + 1
表示指针向内存地址的高位移动1个字节。
指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每次就移动多少个字节。
(2)指针与指针的加法运算
指针只能与整数值进行加减运算,两个指针进行加法是非法的。
unsigned short* j;
unsigned short* k;
x = j + k; // 非法
上面示例是两个指针相加,这是非法的。
(3)指针与指针的减法
相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。
高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。
这时,减法返回的值属于ptrdiff_t
类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h
里面。
short* j1;
short* j2;
j1 = (short*)0x1234;
j2 = (short*)0x1236;
ptrdiff_t dist = j2 - j1;
printf("%td\n", dist); // 1
上面示例中,j1
和j2
是两个指向 short 类型的指针,变量dist
是它们之间的距离,类型为ptrdiff_t
,值为1
,因为相差2个字节正好存放一个 short 类型的值。
(4)指针与指针的比较运算
指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1
(true)或0
(false)。
const int* p
或int const *p
:const
放在*的左边。修饰的是*p
,指针指向的内容不能修改,指针变量本身可以修改。
int * const p
: const
放在*的右边。修饰的是p
,指针变量本身不可以修改,指针指向的内容可以修改。称为指针常量或常指针。
int const * const p
或const int * const p
:*的左右两边都有const
,修饰的是*p
和p
,所以都不能被修改。
void*
类型,无具体类型的指针(泛型指针),void*
类型的指针大部分使用在函数参数的部分,用来接收不同类型数据的地址。
但是void*
类型的指针不能直接与整数作加减运算和解引用运算。
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
如何规避野指针:
NULL
,指针使用之前检查有效性。