Effective C++ 笔记
本文记录了 《Effective C++》的拜读记录。
1 让自己习惯 C++
条款 1 :视 C++ 为一个语言联邦
高效编程守则视状况而变化,取决于使用 C++ 的哪一个部分
- C 基础:区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data type)、数组(arrays)、指针(pointer)等。
- Object-Oriented C++:classes(包括构造函数和析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymophism)、vitual 函数(动态绑定)等。
- Template C++:泛型编程(generic programming)
- STL:template 程序库。对容器、迭代器、算法以及函数对象的规约有极佳的紧密配合与协调。
条款 2:尽量以 const,enum,inline 替换 # define
为什么尽量不用 define?
- 不被视为语言的一部分,不记录在符号表中,意味着编译错误信息没有符号输出,也意味着没有类型检查。
- 可能会导致更多的内存占用,比如 double 进行 define 替换比用 const 占用内存多
- define 全局通用,不能限定作用域即不能提供封装性
用 const 替换时需要注意的点:
- 定义指针注意顶层 const 和底层 const
- class 专属常量应为所有类实体共享需定义为 static const
- (网络)C++ 中的 const 是在编译期间确定的常量,而在 C 中只是只读变量,编译期间不知道。
在类里面的
static const int NumTurns = 5;
是常量声明式。如果编译器需要看到一个定义式时(例如取它的地址)需要在实现文件定义const int GamePlayer::NumTurns;
enum hack:“一个枚举类型的数值(常量)可以权当 int 使用”
用 inline 替代 define 宏:
// 用宏定义时出现大麻烦
// 即使全加上括号
# define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
CALL_WITH_MAX(++a, b); // 当 a 较大时被加了两次,否则一次
// 改用 template inline 函数极高效又安全
template<typename T>
inline void callWithMax(const T& a, const T& b) {
f(a > b ? a : b);
}
条款3:尽可能使用 const
- 搞清楚顶层 const 和 底层 const
- 除非有改动参数或 local 对象,否则将它声明 const
// 为什么返回 const
const Rational operator* (const Rational& lhs, const Rational& rhs);
// 如果不 const 则客户可能会这样合法使用即使不是故意的
Rational a, b, c;
if (a * b = c) ...
- const 成员函数(确认该成员函数可作用于 const 对象上)
- 优点:① 使接口容易被理解 ② 使操作 const 对象成为可能
- bitwise const(编译器 const 成员函数检查策略):const 成员函数不更改对象内的任何一个 bit。缺陷:即使对象内的 bit 不发生改变,但仍有改变指针指向的变量发生改变。
- logical constness(违背编译器策略):const 成员函数可以修改对象内的 bit,前提是客户端侦测不出来。此时要对修改的 bit 设为 mutable 变量。
- 当 const 和 non-const 成员函数有实质等价的实现时,令 non-const 版本调用 const 版本避免代码重复。
const char& operator[](std::size_t position) const {
// ...
return text[position];
}
// 非常量版本调用常量版本
char& operator[](std::size_t position) {
// 使用 const_cast 去除底层 const
return const_cast<char&>(
// 强制转换成带有底层 const 的参数调用其 const 版本
// 注意有无底层 const 的参数是重载函数
static_cast<const TextBlock&>(*this)[position]
);
}
条款 4:确定对象被使用前已被初始化
- 使用内置数据类型手动初始化
- 构造函数使用初始化列表来初始化
- 跨编译单元的 non-local static 对象的初始化顺序是不能确定的,因此以 local static 对象替换之
// file_1
extern FileSystem tfs;
// file_2
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
// 使用
tfs();
构造/析构/赋值函数
条款 5:了解 C++ 默认编写并调用哪些函数
默认编写构造函数、拷贝构造函数、拷贝赋值函数和一个析构函数。内联且共有的。
:::default
- 当有自己的构造函数时,编译器不再默认创建
- 拷贝赋值函数只有在生出代码合法且有意义(不可更改引用和 const 常量)——意味着如果具有这样的需要自定义拷贝赋值函数
:::
条款 6:若不想使用编译器自动生成的函数,就该明确拒绝
拒绝的几种方式:
- 私有声明(非定义)该种函数
- 继承如上的空基类
- 采用
= delete
语法
条款 7:为多态基类声明 virtual 析构函数
为什么?
父类指针是可以指向子类的,当用父类指针析构子类时,如果不带 virtual ,它仅仅调用了父类的析构函数,从而子类派生出去那块内存没被释放。
基类一定要声明 virtual 析构函数吗?
并非如此,比如 uncopyable 基类就不必要。因为总没人会将这个作为父级指针(非多态用途)。
想声明一个接口,但没纯虚函数怎么办?
声明纯虚析构函数并提供一个定义。
条款 8:别让异常逃离析构函数
为什么?
因为在多个元素连续析构抛出异常时有两个选择:① 退出程序,可能导致内存泄漏 ② 吞掉异常,可能会出现多个这样的异常导致不明确行为。
析构函数需要执行一些动作,它们可能失败需要抛出异常怎么办?
将那些可能抛出异常的动作设置为普通函数交由用户完成,析构函数做判定再决定怎么处理异常。
条款 9:绝不在构造和析构过程中调用 virtual 函数
为什么?
构造过程是从基类开始构造的,调用的自然也是基类的那个函数,这绝对不是我们想要的。
解决方案:
将基类的那个函数普通化,然后在基类传递信息调用普通函数。子类构造时传递信息给基类构造。
条款 10:令 operator= 返回一个 reference to *this
一个协议,可以实现连续赋值。
条款 11:在 operator= 处理自我赋值
// 不具有异常安全和自我赋值安全的代码
Wiget&
Widget::operator=(const Widget& rhs) {
delete pb; // Bitmap* pb 是 Wiget 里的私有变量
pb = new Bitmap(*rhs.pb); // 如果是自赋值,rhs.pb 是悬挂指针,不具有自我赋值安全
// 如果 new 时产生异常,pb 为悬挂指针
return *pb;
}
// 不具有异常安全的代码
Wiget&
Widget::operator=(const Widget& rhs) {
if (this == &rhs) {
return *this;
}
delete pb;
pb = new Bitmap(*rhs.pb); // 如果 new 时产生异常,pb 为悬挂指针
return *pb;
}
// 较 OK 的方法
Wiget&
Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb); // 有异常 pb 还是原来的 pb
// 自赋值则是拷贝一份新指向
delete pOrig;
return *pb;
}
// copy and swap 技术
Widget&
Widget::operator=(const Widget& rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
// copy and swap 技术另一种形式
Widget&
Widget::operator=(Widget rhs) {
// 值传递,相当于已经拷贝了
swap(rhs);
return *this;
}
资源管理
条款 13:以对象管理资源
“以对象管理资源”的观念常被称为“资源获取时机便是初始化时机”(Resource Acquisition Is Initialization; RAII),运用 C++ memory 库的智能指针。
条款 15:在资源管理类小心 copying 行为
- 在拷贝时采用拷贝底层
- 不拷贝底层,采用引用计数
条款 16:在资源管理类提供对原始数据的访问
- RAII 类应该提供取原始数据的方法
- 对原始资源的访问可能由显式访问和隐式访问,前者安全,后者方便
条款 17:成对使用 new 和 delete 时要采取相同形式
如果在 new 表达式中使用 [] ,必须在相应的 delete 表达式中使用 [] .
条款 18:以独立语句将 newed 对象置入智能指针
// 可能导致内存泄漏:
// 参数调用可能先 new 再执行 priority
// 如果priority 异常则 Widget 没有被智能指针管理
processWidget(std::shared_ptr<Widget>(new Widget), priority);
// 先将智能指针管理原生指针,再调用确保 new 后立马被智能指针管理
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority);
设计与声明
条款 18:让接口容易被正确使用,不易被误用
- 好的接口很容易被使用,不易被误用
- “促进正确使用”的办法包括接口保持一致性,以及与内置类型的行为相容
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
- ** shared_ptr 自定义删除器解决的 DLL 问题
条款 19:设计 class 犹如设计 type
- 类的对象如何创建和销毁?
- 对象的初始化和对象的赋值该有什么差别?
- 新对象如果以值传递意味着什么?
- 什么是新对象的合法值?
- 需要配合某个继承图系吗?
- 新对象需要什么样的转换?
- 什么操作符和函数是合理的?
- 什么样的标准函数应该被驳回?
- 谁该取用对象的成员?
- ** 什么是新类的“未声明接口”?
- 定义类还是类模板?
- 真的需要新类吗?
条款 20:宁以 pass-by-reference-to-const 替换 pass-by-value
为什么?
① 效率问题,以值传递多次进行不必要的构造和析构
② 切割问题,传入子类会对子类切割为父类部分,调用的函数也是父类中的。
什么情况以值传递?
① 内置类型
② STL 迭代器
③ 函数对象
条款 21:必须返回对象时,别妄想返回其 reference
- 不返回 pointer 或 reference 指向一个 local stack 对象:局部对象离开作用域便析构了
- 不返回 reference 指向一个 heap-allocated 对象:没有谁可以对这个对象进行内存释放
- 不返回 pointer 或 reference 指向一个 local static 对象:可能会使用多个这样的返回值,这个时候它们都是同一个
- 返回一个值:因为返回值是拷贝值
条款 22:将成员变量声明为 private
- 切记将成员变量声明为 private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证。
- protected 并不比 public 更具有封装性,其暴露给子类。
条款 23:宁以 non-member、non-friend 替换 member 函数
宁可拿 non-member non-friend 函数替换 member 函数。这样可以增加封装性、包裹弹性和技能扩充性。
可以将这些函数放在独立的头文件但隶属于同一个命名空间。
条款 24:若所有参数皆需要类型转换,请为此采用 non-member 函数
如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个 non-member 函数
const Rational operator* (const Rational& lhs,
const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator);
}
条款 25:考虑写出一个不抛异常的 swap 函数
当拷贝式 std::swap 效率不高时,采用交换指针之类的策略,成员函数的交换只对基本类型进行处理,不需要抛出异常。另外也应该提供一个 non-member swap 来调用前者。
template <>
void swap<Sample>(Sample& s1, Sample& s2 {
s1.swap(s2);
}
// 如果Sample是普通类,则定义swap位于mysample空间中,同时多定义一个位于std空间中(这个多定义不是必须的,只是防御式编程)
template <class T>
void swap(Sample<T>& s1, Sample<T>& s2){
s1.swap(s2);
}
// 如果Sample是模板类时,只能定义在mysample空间中