指针比较在C中如何工作?比较不指向同一数组的指针是否可以?


33

在K&R(C编程语言第二版)第5章中,我阅读了以下内容:

首先,可以在某些情况下比较指针。如果pq指向同一个数组的成员,则关系一样==!=<>=,等正常工作。

这似乎暗示着只能比较指向同一数组的指针。

但是当我尝试这段代码时

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 被打印到屏幕上。

首先,我以为我会得到未定义或某种类型或错误的信息,因为ptpx没有指向同一数组(至少在我看来)。

同样是pt > px因为两个指针都指向存储在堆栈中的变量,并且堆栈变小,所以的内存地址t大于x?的内存地址。这是为什么pt > px呢?

引入malloc时,我会更加困惑。同样,在K&R的8.7章中,内容如下:

但是,仍然有一个假设,即sbrk可以有意义地比较指向返回的不同块的指针。该标准不能保证这一点,该标准仅允许在数组内进行指针比较。因此,此版本malloc仅可在通用指针比较有意义的机器之间移植。

将指向堆上分配的空间的指针与指向堆栈变量的指针进行比较,我没有任何问题。

例如,以下代码可以很好地工作并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);
    }
}

这些评估结果是什么:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

答案是010

(我的教授确实在考试中声明了问题是针对Ubuntu Linux 16.04、64位版本编程环境的免责声明)

(编者注:如果SO允许更多标签,则最后一部分将保证甚至可能是。如果问题/类的重点是具体的低级OS实现细节,而不是可移植的C。)


17
您可能会混淆有效C内容和安全的内容C。比较两个相同类型的指针总是可以完成的(例如,检查是否相等),但是要使用指针算术和比较><并且只有给定数组(或内存块)中使用时才是安全的
阿德里安·摩尔

13
顺便说一句,您应该从K&R学习C。从一开始,该语言就经历了许多更改。而且,老实说,其中的示例代码是从重视简洁而不是可读性的时代开始的。
paxdiablo

5
不,不能保证它能正常工作。实际上,在具有分段内存模型的计算机上,它可能会失败。请参阅C在C ++中是否具有等效的std :: less?在大多数现代机器上,尽管使用了UB,它仍会起作用。
彼得·科德斯

6
@Adam:关闭,但这实际上是UB(除非OP使用的编译器GCC确实选择定义它。可能)。但是UB并不意味着“绝对爆炸”。UB的可能行为之一就是按照您期望的方式工作!!这就是使UB如此讨厌的原因。它可以在调试版本中正常运行,并且在启用优化的情况下失败,反之亦然,或者取决于周围的代码而中断。 比较其他指针仍然可以为您提供答案,但是该语言并未定义该答案的含义(如果有的话)。 不,允许崩溃。确实是UB。
彼得·科德斯

3
@亚当:哦,是的,没关系我的评论的第一部分,我误读了你的。但是您声称比较其他指针仍然可以为您提供答案。这不是真的。那将是不确定的结果,而不是完整的UB。UB更加糟糕,这意味着如果执行通过这些输入到达该语句(在实际发生之前或之后的任何时候),您的程序就可能发生段错误或SIGILL。(如果UB在编译时可见,则只有在x86-64上才是合理的,但一般而言,任何事情都可能发生。)UB的部分要点是让编译器在生成asm时做出“不安全”的假设。
彼得·科德斯

Answers:


33

根据C11标准,关系运算符<<=>,和>=可以仅在指针用于同一阵列或结构对象的元素。这在第6.5.8p5节中有详细说明:

比较两个指针时,结果取决于所指向对象的地址空间中的相对位置。如果两个指向对象类型的指针都指向同一个对象,或者都指向同一数组对象的最后一个元素,则它们的比较相等。如果指向的对象是同一聚合对象的成员,则指向稍后声明的结构成员的指针比指向结构中先前声明的成员的指针的指针更大,并且指向具有较大下标值的数组元素的指针的指针大于指向同一数组的元素的指针下标值较低。指向同一联合对象的成员的所有指针比较相等。

