多么挑衅的问题!
即使在这个线程的答复和意见的粗略扫描将揭示如何情感你看似简单和直接的查询结果是。
不足为奇。
毫无疑问,对指针的概念和使用的误解是导致总体编程严重失败的主要原因。
在专门设计用于解决(最好避免指针完全引入的挑战)的语言的普遍存在下,很容易认识到这一现实。可以将C ++以及C,Java及其关系的其他派生类,Python和其他脚本视为仅仅是更突出和普遍的脚本,并且在处理该问题的严重性上或多或少地有序。
因此,对每一个渴望在编程方面追求卓越的个人(尤其是在系统级别上)都必须与他们更深入地了解其基本原理。
我想这正是您的老师想证明的意思。
C的性质使其成为进行此探索的便捷工具。比基于汇编的语言不够清晰-尽管也许更容易理解-但仍比基于对执行环境的更深抽象的语言更清晰。
C是一种系统级语言,旨在促进将程序员的意图确定性地转换为机器可以理解的指令。虽然被归类为高级,但实际上属于“中等”类别。但是由于不存在这种情况,因此“系统”名称必须足够。
该特性是用于使得一个主要原因选择的语言对的设备驱动程序,操作系统代码,和嵌入的实现。此外,在最佳效率至关重要的应用中,当之无愧的替代方案;这意味着生存与灭绝之间的差异,因此与奢侈品相对比,这是必需的。在这种情况下,便携性的吸引人的便利性失去了所有吸引力,而选择最小公分母的低调性能成为不可思议的有害选择。
是什么让Ç -和它的一些衍生物-比较特别的是,它允许其用户完全控制-当是他们的愿望-不强加相关的责任在他们身上时,他们没有。然而,它永远不会提供超过最薄绝缘的从机,因此正确使用要求苛刻的理解概念的指针。
从本质上讲,对于您的问题的回答非常简单,令人满意,这是对您怀疑的肯定。提供的,但是,一个十分必要的意义,以每一个概念在此声明:
- 检查,比较和操纵指针的行为始终且必然是有效的,而从结果得出的结论取决于所包含的值的有效性,因此不一定如此。
前者总是 安全的,并且可能是 适当的,而后者只有在被确定为安全时才是适当的。令人惊讶的是-对于某些人-因此确定后者的有效性取决于并要求前者。
当然,部分混淆是由于指针原理中固有的递归效果所致,以及将内容与地址区分开带来的挑战。
您已经正确地推测出
我被认为可以将任何指针与任何其他指针进行比较,无论它们分别指向何处。而且,我认为两个指针之间的指针算法都很好,无论它们分别指向何处,因为该算法只是使用指针存储的内存地址。
并且有几个贡献者确认:指针只是数字。有时有些东西更接近于复数,但仍然不超过数字。
在这里引起了争论的有趣争论,比编程更能说明人性,但仍然值得一提。也许我们以后再做...
随着一则评论开始暗示;所有这些困惑和惊from源自需要从安全的东西中辨别出什么是有效的,但这过于简单了。我们还必须区分什么是功能性的,什么是可靠的,什么是实用的以及什么是适当的,并且还要进一步:在特定情况下,什么是适当的,从更一般的意义上来说,什么是适当的。更何况; 合格与礼节之间的区别。
为了实现这一目标,我们首先需要明白恰恰是什么指针 是。
- 您已经牢牢掌握了这个概念,并且像其他一些插图一样,可能会发现这些插图过于简单化,但此处明显的混乱程度要求澄清起来要如此简单。
正如一些人指出的那样:术语“ 指针”只是一个特殊的名称,仅仅是一个索引,因此无非是任何其他数字。
这应该已经是不言而喻的考虑的事实,所有当下主流计算机都是二进制的机器是必然的工作完全与和数字。量子计算可能会改变这种情况,但这不太可能,而且还没有成熟。
正如您已经注意到的,从技术上讲,指针是更准确的地址。一个显而易见的见解自然会引入有益的类比,即将它们与房屋的“地址”或街道上的地块相关联。
让我们进一步扭转,将难题变成如此迷人的纠结。在上面,为了简单和清楚起见,建议将指针作为地址是很方便的。当然,这是不正确的。指针是不的地址; 指针是对地址的引用,它包含一个地址。像信封一样,是对房屋的参考。考虑到这一点,您可能会发现概念中包含递归建议的含义。仍然; 我们只有那么多的单词,并且在谈论引用地址的地址这样,很快就会使大多数人陷入无效的操作码异常的境地。在大多数情况下,意图很容易从上下文中获得,因此让我们回到街头。
在我们这个虚构的城市中,邮政工作人员与我们在“真实”世界中发现的工作人员非常相似。当你没有一个很可能患中风谈论或询问关于无效的地址,但是当你问他们每个最后一个将不惜采取行动的信息。
假设在我们的单一街道上只有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语言程序在各种计算系统上的可移植性,可靠性,可维护性和有效执行。
这就是为什么它是完全正确的,以保持它独特的自定义和技术规范的语言本身。与许多人所认为的相反,普遍性与例外和榜样是相反的。
结论:
- 检查和操纵指针本身总是有效的,而且通常是富有成果的。结果的解释可能有意义,也可能没有意义,但是直到指针被取消引用之前,才不会引发灾难。直到尝试访问链接到的地址。
如果这不是真的,那么就不可能进行我们所知道的编程并热爱它。
C
内容和安全的内容C
。比较两个相同类型的指针总是可以完成的(例如,检查是否相等),但是要使用指针算术和比较>
,<
并且只有在给定数组(或内存块)中使用时才是安全的。