第一章 基础

hello world 示例

1
2
3
4
5
import std;

int main() {
    std::cout << "Hello, World!\n";
}

函数

函数声明包含名称、参数和返回值,返回值在名称之前。

函数的类型由他的返回值类型和参数类型序列组成 普通函数 double get(const vector<double>& vec, int index) 的类型是 double(const vector<double>&, int); 而类成员函数包含类名,如char& ClassA::operator[](int index) 的类型是char& ClassA::(int)

如果两个函数有相同的名称但不同的参数类型,则编译器会选择最适合每个调用的函数

类型、变量与运算

常见的基础类型有 char、bool、int、unsigned 和double 等等

常用算是类型转换会以最高操作对象精度元素,如 double 和 int 类型的加法会以 double 的精度进行

可以使用 C语言方式 = 来初始化对象,但建议使用 {} 来初始化,避免隐式类型转换

1
2
3
4
5
6
doube d1 = 2.3;
double d2 {2.3};
double d3 = {2.3};  // =符合可以省略

int i1 = 7.3; // 发生隐式转换,i1=7
int i2 {7.8} // 报错!

当定义变量时,可以从初始化符号推导出来,可以无需指定类型,用 auto 代替

1
2
3
auto b = true; // bool 类型
auto i = 123; // int 类型
auto d = 1.2; // double 类型

常量

有 2 种常量

  • const: 主要用来说明接口,编译器负责执行 const 承诺,可以在运行时计算
  • constexpr: 主要用于声明常量,把数据置于只读内存区域,必须由编译器计算
1
2
3
4
int var = 17;
const double sqv = sqrt(var);
constexpr int dmv = 27;
constexprt double sqv2 = sqrt(var); // 错误:不能是个非常量表达式

被声明为 constexpr 或者 consteval 的函数是 c++ 版本的纯函数,必须在编译时计算,且不能有任何副作用,只能使用输入参数作为信息。

指针、数组和引用

在声明中,[] 表示对应类型的数组,* 表示指向对应类型的指针,& 表示指向对应对象的引用。 引用和指针类似,但可以不使用前缀*就能直接访问引用对象的值,而且引用初始化滞后就不能再指向其他的对象。

在表达式中,前置一元操作符* 表示取内容,前置一元操作符& 表示取地址

建议使用 nullptr 而非 0 来表示空指针,避免和整数类型混淆

初始化

初始化是将一段没有被初始化的内存区域变成一个有效的对象,对几乎所有的数据类型而言,对未初始化的对象的读写操作都是未定义的。

要让赋值操作成功进行,被赋值的对象必须拥有一个有效的值

赋值

对于内置类型来说,赋值语句就是简单的机器赋值指令。两个对象是独立的,修改 y 值的时候不会影响 x 的值。

1
2
3
int x = 2;
int y = 3;
x = y;

如果希望不同的对象指向(共享)相同的值,必须明确指定

1
2
3
4
5
int x = 2;
int y = 3;
int* p = &x;
int* q = &y;
p = q;  // p 和 q 两个指针都指向了 y

给引用赋值改变的是引用对象的值

1
2
3
4
5
int x = 2;
int y = 3;
int& r = x;
int& r2 = y;
r = r2;   // 从 r2 读取,通过 r 写入,x 变成 3

第二章 用户自定义类型

结构

struct 将所需的元素组织在一起

如下有一个简单的 Vector struct 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Vector {
    double* elem;
    int sz;
}

void vector_init(Vector& v, int s) {
    v.elem = new double[s];
    v.sz = s;
}

doubel read_and_sum(int s) {
    Vector v;
    vector_init(v, s);
    
    for (int i=0; i !=s; ++i) {
        std::cin >> v.elem;
    }
    
    double sum = 0;
    for (int i=0; i != s; i++) {
        sum += v.elem[i];
    }
    return sum;
}

自定义类型的名称通常使用首字母大写,以便和标准库类型区分

访问 struct 成员有两种方式, 通过名字或引用时用. 符号,通过指针时用-> 符号

1
2
3
4
5
void f(Vector v, Vector& rv, Vector* pv) {
    int i1 = v.sz;
    int i2 = rv.sz;
    int i3 = pv->sz;
}

