1.指针与别名陷阱
如果有一块内存区域,指向这块内存区域的指针有多个,其中每一个指针都是其他指针的别名。
别名操作在优化程序是会造成很多麻烦,如下面的例子:
void f1(int *x, int *y){ *x += 2 * *y;}
void f2(){ *x += *y; *x += *y;}
上面的两段代码,第一段访问两次寄存器,进行一次乘法计算,第二段代码进行4次访问寄存器,两次乘法。这两段代码的效率明显不一样,优化的时候肯定选择效率高的代码,但是两段代码的执行结果都是一样的吗?
当x和y都指向同一块内存时,结果并不是一样的:
当x和y都指向变量a,且a = 3;
执行第一段代码后a的值为9,执行第二段代码后a的值为12 。原因就是x和y都指向同一块内存区域,每次操作都会改变这块内存的值。
2.数组的指针
在C语言中,一个指针变量同样可以指向一个数组:
int (*p)[10];
表示p是一个指针,指向一个数组对象,该数组是一个拥有10个整数的数组。
因此当p+1时,p移动的字节数应该是等于p所指向的数组对象的字节数。如上面的p+1,应该移动sizeof(int)*10也就是40个字节,如下图所示
下面的程序演示了指向数组的指针:
#includeint main(void){ int a[5] = {1, 2, 3, 4, 5}; //数组初始化 int (*p)[5]; //数组指针 int *ptr; p = &a; ptr = (int *)(p + 1); //将数组指针转换为整型的指针 printf("the result is : %d\n", *(ptr-1)); //输出数组的最后一个元素 return 0;}
运行结果为:
the result is : 5
p为数组指针,当p+1后,p指向了数组后面的位置,并转换为整型指针,当整型指针前移4个字节时,就指向了数组的最后一个元素。
3.指针的指针
在C语言中,指针可以指向指针,也就是说,指针变量的值可以是另外一个指针变量的地址。
定义一个指向指针的指针变量:
int **p;
如下程序:
#includeint main(){ int a; int *p; int **q; a = 100; p = &a; //p指向变量a q = &p; //q指向指针p printf("var a : %d\n", a); //输出变量的值 printf("pointer p : 0x%x\n", *p); //输出指针的值 printf("pointer pointer q : 0x%x\n", *q); //输出指向指针的指针的值 return 0;}
运行结果
4.指针与参数传递
使用传递变啦很指针的方法来改变参数本身的值。下面的例子使用了指针作为餐胡传递,交换实现两个变量的值。
#includevoid swap(int *a, int *b){ int t; t = *a; *a = *b; *b = t;}int main(void){ int a, b; a = 1; b = 2; printf("a, b : %d, %d\n", a, b); swap(&a, &b); printf("a, b : %d, %d\n", a, b); return 0;}
运行结果:
函数将两个指针作为参数赋值到栈帧上,但是并没有改变指针本身的值,而是通过指针修改所指向的内容,这时,修改是可以被调用这函数看见的。
同理,如果需要修改指针本身,则需传递的参数应该是指针的指针。
#include#include void alter(int **p){ int *q; q = (int *)malloc(sizeof(int));//分配一块存储整型变量的内存 *q = 100; //将该变量的值设置为100 *p = q; //是指针的指针所指向的内容指向这块新的内存}int main(void){ int a; int *p; a = 10; p = &a; printf("p : 0x%x, *p %d\n", p, *p); alter(&p); //更改指针变量本身的值 printf("p : 0x%x, *p %d\n", p, *p); return 0;}
运行结果:
5.指针类型的意义
指针的本质就是一个无符号的整型,代表一个内存单元的单元号,在定义一个指针变量的同时,往往会声明该指针变所指向的数据的类型,如下:
int *p;
表示该指针变量p指向的数据是一个整型,其作用在于告诉编译器需要从该地址处向后看多少个字节,把这些字节当做一个对象来看。
下面的程序演示指针所指向的数据类型的意义:
#includetypedef struct{ int array[2]; char ch;}Test;int main(void){ Test var = {0x12345678, 0x12345678, 0x30}; //初始化结构体 char *p; Test *q; //将指针p转换为指向字符型变量,向后看一个字节 p = (char *)&var; printf("1 byte : 0x%x\n", *p); //将指针p转换为指向短整型变量,向后看两个字节 printf("2 byte : 0x%x\n", *(short *)p); //将指针转换为指向整型变量,向后看4个字节 printf("4 byte : 0x%x\n", *(int *)p); //将指针p转换为指向长整型,向后看8个字节 printf("8 byte : 0x%lx\n", *(long *)p); //将指针p转换为Test结构类型变量 q = (Test *)p; printf("whole bytes : 0x%x, 0x%x, %c\n", q->array[0], q->array[1], q->ch); return 0;}
运行结果为:
上面程序中的结构变量var的存储结构为:
4.void*型指针
void*指针表示一个任意类型的指针,可以指向任意类型的内存单元。
C语言中定义一个指向任意类型对象的指针:
void *p;
该指针p表示指向任意类型,但是如果需要引用该指针所指向的数据时,结汇发生编译错误。
下面的程序演示了引用一个任意类型指针所指向的数据:
#includeint main(void){ int a = 100; void *p; p = &a; printf("%d\n", *p); //引用p所指向的数据}
编译该程序,发现出错了:
指针类型的意义在于编译器可以知道从该指针所表示的地址开始,向后将多少字节当做一个整体对象来看。但是任意类型指针并不能告诉编译器该对象的大小是多少,因此编译器不知道向后看多少个字节,所以就会出现编译错误。
既然无法引用任意类型指针所指向的数据,void*类型似乎没有存在的意义了,其实不然。有时编译器不清楚用户要吧指针所指向的内容做声明用途,这时候void*类型的指针就派上用场了,编译器认为这块内存用户做什么都是合法的,因此指向这块内存的指针就是任意类型的。
最典型的一个例子就是使用malloc()函数分配一块内存后,得到这块内存的首地址。用户分配了一块内存,编译器不知道用户要做什么,因此就返回一个void*类型的指针,这样就可以躲过不必要的编译器类型检查。之后怎么使用该指针是用户的事情,与malloc()函数无关了。
下面的程序演示使用malloc()函数得到一个void*类型的指针:
#include#include int main(void){ void *p; //p是一个void*类型的指针 int *q; p = malloc(sizeof(int)); //使用malloc()函数分配4个字节 if(p == NULL) { perror("fail to malloc"); exit(1); } q = (int *)p; //要将p转换为指向整型的指针后才可以使用其指向的数据 *q = 100; printf("the value is : %d\n", *q); return 0;}
运行结果为:
C语言中对指针类型的检查十分严格,参与运算或者比较的两个指针指向的对象类型必须相同才被认为是同一类型的指针。
指针的本质是一个无符号的整数,从理论上讲,指针和一个整数的比较应该是没有问题的,但是c语言编译器不允许两者进行比较,因此在比较一个指针和一个整型数据时,首先要将整型数据转换为该指针类型,然后再进行比较。
#includeint main(void){ int *p; if(p == 1000){ //直接拿整型常量和指针进行比较 printf("equal\n"); } else { printf("not equal\n"); } if(p == (int *)1000){ //将整型转换为指针变量后比较 printf("equal\n"); } else { printf("not equal\n"); } return 0;}
编译时,编译器会警告类型不匹配:
由此,为了避免编译器的警告,需要将整型常量转换为指针。
一个典型的例子就是NULL常量,该常量用于表示空指针。其本身并不是C语言中的一个关键字,而是一个定义在stdio.h文件中的宏:
#define NULL (void *)0
空指针实际上就是一个常数0,其代表0号内存单元。在所有的系统中,0号内存单元都是不允许进行读写操作的,因此指向该内存单元的指针作为空指针使用。之所以需要将0转换为void*类型的指针,其目的是要避免编译器做无用的类型检查。