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

时间:2008-12-15 06:55:42

标签: c++ undefined undefined-behavior c++-faq

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

说,像:

a[i] = i++;

11 个答案:

答案 0 :(得分:233)

指针

  • 取消引用NULL指针
  • 取消引用由大小为零的“新”分配返回的指针
  • 使用指向生命周期已结束的对象的指针(例如,堆栈已分配的对象或已删除的对象)
  • 取消引用尚未明确初始化的指针
  • 执行指针运算,在阵列的边界(上方或下方)之外产生结果。
  • 在超出数组末尾的位置取消引用指针。
  • 将指针转换为不兼容类型的对象
  • Using memcpy to copy overlapping buffers

缓冲区溢出

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

整数溢出

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

类型,演员和常数

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

功能和模板

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

OOP

  • 具有静态存储持续时间的对象的级联破坏
  • 分配给部分重叠的对象的结果
  • 在初始化静态对象期间递归重新输入函数
  • 从构造函数或析构函数
  • 对对象的纯虚函数进行虚函数调用
  • 提及尚未建造或已被毁坏的物体的非静止成员

源文件和预处理

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

待分类

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

答案 1 :(得分: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);

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

答案 2 :(得分: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.

答案 3 :(得分:5)

使用const剥离const_cast<>后分配常量:

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

答案 4 :(得分:5)

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

答案 5 :(得分:5)

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

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

实现定义的行为是程序的一个动作,其结果不是由标准定义的,而是需要实现的文档。一个例子是“多字节字符文字”,来自Stack Overflow问题 Is there a C compiler that fails to compile this?

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

答案 6 :(得分:4)

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

int i =1;
i = ++i;

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

答案 7 :(得分:3)

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

  • 一个功能定义中的127个参数
  • 一个函数调用中的127个参数
  • 一个宏定义中的127个参数
  • 一次宏调用中的127个参数
  • 逻辑源行中的4095个字符
  • 字符串中的4095个字符 字面或宽字符串文字(后 连接)
  • 65535字节 对象(仅在托管环境中)
  • #included fi les的15个级别
  • 1023开关的案例标签 声明(不包括 任意切换声明)

我对切换语句的1023个案例标签的限制实际上感到有些惊讶,我可以预见到生成的代码/ lex /解析器的覆盖率相当容易。

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

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

答案 8 :(得分:2)

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

答案 9 :(得分:2)

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

答案 10 :(得分:2)

使用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 * restrict 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个字符复制到a中   不与对象重叠的n个字符的临时数组   由s1和s2指向,然后是临时的n个字符   数组被复制到s1指向的对象中。返回

     

3 memmove函数返回s1的值。