C语言中一个关于指针传递的问题李云UTStarcom通讯有限公司 E-Box Team2005-06-22摘要指针在C语言中扮演着极为重要的角色,它的存在为C语言提供了极大的灵活性,当然,不少问题也是由指针所引起的(双刃剑)。
本文通过分析一个由指针传递所引起的错误,从而使得我们更加重视指针在编程中的传递问题。
关键词C语言指针传递缩略语SignificantByte 最低有效字节LeastLSBMCI Management & Control Interface 管理控制接口Byte 最高有效字节MSB MostSignificant1 问题的提出指针因为灵活使得我们在编程时有意识的利用这一特性,从而使得我们的设计也更加的灵活,如函数指针等等。
在很多情况下,我们需要从被调用函数返回结果。
这可以通过两种方法来实现,一是通过函数的返回值,二是通过将指针作为参数传递给被调用函数。
图 1.1就是一个例子。
00001:S32 mci_module_id_from_name(S8* name, U16* module_id)00002:{00003:mci_module_t *module;00004:U16 index = 0;00005:00006:if(name == NULL || module_id == NULL)00007:return ERR_MCI_INV_PRARAM;00008:00009:for(;index <= g_mci_last_module_id; index ++)00010:{00011:module = g_mci_module_array[index];00012:00013:if(module == NULL)00014:continue;00015:00016:if(strcmp(module->name, name) == 0)00017:{00018:*module_id = index;00019:return 0;00020:}00021:}00022:00023:return ERR_MCI_MOD_NOT_EXIST;00024:}图 1.1 采用指针传递获取返回结果的示例函数在图 1.1中需要关心的是第18行,这一行将找到的MCI模块的ID通过指针传递的方法,将其返回给调用者。
图 1.2是使用图 1.1的mci_module_id_from_name函数的一个例子程序。
00026:int foo(char* name)00027:{00028:U32 module_id = 0;00029:00030:if (mci_module_id_from_name(name, &module_id) < 0)00031:return FAILURE;00032:00033:...00034:00035:return SUCCESS;00036:}图 1.2 使用mci_module_id_from_name函数的一个例子foo函数中对于mci_module_id_from_name函数的调用永远能得到正确的结果吗?答应案是:否。
如果在x86处理器上运行这一程序,则总是能得到正确的结果,但在我们熟悉的PowerPC处理器上运行这一程序则总是很难得到正确的结果(只有当module_id_from_name函数中返回的module_id恰好为0时结果才正确)。
这是为什么呢?产生这一问题的关键是:mci_module_id_from_name函数需要的是一个U16(我们可以理解为32位处理器上的unsigned short int)的指针,但foo函数在调用mci_module_id_from_name时,所给的指针是U32(我们可以理解为32位处理器上的unsigned int)。
这一程序如果在Visual C++中进行编译,则会出现编译错误(指出指针类型不匹配),为了能在Visual C++中编译通过,则需要做一个将U32*强制转换成U16*的转换(如(U16*)&module_id)。
但在我们所使用的VxWorks编译环境中,是能正常编译并且不会出现任何的告警信息的(见注)。
注:这一问题的出现是在VxWorks中采用-ansi编译选项,且在foo函数所在的文件中没有声名mci_module_id_from_name的原型的情况下出现的。
为了让编译器能检查出这一函数指错不匹配的问题可以采用-std=c9x等编译选项进行编译。
当采用-std=c9x编译选项进行编译时,如果在函数调用处,没有找到被调用函数的原型声名,则会报错。
相同的程序,为什么在x86和PowerPC处理器上却会产生截然不同的结果呢?对于这一问题,两种处理器的字节顺序问题是其根源,即x86采用的little-endian,而PowerPC 采用的big-endian。
2 little-endian和big-endianlittle-endian采用低位字节放在低地址内存,而高位字节放在高地址内存的方法。
反之,big-endian是采用高位字节放在低地址内存,低位字节放在高地址内存的方法。
以图 1.2的foo函数中的module_id变量为例(由于是局部变量,所在这一变量位于堆栈内存中),由于module_id被定义为U32,因此,它将占用4个字节的内存,假设module_id位于0x10000地址开始的内存中,则在little-endian的CPU上,其字节在内存中的存放顺序如图 2.1所示,而在big-endian的CPU上其字节在内存中的存放顺序如图 2.2所示。
U32 module_id = 00x100000x100010x100020x10003图 2.1 module_id在little-endian下的字节存放顺序U32 module_id = 00x100000x100010x100020x10003图 2.2 module_id在big-endian下的字节存放顺序思考TCP/IP协议采用的是big-endian模式,因此,在进行网络套接字(socket)编程时,我们需要用到htonl、ntohl等函数,为什么?请仔细分析在不同endian模式的两台主机之间进行通讯时,所需发送的数据在发送主机上的字序存储方式、数据在网络上的传输顺序、以及数据在接收主机上的字序存储方式。
3 问题的分析在知道little-endian和big-endian以后,对于前面所述问题的分析就比较容易了。
首先,让我们看一看在little-endian模式下图 1.2的程序为什么总能得到正确的结果,在分析问题之前,我们仍然以本文第2节的假设为前提,即foo函数中的局部变量module_id 是存放在内存地址0x10000开始的地方(占4个字节)。
由于foo是将module_id的地址传给mci_module_id_from_name函数的,因此,0x10000被传递给mci_module_id_from_name函数。
现在假设在mci_module_id_from_name中需要对module_id赋值为258(即*module_id = 258)。
这一过程在little-endian模式下如图 3.1所示。
U32 module_id = 0* module_id =0x100000x100000x100010x100020x10003foo函数作用范围mci_module_id_from_name函数作用范围U32 module_id = 0* module_id =0x100000x100000x100010x100020x10003斌值操作图 3.1 module_id在little-endian下通过指针传递的操作过程在mci_module_id_from_name函数中,由于module_id被定义为U16的指针,因此,从0x10000开始的2个字节被用来存放module_id的值。
由于little-endian的字序特征,在mci_module_id_from_name中对U16类型module_id的修改,总是能正确的返回到foo 函数中的U32的module_id。
同样的情况在big-endian下又会是怎样呢?如图 3.2所示。
U32 module_id = 0* module_id =0x100000x100000x100010x100020x10003foo函数作用范围mci_module_id_from_name函数作用范围U32 module_id = 0* module_id =0x100000x100000x100010x100020x10003斌值操作图 3.2 module_id在big-endian下通过指针传递的操作过程从图 3.2不难看出,在mci_module_id_from_name函数中我们对module_id赋值为258,但返回到foo函数以后这一值变成了16908288,这显然不是我们所希望的。
产生这一问题的根源还是因为big-endian的字序特征所致。
从以上分析来看,是不是在little-endian下foo函数在调用mci_module_id_from_name 时指针传递的类型不匹配就一定不会产生问题呢?答案是不一定,在foo这一函数中我有意识的在调用mci_module_id_from_name函数前将module_id初始化为0。
从图 3.1可以看出在mci_module_id_from_name函数中只对低两个字节进行赋值操作,而对高两个字节根本不去做任何的改变。
因此,如果module_id的高两字节的值如不为0,则调用mci_module_id_from_name函数同样不能得到正确的结果。
4 总结指针传递应当严格按照所需的指针类型进行传递,否则有可能造成程序无法正常运行。
在进行程序设计时,应尽可能的保证特定的对象类型的一致性,从而可以有效的避免指针类型不匹配这一问题。