C++ 程序员应该了解哪些常见的未定义行为?
说,比如:
a[i] = i++;
NULL
指针memcpy
复制重叠缓冲区。int64_t i = 1; i <<= 72
未定义)int i; i++; cout << i;
)volatile
在sig_atomic_t
接收到信号时long int
#if
在表达式中动态生成定义的标记评估函数参数的顺序是未指定的行为。(这不会使您的程序崩溃、爆炸或订购披萨......不像未定义的行为。)
唯一的要求是在调用函数之前必须对所有参数进行全面评估。
这:
// 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);
它可以是;这取决于编译器。结果可能很重要,具体取决于副作用。
编译器可以自由地重新排序表达式的求值部分(假设含义不变)。
从原来的问题:
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.
const
使用剥离后分配给常数const_cast<>
:
const int i = 10;
int *p = const_cast<int*>( &i );
*p = 1234; //Undefined
我最喜欢的是“模板实例化中的无限递归”,因为我相信它是唯一一个在编译时出现未定义行为的地方。
除了未定义的行为,还有同样讨厌的实现定义的行为。
当程序执行标准未指定结果的操作时,会发生未定义的行为。
实现定义的行为是程序的行为,其结果未由标准定义,但实现需要记录。一个例子是“多字节字符文字”,来自 Stack Overflow 问题Is there a C compiler that failed to compile this? .
实现定义的行为只会在您开始移植时咬你(但升级到新版本的编译器也是移植!)
变量只能在表达式中更新一次(技术上在序列点之间更新一次)。
int i =1;
i = ++i;
// Undefined. Assignment to 'i' twice in the same expression.
对各种环境限制的基本了解。完整列表在 C 规范的第 5.2.4.1 节中。这里有几个;
实际上,我对 switch 语句的 1023 个 case 标签的限制感到有些惊讶,我可以很容易地预见到生成的代码/lex/解析器会超过这个限制。
如果超出这些限制,您将出现未定义的行为(崩溃、安全漏洞等)。
对,我知道这是来自 C 规范,但 C++ 共享这些基本支持。
C++ 保证大小的唯一类型是char
. 大小为 1。所有其他类型的大小取决于平台。
不同编译单元中的命名空间级对象在初始化时不应相互依赖,因为它们的初始化顺序是未定义的。
用于在重叠memcpy
的内存区域之间进行复制。例如:
char a[256] = {};
memcpy(a, a, sizeof(a));
根据 C 标准,该行为是未定义的,该 C 标准包含在 C++03 标准中。
概要
1/ #include void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
描述
2/ memcpy函数将n个字符从s2指向的对象复制到s1指向的对象中。如果复制发生在重叠的对象之间,则行为未定义。返回 3 memcpy 函数返回 s1 的值。
概要
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 的值。