本文意在为有 c++11
基础的同学学习 c++20
标准(也称为 c++2a
标准)的常用特性以及 c++ 工程实践提供指导。由于目前互联网上的优秀资料已经很多了,因此本文会更倾向于互联网现有资源,并作适当补充。如果你对C++
及“C风格语言“本身不太熟悉,也没有关系,可以参考谷雨同学的C++教程,毕竟编程语言其实是相通的。
c++20
中引入三个重要的概念 concept
、ranges
、module
,其中模块由于编译器支持正逐渐完善,目前还未被广泛使用,因为在本篇中暂时略过。
由于 c++20
标准较新,因此其特性需要较新的编译器才能够编译,推荐使用 gcc13
,clang16
以及最新版的 msvc
。你也可以在 Cpp Reference
上查看编译器支持情况表。
gcc13
安装推荐使用 msys2
进行 gcc13
的安装。
先在msys2官网上下载并安装 msys2
,推荐阅读文档中Environments章节了解 msys2
中各个环境的区别,并选择自己喜欢的环境,推荐的环境是 ucrt64
。
选择好环境后推荐把对应环境的 bin
文件夹配置到系统的环境变量中。
启动 msys2
的终端,然后安装 gcc
即可,记得加上对应的前缀。
以 ucrt64
为例,安装命令即为
pacman -Syu mingw-w64-ucrt-x86_64-gcc
# 查看是否安装成功
g++ --version
以 Ubuntu-22.04lts
为例。你可以在Linux 俱乐部测试集群申请云账号,或者使用我们提供的公有云环境。
# 首先添加ppa
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
# 然后安装gcc13
sudo apt install gcc-13 g++-13
# 查看是否安装成功
g++-13 --version
# 可选,用gcc-13和g++-13替代默认的gcc和g++命令
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100
使用 homebrew
安装GCC
,默认版本就是13,可以支持c++20
标准。
c++20
标准的程序并不是安装完支持 c++20
标准的编译器之后就默认能编译 c++20
标准的程序,需要特定配置。
如果你使用 g++
或者 clang++
在命令行中编译程序,那么加上 --std=c++20
的命令行参数即可启用 c++20
标准。
如果你使用的是 Visual Studio
(应该没有人会用 cl.exe
编译吧),那么你需要在 项目属性 -> 配置属性 -> C/C++ -> 语言 -> C++语言标准 中选择ISO C++ 20 标准
详细的阅读资料:
TODO!
auto
众所周知,auto
关键字可以用于自动类型推导,在变量声明中使用可以极大降低心智成本以及简化代码。
在 c++11
之后,auto
也被赋予了更多的功能。
Cpp Reference 相关章节:占位类型说明符
在 c++14
中,引入了返回类型推导,在函数和lambda表达式中可以用 auto
来指定返回值类型。如下例中函数 f
的返回值将被自动推导为 int
。注意,如果一个函数有多个返回点,即多个 return
,那么每个返回点的返回类型都要相同。更进一步,可以利用返回类型推导来实现返回 lambda
表达式的函数。
auto f(bool val) {
if (val) return 123;
else return 456;
}
在 c++20
之后,不只是函数的返回值支持使用 auto
推导,甚至形参类型也支持用 auto
推 导,此时的函数声明可以等价于使用模板的函数声明,如下例
void f(auto a){
std::cout << a << std::endl;
}
// 等价于
template<typename T>
void f(T a){
std::cout << a << std::endl;
}
// 当然也可以写成
auto f(auto a){
std::cout << a << std::endl;
}
详细的阅读资料:
- Cpp Reference 相关条目
任务:写一个a+b函数,不使用任何显式类型标注。(
main
函数必须返回int,可以不用管这个int)
concept
有四种写法吗?(了解即可)在引入 concept
之前,C++ 中的模板使用时更多依赖于鸭子类型的概念。函数模板等在实现的时候更多的是假定模板参数具有某种“行为”,即要求模板参数需要有特定签名的方法或者属性。如果传入的类型不满足这些条件,那么编译器将会以冗长且晦涩难懂的编译错误予以反击。
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
c++20
引入的 concept
和 require
关键字可以给模板添加特定的约束,来使模板的使用条件更加清晰,报错也更加可读。
详细的阅读资料:
C++ 中的指针常被认为是最难学的部分之一。你可能会想,指针不就是指向一块内存地址嘛,可能还要用 new
和 delete
分配和释放一下堆上的内存空间罢了,但真的那么简单吗?在复杂的系统中,你真的能处理好每个堆上内存的释放吗?
和指针相关的问题有:
这些问题一旦出现,很容易导致程序的崩溃。
从 c++11
开始,标准库中引入了智能指针 unique_ptr
和 shared_ptr
来利用 C++ 的 RAII
机制来简化堆上内存的分配与释放。
unique_ptr
主要利用所有权的概念明确谁负责管理内存的释放,shared_ptr
则利用引用计数的方式将内存在无人使用的时候释放。
详细的阅读资料:
- 谷雨同学的C++教程:智能指针
Range
c++20
标准引入了 std::ranges
,也被称为受约束算法,支持以迭代器-哨位对或者单个 range
参数指定范围,如下例。
std::vector vec = {1, 2, 3, 4};
auto fn = [](auto& n){
std::cout << n << std::endl;
};
// 以下若干种方式等价
for(auto i = vec.begin(); i != vec.end(); ++i){
fn(i);
}
std::for_each(vec.begin(), vec.end(), fn);
std::ranges::for_each(vec, fn);
std::ranges::for_each(vec.begin(), vec.end(), fn);
并且可以结合**视图(view)**来实现更多有意思的操作。
详细的阅读资料
<=>
<=>
称为三路比较运算符,也俗称飞船运算符,在 c++20
中引入。
通过重载 <=>
运算符并返回 partial_ordering
、weak_ordering
、strong_ordering
三种关系类型中的一个,编译器就能自动生成 >
、>=
、<
、<=
、==
、!=
共六种二元关系运算符的代码,也就是说,原本需要实现六份的代码现在只需要实现一份。
详细的阅读资料
std::format
我们来看下面一个简单的例子
std::string a = "string_a";
std::string b = "string_b";
int c = 1898;
// 以下三种方式产生的字符串相同
auto demo1 = a + ' ' + b + ' ' + std::to_string(c);
std::stringstream ss;
ss << a << ' ' << b << ' ' << c;
auto demo2 = ss.str();
auto demo3 = std::format("{} {} {}", a, b, c);
可以看到,std::format
的写法显然更加简洁明了。std::format
的好处当然不只是写法更简洁,它利用了模板等c++的高级特性,使得性能比printf
、stringstream
之流好上不少。std::format
的用法远比上面展现的丰富,你可以参考阅读资料来具体学习。
optional
?这是什么,好熟悉如果没有std::optional
,在C++中表达一个"有时存在"或"暂时不存在"的值是比较麻烦的。举个例子,你有个函数可能会返回一个int
类型的整数,但某时可能会因为有些原因,这个整数不存在,那这时如何设计返回类型呢?如果这个整数一定是非负数,那好办,只要规定返回负数时代表不存在就好。但如果这个整数的取值是全体整数呢,呜呼,那完蛋,要使用一点弯弯绕绕的方法了。但如果使用std::optional
,这些苦恼统统不存在。看下面一个例子。
#include <iostream>
#include <format>
#include <optional>
std::optional<double> divide(double a, double b) {
if (b == 0.0){
return std::nullopt;
}
return a / b;
}
int main() {
double a, b;
std::cin >> a >> b;
auto result = divide(a, b);
if (result) {
std::cout << std::format("{} divided by {} is {}", a, b, *result) << std::endl;
} else {
std::cout << "cannot divide a number by zero!" << std::endl;
}
return 0;
}
这个例子中有几个知识点:
std::optional<double>
可以直接显示类型转换到bool
,因此可以作为if
的判断条件。std::optional
中的值可以通过*
运算符取出。std::optional
的空值可以用std::nullopt
表示。T
类型的值可以隐式类型转换到 std::optional<T>
,因为std::optional<T>
可以直接用T
类型的值构造。std::filesystem
你之前可能学习过C++中如何读写文件,但是,关于文件的操作远远不只读和写,还有文件夹的创建与检测,文件权限的检测与修改,等等等。std::filesystem
就提供了方便了文件路径操作、文件夹操作、文件类型检测操作、文件系统权限操作等。
阅读资料:
你是否好奇过控制台中的颜色输出是怎么实现?nano,vim之类的编辑器的光标移动是怎么实现的?
其实这些并不复杂,我们可以通过ANSI 转义序列实现这些功能。
你的任务是:使用 C++ 利用 ANSI 转义序列实现一个类 readline 库,并提供一个简单的示例。注意,你只需考虑ascii编码下的情况,即不用处理汉字等非ascii字符。我们只考察代码是否写得“漂亮”,不看实现了多少额外功能!!!不看实现了多少额外功能!!!不看实现了多少额外功能!!!
CMake
、xmake
git
管理代码版本,并上传至 Github
https://zh.wikipedia.org/wiki/ANSI转义序列
将github仓库地址发送到[email protected],并抄送至[email protected],请在邮件主题中注明HPCFS以收到自动回复!