类将数据和操作组织在一起,类的 public 成员定义了该类的接口,private 成交则只能通过接口访问

class 和 struct 没有本质区别,唯一区别在于 struct 成员默认是 public 的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Vector {
public:
    Vector(int s) : elem{new double[s]}, sz{s} {}

    double &operator[](int i) { return elem[i]; }

    int size() { return sz; }

private:
    double *elem;
    int sz;
};

Vector 对象是一个句柄,包含指向 元素的指针(elem) 和元素的数量(sz), Vector 包含 Vector()、 operator[] 和 size() 三个接口。

和类名同名的成员函数为构造函数,用来构造类的实例

枚举

枚举类型用来表示少量整数数值的集合,提升代码的可读性,降低潜在错误。

1
2
3
4
5
6
enum class Color {
    red, blue, green
};
enum class TrafficLight {
    green, yellow, red
};

enum 后面的 class 表示这个枚举类型是强类型,具备独立的作用域,不同的 enum class 是不同的类型

enum class 不可以隐式地和整数混用

联合

union 是一种特殊的 struct,他的所有成员被分配在同一块内存区域中,实际占用的空间就是它最大的成员所占用的空间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
enum class Type {ptr, num};

union Value {
    char* p;
    int i;
};


struct Entry {
    Type t;
    Value v;
};

void f(Entry* pe) {
    if (pe->t == Type::num) {
        std::cout << "num: " << pe->v.i << "\n";
    } else {
        std::cout << "ptr: " << *(pe->v.p) << "\n";
    }
}

第三章 模块化

c++ 中有两种方式可以实现分离编译

  • 头文件
  • 模块

头文件有明显的缺点

  • 编译耗时,对于同一个文件,每 #include 一次编译器就需要处理一次
  • 顺序依赖
  • 代码膨胀

c++ 20 引入了 module, 使用 import 来引入 module

函数参数和返回值

参数传递

参数默认使用传值的方式,为了性能,可以使用传引用的方式

返回值

返回值的默认行为也是复制传值。 可以通过给对象提供移动构造方法,将对象移动到函数之外

返回类型后置

相比传统的记法,后置返回记法更符合逻辑

1
2
auto next_elem() -> Elem*;
auto sqrt(doyble) -> double;

第四章 错误处理

使用 throw error 抛出错误,使用 try ... catch (error) 来捕捉错误

错误处理通常有三种方式:

  • 抛出异常
  • 返回错误码
  • 终止程序(如调用 exit之类的函数)

第五章 类

具体类

具体类的典型特征是它的成员变量是其定义的一部分

RAII (Resource Acquisition Is Initialization) 资源获取即初始化 是 c++ 一种惯用管理内存的方式, 它能保证已构造的对象,最后会被销毁,即其析构函数会被调用。

抽象类

抽象类将使用者和类的实现细节完全隔离,即将接口和实现完全解耦,并放弃了纯局部变量

1
2
3
4
5
6
class Container {
public:
    virtual double& operator[](int) = 0;
    virtual int size() const = 0;
    virtual ~Container() {}
};

如上所示, Container 即为一个抽象类。使用 关键字virtual 声明的函数为虚函数,虚函数可能会在派生类中被重新定义。 = 0 后缀表示该函数为纯虚函数,必须在派生类中定义该函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class List_container:public Container {
private:
    std::list<double> ld;
public:
    List_container() {}
    List_container(std::initializer_list<double> il): ld{il} {}
    ~List_container() {}

    double& operator[](int i) override;
    int size() const override { return ld.size();}
};

double& List_container::operator[](int i) {
    for (auto& x: ld) {
        if (i == 0) {
            return x;
        }
        --i;
    }
    throw std::out_of_range{"List container"};
}

: public 表示"派生自"或者"是…的子类型"。基类和派生类的关系称之为继承。

override 是可选的,但可以显示声明覆盖基类对应函数的意图

虚函数

每个含有虚函数的类都有一个虚函数表,所以对应的实例都有一个指针,来指向这个共享的表

第六章 基本操作

拷贝和移动