请注意,任何不满足此要求的比较都会调用未定义的行为,即(其中包括)您不能依赖于结果是否可重复。

在您的特定情况下,对于两个局部变量的地址之间的比较以及局部地址和动态地址的地址之间的比较,该操作似乎都是“有效的”,但是通过对您的代码进行看似无关的更改,结果可能会更改甚至使用不同的优化设置来编译相同的代码。对于未定义的行为,仅因为代码可能崩溃或产生错误并不意味着就可以

例如,以8086实模式运行的x86处理器具有分段内存模型,该模型使用16位段和16位偏移量来构建20位地址。因此,在这种情况下,地址不能完全转换为整数。

相等运算符==!=但是没有此限制。可以在指向兼容类型的任何两个指针或NULL指针之间使用它们。因此==!=在两个示例中使用或都会产生有效的C代码。

但是,即使使用==!=您也可能会得到一些出乎意料但仍然定义明确的结果。请参阅不相关指针的相等性比较是否可以评估为true?有关此的更多详细信息。

关于您的教授提出的考试问题,它提出了许多错误的假设:

  • 存在平面存储器模型,其中地址与整数值之间存在1对1的对应关系。
  • 转换后的指针值适合整数类型。
  • 该实现仅在执行比较时简单地将指针视为整数,而不利用未定义行为赋予的自由度。
  • 使用堆栈并将局部变量存储在此处。
  • 该堆用于从中提取分配的内存。
  • 堆栈(因此是局部变量)出现在比堆(因此是分配的对象)更高的地址上。
  • 该字符串常量出现在比堆低的地址处。

如果要在不满足这些假设的体系结构和/或编译器上运行此代码,则可能会获得截然不同的结果。

同样,这两个示例在调用时也都表现出未定义的行为strcpy,因为正确的操作数(在某些情况下)指向单个字符而不是空终止的字符串,从而导致函数读取给定变量的范围之外。


3
@Shisui即使如此,您仍然不应该依赖结果。编译器在进行优化时会变得非常激进,并将使用未定义的行为作为这样做的机会。使用不同的编译器和/或不同的优化设置可能会生成不同的输出。
dbush

2
@Shisui:通常会在具有扁平内存模型(例如x86-64)的计算机上运行。此类系统的某些编译器甚至可能在其文档中定义了行为。但是,如果不是这样,则由于编译时可见的UB,可能会发生“疯狂”行为。(在实践中,我认为没有人希望这样做,所以这不是主流编译器所追求的并“试图打破”。)
Peter Cordes

1
就像如果一个编译器看到的是,执行一个路径会导致<malloc结果和局部变量(自动存储,即堆),它可以承担执行这条道路是从来没有,只是编译整个功能的ud2指令(会引发一个非法-指令异常,内核将通过向进程传递SIGILL来处理该指令)。在实践中,GCC / clang对于其他类型的UB会这样做,例如从void功能失效的结尾掉下来。 godbolt.org被降权现在看来,但尝试复制/粘贴int foo(){int x=2;},并注意缺少的ret
彼得·柯德斯

4
@Shisui:TL:DR:尽管它恰好在x86-64 Linux上运行良好,但它不是可移植的C语言。但是,对比较结果进行假设只是疯狂的。如果您不在主线程中,则将使用与malloc从操作系统获取更多内存相同的机制来动态分配线程堆栈,因此没有理由假定本地var(线程堆栈)位于malloc动态分配之上存储。
彼得·科德斯

2
@PeterCordes:需要的是将行为的各个方面都识别为“可选定义”,以便实现可以随意定义它们,但如果不这样做,则必须以可测试的方式(例如,预定义的宏)进行指示。此外,与其将可观察到优化效果的任何情况描述为“未定义行为”的特征,不如说,如果优化器指出某些方面的行为是“不可观察的”,那将更加有用。这样做。例如,给定int x,y;一个实现...
supercat

12

比较指向两个相同类型的不同数组的指针的主要问题是,数组本身不需要放置在特定的相对位置中-一个可以在另一个之前和之后结束。

首先,我认为我会得到未定义或某种类型或错误的信息,因为pt和px并不指向同一数组(至少在我看来)。

