C++

1. 预处理阶段能做什么

  • 预处理不属于 C++ 语言,过多的预处理语句会扰乱正常的代码,除非必要,应当少用慎用;

  • “#include”可以包含任意文件,所以可以写一些小的代码片段,再引进程序里;

  • 头文件应该加上“Include Guard”,防止重复包含;

  • “#define”用于宏定义,非常灵活,但滥用文本替换可能会降低代码的可读性;

  • “条件编译”其实就是预处理编程里的分支语句,可以改变源码的形态,针对系统生成最合适的代码。

2. 编译阶段能做什么

  • “属性”相当于编译阶段的“标签”,用来标记变量、函数或者类,让编译器发出或者不发出警告,还能够手工指定代码的优化方式。

  • 官方属性很少,常用的只有“deprecated”。我们也可以使用非官方的属性,需要加上名字空间限定。

  • static_assert 是“静态断言”,在编译阶段计算常数和类型,如果断言失败就会导致编译错误。它也是迈向模板元编程的第一步。

  • 和运行阶段的“动态断言”一样,static_assert 可以在编译阶段定义各种前置条件,充分利用 C++ 静态类型语言的优势,让编译器执行各种检查,避免把隐患带到运行阶段。

3. 面向对象编程

  • “面向对象编程”是一种设计思想,要点是“抽象”和“封装”,“继承”“多态”是衍生出的特性,不完全符合现实世界。

  • 在 C++ 里应当少用继承和虚函数,降低对象的成本,绕过那些难懂易错的陷阱。

  • 使用特殊标识符“final”可以禁止类被继承,简化类的层次关系。

  • 类有六大基本函数,对于重要的构造 / 析构函数,可以使用“= default”来显式要求编译器使用默认实现。

  • “委托构造”和“成员变量初始化”特性可以让创建对象的工作更加轻松。

  • 使用 using 或 typedef 可以为类型起别名,既能够简化代码,还能够适应将来的变化。

4. 自动类型推导

  • “自动类型推导”是给编译器下的指令,让编译器去计算表达式的类型,然后返回给程序员。

  • auto 用于初始化时的类型推导,总是“值类型”,也可以加上修饰符产生新类型。它的规则比较好理解,用法也简单,应该积极使用。

  • decltype 使用类似函数调用的形式计算表达式的类型,能够用在任意场合,因为它就是 一个编译阶段的类型。

  • decltype 能够推导出表达式的精确类型,但写起来比较麻烦,在初始化时可以采用 decltype(auto) 的简化形式。

  • 因为 auto 和 decltype 不是“硬编码”的类型,所以用好它们可以让代码更清晰,减少后期维护的成本。

5. const & volatile & mutable

  1. const

  • 它是一个类型修饰符,可以给任何对象附加上“只读”属性,保证安全;

  • 它可以修饰引用和指针,“const &”可以引用任何类型,是函数入口参数的最佳类型;

  • 它还可以修饰成员函数,表示函数是“只读”的,const 对象只能调用 const 成员函数。

  1. volatile

  • 它表示变量可能会被“不被察觉”地修改,禁止编译器优化,影响性能,应当少用。

  1. mutable

  • 它用来修饰成员变量,允许 const 成员函数修改,mutable 变量的变化不影响对象的常量性,但要小心不要误用损坏对象。

尽可能多用 const,让代码更安全

6. 智能指针到底智能在哪里

  • 智能指针是代理模式的具体应用,它使用 RAII 技术代理了裸指针,能够自动释放内存,无需程序员干预,所以被称为“智能指针”。

  • 如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加安全。

  • 如果指针是“共享”使用,就应该选择 shared_ptr,它的功能非常完善,用法几乎与原始指针一样。

  • 应当使用工厂函数 make_unique()、make_shared() 来创建智能指针,强制初始化,而且还能使用 auto 来简化声明。

  • shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。

7. C++ 中的异常处理

  • 异常是针对错误码的缺陷而设计的,它不能被忽略,而且可以“穿透”调用栈,逐层传播到其他地方去处理;

  • 使用 try-catch 机制处理异常,能够分离正常流程与错误处理流程,让代码更清晰;

  • throw 可以抛出任何类型作为异常,但最好使用标准库里定义的 exception 类;

  • 完全用或不用异常处理错误都不可取,而是应该合理分析,适度使用,降低异常的成本;

  • 关键字 noexcept 标记函数不抛出异常,可以让编译器做更好的优化。

8. lambda 表达式

  • lambda 表达式是一个闭包,能够像函数一样被调用,像变量一样被传递;

  • 可以使用 auto 自动推导类型存储 lambda 表达式,但 C++ 鼓励尽量就地匿名使用,缩小作用域;

  • lambda 表达式使用“[=]”的方式按值捕获,使用“[&]”的方式按引用捕获,空的“[]”则是无捕获(也就相当于普通函数);

  • 捕获引用时必须要注意外部变量的生命周期,防止变量失效;

  • C++14 里可以使用泛型的 lambda 表达式,相当于简化的模板函数。

9. C++ 中的字符串

  • C++ 支持多种字符类型,常用的 string 其实是模板类 basic_string 的特化形式;

  • 目前 C++ 对 Unicode 的支持还不太完善,建议尽量避开国际化和编码转化,不要“自讨苦吃”;

  • 应当把 string 视为一个完整的字符串来操作,不要把它当成容器来使用;

  • 字面量后缀“s”表示字符串类,可以用来自动推导出 string 类型;

  • 原始字符串不会转义,是字符串的原始形态,适合在代码里写复杂的文本;

10. 详解 C++ 容器

  • 标准容器可以分为三大类,即顺序容器、有序容器和无序容器;

  • 所有容器中最优先选择的应该是 array 和 vector,它们的速度最快,开销最低;

  • list 是链表结构,插入删除的效率高,但查找效率低;

  • 有序容器是红黑树结构,对 key 自动排序,查找效率高,但有插入成本;

  • 无序容器是散列表结构,由 hash 值计算存储位置,查找和插入的成本都很低;

  • 有序容器和无序容器都属于关联容器,元素有 key 的概念,操作元素实际上是在操作 key,所以要定义对 key 的比较函数或者散列函数。

11. 多线程编程

  • 多线程是并发最常用的实现方式,好处是任务并行、避免阻塞,坏处是开发难度高,有数据竞争、死锁等很多“坑”;

  • call_once() 实现了仅调用一次的功能,避免多线程初始化时的冲突;

  • thread_local 实现了线程局部存储,让每个线程都独立访问数据,互不干扰;

  • atomic 实现了原子化变量,可以用作线程安全的计数器,也可以实现无锁数据结构;

  • async() 启动一个异步任务,相当于开了一个线程,但内部通常会有优化,比直接使用线程更好。

Last updated