现代C++教程

目录

第一部分 C++98

第 1 章 指南

1.1 编码规范

  • 文件名:小写字母开头,下划线
  • :大写字母开头,驼峰
  • 方法:大写字母开头,驼峰
  • 变量:小写字母开头,下划线

提示:深入理解C++11比现代C++教程更详细

1.2 编程指导

将文件间的编译依赖性降至最低

  1. 只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义
  2. 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与
  3. 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类
  4. 不要在头文件中再(通过#include指令)包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类

注意:这种方法的缺点是在运行时会多耗点时间,也会多耗点内存,不会使用内联函数,因为使用任何内联函数时都要访问实现细节

第 2 章 类

2.1 C++ 对象的内存布局

2.1.1 大小和布局

类大小由类的成员变量和虚函数表指针组成,对象大小和类大小一样。

函数部分:
– 不管成员函数、虚函数,还是类成员函数,都是存放在代码区
– 成员函数和类成员函数的区别是成员函数比类成员函数多一个隐含的this指针
– 编译器为了支持虚函数,在数据部分新增了一个虚函数表

数据部分:
– 类成员变量
– 虚函数表指针
– 不包括类静态成员变量,位于全局数据区
– 总大小需要满足内存对齐要求
– 虚函数表指针固定为8个字节
– 空类大小为1

虚函数表:
– 虚函数表跟随类,每个类初始化的时候确定,所有对象共享一个虚函数表
– 虚函数表存在全局数据区

示例:

class EmptyClass {}; // 大小固定为1

class People {
private:
    int age = 10; // 4 bytes
}; // 大小为4

class People {
public:
    virtual ~People() = default;
private:
    int age = 10; // 4 bytes
}; 
// 大小为 4 + 8 = 12 bytes, 由于对齐要求,实际大小为16 bytes

class People {
public:
    virtual ~People() = default;
private:
    int age = 10; // 4 bytes
    static int nums;
}; 
// 大小依然为16个字节

2.2 单重继承

内存布局:
– 虚函数指针
– 父类虚函数
– 子类虚函数
– 父类成员变量
– 子类成员变量

构造函数和析构函数的调用顺序:
1. 父类构造
2. 子类构造
3. 子类析构
4. 父类析构

单重继承内存布局

代码示例:

#include <iostream>
using namespace std;

class Parent {
public:
    int64_t iparent;
    Parent() : iparent(10) { 
        cout << __func__ << endl; 
    }
    ~Parent() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Parent::f()" << endl; 
    }
    virtual void g() { 
        cout << "Parent::g()" << endl; 
    }
    virtual void h() { 
        cout << "Parent::h()" << endl; 
    }
};

class Child : public Parent {
public:
    int64_t ichild;
    Child() : ichild(100) { 
        cout << __func__ << endl; 
    }
    ~Child() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Child::f()" << endl; 
    }
    virtual void g_child() { 
        cout << "Child::g_child()" << endl; 
    }
    virtual void h_child() { 
        cout << "Child::h_child()" << endl; 
    }
};

class GrandChild : public Child {
public:
    int64_t igrandchild;
    GrandChild() : igrandchild(1000) { 
        cout << __func__ << endl; 
    }
    ~GrandChild() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "GrandChild::f()" << endl; 
    }
    virtual void g_child() { 
        cout << "GrandChild::g_child()" << endl; 
    }
    virtual void h_grandchild() { 
        cout << "GrandChild::h_grandchild()" << endl; 
    }
};

void ShowSingleInherit() {
    GrandChild* obj = new GrandChild();
    auto sz = sizeof(GrandChild);
    auto count = sz / sizeof(int64_t*);
    printf("show single inherit size %d count %d............\n", (int)sz, (int)count);
    int64_t* p = (int64_t*)obj;
    for (int i = 0; i < count; ++i) {
        printf("index %d %p %lld\n", i, p, *p);
        if (i == 0) {
            ShowVTable(obj, 6);
        }
        ++p;
    }
    delete obj;
}

运行结果:

Parent
Child
GrandChild
show single inherit size 32 count 4............
index 0 0x7f9ea8f05cc0 4563129104
         virtual func: 0x10ffb81f0 GrandChild::f()
         virtual func: 0x10ffb8230  Parent::g()
         virtual func: 0x10ffb8270  Parent::h()
         virtual func: 0x10ffb82b0 GrandChild::g_child()
         virtual func: 0x10ffb82f0 Child::h_child()
         virtual func: 0x10ffb8330 GrandChild::h_grandchild()
index 1 0x7f9ea8f05cc8 10
index 2 0x7f9ea8f05cd0 100
index 3 0x7f9ea8f05cd8 1000
~GrandChild
~Child
~Parent

2.3 多重继承

内存布局:
– 第一个父类虚函数指针
– 子类复写的虚函数会替换父类虚函数表对应的函数
– 子类虚函数放在第一个父类虚函数表的最后面
– 第一个父类成员变量
– ……
– 第N个父类虚函数指针
– 子类复写的虚函数会替换父类虚函数表对应的函数
– 第N个父类成员变量
– 子类成员变量
构造函数和析构函数顺序:
1. 第一个父类构造函数
2. 第二个父类构造函数
3. 第N个父类构造函数
4. 子类构造函数
5. 子类析构函数
6. 第N个父类析构函数
7. 第二个父类析构函数
8. 第一个父类析构函数

多重继承内存布局

代码示例:

#include <iostream>
using namespace std;

class Base1 {
public:
    int64_t ibase1;
    Base1() : ibase1(10) { 
        cout << __func__ << endl; 
    }
    ~Base1() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Base1::f()" << endl; 
    }
    virtual void g() { 
        cout << "Base1::g()" << endl; 
    }
    virtual void h() { 
        cout << "Base1::h()" << endl; 
    }
};

class Base2 {
public:
    int64_t ibase2;
    Base2() : ibase2(20) {
        cout << __func__ << endl; 
    }
    ~Base2() {
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Base2::f()" << endl; 
    }
    virtual void g() { 
        cout << "Base2::g()" << endl; 
    }
    virtual void h() { 
        cout << "Base2::h()" << endl; 
    }
};

