Skip to content

Commit fd2700e

Browse files
committed
Merge branch 'main' of github.com:parallel101/cppguidebook
2 parents 04a0a05 + 8119e85 commit fd2700e

File tree

7 files changed

+214
-12
lines changed

7 files changed

+214
-12
lines changed

docs/auto.md

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,187 @@ TODO
66

77
## 变量声明为 `auto`
88

9+
```cpp
10+
int i = 0;
11+
```
12+
13+
### `auto` 声明万物的好处
14+
15+
#### 避免复读类型
16+
17+
> {{ icon.fun }} 人类的本质是复读机。
18+
19+
```cpp
20+
QSlider *slider = new QSlider();
21+
```
22+
23+
```cpp
24+
auto slider = new QSlider();
25+
```
26+
27+
TODO
28+
29+
#### 模板编程产生的超长类型名喧宾夺主
30+
31+
在 C++98 时代,仅仅只是保存个迭代器作为变量,就得写一长串:
32+
33+
```cpp
34+
std::map<std::string, int> tab;
35+
std::map<std::string, int>::iterator it = tab.find("key");
36+
```
37+
38+
这踏码的类型名比右侧的表达式都长了!
39+
40+
> {{ icon.fun }} 哮点解析:张心欣的第三条腿比另外两条腿都长。
41+
42+
有了 `auto` 以后,无需复读类型名和繁琐的 `::iterator` 废话,自动从右侧 `find` 函数的返回值推导出正确的类型。
43+
44+
```cpp
45+
std::map<std::string, int> tab;
46+
auto it = tab.find("key");
47+
```
48+
49+
#### 避免未初始化
50+
51+
因为 `auto` 规定必须右侧有赋初始值(否则无法推导类型)。
52+
53+
所以只要你的代码规范能一直使用 `auto` 的话,就可以避免未初始化。
54+
55+
众所周知,读取一个未初始化的变量是未定义行为,C/C++ 程序员饱受其苦,小彭老师也好几次因为忘记初始化成员指针。
56+
57+
例如,你平时可能一不小心写:
58+
59+
```cpp
60+
int i;
61+
cout << i; // 未定义行为!此时 i 还没有初始化
62+
```
63+
64+
但是如果你用了 `auto`,那么 `auto i` 就会直接报错,提醒你没有赋初始值:
65+
66+
```cpp
67+
auto i; // 编译出错,强制提醒你必须赋初始值!
68+
cout << i;
69+
```
70+
71+
你意识到自己漏写了 `= 0`!于是你写上了初始值,编译才能通过。
72+
73+
```cpp
74+
auto i = 0;
75+
cout << i;
76+
```
77+
78+
可见,只要你养成“总是 `auto`”的好习惯,就绝对不会忘记变量未初始化,因为 `auto` 会强制要求有初始值。
79+
80+
#### 自动适配类型,避免类型隐式转换
81+
82+
假设你有一个能返回 `int` 的函数:
83+
84+
```cpp
85+
int getNum();
86+
```
87+
88+
有多处使用了这个函数:
89+
90+
```cpp
91+
int a = getNum();
92+
...
93+
int b = getNum() + 1;
94+
...
95+
```
96+
97+
假如你哪天遇到牢板需求改变,它说现在我们的 `Num` 需要是浮点数了!
98+
99+
```cpp
100+
float getNum();
101+
```
102+
103+
哎呀,你需要把之前那些“多处使用”里写的 `int` 全部一个个改成 `float`
104+
105+
```cpp
106+
float a = getNum();
107+
...
108+
float b = getNum() + 1;
109+
...
110+
```
111+
112+
如果漏改一个的话,就会发生隐式转换,并且只是警告,不会报错,你根本注意不到,精度就丢失了!
113+
114+
现在“马后炮”一下,如果当时你的“多处使用”用的是 `auto`,那该多好!自动适应!
115+
116+
```cpp
117+
auto a = getNum();
118+
...
119+
auto b = getNum() + 1;
120+
...
121+
```
122+
123+
无论你今天 `getNum` 想返回 `float` 还是 `double`,只需要修改 `getNum` 的返回值一处,所有调用了 `getNum` 的地方都会自动适配!
124+
125+
> {{ icon.fun }} 专治张心欣这种小计级扒皮牢板骚动反复跳脚的情况,无需你一个个去狼狈的改来改回,一处修改,处处生效。
126+
127+
#### 统一写法,更可读
128+
129+
```cpp
130+
std::vector<int> aVeryLongName(5);
131+
```
132+
133+
```cpp
134+
auto aVeryLongName = std::vector<int>(5);
135+
```
136+
137+
TODO
138+
139+
#### 强制写明字面量类型,避免隐式转换
140+
141+
有同学反映,他想要创建一个 `size_t` 类型的整数,初始化为 3。
142+
143+
```cpp
144+
size_t i = 3; // 3 是 int 类型,这里初始化时发生了隐式转换,int 转为了 size_t
145+
i = 0xffffffffff; // OK,在 size_t 范围内(64 位编译器)
146+
```
147+
148+
如果直接改用 `auto` 的话,因为 `3` 这个字面量是 `int` 类型的,所以初始化出来的 `auto i` 也会被推导成 `int i`
149+
150+
虽然目前初始只用到了 `3`,然而这位同学后面可能会用到 `size_t` 范围的更大整数存入,就存不下了。
151+
152+
```cpp
153+
auto i = 3; // 错误!auto 会推导为 int 了!
154+
i = 0xffffffffff; // 超出 int 范围!
155+
```
156+
157+
由于 C++ 是静态编译,变量类型一旦确定就无法更改,我们必须在定义时就指定号范围更大的 `size_t`
158+
159+
为了让 `auto` 推导出这位同学想要的 `size_t` 类型,我们可以在 `3` 这个字面量周围显式写出类型转换,将其转换为 `size_t`
160+
161+
> {{ icon.tip }} 显式类型转换总比隐式的要好!
162+
163+
```
164+
auto i = (size_t)3; // 正确
165+
```
166+
167+
这里的类型转换用的是 C 语言的强制类型转换语法 `(size_t)3`,更好的写法是用括号包裹的 C++ 构造函数风格的强制类型转换语法:
168+
169+
```
170+
auto i = size_t(3); // 正确
171+
```
172+
173+
看起来就和调用了 `size_t` 的“构造函数”一样。这也符合我们前面说的统一写法,类型统一和值写在一起,以括号结合,更可读。
174+
175+
> {{ icon.detail }} 顺便一提,`0xffffffffff` 会是 `long` (Linux) 或 `long long` (Windows) 类型字面量,因为它已经超出了 `int` 范围,所以实际上 `auto i = 0xffffffffff` 会推导为 `long i`。字面量类型的规则是,如果还在 `int` 范围内(0x7fffffff 以内),那这个字面量就是 `int`;如果超过了 0x7fffffff 但不超过 0xffffffff,就会变成 `unsigned int`;如果超过了 0xffffffff 就会自动变成 `long` (Linux) 或 `long long` (Windows) ;超过 0x7fffffffffffffff 则变成 `unsigned long` (Linux) 或 `unsigned long long` (Windows) ——这时和手动加 `ULL` 等后缀等价,无后缀时默认 `int`,如果超过了 `int` 编译器会自动推测一个最合适的。
176+
177+
如果需要其他类型的变量,改用 `short(3)``uint8_t(3)` 配合 `auto` 不就行了,根本没必要把类型前置。
178+
179+
#### 避免语法歧义
180+
181+
TODO
182+
183+
### `auto` 的小插曲:初始化列表
184+
9185
TODO
10186

