# 2.8 高级指针 如果你爱一个人,就让他用指针,因为指针是天堂;如果你恨一个人,也让他用指针,因为指针是地狱。 指针是 C/C++ 语言中功能最强大,最具灵活性的功能之一,用好指针可以使程序更加优美简洁。指针的实质是非常简单的,每个人都很容易学会指针,但每一个 C/C++ 开发者,包括很多高手在内,都必须时刻注意指针的使用,因为一旦违反指针的使用规则,程序便极具破坏性。 ## 2.8.1 指针的使用 我们在《2.1 基础语法》展示了指针最基本的用法。可以理解为指针就是某个变量或数据结构的索引地址,根据这个地址可以间接的访问这个变量或数据结构。 ```cpp typedef struct _ST_A { int a; int b; }ST_A, *PST_A; ST_A AStruct = {15, 18}; PST_A PAStruct = &AStruct; // PAStruct 是一个指针,他获得了 AStruct 的索引地址。 printf("%d", PAStruct->a); // 通过 PAStruct 间接访问 AStruct 结构体及其成员。 ``` ## 2.8.2 指针的实质和意义 一旦程序运行起来,所有的变量都存在于内存之中,有特定的内存空间用于容纳变量的值,这个空间的首地址即为对应变量的起始地址。其实,指针也是一个变量,只不过这种变量保存的内容是其他变量的内存起始地址(或称作首地址)。 | Addr | Var | |--------|-----------| | 0xAA55 | short A_L | | 0xAA56 | short A_H | | 0xAA57 | - | 如上表所示,有一个 short 型变量 A, 其中 0xAA55 这个地址保存了 A 的低 8 位(即 A_L),而 0xAA56 这个地址保存了 A 的高 8 位(即 A_H)。如果有一个指针指向 A,那么这个指针的值即为变量 A 的首地址,也就是 0xAA55。 指针是一个变量,每种变量都有长度,指针变量的长度是多少呢?这与你所使用机器的地址总线位宽有关。如果是 32 位机,那么指针的长度就是 4,64 位机对应的指针长度为 8。 使用指针最大的好处是避免了同一组内存数据的重复。比如说,C 语言函数参数是值传递的,也就是说调用函数时会发生参数的拷贝。如果参数是一个整形数据,那么这个拷贝不会花费太长时间。如果这个参数是 1K 长的数组呢,那每次调用这个函数岂不是都要进行 1K 数据的拷贝?那太低效了,这个时候我们可以使用指针,这样在函数调用时,只需要拷贝一个指针长度的数据,却在函数内部通过指针间接访问全部 1K 数据。 即便不是作为参数,你会发现,很多情况下需要获得一个长数据,或者结构体,直接拷贝一份这样的数据是不值当的,我们需要一个类似别名的东西能间接引用这个数据,这个时候我们就可以使用指针。 指针提供了间接访问数据的功能,这样的功能避免了数据使用的重复,避免了拷贝过程,并提供了类似别名访问的机制,使程序变得高效。 ## 2.8.3 指针与数组 我们知道,数组中的元素是何种类型,我们就将这个数组称为 X型数组。例如: ```cpp short arry[256]; ``` 被称作 short 数组。同样,如果数组中保存的是指针类型,我们则称这个数组为指针数组。 ```cpp short* parry[256]; // 这是一个 short* 类型的数组, 数组中的每个元素都是一个指向 short 型变量的指针. ``` 请记住,指针数组的实质仍是数组。 通过 ‘&’ 符号,能够获得变量的地址,那么该如何获得数组的地址? 数组中的元素是连续排列的,如果取某个元素的地址来代表整个数组,那显然是数组的首个元素地址最具代表性。数组首个元素的地址被称作数组首地址,数组名就代表了这个地址。我们看以下的程序: ```cpp /* * @brief: Program 2-8-3-1 */ short* parry[256]; void* tmp = &parry[0]; if(tmp==parry) { printf("True\n"); // 这句一定会被打印出来. } ``` 由于数组的名字代表了数组首地址(也就是指针),那么,我们也可以把一个指向某连续内存区的指针看作数组: ```cpp unsigned char* pointer; pointer = calloc(256); // 分配一片长度为 256 的连续内存区, 并将这片内存区清零. pointer[15] = 0xAA; printf("Pointer+15=0x%x.\n", *(pointer+15)) // 输出内容为 “Pointer+15=0xaa.” ``` 数组名这个特殊指针,与其他指针变量最大的差异在于:我们可以对指针变量进行赋值操作,但向数组名赋值是不和法的。这是因为数组是静态分配的,它的首地址是固定的,可以说数组名这个特殊指针是自带 const 属性的。因此,下面的程序将导致编译错误: ```cpp short* parry[256]; parry++; // 这里有个错误. ``` ## 2.8.4 指针与结构体/联合体 结构体名称和首个元素的地址就代表了结构体的地址: ```cpp /* * @brief: Program 2-8-4-1 */ typedef struct _STA_T { int a; }STA_T; typedef struct _STB_T { STA_T b; int c; }STB_T; void fun(STA_T* st) { STB_T* stb = st; printf("%d", stb->c); // 注意,结构体指针使用 "->" 来访问元素,而非 "." . } int main(void) { STB_T stx; stx.c = 5; fun(&stx); // 打印内容为 “5”. } ``` ## 2.8.5 指针的类型转换 不难注意到,Program 2-8-3-1 中将两种不同类型的指针直接进行了比较,这里会产生一个编译警告。我们该如何消除它? ```cpp short* parry[256]; void* tmp = &parry[0]; if(tmp==(void*)parry) { printf("True\n"); } ``` 没错,指针也是一个类型,也就是说,类型转换的概念同样适用于指针。我们可以把一种类型的指针转换成另一种类型的指针,也可以把指针转换成整数,甚至可以把整数转换成指针。还记得 2.1.11 中的例子么? ```cpp // 假设系统是从低位开始寻址. unsigned int a = 0x11223344; char* pa = (char*)&a; // 通过 & 符号取变量 a 的地址. 指针 pa 的值便是变量 a 的首地址. printf("%d.\n", *pa); // 0x44. pa++; printf("%d.\n", *pa); // 0x33. pa++; printf("%d.\n", *pa); // 0x22. pa++; printf("%d.\n", *pa); // 0x11. pa++; ``` 这是指针类型转换的一种用法,相当于把变量 a 看作长度为 4 的 char 型数组,然后我们通过 pa 指针一次访问了这个数组中的全部元素。 指针的转换同样会遇到位数问题: ```cpp char arry[256]; printf("%d", (short)arry); // 数据将被截断. ``` 有一种特殊类型的指针,它可以接收任何其他类型的指针,即 void* 型指针。可以将任何其他类型的指针直接赋值给 void* 型指针变量而不产生任何编译错误或编译警告。void* 型指针十分有效,常被用作函数参数或结构体成员,用于传入任何数据,或捆绑任何类型。 ```cpp int* p0; void* p1 = p0; ``` ## 2.8.6 指针与函数 函数、或者说程序本身在运行时也是要占用内存空间的。因此,可以使用指针,像指向某个变量那样指向这个函数: ```cpp typedef void (*P_FUN_TYPE)(int a); void fun(int a) { printf("%d", a); } void main(void) { int a = 0x10; P_FUN_TYPE pfun = fun; pfun(a); // 与直接调用 fun(a) 是一样的. } ``` 这样可以指向函数的指针称为函数指针。在一般的程序中,函数指针并不常见。但,如果你编写的程序具有某种框架,例如 linux 内核中的驱动框架,就会大量使用函数指针了。 ## 2.8.7 多级指针 前面提及指针的实质是变量,因此也有一个内存空间用于保存指针的值,同样也有一个内存地址与之对应。这就是说,我们可以通过一个指向指针的指针去访问另一个指针,这被称为二级指针或多级指针。 ```cpp int a=10; int* pa = &a; int** ppa = &pa; printf("%d\n", **ppa); ``` ## 2.8.8 指针参数 以上所说的全部类型指针都可以作为函数的参数来进行传递。 ```cpp void fun(int* arry, int size) { int i; for(i=0; i