201

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

说,比如:

a[i] = i++;

4

11 回答 11

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 中实现定义的)。
  • 超过实现限制(嵌套块的数量、程序中的函数数量、可用的堆栈空间......)
  • 不能用 a 表示的预处理器数值long int
  • 类函数宏定义左侧的预处理指令
  • #if在表达式中动态生成定义的标记

待分类

  • 在销毁具有静态存储持续时间的程序期间调用 exit
于 2008-12-15T07:15:24.583 回答
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);

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

于 2008-12-15T07:15:46.063 回答
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.
于 2008-12-15T07:35:13.420 回答
5

const使用剥离后分配给常数const_cast<>

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
于 2008-12-15T07:36:48.953 回答
5

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

于 2008-12-15T08:17:32.243 回答
5

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

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

实现定义的行为是程序的行为,其结果未由标准定义,但实现需要记录。一个例子是“多字节字符文字”,来自 Stack Overflow 问题Is there a C compiler that failed to compile this? .

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

于 2008-12-15T14:49:10.440 回答
4

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

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
于 2008-12-15T07:22:53.133 回答
3

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

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

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

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

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

于 2011-03-19T05:03:00.623 回答
2

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

于 2008-12-15T07:06:09.873 回答
2

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

于 2009-12-22T05:28:34.973 回答
2

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

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

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

7.21.2.1 memcpy 函数

概要

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

描述

2/ memcpy函数将n个字符从s2指向的对象复制到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 个字符复制到不与 s1 和 s2 指向的对象重叠的 n 个字符的临时数组中,然后将临时数组中的 n 个字符复制到s1 指向的对象。退货

3 memmove 函数返回 s1 的值。

于 2012-06-26T16:10:25.950 回答