概念
syntax & semantics
程序设计语言的语法很好理解,最常见的 C/C++ 语法错误比如:这里少了一个分号,不符合语法的代码是无法通过编译的(编译器会产生警告或者错误)
语义即语句的含义,它考虑将符合语法的语句转换成可执行的指令序列是否有效。语义错误即代码没有产生预期的行为(业务逻辑上有bug除外),发生语义错误的代码有可能是可以通过编译的。根据语义错误发生的时期可将其分为:
- 编译期语义错误
- 读取未初始化的变量 - 链接期语义错误
- 多重定义 - 运行期语义错误
- 除数为0
- 解引用nullptr
ill-formed behavior
不合法行为,它包括:语法错误、通过编译器可以诊断出的某些语义错误以及某些标准库不要求编译器要诊断出的语义错误。不合法行为理解起来更像是一个广义的概念,即某个地方不合法
implementation-defined behavior
由编译器实现定义的行为,即可以有不同的实现,但最终产生的结果(side-effect)需要保持一致,比如一个byte有几个bits
unspecified behavior
同样是由编译器实现定义的行为,但不要求最终的结果一致,比如解析函数参数的顺序
undefined behavior
未定义行为,简单理解就是某个操作逻辑上是不合法的,但是 C++ 标准并没有规定发生这种情况需要如何处理(而是将避免未定义行为的责任交给用户,这可以简化编译器的设计以及生成能更高效运行的代码)。因此发生未定义行为时,程序可能崩溃,可能给出错误结果,也可能运行正常
常见的未定义行为(C++11)
常见的未定义行为如:数组越界访问、解引用悬挂指针等等
C++11 引入了多线程感知内存模型,由多线程造成的数据竞争也是一种未定义行为
实践场景
这也是产生重新认识未定义行为的源头,平时的代码中确实是有未定义行为的,但实际使用过程中并未发生问题
获取线程池队列大小
比如线程池队列是使用std::vector实现的,任务进队与出队都有加锁,但调用获取队列大小时是直接返回std::vector的size成()员函数的结果,根据标准库的定义std::vector::size,这属于一种未定义行为(数据竞争),但实际上并未发生明显问题
float类型的序列化
本质上就是将float数据以字节数组的方式保存下来,然后int32_t与float占相同大小,它们的序列化方式是通用的。比如 JAVA 语言中的 Float 类提供了 floatToIntBits 与 intBitsToFloat 方法,利用这两个方法可以实现对 float 类型数据的序列化与反序列化
由于float的范围比int32_t大,直接赋值有可能会溢出,这属于未定义行为。但将float以字节数组的方式保存下来(保存在int类型数据中,如果用 C++ 实现 Utils::FloatToIntBits 与 Utils::IntBitsToFloat 并不属于未定义行为
参考
Syntax and semantic errors
Undefined behavior
What are all the common undefined behaviours that a C++ programmer should know about?
Undefined, unspecified and implementation-defined behavior
Memory model