C ++程序员应该知道的所有常见的未定义行为是什么?
C ++程序员应该知道的所有常见的未定义行为是什么?
比如说:
a[i] = i++;
指针
- 解引用
NULL
指针 - 取消引用大小为零的“新”分配返回的指针
- 使用指向其生命周期结束的对象(例如,堆栈分配的对象或删除的对象)
- 解引用尚未被初始化的指针
- 执行指针运算,得到数组边界之外(上面或下面)的结果。
- 将指针解引用超出数组末尾的位置。
- 将指针转换为不兼容types的对象
- 使用
memcpy
复制重叠的缓冲区 。
缓冲区溢出
- 读取或写入对象或数组的偏移量为负数或超出该对象的大小(堆栈/堆溢出)
整数溢出
- 有符号整数溢出
- 评估未经math定义的expression式
- 将值左移一个负值(右移一个负值是实现定义的)
- 将值移位的次数大于或等于数中的位数(例如,
int64_t i = 1; i <<= 72
是未定义的)
types,演员和常量
- 将数值转换为目标types无法表示的值(直接或通过static_cast)
- 在明确赋值之前使用自动variables(例如,
int i; i++; cout << i;
) - 在收到信号时使用
volatile
或sig_atomic_t
以外的任何types的对象的值 - 尝试在其生命周期中修改string文字或任何其他的const对象
- 在预处理过程中将窄string连接起来
function和模板
- 不从一个返回值函数返回一个值(直接或者从一个try-block中stream出)
- 同一实体(类,模板,枚举,内联函数,静态成员函数等)的多个不同定义
- 模板实例化中的无限recursion
- 使用不同的参数调用一个函数,或者调用函数被定义为使用的参数和链接。
OOP
- 级联的静态存储持续时间对象的破坏
- 分配给部分重叠对象的结果
- 在初始化静态对象的过程中,recursion地重新input一个函数
- 从构造函数或析构函数中虚拟函数调用对象的纯虚函数
- 提到尚未构build或已经被破坏的物体的非静态成员
源文件和预处理
- 非空的源文件,不以换行符结尾,或以反斜杠结尾(在C ++ 11之前)
- 反斜杠后跟一个字符,不是字符或string常量中指定的转义代码的一部分(这是在C ++ 11中定义的实现)。
- 超出实施限制(嵌套块的数量,程序中的函数数量,可用堆栈空间…)
- 不能用
long int
表示的预处理器数值 - 预处理指令位于函数式macros定义的左侧
- 在
#if
expression式中dynamic生成定义的标记
待分类
- 在销毁静态存储时间的程序期间调用exit
函数参数的评估顺序是未指定的行为 。 (这不会使你的程序崩溃,爆炸,或命令披萨…不像未定义的行为 。)
唯一的要求是所有的参数必须在函数被调用之前完全评估。
这个:
// 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);
它可以是; 这取决于编译器。 结果可能很重要,这取决于副作用。
编译器可以自由地对expression式的评估部分进行重新sorting(假定意义不变)。
从原来的问题来看:
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;
双重检查locking。 还有一个容易犯的错误。
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.
我最喜欢的是“模板实例化中的无限recursion”,因为我相信这是在编译时发生未定义行为的唯一方法。
除了未定义的行为 ,还有同样令人讨厌的实现定义的行为 。
未定义的行为发生在程序执行标准未指定的结果时。
实现定义的行为是由程序执行的行为,其结果不是由标准定义的,而是实现需要logging的行为。 一个例子是“多字节字符文字”,从堆栈溢出的问题是否有一个C编译器无法编译这个? 。
实现定义的行为只会在开始移植时咬你(但升级到新版本的编译器也在移植!)
使用const_cast<>
剥离const
后指定const
:
const int i = 10; int *p = const_cast<int*>( &i ); *p = 1234; //Undefined
variables只能在expression式中更新一次(技术上一次在序列点之间)。
int i =1; i = ++i; // Undefined. Assignment to 'i' twice in the same expression.
对各种环境限制的基本了解。 完整列表在C规范的第5.2.4.1节中。 这里有几个;
- 一个函数定义中的127个参数
- 在一个函数调用中有127个参数
- 一个macros定义中有127个参数
- 一个macros调用中有127个参数
- 逻辑源代码行中有4095个字符
- string文字或宽string文字中的4095个字符(拼接后)
- 一个对象中的65535字节(仅在托pipe环境中)
- #included文件的15个关卡
- switch语句的1023个case标签(不包括anynested switch语句的标签)
实际上,对于一个switch语句的1023个case标签的限制,我感到有些惊讶,我可以预见到,对于生成的代码/ lex / parser而言,它是相当简单的。
如果超出这些限制,你有未定义的行为(崩溃,安全缺陷等)。
对,我知道这是来自C规范,但C ++共享这些基本的支持。
使用memcpy
在重叠的内存区域之间进行复制。 例如:
char a[256] = {}; memcpy(a, a, sizeof(a));
根据C ++ 03标准包含的C标准,行为是不确定的。
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移动function
概要
1 #include void * memmove(void * s1,const void * s2,size_t n);
描述
2 memmove函数将s2指向的对象中的n个字符复制到s1指向的对象中。 复制的过程与从s2指向的对象中的n个字符首先复制到n个字符的临时数组中,这些字符不与s1和s2指向的对象重叠,然后将临时数组中的n个字符复制到s1指向的对象。 返回
3 memmove函数返回s1的值。
C ++保证大小的唯一types是char
。 大小是1.所有其他types的大小是平台相关的。
不同编译单元中的名称空间级别的对象不应该依赖于对方进行初始化,因为它们的初始化顺序是未定义的。