C++程序员应该知道的所有常见未定义行为是什么?[关闭]

What are all the common undefined behaviours that a C++ programmer should know about? [closed]

提问人:yesraaj 提问时间:12/15/2008 最后编辑:Peter Mortensenyesraaj 更新时间:1/23/2014 访问量:83566

问:

已锁定。这个问题及其答案被锁定,因为这个问题偏离了主题,但具有历史意义。它目前不接受新的答案或交互。

C++程序员应该知道的所有常见未定义行为是什么?

比如说:

a[i] = i++;

C 未定义 行为 C++-FAQ

评论

3赞 Martin York 12/15/2008
是否确定。这看起来很明确。
17赞 yesraaj 12/15/2008
6.2.2 C++ 编程语言中的评估顺序 [expr.evaluation] 是这样说的。我没有任何其他参考资料
4赞 dancavallaro 12/15/2008
他是对的。。刚刚在 C++ 编程语言中查看了 6.2.2,它说 v[i] = i++ 未定义
4赞 Evan Teran 12/15/2008
我会想象,因为 comiler make 在计算 v[i] 的内存位置之前或之后执行 i++。当然,我总是会被分配到那里。但它可以写入 v[i] 或 v[i+1],具体取决于操作顺序。
2赞 dancavallaro 12/15/2008
C++ 编程语言所说的只是“表达式中子表达式的操作顺序是未定义的。特别是,你不能假设表达式是从左到右计算的。

答:

2赞 JaredPar 12/15/2008 #1

C++ 保证大小的唯一类型是 。大小为 1。所有其他类型的大小取决于平台。char

评论

0赞 Jasper Bekkers 12/15/2008
这不就是<cstdint>的用途吗?它定义了诸如uint16_6等类型。
0赞 JaredPar 12/15/2008
是的,但大多数类型的大小(比如长)都没有明确定义。
0赞 Evan Teran 12/15/2008
此外,CSTDINT 还不是当前 C++ 标准的一部分。请参阅 boost/stdint.hpp 了解当前可移植的解决方案。
0赞 Daniel Earwicker 12/15/2008
这不是未定义的行为。该标准说,符合标准的平台定义了尺寸,而不是标准定义了尺寸。
1赞 John Dibling 6/30/2012
@JaredPar:这是一篇复杂的文章,有很多对话线索,所以我在这里总结了一下。底线是这样的:“5.为了在二进制中表示 -2147483647 和 +2147483647,你需要 32 位。
232赞 18 revs, 10 users 33%Diomidis Spinellis #2

指针

  • 取消引用指针NULL
  • 取消引用大小为零的“新”分配返回的指针
  • 使用指向生存期已结束的对象的指针(例如,堆栈分配的对象或已删除的对象)
  • 取消引用尚未明确初始化的指针
  • 执行指针算术,在数组的边界(上方或下方)之外生成结果。
  • 在数组末尾以外的位置取消引用指针。
  • 将指针转换为不兼容类型的对象
  • 使用 memcpy 复制重叠的缓冲区

缓冲区溢出

  • 读取或写入偏移量为负或超出该对象大小的对象或数组(堆栈/堆溢出)

整数溢出

  • 有符号整数溢出
  • 计算未以数学方式定义的表达式
  • 向左移动值为负量(右移为负量由实现定义)
  • 将值移位大于或等于数字中的位数(例如 未定义)int64_t i = 1; i <<= 72

类型、演员表和常量

  • 将数值转换为无法由目标类型表示的值(直接或通过static_cast)
  • 在明确赋值之前使用自动变量(例如,int i; i++; cout << i;)
  • 使用接收信号以外的任何类型的对象的值,或在接收信号时volatilesig_atomic_t
  • 尝试在其生存期内修改字符串文本或任何其他 const 对象
  • 在预处理期间将窄字符串与宽字符串文本连接起来

功能与模板

  • 不从返回值的函数返回值(直接或通过从 try-block 流出)
  • 同一实体的多个不同定义(类、模板、枚举、内联函数、静态成员函数等)
  • 模板实例化中的无限递归
  • 使用不同的参数调用函数,或链接到该函数定义为使用的参数和链接。

哎呀

  • 具有静态存储持续时间的对象的级联销毁
  • 分配给部分重叠对象的结果
  • 在函数的静态对象初始化期间以递归方式重新输入函数
  • 从对象的构造函数或析构函数对对象的纯虚函数进行虚函数调用
  • 指尚未构造或已销毁的对象的非静态成员

