Back To Basics_04_未定义行为

发布于:2023-01-11 ⋅ 阅读:(502) ⋅ 点赞:(0)

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,可以看到未定义行为的危害,不同的编译器会产生很不一样的结果,因此在编程的过程中,希望能够了解并注意到这些严重的问题。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到