C ++程序员应该知道哪些常见的未定义行为?[关闭]


201

C ++程序员应该知道哪些常见的未定义行为?

像这样说:

a[i] = i++;


3
你确定吗。看起来定义很好。
马丁·约克

17
6.2.2在C ++编程语言中的评估顺序[expr.evaluation]这样说。我没有任何其他参考
yesraaj

4
他是正确的..刚刚看了C ++编程语言中的6.2.2,它说v [i] = i ++是未定义的
dancavallaro

4
我可以想象,因为编译器在计算v [i]的存储位置之前或之后执行i ++。当然,我总是会被分配在那里。但它可以写要么v [I]或v [I + 1]取决于操作的顺序..
埃文特兰

2
C ++编程语言所说的全部是:“表达式中子表达式的操作顺序是不确定的。特别是,您不能假定该表达式是从左到右求值的。”
dancavallaro

Answers:


233

指针

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

缓冲区溢出

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

整数溢出

  • 有符号整数溢出
  • 评估数学上未定义的表达式
  • 左移数值为负数(实现时定义为负移右数)
  • 将值移位大于或等于数字中位数的数量(例如int64_t i = 1; i <<= 72,未定义)

类型,演员表和常量

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

功能和模板

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

面向对象

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

源文件和预处理

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

待分类

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

嗯... IEN 754涵盖了NaN(x / 0)和Infinity(0/0),如果后来设计了C ++,为什么将x / 0记录为未定义?
new123456'4

Re:“反斜杠后跟一个字符,该字符不属于字符或字符串常量中指定的转义代码的一部分。” 在C89(第3.1.3.4节)和C ++ 03(包含C89)中是UB,但在C99中则不是。C99说“结果不是令牌,并且需要诊断”(第6.4.4.4节)。大概C ++ 0x(包含C89)将是相同的。
亚当·罗森菲尔德

1
C99标准在附录J.2中列出了未定义的行为。要使此列表适应C ++,将需要一些工作。您必须更改对正确C ++子句而不是C99子句的引用,删除所有不相关的内容,并检查所有这些东西在C ++和C中是否确实未定义。但这提供了一个开始。
史蒂夫·杰索普

1
@ new123456-并非所有浮点单元都与IEE754兼容。如果C ++要求符合IEE754,则编译器将需要通过显式检查来测试和处理RHS为零的情况。通过使行为不确定,编译器可以说“如果使用非IEE754 FPU,则不会得到IEEE754 FPU行为”,从而避免了这种开销。
SecurityMatt

1
“计算结果不在相应类型范围内的表达式” ..整数溢出对于UNSIGNED整数类型(不是带符号的整数)是明确定义的。
nacitar sevaht

31

函数参数的评估顺序是未指定的行为。(与未定义的行为不同,这不会使您的程序崩溃,爆炸或订购比萨饼。)

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


这个:

// 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
顺序是不确定的,不是不确定的。
罗伯·肯尼迪

1
我讨厌这个:)一旦找到其中一个案例,我就失去了一天的工作...无论如何我吸取了教训,还没有幸运地再次倒下
罗伯特·古尔德

2
@Rob:我会就这里含义的变化与您争论,但是我知道标准委员会对这两个词的确切定义非常挑剔。因此,我将其更改为:-)
马丁·约克

2
我很幸运。我上大学时就被它咬了,有位教授看了一下它,并在5秒钟内告诉我我的问题。不知道我会浪费多少时间进行调试。
比尔蜥蜴

27

编译器可以自由地对表达式的求值部分重新排序(假设含义不变)。

从最初的问题:

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.

序列点是什么意思?
yesraaj


1
噢,这真令人讨厌,尤其是因为我已经看到Java中推荐的确切结构
汤姆(Tom)2007年

请注意,某些编译器确实在这种情况下定义了行为。例如,在VC ++ 2005+中,如果a易失,则将设置所需的内存屏障以防止指令重新排序,从而使双重检查锁定起作用。

