c++程序员应该了解哪些常见的未定义行为?

c++程序员应该了解哪些常见的未定义行为?

说,像:

a[i] = i++;

79505 次浏览

c++保证其大小的唯一类型是char。大小是1。所有其他类型的大小都依赖于平台。

指针

  • NULL指针进行解引用
  • 取消引用由"new"返回的指针;大小为0的分配
  • 使用指向生命期已经结束的对象的指针(例如,堆栈分配对象或删除对象)
  • 解除对尚未明确初始化的指针的引用
  • 执行指针算术,产生超出数组边界(高于或低于)的结果。
  • 在数组末尾以外的位置解除对指针的引用。
  • 将指针转换为不兼容类型的对象
  • 使用memcpy复制重叠缓冲区

缓冲区溢出

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

整数溢出

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

类型,Cast和Const

  • 将数值转换为目标类型不能表示的值(直接或通过static_cast)
  • 在明确赋值前使用自动变量(例如,int i; i++; cout << i;)
  • 在接收信号时使用除volatilesig_atomic_t之外的任何类型的对象的值
  • 试图在字符串字面量或任何其他const对象的生命周期内修改它
  • 在预处理期间连接窄字符串和宽字符串字面值

函数和模板

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

OOP

  • 具有静态存储持续时间的对象的级联销毁
  • 对部分重叠对象赋值的结果
  • 在初始化静态对象期间递归地重新输入函数
  • 从对象的构造函数或析构函数调用对象的纯虚函数
  • 指尚未构造或已被销毁的对象的非静态成员

源文件和预处理

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

被分类

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

函数参数的求值顺序为< em > < / em >不明的行为。(这不会使您的程序崩溃、爆炸或订购披萨…不像< / em > < em >未定义行为。)

唯一的要求是在调用函数之前必须完全计算所有参数。


这样的:

// 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.

在使用const_cast<>剥离constness后赋值给常量:

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

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

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

当程序执行的某些操作的结果没有被标准指定时,就会发生未定义行为。

实现定义的行为是程序的一种行为,其结果不由标准定义,但需要实现对其进行记录。一个例子是“多字节字符字面量”,来自堆栈溢出问题是否有C编译器编译失败?< / >

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

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

对各种环境限制有基本的了解。完整的列表见C规范的5.2.4.1节。这里有一些;

  • 一个函数定义中有127个参数
  • 一次函数调用127个参数
  • 一个宏定义127个参数
  • 在一次宏调用中有127个参数
  • 逻辑源行中包含4095个字符
  • 字符串中的4095个字符 字面值或宽字符串字面值(在 李连接)< / >
  • an中65535字节 对象(仅在托管环境中)
  • #includedfiles的15个嵌套层
  • 1023开关大小写标签 语句(不包括用于 任何嵌套的开关语句)

实际上,我对switch语句的1023个大小写标签的限制感到有点惊讶,我可以预见,生成的code/lex/解析器很容易就会超过这个限制。

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

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

使用memcpy在重叠的内存区域之间进行复制。例如:

char a[256] = {};
memcpy(a, a, sizeof(a));

根据c++ 03标准包含的C标准,该行为是未定义的。

7.21.2.1 memcpy功能

剧情简介

1/ #包含void *memcpy(void *限制s1, const . Void *限制s2, size_t n);< / p >

描述

2/ memcpy函数 将s2指向的对象中的n个字符复制到对象中 被s1指向。如果复制发生在重叠的物体之间, 行为是未定义的。memcpy函数返回 s1的值。

7.21.2.2 memmove函数

剧情简介

1 #包含void *memmove(void *s1, const void *s2, size_t < / p > n);

描述

memmove函数从所指向的对象中复制n个字符 s2指向s1指向的对象。复制发生的时候就好像 s2指向的对象中的N个字符首先复制到a中 不重叠对象的n个字符的临时数组 由s1和s2指向,然后从临时的n个字符 数组被复制到s1所指向的对象中。返回< / p >

memmove函数返回s1的值。