1. C++为多语言联合体

没有包打天下的最佳实践

2. 用const, enum替换宏define

const: 常变量

enum: int常量

3. 任何应该const的地方都const

除了字面含义外,提到了const性的不同也会影响重载。

还有两种实践:

第一种:const对象对外表现出const性,但实际上内部成员需要改变,如缓存成员。使用mutable修饰缓存成员。

第二种:同时实现const方法和非const方法时,可以用非const方法调用const方法。将*this通过static_cast转为const的对象,再通过const_cast去掉返回值的const性。

4. 对象使用前初始化

非成员手动初始化。

全部成员均在初始化列表里进行初始化(初始化列表能够保证初始化顺序)。

非局部的静态对象使用单例模式保证初始化顺序(magic static)。

5. 编译器为类加的函数

默认构造、拷贝构造、移动构造、拷贝运算符、移动运算符、析构。

一旦有构造函数被定义,无论参数如何,都不会自动生成默认构造。

移动是拷贝的特例,定义拷贝则不生成移动,但定义移动依然可生成拷贝。

特别要注意引用成员的拷贝问题,到底要拷贝谁。

6. 禁用不希望编译器添加的函数

除了用= delete;还可以创建一个基类,将禁用的函数声明在private域内(不用实现)。这个基类描述子类的一种特征。但多继承会使得这样的禁用失效。我觉得这个约束并不好。

7. 多态基类的析构函数必须是virtual的

一旦成员函数有virtual(表明要多态),析构就应该是virtual的

不用多态的类,析构不应该加virtual

8. 析构函数不能抛异常

处理一个异常,进行栈展开时,会析构很多对象,如果析构对象时出现异常,就会双重异常,直接std::terminate

还可能资源泄漏。

本身也是一个未定义行为。

如果实在有异常,应该原地解决:std::abort()或者catch(...){}

9. 构造/析构时不要调用virtual function

只会调用到自己的实现,不会调用到派生的实现,因为派生要么没构造,要么已经被析构。

10. 支持链式操作的函数返回自身的引用

Foo& operator=(const Foo& rhs) { return *this; }

感觉这是在水字数

11. 注意operator=中的自我赋值

在operator=中有可能发生自我赋值,如果自己有指针成员,就有可能出现问题:

Foo& operator=(const Foo& rhs)
{
    // 先释放有可能存在的已有内存,如果rhs和*this是一个对象,那么该对象的ptr_member已被释放
    delete this->ptr_member;
    // rhs.ptr_member是悬空指针
    this->ptr_member = new MemberType(rhs.ptr_member); // 会调用MemberType的拷贝构造
    // 好了,悬空了,歇菜了
    return *this;
}

解决方案1:在函数调用的最开始判断this是否和&rhs相等。

解决方案2:先缓存this->ptr_member,再拷贝rhs成员至this,再释放缓存指针指向的内存(同时异常安全)。

解决方案3:拷贝rhs,创建临时实例。实现swap成员函数将this与临时实例交换(同时异常安全)。

12. 拷贝构造/运算符要所有自己的、基类的成员

派生类拷贝构造要调用基类拷贝构造;派生类operator=要调用基类的operator=。

拷贝构造不应调用operator=,因为operator=要求对象已初始化。

operator=不应该调用拷贝构造,因为对象已经被构造完毕。

减少代码重复的方式是单开一个private函数来实现逻辑,一般叫init

13. RAII

一般翻译成“资源获取即初始化”,正常人看不懂的。感觉“把资源获取作为对象的初始化”才合理一些,同样“把资源释放作为对象的析构”。

这里特别提了一下,C风格的array存在智能指针里是有问题的,智能指针析构默认调用delete而不是delete[]。

14. 注意处理RAII类的拷贝逻辑

拷贝逻辑还是得遵循资源本身的拷贝逻辑,比如堆内存应该深拷贝、锁应该移动等。

常见的拷贝逻辑:禁止拷贝/移动、引用计数、深拷贝、move only。

特别提到了shared_ptr是可以指定deleter的,所以shared_ptr可以在RAII类内部去当引用计数实现。

15. RAII类提供对原始资源的访问

比如smart_ptr->get()返回堆内存头的裸指针。

这种行为破坏了封装性。但是!但是!RAII的设计哲学不在封装,而在通过构造和析构的机制去管理资源的获取和释放。

同时我们(我自己)了解到,class还能实现隐式类型转换操作符,写作operator Type() {...}。没有返回值类型,没有参数,但是有返回值。(奇怪又没用的知识增加了)但是这东西看起来就很危险,毕竟现在我们倡导的是杜绝一切隐式类型转换,哪怕增加语法噪音。

16. new和delete成对出现,new Foo[N]和delete[] foo_array成对出现

否则就是未定义行为。

这里主要是针对用typedef的人,比如typedef std::string Line[4];,释放的时候就应该用delete[]了。

17. 使用独立语句将资源置入RAII类

比如func(std::shared_ptr<Foo>(new Foo), other_func());

可能会产生这么一个运算顺序:new Foo -> other_func() -> shared_ptr构造

这本身没问题,但如果other_func()抛异常了就内存泄漏了

所以建议像shared_ptr这种RAII类的初始化单独写。

但其实在新标准中使用make_shared就没这个问题了吧应该。

如果觉得我的文章对你有用,请随意赞赏