遇到C的常见未定义/未指定行为是什么?[关闭]


69

C语言中未指定行为的一个示例是对函数自变量的求值顺序。您可能不知道它可能是从左到右或从右到左。这将如何影响foo(c++, c)foo(++c, c)获取评估。

还有哪些其他未说明的行为可能会使无意识的程序员感到惊讶?


4
foo(c++, c)foo(++c, c)都是未定义的行为,这完全胜过未指定的行为
Pascal Cuoq

Answers:


78

语言律师的问题。嗯

我的个人top3:

  1. 违反严格的别名规则

  2. 违反严格的别名规则

  3. 违反严格的别名规则

    :-)

编辑这是一个小示例,它两次出错:

(假设32位整数和小尾数)

该代码试图通过在浮点数表示中直接与符号位进行位旋转来获取浮点数的绝对值。

但是,通过从一种类型转换为另一种类型来创建指向对象的指针的结果不是有效的C。编译器可能会假定指向不同类型的指针没有指向同一块内存。这对于除void *和char *之外的所有类型的指针都是正确的(符号无关紧要)。

在上面的例子中,我做了两次。一次获取float的整数别名,一次将值转换回float。

有三种有效的方法可以做到这一点。

在转换过程中使用char或void指针。这些总是别名,因此是安全的。

使用内存复制。Memcpy使用void指针,因此也会强制使用别名。

第三种有效方式:使用联合。从C99开始,这显然不是未定义的:


这听起来很有趣...您能扩展吗?
Benoit

aehm。我提到我假设32位整数和小尾数。顺便说一句-联合用法仍然是未定义的行为,不是因为IEEE位表示,而是因为(理论上)不允许您写入字段f和从字段i中读取。
尼尔斯·派宾布里克

一个人,即使实现使用ieee,它也是未定义的行为。关键是它从最后写入的另一个成员读取。
Johannes Schaub-litb

csci.csusb.edu/dick/c++std/cd2/basic.html#basic.lval项目符号15似乎暗示通过联合修剪的类型是安全的。c标准中的措词是相同的。
格雷格·罗杰斯

2
C99标准允许通过联合进行类型修剪;请参见脚注82,该脚注已与TC3一起添加:“如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分按照6.2.6中的描述(一种有时称为“类型调整”的过程)将“对象”重新解释为新类型的对象表示形式。这可能是陷阱表示形式。”
Christoph

31

我个人最喜欢的未定义行为是,如果非空源文件未以换行符结尾,则行为未定义。

我怀疑这是真的,尽管我不会见过任何编译器会根据是否以换行符结尾来区别源文件,而不是发出警告。因此,除了警告可能会令他们惊讶之外,这实际上不会使不知情的程序员感到惊讶。

因此,对于真正的可移植性问题(主要是依赖于实现的,而不是未指定或未定义的,但我认为这属于问题的实质):

  • char不一定(未)签名。
  • int可以是16位中的任意大小。
  • 浮点数不一定是IEEE格式或一致的。
  • 整数类型不一定是二进制补码,并且整数算术溢出会导致不确定的行为(现代硬件不会崩溃,但是某些编译器优化会导致行为不同于环绕操作,即使这是硬件所做的操作。例如,if (x+1 < x)可能会始终错误地进行优化何时x签署类型:请参阅-fstrict-overflowGCC中的选项)。
  • “ /”,“。” #include中的“ ..”和“ ..”没有定义的含义,可以由不同的编译器以不同的方式处理(这实际上有所不同,如果出错,将会毁了您的一天)。

真正严重的行为,甚至在您开发的平台上也可能令人惊讶,因为行为只是部分未定义/未指定:

  • POSIX线程和ANSI内存模型。并发访问内存的定义不如新手想象的那样。volatile不会像新手那样做。内存访问的顺序没有新手认为的那样明确。可以在某些方向跨存储屏障移动访问。不需要内存缓存一致性。

  • 分析代码并不像您想的那么容易。如果您的测试循环无效,则编译器可以删除其中的一部分或全部。内联没有定义的效果。

而且,正如我认为Nils提到的那样:

  • 违反严格的别名规则。

史蒂夫(Steve)-我在上世纪90年代初遇到了您所说的(结束换行问题),当时带有用于68K系列的Microtec编译器。我以为该工具有问题,但是我只是添加了换行符“以解决愚蠢的工具”。与我过分自信的同事(请参阅我对此主题的其他评论)不同,我不是很自信,我会写一份缺陷报告……好东西我没有。

1
未定义的有符号整数溢出不只是脚手架。至少GCC会假设它永远不会发生而应用优化,例如“ if(a + 1> a)”总是通过并且从未检测到环绕。
BCoates 2011年

@BCoates:整数溢出产生部分不确定的值对我没有问题,这在说明的情况下足以证明GCC的优化是合理的。不幸的是,一些编译器作者似乎认为整数溢出应该否定时间和因果关系的定律(如果代码在假定不会溢出的情况下重新排序,我可能可以忍受的时间;对因果关系的否定应被视为恕我直言,但not,不是每个人都同意。)
supercat 2015年

22

用指向某物的指针划分某物。只是由于某种原因不会编译... :-)