class Base3 {
public:
    int64_t ibase3;
    Base3() : ibase3(30) {
        cout << __func__ << endl; 
    }
    ~Base3() {
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Base3::f()" << endl; 
    }
    virtual void g() { 
        cout << "Base3::g()" << endl; 
    }
    virtual void h() { 
        cout << "Base3::h()" << endl; 
    }
};

class Derive : public Base1, public Base2, public Base3 {
public:
    int64_t iderive;
    Derive() : iderive(100) {
        cout << __func__ << endl; 
    }
    ~Derive() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        cout << "Derive::f()" << endl; 
    }
    virtual void g1() { 
        cout << "Derive::g1()" << endl; 
    }
};

void ShowMultiInherit() {
    Derive* obj = new Derive();
    int64_t* p = (int64_t*)obj;
    auto sz = sizeof(Derive);
    auto count = sz / sizeof(int64_t*);
    printf("show multi inherit size %d count %d...........\n", (int)sz, (int)count);
    for (int i = 0; i < count; ++i) {
        printf("index %d %p %lld\n", i, p, *p);
        if (i == 0) {
            ShowVTable(p, 4);
        }
        if (i == 2 || i == 4) {
            ShowVTable(p, 3);
        }
        ++p;
    }
    delete obj;
}

运行结果:

Base1
Base2
Base3
Derive
show multi inherit size 56 count 7...........
index 0 0x7fc859f05cc0 4534506480
         virtual func: 0x10e46c860 Derive::f()
         virtual func: 0x10e46c8a0 Base1::g()
         virtual func: 0x10e46c8e0 Base1::h()
         virtual func: 0x10e46c920 Derive::g1()
index 1 0x7fc859f05cc8 10
index 2 0x7fc859f05cd0 4534506528
         virtual func: 0x10e46c960 Derive::f()
         virtual func: 0x10e46c980 Base2::g()
         virtual func: 0x10e46c9c0 Base2::h()
index 3 0x7fc859f05cd8 20
index 4 0x7fc859f05ce0 4534506568
         virtual func: 0x10e46ca00 Derive::f()
         virtual func: 0x10e46ca20 Base3::g()
         virtual func: 0x10e46ca60 Base3::h()
index 5 0x7fc859f05ce8 30
index 6 0x7fc859f05cf0 100
~Derive
~Base3
~Base2
~Base1

2.4 重复继承

重复继承和多重继承的布局完全一样,只是相同的基类会存在两份成员变量的拷贝。

内存布局:
– 第一个父类虚函数指针
– 子类复写的虚函数会替换父类虚函数表对应的函数
– 子类虚函数放在第一个父类虚函数表的最后面
– 第一个父类成员变量
– ……
– 第N个父类虚函数指针
– 子类复写的虚函数会替换父类虚函数表对应的函数
– 第N个父类成员变量
– 子类成员变量
构造函数和析构函数顺序:
1. 第一个祖先构造函数
2. 第一个父类构造函数
3. 第二个祖先构造函数
4. 第二个父类构造函数
5. 子类构造函数
6. 子类析构函数
7. 第二个父类析构函数
8. 第二个祖先析构函数
9. 第一个父类析构函数
10. 第一个祖先析构函数

重复继承内存布局1

重复继承内存布局2

代码示例:

#include <stdio.h>
int g_count = 10;

class parent {
public:
    parent() : a(g_count++) { 
        cout << __func__ << endl; 
    }
    ~parent() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        printf("parent f\n"); 
    }
    virtual void g() { 
        printf("parent g\n"); 
    }
private:
    int64_t a = 10;
};

class parent1 : public parent {
public:
    parent1() { 
        cout << __func__ << endl; 
    }
    ~parent1() {
        cout << __func__ << endl; 
    }
    virtual void f() { 
        printf("parent1 f\n"); 
    }
    virtual void g1() { 
        printf("parent1 g1\n"); 
    }
private:
    int64_t a = 101;
};

class parent2 : public parent {
public:
    parent2() { 
        cout << __func__ << endl; 
    }
    ~parent2() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        printf("parent2 f\n"); 
    }
    virtual void g2() { 
        printf("parent2 g2\n"); 
    }
private:
    int64_t a = 102;
};

class son : public parent1, public parent2 {
public:
    son() { 
        cout << __func__ << endl; 
    }
    ~son() { 
        cout << __func__ << endl; 
    }
    virtual void f() { 
        printf("son f\n"); 
    }
    virtual void gs() { 
        printf("son g\n"); 
    }
private:
    int64_t a = 1000;
};

void ShowRepeatInherit() {
    son* obj = new son;
    int64_t* p = (int64_t*)obj;
    auto sz = sizeof(son);
    auto count = sz / sizeof(int64_t*);
    printf("show repeat inherit size %d count %d.......\n", (int)sz, (int)count);
    for (int i = 0; i < 7; ++i) {
        printf("index %d %p %lld\n", i, p, *p);
        if (i == 0) {
            ShowVTable(p, 4);
        }
        if (i == 3) {
            ShowVTable(p, 3);
        }
        ++p;
    }
    delete obj;
}

运行结果:

parent
parent1
parent
parent2
son
show repeat inherit size 56 count 7.......
index 0 0x7ff253f05cc0 4384023904
         virtual func: 0x1054e9ed0 son f
         virtual func: 0x1054e9ef0 parent g
         virtual func: 0x1054e9f10 parent1 g1
         virtual func: 0x1054e9f30 son g
index 1 0x7ff253f05cc8 10
index 2 0x7ff253f05cd0 101
index 3 0x7ff253f05cd8 4384023952
         virtual func: 0x1054e9f50 son f
         virtual func: 0x1054e9ef0 parent g
         virtual func: 0x1054e9f70 parent2 g2
index 4 0x7ff253f05ce0 11
index 5 0x7ff253f05ce8 102
index 6 0x7ff253f05cf0 1000
~son
~parent2
~parent
~parent1
~parent

2.5 虚继承

虚继承内存布局

虚继承比重复继承会少成员变量,但是会多一个子类虚函数指针,和N个祖先类虚函数指针。虚继承会比重复继承更节省空间。