源文件和预处理

  • 不以换行符结尾或以反斜杠结尾的非空源文件(C++11 之前)
  • 后跟字符或字符串常量中不属于指定转义码的字符的反斜杠(这是在 C++11 中定义的实现)。
  • 超出实现限制(嵌套块数、程序中的函数数、可用堆栈空间等)
  • 不能用long int
  • 类似函数的宏定义左侧的预处理指令
  • 在表达式中动态生成定义的标记#if

待分类

  • 在销毁具有静态存储持续时间的程序期间调用 exit

评论

0赞 new123456 4/5/2011
嗯......NaN (x / 0) 和 Infinity (0 / 0) 被 IEE 754 覆盖,如果 C++ 是后来设计的,为什么它会将 x / 0 记录为未定义?
0赞 Adam Rosenfield 6/7/2011
回复:“反斜杠后跟一个字符,该字符不属于字符或字符串常量中指定的转义码。这是 C89 (§3.1.3.4) 和 C++03 (包含 C89) 中的 UB,但不是 C99 中的 UB。C99 说“结果不是令牌,需要诊断”(§6.4.4.4)。大概C++0x(包含C89)将是相同的。
1赞 Steve Jessop 6/7/2011
C99 标准在附录 J.2 中列出了未定义的行为。将此列表调整为 C++ 需要一些工作。您必须更改对正确 C++ 子句而不是 C99 子句的引用,删除任何不相关的内容,并检查所有这些内容是否真的在 C++ 和 C 中未定义。但它提供了一个开始。
1赞 SecurityMatt 9/1/2012
@new123456 - 并非所有浮点单元都与 IEE754 兼容。如果 C++ 需要符合 IEE754,编译器将需要通过显式检查来测试和处理 RHS 为零的情况。通过使行为未定义,编译器可以通过说“如果您使用非 IEE754 FPU,您将不会获得IEEE754 FPU 行为”来避免这种开销。
1赞 nacitar sevaht 8/10/2013
“计算结果不在相应类型范围内的表达式”....整数溢出是为 UNSIGNED 整型定义明确的,但不是有符号的。
31赞 Martin York 12/15/2008 #3

函数参数的计算顺序是未指定的行为。(这不会使您的程序崩溃、爆炸或订购披萨......与未定义的行为不同。

唯一的要求是在调用函数之前必须完全评估所有参数。


这:

// The simple obvious one.
callFunc(getA(),getB());

可以等效于此:

int a = getA();
int b = getB();
callFunc(a,b);

或者这个:

int b = getB();
int a = getA();
callFunc(a,b);

它可以是;这取决于编译器。结果可能很重要,具体取决于副作用。

评论

23赞 Rob Kennedy 12/15/2008
顺序未指定,而不是未定义。
1赞 Robert Gould 12/15/2008
我讨厌这个:)一旦追踪到其中一个案例,我就失去了一天的工作......反正吸取了教训,幸好没有再跌倒
2赞 Martin York 12/15/2008
@Rob:我想和你争论一下这里的含义变化,但我知道标准委员会对这两个词的确切定义非常挑剔。所以我会改变它:-)
2赞 Bill the Lizard 12/16/2008
我很幸运。我在大学的时候被它咬了一口,有一位教授看了一眼它,并在大约 5 秒钟内告诉我我的问题。不知道否则我会浪费多少时间进行调试。
4赞 Martin York 12/15/2008 #4

变量在表达式中只能更新一次(从技术上讲,在序列点之间更新一次)。

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

评论

0赞 Prasoon Saurav 8/12/2010
事实上,在两个序列点之间至少存在一次。
2赞 Nawaz 6/7/2011
@Prasoon:我想你的意思是:在两个序列点之间最多一次。:-)
27赞 Martin York 12/15/2008 #5

编译器可以自由地对表达式的计算部分进行重新排序(假设含义保持不变)。

从原来的问题:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

双重检查锁定。 而且很容易犯一个错误。

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

评论