不,结果取决于实施和其他不可预测的因素。

pt> px也是因为两个指针都指向存储在堆栈中的变量,并且堆栈变小,所以t的内存地址大于x的内存地址吗?为何pt> px为真?

不一定有堆栈。当它存在时,它不需要长大。它可能长大。它可能以某种奇怪的方式是不连续的。

而且,我认为两个指针之间的指针算法都很好,无论它们分别指向何处,因为该算法只是使用指针存储的内存地址。

让我们看一下第85页的C规范 §6.5.8,其中讨论了关系运算符(即,您正在使用的比较运算符)。请注意,这不适用于直接!===比较。

比较两个指针时,结果取决于所指向对象的地址空间中的相对位置。...如果所指向的对象是同一聚合对象的成员,则...具有较大下标值的数组元素的指针比具有较低下标值的相同数组的元素的指针要大。

在所有其他情况下,行为是不确定的。

最后一句话很重要。虽然我减少了一些无关的情况以节省空间,但有一种情况对我们很重要:两个数组,而不是同一struct / aggregate对象1的一部分,我们正在比较指向这两个数组的指针。这是未定义的行为

虽然您的编译器只是插入了某种CMP(比较)机器指令,可以对指针进行数值比较,但是您在这里很幸运,UB是非常危险的野兽。从字面上看,任何事情都可能发生-您的编译器可以优化整个函数,包括可见的副作用。它可能会产生鼻恶魔。

1可以比较属于同一结构的两个不同数组的指针,因为这属于两个数组是同一聚合对象(结构)的一部分的子句。


1
更重要的是,tx在同一个函数被定义,我们有理由零假设有关如何编译器针对X86-64将在此函数的栈帧布局当地人什么。向下增长的堆栈与一个函数中变量的声明顺序无关。即使在单独的函数中,如果一个函数可以内联到另一个函数,则“子”函数的本地人仍然可以与父函数混合。
彼得·科德斯

1
你的编译器可以优化了整体功能,包括可见的副作用 并非言过其实:其他类型的UB(如脱落的非结束void函数)g ++以及铛++真正做到这一点的做法: godbolt.org/z/g5vesB他们假设没有执行路径,因为它导致了UB,并将任何这样的基本块编译为非法指令。或根本没有任何指令,只要调用了该函数,就静静地进入下一步的asm。(仅出于某种原因,gcc它不这样做g++)。
彼得·科德斯

6

然后问什么

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

评估为。答案是0、1和0。

这些问题简化为:

  1. 堆是在堆栈之上还是之下。
  2. 是堆位于程序的字符串文字部分的上方还是下方。
  3. 与[1]相同。

这三个答案都是“实现定义”。教授的问题是假的。他们以传统的Unix布局为基础:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

但是一些现代的联合国(和替代系统)不符合这些传统。除非他们在问题开头加上“自1992年起”;确保在评估中给出-1。


3
未定义实现,未定义!以这种方式考虑,前者可能在实现之间有所不同,但是实现应记录如何确定行为。后者意味着行为可能会以任何方式发生变化,并且实施过程不必告诉您深蹲:-)
paxdiablo

1
@paxdiablo:根据该标准作者的基本原理,“未定义的行为...还标识了可能符合语言扩展的区域:实现者可以通过提供正式的未定义行为的定义来扩展语言。” 基本原理还说:“目标是给程序员一个挣扎的机会,以使功能强大的C程序具有很高的可移植性,而又不会贬低恰好有用的C语言程序,而这些C程序恰好不是可移植的,因此严格来说是副词。” 商业编译器作者了解这一点,但其他一些编译器作者则不了解。
超级猫

还有另一个实现定义的方面。指针比较是带符号的,因此根据机器/ os /编译器的不同,某些地址可能会解释为负数。例如,将堆栈放在0xc << 28的32位计算机可能会在比堆或rodata更小的地址处显示自动变量。
mevets '19