内存布局: 祖先类虚函数指针和成员变量只有一份,而且出现在首个继承类后面。
– 子类虚函数指针
– 子类成员变量
– 父类虚函数指针
– 父类成员变量
– 祖先类虚函数指针
– 祖先类成员变量
– 第N个父类虚函数指针
– 第N个父类成员变量

构造函数和析构函数顺序:
1. 祖先构造函数
2. 第一个父类构造函数
3. 第二个父类构造函数
4. 子类构造函数
5. 子类析构函数
6. 第二个父类析构函数
7. 第一个父类析构函数
8. 祖先析构函数

代码示例:

#include <stdio.h>
namespace ClassDemo {
class parent {
public:
    virtual void f() { 
        printf("parent f\n"); 
    }
    virtual void g() { 
        printf("parent g\n"); 
    }
private:
    int64_t a = 10;
};

class parent1 : virtual public parent {
public:
    virtual void f() { 
        printf("parent1 f\n"); 
    }
    virtual void g1() { 
        printf("parent1 g1\n"); 
    }
private:
    int64_t a = 101;
};

class parent2 : virtual public parent {
public:
    virtual void f() { 
        printf("parent2 f\n"); 
    }
    virtual void g2() { 
        printf("parent2 g2\n"); 
    }
private:
    int64_t a = 102;
};

class son : virtual public parent1, virtual public parent2 {
public:
    virtual void f() { 
        printf("son f\n"); 
    }
    virtual void gs() { 
        printf("son g\n"); 
    }
private:
    int64_t a = 1000;
};

void ShowVirtualInherit() {
    son* obj = new son;
    printf("show virtual inherit.......\n");
    int64_t* p = (int64_t*)obj;
    for (int i = 0; i < 9; ++i) {
        printf("index %d %p %lld\n", i, p, *p);
        if (i == 0) {
            ShowVTable(p, 4);
        }
        ++p;
    }
}
}

运行结果:

parent
parent1
parent2
son
show virtual inherit size 64 count 8.......
index 0 0x7ff612705cc0 4390753976
         virtual func: 0x105b55480 son f
         virtual func: 0x105b554a0 son g
index 1 0x7ff612705cc8 1000
index 2 0x7ff612705cd0 4390754032
index 3 0x7ff612705cd8 101
index 4 0x7ff612705ce0 4390754080
index 5 0x7ff612705ce8 10
index 6 0x7ff612705cf0 4390754136
index 7 0x7ff612705cf8 102
~son
~parent2
~parent1
~parent

2.6 默认函数控制(编译器生成函数规则)

C++声明自定义的类,编译器默认会生成未自定义的成员函数,这些函数包括构造函数、拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数。

2.6.1 生成规则(默认生成低优先级函数)

优先级: 构造函数 > 拷贝 > 移动

规则说明:
– 定义了高优先级的函数,编译器默认会生成低优先级的,但是定义了低优先级的函数,编译器不会生成高优先级函数
– 定义了移动构造或者拷贝构造,不会生成默认的构造函数
– 定义了拷贝函数,编译器会生成移动函数
– 定义了移动函数,编译器不会自动生成拷贝函数

术语定义:
define: 用户定义
default: 编译器生成
undefine: 函数没有定义
define_must: 初始状态是undefine,用户必须定义才可以使用

construct 1 copy construct 2 move construct 3 copy assign 4 move assin 5
分组1 define(1) default default default default
分组2 define_must define(1) default default default
分组3 define_must undefine define(1) undefine undefine
分组4 default default default define(1) default
分组5 default undefine undefine undefine define(1)
  • 定义了构造函数(1),[2,3,4,5] 有默认函数
    拷贝构造和拷贝赋值:
  • 定义了拷贝构造(2), [ 3,4,5]有默认行为,1 必须定义
  • 定义了拷贝赋值(4), [1,2,3, 5]有默认行为,1有默认函数
    移动构造和移动赋值:
  • 定义了移动构造(3), [2, 4, 5] 未定义, 1必须定义
  • 定义了移动赋值(5), [2, 4, 5] 未定义, 1有默认函数

2.7 成员继承属性

public 继承 protected 继承 private 继承
public 成员 public protected private
protected 成员 protected protected private
private 成员 private private private

2.8 其它特性

  • 运算符重载
  • 可以时候用成员函数重载运算符,也可以使用非成员函数重载运算符
  • 关键字
  • final
  • override:声明虚函数被重载
  • virtual:
  • friends
  • 友元函数或者友元类的声明位置,可以在public, protect, private里面
  • 一个函数或者类,可以是多个类的友元函数或者友元类
  • friend class T可以精简为friend T
  • 单列实现要点
  • 禁止拷贝构造,拷贝赋值,移动构造和移动赋值
  • 构造函数和析构函数声明为protect或者private
  • 实现一个唯一单例对象和一个全局访问函数
  • explicit
  • explicit 关键字只对单个参数的构造函数有效
  • explicit 可以阻止编译器的隐式转换规则

cpp
Person p = {"mj"}; // 拷贝初始化
Person p{"mj"}; // 直接初始化

  • 继承构造函数禁止使用场景
  • 基类构造函数为私有成员函数
  • 派生类从基类中虚继承
  • 使用继承构造函数,编译器就不会为子类生成默认的构造函数
  • 使用继承构造函数,无法初始化派生类成员变量,可以使用c11成员变量初始化特性
  • 几种初始化方式
  • 列表初始化:int a[] = {1, 2, 3, 4}
  • 初始化列表:A(int a, int b) : a_(a), b_(b) {}

第 3 章 容器类型

说明:类的静态成员为什么需要定义?静态成员变量为所有类对象共享,类定义只是声明了变量。

类型 名称 存储结构 插入和删除 随机存取 备注
顺序结构 list 非连续存储 内存占用中 效率高 T(n)=O(n)
vector 连续存储 内存占用少 不必要的内存拷贝 T(n)=O(n) T(n)=O(1)
deque 连续存储 内存占用多 首尾快速插入和删除 T(n)=O(1) 融合了vector和list的功能
forward_list 单向链表
std::array 数组,方便使用stl算法
联合容器 map 红黑树 T(n)=O(lgN) 有序
multimap
set 红黑树
multiset T(n)=O(lgN)
unordered_set 哈希表 一维数组+链表 T(n)=O(1)
unordered_map 哈希表 一维数组+链表 T(n)=O(1) 无序
容器适配 priority queue vector+heap
stack list or deque封闭头部实现
queue list or deque封闭头部实现 默认使用deque

