计算机中所有的数据都必须放在内存中,以二进制的形式存储在内存中,才能被CPU所使用。不同类型的数据占用的字节数不一样,为了正确地访问这些数据,必须为每个字节都编上号码。将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,权限中包含执行权限的内存块就是代码,否则是数据。
CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
1、指针变量
若果一个变量存储了一份数据的指针,我们就称它为指针变量,指针变量的值就是某份数据的地址。
1.1 定义
定义指针变量和定义普通变量类似,不过需要在变量名前加*号。*表示其实一个指针变量,类型表示该指针变量所指向的数据的类型 。指针变量也可以被多次写入,随时都能够改变指针变量的值。定义指针变量时必须带*
,给指针变量赋值时不能带*
。指针变量也可以连续定义,但是要注意每个变量前面都要带*。
1.2 取值
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:*pointer; *在此处
称为指针运算符,用来取得某个地址上的数据。普通变量只需一次即可取到数据,指针变量需要2次,因为其内存中存的是地址,还需要根据地址去取数据,即使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
*
在不同的场景下有不同的作用:*
可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*
表示获取指针指向的数据,或者说表示的是指针指向的数据本身。指针变量定义时初始化,可使用*pointer取得数据,给指针变量本身赋值时不能加*。
1.2.1 关于*和&
若有一个 int 类型的变量 a,pa 是指向它的指针,即int a = 12;int* pa = &a;那么*&a
和&*pa的含义是:
*&a
可以理解为*(&a)
,&a
表示取变量 a 的地址(等价于 pa),*(&a)
表示取这个地址上的数据(等价于 *pa),即*&a
仍然等价于 a。 &*pa
可以理解为&(*pa)
,*pa
表示取得 pa 指向的数据(等价于 a),&(*pa)
表示数据的地址(等价于 &a),所以&*pa
等价于 pa。
1.2.2 指针变量的运算
对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,这样也就不知道在这个变量地址的后面是否有数据。因此不要尝试通过指针获取下一个变量的地址,如*(p+i),获取到的数据有可能并不知道你想要的的。
当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。最后对于普通变量而言,最好不要对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
2、数组指针
定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。数组名和数组首地址并不总是等价,可以说“被转换成了一个指针”。对于数组而言,可以直接将数组名赋值给指针变量 。若一个指针指向了数组,就称它为数组指针(Array Pointer)。数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关。
1 int arr[] = { 99, 15, 100, 888, 252 };
2 int *p = arr;
数组指针可以使用两种方法来访问数组元素,一种是使用下标,另外一种是使用指针。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
2.1 数组指针使用含义
若p 是指向数组 arr 中第 n 个元素的指针, *p++、*++p、(*p)++ 的含义是:
*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素。
*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
(*p)++ ,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
3、指针数组
若一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:
dataType *arrayName[length];
[ ]
的优先级高于*
,该定义形式应该理解为:
dataType *(arrayName[length]);
若存放的是字符串,需要注意的是:字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。
4、二维数组指针
1 int (*p)[4] = a;
括号中的*
表明 p 是一个指针,它指向一个数组,数组的类型为int [4]
,这正是 a 所包含的每个一维数组的类型。[ ]
的优先级高于*
,( )
是必须要加的,否则就是指针数组。对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4]。p+1就是前进 4×4 = 16 个字节。
根据定义可以知道:p
指向数组 a 的开头,也即第 0 行;p+1
前进一行,指向第 1 行;*(p+1)
表示取地址上的数据,也就是整个第 1 行数据。
4.1 表达式含义
*(p+1)+1
表示第 1 行第 1 个元素的地址:*(p+1)
单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针。
*(*(p+1)+1)
表示第 1 行第 1 个元素的值:*(p+1)+1表示第 1 行第 1 个元素的地址,加上*表示取地址中的值。
可知:
1 a+i == p+i
2 a[i] == p[i] == *(a+i) == *(p+i)
3 a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
4.2 指针数组和二维数组指针的区别
定义如下:
1 int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
2 int (*p2)[5]; //二维数组指针,不能去掉括号
指针数组是一个数组,只是每个元素保存的都是指针。二维数组指针是一个指针,它指向一个二维数组。
5、字符串指针
2种方法:定义字符串数组,再赋值给指针变量;直接使用一个指针指向字符串。
1 //使用*(str+i)
2 for(i=0; i<len; i++){
3 printf("%c", *(str+i));
4 }
5 printf("\n");
6 //使用str[i]
7 for(i=0; i<len; i++){
8 printf("%c", str[i]);
9 }
使用指针变量输出字符串和下标输出的区别:在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
6、指针变量作为函数参数
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
1 max(int *intArr, int len)
数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。但数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率。需要强调的是:不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。
7、指针作为函数返回值
允许函数的返回值是一个指针(地址),这样的函数称为指针函数。用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。也就是所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。
8、函数指针
函数指针的定义形式为:returnType (*pointerName)(param list);
注意( )
的优先级高于*
,第一个括号不能省略,如果写作returnType *pointerName(param list);
就成了函数原型,它表明函数的返回值类型为returnType *
。
8.1 使用指针实现对函数的调用
1 //定义函数指针 max是一个函数
2 int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
3 ...
4 maxval = (*pmax)(x, y);
pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )
的优先级高于*
,第一个括号不能省略。
9、二级指针
若一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。指针变量也是一种变量,也会占用存储空间,也可以使用&
获取它的地址。获取指针指向的数据时,一级指针加一个*
,二级指针加两个*,类推其他级别指针。
1 int a =100;
2 int *p1 = &a;
3 int **p2 = &p1;
4 int ***p3 = &p2;
5 printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
6 printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
7 printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
8 printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
以上每行输出的值是一样的,因为指向一样的地址。
方框里面是变量本身的值,方框下面是变量的地址。
10、总结
程序在运行过程中需要的是数据和指令的地址,程序被编译和链接后,变量名、函数名等都会消失,取而代之的是它们对应的地址。
10.1 常见指针变量的定义
定 义 | 含 义 |
---|---|
int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。 |
int **p; | p 为二级指针,指向 int * 类型的数据。 |
int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
int (*p)[n]; | p 为二维数组指针。 |
int *p(); | p 是一个函数,它的返回值类型为 int *。 |
int (*p)(); | p 是一个函数指针,指向原型为 int func() 的函数。 |
10.2 使用
1)指针变量可以进行加减运算,并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关
2)给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数
3)使用指针变量之前一定要初始化,对于暂时没有指向的指针,建议赋值NULL
4)两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数
5)数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针