1
@mevets:标准中是否规定了在比较中可以观察到指针签名的任何情况?我希望,如果16位平台允许大于32768字节arr[]的对象并且是这样的对象,那么标准将要求该arr+32768比较大于,arr即使有符号的指针比较会报告其他情况。
超级猫

我不知道; C标准在但丁的第9圈旋转,为安乐死祈祷。OP特别提到了K&R和一个考试问题。#UB是来自一个懒惰工作组的碎片。
mevets '19

1

在几乎任何远程现代平台上,指针和整数都具有同构的排序关系,并且指向不相交对象的指针不会交织。禁用优化功能后,大多数编译器会将这种顺序显示给程序员,但是标准没有区分具有这种顺序的平台与没有这种顺序且不需要的平台。这种顺序的平台,甚至在任何实现中向程序员公开这种顺序的平台。定义它。因此,一些编译器作者基于这样的假设执行各种优化和“优化”,即代码将永远不会比较指向不同对象的指针上的使用关系运算符。

根据已发布的《基本原理》,该标准的作者打算通过指定标准在其被定义为“未定义行为”的情况下的行为方式(即标准未规定任何要求)来扩展语言。)的这将是有用且实用的,但有些编译器作者宁愿程序不会尝试从标准授权之外的任何内容中受益,而不是允许程序有用地利用平台可以支持的行为,而无需任何额外费用。

我不知道有任何商业设计的编译器会对指针比较做任何奇怪的事情,但是随着编译器转向非商业LLVM作为其后端,它们越来越有可能处理其行为已由早先指定的无意义的代码。平台的编译器。这种行为不仅限于关系运算符,甚至可能影响相等性/不平等性。例如,即使标准指定一个对象的指针与紧接在前的对象的“刚刚过去”指针之间的比较将比较相等,但是,如果程序执行这样的编译器,则基于gcc和LLVM的编译器很容易生成无意义的代码比较。

例如,即使相等比较在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都将生成始终返回4的代码,即使x是10个元素,y紧随其后的i也是0,导致比较为真并p[0]以值1写入。我认为发生的情况是一遍优化重写函数好像*p = 1;被替换了x[10] = 1;。如果编译器解释*(x+10)为等效于后一个代码,则后者等效*(y+i),但不幸的是,下游优化阶段认识到x[10]只有在x具有至少11个元素的情况下才定义对的访问,这将使该访问无法生效y

如果编译器可以通过标准描述的指针相等方案获得“创意”,那么我将不相信他们在标准未施加要求的情况下就可以发挥更大的创造力。


0

很简单:比较指针没有意义,因为永远不能保证对象的存储位置与声明它们的顺序相同。数组除外。&array [0]低于&array [1]。那就是K&R指出的。实际上,根据我的经验,结构成员地址也按照您声明它们的顺序排列。对此不做任何保证。...另一个例外是,如果将指针比较为相等。当一个指针与另一个指针相等时,您就知道它指向同一对象。不管是什么。如果你问我不好的考试问题。取决于Ubuntu Linux 16.04、64位版本编程环境的考试问题?真的吗


从技术上讲,阵列是不是一个真正的异常,因为你没有申报arr[0]arr[1]等分开。您声明arr为一个整体,因此单个数组元素的排序与此问题中描述的问题不同。
paxdiablo

1
保证结构元素是有序的,这保证了一个元素可以memcpy用来复制结构的连续部分并影响其中的所有元素,而不影响其他任何元素。该标准对使用结构或malloc()分配的存储可以完成哪种指针​​算术的术语草率。offsetof如果不能用a的结构字节对同一类型的指针进行算术运算,则该宏将毫无用处char[],但是Standard并未明确指出该结构的字节是(或可以用作)数组对象。
supercat

-4

多么挑衅的问题!

即使在这个线程的答复和意见的粗略扫描将揭示如何情感你看似简单和直接的查询结果是。

不足为奇。

毫无疑问,对指针概念和使用误解导致总体编程严重失败的主要原因

在专门设计用于解决(最好避免指针完全引入的挑战)的语言的普遍存在下,很容易认识到这一现实。可以将C ++以及C,Java及其关系的其他派生类,Python和其他脚本视为仅仅是更突出和普遍的脚本,并且在处理该问题的严重性上或多或少地有序。

