c++ summary

函数

C++ 里类的四大函数:构造函数、析构函数、拷贝构造函数、拷贝赋值函数。C++11 因为引入了右值(Rvalue)和转移(Move),又多出了两大函数:转移构造函数和转移赋值函数。

所以,在现代 C++ 里,一个类总是会有六大基本函数:三个构造、两个赋值、一个析构。

  • 在 C/C++ 里,所有的函数都是全局的,没有生存周期的概念(static、名字空间的作用很弱,只是简单限制了应用范围,避免名字冲突)。而且函数也都是平级的,不能在函数里再定义函数,也就是不允许定义嵌套函数、函数套函数。

构造函数

skill

“委托构造”(delegating constructor)

使用“委托构造”的新特性,一个构造函数直接调用另一个构造函数,把构造工作“委托”出去,既简单又高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DemoDelegating final
{
private:
int a; // 成员变量
public:
DemoDelegating(int x) : a(x) // 基本的构造函数
{}

DemoDelegating() : // 无参数的构造函数
DemoDelegating(0) // 给出默认值,委托给第一个构造函数
{}

DemoDelegating(const string& s) : // 字符串参数构造函数
DemoDelegating(stoi(s)) // 转换成整数,再委托给第一个构造函数
{}
};

“成员变量初始化”(In-class member initializer)

1
2
3
4
5
6
7
8
9
10
11
12
class DemoInit final
{
private:
int a = 0;
std::string s = "hello";
std::vector<int> v{1, 2, 3};
public:
DemoInit() = default;
~DemoInit() = default;
public:
DemoInit(int x) : a(x) {}
};

“类型别名”(Type Alias)

1
2
using uint_t = unsigned int; // using别名
typedef unsigned int uint_t// 等价的typedef

lambda

C++ 没有为 lambda 表达式引入新的关键字,并没有“lambda”这样的词汇,而是用了一个特殊的形式“[]”,术语叫“lambda 引出符”(lambda introducer)。

1
2
# lambda 表达式示例
auto f1 = [](<入口参数>){函数体};
  • lambda表达式赋值必须用auto(但auto不能用在类成员初始化)
  • lambda 表达式是一个闭包,能够像函数一样被调用,像变量一样被传递
  • 捕获引用时必须要注意外部变量的生命周期,防止变量失效

变量捕获

  • [=]表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改
  • [&]是按引用捕获所有外部变量,内部以引用的方式使用,可以修改

智能指针

尽量不要再使用裸指针、new 和 delete 来操作内存

unique_ptr

  • 尽量不要对 unique_ptr 执行赋值操作就好了,让它“自生自灭”,完全自动化管理。

shared_ptr

  • shared_ptr 支持安全共享的秘密在于内部使用了“引用计数”
  • 因为 shared_ptr 具有完整的“值语义”(即可以拷贝赋值),所以,它可以在任何场合替代原始指针,而不用再担心资源回收的问题,比如用于容器存储指针、用于函数安全返回动态创建的对象,等等

容器

  • 容器都具有的一个基本特性:它保存元素采用的是“值”(value)语义,也就是说,容器里存储的是元素的拷贝、副本,而不是引用。从这个基本特性可以得出一个推论,容器操作元素的很大一块成本就是值的拷贝。所以,如果元素比较大,或者非常多,那么操作时的拷贝开销就会很高,性能也就不会太好。
  • 尽量为元素实现转移构造和转移赋值函数
  • 依据元素的访问方式,分成顺序容器、有序容器和无序容器三大类别

顺序容器

顺序容器就是数据结构里的线性表,一共有 5 种:arrayvectordequelistforward_list,按照存储结构,这 5 种容器又可以再细分成两组。

连续存储的数组:arrayvectordeque

指针结构的链表:listforward_list

  • arrayvector 直接对应 C 的内置数组,内存布局与 C 完全兼容,所以是开销最低、速度最快的容器。它们两个的区别在于容量能否动态增长。array 是静态数组,大小在初始化的时候就固定了,不能再容纳更多的元素。而 vector 是动态数组,虽然初始化的时候设定了大小,但可以在后面随需增长,容纳任意数量的元素。

  • deque 也是一种可以动态增长的数组,它和vector 的区别是,它可以在两端高效地插入删除元素,这也是它的名字 double-end queue 的来历,而 vector 则只能用 push_back 在末端追加元素。

  • vectordeque 里的元素因为是连续存储的,所以在中间的插入删除效率就很低,而 listforward_list 是链表结构,插入删除操作只需要调整指针,所以在任意位置的操作都很高效。

  • 链表的缺点是查找效率低,只能沿着指针顺序访问,这方面不如 vector 随机访问的效率高。list 是双向链表,可以向前或者向后遍历,而 forward_list,顾名思义,是单向链表,只能向前遍历,查找效率就更低了

  • 链表结构比起数组结构还有一个缺点,就是存储成本略高,因为必须要为每个元素附加一个或者两个的指针,指向链表的前后节点。

  • vector 的容量到达上限的时候(capacity),它会再分配一块两倍大小的新内存,然后把旧元素拷贝或者移动过去。这个操作的成本是非常大的,所以,你在使用 vector 的时候最好能够“预估”容量,使用 reserve 提前分配足够的空间,减少动态扩容的拷贝代价。

  • vector 的做法太“激进”,而 dequelist 的的扩容策略就“保守”多了,只会按照固定的“步长”(例如 N 个字节、一个节点)去增加容量。但在短时间内插入大量数据的时候就会频繁分配内存,效果反而不如 vector 一次分配来得好。

