C++编程规范:构造、析构与复制

第47条:以同样的顺序定义和初始化成员变量

  1. 成员变量初始化的顺序要与类定义中声明的顺序始终保持一致,不用考虑构造函数初始化列表中编写的顺序
  2. C++语言之所以采取这样的设计,是因为要确保销毁成员的顺序是唯一的。否则,析构函数将以不同的顺序销毁对象,具体顺序取决于构造对象的构造函数。 解决方案是,总是按成员声明的顺序编写成员初始化语句。

    第48条:在构造函数中使用初始化代替赋值

  3. 在构造函数中,使用初始化代替赋值来设置成员变量,能够防止发生不必要的运行时操作,而输入代码的工作量则保持不变。
  4. 例外情况:应该总是在构造函数体内而不是初始化列表中执行非托管资源获取,比如并不立即将结果才传递给智能指针构造函数的new表达式。

    第49条:避免在构造函数和析构函数中调用虚拟函数

  5. 从构造函数或析构函数直接或间接调用未实现的纯虚拟函数,会导致未定义的行为。
  6. 如果希望从基类构造函数或者析构函数虚拟分派到派生类,那么需要采用其他技术,比如后构造函数(post-constructor)
  7. 后构造技术:必须在构造了完整的对象之后立即调用虚拟函数,以下为后构造的实现技术的选项列表:
    • 推卸责任:在文档中说明用户代码必须在构造了对象之后立即调用后初始化(post-initizlization)函数
    • 迟缓后初始化:在第一次成员函数调用时进行后初始化,基类中有一个布尔标识说明是否已经发生了后初始化。
    • 使用虚拟基类语义:语言规则要求构造函数最底层的派生类决定调用哪个基类构造函数
    • 使用工厂函数
class B{
protected:
    B(){/*...*/}
    virtual void postInitizlize(){/*...*/}

public:
    template<class T>
    static shared_ptr<T> create(){
        shared_ptr<T> p(new T);
        p->postInitizlize();
        return p;
    }
};

class D: public B{/*...*/};
shared_ptr<D> p = D::create<D>();

第50条:将基类析构函数设为公有且虚拟的,或者保护且非虚拟的

  1. 对于基类析构函数,要么允许通过基类指针虚拟地调用,要么完全不允许,不能选择非虚拟的调用。所以基类析构函数如果能够被调用(即是公用的),那么它就是虚拟的,否则就是非虚拟的。
  2. 客户要么能够使用基类指针多态地删除,要么不能:
    • 含有多态删除的基类。如果允许多态删除,则析构函数必须是公用的,而且必须是虚拟的(否则会导致未定义行为)
    • 不含多态删除的基类。如果不允许多态删除,则析构函数就必须是非公用的(这样调用代码就不能调用它),而且应该是非虚拟的(因为不需要是虚拟的)
    • 例外情况:B是一个基类又是一个可以被自身实例化的具体类,而且既没有虚拟函数也不想被多态地调用。此时,可以将析构函数设为公用且非虚拟的,但是应该在文档中明确说明:进一步派生的对象要和B一样不能被多态地调用。

      第51条:析构函数、释放和交换绝对不能失败

  3. 绝对不允许析构函数、资源释放(deallocation)函数或者交换函数报告错误,具体一些就是,绝对不允许将那些析构函数可能会抛出的异常的类型用于C++标准库 原因是:它们时事务编程中两个关键操作所必需的:在处理过程中,遇到问题时就撤销操作,没有问题出现时就提交任务。
  4. 当使用异常作为错误处理机制时,建议使用一和注释掉的空异常规范/throw()/来声明这些函数,通过这种方式说明这一行为。

    第52条:一致地进行复制和销毁

  5. 如果定义了复制构造函数、复制赋值操作符或者析构函数中任何一个,那么可能也需要定义另一个或者另外两个。
  6. 在许多情况下,如果能通过RAII“拥有”对象的方式正确地持有封装起来的资源,就没有必要自己编写这些操作了。
  7. 例外情况:如果声明这三个特殊函数之一,只是为了将它们设为私有的或者虚拟的,而没有什么特殊语义的话,那么就意味着不需要其余两个函数。

在一个包含引用或者auto_ptr的类中,可能还需要编写复制构造函数和赋值操作符,但是默认析构函数已经能够正常工作了。

第53条:显式地启用或者禁止复制

要保证类能够提供合理的复制,否则就根本不要提供,可能的选择如下: * 显式地禁止复制和赋值 * 显式地编写复制和赋值 * 使用编译器生成的版本,最好是加上一个明确的注释

第54条:避免切片。在基类中考虑使用克隆代替复制

  1. C++内存模型规定:如果出现继承结构,内存分别一定是先基类部分的数据,后派生部分的数据。将派生类对象转换为基类对象时,就会发生对象切片。
  2. 在基类中,如果客户需要进行多态复制的话,那么要考虑禁止复制构造函数和赋值操作符,而改为提供虚拟的Clone成员函数
  3. 将基类的复制构造函数设为explicit,不仅有助于避免隐含的切片,而且也阻止了所有通过值的传递
  4. 例外情况:有些设计可能会要求基类的复制构造函数保持为公有的,在这种情况下应该使用(智能)指针来传递,而不是通过引用来传递。

    第55条:使用赋值的标准形式

  5. 在实现operator=时,应该使用标准形式–具有特定签名的非虚拟形式
  6. 签名形式: T& operator=(const T&); t& operator=(const T&);
  7. 不要返回const T&
  8. 要显式调用所有基类赋值操作符,并为所有数据成员赋值,交换可以自动为我们处理好所有这一切,要返回*this。

    第56条:只要可行,就提供不会失败的swap

  9. 对于原始类型和标准容器而言,使用std::swap就可以了,其他的类可能需要各种名字的成员函数来实现交换 ``` c++ T& T::operator=(const T& other){ T temp(other); swap(temp); return *this; }

T& T::operator=(T temp){ swap(temp); return *this; }

2. 绝对不要这样做:通过一个后跟定位new的显式析构函数,用复制构造来实现复制赋值。也就是说,绝对不要这样编写:
``` c++
T& T::operator=(const T& rhs){
    if(this != &rhs){
        this->~T();
        new(this)T(rhs);
    }
    return *this;
}
  1. 当用户定义类型的对象有办法比野蛮赋值更高效地交换值时,应该在与用户自动以类型相同的名字空间中提供一个非成员交换函数。此外,还可以考虑为自己的非模板类特化std::swap
    namespace std{
     template<>
     void swap(MyType& lhs,MyType& rhs){
         lhs.swap(rhs);   //使用MyType::swap()
     }
    }
    
  2. 例外情况:对于有值语义的类来说,交换是有用的。但是对于基类来说往往就没有很大用处了,因为总是在通过指针使用基类。
Table of Contents