因此,对每一个渴望在编程方面追求卓越的个人(尤其是在系统级别上)必须他们更深入地了解其基本原理。

我想这正是您的老师想证明的意思。

C的性质使其成为进行此探索的便捷工具。比基于汇编的语言不够清晰-尽管也许更容易理解-但仍比基于对执行环境的更深抽象的语言更清晰。

C是一种系统级语言,旨在促进将程序员的意图确定性地转换为机器可以理解的指令。虽然被归类为高级,但实际上属于“中等”类别。但是由于不存在这种情况,因此“系统”名称必须足够。

该特性是用于使得一个主要原因选择的语言的设备驱动程序操作系统代码,和嵌入的实现。此外,在最佳效率至关重要的应用中,当之无愧的替代方案;这意味着生存与灭绝之间的差异,因此与奢侈品相对比,这是必需的。在这种情况下,便携性的吸引人的便利失去了所有吸引力,而选择最小公分母低调性能成为不可思议的有害选择。

是什么让Ç -和它的一些衍生物-比较特别的是,它允许其用户完全控制-当是他们的愿望-强加相关的责任在他们身上时,他们没有。然而,它永远不会提供超过最薄绝缘的,因此正确使用要求苛刻的理解概念的指针

从本质上讲,对于您的问题的回答非常简单,令人满意,这是对您怀疑的肯定。提供的,但是,一个十分必要的意义,以每一个概念在此声明:

  • 检查,比较和操纵指针的行为始终且必然是有效的,而从结果得出的结论取决于所包含的值的有效性,因此不一定如此。

前者总是 安全的,并且可能是 适当的,而后者只有在被确定安全时才是适当的。令人惊讶的是-对于某些人-因此确定后者的有效性取决于要求前者。

当然,部分混淆是由于指针原理中固有的递归效果所致,以及将内容与地址区分开带来的挑战。

您已经正确地推测出

我被认为可以将任何指针与任何其他指针进行比较,无论它们分别指向何处。而且,我认为两个指针之间的指针算法都很好,无论它们分别指向何处,因为该算法只是使用指针存储的内存地址。

并且有几个贡献者确认:指针只是数字。有时有些东西更接近于复数,但仍然不超过数字。

在这里引起了争论的有趣争论,比编程更能说明人性,但仍然值得一提。也许我们以后再做...

随着一则评论开始暗示;所有这些困惑和惊from源自需要从安全的东西中辨别出什么是有效的,但这过于简单了。我们还必须区分什么是功能性的,什么是可靠的,什么是实用的以及什么是适当的,并且还要进一步:在特定情况下,什么是适当的,从更一般的意义上来说,什么是适当的。更何况; 合格礼节之间的区别。

为了实现这一目标,我们首先需要明白恰恰是什么指针

  • 您已经牢牢掌握了这个概念,并且像其他一些插图一样,可能会发现这些插图过于简单化,但此处明显的混乱程度要求澄清起来要如此简单。

正如一些人指出的那样:术语“ 指针”只是一个特殊的名称,仅仅是一个索引,因此无非是任何其他数字

这应该已经是不言而喻的考虑的事实,所有当下主流计算机都是二进制的机器必然的工作完全与和数字。量子计算可能会改变这种情况,但这不太可能,而且还没有成熟。

正如您已经注意到的,从技术上讲,指针是更准确的地址。一个显而易见的见解自然会引入有益的类比,即将它们与房屋的“地址”或街道上的地块相关联。

  • 平面内存模型中:整个系统内存按单个线性顺序进行组织:城市中的所有房屋都位于同一条道路上,并且每个房屋仅通过其编号即可唯一标识。非常简单。

  • 分段方案中:在编号房屋的上方引入了编号道路的层次结构,因此需要组合地址。

    • 有些实现还是比较绕口,以及独特的“道路”需要的全部不是总和为连续序列,但没有改变有关基础东西。
    • 我们必须能够将每个这样的层次结构链接分解成一个平面组织。组织越复杂,我们要做的越困难,但这必须是可能的。实际上,这也适用于x86上的“实模式”。
    • 否则,链接到位置的映射不会是双射的,因为可靠的执行(在系统级别上)要求必须如此。
      • 多个地址不得映射到单个内存位置,并且
      • 单数地址绝不能映射到多个内存位置。

