前言

额。。惭愧,今天在群里听到UB,我还以为是啥咋没听过。

UB:undefined behaviour,简称:未定义行为
不过u1s1啊。undefined,这个经常见,所以一听就知道,组合词确实是我孤陋寡闻了。


正文

这是当时群里的题

不过借此,也可以适当进一步了解未定义行为。

首先,未定义行为怎么来的?也不合适,应该是先考虑定义。
所谓的语言标准,自然有自己的标准委员会提出,也就是c++ iso的说法,即c++11、c++14巴拉巴拉的,除去这一层,不同的编译器也可能自己做了优化,又多了一层定义。那么这层定义就比较搞了,因为相同的代码在不同编译器下就很有可能出问题。所以尽量不要再这上面钻牛角尖。

未定义行为:可以说是语言标准没有规定,编译器自行决定的行为,而且在不同的编译器上有时会有不同的结果。


例一

就是很无聊的++问题

1
2
int i = 0;
i = i++;

在我们的印象里,后置递增,先操作后自增。即i=i; i++
但是还有一种恶心的情况,i++; i=i

当然这里能想到后者就完犊子了,肯定是先赋值后递增,所以i=1

然后就来了

1
2
3
int i = 0;
int j = 0;
i = i+++j;

问?正常情况下你觉得是i=(i++)+j还是i=i+(++j)

让我们瞅瞅vs的反汇编

1
2
3
4
5
6
7
8
00007FF72C6A1E22  mov         eax,dword ptr [j]  
00007FF72C6A1E25 mov ecx,dword ptr [i]
00007FF72C6A1E28 add ecx,eax
00007FF72C6A1E2A mov eax,ecx
00007FF72C6A1E2C mov dword ptr [i],eax
00007FF72C6A1E2F mov eax,dword ptr [i]
00007FF72C6A1E32 inc eax
00007FF72C6A1E34 mov dword ptr [i],eax

汇编指令inc 是让操作数+1

首先把j的值传给eax,把i的值传给ecx,然后ecx+eax,再把ecx的值传给eax,也就是i+j,
然后把eax的值传回给变量i,变量i的值又传给eax,然后eax 完成inc,也就是自增,最后再把eax的值传回变量i。也就是后面的自增。

显然在vs中,i = (i++)+j

tips:这里其实也有个小坑,就是运算符优先级,他会优先处理掉高等级的,所以在有的时候你不要去让编译器考虑优先级,你应该加括号自己先预演好优先级

还有一种吊炸天的问题

1
2
3
4
int i = 0;
int j = 0;
i = i+++++j;
std::cout << i;

你说是i+ (++(++j))还是i++ + ++j
很无厘头啊,虽然现在的vs会要求你有格式控制,一整串他是不认账的。

但是你要放在试卷上,就是铁nt行为。

像已知的,gcc、msvc、clang他们的支持就有区别了。
这种无厘头的说实在没有必要太纠结,开发这么写要给打死。
也别只拿vs的反汇编说话,虽然能给出一个答案,但是不管从什么角度而言这种题目都很反人类。


例二

标准定义,常量是在初始化也可以说定义后是不能被修改的

1
2
3
const int a = 1;
int *b = (int*)&a;
*b = 2;

那么a的值?

在iso硬性规定常量不能被修改,因为常量被当成右值了。
当我们强制转换成整型指针然后取他的地址。就变相的控制了

很显然这也是未定义行为,不过有些时候还真有这么用的,但是不太好。毕竟万一常量释放了,这个指针又不知道搞哪去了。


例三

就是重复释放

1
2
3
4
int *p = new int;
delete p;

delete p;

编译器不会报错,但不能保证会不会出现别的结果。


总而言之

我所知道的都是比较浅薄的,百度一搜都有很多高端的东西,像是容器+算法然后整的活。可能这才是真正的程序员吧,还要翻阅文献什么的确实太顶了。。
严格意义上,未定义行为不算一个详细的知识体系,你不可能记住所有的未定义行为吧。。。毕竟是叫未定义啊,有些指不定还是你整的活。

常见的反正就是那么几种

  1. 有符号类型赋值的时候超出范围
  2. 函数体之间忘记初始化之类的
  3. 算术表达式写的不好就会产生未定义的结果
  4. 数学性质除以0
  5. 经典溢出,有的时候溢出是不一定会报错的

结语

坦白的说,还不够深入了解
但是一码归一码,像这种未定义行为可能还是因为c/c++比较自由
还得努力学啊~~~感觉自己就是废物