3.1 std::tuple 和 std::array

(待补充内容)

第 4 章 std::string

4.1 构造函数

// 1. 默认构造函数:长度为0,容量为某个固定大小
std::string empty;

// 2. 错误用法:传入NULL会crash,因为strlen(NULL)会crash
// std::string empty(NULL); // 不要这样做!

// 3. 移动构造:str会被设置为空
std::string moved = std::move(str); // str will be set empty

4.2 跨模块传递问题

场景 原因 现象
VC运行库静态和动态混用 存在两个crtheap crash
VC运行库使用不同版本 存在两个crtheap crash

4.3 STL类跨模块存在问题的本质

  • STL提供的类直接或者间接的使用了静态变量,导致不同模块存在多份静态变量
  • 不同STL版本资源的分配和删除实现不一样

4.4 解决方法

模块之间的数据传递使用基本的数据类型。

第 5 章 强类型枚举

5.1 原有枚举类型的缺陷

  • 枚举变量全局可见
  • 数值比较时,被隐式提升为int类型的数据
  • 枚举类型占用的空间大小是不确定的

5.2 强类型枚举定义

enum class Type : char {
    General, 
    Light, 
    Medium, 
    Heavy
};

5.3 原有枚举类型的扩展

enum Type: char {
    General, 
    Light, 
    Medium, 
    Heavy
};

// 枚举成员除了自动输出到父作用域,也可以在枚举类型定义的作用域有效
Type t1 = General;      // 可以使用
Type t2 = Type::General; // 也可以使用

第二部分 C++11

第 1 章 类型推导

1.1 模板类型推导

template <typename T>
void f(ParamType param);

f(expr);

情形1:ParamType 是一个指针或者引用类型,但不是一个通用引用

  • 如果expr是一个引用,忽略引用部分
  • 将expr的类型和ParamType进行模式匹配,来决定T的类型
int x = 27;
const int cx = x;
const int& rx = x;

// sample 1
template <typename T>
void f(T& param);
f(x);  // T is int, ParamType is int&
f(cx); // T is const int, ParamType is const int&
f(rx); // T is const int, ParamType is const int&

// sample 2
template <typename T>
void f(const T& param);
f(x);  // T is int, ParamType is const int&
f(cx); // T is int, ParamType is const int&
f(rx); // T is int, ParamType is const int&

情形2:ParamType 是一个通用引用

  • 如果expr是一个左值,T和ParamType都被推导为左值引用
  • 如果expr是一个右值,参照情形1
int x = 27;
const int cx = x;
const int& rx = x;

template <typename T>
void f(T&& param);
f(x);  // T is int&, ParamType is int&
f(cx); // T is const int&, ParamType is const int&
f(rx); // T is const int&, ParamType is const int&
f(27); // T is int, ParamType is int&&

情形3:ParamType 既不是指针也不是引用

  • 如果expr是一个引用,忽略引用部分
  • 忽略expr的cv特性
int x = 27;
const int cx = x;
const int& rx = x;

template <typename T>
void f(T param);
f(x);  // T 和 ParamType 都是 int
f(cx); // T 和 ParamType 都是 int
f(rx); // T 和 ParamType 都是 int

注意:数组和函数实参退化为指针,除非是传给引用。

1.2 auto类型推导

原理: 模板的类型推导是auto特性的基础,auto只是类型推导,不会带走任何限定符或者引用类型。

template <typename T> 
void f(ParamType param);

const auto cx = 27;
// T -> auto
// ParamType -> const auto

auto类型推导遵循与模板类型推导相同的规则:

  • 情形1: 类型指示符是一个指针或者引用,但不是一个通用引用
  • 情形2: 类型指示符是一个通用引用
  • 情形3: 类型指示符既不是指针也不是引用
auto x = 27;              // x is int
const auto cx = x;        // cx is const int
const auto& rx = x;       // rx is const int&
auto&& uref1 = x;         // x is int and lvalue, so uref1 is int&
auto&& uref2 = cx;        // cx is const int and lvalue, so uref2 is const int&
auto&& uref3 = 27;        // 27 is int and rvalue, so uref3 is int&&

注意事项:
– auto假花括号初始化代表一个std::initializer_list,但模板类型推导却不是
– C++14使用auto来推导函数返回类型和lambdas形参,走的是模板类型推导
– 声明引用的变量会保持其引用对象相同的属性
– 不能用于函数参数、非静态成员变量、数组
– 不可见的代理类型会导致auto从初始化表达式推导出来的类型是错误的
– 显式初始化类型原则强行让auto推导出你想要的类型

1.3 decltype

概述: decltype类型推导会带走cv限定符、指针或者引用,复述一遍变量名或表达式的实际类型。

规则:
– 对于非变量名的类型T表达式,decltype总是报告为类型T&
– T是一个将亡值,返回T&&
– 剩下的都是返回T
– decltype能带走表达式的cv限定符

// decltype(auto): auto标明类型要被推导,而decltype标明推导时用decltype类型
decltype(auto) x = expr;

第 2 章 右值

2.1 基本概念

  • 编译器区分了左值和右值,对右值调用了移动构造函数和移动赋值操作符
  • 没有移动构造函数的情况,调用拷贝构造函数
  • std::move_if_noexcept:移动构造函数存在有noexcept关键字才移动
  • 拷贝语义和移动语义
  • 何时触发移动构造函数:当使用右值(包括将亡值)进行构造或赋值时

左值和右值的定义:
左值:可取地址,有名字的
右值:不能取地址,没有名字的

名称 解释
左值 可取地址,有名字
将亡值 C++11新增,函数返回的右值引用,std::move的返回值
纯右值 C++98的概念,函数返回的临时变量,运算表达式,字面常量,lambda表达式

2.2 左值和右值引用

左值和右值引用示意图

2.2.1 引用类型判断

#include <type_traits>

// 使用示例
is_rvalue_reference<T>
is_lvalue_reference<T>
is_reference<T>

