提问人:yesraaj 提问时间:12/15/2008 最后编辑:Peter Mortensenyesraaj 更新时间:1/23/2014 访问量:83566
C++程序员应该知道的所有常见未定义行为是什么?[关闭]
What are all the common undefined behaviours that a C++ programmer should know about? [closed]
问:
C++程序员应该知道的所有常见未定义行为是什么?
比如说:
a[i] = i++;
答:
C++ 保证大小的唯一类型是 。大小为 1。所有其他类型的大小取决于平台。char
评论
指针
- 取消引用指针
NULL
- 取消引用大小为零的“新”分配返回的指针
- 使用指向生存期已结束的对象的指针(例如,堆栈分配的对象或已删除的对象)
- 取消引用尚未明确初始化的指针
- 执行指针算术,在数组的边界(上方或下方)之外生成结果。
- 在数组末尾以外的位置取消引用指针。
- 将指针转换为不兼容类型的对象
- 使用
memcpy
复制重叠的缓冲区。
缓冲区溢出
- 读取或写入偏移量为负或超出该对象大小的对象或数组(堆栈/堆溢出)
整数溢出
- 有符号整数溢出
- 计算未以数学方式定义的表达式
- 向左移动值为负量(右移为负量由实现定义)
- 将值移位大于或等于数字中的位数(例如 未定义)
int64_t i = 1; i <<= 72
类型、演员表和常量
- 将数值转换为无法由目标类型表示的值(直接或通过static_cast)
- 在明确赋值之前使用自动变量(例如,
int i; i++; cout << i;
) - 使用接收信号以外的任何类型的对象的值,或在接收信号时
volatile
sig_atomic_t
- 尝试在其生存期内修改字符串文本或任何其他 const 对象
- 在预处理期间将窄字符串与宽字符串文本连接起来
功能与模板
- 不从返回值的函数返回值(直接或通过从 try-block 流出)
- 同一实体的多个不同定义(类、模板、枚举、内联函数、静态成员函数等)
- 模板实例化中的无限递归
- 使用不同的参数调用函数,或链接到该函数定义为使用的参数和链接。
哎呀
- 具有静态存储持续时间的对象的级联销毁
- 分配给部分重叠对象的结果
- 在函数的静态对象初始化期间以递归方式重新输入函数
- 从对象的构造函数或析构函数对对象的纯虚函数进行虚函数调用
- 指尚未构造或已销毁的对象的非静态成员
源文件和预处理
- 不以换行符结尾或以反斜杠结尾的非空源文件(C++11 之前)
- 后跟字符或字符串常量中不属于指定转义码的字符的反斜杠(这是在 C++11 中定义的实现)。
- 超出实现限制(嵌套块数、程序中的函数数、可用堆栈空间等)
- 不能用
long int
- 类似函数的宏定义左侧的预处理指令
- 在表达式中动态生成定义的标记
#if
待分类
- 在销毁具有静态存储持续时间的程序期间调用 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);
它可以是;这取决于编译器。结果可能很重要,具体取决于副作用。
评论
变量在表达式中只能更新一次(从技术上讲,在序列点之间更新一次)。
int i =1;
i = ++i;
// Undefined. Assignment to 'i' twice in the same expression.
评论
编译器可以自由地对表达式的计算部分进行重新排序(假设含义保持不变)。
从原来的问题:
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.
评论
在剥离 ness 后使用以下命令赋值为常量:const
const_cast<>
const int i = 10;
int *p = const_cast<int*>( &i );
*p = 1234; //Undefined
我最喜欢的是“模板实例化中的无限递归”,因为我相信它是唯一一个在编译时发生未定义行为的。
评论
除了未定义的行为外,还有同样令人讨厌的实现定义行为。
当程序执行标准未指定其结果的操作时,就会发生未定义的行为。
实现定义的行为是程序的操作,其结果未由标准定义,但实现需要记录。一个例子是“多字节字符文字”,来自 Stack Overflow 问题 是否有无法编译的 C 编译器?。
实现定义的行为仅在您开始移植时咬您(但升级到新版本的编译器也是移植!
不同编译单元中的命名空间级对象不应相互依赖进行初始化,因为它们的初始化顺序是未定义的。
对各种环境限制有基本的了解。完整列表位于 C 规范的第 5.2.4.1 节中。这里有几个;
- 一个函数定义中有 127 个参数
- 一次函数调用中有 127 个参数
- 一个宏定义中有 127 个参数
- 一次宏调用中有 127 个参数
- 逻辑源行中有 4095 个字符
- 字符串中有 4095 个字符 literal 或宽字符串文字(在 串联)
- 65535 字节 对象(仅在托管环境中)
- 15 个嵌套级别用于 #includedfiles
- 开关的 1023 个外壳标签 声明(不包括那些 anynested switch 语句)
实际上,我对 switch 语句的 1023 个大小写标签的限制感到有点惊讶,我可以预见,对于生成的代码/lex/解析器来说,这很容易被超越。
如果超过这些限制,则会出现未定义的行为(崩溃、安全漏洞等)。
是的,我知道这是来自 C 规范,但 C++ 共享这些基本支持。
评论
用于在重叠的内存区域之间复制。例如: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 * 限制 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 个字符复制到 不与对象重叠的 n 个字符的临时数组 由 s1 和 s2 指向,然后是临时的 n 个字符 数组被复制到 S1 指向的对象中。返回
3 memmove 函数返回 s1 的值。
上一个:未定义的行为和重新加载的序列点
评论