undefined behavior 未定义行为
未定义行为
指C++历代版本中没有明确规定的行为,因此在不同的编译器情况下,会产生不同的结果,因此这种不确定性很大概率在编程中出现bug,导致很恶性的后果,如何识别并处理未定义行为,非常重要,本文中将会对未定义行为分析,并举出几个未定义行为的例子,同时此文也是Back To Basics 笔记的第四篇。
Overview
在了解未定义行为之前,我们先来看看一些前情提要
一些对未定义行为的误解
我们时常认为项目组的leader已经有过多年的编程经验,也过度依赖编辑器和编译器,认为在code review ,unit test中就找到未定义行为,认为高级的编译器会对未定义行为报错,但实际上并不是的,常常非常有经验的程序员和测试工程师都不会发现未定义行为,也经常会出现写未定义行为的错误。
unspecified behavior
unspecified behavior指有多重含义的代码,编译器被允许随机按照一种方式解读,比如下面的代码
if ("abc" == "abc") { }
编译器可以理解为两个字符串的地址是否相同,也可以选择进行字符串比较,但在编译器眼中,无论他怎么解释这段比较,都会返回一个true或false,虽然这种不是未定义行为,但因为编译器会返回不确定的结果,这也是一种需要注意的代码,尽量不要写出这种代码,这会对测试的同学造成极大地阻碍
undefined behavior
终于到了本文的主题,未定义行为,未定义行为不同于上面的行为,而是一种编译器看不懂的语言,代表这并不是c++代码,是一种毫无意义的语言,请看下面代码:
int* varA = nullptr;
*varA = 19; //未定义行为 这里会运行时报错,引发了异常: 写入访问权限冲突。varA 是 nullptr。
int varB;
varA = &varB;
std::cout << *varA;
std::cout << varB;
第二行代码就是一种未定义行为:试图对nullptr重新赋值。
而三四行代码是合法的,虽然varB没有被初始化,但还是会有一个地址被保存,因此可以把这个地址给varA指针来进行保存
第五行代码也是合法的,因为在重定义varA = &varB的时候这个步骤是合法的,虽然varB可能指向的是一个位置的地址,但在编译器眼中,这里是合法的
第六行则是一种未定义行为:直接访问一个未初始化的随机地址。
请再看一段代码:
template<typename T1, typename T2>
void doLessThanLessThan(T1& x, T2& y) {
x << y;
}
int main() {
doLessThanLessThan(250, 75); // 未定义行为,但在C++20无法编译通过
doLessThanLessThan(std::cout, "cat");// 正常输出cat
return 0;
}
定义的模板函数中,可以理解为x左移y位(主函数中第一行),也可以理解为流符号(主函数中第二行的调用),这两种调用模板函数的形式中,第一种则会直接编译不通过,因为c++中不允许移位超过类型原本的长度,int类型肯定是不会有75位,因此这是一种未定义行为,但第二种则是正常的代码
一部分未定义行为:
访问std:vector中超出末尾的元素
对空指针的重复引用
使用未初始化变量
从构造函数或析构函数调用纯虚函数
在对象被销毁后使用(被释放后使用)
转换指向不兼容类型的指针,然后使用结果
没有边界条件的无限循环
修改字符串字面值或任何其他const对象
函数声明的时候有返回值,但实现的部分没有返回值
任何竞争条件
整数除以零
带符号整数溢出,但无符号整数溢出则不是
上面这些未定义行为并不完整,而且有些是可以编译通过的有些则直接在编译期间就会报错,这取决于不同的编译器,但相同的是上面这些都属于未定义行为:
在请看一些例子:
std::vector<int> myContainer = { 1,2,5,4,6 };
for (auto& item : myContainer) {
if (item == 5) {
myContainer.insert(myContainer.begin(), -5);
}
}
因为insert方法会使所有的迭代器失效,因此在第四行之后,不清楚当前的迭代器指向哪里,因此这是一种未定义行为,下一次循环中就不知道会如何进行,可能死循环,也可能直接crash
void doThing8(const std::string input) {
std:string& tmp = const_cast<std::string&>(input);// line B
tmp = "bear"; // line C
}
const std :: string value = "tiger"; // line A
doThing8(value);
对一个已经有了const属性的对象进行const_cast取消他的const属性,也是未定义行为:
int varA = 5;
varA == ++varA + 2; // c++ 03, 未定义行为, C++ 11 以及更新的版本是正确的
varA == 8;
int varB = 3;
varB = varB++ + 2; // C++11以前是未定义,17以后是正确的
谭浩强老师版本的未定义行为(据说是因为后面的修订版本导致的有类似的代码,可能也不是谭老师本人的意愿):
上面的代码中如果在C++14的版本中运行,则varB为6,
而c++20的版本则为5,可以看到未定义行为的危害,不同的编译器会产生很不一样的结果,因此在编程的过程中,希望能够了解并注意到这些严重的问题。