使用指针7年的C程序员问了一个问题

今天小Y发来消息,说自己被一个代码弄的焦头烂额。大概是这个样子


unsigned char phSessionHandle[20] = {0};
SDF_GenerateRandom(phSessionHandle,uiLength,ucRandom);//1
SDF_GenerateRandom(*(void**)phSessionHandle,uiLength,ucRandom);//2
SDF_GenerateRandom(*(void**)&phSessionHandle,uiLength,ucRandom);//3

函数SDF_GenerateRandom的原型是:
unsigned int SDF_GenerateRandom(void* hSessionHandle, unsigned int uiLength, unsigned char ucRandom)

就是这么简单的一段代码,小Y说:“1,3方式的调用我得到了期望的结果。但是2方式错了”。别忘了,小Y可是一个有7年C语言编程经验的程序员。
这种传指针的把戏怎么可能难住TA。我们来看看。[read]
1符合大家的使用习惯,一个数组unsigned char [20]类型传入到函数,自动退化为一个unsigned char*指针类型,最终变成void*类型。不会出现什么问题。

2是一个 unsigned char[20] 类型被强转为 void** 类型。最终我们用*解引用,作者目的是想让它强制为一个 void* 类型,这样就和函数原型声明一致了(难道是因为静态检查让作者做出的改动?奇妙的是,调用2并没有返回期望的结果。

3是一个 unsigned char[20] 类型被取地址后变成一个 unsigned char* [20] 类型。很明显,作者在发现2调用出现问题后,觉得可能是因为自己没有取地址的原因,特意加上&,让(&phSessionHandle)看起来是一个二级指针。这样名正言顺的变成 void **,最后解引用为void*。小Y顺利运行成功,感觉“对”了。

有过这次经验,小Y每次碰到数组就这么用。在一次使用memcpy的代码中,TA这样写。


memcpy(&phSessionHandle,&source,sizeof(source))

小Y的领导发火了,你干啥在数组前加一个取地址符号&啊。他可是一个“二级”指针啊。你应该这样用。
memcpy(phSessionHandle,&source,sizeof(source))

对于一个专注业务逻辑的程序员,小Y即使当了这么多年的码农,仍然很讨厌指针。
第一:同样是在数组上使用&,TA第一次用“对”了,第二次却不被领导认可。
第二:到底数组和指针在编译器内部如何被识别,又发生了哪些类型转换。

为了解答小Y的疑问。我们先来看看C11标准里怎么解释&运算符号。

查阅C11标准文档:

6.5.3 The unary & operator yields the address of its operand. If the operand has type ‘‘type’’,
the result has type ‘‘pointer to type’’. If the operand is the result of a unary * operator,
neither that operator nor the & operator is evaluated and the result is as if both were
omitted, except that the constraints on the operators still apply and the result is not an
lvalue. Similarly, if the operand is the result of a [] operator, neither the & operator nor
the unary * that is implied by the [] is evaluated and the result is as if the & operator
were removed and the [] operator were changed to a + operator. Otherwise, the result is
a pointer to the object or function designated by its operand.

翻译如下:

&单目操作符得到它操作数的一个地址。
如果操作数的类型是‘type’,那么结果是一个指向type的指针。
如果操作数是单目运算符*的结果。那么*和&都会被编译器忽略。除非操作符上叠加其它操作而且结果不是一个左值。
同样,如果操作数是[]运算符的结果。那么&和[]所隐含的*操作都不会被执行。结果和去掉&运算并且把[]变成+操作一样。(意思是可能存在[]偏移的情况)
其它情况,&操作符返回的结果是指向对象的指针或者操作数所指定的函数。

再来看C99的阐述,
rationale for the C99 standard C99标准补充提到取地址符号时说明

6.5.3.2 Address and indirection operators
Some implementations have not allowed the & operator to be applied to an array or a function.
(The construct was permitted in early versions of C, then later made optional.) The C89
Language Committee endorsed the construct since it is unambiguous, and since data abstraction is
enhanced by allowing the important & operator to apply uniformly to any addressable entity.

翻译如下:

一些(编译器)实现不允许&操作符用在数组和函数上。(这种搭配结构在早期的C版本中是允许的,后来变成一个可选项)。C89语言委员会认为这种搭配含义并不模糊,表示支持。因此,基于数据抽象概念被加强的原因,委员会允许&这个重要操作符用在任何可以寻址的实体对象上。

再回到我们的代码

SDF_GenerateRandom(*(void**)phSessionHandle,uiLength,ucRandom);//2
SDF_GenerateRandom(*(void**)&phSessionHandle,uiLength,ucRandom);//3

2的类型转换过程是:(2a) unsigned char [20] (编译器维护的一个类型)-> (2b) void ** -> (2c) void*
3的类型转换过程是:(3a) unsigned char [20] (编译器维护的一个类型)-> (3b) unsigned char* [20] (编译器维护的一个类型) -> (3c) void** -> (3d) void *

通过上面标准的解释我们最多也就知道在对3a->3b的过程中并没有发生实质性的类型变化。只是编译器维护的类型发生了变化,本质上[]隐含了一个一级指针在编译器看来做了点改动(为什么编译器要这样做?我们后面看)。

而实际上2得到了错误的结果,因为2a到2b这一个过程,将一个*强制变成了**的二级指针。这个二级指针是非法的。再去解引用(dereference *)是一个未定义的行为。对此标准也有这样的解释。

The unary * operator denotes indirection. If the operand points to a function, the result is
a function designator; if it points to an object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an
invalid value has been assigned to the pointer, the behavior of the unary * operator is
undefined.102)

而为什么3的运行结果是对的。我能想到的是小Y的编译环境正好对这种未定义行为解释到一个合法的地址返回了合法值,并没有发生段错误罢了。3a到3b的过程中并未发生实质的类型变化。程序不可能记住了这个非法的二级指针,在解引用后指回。

于是我在Mac上写了一段类似的程序。dereference以后指针并没按照期望变回原来的地址,每次运行都是一个变化的地址,并导致了段错误。说明这种代码是完全不可移植。我们在平时要完全禁止。

值得一说的是,编译器在报错是经常会带出 type*[] 和 type* 不匹配的警告。
所以我们可以再上升一下:看看Edwin Brady 在《Type-driven Development with Idris》怎么描述不同“物体”怎么观察到程序的类型的

Types serve several important roles:
For a machine, types describe how bit patterns in memory are to be interpreted.
For a compiler or interpreter, types help ensure that bit patterns are interpreted consistently when a program runs.
For a programmer, types help name and organize concepts, aiding documentation and supporting interactive editing environments.

翻译:

类型扮演几个不同的重要角色:
从一台机器看来(0和1),类型用来描述内存中0,1排列组合而成比特流怎么被解释。(我给翻译多加了点料)
对编译器和解释器而言(它们看到int [],char[]),类型用来保证0,1排列组合而成的比特流在程序运行期间有一致的解释结果。(比如 char a + int b 到底得到一个char还是int,是否允许这种操作。说大白话就是让解释器可以做很多中间转换,编译检查等)
对程序员而言(我们看到int,char),类型帮助我们命名和组织概念,书写文档说明支持交互式编辑环境(有具体类型来承载变量,向代码阅读者解释说明,支持IDE等给出提示检查)

[/read]

All posts

Other pages