指针比较在 C 中如何工作?可以比较不指向同一个数组的指针吗?
How does pointer comparison work in C? Is it ok to compare pointers that don't point to the same array?
在 K&R(C 程序设计语言第 2 版)第 5 章中,我阅读了以下内容:
First, pointers may be compared under certain circumstances.
If p
and q
point to members of the same array, then relations like ==
, !=
, <
, >=
, etc. work properly.
这似乎暗示只能比较指向同一数组的指针。
但是当我尝试这段代码时
char t = 't';
char *pt = &t;
char x = 'x';
char *px = &x;
printf("%d\n", pt > px);
1
打印到屏幕上。
首先,我认为我会得到未定义或某种类型或错误,因为 pt
和 px
没有指向同一个数组(至少在我的理解中)。
也是pt > px
,因为两个指针都是指向存储在栈中的变量,栈向下增长,所以t
的内存地址大于x
?这就是为什么 pt > px
是真的?
当引入malloc时,我变得更加困惑。同样在第8.7章的K&R中写了以下内容:
There is still one assumption, however, that pointers to different blocks returned by sbrk
can be meaningfully compared. This is not guaranteed by the standard which permits pointer comparisons only within an array. Thus this version of malloc
is portable only among machines for which the general pointer comparison is meaningful.
将指向堆上分配的 space 的指针与指向堆栈变量的指针进行比较,我没有遇到任何问题。
例如,下面的代码运行良好,打印了 1
:
char t = 't';
char *pt = &t;
char *px = malloc(10);
strcpy(px, pt);
printf("%d\n", pt > px);
根据我对编译器的实验,我被引导认为任何指针都可以与任何其他指针进行比较,无论它们分别指向何处。此外,我认为两个指针之间的指针算法很好,无论它们分别指向哪里,因为算法只是使用指针存储的内存地址。
不过,我对在 K&R 中阅读的内容感到困惑。
我问的原因是因为我的教授。实际上把它作为考试题。他给出了以下代码:
struct A {
char *p0;
char *p1;
};
int main(int argc, char **argv) {
char a = 0;
char *b = "W";
char c[] = [ 'L', 'O', 'L', 0 ];
struct A p[3];
p[0].p0 = &a;
p[1].p0 = b;
p[2].p0 = c;
for(int i = 0; i < 3; i++) {
p[i].p1 = malloc(10);
strcpy(p[i].p1, p[i].p0);
}
}
What do these evaluate to:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
答案是0
、1
和0
。
(我的教授确实在考试中包含免责声明,即这些问题是针对 Ubuntu Linux 16.04,64 位版本编程环境的)
(编者注:如果 SO 允许更多标签,那么最后一部分将保证 x86-64, linux, and maybe assembly。如果问题的重点 /class 是特别低级的 OS 实现细节, 而不是便携式 C.)
根据 C11 standard,关系运算符 <
、<=
、>
和 >=
只能用于指向元素的指针相同的数组或结构对象。这在第 6.5.8p5 节中有详细说明:
When two pointers are compared, the result depends on the
relative locations in the address space of the objects pointed to.
If two pointers to object types both point to the same object, or
both point one past the last element of the same array
object, they compare equal. If the objects pointed to are
members of the same aggregate object,pointers to structure
members declared later compare greater than pointers to
members declared earlier in the structure, and pointers to
array elements with larger subscript values compare greater than
pointers to elements of the same array with lower subscript values.
All pointers to members of the same union object compare
equal. If the expression P points to an element of an array
object and the expression Q points to the last element of the
same array object, the pointer expression Q+1 compares greater than P.
In all other cases, the behavior is undefined.
请注意,任何不满足此要求的比较都会调用 undefined behavior,这意味着(除其他外)您不能依赖于可重复的结果。
在您的特定情况下,对于两个局部变量的地址之间以及局部地址和动态地址之间的比较,该操作似乎 "work",但是结果可能会通过以下方式改变对您的代码进行看似无关的更改,甚至使用不同的优化设置编译相同的代码。对于未定义的行为,仅仅因为代码 可能 崩溃或产生错误并不意味着它 会 .
例如,在 8086 实模式下 运行ning 的 x86 处理器具有分段内存模型,使用 16 位段和 16 位偏移来构建 20 位地址。所以在这种情况下,地址不会完全转换为整数。
但是等号运算符==
和!=
没有这个限制。它们可以用在任何两个指向兼容类型的指针或 NULL 指针之间。因此,在您的两个示例中使用 ==
或 !=
将生成有效的 C 代码。
然而,即使使用 ==
和 !=
,您也可能会得到一些意想不到但仍然定义明确的结果。有关详细信息,请参阅 。
关于你的教授给出的试题,它做出了一些有缺陷的假设:
- 存在平面内存模型,其中地址和整数值之间存在一对一的对应关系。
- 转换后的指针值适合整数类型。
- 在执行比较时,该实现只是将指针视为整数,而没有利用未定义行为所提供的自由。
- 使用了堆栈,局部变量存储在那里。
- 堆用于从中提取分配的内存。
- 堆栈(因此局部变量)出现在比堆(因此分配的对象)更高的地址。
- 字符串常量出现在比堆低的地址。
如果您 运行 使用不满足这些假设的编译器在体系结构 and/or 上编写此代码,那么您可能会得到截然不同的结果。
此外,这两个示例在调用 strcpy
时也表现出未定义的行为,因为右操作数(在某些情况下)指向单个字符而不是空终止字符串,导致函数读取超过给定变量的边界。
比较指向相同类型的两个不同数组的指针的主要问题是数组本身不需要放置在特定的相对位置——一个可能在另一个之前和之后结束。
First of all, I thought I would get undefined or some type or error, because pt an px aren't pointing to the same array (at least in my understanding).
不,结果取决于实施和其他不可预测的因素。
Also is pt>px because both pointers are pointing to variables stored on the stack, and the stack grows down, so the memory address of t is greater than that of x? Which is why pt>px is true?
There isn't necessarily a stack。当它存在时,它不需要向下生长。它可以长大。它可能以某种奇怪的方式不连续。
Moreover, I think pointer arithmetic between two pointers is fine, no matter where they individually point because the arithmetic is just using the memory addresses the pointers store.
让我们看看第 85 页的 C specification,§6.5.8,其中讨论了关系运算符(即您正在使用的比较运算符)。请注意,这不适用于直接 !=
或 ==
比较。
When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. ... If the objects pointed to are members of the same aggregate object, ... pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values.
In all other cases, the behavior is undefined.
最后一句话很重要。虽然我减少了一些不相关的案例以保存 space,但有一个案例对我们很重要:两个数组,不是同一个 struct/aggregate 对象 1 的一部分,并且我们正在比较指向这两个数组的指针。这是undefined behavior。
虽然你的编译器只是插入了某种 CMP(比较)机器指令,它在数字上比较指针,你在这里很幸运,但 UB 是一个非常危险的野兽。从字面上看,任何事情都有可能发生——您的编译器可以优化整个函数,包括可见的副作用。它可以产生鼻恶魔。
1可以比较指向属于同一结构的两个不同数组的指针,因为这属于两个数组属于同一聚合对象的子句 (结构)。
指针只是整数,就像计算机中的其他所有东西一样。您绝对可以将它们与 <
和 >
进行比较,并在不导致程序崩溃的情况下产生结果。也就是说,该标准不保证这些结果在数组比较之外具有任何含义。
在您的堆栈分配变量示例中,编译器可以自由地将这些变量分配到寄存器或堆栈内存地址,并且可以选择任何顺序。因此,诸如 <
和 >
之类的比较在编译器或架构之间不会保持一致。但是,==
和 !=
并没有那么受限,比较指针 相等性 是有效且有用的操作。
多么挑衅的问题!
即使粗略地浏览一下这个话题中的回复和评论,也会发现 情绪化 您看似简单直接的查询结果如何是。
这应该不足为奇。
毫无疑问,对概念的误解 of pointers 表示一个主要的 cause of一般编程严重失败。
对这一现实的认识在专门设计用于解决的语言无处不在,最好是 避免 挑战指针引入。 想想 C++ 和 C 的其他派生词,Java 及其关系,Python 和其他脚本——仅仅作为更突出和流行的脚本,并且或多或少地按处理的严重程度排序有问题。
发展对基本原则的更深入理解,因此必须相关每个渴望编程方面的卓越——尤其是在系统级别。
我想这正是你老师要演示的意思。
并且 C 的性质使其成为这种探索的便捷工具。不如汇编清晰——尽管可能更容易理解——但仍然比基于执行环境更深层次抽象的语言明确得多。
旨在促进 确定性 将程序员的意图翻译成机器可以理解的指令,C 是 系统级语言。虽然被归类为 high-level,但它确实属于“中等”类别;但是由于 none 这样的存在,所以“系统”名称就足够了。
此特性在很大程度上使其成为 设备驱动程序的首选语言 、操作系统代码,以及嵌入式 实现。此外,在 最佳效率 至关重要的应用程序中,这是一个当之无愧的备选方案;这意味着生存和灭绝之间的差异,因此是 必需品 而不是奢侈品。在这种情况下,便携性 的便利性就失去了所有吸引力,而选择 最小公分母 的 lack-lustre 性能就变成了难以想象的有害选项。
是什么让 C —— 以及它的一些衍生产品 —— 非常特别,因为它 允许 它的用户 完成控制--当那是他们想要的--没有强加 相关的 责任 当他们不这样做时。尽管如此,它提供的绝不会超过 最薄的绝缘层 来自 machine,因此正确使用要求要求理解指针的概念.
从本质上讲,您问题的答案非常简单且令人满意 - 证实了您的怀疑。 提供了,然而,人们将必要的重要性附加到每个概念声明:
- 检查、比较和操作指针的行为总是并且必然有效,而从结果中得出的结论取决于所包含值的有效性,因此需要不是是。
前者既总是安全又潜在 proper,而后者只能在 proper 建立为安全。令人惊讶的是 -- 对某些人来说 -- 所以确定后者的有效性 取决于 和 要求前者。
当然,部分混淆是由于指针原理中固有的递归效应以及区分内容和地址所带来的挑战。
你已经正确地推测,
I'm being led to think that any pointer can be compared with any other pointer, regardless of where they individually point. Moreover, I think pointer arithmetic between two pointers is fine, no matter where they individually point because the arithmetic is just using the memory addresses the pointers store.
一些贡献者已经确认:指针只是数字。 有时更接近 复杂 数字,但仍然不超过数字。
这里收到的这种争论所引起的有趣的尖酸刻薄更多地揭示了人性而不是编程,但仍然值得一提和详细说明。也许我们稍后会这样做...
正如一条评论开始暗示的那样;所有这些困惑和惊愕都源于需要区分什么是 有效 什么是 安全,但这是过于简单化了。我们还要分清什么是实用,什么是可靠,什么是实用,什么是可能正确,更进一步:什么是正确的在特定情况下,什么是更一般意义上的正确[=578] =].更何况; 合规和礼仪之间的区别。
为此,我们首先需要欣赏 指针 是.
- 您已经表现出对这个概念的坚定把握,并且像其他一些人一样可能会发现这些插图过于简单化,但这里明显的混乱程度需要如此简单的澄清。
正如一些人所指出的:术语 pointer 只是 index 的一个特殊名称,因此无非是任何其他 number。
这应该已经是 self-evident 考虑到所有现代主流计算机都是 二进制机器 必须 工作独家 数字。量子计算 可能 改变这一点,但这不太可能,而且它还没有成熟。
从技术上讲,正如您所指出的,指针更准确地址;一个明显的洞察力自然而然地引入了将它们与房屋或街道上的地块的“地址”相关联的有益类比。
在平坦内存模型中:整个系统内存组织在一个单一的线性序列中:所有城里的房子都在同一条路上,每栋房子都由编号唯一标识。非常简单。
在segmented方案中:在编号房屋之上引入编号道路的分层组织,以便复合需要地址。
- 一些实现仍然更加复杂,不同“道路”的整体需要 而不是 求和到一个连续的序列,但是 none 这改变了关于潜在的。
- 我们必须能够将每个这样的层级结构 link 分解回扁平组织。组织越复杂,我们就必须越过越多的圈子才能做到这一点,但这 必须 是可能的。实际上,这也适用于 x86 上的“实模式”。
- 否则 link 到位置的映射将不会是 bijective,因为可靠的执行——在系统级别——要求它 必须。
- 多个地址必须不映射到单个内存位置,并且
- 单一地址必须永远不会映射到多个内存位置。
给我们带来 进一步的转折 将难题变成如此复杂得令人着迷 纠结。上面,为了简单明了起见,建议指针 是 地址是权宜之计。当然,这是不正确的。指针是不是地址;指针是对地址的引用,它包含地址。就像信封上提到的房子一样。考虑这一点可能会让您瞥见概念中包含的递归建议的含义。仍然;我们只有这么多的话,并且谈论 地址引用地址 等等,很快就会让大多数大脑陷入 无效 op-code 异常 .在大多数情况下,意图很容易从上下文中获得,所以让我们 return 走上街头。
我们这个想象中的城市中的邮政工作人员与我们在“真实”世界中找到的工作人员非常相似。当您交谈或询问[=时,没有人会中风579=] 关于一个 无效的 地址,但是当你要求他们 采取行动时,每个最后一个人都会犹豫 关于那个信息。
假设我们单一的街道上只有 20 所房子。进一步假装某个误入歧途或阅读障碍的灵魂将一封非常重要的信寄给了 71 号。现在,我们可以问我们的承运人 Frank,是否有这样的地址,他会简单而平静地报告:没有。我们甚至可以期望他估计如果 确实 存在,这个位置会在街道外多远:大约是终点的 2.5 倍。 None 这会让他生气。 但是,如果我们要他送这封信,或者 从那个地方捡到一件东西,他很可能会很坦诚地表达自己的不快,拒绝遵守。
指针是只是地址,地址是只是个数。
验证以下输出:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
在任意多的指针上调用它,无论有效与否。请 do post 如果它在您的平台上失败,或者您的 (contemporary) 编译器抱怨。
现在,因为指针是简单的数字,比较它们不可避免地有效。从某种意义上说,这正是你的老师所展示的。以下所有陈述都是完全有效的 -- 并且正确! -- C,并且在编译时 将 运行 而不会遇到问题 ,即使两个指针都不需要初始化,因此它们包含的值可能是 未定义:
- 我们只是为了清晰度和打印[=]而
result
明确计算578=] 它 强制 编译器计算否则将是冗余的死代码。
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
当然,当 a 或 b 未定义时,程序是 ill-formed(阅读:未正确初始化)在测试点,但这与我们讨论的这一部分完全 无关 。这些片段以及以下陈述 保证 -- 由“标准”--完美地编译和运行,尽管IN-涉及的任何指针的有效性。
仅当无效指针被解引用时才会出现问题。当我们要求 Frank 在无效的 non-existent 地址取货或送货时。
给定任意指针:
int *p;
虽然这条语句必须编译并且运行:
printf(“%p”, p);
...必须这样做:
size_t foo( int *p ) { return (size_t)p; }
...下面这两个,形成鲜明对比的是,仍然可以轻松编译,但是执行失败 unless 指针 是 有效的 -- 我们在这里仅表示它 引用了当前应用程序已被授予访问权限的地址 :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
变化有多微妙?区别在于指针的值——是地址和内容的值之间的区别:那个数字。直到指针解引用才出现问题;直到尝试访问它 link 的地址。试图在路段之外递送或提取包裹...
推而广之,同样的原理必然适用于更复杂的例子,包括前面提到的需要到建立必要的有效性:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
关系比较和算术提供了相同的效用来测试等价性,并且在原则上是等价的。 然而,这样计算的结果会signify , 完全是另一回事 - 正是您所引用的引文所解决的问题。
在 C 中,数组是一个连续的缓冲区,一个不间断的线性内存位置系列。比较和算术应用于在这样一个 singular 系列中引用位置的指针是自然的,并且对于彼此之间的关系来说显然是有意义的,并且对于这个'数组'(由基数简单标识)。这同样适用于通过 malloc
或 sbrk
分配的每个块。 因为这些关系是隐含的,编译器能够在它们之间建立有效关系,因此可以有信心计算将提供预期的答案。
对引用 distinct bl 的指针执行类似的操作cks 或数组不提供任何此类 inherent 和 apparent 实用程序。更重要的是,因为在某一时刻存在的任何关系都可能因随后的重新分配而无效,其中这种关系极有可能发生变化,甚至被逆转。在这种情况下,编译器无法获得必要的信息来建立它对先前情况的信心。
你,但是,作为程序员,可能有这样的知识!在某些情况下,我们不得不利用这一点。
存在ARE,因此,EVEN THIS完全VALID 并且完美 PROPER.
事实上,正是 malloc
本身在尝试合并回收块时必须在内部做的事情- 在绝大多数架构上。操作系统分配器也是如此,比如sbrk
后面那个;如果更明显,经常,在更多不同的实体上,更多critically——并且在 malloc
可能不存在的平台上也相关。 其中有多少不是用 C 语言编写的?
一项行动的有效性、安全性和成功性不可避免地取决于其前提和应用的洞察力水平。
在您提供的引述中,Kernighan 和 Ritchie 正在解决一个密切相关但 none 又是独立的问题。它们是定义语言的限制,并解释了如何利用编译器的功能至少通过检测潜在的错误结构来保护您。他们描述了该机制能够 -- 设计-- 的长度,以帮助您完成编程任务。 编译器是你的仆人,你是主人。然而,聪明的主人是非常熟悉他的各种仆人的能力。
在此上下文中,undefined behaviour 用于表示潜在的危险和伤害的可能性;并不是暗示迫在眉睫的、不可逆转的厄运,或者我们所知道的世界末日。它只是意味着我们——“意味着编译器”——不能对这个东西可能是什么做出任何推测,或者代表 出于这个原因,我们选择洗手。 对于因使用或mis-use此设施可能导致的任何意外事故,我们概不负责。
实际上,它只是说:“除此之外,cowboy:你只能靠自己了……”
您的教授正在寻求向您展示更细微的差别。
注意他们非常小心他们在制作他们的榜样时所采取的措施;以及脆它仍然如何。通过取a
的地址,在
p[0].p0 = &a;
编译器被迫为变量分配实际存储空间,而不是将其放在寄存器中。它是一个自动变量,但是,程序员 no 控制分配的 where,因此无法对什么会发生任何有效的推测跟着它。这就是为什么 a
必须 设置为零以使代码按预期工作。
仅仅改变这一行:
char a = 0;
对此:
char a = 1; // or ANY other value than 0
导致程序的行为变为未定义。至少,第一个答案现在是 1;但问题要险恶得多。
现在代码是自找麻烦
虽然仍然完全有效甚至符合标准,但现在ill-formed 并且虽然肯定可以编译,但可能会因各种原因而无法执行。目前有 多个 问题 -- none 其中 编译器是能够到认识.
strcpy
将从 a
的地址开始,并在此之后继续消费——并传输——一个字节接一个字节,直到遇到空值。
p1
指针已初始化为恰好 10 字节的块。
如果 a
恰好放在块的末尾并且进程无法访问后面的内容,那么下一次读取 -- p0[1] -- 将会引发段错误。这种情况在 x86 架构上不太可能,但有可能。
如果a
地址之外的区域是可访问的,不会出现读取错误,但程序仍然没有幸免于难。
如果零字节碰巧出现在从a
地址开始的十个字节中,它 可能 仍然存在,因为那时 strcpy
将停止并且至少我们不会遭受写违规。
如果不是读取错误,但是没有零字节出现在这个10的跨度内, strcpy
将继续并尝试 写入 超出 malloc
分配的块。
如果这个区域不属于进程,应该立即触发段错误。
更加灾难性的——而且微妙——当下面的块是进程拥有的,因此无法检测到错误 , 无法发出信号,因此它可能 '看起来'仍然'工作',而实际上它会是 覆盖 其他数据、分配器的管理结构,甚至代码(在某些操作环境中)。
这就是为什么指针相关的bug会如此hard 到 track。想象一下,这些行深埋在其他人编写的数千行错综复杂的相关代码中,而您被引导去深入研究。
然而,程序必须仍然编译,因为它仍然 完全有效 和 符合标准 C.
这种错误,no standard and no compiler 可以防大意。我想这正是他们打算教你的。
偏执的人不断寻求改变自然[= C 的 579=] 来处理这些有问题的可能性,从而使我们摆脱自我;但那是不诚实。这是的责任我们有义务接受当我们选择追求权力并获得自由 对机器的控制更直接更全面。 性能完美的推动者和追求者绝不会接受任何不足。
可移植性和通用性它代表是一个根本上独立的考虑因素,all the standard 试图解决:
This document specifies the form and establishes the interpretation of programs expressed in the programming language C. Its purpose is to promote portability, reliability, maintainability, and efficient execution of C language programs on a variety of computing systems.
这就是为什么将它 distinct 与 定义 区别开来是完全正确的原因 和 语言本身的技术规范。与许多人似乎相信的相反,普遍性与例外和典型对立 =578=].
总结:
- 检查和操纵指针本身总是有效并且通常富有成果。结果的解释,可能有意义,也可能没有意义,但在指针被取消引用之前,灾难永远不会被邀请;直到尝试访问地址link。
如果这不是真的,我们所知道的编程 -- 并且喜欢它 -- 将不可能。
Then asked what
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
Evaluate to. The answer is 0, 1, and 0.
这些问题简化为:
- 堆是在栈之上还是栈之下。
- 堆是在程序的字符串文字部分之上还是之下。
- 同[1]。
所有三个问题的答案都是 "implementation defined"。你教授的问题是假的;他们基于传统的 unix 布局:
<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel
但是一些现代的 unices(和替代系统)不符合这些传统。除非他们在问题前加上“截至 1992 年”;确保在评估中给出 -1。
在几乎所有远程现代平台上,指针和整数都具有同构的排序关系,并且指向不相交对象的指针不会交错。大多数编译器在禁用优化时向程序员公开此排序,但标准不区分具有此类排序的平台和没有且不要求任何实现公开此类的平台即使在可以定义它的平台上,也可以向程序员发出命令。因此,一些编译器编写者执行各种优化,并且 "optimizations" 基于代码永远不会比较的假设,在指向不同对象的指针上使用关系运算符。
根据已发布的基本原理,标准的作者打算通过指定实现在标准描述为 "Undefined Behavior" 的情况下的行为方式来扩展语言(即标准未强制执行 requirements) 这样做是有用和实用的,但是一些编译器作者宁愿假设程序永远不会尝试从标准要求之外的任何东西中受益,而不是允许程序有效地利用平台可以支持的行为没有额外费用。
我不知道有任何商业设计的编译器会通过指针比较做任何奇怪的事情,但是随着编译器将其后端转移到非商业 LLVM,他们越来越有可能处理无意义的代码,这些代码的行为早期的编译器已为其平台指定。这种行为不仅限于关系运算符,甚至可以影响 equality/inequality。例如,即使标准规定指向一个对象的指针与指向紧接在前的对象的 "just past" 指针之间的比较将比较相等,但如果程序执行这样的比较。
作为偶数比较在 gcc 和 clang 中表现不正常的情况的示例,请考虑:
extern int x[],y[];
int test(int i)
{
int *p = y+i;
y[0] = 4;
if (p == x+10)
*p = 1;
return y[0];
}
clang 和 gcc 都会生成始终 return 4 的代码,即使 x
是十个元素,y
紧随其后,而 i
是零结果在比较中,p[0]
被写入值 1。我认为发生的情况是一次优化重写函数,就好像 *p = 1;
被 x[10] = 1;
替换了一样。如果编译器将 *(x+10)
解释为等同于 *(y+i)
,则后一代码将是等价的,但不幸的是,下游优化阶段认识到只有在 x
具有时才定义对 x[10]
的访问至少 11 个元素,这将使该访问不可能影响 y
.
如果编译器可以通过标准描述的指针相等方案获得 "creative",我不相信他们会在标准不强加要求的情况下避免变得更有创意。
很简单:比较指针没有意义,因为永远无法保证对象的内存位置与您声明它们的顺序相同。
例外是数组。 &array[0] 低于 &array[1]。这就是 K&R 指出的。实际上,根据我的经验,结构成员地址也按照您声明它们的顺序排列。对此没有任何保证......
另一个例外是比较指针是否相等。当一个指针等于另一个指针时,您知道它指向同一个对象。不管是什么。
如果你问我,考试问题很糟糕。根据Ubuntu Linux 16.04, 64位版本编程环境做题?真的吗?
在 K&R(C 程序设计语言第 2 版)第 5 章中,我阅读了以下内容:
First, pointers may be compared under certain circumstances. If
p
andq
point to members of the same array, then relations like==
,!=
,<
,>=
, etc. work properly.
这似乎暗示只能比较指向同一数组的指针。
但是当我尝试这段代码时
char t = 't';
char *pt = &t;
char x = 'x';
char *px = &x;
printf("%d\n", pt > px);
1
打印到屏幕上。
首先,我认为我会得到未定义或某种类型或错误,因为 pt
和 px
没有指向同一个数组(至少在我的理解中)。
也是pt > px
,因为两个指针都是指向存储在栈中的变量,栈向下增长,所以t
的内存地址大于x
?这就是为什么 pt > px
是真的?
当引入malloc时,我变得更加困惑。同样在第8.7章的K&R中写了以下内容:
There is still one assumption, however, that pointers to different blocks returned by
sbrk
can be meaningfully compared. This is not guaranteed by the standard which permits pointer comparisons only within an array. Thus this version ofmalloc
is portable only among machines for which the general pointer comparison is meaningful.
将指向堆上分配的 space 的指针与指向堆栈变量的指针进行比较,我没有遇到任何问题。
例如,下面的代码运行良好,打印了 1
:
char t = 't';
char *pt = &t;
char *px = malloc(10);
strcpy(px, pt);
printf("%d\n", pt > px);
根据我对编译器的实验,我被引导认为任何指针都可以与任何其他指针进行比较,无论它们分别指向何处。此外,我认为两个指针之间的指针算法很好,无论它们分别指向哪里,因为算法只是使用指针存储的内存地址。
不过,我对在 K&R 中阅读的内容感到困惑。
我问的原因是因为我的教授。实际上把它作为考试题。他给出了以下代码:
struct A { char *p0; char *p1; }; int main(int argc, char **argv) { char a = 0; char *b = "W"; char c[] = [ 'L', 'O', 'L', 0 ]; struct A p[3]; p[0].p0 = &a; p[1].p0 = b; p[2].p0 = c; for(int i = 0; i < 3; i++) { p[i].p1 = malloc(10); strcpy(p[i].p1, p[i].p0); } }
What do these evaluate to:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
答案是0
、1
和0
。
(我的教授确实在考试中包含免责声明,即这些问题是针对 Ubuntu Linux 16.04,64 位版本编程环境的)
(编者注:如果 SO 允许更多标签,那么最后一部分将保证 x86-64, linux, and maybe assembly。如果问题的重点 /class 是特别低级的 OS 实现细节, 而不是便携式 C.)
根据 C11 standard,关系运算符 <
、<=
、>
和 >=
只能用于指向元素的指针相同的数组或结构对象。这在第 6.5.8p5 节中有详细说明:
When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. If two pointers to object types both point to the same object, or both point one past the last element of the same array object, they compare equal. If the objects pointed to are members of the same aggregate object,pointers to structure members declared later compare greater than pointers to members declared earlier in the structure, and pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values. All pointers to members of the same union object compare equal. If the expression P points to an element of an array object and the expression Q points to the last element of the same array object, the pointer expression Q+1 compares greater than P. In all other cases, the behavior is undefined.
请注意,任何不满足此要求的比较都会调用 undefined behavior,这意味着(除其他外)您不能依赖于可重复的结果。
在您的特定情况下,对于两个局部变量的地址之间以及局部地址和动态地址之间的比较,该操作似乎 "work",但是结果可能会通过以下方式改变对您的代码进行看似无关的更改,甚至使用不同的优化设置编译相同的代码。对于未定义的行为,仅仅因为代码 可能 崩溃或产生错误并不意味着它 会 .
例如,在 8086 实模式下 运行ning 的 x86 处理器具有分段内存模型,使用 16 位段和 16 位偏移来构建 20 位地址。所以在这种情况下,地址不会完全转换为整数。
但是等号运算符==
和!=
没有这个限制。它们可以用在任何两个指向兼容类型的指针或 NULL 指针之间。因此,在您的两个示例中使用 ==
或 !=
将生成有效的 C 代码。
然而,即使使用 ==
和 !=
,您也可能会得到一些意想不到但仍然定义明确的结果。有关详细信息,请参阅
关于你的教授给出的试题,它做出了一些有缺陷的假设:
- 存在平面内存模型,其中地址和整数值之间存在一对一的对应关系。
- 转换后的指针值适合整数类型。
- 在执行比较时,该实现只是将指针视为整数,而没有利用未定义行为所提供的自由。
- 使用了堆栈,局部变量存储在那里。
- 堆用于从中提取分配的内存。
- 堆栈(因此局部变量)出现在比堆(因此分配的对象)更高的地址。
- 字符串常量出现在比堆低的地址。
如果您 运行 使用不满足这些假设的编译器在体系结构 and/or 上编写此代码,那么您可能会得到截然不同的结果。
此外,这两个示例在调用 strcpy
时也表现出未定义的行为,因为右操作数(在某些情况下)指向单个字符而不是空终止字符串,导致函数读取超过给定变量的边界。
比较指向相同类型的两个不同数组的指针的主要问题是数组本身不需要放置在特定的相对位置——一个可能在另一个之前和之后结束。
First of all, I thought I would get undefined or some type or error, because pt an px aren't pointing to the same array (at least in my understanding).
不,结果取决于实施和其他不可预测的因素。
Also is pt>px because both pointers are pointing to variables stored on the stack, and the stack grows down, so the memory address of t is greater than that of x? Which is why pt>px is true?
There isn't necessarily a stack。当它存在时,它不需要向下生长。它可以长大。它可能以某种奇怪的方式不连续。
Moreover, I think pointer arithmetic between two pointers is fine, no matter where they individually point because the arithmetic is just using the memory addresses the pointers store.
让我们看看第 85 页的 C specification,§6.5.8,其中讨论了关系运算符(即您正在使用的比较运算符)。请注意,这不适用于直接 !=
或 ==
比较。
When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. ... If the objects pointed to are members of the same aggregate object, ... pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values.
In all other cases, the behavior is undefined.
最后一句话很重要。虽然我减少了一些不相关的案例以保存 space,但有一个案例对我们很重要:两个数组,不是同一个 struct/aggregate 对象 1 的一部分,并且我们正在比较指向这两个数组的指针。这是undefined behavior。
虽然你的编译器只是插入了某种 CMP(比较)机器指令,它在数字上比较指针,你在这里很幸运,但 UB 是一个非常危险的野兽。从字面上看,任何事情都有可能发生——您的编译器可以优化整个函数,包括可见的副作用。它可以产生鼻恶魔。
1可以比较指向属于同一结构的两个不同数组的指针,因为这属于两个数组属于同一聚合对象的子句 (结构)。
指针只是整数,就像计算机中的其他所有东西一样。您绝对可以将它们与 <
和 >
进行比较,并在不导致程序崩溃的情况下产生结果。也就是说,该标准不保证这些结果在数组比较之外具有任何含义。
在您的堆栈分配变量示例中,编译器可以自由地将这些变量分配到寄存器或堆栈内存地址,并且可以选择任何顺序。因此,诸如 <
和 >
之类的比较在编译器或架构之间不会保持一致。但是,==
和 !=
并没有那么受限,比较指针 相等性 是有效且有用的操作。
多么挑衅的问题!
即使粗略地浏览一下这个话题中的回复和评论,也会发现 情绪化 您看似简单直接的查询结果如何是。
这应该不足为奇。
毫无疑问,对概念的误解 of pointers 表示一个主要的 cause of一般编程严重失败。
对这一现实的认识在专门设计用于解决的语言无处不在,最好是 避免 挑战指针引入。 想想 C++ 和 C 的其他派生词,Java 及其关系,Python 和其他脚本——仅仅作为更突出和流行的脚本,并且或多或少地按处理的严重程度排序有问题。
发展对基本原则的更深入理解,因此必须相关每个渴望编程方面的卓越——尤其是在系统级别。
我想这正是你老师要演示的意思。
并且 C 的性质使其成为这种探索的便捷工具。不如汇编清晰——尽管可能更容易理解——但仍然比基于执行环境更深层次抽象的语言明确得多。
旨在促进 确定性 将程序员的意图翻译成机器可以理解的指令,C 是 系统级语言。虽然被归类为 high-level,但它确实属于“中等”类别;但是由于 none 这样的存在,所以“系统”名称就足够了。
此特性在很大程度上使其成为 设备驱动程序的首选语言 、操作系统代码,以及嵌入式 实现。此外,在 最佳效率 至关重要的应用程序中,这是一个当之无愧的备选方案;这意味着生存和灭绝之间的差异,因此是 必需品 而不是奢侈品。在这种情况下,便携性 的便利性就失去了所有吸引力,而选择 最小公分母 的 lack-lustre 性能就变成了难以想象的有害选项。
是什么让 C —— 以及它的一些衍生产品 —— 非常特别,因为它 允许 它的用户 完成控制--当那是他们想要的--没有强加 相关的 责任 当他们不这样做时。尽管如此,它提供的绝不会超过 最薄的绝缘层 来自 machine,因此正确使用要求要求理解指针的概念.
从本质上讲,您问题的答案非常简单且令人满意 - 证实了您的怀疑。 提供了,然而,人们将必要的重要性附加到每个概念声明:
- 检查、比较和操作指针的行为总是并且必然有效,而从结果中得出的结论取决于所包含值的有效性,因此需要不是是。
前者既总是安全又潜在 proper,而后者只能在 proper 建立为安全。令人惊讶的是 -- 对某些人来说 -- 所以确定后者的有效性 取决于 和 要求前者。
当然,部分混淆是由于指针原理中固有的递归效应以及区分内容和地址所带来的挑战。
你已经正确地推测,
I'm being led to think that any pointer can be compared with any other pointer, regardless of where they individually point. Moreover, I think pointer arithmetic between two pointers is fine, no matter where they individually point because the arithmetic is just using the memory addresses the pointers store.
一些贡献者已经确认:指针只是数字。 有时更接近 复杂 数字,但仍然不超过数字。
这里收到的这种争论所引起的有趣的尖酸刻薄更多地揭示了人性而不是编程,但仍然值得一提和详细说明。也许我们稍后会这样做...
正如一条评论开始暗示的那样;所有这些困惑和惊愕都源于需要区分什么是 有效 什么是 安全,但这是过于简单化了。我们还要分清什么是实用,什么是可靠,什么是实用,什么是可能正确,更进一步:什么是正确的在特定情况下,什么是更一般意义上的正确[=578] =].更何况; 合规和礼仪之间的区别。
为此,我们首先需要欣赏 指针 是.
- 您已经表现出对这个概念的坚定把握,并且像其他一些人一样可能会发现这些插图过于简单化,但这里明显的混乱程度需要如此简单的澄清。
正如一些人所指出的:术语 pointer 只是 index 的一个特殊名称,因此无非是任何其他 number。
这应该已经是 self-evident 考虑到所有现代主流计算机都是 二进制机器 必须 工作独家 数字。量子计算 可能 改变这一点,但这不太可能,而且它还没有成熟。
从技术上讲,正如您所指出的,指针更准确地址;一个明显的洞察力自然而然地引入了将它们与房屋或街道上的地块的“地址”相关联的有益类比。
在平坦内存模型中:整个系统内存组织在一个单一的线性序列中:所有城里的房子都在同一条路上,每栋房子都由编号唯一标识。非常简单。
在segmented方案中:在编号房屋之上引入编号道路的分层组织,以便复合需要地址。
- 一些实现仍然更加复杂,不同“道路”的整体需要 而不是 求和到一个连续的序列,但是 none 这改变了关于潜在的。
- 我们必须能够将每个这样的层级结构 link 分解回扁平组织。组织越复杂,我们就必须越过越多的圈子才能做到这一点,但这 必须 是可能的。实际上,这也适用于 x86 上的“实模式”。
- 否则 link 到位置的映射将不会是 bijective,因为可靠的执行——在系统级别——要求它 必须。
- 多个地址必须不映射到单个内存位置,并且
- 单一地址必须永远不会映射到多个内存位置。
给我们带来 进一步的转折 将难题变成如此复杂得令人着迷 纠结。上面,为了简单明了起见,建议指针 是 地址是权宜之计。当然,这是不正确的。指针是不是地址;指针是对地址的引用,它包含地址。就像信封上提到的房子一样。考虑这一点可能会让您瞥见概念中包含的递归建议的含义。仍然;我们只有这么多的话,并且谈论 地址引用地址 等等,很快就会让大多数大脑陷入 无效 op-code 异常 .在大多数情况下,意图很容易从上下文中获得,所以让我们 return 走上街头。
我们这个想象中的城市中的邮政工作人员与我们在“真实”世界中找到的工作人员非常相似。当您交谈或询问[=时,没有人会中风579=] 关于一个 无效的 地址,但是当你要求他们 采取行动时,每个最后一个人都会犹豫 关于那个信息。
假设我们单一的街道上只有 20 所房子。进一步假装某个误入歧途或阅读障碍的灵魂将一封非常重要的信寄给了 71 号。现在,我们可以问我们的承运人 Frank,是否有这样的地址,他会简单而平静地报告:没有。我们甚至可以期望他估计如果 确实 存在,这个位置会在街道外多远:大约是终点的 2.5 倍。 None 这会让他生气。 但是,如果我们要他送这封信,或者 从那个地方捡到一件东西,他很可能会很坦诚地表达自己的不快,拒绝遵守。
指针是只是地址,地址是只是个数。
验证以下输出:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
在任意多的指针上调用它,无论有效与否。请 do post 如果它在您的平台上失败,或者您的 (contemporary) 编译器抱怨。
现在,因为指针是简单的数字,比较它们不可避免地有效。从某种意义上说,这正是你的老师所展示的。以下所有陈述都是完全有效的 -- 并且正确! -- C,并且在编译时 将 运行 而不会遇到问题 ,即使两个指针都不需要初始化,因此它们包含的值可能是 未定义:
- 我们只是为了清晰度和打印[=]而
result
明确计算578=] 它 强制 编译器计算否则将是冗余的死代码。
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
当然,当 a 或 b 未定义时,程序是 ill-formed(阅读:未正确初始化)在测试点,但这与我们讨论的这一部分完全 无关 。这些片段以及以下陈述 保证 -- 由“标准”--完美地编译和运行,尽管IN-涉及的任何指针的有效性。
仅当无效指针被解引用时才会出现问题。当我们要求 Frank 在无效的 non-existent 地址取货或送货时。
给定任意指针:
int *p;
虽然这条语句必须编译并且运行:
printf(“%p”, p);
...必须这样做:
size_t foo( int *p ) { return (size_t)p; }
...下面这两个,形成鲜明对比的是,仍然可以轻松编译,但是执行失败 unless 指针 是 有效的 -- 我们在这里仅表示它 引用了当前应用程序已被授予访问权限的地址 :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
变化有多微妙?区别在于指针的值——是地址和内容的值之间的区别:那个数字。直到指针解引用才出现问题;直到尝试访问它 link 的地址。试图在路段之外递送或提取包裹...
推而广之,同样的原理必然适用于更复杂的例子,包括前面提到的需要到建立必要的有效性:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
关系比较和算术提供了相同的效用来测试等价性,并且在原则上是等价的。 然而,这样计算的结果会signify , 完全是另一回事 - 正是您所引用的引文所解决的问题。
在 C 中,数组是一个连续的缓冲区,一个不间断的线性内存位置系列。比较和算术应用于在这样一个 singular 系列中引用位置的指针是自然的,并且对于彼此之间的关系来说显然是有意义的,并且对于这个'数组'(由基数简单标识)。这同样适用于通过 malloc
或 sbrk
分配的每个块。 因为这些关系是隐含的,编译器能够在它们之间建立有效关系,因此可以有信心计算将提供预期的答案。
对引用 distinct bl 的指针执行类似的操作cks 或数组不提供任何此类 inherent 和 apparent 实用程序。更重要的是,因为在某一时刻存在的任何关系都可能因随后的重新分配而无效,其中这种关系极有可能发生变化,甚至被逆转。在这种情况下,编译器无法获得必要的信息来建立它对先前情况的信心。
你,但是,作为程序员,可能有这样的知识!在某些情况下,我们不得不利用这一点。
存在ARE,因此,EVEN THIS完全VALID 并且完美 PROPER.
事实上,正是 malloc
本身在尝试合并回收块时必须在内部做的事情- 在绝大多数架构上。操作系统分配器也是如此,比如sbrk
后面那个;如果更明显,经常,在更多不同的实体上,更多critically——并且在 malloc
可能不存在的平台上也相关。 其中有多少不是用 C 语言编写的?
一项行动的有效性、安全性和成功性不可避免地取决于其前提和应用的洞察力水平。
在您提供的引述中,Kernighan 和 Ritchie 正在解决一个密切相关但 none 又是独立的问题。它们是定义语言的限制,并解释了如何利用编译器的功能至少通过检测潜在的错误结构来保护您。他们描述了该机制能够 -- 设计-- 的长度,以帮助您完成编程任务。 编译器是你的仆人,你是主人。然而,聪明的主人是非常熟悉他的各种仆人的能力。
在此上下文中,undefined behaviour 用于表示潜在的危险和伤害的可能性;并不是暗示迫在眉睫的、不可逆转的厄运,或者我们所知道的世界末日。它只是意味着我们——“意味着编译器”——不能对这个东西可能是什么做出任何推测,或者代表 出于这个原因,我们选择洗手。 对于因使用或mis-use此设施可能导致的任何意外事故,我们概不负责。
实际上,它只是说:“除此之外,cowboy:你只能靠自己了……”
您的教授正在寻求向您展示更细微的差别。
注意他们非常小心他们在制作他们的榜样时所采取的措施;以及脆它仍然如何。通过取a
的地址,在
p[0].p0 = &a;
编译器被迫为变量分配实际存储空间,而不是将其放在寄存器中。它是一个自动变量,但是,程序员 no 控制分配的 where,因此无法对什么会发生任何有效的推测跟着它。这就是为什么 a
必须 设置为零以使代码按预期工作。
仅仅改变这一行:
char a = 0;
对此:
char a = 1; // or ANY other value than 0
导致程序的行为变为未定义。至少,第一个答案现在是 1;但问题要险恶得多。
现在代码是自找麻烦
虽然仍然完全有效甚至符合标准,但现在ill-formed 并且虽然肯定可以编译,但可能会因各种原因而无法执行。目前有 多个 问题 -- none 其中 编译器是能够到认识.
strcpy
将从 a
的地址开始,并在此之后继续消费——并传输——一个字节接一个字节,直到遇到空值。
p1
指针已初始化为恰好 10 字节的块。
如果
a
恰好放在块的末尾并且进程无法访问后面的内容,那么下一次读取 -- p0[1] -- 将会引发段错误。这种情况在 x86 架构上不太可能,但有可能。如果
a
地址之外的区域是可访问的,不会出现读取错误,但程序仍然没有幸免于难。如果零字节碰巧出现在从
a
地址开始的十个字节中,它 可能 仍然存在,因为那时strcpy
将停止并且至少我们不会遭受写违规。如果不是读取错误,但是没有零字节出现在这个10的跨度内,
strcpy
将继续并尝试 写入 超出malloc
分配的块。如果这个区域不属于进程,应该立即触发段错误。
更加灾难性的——而且微妙——当下面的块是进程拥有的,因此无法检测到错误 , 无法发出信号,因此它可能 '看起来'仍然'工作',而实际上它会是 覆盖 其他数据、分配器的管理结构,甚至代码(在某些操作环境中)。
这就是为什么指针相关的bug会如此hard 到 track。想象一下,这些行深埋在其他人编写的数千行错综复杂的相关代码中,而您被引导去深入研究。
然而,程序必须仍然编译,因为它仍然 完全有效 和 符合标准 C.
这种错误,no standard and no compiler 可以防大意。我想这正是他们打算教你的。
偏执的人不断寻求改变自然[= C 的 579=] 来处理这些有问题的可能性,从而使我们摆脱自我;但那是不诚实。这是的责任我们有义务接受当我们选择追求权力并获得自由 对机器的控制更直接更全面。 性能完美的推动者和追求者绝不会接受任何不足。
可移植性和通用性它代表是一个根本上独立的考虑因素,all the standard 试图解决:
This document specifies the form and establishes the interpretation of programs expressed in the programming language C. Its purpose is to promote portability, reliability, maintainability, and efficient execution of C language programs on a variety of computing systems.
这就是为什么将它 distinct 与 定义 区别开来是完全正确的原因 和 语言本身的技术规范。与许多人似乎相信的相反,普遍性与例外和典型对立 =578=].
总结:
- 检查和操纵指针本身总是有效并且通常富有成果。结果的解释,可能有意义,也可能没有意义,但在指针被取消引用之前,灾难永远不会被邀请;直到尝试访问地址link。
如果这不是真的,我们所知道的编程 -- 并且喜欢它 -- 将不可能。
Then asked what
p[0].p0 < p[0].p1 p[1].p0 < p[1].p1 p[2].p0 < p[2].p1
Evaluate to. The answer is 0, 1, and 0.
这些问题简化为:
- 堆是在栈之上还是栈之下。
- 堆是在程序的字符串文字部分之上还是之下。
- 同[1]。
所有三个问题的答案都是 "implementation defined"。你教授的问题是假的;他们基于传统的 unix 布局:
<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel
但是一些现代的 unices(和替代系统)不符合这些传统。除非他们在问题前加上“截至 1992 年”;确保在评估中给出 -1。
在几乎所有远程现代平台上,指针和整数都具有同构的排序关系,并且指向不相交对象的指针不会交错。大多数编译器在禁用优化时向程序员公开此排序,但标准不区分具有此类排序的平台和没有且不要求任何实现公开此类的平台即使在可以定义它的平台上,也可以向程序员发出命令。因此,一些编译器编写者执行各种优化,并且 "optimizations" 基于代码永远不会比较的假设,在指向不同对象的指针上使用关系运算符。
根据已发布的基本原理,标准的作者打算通过指定实现在标准描述为 "Undefined Behavior" 的情况下的行为方式来扩展语言(即标准未强制执行 requirements) 这样做是有用和实用的,但是一些编译器作者宁愿假设程序永远不会尝试从标准要求之外的任何东西中受益,而不是允许程序有效地利用平台可以支持的行为没有额外费用。
我不知道有任何商业设计的编译器会通过指针比较做任何奇怪的事情,但是随着编译器将其后端转移到非商业 LLVM,他们越来越有可能处理无意义的代码,这些代码的行为早期的编译器已为其平台指定。这种行为不仅限于关系运算符,甚至可以影响 equality/inequality。例如,即使标准规定指向一个对象的指针与指向紧接在前的对象的 "just past" 指针之间的比较将比较相等,但如果程序执行这样的比较。
作为偶数比较在 gcc 和 clang 中表现不正常的情况的示例,请考虑:
extern int x[],y[];
int test(int i)
{
int *p = y+i;
y[0] = 4;
if (p == x+10)
*p = 1;
return y[0];
}
clang 和 gcc 都会生成始终 return 4 的代码,即使 x
是十个元素,y
紧随其后,而 i
是零结果在比较中,p[0]
被写入值 1。我认为发生的情况是一次优化重写函数,就好像 *p = 1;
被 x[10] = 1;
替换了一样。如果编译器将 *(x+10)
解释为等同于 *(y+i)
,则后一代码将是等价的,但不幸的是,下游优化阶段认识到只有在 x
具有时才定义对 x[10]
的访问至少 11 个元素,这将使该访问不可能影响 y
.
如果编译器可以通过标准描述的指针相等方案获得 "creative",我不相信他们会在标准不强加要求的情况下避免变得更有创意。
很简单:比较指针没有意义,因为永远无法保证对象的内存位置与您声明它们的顺序相同。 例外是数组。 &array[0] 低于 &array[1]。这就是 K&R 指出的。实际上,根据我的经验,结构成员地址也按照您声明它们的顺序排列。对此没有任何保证...... 另一个例外是比较指针是否相等。当一个指针等于另一个指针时,您知道它指向同一个对象。不管是什么。 如果你问我,考试问题很糟糕。根据Ubuntu Linux 16.04, 64位版本编程环境做题?真的吗?