指针优点之一是能灵活读写内存,实现这一功能主要依靠灵活多变的类型转换,重复这里类型指指针所指向的内存的类型。通过改变编译器对“被指向内存”的解读方式实现对内存中数据的灵活转换,这是指针的精髓应用之一。
指针本身存储的值被编译器当作一个地址,这个值的类型,即指针所指内存块的类型尤为重要,”a pointer make no sense if it has no type definition for the pointed memory segment”。比如void *p=malloc(100); 这个定义只能说明p是一个指针,它的值是一段内存的地址,但由于类型是void,编译器不知道怎样解析p所指内存中的数值,为什么?假设p指向地址0x1000,在0x1000这个内存位置按字节依次存放数据,如下:
此时如果试图用*p去取指针p所指内存的值,问题就出现了:p=0x1000,这个0x1000代表内存起始地址,那么*p=0x11?还是2字节的0x1122?或4字节的0x11223344?如果取0x11223344,这个值怎么解释?有符号or无符号?整型or浮点型?很明显还缺少一个因素。
所以完整指针定义必须包含另一个限定,用来解决取多长,取出的数据代表什么或者说编译器怎么解释它的问题。这就是指针类型定义(简化称呼)所起的作用,也就是指针定义时*号之前的类型。比如把之前定义改为char *p,就可以确定*p等于0x11(char取1字节长度),如果定义为unsigned short *p,会得到*p = 0x1122。
计算机内存中只有二进制数,没有整型、浮点、字符等的区别,二进制数的具体含义取决于编译器根据内存类型的翻译解读,同一段内存里的二进制数用不同的类型“解释”,意义千差万别,据此可引出指针的一个重要技巧:指针指向内存的强制类型转换。下面是有关的两道C考题:
题1:完成一个C函数,利用指针的某功能特性检测CPU的Endian属性,Big_endian返回0;Little_endian返回1。答案:
int checkCPU( )
{
int a =0x1234abcd;
char *b;
b=(char *)&a; //强制转换
if(0x12 == (*b)) return 0; //Big-endian
else if(0xcd == (*b)) return 1; //Little-endian
}
分析:Endian是硬件概念。Little-endian的CPU内存中多字节数从低字节到高字节存放,而Big-endian是从高字节到低字节。以上代码利用指针类型变换,改变对指针所指目标内存的“解读”方式,从而判断数据在内存中的实际存放顺序。
题2:用一句话让程序跳转到绝对地址0x1000处去执行。
答案:*((void (*)( ))0x1000 ) ( );
首先把0x1000强制转换成函数指针,即:(void (*)())0x1000,然后调用此函数: *((void (*)())0x1000)()。用typedef方式可能更容易看懂:
typedef void(*)() FuncPtr;
*((FuncPtr)0x1000)();
一个普通地址值硬是变成函数入口地址,这就是指针强制类型转换的威力,前提是完全掌握各种变量的存储形式,比如上例事先必须确认地址0x1000中存放着合法的程序指令,否则就会”illegal instruction exception”了。
这两个例子再次呼应说明:访问指针所指内存区,指针指向的那块内存的类型决定编译器把那块内存里的内容当做什么来操作。凡事有利必有弊,类型转换让指针可以千变万化,灵活使用,同时也带来了很多问题。
指针指向内存的类型转换与普通变量强制类型转换的区别:
指针指向内存的强制类型转换,并不做内存拷贝,只是对内存数值重新解读;而变量强制类型转换会构建一个不同类型的新变量,把原来的数“加工转换”后赋给新变量。前者是用新类型解读内存中的二进制数,后者则是用新类型转换旧类型。运行下面代码,从中对比可以看出两种转换的不同:
void main()
{
float f = 1.25;
printf(“%d\n”, *(int *)&f); //重新解读f所在的内存,指针类型转换
printf(%d\n”, (int)f); //转换f变量本身,变量类型转换
}
指针变换的安全性
指针变换并不能随心所欲,例如:
unsigned char ca=’a’;
int *pa;
pa=(int*)&ca;
*pa=1298;
指针pa的定义int *pa代表pa指向int型,所指内存占4字节,但之后pa又被指向unsigned char型的ca,而ca只有一字节。因此最后一句会发生错误,它不但改写ca所在的一字节内存,还将改变相邻的另外三个字节。也许这三字节里原本存储着重要数据,意外改变将带来崩溃性错误。这就是典型的错误指针类型变换导致内存越界访问。
因此类似ptr1=(TYPE*)ptr2这种指针强制类型转换,如果ptr2指向的内存类型长度大于ptr1所指的类型,那么用指针ptr1访问ptr2所指的内存就是安全的,反之会越界。
指针类型不匹配导致错误的隐式指针类型变换
在赋值以及函数参数传递时如果对应的指针类型不匹配,且编译器设置不严格就会自动隐式的变换指针类型,这往往会导致读写错误。例如:
void main()
{
int i;
char ca;
for(i = 0; i < 5; i++)
{
scanf("%d", &ca);
printf("%d", i);
}
}
程序用于从标准输入中读五个数并向标准输出写入0 1 2 3 4。但某些环境下,会输出类似0 0 0 0 0 1 2 3 4等很奇怪的数值。
scanf(%d)需要一个指向整型的指针,而&ca指向char型,scanf()并不知道其参数类型是否匹配,它仍然把&ca看成指向整型的指针并试图向其中写入一个整数。由于整数比字符占用更多内存,这就影响到ca的邻近。ca位于栈中,邻近很可能是紧挨着的局部变量i。这样每次c读入一个值,i就被部分覆盖,于是导致输出奇怪的数。所以函数传递指针参数时,实参和形参类型必须匹配,以避免隐含的错误指针类型变换。
总结:指针指向的内存类型是指针最重要的属性,自如的运用指针类型变换,才能初步体会到C相对其他语言的优势。