让我们进一步扭转,将难题变成如此迷人的纠结。在上面,为了简单和清楚起见,建议将指针作为地址是很方便的。当然,这是正确的。指针的地址; 指针是对地址引用,它包含一个地址。像信封一样,是对房屋的参考。考虑到这一点,您可能会发现概念中包含递归建议的含义。仍然; 我们只有那么多的单词,并且在谈论引用地址的地址这样,很快就会使大多数人陷入无效的操作码异常的境地。在大多数情况下,意图很容易从上下文中获得,因此让我们回到街头。

在我们这个虚构的城市中,邮政工作人员与我们在“真实”世界中发现的工作人员非常相似。当你没有一个很可能患中风谈论询问关于无效的地址,但是当你问他们每个最后一个将不惜采取行动的信息。

假设在我们的单一街道上只有20栋房屋。进一步假设某位被误导或诵读困难的人将一封非常重要的字母指向71 。我们甚至可以期待他估计多远外面的街道上这个位置会是否说谎确实存在:进一步大约2.5倍的结束。这些都不会使他感到生气。然而,如果我们要请他这封信,或拿起从那个地方的项目,他很可能是相当坦率地谈了他的不满,并拒绝遵守。

指针只是地址,地址只是数字。

验证以下内容的输出:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

根据需要调用任意数量的指针,无论指针是否有效。如果在您的平台上失败,或者您(当前)的编译器抱怨,请不要发布您的发现。

现在,因为指针简单的数字,它必然是有效的对它们进行比较。从某种意义上讲,这正是您的老师所演示的。以下所有陈述均完全正确-正确!- C,并在编译时将不会遇到任何问题上运行,即使没有指针需要被初始化的,因此它们所包含的值可以未定义

  • 为了清楚起见,我们只是在result 明确地进行计算,并打印出来以迫使编译器计算本来会多余的,无效的代码。
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(读取:未正确初始化)时,该程序的格式不正确,但这与我们的讨论部分完全无关。这些代码段以及以下语句,即使涉及到的任何指针都具有IN有效性,“标准”保证它们 可以完美地编译运行

仅当取消引用无效的指针时,才会出现问题。当我们要求弗兰克在无效,不存在的地址取货或送货时。

给定任意指针:

int *p;

虽然此语句必须编译并运行:

printf(“%p”, p);

...必须如此:

size_t foo( int *p ) { return (size_t)p; }

...以下两个形成鲜明对比,还是会很容易编译,但不能在执行,除非指针有效的 -由我们在这里仅仅意味着它引用到本申请已被授予访问的地址

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

变化有多微妙​​?区别在于指针的值即地址)与内容(该数字处的房子)的值之间的差异。直到指针被取消引用才出现问题。直到尝试访问它链接的地址为止。尝试在路途之外交付或领取包裹时...

推而广之,同样的原则一定适用于更复杂的例子,包括前面提到的需要,以建立必要的有效性:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

关系比较和算术为测试等效性提供相同的效用,并且在原理上等效。但是,这种计算的结果意味着什么,完全是另一回事-恰恰是您所引用的报价所解决的问题。

在C语言中,数组是连续的缓冲区,即存储位置的不间断线性序列。应用于引用此类奇异序列中的位置的指针的比较和算术是自然的,并且显然彼此之间以及与此“数组”(由基数简单标识)之间都有意义。确切地说,适用于通过malloc或分配的每个块sbrk因为这些关系是隐式的,所以编译器能够在它们之间建立有效的关系,因此可以确信计算将提供预期的答案。

对引用不同块或数组的指针执行类似的操作不会提供任何此类固有的明显的实用性。更重要的是,由于某一时刻存在的任何关系都可能因随后的重新分配而无效,其中重新分配的可能性很大,甚至被颠倒了。在这种情况下,编译器无法获得必要的信息以建立对先前情况的信心。