11187
## 返回类型 `auto`
12188

13-
C++11 引入的 `auto` 关键字可以用作函数的返回类型,但它只是一个“占位”,让我们得以后置返回类型,并没有多大作用,非常残废。
189+
C++11 引入的 `auto` 关键字可以用作函数的返回类型,但它只是一个“占位”,让我们得以后置返回类型,并没有多大作用,所以 C++11 这版的 `auto` 非常残废。
14190

15191
```cpp
16192
auto f() -> int;
@@ -22,7 +198,7 @@ int f();
22198
23199
> {{ icon.detail }} 当初引入后置返回类型实际的用途是 `auto f(int x) -> decltype(x * x) { return x * x; }` 这种情况,但很容易被接下来 C++14 引入的真正 `auto` 返回类型推导平替了。
24200
25-
但是 C++14 引入了函数**返回类型推导**`auto` 才算真正意义上能用做函数返回类型它会自动根据函数中的 `return` 表达式推导出函数的返回类型。
201+
终于,C++14 引入了函数**返回类型推导**`auto` 才算真正意义上能用做函数返回类型它会自动根据函数中的 `return` 表达式推导出函数的返回类型。
26202

27203
```cpp
28204
auto f(int x) {

docs/cpp_tricks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ struct RAIIHandle {
320320
}
321321
RAIIHandle(RAIIHandle const &) = delete;
322322
RAIIHandle &operator=(RAIIHandle const &) = delete;
323-
RAIIHandle() {
323+
~RAIIHandle() {
324324
DeleteObject(handle);
325325
}
326326
};

docs/functions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,19 @@ void compute()
3636
```
3737

3838
> {{ icon.warn }} 对于返回类型不为 `void` 的函数,必须写 `return` 语句,如果漏写,会出现可怕的未定义行为 (undefined behaviour)。编译器不一定会报错,而是到运行时才出现崩溃等现象。建议 GCC 用户开启 `-Werror=return-type` 让编译器在编译时就检测此类错误,MSVC 则是开启 `/we4716`。更多未定义行为可以看我们的[未定义行为列表](undef.md)章节。
39+
3940
> {{ icon.detail }} 但有两个例外:1. main 函数是特殊的可以不写 return 语句,默认会自动帮你 `return 0;`。2. 具有 co_return 或 co_await 的协程函数可以不写 return 语句。
4041
42+
`void`只是说明该函数没有返回值,返回还是能返回的。相当于你出门跑了一趟,但是没有带任何东西回家,并不代表你无法回家死外面了。也有一种真无法返回的函数,一旦调用了,就“无期徒刑”直到死亡都无法离开:
43+
44+
```cpp
45+
[[noreturn]] void func() {
46+
std::terminate();
47+
}
48+
```
49+
50+
`std::terminate()`的效果是终止当前进程。而我们的函数`func`所有可能的分支都会走向`std::terminate()`,一旦调用了`std::terminate()`,程序就退出了,不可能再执行下面的语句,这种函数叫做“无返回函数”。C语言可以用`noreturn`关键字修饰,而现代C++可以用`[[noreturn]]`修饰,提醒编译器这是一个不可能正常返回的函数,从而帮助它优化和诊断你的程序。例如:`std::exit``throw``std::terminate``std::abort``while (1)`都会让函数变成无返回函数(除非有其他能正常返回的分支)。
51+
4152
### 接住返回值
4253

4354
## 函数的参数

docs/llvm_intro.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ ls build/bin
276276

277277
编译器的前端负责解析 C++ 这类高级语言的源代码,生成抽象语法树(Abstract Syntax Tree,AST)。AST 是源代码的一种抽象表示,其中每个节点代表源代码中的一个语法结构,例如 if、while、for、函数调用、运算符、变量声明等。每个 AST 节点都有自己的属性,例如类型、作用域、修饰符等。
278278

279-
不同类型的 AST 节点有不同的类型名,例如 IntegerLiterial 就表示这是一个整数类型的常量,而 BinaryOperator 就表示这是一个二元运算符(可能是加减乘除等二元运算)。
279+
不同类型的 AST 节点有不同的类型名,例如 IntegerLiteral 就表示这是一个整数类型的常量,而 BinaryOperator 就表示这是一个二元运算符(可能是加减乘除等二元运算)。
280280

281281
AST 节点可以有一个或多个子节点,许多节点就构成了一颗语法树。每个 .cpp 文件都可以解析得到一颗语法树,在 C++ 的语法中,每颗树的根部总是一个 TranslationUnitDecl 类型的节点。这是整个翻译单元(TU)的声明,其中包含了任意多的变量、函数、类型的声明等,作为 TU 的子节点存在其中。
282282

@@ -342,9 +342,9 @@ clang -fsyntax-only -Xclang -ast-dump test.cpp
342342

343343
接下来可以看到 CompountStmt 内部,又有两个子节点:CallExpr 和 ReturnStmt,分别是我们对 printf 函数的调用,和 `return 0` 这两条子语句。
344344

345-
+ ReturnStmt 很好理解,他只有一个子节点,类型是 IntegerLiterial,表示一个整形常数,整数的类型是 int,值是 0。这种有一个子节点的 ReturnStmt 节点,就表示一个有返回值的 return 语句,整体来看也就是我们代码里写的 `return 0`
345+
+ ReturnStmt 很好理解,他只有一个子节点,类型是 IntegerLiteral,表示一个整形常数,整数的类型是 int,值是 0。这种有一个子节点的 ReturnStmt 节点,就表示一个有返回值的 return 语句,整体来看也就是我们代码里写的 `return 0`
346346

347-
> {{ icon.story }} 举一反三,可以想象:如果代码里写的是 `return x + 1`,那么 ReturnStmt 的子节点就会变成运算符为 `+` 的 BinaryOperator。其又具有两个子节点:左侧是 DeclRefExpr 节点,标识符为 `x`;右侧是 IntegerLiterial 节点,值为 1。
347+
> {{ icon.story }} 举一反三,可以想象:如果代码里写的是 `return x + 1`,那么 ReturnStmt 的子节点就会变成运算符为 `+` 的 BinaryOperator。其又具有两个子节点:左侧是 DeclRefExpr 节点,标识符为 `x`;右侧是 IntegerLiteral 节点,值为 1。
348348
349349
然后我们来看 printf 函数调用这条语句:
350350
![](img/clang-ast-example.png)
@@ -358,7 +358,7 @@ clang -fsyntax-only -Xclang -ast-dump test.cpp
358358

359359
注意到这里 printf 发生了一个隐式转换 ImplicitCastExpr 后才作为 CallExpr 的第一个子节点(回答了调用哪个函数的问题),并且后面注释了说 `FunctionToPointerDecay`。也就是说,`printf` 这个标识符(DeclRefExpr)本来是一个对函数标识符的引用,还没有变成函数指针,这时候还没有完成函数的重载决议。是等到函数被 `()` 调用时,才会触发重载决议,而实现区分重载的方式,实际上就是函数引用自动隐式转换成函数指针的过程所触发的,也就是这里的 ImplicitCastExpr 隐式转换节点了。这种自动发生的隐式转换被称为“退化”(decay)。所以,函数引用无法直接调用,Clang 里一直都是需要退化成指针才调用的。
360360

361-
然后,这里的函数参数是一个字符串常量,按理说一个 StringLiterial 节点就可以了,为什么还有个 ImplicitCastExpr?这里有个常见误区需要纠正:很多同学常常想当然以为字符串常量的类型是 `const char *`。实际上,字符串常量的类型是 `const char []`,是一个数组类型!数组不是指针,他们是两个完全不同的类型。之所以你会有数组是指针的错觉,是因为数组可以隐式转换为元素类型的指针。而这是“退化”规则之一,这个过程在函数参数、auto 推导的时候是自动发生的(正如上面说的函数引用会在调用时自动“退化”成函数指针一样)。
361+
然后,这里的函数参数是一个字符串常量,按理说一个 StringLiteral 节点就可以了,为什么还有个 ImplicitCastExpr?这里有个常见误区需要纠正:很多同学常常想当然以为字符串常量的类型是 `const char *`。实际上,字符串常量的类型是 `const char []`,是一个数组类型!数组不是指针,他们是两个完全不同的类型。之所以你会有数组是指针的错觉,是因为数组可以隐式转换为元素类型的指针。而这是“退化”规则之一,这个过程在函数参数、auto 推导的时候是自动发生的(正如上面说的函数引用会在调用时自动“退化”成函数指针一样)。
362362

363363
数组能自动退化成指针,不代表数组就是指针。例如 int 可以隐式转换为 double,难道就可以说“int 就是 double”吗?同样地,不能说“数组就是指针”。字符串常量的类型,从来都是 `const char [N]`,其中 `N` 是字符串中字符的个数(包括末尾自动加上的 `'\0'` 结束符)。只不过是在传入函数参数(此处是 printf 函数的字符串参数)时,自动隐式转换为 `const char *` 了而已。正如这个 ImplicitCastExpr 后面尖括号的提示中所说,ArrayToPointerDecay,是数组类型到指针类型的自动退化,从 `const char [14]` 自动隐式转换到了 `const char *`
364364

docs/type_rich_api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ Sleep(MilliSeconds(3));
12321232
this_thread::sleep_for(chrono::seconds(3));
12331233
```
12341234

1235-
如果你 `using namespace std::literials;` 还可以这样快捷地创建字面量:
1235+
如果你 `using namespace std::literals;` 还可以这样快捷地创建字面量:
12361236
```cpp
12371237
this_thread::sleep_for(3ms); // 3 毫秒
12381238
this_thread::sleep_for(3s); // 3 秒

docs/undef.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ i << 0; // 可以
314314
i << -1; // 错!
315315
```
316316

317+
但是你还需要考虑一件事情:**隐式转换**,或者直接点说:**整数提升**
318+
319+
- 在 C++ 中算术运算符不接受小于 int 的类型进行运算。如果你觉得可以,那只是隐式转换,整形提升了。
320+
321+
```cpp
322+
std::uint8_t c{ '0' };
323+
using T1 = decltype(c << 1); // int
324+
```
325+
326+
即使移位大于等于 8 也不成问题。
327+
328+
---
329+
317330
对于有符号整数,左移还不得破坏符号位
318331
319332
```cpp

docs/unicode.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2848,11 +2848,13 @@ int main() {
28482848
setlocale(LC_ALL, ".utf-8"); // 设置标准库调用系统 API 所用的编码,用于 fopen,ifstream 等函数
28492849
SetConsoleOutputCP(CP_UTF8); // 设置控制台输出编码,或者写 system("chcp 65001") 也行,这里 CP_UTF8 = 65001
28502850
SetConsoleCP(CP_UTF8); // 设置控制台输入编码,用于 std::cin
2851+
#elif __APPLE__
2852+
// 通常来说 MacOS 的默认编码就是 UTF-8,这里设置全局 locale 是为了让 iswspace 接受全角空格、iswpunct 接受全角逗号 L',' 等
2853+
setlocale(LC_ALL, "UTF-8"); // MacOS 设置 UTF-8 编码,让 iswspace 接受全角空格等
28512854
#elif __unix__
28522855
// 反正 Unix 系统默认都是 UTF-8,不设置也行,这里设置全局 locale 是为了让 iswspace 接受全角空格、iswpunct 接受全角逗号 L',' 等
28532856
//setlocale(LC_ALL, "zh_CN.utf-8"); // 设置使用中文本地化,可使 strerror 输出中文(但用户必须 locale-gen 过中文!)
2854-
//setlocale(LC_ALL, "C.utf-8"); // 设置使用语言中性 locale,只影响 iswspace、iswpunct 等函数,不会使 strerror 等输出中文
2855-
setlocale(LC_ALL, ".utf-8"); // 若不带任何前缀(推荐),则默认使用当前系统环境变量中的语言 $LANG,使 strerror 自动适应
2857+
setlocale(LC_ALL, "C.utf-8"); // 设置使用语言中性 locale(推荐),只影响 iswspace、iswpunct 等函数,不会使 strerror 等输出中文
28562858
#endif
28572859
// 这里开始写你的主程序吧!
28582860
// ...
@@ -3051,10 +3053,10 @@ QString str = codec->toUnicode(bytes);
30513053
#### 字符串常量
30523054
30533055
```cpp
3054-
QString str = QStringLiterial("你好,世界");
3056+
QString str = QStringLiteral("你好,世界");
30553057
```
30563058

3057-
`QStringLiterial` 可以保证,转换时采用的是所谓“运行字符集”(实际应该叫字面量字符编码),也就是我们开发者电脑上的“区域设置”,是编译期确定的。而如果写 `QString::fromLocal8Bits("")` 就变成 “ANSI”,客户的“区域设置”了。这两个字符编码,比如在之前跨国 galgame 的案例中,就是不同的。
3059+
`QStringLiteral` 可以保证,转换时采用的是所谓“运行字符集”(实际应该叫字面量字符编码),也就是我们开发者电脑上的“区域设置”,是编译期确定的。而如果写 `QString::fromLocal8Bits("")` 就变成 “ANSI”,客户的“区域设置”了。这两个字符编码,比如在之前跨国 galgame 的案例中,就是不同的。
30583060

30593061
#### QTextStream
30603062

0 commit comments

Comments
 (0)