线上阅读链接:https://changkun.de/modern-cpp/zh-cn/00-preface/
最近正在阅读链接上这本《现代C++教程》,发帖作为学习笔记,如有侵权,告知即删。
第1章 迈向现代C++
“现代C++”与“传统C++”
C++经历了C++98/11/20三个大的标准,中间穿插了C++14/17对其前面标准的补充和优化。本书将C++11及其之后的C++特性称为**“现代C++”,而C++98及其之前的C++特性称为“传统C++”**。
现代C++,一方面大大增强了语言的可用性,auto关键字方便使用者操纵更加复杂的模板类型,Lambda表达式使得C++具有“匿名函数”的“闭包”特性,右值引用解决了C++长期被人诟病的临时对象效率问题。另一方面,为自身的标准库增加了非常多的工具和方法,如 std::thread
不依赖于系统底层地支持并发编程,std::regex
提供了完整的正则表达式支持等等。
C:gcc,C++:g++
C语言风格的类型转换被弃用(即在变量前使用(convert_type)
),而应该使用 static_cast
、reinterpret_cast
、const_cast
等。
C++不是C的一个超集,C++98与C99之间是存在区别的。
第2章 语言可用性的强化
新特性的出现是为了解决和优化传统C++中存在的一些问题,使得使用更加便捷,在强化语言的可用性上,有常量、变量及其初始化、类型推导、控制流、模板和面向对象六个方面的改进。
常量:① 引入nullptr
关键字专门区分空指针和0,能够隐式地转换为任何指针或成员指针,并且进行相等或不等的比较;② 引入了constexpr
关键字,可以修饰 const 常数以及函数,从而能够直接放入要求常量表达式的位置(如创建数组时的数量)。
变量及其初始化:① 可以将临时变量的创建放入if/switch
语句内,避免只要一次的临时变量占用内存及名称。② 引入std::initializer_list
,允许构造函数或其他函数像参数一样使用初始化链表。③ 引入元组std::tuple
实现了多返回值,并且auto [x,y,..]
使用来自动获取内容。
类型推导:使用auto
和decltype
对变量或者表达式进行类型推导,并且提供是否为同一类型的比较,并且进一步用于函数返回值中。
控制流:对条件语句、循环语句进行的一些优化,例如① 引入constexpr
关键字特性使代码在编译时就完成分支判断,② 用auto
自动遍历容器区间。
模板:① 显式通知编译器何时进行模板的实例化;② 不把连续右尖括号>>
直接作为右移运算符,而是考虑处理嵌套模板类的情况;③ 引入using
实现模板别名的定义;④ 引入...
作为不定长模板参数,允许任意个数、任意类别的模板参数;⑤ 使用折叠表达式简化不定长模板参数的运算;⑥ 利用auto
对<>之间的数据类型进行自动推导。
面向对象:① 委托构造,在同一个类的构造函数中调用另一个构造函数,从而达到简化代码的目的;② 使用using
继承构造函数;③ 使用override
和final
两个关键字解决虚函数重载和类继承的一些问题;④ 允许显式地声明采用或拒绝编译器自带的构造函数。⑤ 引入了枚举类(enumeration class)以保证枚举的类型安全。
2.1 常量
nullptr
C++11引入了 nullptr
关键字,专门用来区分空指针和0。 nullptr
的类型为 nullptr_t
,能够隐式地转换为任何指针或成员指针的类型,并且与之进行相等或不等的比较。建议养成使用 nullptr
的习惯。
decltype
用于类型推导:decltype(NULL)
std::is_same
用于判断两个类型是否相同:std::is_same<decltype(NULL), std::nullptr_t>::value
constexpr
在创建数组的时候,C++标准要求数组的长度必须是一个常量表达式,当 char arr_4[xx]
中的 xx
包含const
常数或者函数时,创建数组的表达式就非法,此时可以使用 constexpr
特性解决这一问题。
2.2 变量及其初始化
if/switch变量声明强化
可以将临时变量放入 if 语句内。
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
初始化链表
将初始化链表的改变绑定到类型上,称其为 std::initializer_list
,允许构造函数或其他函数像参数一样使用初始化列表:
#include <initializer_list> // 需要引用对应头文件
#include <vector>
#include <iostream>
class MagicFoo {
public:
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it)
vec.push_back(*it);
}
void foo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it) vec.push_back(*it);
}
};
int main() {
// after C++11
MagicFoo magicFoo = {1, 2, 3, 4, 5};
magicFoo.foo({6,7,8,9});
Foo foo2 {3, 4};
}
结构化绑定
用于需要多返回值的情况,C++11提供了 std::tuple
容器用于构造一个元组,而C++11/14并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,并且需要清楚知道元组包含几个对象、分别是什么类型。直到C++17中完善了这一设定:
#include <iostream>
#include <tuple>
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}
int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << std::endl;
return 0;
}
2.3 类型推导
auto、decltype
auto
和 decltype
两个关键字实现了类型推导,让编译器操心变量类型:
-
auto
:对变量进行类型推导,从C++20起,auto
甚至能用于函数传参,但是目前还不能用于推导数组类型。 -
decltype
:用于计算某个表达式的类型。auto x = 1; auto y = 2; decltype(x+y) z;
-
判断是否为同一类型:
if (std::is_same<decltype(x), int>::value) std::cout << "type x == int" << std::endl; if (std::is_same<decltype(x), float>::value) std::cout << "type x == float" << std::endl; if (std::is_same<decltype(x), decltype(z)>::value) std::cout << "type z == type x" << std::endl;
尾返回类型推导
C++11,尾返回类型,需要在末尾添加类型推导,可以使用 std::is_same
检查一下类型推导是否正确
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
C++14,让普通函数直接具备返回值推导,注意列表初始化的返回值不能和auto一起使用。
template<typename T, typename U>
auto add3(T x, U y){
return x + y;
}
decltype(auto) 参数转发
std::string lookup1();
decltype(auto) look_up_a_string_1() {
return lookup1();
}
2.4 控制流
if constexpr
引入constexpr关键字特性到条件判断中,使得代码在编译时就完成分支判断,如:
#include <iostream>
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}
编译时表现为:
int print_type_info(const int& t) {
return t + 1;
}
double print_type_info(const double& t) {
return t + 0.001;
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}
区间for迭代
写出像python那样简洁的循环语句
for (auto element : vec)
std::cout << element << std::endl; // read only
for (auto &element : vec) {
element += 1; // writeable
}
2.5 模板
模板的哲学在于将一些能够在编译期处理的问题丢到编译期间处理,仅在运行时处理最核心的动态服务。
【外部模板】
扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式地通知编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
尖括号“>”
传统C++的编译器中,>>
一律作为右移运算符处理,但是很容易在嵌套模板的代码中写出(如下),因此C++11开始,连续的右尖括号变得合法。
std::vector<std::vector<int>> matrix;
类型别名模板
传统C++中可以用 typedef
为类型定义一个新的名称,但是没有办法为模板定义一个新的名称,C++11中使用using
引入下面这种形式,支持了模板别名的定义
template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
int main() {
TrueDarkMagic<bool> you;
}
【变长参数模板】
C++11加入新的表示方法,允许任意个数、任意类别的模板参数,用 ...
表示不定长模板参数,同样的方法也可以用于函数参数表示不定长参数,也可以手动定义至少一个模板参数
template<typename... Ts> class Magic;
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;
// 手动定义至少一个模板参数
template<typename Require, typename... Args> class Magic;
// 定义变长函数
template<typename... Args> void printf(const std::string &str, Args... args);
// 解包
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
在解包时,先用 sizeof...
计算参数个数,然后有几种经典的处理方法:① 递归模板;② (C++17) 变参模板展开;③ 初始化列表展开:用到了初始化列表以及Lambda表达式的特性。
/*1.递归模板----------------------------------------------------*/
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
/*2.变参模板展开----------------------------------------------------*/
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}
/*3.初始化列表展开----------------------------------------------------*/
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
【折叠表达式】
#include <iostream>
template<typename ... T>
auto sum(T ... t) {
return (t + ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
}
非类型模板参数推导
使用 auto
自动推导 <>
之间的数据的类型
template <auto value> void foo() {
std::cout << value << std::endl;
return;
}
int main() {
foo<10>(); // value 被推导为 int 类型
}
2.6 面向对象
委托构造
在同一个类中的一个构造函数调用另一个构造函数,从而达到简化代码的目的,如:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}
继承构造
使用 using
引入继承构造函数的概念
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}
显式函数重载
首先介绍虚函数的概念和作用:指向基类的指针在操作它的多态类对象时,会根据不同的类对象调用其相应的函数,这个函数就是虚函数。在基类中,使用 virtual
定义了虚函数后,就默认子类的同名函数也为虚函数,不管是否再使用 virtual
声明。可能发出两种意外情况:
- 程序员并不想要尝试重载函数,而只是恰好加入了一个具有相同名字的函数。(?如果真的恰好名字和参数类型都相同的话,就不会报错,就难以发现该问题)
- 程序员想要重载某函数,而基类中的该虚函数已经被删除了,此时再向子类加入的函数变成了普通的类方法。
为此,C++11引入了 override
和 final
两个关键字:
override
:显式地告知编译器进行重载,编译器将检查基类是否存在这样的虚函数,如果没有则无法通过编译。final
:为了防止类被继续继承以及虚函数继续重载,也即终止继承+终止虚函数重载。
/*1. override 用法-------------------------------------------------*/
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};
/*2.final 用法------------------------------------------------------*/
// 防止类继承(对再上层的基类无效)
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
// 防止虚函数重载
struct Base {
virtual void foo() final;
};
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
【显式地禁止用默认函数】
允许显式地声明采用或拒绝编译器自带的构造函数
class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}
强类型枚举
引入枚举类(enumeration class),使用 enum class
语法进行声明如下,这样定义的枚举实现了类型安全:不能隐式地转换为整数,同时也不能与整数数字进行比较,也不能与不同枚举类型的枚举值进行比较,只能与相同枚举值进行比较。
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
从上面可以看到,可以在枚举类型后面使用冒号及类型关键字来指定枚举中枚举值的类型。
另外,还可以通过重载 <<
符号来输出、获取枚举值。