指针指向一个存储单元,而
取消指向指针意味着读取指定单元的值。 指针本身的值就是存储单元的地址。 C语言标准未指定表示内存地址的形式。 这是非常重要的一点,因为不同的体系结构可能使用不同的寻址模型。 大多数现代体系结构使用线性地址空间或类似地址。 但是,由于地址可以是物理的也可以是虚拟的,因此即使是这个问题也没有严格说明。 一些架构完全使用非数字表示。 因此,Symbolics Lisp Machine使用形式为
(对象,偏移量)的元组作为地址进行操作。
一段时间后,在哈布雷(Habré)译本出版后,作者对该文章的文字进行了较大的修改。 在Habré上更新翻译不是一个好主意,因为某些评论会失去其含义或显得格格不入。 我不想将文本发布为新文章。 因此,我们只是在viva64.com上更新了文章的翻译,在这里我们将一切保持原样。 如果您是新读者,建议您点击上面的链接,在我们的网站上阅读最新的翻译。 |
该标准没有规定指针的表示形式,而是或多或少地规定了对它们的操作。 下面我们考虑这些操作及其在标准中定义的功能。 让我们从以下示例开始:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
如果我们以优化级别1编译此GCC代码并在Linux x86-64下运行该程序,它将打印以下内容:
0x7fff4a35b19c 0x7fff4a35b19c 0
注意,指针
p和
q指向相同的地址。 但是,表达式
p == q的结果为
false ,乍一看似乎很奇怪。 指向同一地址的两个指针不应该相等吗?
这是C标准定义如何检查两个指针是否相等的结果:
C11§6.5.9第6段
当且仅当两个指针均为零时,两个指针才相等,要么指向同一个对象(包括指向该对象的指针和该对象中的第一个子对象)或一个函数,要么指向数组的最后一个元素之后的位置,或者指向一个指针指的是数组最后一个元素之后的位置,另一个指的是在相同地址空间中紧接第一个元素之后的另一个数组的开始。 |
首先,出现了一个问题:什么是“对象
” ? 由于我们正在谈论C语言,因此很明显,这里的对象与OOP语言(如C ++)中的对象无关。 在C标准中,未完全定义此概念:
C11§3.15
对象是运行时存储区,其内容可用于表示值
注:当提到一个对象时,可以认为它具有特定的类型。 见6.3.2.1。 |
让我们做对。 16位整数变量是内存中的一组数据,可以表示16位整数值。 因此,这样的变量是一个对象。 如果两个指针之一指向给定整数的第一个字节,而第二个指针指向相同数字的第二个字节,两个指针是否相等? 语言标准化委员会当然根本不是这个意思。 但在这里应该指出,在这方面他没有明确的解释,我们被迫猜测真正的含义。
当编译器遇到问题时
让我们回到第一个例子。 指针
p从对象
a获得,指针
q从对象
b获得 。 在第二种情况下,使用地址算术,为加号和减号运算符定义如下:
C11§6.5.6第7条
与这些运算符一起使用时,指向不是数组元素的对象的指针的行为类似于指向长度为一个元素的数组开头的指针,该数组的类型与原始对象的类型相对应。 |
由于任何指向非数组对象的指针
实际上都变成了一个长度为一个元素的数组的指针,因此该标准仅针对指向数组的指针定义了地址算法-这是第8点。我们对以下部分感兴趣:
C11§6.5.6第8条
如果将整数表达式添加到指针或从指针中减去,则所得指针与原始指针的类型相同。 如果源指针指向一个数组元素并且该数组具有足够的长度,则源元素和结果元素彼此分开,以使它们的索引之间的差等于整数表达式的值。 换句话说,如果表达式P指向数组的第i个元素,则表达式(P)+ N (或其等价N +(P) )和(P)-N (其中N的值为n)分别表示(i + n)数组的第(i-n)个元素(如果存在)。 此外,如果表达式P指向数组的最后一个元素,则表达式(P)+1表示在数组最后一个元素之后的位置,如果表达式Q表示在数组最后一个元素之后的位置,则表达式(Q)-1表示最后一个元素数组。 如果源指针和结果指针均指向同一数组的元素或指向数组最后一个元素之后的位置,则溢出被排除; 否则,行为是不确定的。 如果结果指针指向数组最后一个元素之后的位置,则不能将一元*运算符应用于该数组。 |
因此,表达式
&b + 1的结果一定是地址,因此
p和
q是有效的指针。 让我提醒您,如何定义标准中
两个指针的相等性 :“
当且仅当一个指针指向数组的最后一个元素之后的位置,而另一个指针指向相同的第一个元素之后的另一个数组的开始时,两个指针才相等。地址空间” (C11§6.5.9第6条)。 这正是我们在示例中观察到的。 指针q指的是对象b之后的位置,紧接对象a的是指针p所指向的位置。 那么,GCC中是否有bug? 这个矛盾在2014年被描述为
bug#61502 ,但是GCC开发人员并不认为它是bug,因此不会对其进行修复。
Linux程序员在2016年遇到了类似的问题。 考虑以下代码:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
符号
_start和
_end指定存储区域的边界。 由于它们已传输到外部文件,因此编译器不知道数组在内存中的实际位置。 因此,他在这里应格外小心,并假设它们在地址空间中相互跟随。 但是,GCC将循环条件编译为始终为真,从而使循环无限。
在LKML上的这篇
文章中对此问题进行了描述-此处使用了类似的代码片段。 似乎在这种情况下,GCC的作者仍然考虑了注释并改变了编译器的行为。 至少我在Linux x86_64下的GCC版本7.3.1中无法重现此错误。
解决方案-错误报告260中?
我们的案例可能会澄清错误报告
#260 。 它更多地是关于不确定的值,但是您可以在其中找到委员会的奇怪评论:
编译器实现也可以区分从不同对象获得的指针,即使这些指针具有相同的位集合。如果我们从字面上看这句话,那么逻辑上表达式
p == q的结果是“假”,这是合乎逻辑的,因为
p和
q是从不以任何方式连接的不同对象获得的。 看来我们越来越接近真相了-还是不? 到目前为止,我们已经处理了相等运算符,但是关系运算符呢?
最后的线索是关系运算符?
在指针比较的上下文中,
< ,
<= ,
>和
> =关系运算符的定义包含一种奇怪的想法:
C11§6.5.8第5段
比较两个指针的结果取决于所指示对象在地址空间中的相对位置。 如果两个指向对象类型的指针引用同一对象,或者两个指针都指向同一数组的最后一个元素之后的位置,则此类指针相等。 如果指示的对象是同一复合对象的成员,则指向稍后声明的结构的成员的指针比指向早先声明的成员的指针更多,指向具有较高索引的数组元素的指针大于指向具有较低索引的同一数组元素的指针。 指向相同关联成员的所有指针都是相等的。 如果表达式P指向数组的一个元素,并且表达式Q表示同一数组的最后一个元素,则指针表达式Q +1的值大于表达式P的值。 在所有其他情况下,行为均未定义。 |
根据该定义,仅当从
同一对象获得指针时才确定比较指针的结果。 我们用两个例子来说明这一点。
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
此处,指针
p和
q指的是两个未互连的不同对象。 因此,它们的比较结果未定义。 但是在以下示例中:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
指针
p和
q指向同一对象,因此是相互连接的。 因此,可以将它们进行比较-除非
malloc返回空值。
总结
C11标准没有充分描述指针比较。 我们遇到的最棘手的问题是第6节第6.5.9节,明确允许比较两个引用两个不同数组的指针。 这与来自错误报告260的评论相矛盾。 但是,我们在这里谈论的是不确定的含义,我不想仅凭此注释来构建我的推理,而要在另一种情况下对其进行解释。 比较指针时,关系运算符的定义与相等运算符的定义稍有不同-即,仅当两个指针均来自
同一对象时才定义关系运算符。
如果我们忽略标准文本,并询问是否有可能比较从两个不同对象获得的两个指针,那么在任何情况下答案都将很可能是“否”。 本文开头的示例演示了一个理论问题。 由于变量
a和
b具有自动存储时间,因此我们关于它们在内存中的放置的假设将是不可靠的。 在某些情况下,我们可以猜测,但是很明显,不能安全地移植此类代码,并且仅通过编译,运行或反汇编代码就可以了解程序的含义,这与任何严肃的编程范例相矛盾。
但是,总的来说,我对C11标准中的措词不满意,并且由于已经有人遇到了这个问题,所以问题仍然存在:为什么不更清晰地制定规则?
加法
指向数组最后一个元素之后的位置的指针
至于将指针指向数组最后一个元素之后的位置进行比较和寻址的规则,通常可以找到它的例外。 假定该标准不允许比较从
同一数组获得的两个指针,即使其中至少有一个指针指向数组末尾之外的位置。 然后,以下代码将不起作用:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
使用循环,我们遍历整个
x数组,该数组由64个元素组成,即 循环体应准确执行64次。 但是实际上,该条件被检查了65次-比数组中元素的数量多一倍。 在前64次迭代中,指针
i始终指向数组
x的内部,而表达式
&x [num]始终表示数组最后一个元素之后的位置。 在第65次迭代中,指针
i也将指向数组
x末尾以外的位置,因此循环条件变为false。 这是绕过整个数组的便捷方法,并且在比较此类指针时依赖行为不确定性规则的例外。 注意,该标准仅描述比较指针时的行为。 取消引用是一个单独的问题。
是否可以更改我们的示例,以便没有单个指针指向数组
x的最后一个元素之后的位置? 可能,但是会更加困难。 我们将不得不更改循环条件并禁止在最后一次迭代中变量
i的增加。
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
这段代码充满了技术上的细微差别,使人大惊小怪,从而分散了主要任务的注意力。 此外,循环的主体中还出现了一个分支。 因此,我发现在比较数组最后一个元素之后的位置指针时,该标准允许例外是合理的。
PVS-Studio团队说明在开发PVS-Studio代码分析器时,有时我们必须处理一些细微问题,以使诊断更加准确或向客户提供详细咨询。 这篇文章对我们来说似乎很有趣,因为它涉及到我们自己没有完全感到自信的问题。 因此,我们要求作者发表她的翻译。 我们希望更多的C和C ++程序员能够认识她,并且了解它并不那么简单,并且当分析仪突然显示一条奇怪的消息时,您不要着急将其视为假阳性:)。该文章最初以英文发表在stefansf.de。 翻译经作者许可出版。