C++ 程序员应该了解哪些常见的未定义行为?[关闭]
-
21-08-2019 - |
题
C++ 程序员应该了解哪些常见的未定义行为?
说吧,比如:
a[i] = i++;
解决方案
指针
- 解除引用
NULL
指针 - 取消引用大小为零的“新”分配返回的指针
- 使用指向生命周期已结束的对象的指针(例如,堆栈分配的对象或已删除的对象)
- 取消引用尚未明确初始化的指针
- 执行指针算术,产生超出数组边界(上方或下方)的结果。
- 在超出数组末尾的位置取消引用指针。
- 将指针转换为不兼容类型的对象
- 使用
memcpy
复制重叠缓冲区.
缓冲区溢出
- 以负偏移量或超出该对象的大小(堆栈/堆溢出)读取或写入对象或数组
整数溢出
- 有符号整数溢出
- 计算未数学定义的表达式
- 将值左移负数(右移负数由实现定义)
- 将值移位的量大于或等于数字中的位数(例如
int64_t i = 1; i <<= 72
未定义)
类型、强制转换和常量
- 将数值转换为目标类型无法表示的值(直接或通过 static_cast)
- 在明确分配自动变量之前使用它(例如,
int i; i++; cout << i;
) - 使用除以下类型之外的任何对象的值
volatile
或者sig_atomic_t
收到信号时 - 尝试在字符串文字或任何其他 const 对象的生命周期内修改它
- 在预处理期间连接窄字符串和宽字符串文字
功能与模板
- 不从值返回函数返回值(直接或通过从 try 块流出)
- 同一实体的多个不同定义(类、模板、枚举、内联函数、静态成员函数等)
- 模板实例化中的无限递归
- 使用不同的参数或到参数的链接以及函数定义为使用的链接来调用函数。
面向对象编程
- 具有静态存储持续时间的对象的级联销毁
- 分配给部分重叠对象的结果
- 在静态对象初始化期间递归地重新进入函数
- 从对象的构造函数或析构函数中对对象的纯虚函数进行虚函数调用
- 引用尚未构造或已被析构的对象的非静态成员
源文件和预处理
- 不以换行符结尾或以反斜杠结尾的非空源文件(C++11 之前的版本)
- 反斜杠后跟不属于字符或字符串常量中指定转义代码的字符(这是在 C++11 中实现定义的)。
- 超出实现限制(嵌套块的数量、程序中的函数数量、可用堆栈空间...)
- 无法用 a 表示的预处理器数值
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);
它可以是;它是由编译器。结果可以不管,取决于副作用。
,编译器可以自由地重新排序的表达式的求值的部分(假设意义是不变)。
从原来的问题:
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<>
ness后分配到一个常数:
const int i = 10;
int *p = const_cast<int*>( &i );
*p = 1234; //Undefined
另外的未定义的行为下,也有同样的讨厌的实现定义强>
当一个程序做了其结果不是由标准指定,发生未定义的行为。
实现定义的行为是其结果不受标准定义的程序,但是其中所述实现记录所需的动作。一个例子是“多字节字符文字”,从堆栈溢出问题的 是否有编译失败呢?
实现定义的行为,只有当你开始移植咬你(但升级到编译器也被移植新版本!)
变量仅可一次在表达式(序列点之间技术上一次)更新。
int i =1;
i = ++i;
// Undefined. Assignment to 'i' twice in the same expression.
对各种环境限制的基本了解。完整列表位于 C 规范的第 5.2.4.1 节中。这里有一些;
- 一个函数定义中有 127 个参数
- 一次函数调用中有 127 个参数
- 一个宏定义中有 127 个参数
- 一次宏调用中有 127 个参数
- 逻辑源代码行中有 4095 个字符
- 字符串中有 4095 个字符 literal 或宽字符串文字(在 串联)
- 65535 字节 对象(仅在托管环境中)
- #includedfiles 的 15 个嵌套级别
- 开关的 1023 个外壳标签 声明(不包括那些 anynested switch 语句)
实际上,我对 switch 语句的 1023 个 case 标签的限制感到有点惊讶,我可以预见生成的代码/lex/解析器很容易超出这个限制。
如果超出这些限制,则会出现未定义的行为(崩溃、安全缺陷等)。
是的,我知道这是来自 C 规范,但 C++ 共享这些基本支持。
使用memcpy
到重叠存储器区域之间复制。例如:
char a[256] = {};
memcpy(a, a, sizeof(a));
该行为是根据C标准,它是由C ++ 03标准所包含未定义的。
7.21.2.1 memcpy函数
概要
1 /#包括无效*的memcpy(无效*限制S1,常量 无效*限制S2,为size_t n)的;
描述
2 / memcpy函数 复制n个字符由对象指向S2到对象 到s1指向的。如果进行复制操作是重叠的对象之间, 行为是不确定的。返回3 memcpy函数返回 s1的值。
7.21.2.2的的memmove功能
概要
1个的#include无效*的memmove(无效* S1,常量无效* S2,为size_t N);
描述
2所述的memmove函数复制n个字符由对象指向 由S2到对象s1指向到。复制发生,就好像 n的对象字符s2指向首先复制到 的n个字符临时数组不重叠的对象 指向由S1和S2,然后从临时的n个字符 阵列被复制到对象指向S1。返回
3的memmove函数返回s1的值。
针对C ++保证了大小的唯一类型是char
。并且大小为1所有其它类型的大小是依赖于平台的。
在一个不同的编译单元命名空间级对象,决不应该依靠彼此进行初始化,因为它们的初始化顺序是不确定的。