C不检查指针是否检查指针是否超出范围?
我和一些人说过,即使C没有被解除引用,C的out-of-bound指针也会导致未定义的行为。 例:
int a; int *p = &a; p = p - 1;
这里的第三行会导致未定义的行为,即使p
永远不会被解除引用( *p
永远不会被使用)。
在我看来,如果没有使用指针,C会检查指针是否超出范围,这听起来是不合逻辑的(就像有人会检查街上的人看他们是否携带枪支,以防他们进入他的房子。那些理想的事情就是在人们进入房子的时候检查他们)。 我认为,如果C检查,那么会发生很多运行时间的开销。
另外,如果C真的检查OOB指针,那么为什么这不会导致UB:
int *p; // uninitialized thus pointing to a random adress
在这种情况下,即使指向OOB地址的机会很高,为什么什么也不会发生。
加:
int a; int *p = &a; p = p - 1;
说&a
是1000.评估第三行后p
的值是:
- 996,但仍然是未定义的行为,因为
p
可能在其他地方被解引用并导致真正的问题。 - 未定义的值 ,这是未定义的行为。
因为我认为“第三行被称为”未定义的行为“首先是因为将来可能会使用OOB指针(取消引用)和人,随着时间的推移,它将其作为一个未定义的行为。 现在, p
的值是100%还是996 ,还有未定义的行为或者它的值是不确定的?
C不检查指针是否超出范围 。 但是当计算出的地址超出对象边界时,底层硬件可能会以奇怪的方式运行,只是在对象结束之后出现exception。 C标准明确地将其描述为导致未定义的行为。
对于大多数当前的环境,上面的代码不会造成问题,但类似的情况可能会在25年前的x86 16位保护模式下导致分段错误。
在标准语言中,这样的一个值可能是一个陷阱值 ,这个值在不调用未定义的行为的情况下是不能被操纵的。
C11标准的相关部分是:
6.5.6添加操作符
- 当具有整数types的expression式被添加到指针或从指针中减去时,结果具有指针操作数的types。 如果指针操作数指向数组对象的一个元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果数组元素和原始数组元素的下标之差等于整数expression式。 […]如果指针操作数和结果指向同一个数组对象的元素,或者一个超过了数组对象的最后一个元素,则评估不会产生溢出; 否则,行为是不确定的。 如果结果指向一个超过数组对象的最后一个元素,则不应将其用作所评估的一元运算符的操作数。
未定义行为的一个类似的例子是:
char *p; char *q = p;
仅仅加载未初始化的指针p
的值将调用未定义的行为,即使它从不被解除引用。
编辑:这是一个争议点争论。 标准说计算这样的地址调用未定义的行为,所以它。 一些实现可能只是计算一些值并存储它的事实是不相关的。 不要依赖任何有关未定义行为的假设:编译器可能会利用其固有的不可预测性来执行您无法想象的优化。
例如这个循环:
for (int i = 1; i != 0; i++) { ... }
如果i
是INT_MAX
, i++
调用未定义的行为,所以编译器的分析是这样的:
-
i
初始值是> 0
。 - 对于
i < INT_MAX
任何正值,i++
仍然> 0
- 对于
i = INT_MAX
,i++
调用未定义的行为,所以我们可以假设i > 0
因为我们可以假设任何我们i = INT_MAX
。
因此, i
总是> 0
,testing代码可以被删除。
事实上,如果C程序试图通过指针运算来计算一个值,那么C程序的行为是不确定的,它不会导致指向元素的指针,也不会导致相同数组元素的末尾。 从C11 6.5.6 / 8:
如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不会产生溢出; 否则,行为是不确定的。
(为了描述的目的,typesT
的对象的地址可以被看作是数组T[1]
的第一个元素的地址。)
为了澄清,“未定义的行为”意味着代码的结果在pipe理该语言的标准中没有定义。 实际的结果取决于编译器的实现方式,可以从无到有,甚至完全崩溃。
标准没有规定指针的任何范围检查应该发生。 但是就你的具体例子而言,这就是他们所说的:
当具有整数types的expression式被添加到指针或从指针中减去…如果指针操作数和结果指向相同数组对象的元素,或者指向数组对象的最后一个元素之后,则评估不应该产生溢出; 否则,行为是不确定的。 如果结果指向一个超过数组对象的最后一个元素,则不应将其用作所评估的一元运算符的操作数。
以上引用来自C99§6.5.6第8段(我手边有最新版本)。
请注意,上面也适用于非数组指针,因为在前面的子句中它说:
对于这些运算符而言,指向不是数组元素的对象的指针的行为与以对象types作为其元素types的长度为1的数组的第一个元素的指针相同。
所以,如果你执行指针运算,并且结果是在边界内,或者指向对象的末尾,那么你将得到一个有效的结果,否则你会得到未定义的行为。 这种行为可能是你最终得到一个stream浪的指针,但它可能是别的东西。
是的,即使指针未被解除引用,也是未定义的行为。
C只允许指针指向只有一个元素通过数组边界 。
当规范说什么东西是不确定的,这可能是相当混乱。
这意味着,在这种情况下,规范的实施可以自由地做任何事情。 在某些情况下,它会做出一些看似直观,正确的事情。 在其他情况下,它不会。
对于地址边界的规范,我知道我的直觉来自我对统一的统一内存模型的假设。 但是还有其他的内存模型。
“未定义”一词从来不会出现在完整的规范中。 当标准委员会知道标准的不同实现需要做不同的事情时,通常会决定使用这个词。 在很多情况下,不同的事情的原因是性能。 所以:规范中单词的出现是一个红旗警告 ,对于我们这些凡人,规范的使用者,我们的直觉可能是错误的。
几年前,这种“无论如何”的规范都令公司感到恼火。 所以,当他遇到一些未定义的东西时,他让Gnu Compiler Collection(gcc)的某些版本尝试玩电脑游戏。
IBM在360/370天内在其规格中使用了不可预测的字眼。 这是一个更好的词。 这使得结果听起来更随机,更危险。 在“不可预知”的行为范围内,存在“停火”等问题。
虽然这是事情。 “随机”是描述这种不可预知的行为的一种不好的方法,因为“随机”意味着系统在每次遇到问题时可能会做出不同的事情。 如果每次都做一些不同的事情,你就有机会在testing中发现问题。 在“未定义”/“不可预知”行为的世界里,系统每次都会做同样的事情, 直到没有。 而且,你知道在你认为你已经完成testing你的东西之后的时间不会有多less年。
所以,当规范说什么是不明确的时候,不要这样做。 除非你是墨菲的朋友。 好?
“未定义的行为”意味着“任何事情都可能发生”。 “任何事情”的共同价值是“没有什么不好的事情发生”和“你的代码崩溃”。 “任何事情”的其他常见值是“优化时发生坏事”,或者“在开发中不运行代码但客户正在运行时发生坏事”,而其他值则是“您的代码做一些意想不到的事情“,”你的代码做了一些它不应该做的事情“。
所以,如果你说“C不检查指针是否超出了指针的使用范围,那么C就不合逻辑了”,那么你将处于非常非常危险的境地。 拿这个代码:
int a = 0; int b [2] = { 1, 2 }; int* p = &a; p - 1; printf ("%d\n", *p);
编译器可以假定没有未定义的行为。 评估p – 1。 编译器(在法律上)总结(p =&a [1],p =&b [1]或p =&b [2]),因为在所有其他情况下,评估p或评估p-1时都存在未定义的行为。 然后编译器假定* p不是未定义的行为,所以它(在法律上)得出p =&b [1]并打印出值2.你没有想到,是吗?
这是合法的, 而且会发生 。 所以教训是:不要调用未定义的行为。
有些平台将指针视为整数,并以与整数算术相同的方式来处理指针算术,但根据对象的大小按一定的比例放大或缩小。 在这样的平台上,这将有效地定义所有指针算术运算的“自然”结果,除了差值不是指针目标types大小倍数的指针的减法。
其他平台可能以其他方式表示指针,并且添加或减去某些指针组合可能会导致不可预知的结果。
C标准的作者并不希望对任何一种平台都performance出偏袒,所以如果指针在某些平台上会导致问题,就不会有什么要求。 在C标准出台之前,以及几年之后,程序员可以合理地预期,处理指针运算(如缩放整数运算)的平台的通用实现本身也将处理指针运算,而处理指针运算的平台可能会以不同的方式对待自己。
然而,在过去十年左右,为了追求“优化”,编译器作者决定把“最小惊讶原则”抛出窗外。 即使在程序员知道某个指针操作的效果会被赋予一个平台的自然指针表示的情况下,也不能保证编译器会生成那些performance自然指针表示行为的代码。 标准规定行为未定义的事实被解释为要求编译器实施强制程序员编写代码的“优化”,这种代码比仅仅以与文档一致的方式实现的实现要慢而笨重潜在环境的行为(C89的作者明确指出的三种处理之一是常见的)。
因此,除非知道一个人正在使用一个没有任何古怪的“优化”的编译器,否则指针计算序列中的一个中间步骤调用Undefined Behavior会导致无法推理,无论常识有多强烈意味着一个特定平台的质量实现应该有一个特定的方式。
问题中涉及未定义行为的部分非常明确,答案是“是的,当然这是不确定的行为”。
我将把“请检查…”这个字眼解释为以下两点:
- C编译器检查…?
- 我的编译程序检查…?
(C本身是一个语言规范,它不检查或做任何事情)
第一个问题的答案是:是的,但不可靠,而不是你想的方式。 现代编译器相当聪明,有时比你想要的更聪明。 在某些情况下,编译器将能够诊断您非法使用指针。 由于每个定义调用未定义的行为,因此语言不再需要编译器特别做任何事情,编译器通常会以不可预知的方式进行优化。 这可能会导致代码与您最初的意图大不相同。 如果整个范围甚至完整的function都被剥离,不要感到惊讶。 对于未定义的行为,许多不希望的“惊喜优化”也是如此。
强制性阅读: 每个C程序员应该知道关于未定义的行为 。
对第二个问题的回答是:否,除非您使用支持边界检查的编译器,并且在启用了运行时边界检查的情况下进行编译,这意味着相当不重要的运行时间开销。
在实践中,这意味着如果你的程序“幸免于难”,编译器会优化未定义的行为,那么它将会顽固地做你所说的事情,带有不可预知的结果 – 通常是垃圾值被读取,或者你的程序导致分割故障。
但是什么是未定义的行为? 这仅仅意味着没有人愿意说出会发生什么。
我从几年前就已经是一台老式的大型机了,我也喜欢IBM的这句话: 结果是不可预测的 。
顺便说一句:我喜欢不检查数组边界的想法。 例如,如果我有一个指向string的指针,并且想要查看指向该字节之前的内容,则可以使用:
pointer[-1]
看看它。