,但是,作为一个程序员,可能有这样的知识!在某些情况下,必须加以利用。

ARE,因此,在何种情况下连这完全是VALID和完美PROPER。

事实上,这是究竟什么malloc本身具有当时间来尝试合并回收块内部完成-在绝大多数架构。对于操作系统分配器也是如此,就像后面的那样sbrk;如果更明显频繁地更分散的实体上(更关键的是),并且在malloc可能不是这样的平台上也很相关。其中有多少不是用C编写的?

行动的有效性,安全性和成功不可避免地是前提和应用的洞察力水平的结果。

在您提供的报价中,克尼根(Kernighan)和里奇(Ritchie)正在解决一个密切相关的问题,但仍是单独的问题。他们定义限制的的语言,并解释你可以如何利用编译器的能力至少检测潜在错误的构造来保护你。他们描述了该机制能够(旨在设计)以帮助您完成编程任务的长度。编译器是您的仆人,大师。然而,一个明智的主人是一个非常熟悉他各种仆人能力的人。

在这种情况下,不确定的行为用来表明潜在的危险和伤害的可能性;不要暗示即将到来,不可逆转的厄运,或者我们所知道的世界末日。它只是意味着我们 -“意味着编译器”- 无法对这件事可能是什么或代表什么做出任何猜想,因此我们选择洗手。对于因使用或滥用此工具而引起的任何意外事故,我们概不负责

实际上,它只是说:“ 牛仔,超越这一点:你是一个人……”

您的教授正在设法向您证明更细微的差别

注意他们在制作榜样时非常注意;以及它仍然有多。通过输入的地址,a

p[0].p0 = &a;

编译器被迫为变量分配实际存储空间,而不是将其放置在寄存器中。它是一个自动变量,但是,程序员无法控制在何处分配变量,因此无法对将要遵循的变量做出任何有效的推测。这就是为什么a 必须将其设置为零以使代码按预期工作的原因。

仅更改此行:

char a = 0;

对此:

char a = 1;  // or ANY other value than 0

导致程序的行为变得不确定。至少,第一个答案现在是1;但问题更加严重。

现在,该代码正在引发灾难。

尽管它仍然是完全有效的,甚至符合标准,但它现在还是正确的格式,尽管可以编译,但可能由于各种原因而无法执行。现在有多个问题- 没有它的编译器能够认识。

strcpy将开始于地址a,然后超出该地址以逐个字节消耗(并传输),直到遇到空值为止。

p1指针被初始化至正好一个块10个字节。

  • 如果a碰巧将其放置在块的末尾,并且该进程无法访问其后的内容,则p0 [1]的下一个读取将引发段错误。在x86架构上不太可能出现这种情况,但有可能。

  • 如果a 可以访问地址之外的区域,不会发生读取错误,但仍不会因不幸而保存程序。

  • 如果在从的地址开始的十个字节中发生了一个零字节a,它可能仍然存在,因为strcpy它将停止并且至少我们不会遭受写冲突。

  • 如果没有错误读取错误,但在10的跨度中没有零字节出现,strcpy它将继续并尝试写入超出分配的块malloc

    • 如果该进程不属于该区域,则应立即触发段故障。

    • 在更灾难性的-而微妙 ---的情况出现时,以下块该进程拥有的,因为那时的错误无法被检测到,没有信号可以被提高,所以它可能“出现”仍然“工作”,而实际上它将覆盖其他数据,分配器的管理结构,甚至是代码(在某些操作环境中)。

这就是为什么指针相关的错误可以这么辛苦跟踪。想象一下,这些行深埋在其他人编写的成千上万行复杂相关的代码中,您被指示深入研究。

尽管如此,该程序仍必须编译,因为它仍然完全有效符合标准 C。

这些错误,没有标准的没有编译器可以防止不必要的麻烦。我想这正是他们打算教您的。

偏执狂的人们不断寻求改变 C 的性质以处理这些有问题的可能性,从而使我们脱离自我。但这是不诚实的。这是责任,我们有义务接受当我们选择追求的动力和获得自由的是更直接,更全面的控制,该机提供了我们。追求完美的推动者和追求者将永远接受。