有序容器

顺序容器的特点是,元素的次序是由它插入的次序而决定的,访问元素也就按照最初插入的顺序。而有序容器则不同,它的元素在插入容器后就被按照某种规则自动排序,所以是“有序”的。C++ 的有序容器使用的是树结构,通常是红黑树——有着最好查找性能的二叉树。

标准库里一共有四种有序容器:set/multisetmap/multimapset 是集合,map 是关联数组(在其他语言里也叫“字典”),有 multi 前缀的容器表示可以容纳重复的 key。

无序容器

分别是 unordered_set/unordered_multisetunordered_map/unordered_multimap。无序容器同样也是集合和关联数组,用法上与有序容器几乎是一样的,区别在于内部数据结构:它不是红黑树,而是散列表(也叫哈希表,hash table)。因为它采用散列表存储数据,元素的位置取决于计算的散列值,没有规律可言,所以就是“无序”的,你也可以把它理解为“乱序”容器。

算法

  • 算法其实并不神秘,因为所有的算法本质上都是 for 或者 while,通过循环遍历来逐个处理容器里的元素。
  • 追求更高层次上的抽象和封装
  • 算法是专门操作容器的函数,是一种“智能 for 循环”,它的最佳搭档是 lambda 表达式

迭代器

  • 算法只能通过迭代器去“间接”访问容器以及元素,算法的能力是由迭代器决定的。
  • 迭代器也有很多种,比如输入迭代器、输出迭代器、双向迭代器、随机访问迭代器

并发

线程

  • 在 C++ 语言里,线程就是一个能够独立运行的函数
  • 任何程序一开始就有一个主线程,它从 main() 开始运行。主线程可以调用接口函数,创建出子线程。子线程会立即脱离主线程的控制流程,单独运行,但共享主线程的数据。程序创建出多个子线程,执行多个不同的函数,也就成了多线程。
  • 最好的并发就是没有并发,最好的多线程就是没有线程。(简单来说,就是在大的、宏观的层面上“看得到”并发和线程,而在小的、微观的层面上“看不到”线程,减少死锁、同步等恶性问题的出现几率)
  • 四个基本的工具:仅调用一次线程局部存储原子变量线程对象

仅调用一次

先声明一个 once_flag 类型的变量,然后调用专门的 call_once() 函数,以函数式编程的方式,传递这个标志和初始化函数。这样即使多个线程重入 call_once(),也只能有一个线程会成功运行初始化。

1
2
3
4
5
6
static once_flag flag; // 全局的初始化标志
std::call_once(flag,
[](){
cout << "only once" << endl;
}
);

线程局部存储

thread_local 标记的变量在每个线程里都会有一个独立的副本,是“线程独占”的,所以就不会有竞争读写的问题

原子变量

原子变量禁用了拷贝构造函数,在初始化的时候不能用“=”的赋值形式,只能用圆括号或者花括号

1
2
atomic_int x {0}; 
atomic_long y {1000L};

线程对象

thread

TBD

async

async() 会返回一个 future 变量,可以认为是代表了执行结果的“期货”,如果任务有返回值,就可以用成员函数 get() 获取。不过要特别注意,get() 只能调一次,再次获取结果会发生错误,抛出异常std::future_error

如果只是想简单地在线程里启动一个异步任务,完全不关心返回值,可以调用thread的成员函数detach(),比async()会方便一点

序列化

序列化,就是把内存里“活的对象”转换成静止的字节序列,便于存储和网络传输;而反序列化则是反向操作,从静止的字节序列重新构建出内存里可用的对象。

JSON 是纯文本,容易阅读,方便编辑,适用性最广

MessagePack 是二进制,小巧高效,在开源界接受程度比较高

ProtoBuffer 是工业级的数据格式,注重安全和性能,多用在大公司的商业产品里

Avro

Thrift

网络

其他

  • C++ 里也是有垃圾回收的,不过不是 Java、Go 那种严格意义上的垃圾回收,而是广义上的垃圾回收,这就是构造 / 析构函数和 RAII 惯用法(Resource Acquisition Is Initialization)

  • noexcept 专门用来修饰函数,告诉编译器:这个函数不会抛出异常。编译器看到noexcept,就得到了一个“保证”,就可以对函数做优化,不去加那些栈展开的额外代码,消除异常处理的成本。

reference

  • <<罗剑锋的C++实战笔记>>