2.2.2 移动类型判断

#include <type_traits>

// 使用示例
is_move_constructible<T>
is_trivially_move_constructible<T>
is_nothrow_move_constructible<T>

2.3 引用折叠与完美转发

  • 定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用
  • 如果两个引用中的任何一个是lvalue引用,那么结果就是lvalue引用
  • 产生引用折叠的上下文有4种:模板实例化,auto类型生成,typedef和别名声明,以及decltype
using TR = T&;
TR& v = 1;

引用折叠示意图

完美转发示例:

template <typename T>
void IamForwording(T&& t) {
    IrunCodeActually(static_cast<T &&>(t));
}

// 左值引用
void IamForwording(T& && t) {
    IrunCodeActually(static_cast<T& &&>(t));
}
// 折叠为:
void IamForwording(T& t) {
    IrunCodeActually(static_cast<T&>(t));
}

// 右值引用
void IamForwording(T&& && t) {
    IrunCodeActually(static_cast<T&& &&>(t));
}
// 折叠为:
void IamForwording(T&& t) {
    IrunCodeActually(static_cast<T&&>(t));
}

2.4 理解std::move和std::forward

(待补充详细内容)

2.5 区分通用引用和右值引用

通用引用的条件:
– 存在类型推导
– 引用声明必须是T&&
– 或者一个对象声明为auto&&

2.6 对右值引用使用std::move,对通用引用使用std::forward

注意:糟糕的想法是对通用引用使用std::move。

返回值优化(RVO):
– 局部对象的类型和返回值的类型相同
– 返回的就是那个局部变量

如果RVO条件满足,但编译器选择不省去拷贝,返回的对象必须是右值。在符合RVO的局部对象,坚决不能使用std::move或者std::forward

2.7 通用引用重载

建议: 避免对通用引用的重载。

通用引用重载的可选方式:
– 放弃重载
– 使用const左值引用,效率不是那么高
– 传值
– 使用标签分派

代码示例:

std::multiset<std::string> names;

template <typename T>
void logAndAdd(T&& name) {
    names.emplace(std::forward<T>(name));
}

void logAndAdd(int idx) {
    names.emplace(std::to_string(idx));
}

// 标签分派
template <typename T>
void logAndAddTag(T&& name) {
    logAndAddImpl(std::forward<T>(name), 
                  std::is_integral<typename std::remove_reference<T>::type>());
}

template <typename T>
void logAndAddImpl(T&& name, std::false_type) {
    names.emplace(std::forward<T>(name));
}

template <typename T>
void logAndAddImpl(int idx, std::true_type) {
    names.emplace(std::to_string(idx));
}
  • 约束接收通用引用的模板

2.8 完美转发失败的情形

  • 花括号初始化
  • 0或者NULL作为空指针
  • 只声明的完整static const数据成员
  • 重载的函数名或者模板名
  • 位字段

第 3 章 Lambda 表达式

3.1 Lambda和函数对象(仿函数)

  • lambda函数会转换为一个仿函数对象
  • lambda函数默认是内联的,而且是const
  • lambda的返回类型可以省略,由编译器进行类型推导
  • lambda按值传递,定义lambda的时候就已经确定
  • lambda按引用传递,其传递的值等于lambda函数调用的值
  • lambda是一种闭包类型,lambda表达式会产生一个闭包类型的临时对象
  • lambda转换函数指针的前提是,和函数原型一致,并且没有捕获任何变量
  • 块作用域以外的lambda函数,捕获列表必须为空

3.1.1 Lambda和仿函数的区别

  • lambda 局部共享:lambda表达式在定义时创建,每个lambda实例是独立的
  • 仿函数全局共享:仿函数对象可以在多个地方复用同一个实例

3.1.2 Lambda捕捉列表

  • [=]:所有捕捉的变量在lambda声明一开始就被拷贝

3.2 建议

  • 避免使用默认捕获模式
  • 默认按引用捕获可能导致引用悬挂
  • 默认按值捕获容易受野指针影响,并且会误导我们,认为lambda是自给自足的
  • 优先使用lambda而非std::bind

第 4 章 智能指针

4.1 智能指针概览

指针类型 应用场景 优点 缺点 备注
auto_ptr 已废弃 不支持拷贝和赋值,容易出现野指针,不能用于数组和容器,不能调用delete[] C++11已废弃
unique_ptr 独享指针 性能好,开销小 不支持拷贝 1. 可用于数组和容器 2. 通过move实现所有权的转移
shared_ptr 共享指针 自动管理引用计数 性能开销较大 线程安全的引用计数
weak_ptr 弱引用指针 解决循环引用问题 不能直接访问对象 需要lock()获取shared_ptr

4.2 shared_ptr 特性

  • std::shared_ptr是原生指针的两倍大小,因为它包含一个指向资源的原生指针,还包括一个指向资源控制块的指针(控制块)
  • 引用计数的递增或者递减必须是原子操作
  • 默认的deleter和allocator的控制块只有3个字节
  • 避免从原生指针创建std::shared_ptr
  • std::shared_ptr和std::weak_ptr共享同一个控制块
  • 带自定义new和delete的类(自定义内存管理的类)不适合使用std::make_shared
  • weak_ptr会导致std::make_shared对象无法释放
支持 例子 备注
构造 shared_ptr() Y std::shared_ptr<int> p;
shared_ptr(T* p) Y std::shared_ptr<int> p(new int());
指针直接赋值构造 N std::shared_ptr<int> p = new int();
自定义删除函数 Y std::shared_ptr<int> p(new int[10], std::default_delete<int[]>()) 支持数组
拷贝构造 Y std::shared_ptr<int> ccp(p);
拷贝赋值 Y std::shared_ptr<int> cap = p;
移动构造 Y std::shared_ptr<int> mcp(p);
移动赋值 Y std::shared_ptr<int> map(p);
交换 Y swap()
重置 Y reset()
运算符* Y
运算符-> Y
获取裸指针 Y get()
获取当前引用计数 Y use_count()
空智能指针 Y 引用计数为0

4.2.1 shared_ptr 构造方式对比

