mirror of
https://github.com/changkun/modern-cpp-tutorial.git
synced 2025-12-16 20:27:08 +03:00
book: fmt (#154)
* Add space between english and chinease char * Add space between english and chinease char * book: typo fixes Co-authored-by: guoliang wang <tswanggl@hotmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@ C++17 则是近三年依赖 C++ 社区一致推进的方向,也指出了**现
|
||||
|
||||
## 目标读者
|
||||
|
||||
1. 本书假定读者已经熟悉了传统 C++ ,至少在阅读传统 C++ 代码上不具备任何困难。换句话说,那些长期使用传统 C++进行编码的人、渴望在短时间内迅速了解**现代 C++** 特性的人非常适合阅读本书;
|
||||
1. 本书假定读者已经熟悉了传统 C++ ,至少在阅读传统 C++ 代码上不具备任何困难。换句话说,那些长期使用传统 C++ 进行编码的人、渴望在短时间内迅速了解**现代 C++** 特性的人非常适合阅读本书;
|
||||
2. 本书一定程度上介绍了一些现代 C++ 的**黑魔法**,但这些魔法毕竟有限,不适合希望进阶学习现代 C++ 的读者,本书的定位系**现代 C++ 的快速上手**。当然,希望进阶学习的读者可以使用本书来回顾并检验自己对 **现代 C++** 的熟悉度。
|
||||
|
||||
## 本书目的
|
||||
|
||||
@@ -91,7 +91,7 @@ int main() {
|
||||
gcc -c foo.c
|
||||
```
|
||||
|
||||
编译出 `foo.o` 文件,再使用 `clang++` 将 C++代码和 `.o` 文件链接起来(或者都编译为 `.o` 再统一链接):
|
||||
编译出 `foo.o` 文件,再使用 `clang++` 将 C++ 代码和 `.o` 文件链接起来(或者都编译为 `.o` 再统一链接):
|
||||
|
||||
```bash
|
||||
clang++ 1.1.cpp foo.o -std=c++2a -o 1.1
|
||||
@@ -120,7 +120,7 @@ clean:
|
||||
|
||||
> 注意:`Makefile` 中的缩进是制表符而不是空格符,如果你直接复制这段代码到你的编辑器中,制表符可能会被自动替换掉,请自行确保在 `Makefile` 中的缩进是由制表符完成的。
|
||||
>
|
||||
> 如果你还不知道 Makefile 的使用也没有关系,本教程中不会构建过于复杂的代码,简单的在命令行中使用 `clang++ -std=c++2a` 也可以阅读本书。
|
||||
> 如果你还不知道 `Makefile` 的使用也没有关系,本教程中不会构建过于复杂的代码,简单的在命令行中使用 `clang++ -std=c++2a` 也可以阅读本书。
|
||||
|
||||
如果你是首次接触现代 C++,那么你很可能还看不懂上面的那一小段代码,即:
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ order: 2
|
||||
|
||||
### nullptr
|
||||
|
||||
`nullptr` 出现的目的是为了替代 `NULL`。在某种意义上来说,传统 C++ 会把 `NULL`、`0` 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 `((void*)0)`,有些则会直接将其定义为 `0`。
|
||||
`nullptr` 出现的目的是为了替代 `NULL`。在某种意义上来说,传统 C++ 会把 `NULL`、`0` 视为同一种东西,这取决于编译器如何定义 `NULL`,有些编译器会将 `NULL` 定义为 `((void*)0)`,有些则会直接将其定义为 `0`。
|
||||
|
||||
C++ **不允许**直接将 `void *` 隐式转换到其他类型。但如果编译器尝试把 `NULL` 定义为 `((void*)0)`,那么在下面这句代码中:
|
||||
|
||||
@@ -22,7 +22,7 @@ C++ **不允许**直接将 `void *` 隐式转换到其他类型。但如果编
|
||||
char *ch = NULL;
|
||||
```
|
||||
|
||||
没有了 `void *` 隐式转换的 C++ 只好将`NULL` 定义为 `0`。而这依然会产生新的问题,将 `NULL` 定义成 0 将导致 `C++` 中重载特性发生混乱。考虑下面这两个 `foo` 函数:
|
||||
没有了 `void *` 隐式转换的 C++ 只好将 `NULL` 定义为 `0`。而这依然会产生新的问题,将 `NULL` 定义成 `0` 将导致 `C++` 中重载特性发生混乱。考虑下面这两个 `foo` 函数:
|
||||
|
||||
```cpp
|
||||
void foo(char*);
|
||||
@@ -31,9 +31,9 @@ void foo(int);
|
||||
|
||||
那么 `foo(NULL);` 这个语句将会去调用 `foo(int)`,从而导致代码违反直觉。
|
||||
|
||||
为了解决这个问题,C++11 引入了 `nullptr` 关键字,专门用来区分空指针、0。而 `nullptr` 的类型为 `nullptr_t`,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
|
||||
为了解决这个问题,C++11 引入了 `nullptr` 关键字,专门用来区分空指针、`0`。而 `nullptr` 的类型为 `nullptr_t`,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
|
||||
|
||||
你可以尝试使用 clang++ 编译下面的代码:
|
||||
你可以尝试使用 `clang++` 编译下面的代码:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
@@ -73,11 +73,11 @@ foo(char*) is called
|
||||
|
||||
从输出中我们可以看出,`NULL` 不同于 `0` 与 `nullptr`。所以,请养成直接使用 `nullptr`的习惯。
|
||||
|
||||
此外,在上面的代码中,我们使用了 `decltype` 和 `std::is_same` 这两个属于现代 C++ 的语法,简单来说,`decltype` 用于类型推导,而 `std::is_same` 用于比较两个类型是否相等,我们会在后面 [decltype](#decltype) 一节中详细讨论。
|
||||
此外,在上面的代码中,我们使用了 `decltype` 和 `std::is_same` 这两个属于现代 C++ 的语法,简单来说,`decltype` 用于类型推导,而 `std::is_same` 用于比较两个类型是否相同,我们会在后面 [decltype](#decltype) 一节中详细讨论。
|
||||
|
||||
### constexpr
|
||||
|
||||
C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:
|
||||
C++ 本身已经具备了常量表达式的概念,比如 `1+2`, `3*4` 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
@@ -123,7 +123,7 @@ int main() {
|
||||
|
||||
C++11 提供了 `constexpr` 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 `len_foo` 在编译期就应该是一个常量表达式。
|
||||
|
||||
此外,`constexpr` 的函数可以使用递归:
|
||||
此外,`constexpr` 修饰的函数可以使用递归:
|
||||
|
||||
```cpp
|
||||
constexpr int fibonacci(const int n) {
|
||||
@@ -131,7 +131,7 @@ constexpr int fibonacci(const int n) {
|
||||
}
|
||||
```
|
||||
|
||||
从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:
|
||||
从 C++14 开始,`constexpr` 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:
|
||||
|
||||
```cpp
|
||||
constexpr int fibonacci(const int n) {
|
||||
@@ -181,7 +181,7 @@ int main() {
|
||||
}
|
||||
```
|
||||
|
||||
在上面的代码中,我们可以看到 `itr` 这一变量是定义在整个 `main()` 的作用域内的,这导致当我们需要再次遍历整个 `std::vectors` 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:
|
||||
在上面的代码中,我们可以看到 `itr` 这一变量是定义在整个 `main()` 的作用域内的,这导致当我们需要再次遍历整个 `std::vectors` 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 `if`(或 `switch`)中完成这一操作:
|
||||
|
||||
```cpp
|
||||
// 将临时变量放到 if 语句内
|
||||
@@ -295,7 +295,7 @@ int main() {
|
||||
|
||||
## 2.3 类型推导
|
||||
|
||||
在传统 C 和 C++中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。
|
||||
在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。
|
||||
|
||||
C++11 引入了 `auto` 和 `decltype` 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。
|
||||
|
||||
@@ -369,7 +369,7 @@ auto arr = new auto(10); // arr 被推导为 int *
|
||||
|
||||
### decltype
|
||||
|
||||
`decltype` 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 `typeof` 很相似:
|
||||
`decltype` 关键字是为了解决 `auto` 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 `typeof` 很相似:
|
||||
|
||||
```cpp
|
||||
decltype(表达式)
|
||||
@@ -403,7 +403,7 @@ type z == type x
|
||||
|
||||
### 尾返回类型推导
|
||||
|
||||
你可能会思考,在介绍 `auto`时,我们已经提过 `auto` 不能用于函数形参进行类型推导,那么 `auto` 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:
|
||||
你可能会思考,在介绍 `auto` 时,我们已经提过 `auto` 不能用于函数形参进行类型推导,那么 `auto` 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:
|
||||
|
||||
```cpp
|
||||
template<typename R, typename T, typename U>
|
||||
@@ -423,7 +423,7 @@ R add(T x, U y) {
|
||||
decltype(x+y) add(T x, U y)
|
||||
```
|
||||
|
||||
但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,`x` 和 `y` 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:
|
||||
但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,`x` 和 `y` 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 `auto` 关键字将返回类型后置:
|
||||
|
||||
```cpp
|
||||
template<typename T, typename U>
|
||||
@@ -575,7 +575,7 @@ extern template class std::vector<double>; // 不在该当前编译文件中实
|
||||
std::vector<std::vector<int>> matrix;
|
||||
```
|
||||
|
||||
这在传统C++编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:
|
||||
这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:
|
||||
|
||||
```cpp
|
||||
template<bool T>
|
||||
@@ -624,13 +624,17 @@ int main() {
|
||||
我们可能定义了一个加法函数:
|
||||
|
||||
```cpp
|
||||
// c++11 version
|
||||
template<typename T, typename U>
|
||||
auto add(T x, U y) -> decltype(x+y) {
|
||||
return x+y;
|
||||
}
|
||||
|
||||
// Call add function
|
||||
auto ret = add<int, int>(1,3);
|
||||
```
|
||||
|
||||
但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。
|
||||
但在使用时发现,要使用 `add`,就必须每次都指定其模板参数的类型。
|
||||
|
||||
在 C++11 中提供了一种便利,可以指定模板的默认参数:
|
||||
|
||||
@@ -639,6 +643,9 @@ template<typename T = int, typename U = int>
|
||||
auto add(T x, U y) -> decltype(x+y) {
|
||||
return x+y;
|
||||
}
|
||||
|
||||
// Call add function
|
||||
auto ret = add(1,3);
|
||||
```
|
||||
|
||||
### 变长参数模板
|
||||
@@ -661,9 +668,9 @@ class Magic<int,
|
||||
std::vector<int>>> darkMagic;
|
||||
```
|
||||
|
||||
既然是任意形式,所以个数为 0 的模板参数也是可以的:`class Magic<> nothing;`。
|
||||
既然是任意形式,所以个数为 `0` 的模板参数也是可以的:`class Magic<> nothing;`。
|
||||
|
||||
如果不希望产生的模板参数个数为0,可以手动的定义至少一个模板参数:
|
||||
如果不希望产生的模板参数个数为 `0`,可以手动的定义至少一个模板参数:
|
||||
|
||||
```cpp
|
||||
template<typename Require, typename... Args> class Magic;
|
||||
@@ -672,7 +679,7 @@ template<typename Require, typename... Args> class Magic;
|
||||
变长参数模板也能被直接调整到到模板函数上。传统 C 中的 `printf` 函数,
|
||||
虽然也能达成不定个数的形参的调用,但其并非类别安全。
|
||||
而 C++11 除了能定义类别安全的变长参数函数外,
|
||||
还可以使类似 printf 的函数能自然地处理非自带类别的对象。
|
||||
还可以使类似 `printf` 的函数能自然地处理非自带类别的对象。
|
||||
除了在模板参数中能使用 `...` 表示不定长模板参数外,
|
||||
函数参数也使用同样的表示法代表不定长参数,
|
||||
这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
|
||||
@@ -847,7 +854,7 @@ int main() {
|
||||
|
||||
### 继承构造
|
||||
|
||||
在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:
|
||||
在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 `using` 引入了继承构造函数的概念:
|
||||
|
||||
```cpp
|
||||
#include <iostream>
|
||||
@@ -875,7 +882,7 @@ int main() {
|
||||
|
||||
### 显式虚函数重载
|
||||
|
||||
在传统 C++中,经常容易发生意外重载虚函数的事情。例如:
|
||||
在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:
|
||||
|
||||
```cpp
|
||||
struct Base {
|
||||
@@ -975,7 +982,7 @@ if (new_enum::value3 == new_enum::value4) {
|
||||
}
|
||||
```
|
||||
|
||||
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用 int)。
|
||||
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用 `int`)。
|
||||
|
||||
而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 `<<` 这个算符来进行输出,可以收藏下面这个代码段:
|
||||
|
||||
@@ -998,8 +1005,8 @@ std::cout << new_enum::value3 << std::endl
|
||||
|
||||
本节介绍了现代 C++ 中对语言可用性的增强,其中笔者认为最为重要的几个特性是几乎所有人都需要了解并熟练使用的:
|
||||
|
||||
1. auto 类型推导
|
||||
2. 范围 for 迭代
|
||||
1. `auto` 类型推导
|
||||
2. 范围 `for` 迭代
|
||||
3. 初始化列表
|
||||
4. 变参模板
|
||||
|
||||
|
||||
Reference in New Issue
Block a user