第一章 基础
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';
}
|