一、C++四个层级¶
C++的难学,还在于它提供了四种不同(但相辅相成)的程序设计思维模式:
- procedural-based
- object-based
- object-oriented (封装,继承,多态)
- generics
C++ Primer Plus从以下章节开始,
- 将在1、2、3章记录procedural-based。
- 在第4章记录object-based、object-oriented
- 在第5章记录generics
- 在第6、7、8章记录stl、boost等工具。
而自己cpp系列的笔记,是从上学期间到工作初期整理的笔记,并不适合初学,在大模型时代,C++可能也没必要死记那知识点,仅做C++知识结构化的一个参考吧。
C语言入门参考:The Book – The Little Book of C
二、程序基本结构¶
2.1 Token(词元)¶
空白(white space): 通常,必须用空格、制表符或回车将两个标记(Token)分开。
非通常的例子:#define foo()test, a+=++b;
标记包括:
- identifiers
- variable、function name:
变量名要求
^[A-Za-z_][A-Za-z_0-9]*$,且不能是keyword。C++支持通用字符名(universal character name)。 以两个下划线或下划线和大写字符打头的名称被保留给实现(编译器及其资源)使用。 - keyword: cppreference: keyword
sizeof:除指针外,用于获取变量所占的字节数。typedef:如果放在所有函数之外,它的作用域就是从它定义开始直到文件尾;如果放在某个函数内,定义域就是从定义开始直到该函数结尾;- 内建类型(POD, Plain Old Data):
voidboolcharshortintlongfloatdoublesignedunsigned - 标准对宽度的约束:
char(≥8) ≤short(≥16) ≤int(≥16) ≤long(≥32) ≤long long(≥64),单位为位。 - 极值C语言具体见头文件
climits,C++std::numeric_limits<int>::max()。 - 类型选择:可用性-极值和符号;效率-自然长度(natural size);成本-存储大小。
- 存储说明符(控制 存储连续性和链接性)
- auto(在C++11中不再是说明符)
- register:C语言中提示编译器把变量放入寄存器,且禁止用
&取址;C++中register仅仅是一个被保留的关键字(C++11弃用、C++17移除),语义上没有任何效果,也允许对其取地址。这种方式特别适用于不需要经常修改,但是会被频繁访问的变量,这样可以一定程度上提高访问效率。 - static:总是具有连续性。在C++中,程序块中不具有链接性,文件中具有内部链接性,类中具有外部链接性。由于函数无存储,故只表达其链接性。
- 函数内的静态对象称为 local static对象,反之称为non-local static对象。
- extern:声明/定义外部链接性的静态连续存储变量。除const外定义时可省略;声明中函数可省略,变量不可省略,且与
=、{}互斥。 - thread_local (C++11新增):线程存储连续性
- mutable: 即使结构(或类)为const,其修饰的成员依然可以被修改。
- cv限定符
- const: 运行时常量限定。C++中,默认下全局变量的链接性是外部的,但const全局变量的链接性是内部的,即在C++中等效于
static const,所以一般在头文件中声明。允许使用extern const。P318- 可施加:对象(指针常量,常量指针)、函数形参、函数返回类型、成员函数本体。
- 作用为帮助编译器侦测出错误。编译器强制实施bitwise constness,对于使用者则为conceptual constness。
- 应用:四则运算重载的const叠满,避免if中存在赋值和对运算后的临时变量赋值等问题。常量复合对象会根据const判断所调用的方法。
- constexpr: 编译时常量限定。constexpr:在C++11中,当返回值为constexpr时,函数的内只能有一行。C++14解除了这个限制。
- volatile: 指明变量在程序执行中可被隐含地改变。volatile所做的就是要求变量必须保存在内存中,而不允许被编译器进行优化放入缓存或者寄存器等等。在多线程情况下,并且多个线程对这个变量会进行频繁读写的话,那就会出现脏数据。
const volatile一般用于只读寄存器。 - 类型转换运算符:
xxx_cast<type-name> (expression) - static_cast:强迫隐式转换。仅当type_name可被隐式转换为expression或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的。即支持【父⇌子】类指针的相关转换。应用
int→double,void*→type* - const_cast:改变常量性(const)或变易性(volatile)。注意:转换定义时就是const的变量为非const,能否真正修改是由编译器决定的(放在ROM区还是RAM区),而非语言本身能控制的。
- reinterpret_cast:一般用于数值与地址之间的相互转换。依赖于实现的底层编译技术,是不可移植的。不推荐反复转换,大概率会触发编译器bug。例子:
int*→int - dynamic_cast:最常用的RTTI组件(Run-Time Type Identification)。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。应用场景:想在一个你认定为derived class对象身上执行 derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference时。该函数CPU消耗大,一般是可以通过合理的程序设计来避免使用的。(条款27)
- 注:
char ch = char(d&)在C中是允许的,但在C++中是不允许的。「对象生成」的动作不怎么像转换,所以推荐使用函数风格(C风格)的转换。 - 注:转换失败,可抛出
bad_cast类型的异常。 - 普通运算符:优先级
- new / delete:
- delete一个指针之后,只是回收指针指向位置的空间,而指针本身的值不变。你需要手工将其赋值为NULL。
- delete NULL的话不会有任何事情发生。
- nullptr(C++11):(M条款8) 使用nullptr代替NULL:nullptr的类型为std::nullptr_t,可以隐式转换到所有的裸指针型别。使用nullptr避免了重载参数为int和void*参数的歧义。也避免了nullptr在模板类型转换过程中被识别为int 0。(注:智能指针目前可直接接收NULL,并识别为空指针)
https://en.cppreference.com/w/cpp/named_req/StandardLayoutType
| 数据类型 | LP64(linux64) | ILP64 | LLP64 (win64) | ILP32(win32 , linux32) | LP32 |
|---|---|---|---|---|---|
| pointer | 64 | 64 | 64 | 32 | 32 |
| char | 8 | 8 | 8 | 8 | 8 |
| short | 16 | 16 | 16 | 16 | 16 |
| __int32 | N/A | 32 | N/A | N/A | N/A |
| int | 32 | 64 | 32 | 32 | 16 |
| long | 64 | 64 | 32 | 32 | 32 |
| long long | 64 | 64 | 64 | 64 | 64 |
注1:基本类型的宽度是实现定义的(由编译器厂家文档说明),但具体取值由平台 ABI钉死——同一平台上的 GCC/Clang/MSVC 必须一致,否则互不兼容。
注2: 参数极限值可通过std::numeric_limits<T>获取
标准与实现:基本类型宽度谁说了算
C++ 标准只规定宽度的偏序关系(char ≤ short ≤ int ≤ long ≤ long long)和下限(char≥8、short≥16、int≥16、long≥32、long long≥64 位),具体取值是实现定义(implementation-defined)。
真正把数值钉死的不是编译器自己拍脑袋,而是**平台 ABI**:
- **Linux/macOS x86-64** 遵循 **Itanium C++ ABI** 配合 System V AMD64 PSI,数据模型为 **LP64**(`long` 与指针 8 字节、`int` 4 字节)。
- **Windows x86-64** 遵循 **MSVC ABI**,数据模型为 **LLP64**(`long` 仍是 4 字节、指针 8 字节)——这正是跨平台代码用 `long` 会踩坑的根源。
- 32 位 ARM 嵌入式(如 Cortex-M)走 **AAPCS**,多为 ILP32。
- `__int32`/`__int64` 这种带双下划线的是 **MSVC 厂家扩展**,不属于标准类型;标准写法是 `#include <cstdint>` 里的 `int32_t`/`int64_t`(同样是实现定义,但有了固定宽度的别名)。
所以:跨平台代码请用 `<cstdint>` 的 `int32_t`/`int64_t`/`intptr_t`,不要把 `long` 当成 8 字节用。
| 存储描述 | 连续性 | 作用域 | 链接性 | 如何声明 |
|---|---|---|---|---|
| 自动 | 自动 | 代码块 | 无 | 在代码块中 |
| 寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字 register |
| 静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字 static |
| 静态,内部链接性 | 静态 | 文件(翻译单元) | 内部 | 不在任何函数中,使用关键字 static ,若为内联函数,则使用inline |
| 静态,外部链接性 | 静态 | 全局 | 外部 | 不在任何函数中。定义和声明中可以用 extern. 类中的static对象。 |
| C/C++链接性 | -- | -- | C/C++链接性 | C链接性使用:extern "C",C++连接可以使用:extern "C++" |
| 堆连续性 | 堆连续性 | -- | -- | new,delete,定位new运算符new() var,delete var``new [],delete [] ``malloc,free |
注1:静态变量初始化分为:静态初始化(即直接通过「字面值、字符串或constexpr修饰的变量」来初始化,在编译时确定初值),动态初始化(在程序执行时确定初值)。 注2:具有链接性,不意味着一定可以调用,因为它还受到访问属性、是否为友元函数等的控制(public、protect、private)。 注3:lambda表达式和函数(即代码块)一样隔绝作用域的功能。 注4:(个人理解)在大多数场景下,作用域和链接性有一一对应的关系。
Q: 类中的static成员具有全局链接性,那么为什么类中的静态成员不用extern关键字?
A: 个人认为两种语法定义方式各有优劣,但都很别扭。extern关键字在C++中通常用于声明一个变量,而不是定义它(extern int x = 1;的确是定义)。extern即可以用于声明,也可用于定义。但static只可用于定义。
-
numbers: 字面值是指以人类可读形式表示的固定值。
-
浮点数:
- 正则表达式表示:
"+?^((([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))([Ee][-+]?[0-9]*[1-9][0-9]*)?$" //正浮点数"^((-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))))([Ee][-+]?[0-9]*[1-9][0-9]*)?$" //负浮点数 - 后缀:单精度
f/F,默认双精度 ,L/l为long double - 注意浮点数于0的比较。
-
整数字面值:除非有理由存储为其他类型(如果使用特殊的后缀来表示特定的类型,或者值太大,不能存储为
int),否则C++将整型常量存储为int。 -
前缀:
0x - 后缀:
l/L,u/U,ll/LL,ull/ULL - 字符字面值:用单引号
''引起来。 - 默认类型
char: 是与signed char、unsigned char都不同的独立类型;它到底按有符号还是无符号处理,由实现决定。- 常规字符(ascii中的可显示字符):符号/字符A的字面值为
'A'或65;'A'为65,65是符号A的(十进制)ASCII码。 - 特殊字符——ascii中的控制字符:符号(名称)——换行的ASCII符号为
NL(LF),字面值为0xA、10、'\n'。\n为转义序列。 - 特殊字符——其他编码的字符。使用Unicode编码,
\u后面是4个十六进制位,\U后面是8个十六进制位。
- 常规字符(ascii中的可显示字符):符号/字符A的字面值为
wchar_t:添加前缀L,宽度由实现决定(Windows 上 2 字节、Linux/macOS 上 4 字节)char16_t:使用前缀uchar32_t:使用前缀U
标准与实现:char 的符号性与 wchar_t 的宽度
char的符号性:实现定义。具体取值由编译器厂家决定并写入文档——GCC/Clang 用-fsigned-char/-funsigned-char切换,x86/x86-64 默认有符号,ARM 平台(AAPCS)默认无符号。这是跨平台做字符算术时容易踩的坑。-
wchar_t的宽度:同样是实现定义,但被 ABI 钉死——Windows(MSVC ABI)固定 2 字节(UTF-16),Linux/macOS(Itanium ABI)固定 4 字节(UTF-32)。这也是跨平台处理 Unicode 时推荐改用char8_t/char16_t/char32_t(C++11 起,宽度由标准固定)的原因。 -
C++14支持
1'000'000表示 1 million. - string literals
- 正则表达式表示:
-
用双引号
""引起来,最后一个字符后自动填充空NULL; - 长字符串表示
R""(...)"",两个字符串自动拼接"part1" "part2"; - 使用上与字符字面值基本一致。
-
punctuators
-
成员运算符:
. - other
2.1.1 type¶
- Object types(CSDN:linuxheik 的类型分类笔记)
- Scalars
- arithmetic (integral, float)
- pointers: T * for any type T
- enum
- pointer-to-member
- nullptr_t
- Arrays: T[] or T[N] for any complete, non-reference type T
- Classes: class Foo or struct Bar
- Trivial classes
- Aggregates
- POD classes
- (etc. etc.)
- Unions: union Zip
- References types: T &, T && for any object or free-function type T
- Function types
- Free functions: R foo(Arg1, Arg2, ...)
- Member functions: R T::foo(Arg1, Arg2, ...)
- void
2.2 表达式¶
(a=5,b=2,a>b?a++:b++,a+b)的值是8
说明a++运算时遇到逗号或分号时增加。
2.2.1 基础类型转换¶
隐式转换(自动类型转换)
- 发生在:算术运算,参数传递,赋值
- 算术运算的转换规则(有转换优先级):
- 内建类型:整型向浮点型转换;(整型的)低位宽向高位宽转换;(整型的)有符号向无符号转换。有符号整数溢出在 C/C++ 标准里是未定义行为(详见下方「undefined」一节)。
- 指针类型:对于指向内建类型的指针,「指针间转换时」,或「指针和整型互相转换时」,指针指向的地址不变。避免指针和整型反复转换,很多编译器容易触发难以发现的bug,因此对于ota升级等涉及底层的代码,推荐地址一开始一直使用整型进行运算,直到最后再转为指针。 显示转换:类似于隐式转换的参数传递和赋值。
标准与实现:整数溢出与指针↔整数转换
- 有符号整数溢出(如
INT_MAX + 1)是真正的未定义行为(Undefined Behavior):没有任何人"定义"它如何回绕,编译器可基于"未定义行为不会发生"做优化(典型表现:溢出检查被删掉)。需要回绕语义请用无符号类型,或 GCC/Clang 的-fwrapv扩展(编译器厂家定义)。 - 无符号整数溢出是良定义的:按 2^n 回绕,标准明确规定。
- 指针与整数的相互转换:结果是实现定义的(取值由编译器厂家文档),且只有把指针转成足够大的整数再转回来才保证可用;
reinterpret_cast在两者间的具体位级映射同样由实现定义,不可移植。
2.3 语句¶
- 声明语句:
- 引用声明(reference declaration), 简称声明
- 定义声明(defining declaration), 简称定义。定义声明一般会进行初始化(initialization)。
程序必须记录3个基本属性:信息将存储在哪里(由链接器或指针确定);要存储什么值(初始化或赋值时确定);存储何种类型的信息(定义中确定)。
- 未初始化:
int owls;//value is unknown - 等号初始化:
int uncles =5;int aunts =uncles; - 大括号初始化:可防止类型转换,
int emus{7};,int rocs{};//set to 0,int rocs={};
- 未初始化:
声明:告诉编译器某个标识符的存在和它的类型,但不分配内存。 定义:实际创建标识符,为其分配内存空间(对于变量),或提供实现代码(对于函数)。
- 赋值语句:
- 消息语句
- 函数调用:
Function arguments are the real values passed to the function.
注:C++从未明确定义函数调用动作中各参数的评估顺序——这是未指定(unspecified)行为(注意不是未定义行为):标准允许几种合法结果,编译器无需文档化选哪种,但每个参数本身仍要被正确求值;若参数表达式之间存在副作用冲突(如
f(i++, i++)),才会升级为未定义行为。 - 函数原型 函数原型描述了函数接口,即如何与程序的其他部分交互。 Function parameters are the names listed in the function's definition.
- 返回语句
在一些语言中,有返回值的函数被称为函数(function);没有返回值的函数被称为过程(procedure)或子程序(subroutine)。
函数
类是用户定义的一种数据类型 类之于对象就像类型之于变量。类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
三、编译¶
Language servers allow you to add your own validation logic to files open in VS Code. Typically you just validate programming languages. However validating other file types is useful as well. A language server could, for example, check files for inappropriate language. VS Code: Language Server Extension Guide
The Language Server protocol is used between a tool (the client) and a language smartness provider (the server) to integrate features like auto complete, goto definition, find all references and alike into the tool. Microsoft/language-server-protocol (GitHub)
编译过程:预处理 → 编译 → 汇编 → 链接 编译工具(toolset):Clion、Xcode、dev C++、VS,这些都是IDE,而不是编译器。Clion和dev C++默认都是gcc (mingw32),Xcode用的是clang,VS 使用 msvc (Microsoft Visual C++,注意不是编辑器 Visual Studio Code)的cl.exe。嵌入式编译器有:armcc。 代码检查工具:cpplint,coverity,clang-tidy(clangd子集)等。 语言服务器:cquery, ccls, clangd 调试器:gdb(GNU Debugger),lldb(LLVM Debugger)
3.1 编译选项¶
注:如未显示声明,则默认适用于GCC。
g++ -o <a.out> <main.cpp>(编译 C++ 用 g++;gcc 默认按 C 处理且不自动链接 libstdc++)
-L指定库的路径-l指定需连接的库名-ggdb调试的时候需要-Dmacro:定义宏macro-I: 指定include路径-std=c++11,-std=c++2a(C++20)-Wall生成所有警告信息。-l pthread:多线程
版本问题:g++ -std=c++2a -E - < /dev/null(用预定义宏探测当前 GCC 对各版本特性的支持)
参考:GCC C++ 标准支持状态、Clang C++ 标准支持状态、Stack Overflow: what is the value of __cplusplus for C++17
- [ ] 待确认:gcc 从官网上看语法特性应该是支持了,但实测编译还是无法通过——需要核对该特性的具体宏开关与编译选项。
gcc和g++的主要区别:知乎:gcc 与 g++ 的区别
- 对于
*.c和*.cpp文件,gcc分别当做c和cpp文件编译(c和cpp的语法强度是不一样的) - 对于
*.c和*.cpp文件,g++则统一当做cpp文件编译 - 使用g++编译文件时,g++会自动链接标准库STL,而gcc不会自动链接STL
- gcc在编译C文件时,可使用的预定义宏是比较少的
- gcc在编译cpp文件时/g++在编译c文件和cpp文件时(这时候gcc和g++调用的都是cpp文件的编译器),会加入一些额外的宏。
- 在用gcc编译c++文件时,为了能够使用STL,需要加参数
-lstdc++,但这并不代表gcc -lstdc++和g++等价,它们的区别不仅仅是这个。
RTTI:
- cl 编译器中编译这个代码片段,需要显式地打开
/GR切换开关。 - g++ 编译器不需要任何特殊的选项以打开 RTTI。lyssoft.cn:VS 与 GCC 对 RTTI 实现的对比
函数上下文(调用约定):
| 调用名称 | cl | g++ | 说明 |
|---|---|---|---|
| C调用 | __cdecl |
__attribute__((cdecl)) |
使用栈来传递参数,从右向左将参数入栈,由调用者恢复栈 |
| 标准调用 | __stdcall |
__attribute__((stdcall)) |
也使用栈从右至左来传递参数,在函数内部恢复栈帧 windows api一般采用这个方式 |
| 快速调用 | __fastcall |
__attribute__((fastcall)) |
fastcall将使用寄存器和栈结合的方式传递参数,ecx为第一个参数,edx为第二个参数,其余的参数从右向左入栈。微机一般使用这种方式或类似方式。 |
标准与实现:调用约定由谁定义
调用约定(哪些参数进寄存器、哪些进栈、谁清栈、名字修饰)C++ 标准完全不管,完全由 ABI 规定:
- **Linux x86-64 / macOS**:System V AMD64 + **Itanium C++ ABI**——前 6 个整数/指针参数走 `rdi/rsi/rdx/rcx/r8/r9`,浮点走 `xmm0-7`,栈由调用者清。
- **Windows x86-64**:**MSVC ABI**——前 4 个参数(不限整浮)走 `rcx/rdx/r8/r9`,调用者清栈。
- **32 位 Windows**:即上表 `__cdecl`/`__stdcall`/`__fastcall` 那套(MSVC ABI)。
- **ARM(含 32/64 位)**:**AAPCS**——`r0-r7` 传参(aapcs32)、`x0-x7`(aapcs64)。
换句话说:C++ 函数签名相同 ≠ 二进制兼容。跨编译器/跨平台动态链接必须先对齐 ABI。
修改对齐方式:
- cl:
/Zp(MSVC: /Zp 结构成员对齐),typedef __declspec(align(32)) struct { int a; } S;(MSVC: alignof Operator) - g++:
struct T1 { int n1; double d1;} __attribute__((aligned(8)));
指定静态对象地址:
- armcc:
int value __attribute__((section(".ARM.__at_0x20000000"))) = 0x33;
提示:
__attribute__((warn_unused_result)) :
如果一个方法的返回值比较重要,则提醒开发人员检查返回值是否被使用。
__attribute__((unused)) static void func(void):
如果定义了一个静态函数,而没有去使用,编译时会有一个告警,而使用该字段可以告诉编译器忽略此告警。
void func(int unusedPara __unused){ return;}:输入参数未使用时不提示。
分支预测:
#define __ulog_likely(_cond) __builtin_expect((_cond), true)
#define __ulog_unlikely(_cond) __builtin_expect((_cond), false)
3.2 预处理指令(GCC: The C Preprocessor)¶
The C preprocessor, often known as cpp, is a macro processor that is used automatically by the C compiler to transform your program before compilation. It is called a macro processor because it allows you to define macros, which are brief abbreviations for longer constructs.
CPP 的第一个动作,甚至在寻找行边界之前,就是将文件转换为它用于内部处理的字符集。它必须与 ISO 10646(也称为 Unicode)同构。 CPP 使用 Unicode 的 UTF-8 编码。所有预处理工作都在源字符集(source character set)中进行。 预处理完成后,再次将字符串和字符常量转换为执行字符集(execution character set),但八进制和十六进制转义序列(escape sequences)不进行转换。
源字符集即可用来编写代码的字符集。
- 预处理宏扩展:expansion of predefined macros:
#define,#undef, 常用于实现头文件的防护(guarding)方案。 - 预处理器指令(directives ):
- 编译指令:
#include - 诊断(Diagnostics)指令:
#error,#warning - 条件编译指令:
#if#endif#elif#else - 注释命令:
//,/* */ - 行控制指令
预处理命令常量:__LINE__, __FUNCTION__, __DATE__,__TIME__
预处理
3.3 编译指令¶
3.4 using 和 名称查找法则(name lookup rules)¶
即依赖名称查找(ADL)问题。
应用:希望调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本。
template<typename T> void doSomething(T& obj1, T& obj2) {
using std::swap;
swap(obj1, obj2); //为T型对象调用最佳swap版本。优先在global或T所在之命名空间内的任何T专属的swap。如果没找到,则使用std::swap
}
四、编译过程(参考CSDN:weixin_50964793 的编译流程整理)¶
可能大家觉得为什么微软声称要遵照标准之后还是有很多神奇的地方,我认为大概是因为为了和上一代自己的编译器尽可能的兼容。知乎:MSVC 为什么不那么标准? 微软的客户企业更新还是相对慢和保守,升级一下编译器,代码全部不兼容了,客户一定要炸毛。 MS背负的这种所谓不“兼容”的无端指责实在太多了。 其实事实上是它当初搞这一套东西的时候还没有标准或者标准还不完善。
trivial copy-assignment operator
五、------重新理解表达式及表达式序--------¶
下面的内容以英文原文为主,转译可能引入不准确的表达。
六、base terms¶
[undefined](参考CSDN:MDwalu 的未定义行为笔记、知乎:C++ 未定义行为) 未定义行为是在某些不正确的情况下,标准并未规定应该怎样做,实现可以采取任何行动,也可以什么都不做。 如当有符号整数溢出时该采取什么行动。 [defns.undefined] behavior for which this International Standard imposes no requirements. [Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. Evaluation of a constant expression never exhibits behavior explicitly specified as undefined (8.20). — end note]
[unspecified] 未确定行为是在某些正确的情况下,标准并未规定应该怎样做。 如函数参数的求值顺序。 [defns.unspecified] behavior, for a well-formed program construct and correct data, that depends on the implementation [Note: The implementation is not required to document which behavior occurs. The range of possible behaviors is usually delineated by this International Standard. — end note]
[implementation-defined] 实现定义的行为是由编译器设计者决定采取何种行动,并写入实现文档(如 GCC/Clang/MSVC 各自手册)。 如当一个整型数向右移位时,要不要扩展符号位。 [defns.impl.defined] behavior, for a well-formed program construct and correct data, that depends on the implementation and that each implementation documents.
辨析:实现定义 vs ABI vs 真·未定义行为
笔记原文把"实现定义"等同于"编译器设计者决定",这只是第一层。对涉及二进制布局的那部分实现定义行为,编译器没有自由,必须跟着平台 ABI 走——同一平台下 GCC/Clang/MSVC 取值一致。归纳一下三类典型:
- **纯实现定义、编译器可独立选择**:有符号 `>>` 是否算术右移、`int` 的位宽上限、`sizeof(wchar_t)` 取值、`__int128` 是否支持。这些写在各家编译器文档里。
- **ABI 决定、编译器无选择**:基本类型宽度(LP64/LLP64)、`sizeof(指针)`、结构体布局/padding、对齐、vtable 布局、名字修饰、调用约定。
- **真正的未定义行为、无人定义**:有符号整数溢出、越界访问、解引用空指针/野指针、未初始化变量读取、违反 ODR、数据竞争、双重 free、`i++ + i++`——编译器优化会基于"这些不会发生"做假设,依赖观察到的行为是危险的。
[ill-formed] 通常是那些在语法上就不满足标准规定的东西.通常编译器无法编译这种代码(标准要求编译器对此发出诊断).
speculative loads(Intel: 推测执行侧信道分析)
Control flow Enforcement Technology (CET)
七、statements¶
Statements are fragments of the C++ program that are executed in sequence. The body of any function is a sequence of statements.cppreference: statements
1) labeled statements;
2) expression statements; n = n + 1;
3) compound statements; {...}
4) selection statements;if(...){...}
5) iteration statements; while(...){...}
6) jump statements;return ...;,goto ...;,break;,continue;
7) declaration statements; int n = 1;
8) try blocks;try{...}catch{...}
9) atomic and synchronized blocks (TM TS). 事务内存 cppreference: transactional_memory (Note: __cpp_transactional_memory with a value equal or greater 201505)
语句可以嵌套,完整表达式直接在 「表达式语句」或「返回语句」或「声明语句」中。
八、expression¶
An expression is a sequence of operators and their operands, that specifies a computation.cppreference: expressions freeCodeCamp: Statement vs Expression
- Primary expressions:
this, literals, id-expressions, lambda-expressions(C++11), fold-expressions(C++17), requires-expressions(C++20) -
constituent expression: (针对花括号和括号而定义的名词)
-
1.itself ;
- 2.the constituent expressions of the elements of the respective list.
- 3....
-
immediate subexpressions:
-
- the constituent expressions of E’s operands ;
-
- any function call that E implicitly invokes;
-
- if E is a lambda expression, the initialization of the entities captured by copy and the constituent expressions of the initializer of the captures,
- if E creates an aggregate object, the constituent expressions of each default member initializer used in the initialization.
- subexpression: A subexpression of an expression E is an immediate subexpression of E or a subexpression of an immediate subexpression of E.
- full-expression: A full-expression is an expression that is not a subexpression of another expression. (充分但不必要)
- discarded-value expression: A discarded-value expression is an expression that is used for its side-effects only (not used to value computations).
再例:*p = i++;表达式中,*p = i++为 完全表达式,*p和i++为上述完整表达式的直接子表达式,且这两个直接子表达式的evaluation顺序是由编译器决定的。
8.1 postfix expressions¶
Postfix expressions consist of primary expressions or expressions in which postfix operators follow a primary expression. The postfix operators are listed in the following table.
| Operator Name | Operator Notation |
|---|---|
| Subscript operator | [ ] |
| Function call operator | ( ) |
| Explicit type conversion operator | type-name ( ) |
| Member access operator | . or -> |
| Postfix increment operator | ++ |
| Postfix decrement operator | -- |
8.2 Evaluation¶
Evaluation of each expression includes:
- Value computations
- Initiation of side effects:
- access (read or write) to an object designated by a volatile glvalue,
- modification (writing) to an object,
- calling a library I/O function,
- or calling a function that does any of those operations.
8.3 expression order¶
8.3.1 C and C++98/03¶
At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
Except where noted, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.
8.3.1.1 occur places¶
- Between evaluation of the left and right operands of the
&&(logical AND),||(logical OR) (as part of short-circuit evaluation), and comma operators. For example, in the expression*p++ != 0 && *q++ != 0, all side effects of the sub-expression*p++ != 0are completed before any attempt to accessq. - Between the evaluation of the first operand of the ternary "question-mark" operator and the second or third operand. For example, in the expression
a = (*p++) ? (*p++) : 0there is a sequence point after the first*p++, meaning it has already been incremented by the time the second instance is executed.(Note: cannot be overload) - At the end of a full expression. This category includes expression statements (such as the assignment
a=b;), return statements, the controlling expressions ofif,switch,while, ordo-whilestatements, and all three expressions in a for statement. - (C++2003)Before a function is entered in a function call. The order in which the arguments are evaluated is not specified, but this sequence point means that all of their side effects are complete before the function is entered. In the expression
f(i++) + g(j++) + h(k++), f is called with a parameter of the original value ofi, butiis incremented before entering the body off. Similarly,jandkare updated before enteringgandhrespectively. However, it is not specified in which orderf(),g(),h()are executed, nor in which orderi,j,kare incremented. If the body offaccesses the variablesjandk, it might find both, neither, or just one of them to have been incremented. (The function callf(a,b,c)is not a use of the comma operator; the order of evaluation fora,b, andcis unspecified.) - (C++2003)At a function return, after the return value is copied into the calling context. (This sequence point is only specified in the C++ standard; it is present only implicitly in C.[7])
- At the end of an initializer; for example, after the evaluation of
5in the declarationint a = 5;. - Between each declarator in each declarator sequence; for example, between the two evaluations of
a++ in int x = a++, y = a++.[8] (This is not an example of the comma operator.) - After each conversion associated with an input/output format specifier. For example, in the expression
printf("foo %n %d", &a, 42), there is a sequence point after the%nis evaluated and before printing42.
1和2指明运算符;3指语句运行结束时;4和5指明函数调用;6、7、8是不关注或理解错都不会有问题的。
8.3.2 limitation¶
Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.博客园:a3192048 的序列点笔记
extern int i, a[];
extern int foo(int, int);
i = ++i + i; // undefined
a[i++] = i; // undefined
foo(foo(0, i++), i++); // undefined
在C语言中,assignment operators的结果是non-lvalue,C++2003则将assignment operators的结果改成了lvalue。则C++2003的不确定行为有:
i = j = 1; // undefined in C++2003
i = ++j; // undefined in C++2003
(i += 1) += 2; // invalid in C99
8.4 since C++11¶
- (precedence operation) is sequenced before,(subsequent operation) is sequence after cppreference: eval_order
- unsequenced (CPU instructions may overlap)
- is indeterminately sequenced with
1) Each value computation and side effect of a full-expression is sequenced before each value computation and side effect of the next full-expression.
2) The value computations (but not the side effects) of the operands to any operator are sequenced before the value computation of the result of the operator (but not its side effects).
3) When calling a function (whether or not the function is inline, and whether or not explicit function call syntax is used), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.
4) The value computation of the built-in post-increment and post-decrement operators is sequenced before its side effect.
5) The side effect of the built-in pre-increment and pre-decrement operators is sequenced before its value computation (implicit rule due to definition as compound assignment).
6) Every value computation and side effect of the first (left) argument of the built-in logical AND operator && and the built-in logical OR operator || is sequenced before every value computation and side effect of the second (right) argument.
7) Every value computation and side effect associated with the first expression in the conditional operator ?: is sequenced before every value computation and side effect associated with the second or third expression.
8) The side effect (modification of the left argument) of the built-in assignment operator and of all built-in compound assignment operators is sequenced after the value computation (but not the side effects) of both left and right arguments, and is sequenced before the value computation of the assignment expression (that is, before returning the reference to the modified object).
9) Every value computation and side effect of the first (left) argument of the built-in comma operator , is sequenced before every value computation and side effect of the second (right) argument.
10) In list-initialization, every value computation and side effect of a given initializer clause is sequenced before every value computation and side effect associated with any initializer clause that follows it in the brace-enclosed comma-separated list of initializers.
11) A function call that is not sequenced before or sequenced after another expression evaluation outside of the function (possibly another function call) is indeterminately sequenced with respect to that evaluation (the program must behave as if the CPU instructions that constitute a function call were not interleaved with instructions constituting evaluations of other expressions, including other function calls, even if the function was inlined).
12) The call to the allocation function (operator new) is indeterminately sequenced with respect to (until C++17)~~sequenced before (since C++17)~~ the evaluation of the constructor arguments in a new-expression.
13) When returning from a function, copy-initialization of the temporary that is the result of evaluating the function call is sequenced before the destruction of all temporaries at the end of the operand of the return statement, which, in turn, is sequenced before the destruction of local variables of the block enclosing the return statement.
在新的表述下,1、2、3、6、7、9、11与之前的基本一致。10只是对初始化列表进行补充,符合直观预期。
- 12说明new及其参数的先后关系是不确定,13应该是表达 函数可以返回临时对象给拷贝初始化对象(原文没看懂)
- 4、5:还是原来的配方,但估计是相关运算被人诟病太多,所以写明此点,从而更好地和8组合。
- 8 以前赋值运算符没有序列点,导致两个序列点间可能有两个side effect,导致未定义行为。现在。使得
i= ++i的行为得到定义,最终i的值为i+1(CSDN:shuangguo121 的 sequenced-before 笔记)。因为++i的side effect 先序于 computation;而赋值运算左右两边的computation 又先序于 赋值本身的side effect(即修改左值);最终赋值本身的side effect 又先序于 赋值本身的computation。同理有理由认为i=i++是未定义的。
本质上,sequence before相较于 sequence point,只是提供了把 computation 和 side effect 分开的能力。
8.5 since C++17¶
11) The rule 11 has one exception: function calls made by a standard library algorithm executing under std::execution::par_unseq execution policy are unsequenced and may be arbitrarily interleaved with each other.
12) In a function-call expression, the expression that names the function is sequenced before every argument expression and every default argument.
13) In a function call, value computations and side effects of the initialization of every parameter are indeterminately sequenced with respect to value computations and side effects of any other parameter.
14) Every overloaded operator obeys the sequencing rules of the built-in operator it overloads when called using operator notation.
15) In a subscript expression E1[E2], every value computation and side effect of E1 is sequenced before every value computation and side effect of E2.
16) In a pointer-to-member expression E1.*E2 or E1->*E2, every value computation and side effect of E1 is sequenced before every value computation and side effect of E2 (unless the dynamic type of E1 does not contain the member to which E2 refers).
17) In a shift operator expression E1 << E2 and E1 >> E2, every value computation and side effect of E1 is sequenced before every value computation and side effect of E2.
18) In every simple assignment expression E1 = E2 and every compound assignment expression E1 @= E2, every value computation and side effect of E2 is sequenced before every value computation and side effect of E1.
19) Every expression in a comma-separated list of expressions in a parenthesized initializer is evaluated as if for a function call (indeterminately-sequenced).
15对一个已知的函数多参数场景进行说明。 16提升了用户重载运算符号的能力。 17、18进一步丰富了postfix expressions的语义支持能力,从左往右,更符合预期。 19和17、18同理,但不清楚其添加的意义是什么,毕竟大多数二值运算符左右两边是indeterminately sequence关系。 20是各个好设定,感觉应该是直接干掉了8中复杂的语义,从右往左,非常符合直观感觉。
i = ++i + 2; // well-defined
//1. ++i computation 先序于 side effect
//2. ++i computation 先序于 ++i + 2 computation, 但side effect 未先序于。
//3. i 和 ++i + 2先序于 赋值计算; 所以 左边是i的左值, 右边是i存储为1,且值为2。
//4. i赋值为2。
i = i++ + 2; // undefined behavior until C++17
f(i = -2, i = -2); // undefined behavior until C++17, 15条
f(++i, ++i); // undefined behavior until C++17, unspecified after C++17
//1. a 先序于 ++k
//2. ++k computation 先序于 a[++k] computation, 但side effect 未先序于。
//3. a[++k] 先序于 k。 若k=0,则k先存储为1, 然后再取a[1] = 1
//则以 a[1] = 1
//但最终结果为a [0] ==0, 说明gcc --std=17版本支持还不完整
a[++k] = k;
i = ++i + i++; // undefined behavior
总之,在无法确定各个平台对 C++ 特性是否完整实现前,还是遵循 C99 limitation 为好。
九、memory ordering¶
9.1 sequential consistent ordering¶
If all operations on instances of atomic types are sequentially consistent, the behavior of a multithreaded program is as if all these operations were performed in some particular sequence by a single thread.(a simple sequential view of the world)
It also means that operations can’t be reordered; if your code has one operation before another in one thread, that ordering must be seen by all other threads.
Unless you specify otherwise for a particular operation, the memory-ordering option for all
operations on atomic types is memory_order_seq_cst, which is the most stringent of the available options.
Any sequentially consistent atomic operations done after that load must also appear after the store to other threads in the system using sequentially consistent atomic operations.
9.2 relaxed ordering¶
They only guarantee atomicity and modification order consistency. Modification order:All modifications to any particular atomic variable occur in a total order that is specific to this one atomic variable.
I strongly recommend avoiding relaxed atomic operations unless they’re absolutely necessary and even then using them only with extreme caution. 详见例5.7
9.3 acquire-release ordering¶
here’s where the acquire-release semantics kick in: if you tell the man all the batches you know about when you ask for a value, he’ll look down his list for the last value from any of the batches you know about and either give you that number or one further down the list.
注: tell the man all the batches you know是因为release-acquire原语下,acquire侧的线程将获取batch id。此后底层将提供该batch id及之后的数据,而不会给你前后不相关的数据。
Read-modify-write operations with memory_order_acq_rel semantics behave as both an acquire and a release, so a prior store can synchronize-with such an operation, and it can synchronize-with a subsequent load, as is the case in this example.
注:此时就是release-acq_rel原语对。例:P137.
9.4 compare¶
Essentially memory_order_acq_rel provides read and write orderings relative to the atomic variable, while memory_order_seq_cst provides read and write ordering globally.
happen before的作用为:
- 注: sync 为
synchronize with的缩写。表线程1上的表达式A中,施加release原语;在线程2上的表达式B中,施加acquire原语;则A synchronize with B。 - 注: dob 为
dependency order before的缩写。要求在不同线程,分别为release和consume原语,且对同一个对象分别进行写和读的操作。 - 注: cadi 为
carries a dependency into的缩写。要求在同一个线程,并进行例如B=X+1或者B=func(X)或者Y=X+1;B=Y+1;的操作。而&&、||、?:,,,std::kill_dependency将打断依赖关系(例P140)。 - 注: rA 为
read a value written (... until C++20) by A。可以看出consume原语是acquire原语的一个子集。或者说,consume原语让右边有更少的depend before,从而一定程度上提升性能。其中省略号的部分为by any part of the release sequence headed。 - 注:happen before 等术语支持传递。因此release-acquire组合不一定非得在两个线程中使用(例5.9)。
十、链接¶
Google C++ Style Guide cppreference C++参考手册 cppreference Cpp 多线程 MSVC 2022