内存分配 图示 优点 缺点
构造函数 两次内存分配 共享信息控制块 对象内存 descript 效率低
std::make_shared 一次内存分配 共享信息控制块和对象内存放一起 descript 效率高 构造函数为保护或者私有无法使用 对象内存可能无法及时回收

说明std::make_shared的优势是一次内存分配,将控制块和对象内存放在一起,效率更高。

4.3 unique_ptr 特性

基本特性:
– unique_ptr是独占式指针,替换auto_ptr
– 无法拷贝赋值
– 保留了移动构造函数
– 默认deleter的std::unique_ptr和原生指针同等大小
– 原生指针无法直接赋值给std::unique_ptr
– deleter函数会增加std::unique_ptr的大小,函数指针会使std::unique_ptr的大小增加一个到两个字节,无状态的函数对象不会导致额外的大小开销,优先使用lambda
– 很容易将一个std::unique_ptr转换为std::shared_ptr

支持 例子 备注
构造 unique_ptr() Y std::unique_ptr<int> p;
unique_ptr(T* p) Y std::unique_ptr<int> p(new int());
指针直接赋值构造 N std::unique_ptr<int> p = new int();
自定义删除函数 Y struct myDel{ void operator()(int *p) { delete []p; } }; std::unique_ptr<int, myDel> p(new int[10]) 支持数组 删除函数只支持函数对象
拷贝构造 N std::shared_ptr<int> ccp(p);
拷贝赋值 N std::shared_ptr<int> cap = p;
移动构造 Y std::shared_ptr<int> mcp(p);
移动赋值 Y std::shared_ptr<int> map(p);
交换 Y swap()
重置 Y reset() 释放指针对象
运算符* Y
运算符-> Y
获取裸指针 Y get()
释放所有权 Y release() 释放当前 unique_ptr 指针对所指堆内存的所有权,但该存储空间并不会被销毁
空智能指针 Y 引用计数为0

4.4 weak_ptr 特性

基本特性:
– weak_ptr没有重载运算符*和->
– 只能从shared_ptr进行初始化
lock()返回shared_ptr,所指对象无效的时候,返回空值
– 不影响原有shared_ptr的计数
– 自定义deleter和花刮花初始化不适用使用std::make_unique

支持 例子 备注
构造 unique_ptr() Y std::weak_ptr<int> p;
unique_ptr(T* p) N std::weak_ptr<int> p(new int());
unique_ptr(shared_ptr p) Y 支持从shared_ptr初始化
指针直接赋值构造 N std::unique_ptr<int> p = new int();
拷贝构造 Y std::weak_ptr<int> ccp(p);
拷贝赋值 & std::weak_ptr<int> cap = p;
移动构造 Y std::weak_ptr<int> mcp(p);
移动赋值 Y std::weak_ptr<int> map(p);
交换 Y swap()
重置 Y reset()
运算符* N
运算符-> N
Y lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
Y expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)
空智能指针 Y 引用计数为0

4.5 shared_ptr实现要点

  • 重载运算符*和->
  • 实现构造函数,虚构函数,拷贝构造,拷贝赋值,移动构造和移动赋值
  • 实现获取裸指针函数和引用计数函数
  • 空智能指针引用计数为0

4.6 空指针类型

4.6.1 NULL指针的缺陷

  • C语言定义:(void*) 0
  • C++定义:0
  • NULL指针被定义为0
  • 0 既可以是一个整形,也可以是一个无类型的指针(void*)
  • C++不允许直接将(void*)0隐式转换到其它类型
  • 重载函数存在二义性
int get(int a);
int get(int* p);
get(NULL);  // 调用的是get(int a),存在二义性

4.6.2 nullptr 指针

  • nullptr是有类型的,它是一个编译器常量,可以被转换成任何类型的指针
  • nullptr_t类型的数据不能转换为非指针,不适用于算术运算表达式
  • nullptr_t类型数据可以用于关系表达式,只能和同类或者指针进行比较

第 5 章 其它特性

5.1 常量表达式

  • 所有constexpr对象都是const对象,但不是所有const对象都是constexpr
  • constexpr函数,使用编译期常量调用它,constexpr返回编译期常量,使用运行期变量调用它,返回运行时结果

常量表达式是一种编译期的常量。

5.1.1 常量表达式函数

规则:
– 函数体只有单一的return语句
– 函数必须返回值
– 在使用前必须已有定义
– return返回语句必须是一个常量表达式,不能使用全局数据

// 定义
constexpr int GetConst() { 
    return 1;
}

5.1.2 常量表达式值

constexpr int i = 5; // i如果是全局变量,只有显式使用,才会生成全局数据

5.1.3 const 二义性

  • const表示只读变量,只读变量和是否允许修改,没有必然的联系
  • const表示常量
  • C++11:const只用于表达只读变量,constexpr表示常量

5.2 for循环

几种for循环对比:

// C++
void Printf(std::vector<std::string>& arr) {
    for (auto& e : arr) {
        printf("%s", e.c_str());
    }
}
// Objective-C
- (void)PrintfArray:(NSArray*) arr {
    for (id e in arr) {
       NSLog(@"%@", e);
    }
}
// JavaScript
const numbers = [45, 4, 9, 16, 25];
let txt = "";
for (let x in numbers) {
  txt += numbers[x];
}
// Swift
for i in 1...5 {
    print(i)
}
# Python
fruits = ["apple", "banana", "cherry"]
for x in fruits:
  print(x)

5.3 类型转换