0赞 yesraaj 12/15/2008
序列点是什么意思?
0赞 Martin York 12/15/2008
en.wikipedia.org/wiki/Sequence_point
1赞 Tom 12/15/2008
哦。。。这很讨厌,特别是因为我已经看到了 Java 中推荐的确切结构
0赞 Eclipse 6/24/2009
请注意,某些编译器确实定义了这种情况下的行为。例如,在 VC++ 2005+ 中,如果 a 是易失性的,则设置所需的内存条以阻止指令重新排序,以便双重检查锁定起作用。
0赞 supercat 8/15/2010
Martin York:<i>// (c) 肯定会在 (a) 和 (b) 之后发生</i>是吗?诚然,在那个特定的例子中,唯一可能很重要的场景是,如果“i”是一个映射到硬件寄存器的易失性变量,并且a[i]('i'的旧值)被别名到它,但是否能保证增量会发生在序列点之前?
5赞 yesraaj 12/15/2008 #6

在剥离 ness 后使用以下命令赋值为常量:constconst_cast<>

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
5赞 Daniel Earwicker 12/15/2008 #7

我最喜欢的是“模板实例化中的无限递归”,因为我相信它是唯一一个在编译时发生未定义行为的。

评论

0赞 Robert Gould 12/15/2008
以前做过这个,但我不明白它是如何未定义的。很明显,你在事后进行了无限递归。
0赞 Daniel Earwicker 12/15/2008
问题在于编译器无法检查您的代码并准确决定它是否会遭受无限递归。这是停止问题的一个例子。请参阅:stackoverflow.com/questions/235984/...
0赞 Robert Gould 12/16/2008
是的,这绝对是一个停滞不前的问题
0赞 Johannes Schaub - litb 12/28/2008
它使我的系统崩溃,因为内存太少导致交换。
2赞 Joshua 8/18/2010
不适合 int 的预处理器常量也是编译时。
5赞 Constantin 12/15/2008 #8

除了未定义的行为外,还有同样令人讨厌的实现定义行为

当程序执行标准未指定其结果的操作时,就会发生未定义的行为。

实现定义的行为是程序的操作,其结果未由标准定义,但实现需要记录。一个例子是“多字节字符文字”,来自 Stack Overflow 问题 是否有无法编译的 C 编译器?

实现定义的行为仅在您开始移植时咬您(但升级到新版本的编译器也是移植!

2赞 yesraaj 12/22/2009 #9

不同编译单元中的命名空间级对象不应相互依赖进行初始化,因为它们的初始化顺序是未定义的。

3赞 RandomNickName42 3/19/2011 #10

对各种环境限制有基本的了解。完整列表位于 C 规范的第 5.2.4.1 节中。这里有几个;

  • 一个函数定义中有 127 个参数
  • 一次函数调用中有 127 个参数
  • 一个宏定义中有 127 个参数
  • 一次宏调用中有 127 个参数
  • 逻辑源行中有 4095 个字符
  • 字符串中有 4095 个字符 literal 或宽字符串文字(在 串联)
  • 65535 字节 对象(仅在托管环境中)
  • 15 个嵌套级别用于 #includedfiles
  • 开关的 1023 个外壳标签 声明(不包括那些 anynested switch 语句)

实际上,我对 switch 语句的 1023 个大小写标签的限制感到有点惊讶,我可以预见,对于生成的代码/lex/解析器来说,这很容易被超越。

如果超过这些限制,则会出现未定义的行为(崩溃、安全漏洞等)。

是的,我知道这是来自 C 规范,但 C++ 共享这些基本支持。

评论

9赞 new123456 4/22/2011
如果达到这些限制,则遇到的问题比未定义的行为要多。
0赞 Demi 9/13/2013
在对象中,您可以很容易地超过 65535 个字节,例如 STD::vector
2赞 John Dibling 6/27/2012 #11

用于在重叠的内存区域之间复制。例如:memcpy

char a[256] = {};
memcpy(a, a, sizeof(a));

根据 C 标准,该行为是未定义的,该标准包含在 C++03 标准中。

7.21.2.1 memcpy 函数

概要

1/ #include void *memcpy(void * restrict s1, const void * 限制 s2, size_t n);

描述

2/ memcpy 函数 将 S2 指向的对象中的 n 个字符复制到该对象中 由 S1 指向。如果在重叠的对象之间进行复制, 行为未定义。返回 3 memcpy 函数返回 S1 的值。

7.21.2.2 memmove函数

概要

1 #include void *memmove(void *s1, const void *s2, size_t n);

描述

2 memmove 函数从指向的对象复制 n 个字符 通过 S2 进入 S1 指向的对象。复制就像 首先将 S2 指向的对象中的 n 个字符复制到 不与对象重叠的 n 个字符的临时数组 由 s1 和 s2 指向,然后是临时的 n 个字符 数组被复制到 S1 指向的对象中。返回

3 memmove 函数返回 s1 的值。