马丁·约克:<i> //(c)一定会在(a)和(b)</ i>之后发生吗?可以肯定的是,在该特定示例中,唯一可能有意义的情况是“ i”是否是映射到硬件寄存器的易失性变量,而a [i](“ i”的旧值)是别名,但是否存在保证增量将发生在序列点之前?
超级猫

5

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


以前做过这个,但是我看不出它是如何定义的。很明显,您在事后进行了无限递归。
罗伯特·古尔德

问题在于,编译器无法检查您的代码,无法精确地确定其是否将受到无限递归的影响。这是停顿问题的一个实例。请参阅:stackoverflow.com/questions/235984/...
丹尼尔·埃里克

是的,这绝对是一个棘手的问题
Robert Gould

由于内存不足导致的交换,这使我的系统崩溃。
Johannes Schaub-litb

2
不适合int的预处理器常量也在编译时。
约书亚

5

分配到一个恒定的剥离后const使用内斯const_cast<>

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

除了未定义的行为外,还存在同样讨厌的实现定义的行为

当程序执行某项结果(标准未指定)时,会发生未定义的行为。

实现定义的行为是程序的一种动作,其结果不是标准定义的,但是实现需要记录下来。堆栈溢出问题中的一个示例是“多字节字符文字”,是否有C编译器无法对此进行编译?

实现定义的行为只会在您开始移植时咬住您(但是也要移植到新版本的编译器!)


4

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

int i =1;
i = ++i;

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

逸岸至少一次两个控制顺序点之间。
Prasoon Saurav

2
@Prasoon:我想你的意思是:两个序列点之间最多一次。:-)
Nawaz

3

基本了解各种环境限制。完整列表在C规范的5.2.4.1节中。这里有一些;

  • 一项功能定义中的127个参数
  • 一个函数调用中包含127个参数
  • 一宏定义中包含127个参数
  • 一次宏调用中包含127个参数
  • 逻辑源代码行中的4095个字符
  • 字符串文字或宽字符串文字中的4095个字符(串联后)
  • 对象中的65535字节(仅在托管环境中)
  • #included文件的15个嵌套级别
  • 1023个switch语句的案例标签(不包括任何嵌套的switch语句的标签)

实际上,对于switch语句的1023个case标签的限制我感到有些惊讶,我可以预见到,生成的代码/ lex /解析器相当容易被超过。

如果超出这些限制,则您将具有不确定的行为(崩溃,安全漏洞等)。

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


9
如果达到这些限制,则比未定义的行为要麻烦得多。
new123456 2011年

您可以轻松地在一个对象中超过65535个字节,例如STD :: vector
Demi

2

使用memcpy重叠的内存区域之间进行复制。例如:

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

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

7.21.2.1 memcpy函数

概要

1 / #include无效* memcpy(无效*限制s1,常量无效*限制s2,size_t n);

描述

2 / memcpy函数将s2指向的对象中的n个字符复制到s1指向的对象中。如果在重叠的对象之间进行复制,则行为是不确定的。返回3 memcpy函数返回s1的值。

7.21.2.2记忆功能

概要

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

描述

2 memmove函数将s2指向的对象中的n个字符复制到s1指向的对象中。就像是先将s2所指向的对象中的n个字符复制到不与s1和s2所指向的对象重叠的n个字符的临时数组中,然后再将临时数组中的n个字符复制到s1指向的对象。退货

3 memmove函数返回s1的值。


2

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


这不是<cstdint>的目的吗?它定义了诸如uint16_6等的类型。
贾斯珀·贝克斯

是的,但是大多数类型的大小(比如说很长)并不确定。
JaredPar

cstdint也不是当前c ++标准的一部分。有关当前可移植的解决方案,请参见boost / stdint.hpp。
埃文·特兰

这不是不确定的行为。该标准说,一致性平台定义尺寸,而不是标准定义尺寸。
Daniel Earwicker

1
@JaredPar:这是一个复杂的帖子,包含很多对话话题,所以我在这里总结了所有内容。底线是这样的:“ 5。为了用二进制表示-2147483647和+2147483647,您需要32位。”
John Dibling 2012年

2

不同编译单元中的命名空间级别的对象永远不要依赖彼此进行初始化,因为它们的初始化顺序是不确定的。

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.