result = x/*y;

1
哈哈,一个不错的人,我正在写下它:-)
Drealmer's

因为威胁“ / *”作为注释,所以只需在“ /”和“ *”之间添加一个空格即可,它应该可以正常工作(至少在我的gcc 8.1.1上有效)。
Charles Gueunet '18

1
难以置信的搞笑和错误答案是什么?它根本无法回答问题,并提出错误的C代码假设。给定的代码是语法错误。它与未定义的行为无关。-也许您打算将基本类型的值除以指针值,但这不是您所显示的。用除引用的指针除以基本类型的值是不正确的,例如:double x = 2; int z = 1, *y; y = &z; int result = x / *y;-答案需要彻底编辑或紧急删除。-1
RobertS支持Monica Cellio '20年

21

我最喜欢的是:

为了回答一些意见,根据标准,它是未定义的行为。看到这一点,编译器就可以做任何事情,包括格式化硬盘。例如,在此处查看此评论。关键不是您可以看到对某些行为的合理预期。由于C ++标准和序列点的定义方式,这一行代码实际上是未定义的行为。

例如,如果我们x = 1在上面的行之前,那么之后的有效结果是什么?有人评论说应该

x增加1

因此我们之后应该看到x == 2。但是,实际上并非如此,您将发现某些编译器之后的x == 1,甚至x ==3。您必须仔细查看生成的程序集,以了解可能的原因,但是差异是由于潜在的问题。本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此它可以执行第x++一个或第x =一个。


17
在标准C和C ++中,在两个序列点之间多次修改变量被明确声明为未定义行为。
KTC

11
我现在正在开怀大笑,因为有人想到编写一个C编译器,当看到x = x ++时会格式化硬盘,因为它在标准中是未定义的:-)
dancavallaro

4
+1,特别是对于“格式化硬盘部分”。实际上,对于像这样编写代码的人,格式化硬盘可能会为后代的维护程序员节省很多
麻烦

1
两件事:1)这是绝对未定义的行为;大约15年前,我与小组中的某人进行了辩论,当时他向编译器供应商写了一个缺陷报告(很喜欢!),当时他编写了这个确切的代码(除了他使用“ i”代替“ x”),并且“ i”停留在1; 2)当我阅读有关格式化硬盘的部分时,我笑了,可能是因为这也是我要说的。

1
我会说x是递增的,然后赋值为它的先前值,因为x ++返回该值并且优先于赋值。但是,是的,它是不确定的...语言中的许多事情(令人头疼...)
Calmarius 2011年

10

我遇到的另一个问题(已定义,但绝对是意外的)。

炭是邪恶的。

  • 有符号的还是无符号的,取决于编译器的感觉
  • 强制为8位

3
好吧,如果您将其用于含义即字符即不是邪恶的……
sleske

2
事实上,有三种不同类型的字符组成:charunsigned charsigned char。它们是明确不同的类型。
Lstor 2012年

必须使用(指针或普通的数组)char处理字符串的时候。许多标准库函数(像所有str *()函数一样)都使用了指向char的指针,而给它们提供任何其他内容都需要进行丑陋的强制转换。
詹斯(Jens)2013年

1
谁对弦说什么?嵌入式程序员有时为了提高效率而玩可变大小的游戏。假设关于char的任何事情都无法跨平台工作。调用针对字符串的库函数,但是在字符串仅仅是char *且尚未发明Unicode时定义库函数可能是可以的,但是如果我直言不讳……不编写至少支持Unicode字符的程序是愚蠢的
itj 2013年

8

我无法计算已更正printf格式说明符以匹配其参数的次数。任何不匹配都是不确定的行为

  • 不可以,您不得将int(或long)传递给%x-unsigned int必填
  • 不,您不能将传递unsigned int%d-int是必填项
  • 不,您不得将或传递size_t给-%u%d%zu
  • 不可以,您不得使用%d%x-打印指针%p并将其强制转换为void *

4
该标准暗示(在非规范性脚注中),只要值在两种类型的范围内,就可以传递intto%xunsigned intto to %d。尽管如此,我还是希望避免这种情况。
基思·汤普森

7

如果函数原型不可用,则编译器不必告诉您您正在使用错误的参数数量/错误的参数类型来调用函数。


是。仁慈的编译器通常会为您提供警告...
sleske

从C99开始,调用没有可见声明的函数需要进行诊断。该声明并不具备成为一个原型(即声明指定类型的参数),但它始终应该是。(像这样的可变函数printf仍然会出现问题。)
Keith Thompson

7

我已经看到许多相对缺乏经验的程序员被多字符常量所咬住。

这个:

是一个字符串文字(其类型为,char[2]并衰减为char*在大多数情况下)。

这个:

是一个普通字符常量(出于历史原因,它是类型 int)。

这个:

也是一个完全合法的字符常量,但是其值(仍为类型int)是实现定义的。这是一种几乎没有用的语言功能,主要用于引起混乱。


在Macintosh上编写C时很有用,它经常使用32位整数来保存四个字符的文件类型,应用程序签名等,尽管三字母组合会令人讨厌'????'
supercat 2015年

这与采取重载函数特别危险char*以及char。我见过很多人被它咬伤(例如
rustyx 16/09/3

4
问题是关于C,而不是C ++。没有重载函数。
基思·汤普森

4

不久前,Clang开发人员发布了一些很棒的示例,每个C程序员都应该阅读该示例。一些以前没有提到的有趣的东西:

  • 有符号整数溢出-不可以将有符号变量包装成超过其最大值。
  • 取消引用NULL指针-是的,这是未定义的,可能会被忽略,请参阅链接的第2部分。


1

使用变量之前,请务必始终对其进行初始化!当我刚开始使用C时,这使我头痛不已。

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.