我们可以通过定义拷贝构造函数和拷贝赋值操作符来控制拷贝过程,通过使用引用类型可以减少拷贝对象的开销

1
2
3
4
Vector::Vector(Vector&& a) :elem{a.elem}, sz{a.sz} {
    a.elem = nullptr;
    a.sz = 0;
}

Vector 的移动构造函数如上,符号&& 代表右值引用,可以给该引用绑定一个右值。

左值的大致含义是能出现在赋值操作符左侧的内容,而右值正好与其相反,是无法为其赋值的值。

右值引用就是引用了一个别人无法赋值的内容,所以可以安全地"窃取"它的值。

第七章 模版

参数化类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template<typename T>
class Vector {
private:
    T *elem;
    int sz;
public:
    explicit Vector(int s);

    ~Vector() { delete[] elem; }

    T &operator[](int i);

    const T &operator[](int i) const;

    int size() const { return sz; };
};

template<typename T>
Vector<T>::Vector(int s) {
    if (s < 0) {
        throw std::length_error{"Vector constructor: negative size"};
    }

    elem = new T[s];
    sz = s;
}

template<typename T>
const T &Vector<T>::operator[](int i) const {
    if (i < 0 || i >= size()) {
        throw std::out_of_range{"Vector::operator[]"};
    }
    return elem[i];
}

使用 template 可以将 Vector 改成成支持任意类型的动态数组

模版是一种编译时机制,在编译过程进行实例化时,每个实例都会生成一份代码

可以对模版添加 concept 概念,用来对模版参数添加限制

参数化操作

模版函数

1
2
3
4
5
6
7
template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v) {
    for (auto x: s) {
        v += x;
    }
    return v;
}

模版函数可以是类的成员函数,但不能是虚函数,因为编译器不知道模版的所有实例,不能为模版函数生成 vtb1 虚函数表

函数对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template<typename T>
class Less_than {
       const T val;
public:
        Less_than(const T& v): val{v} {}
        bool operator()(const T& x) const {return x < val;}
};

void fct(int n, const std::string& s) {
    Less_than lti {42};
    Less_than<std::string> lts {"Backus"};

    bool b1 = lti(n);
    bool b2 = lts(s);
}

函数对象用来定义对象,该对象可以像函数一样被调用。

operator() 称之为应用操作符

匿名函数

[&](int a) {return a < x} 是一个匿名函数,

[&]是匿名函数的捕获列表,它指定了函数体内的局部变量可以使用引用形式访问,如果使用值的方式则为[=], 什么都不捕获则为[]

模版机制

别名模版

1
2
3
4
5
6
7
template<typename Key, typename Value>
class Map {};

template<typename Value>
using String_map = Map<std::string, Value>;

using String_int_map = String_map<int>;

第八章 概念和泛型编程

概念

模版中,类型名称指示符 typename 是限定程度最低的,我们可以使用概念使之定义更加清晰明确

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template<std::forward_iterator Iter>
void advance(Iter p, int n) {
    while (--n) {
        ++p;
    }
}

template<std::random_access_iterator Iter>
void advance(Iter p, int n) {
    p += n;
}

void use_advance(std::vector<int>::iterator vip, std::list<std::string>::iterator  lsp) {
    advance(vip, 10);
    advance(lsp, 10);
}

如果有多个可选的模版,编译器会选择满足最严格参数需求的版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template<typename B>
concept Boolean = requires(B x, B y) {
    { x = true };
    { x = false };
    { x = ( x == y) };
    { x = ( x != y) };
    { x = !x };
    { x = (x = y) };
};

template<typename T, typename T2 = T>
concept Equality_comparable = requires (T a, T2 b) {
            { a == b } -> Boolean;
            { a != b } -> Boolean;
            { b == a } -> Boolean;
            { b != a } -> Boolean;
};

如上示例,我们可以使用 concept 自己定义概念

泛型编程

可变参数模版

定义模版时,可以令其接受任意数量任意类型的实参,这样的模版称之为可变参数模版

1
2
3
4
5
6
7
template<typename T>
concept Printable = requires(T t) {std::cout << t;};

template<Printable ...T>
void print(T&&... args) {
    (std::cout << ... << args) << '\n';
}