类型 作用 使用场景 缺点
static_cast 普通类型转换 相关类型转换,编译器会自动截断或补齐;基本类型互转;void*指针;基类和子类的转换(不安全) double转换指针;struct转换为int
const_cast 去除const属性 需要修改const对象时 需谨慎使用
dynamic_cast 父类指针转换为子类 基类和子类互转(安全) 运行时开销
reinterpret_cast 运行时转换 指针和整数的互转;互不相关类型转换,执行按位拷贝 代码移植性差
void dis_1(const int x) {
    // 错误,x是只读的变量
    array<int, x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

void dis_2() {
    const int x = 5;
    array<int, x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

int a = 10;
const int& con_b = a;
cout << con_b << endl;
a = 20;

5.4 顶层const

  • 顶层const:表示指针本身是个常量
  • 底层const:表示指针所指的对象是一个常量

5.5 std::function

提供可调用对象的统一定义方式,支持:
– 函数对象
– 函数指针
– lambda表达式

std::function<void(int, int)> a;

void func(std::function<int(int, int)> p) {
    std::cout << p(3, 4) << std::endl;
}

struct divide {
    int operator()(int denominator, int divisor) {
        return denominator - divisor;
    }
};

divide d;
func(d);  // 可以编译通过

// 如果是下面定义
typedef int (*Func)(int, int);
void func(Func p);
// 编译不通过
divide d;
func(d);  // 错误:不能将divide转换为函数指针

5.6 断言和异常

类型 作用 实现方式
static_assert 静态断言 编译期检查,错误会导致编译器报错
assert 运行断言 运行时检查,失败会终止程序
noexcept 禁止抛出异常 如果抛出异常,程序直接调用std::terminate()终止

5.7 通用属性

  • [[noreturn]]:函数不会返回
  • [[carries_dependency]]:内存模型相关

5.8 正则表达式

(待补充内容)

5.9 std::bind

(待补充内容)

第 6 章 多线程

6.1 std::thread特性

  • std::thread 对象销毁之前,必须调用join或者detach, 否则程序会被terminate。
  • join 和detach都只能调用一次,调用之前用joinable判断是否可以等待
  • 线程detach之后,std::thread对象就无效了
拷贝构造 拷贝赋值 移动构造 移动赋值 detach
std::thread N N Y Y 对象无效

6.2 锁

类型 拷贝构造 移动构造 初始状态 重入 效率
mutex 不允许 不允许 unlock deadlock
recursive_mutex 不允许 不允许 unlock 正常 逻辑复杂,效率低
time_mutex
time_recurisve_mutex
condition_variable 只能配合mutex工作
atomic_flag 不允许 不允许 自旋锁 最小的且不可并行化的操作
类型 优点 缺点
lock_guard 功能简单 性能和内存开销小
unique_lock 功能丰富 性能和内存开销到大

std::adopt_lock : 只获取锁,不加锁
std::defer_lock: 延迟加锁

6.3 std::condition_variable

// std::condition_variable 调用wait函数,当线程block的时候,会自动释放锁
// 当线程被notify的时候,会尝试重新获得锁
// 当线程没有在wait的时候,notify调用是无效的
wait(locker, pred) {
    locker.lock();
    while (pred == null || pred() == false) {
        block thread;
        locker.unlock();
        recv notify;
        locker.lock();
        if (pred == null) {
            break;
        }
    }
}

6.4 内存模型

6.4.1 顺序一致性(C++原子类型默认内存模型)

  • memory_order_seq_cst:全部存取操作都按顺序执行
  • 编译器保证原子操作的指令间顺序不变
  • 处理器对原子操作的汇编指令的执行顺序不变

6.4.2 内存模型枚举值

问题memory_order_relaxedmemory_order_consume有什么区别?

内存模型 枚举值 类型 定义
自由序列 memory_order_relaxed 读写 不对执行顺序做任何保证
排序一致序列 memory_order_seq_cst 读写 按顺序执行(默认)
获取-释放序列 memory_order_acq_rel 读写 memory_order_release和memory_order_acquire先读或写
memory_order_release 本线程,所有写操作完成之后,才能执行本条原子操作;本原子操作之前所有的写原子操作必须完成
memory_order_acquire 本线程,所有后续读操作,必须在本条原子操作完成之后执行;本原子操作必须完成才能执行之后所有读原子操作
memory_order_consume 读写(松散) 本线程,所有后续的有关本原子类型的操作,必须在本条原子操作完成之后执行;只对本原子类型的约束有效

6.5 线程的局部存储

线程独立使用的全局变量:

thread_local int errCode;

6.6 终止函数

函数名称 函数类型 伪代码
terminate 异常退出 调用析构函数 abort()
abort 异常退出
exit 正常退出 调用析构函数 调用atexit注册的函数
quick_exit 正常退出 调用at_quick_exit

6.7 std::ref

用于取某个变量的引用。C++本来就有引用的存在,为什么还要引入std::ref

std::bindstd::thread默认参数的传递方式是值拷贝,如果要使用引用传递值,必须使用std::ref来进行引用绑定。

6.8 std::future

描述 备注
std::aysnc 上层封装 A->B
std::packaged_task 中间层封装 A->B,使用std::thread
std::promise 最底层封装 A↔B, 双向通信,使用std::thread

结果获取:
get():其实就是wait和get的结合,如果发生异常,get会重新触发异常的产生

状态检查:
valid():检查future是否拥有共享状态
wait():等待结果变得可用
wait_for():带超时的等待
wait_until():带时间点的等待

有条件等待结果是否可用,有readytimeoutdeferred三种状态。

6.9 std::async

auto fu = std::async([]{
    LOG("start task");
    std::this_thread::sleep_for(std::chrono::seconds(2));
    LOG("end task");
    return 1;
});
auto status = fu.get();

6.10 std::call_once

void C11Test::CallOnce() {
    static std::once_flag flag;
    std::call_once(flag, []{
        std::cout << "execute once" << std::endl;
    });
}

6.11 std::packaged_task

reset():重置共享状态。

int CountDown(int from, int to) {
    return from - to;
}

void C11Test::TestPackageTask() {
    std::packaged_task<int(int, int)> task(CountDown);
    auto f = task.get_future();
    std::thread th{std::move(task), 10, 0};
    int value = f.get();
    LOG("package task result %d", value);
    th.join();
}

6.12 std::promise

void PrintInt(std::future<int>& f) {
    auto x = f.get();
    LOG("get result %d", x);
}

void C11Test::TestPromise() {
    std::promise<int> pro;
    auto f = pro.get_future();
    std::thread th{PrintInt, std::ref(f)};
    pro.set_value(10);
    th.join();
}

第 7 章 时钟

7.1 duration

std::chrono::duration 表示一段时间,

template <class Rep, class Period = ratio<1> > 
class duration;
  • Rep表示一种数值类型,用来表示Period的数量,比如int、float、double
  • Period是ratio类型,用来表示【用秒表示的时间单位】比如second、milisecond
  • 常用的duration<Rep,Period>已经定义好了,在std::chrono::duration下:
  • ratio<3600, 1> hours
  • ratio<60, 1> minutes
  • ratio<1, 1> seconds
  • ratio<1, 1000> microseconds
  • ratio<1, 1000000> microseconds
  • ratio<1, 1000000000> nanosecons

这里需要说明一下ratio这个类模版的原型:

template <intmax_t N, intmax_t D = 1> 
class ratio;

N代表分子,D代表分母,所以ratio表示一个分数值。
注意,我们自己可以定义Period,比如ratio<1, -2>表示单位时间是-0.5秒。由于各种duration表示不同,chrono库提供了duration_cast类型转换函数

template <class ToDuration, class Rep, class Period> 
constexpr ToDuration duration_cast(const duration<Rep,Period>& dtn);

7.2 time_point

template <class _Clock, class _Duration = typename _Clock::duration>
class _LIBCPP_TEMPLATE_VIS time_point;

7.3 Clocks

  • system_clock:系统时钟,可能被调整
  • steady_clock:单调时钟,不会被调整
  • high_resolution_clocksystem_clock或者steady_clock的别名,提供最高精度的时钟

第 8 章 模板

  • 模板函数的默认模板参数
  • 外部模板
  • 允许局部和匿名类型作为模板实参

8.1 变长模板

变长模板类: 模板参数包

// 模板参数包,多个模板参数打包成单个模板参数包Elements
template<typename... Elements> class tuple;
template<int... A> class NonType;

// 解包,C++11通过一个名为包扩展的表达式完成
template<typename... A> class TDemo : private B<A...> {}

变长模板函数: 模板参数包和函数参数包

8.1.1 模板参数包

模板参数包是模板类或者模板函数的最后一个参数

template<typename... A>

8.1.2 包扩展

template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
    Head head;
};

8.2 模板别名using

using namespace std; // 声明使用命名空间

typedef void (*tFunc)(void);
using uFunc = void(*)(void); // 类型定义

第 9 章 Unicode

Unicode码位从0到10FFFF,总共1114112个,常见的编码方式有UTF8、UTF16、UTF32。Windows内部采用UTF-16,macOS、Linux采用UTF-8编码方式。

  • UTF-8总共用1~4个字节的变长编码方式编码unicode,英文用1个字节表示,中文用3个字节表示
  • UTF-16使用2个或者4个字节的编码方式
  • UTF-32使用4个字节的编码方式

9.1 C++11中的Unicode支持

  • wchar_t:Windows实现为16位
  • C++11规定用UTF-16码位来标识一个unicode字符
  • char16_t:用于存储UTF-16编码的unicode数据
  • char32_t:用于存储UTF-32编码的unicode数据

3种前缀:
u8:表示UTF-8编码
u:表示UTF-16编码
U:表示UTF-32编码
– 宽字符wchar_t前缀L

影响Unicode的因素:
– 代码编辑器
– 编译器
– 输出环境

第 10 章 调试工具

10.1 符号查看工具

  • nm:查看目标文件中的符号
    bash
    nm *.o *.exe

  • c++filt:解析C++符号名称(demangle)
    bash
    c++filt _ZTIN15ilive_noble_svr18QueryNobleLevelReqE

第三部分 C++14

第 1 章 C++14新特性

1.1 函数返回值类型推导优化

// C++11
template <typename T1, typename T2>
auto CustomeAdd11(T1 t1, T2 t2) -> decltype(t1+t2) {
    return t1 + t2;
}

// C++14
template <typename T1, typename T2>
auto CustomeAdd(T1 t1, T2 t2) {
    return t1 + t2;
}

1.2 Lambda表达式可以auto参数和返回值

auto la = [](auto a, auto b) {
    return a + b;
};
auto sum2 = la(10, 16.5);
LOG("add lambda %.2f", sum2);

1.3 变量模板

template<class T>
constexpr T pi = T(3.1415926535897932385L);

int main() {
    cout << pi<int> << endl;    // 3
    cout << pi<double> << endl; // 3.14159
    return 0;
}

1.4 别名模板

template<typename T, typename U>
struct A {
    T t;
    U u;
};

template<typename T>
using B = A<T, int>;

int main() {
    B<double> b;
    b.t = 10;
    b.u = 20;
    cout << b.t << endl;
    cout << b.u << endl;
    return 0;
}

1.5 其它特性

  • std::make_unique
  • std::shared_timed_mutexstd::shared_lock
  • std::exchange
  • std::quoted:给字符串添加引号
  • [[deprecated]]标记:标记已废弃的代码

第四部分 C++17

第 1章 新增特性

1.1 构造函数模板推导

// before C++17
pair<int, double> p(1, 2.2);

// C++17 自动推导
pair p(1, 2.2);
vector v = {1, 2, 3};

1.2 结构化绑定

可以绑定tuple、map、数组、结构体:

std::tuple<int, double> func() {
    return std::tuple(1, 2.2);
}

auto [i, d] = func();
auto& [i, d] = func();

1.3 if-switch语句允许初始化

if (int a = GetValue(); a < 101) {
    cout << a;
}

1.4 内联变量

// header file
struct A {
    static const int value;
};
inline int const A::value = 10;

// ==========或者========
struct A {
    inline static const int value = 10;
};

1.5 折叠表达式

template <typename ... Ts>
auto sum(Ts ... ts) {
    return (ts + ...);
}

1.6 namespace嵌套

namespace A {
    namespace B {
        namespace C {
            void func();
        }
    }
}

// C++17,更方便更舒适
namespace A::B::C {
    void func();
}

1.7 其它特性

  • Lambda表达式捕获*this对象
  • 字符串转换工具
  • std::variant:类似union功能
  • std::optional:可选值类型
  • std::any:可以存储任何类型
  • std::apply:展开tuple作为函数参数
  • std::make_from_tuple:tuple展开作为构造函数参数
  • std::string_view:字符串视图

第五部分 C++20

第 1章 新增特性

  • 模块(Modules):创建模块
  • 协程(Coroutines):异步编程支持
  • std::format:格式化字符串

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

目录

Index