可移植性及其所代表的通用是一个根本上独立的考虑因素,也是该标准旨在解决的所有问题:

本文档指定了形式,并建立了以编程语言C表示的程序的解释。其目的促进 C语言程序在各种计算系统上的可移植性,可靠性,可维护性和有效执行。

这就是为什么它是完全正确的,以保持它独特的定义技术规范的语言本身。与许多人所认为的相反普遍性例外榜样相反的

结论:

  • 检查和操纵指针本身总是有效的,而且通常是富有成果的。结果的解释可能有意义,也可能没有意义,但是直到指针被取消引用之前,才不会引发灾难。直到尝试访问链接到的地址。

如果这不是真的,那么就不可能进行我们所知道的编程并热爱它


3
不幸的是,这个答案本质上是无效的。您无法对未定义的行为做出任何推理。比较不需要在机器级别进行。
安蒂·哈帕拉

6
吉,实际上没有。如果您查看C11附录J和6.5.8,比较的行为本身就是UB。取消引用是一个单独的问题。
paxdiablo

6
不,即使在取消引用指针之前,UB仍然可能有害。编译器可以自由地将UB的功能完全优化为单个NOP,即使这显然改变了可见的行为。
nanofarad

2
@Ghii,附件J(我提到的位)是未定义行为的列表,因此,我不确定如何支持您的论点:-) 6.5.8明确将比较称为UB。对于您对supercat的评论,在打印指针时没有进行比较,因此您很可能不会崩溃。但这不是OP的要求。3.4.3这也是您应该查看的部分:它将UB定义为“本国际标准对此没有规定”的行为。
paxdiablo

3
@GhiiVelte,尽管有人向您指出了错误,但您仍在陈述明显错误的内容。是的,您发布的代码段必须可以编译,但是您认为该代码段运行顺利的争论是不正确的。我建议您实际阅读该标准,特别是(在这种情况下)C11 6.5.6/9,请记住,单词“ shall”表示需求L “当减去两个指针时,两个指针都应指向同一数组对象的元素,或者指向最后一个数组对象的元素数组对象的元素”。
paxdiablo

-5

指针只是整数,就像计算机中的所有其他内容一样。您绝对可以将它们与<和进行比较,>并产生结果而不会导致程序崩溃。也就是说,该标准不能保证这些结果在数组比较之外没有任何意义

在您的堆栈分配变量示例中,编译器可以自由选择将这些变量分配给寄存器或堆栈存储器地址,并可以选择任意顺序。比较如<>因此将不会在编译器或体系结构是一致的。然而,==!=不那么受限制,比较指针平等是一个有效的和有用的操作。


2
在C11标准中,单词堆栈出现的次数恰好为零。未定义的行为意味着任何事情都可能发生(包括程序崩溃)。
paxdiablo

1
@paxdiablo我说了吗?
Nickpro

2
您提到了堆栈分配的变量。标准中没有堆栈,这只是实现细节。这个答案最严重的问题是争用您可以比较指针而没有崩溃的机会-这是错误的。
paxdiablo

1
@nickelpro:如果希望编写与gcc和clang中的优化程序兼容的代码,则有必要跳过很多愚蠢的事情。只要有任何方法可以扭曲标准以证明它们是合理的(甚至有时没有),这两个优化器就会积极地寻找机会推断出指针将访问哪些内容。给定int x[10],y[10],*p;,如果代码求值y[0],则求值p>(x+5)并且在过渡期间*p不做任何修改就写入p,最后y[0]再次求值,...
supercat

1
Nickelpro,同意同意不同意,但是您的回答仍然是根本错误的。我将您的方法比作使用人员的方法,(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')而不是isalpha()因为什么明智的实现会使这些字符不连续?底线是,即使您没有实施知道都存在问题,但如果您重视可移植性,就应该尽可能按照标准进行编码。我非常感谢标签“ standards maven”,谢谢。我可能会加入我的简历:-)
paxdiablo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.