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

来源:互联网 发布:善领电子狗车载软件 编辑:程序博客网 时间:2024/05/16 10:16

第47条 以同样的顺序定义和初始化成员变量
与编译器一致:成员变量初始化的顺序要与类定义中声明的顺序始终保持一致;不用考虑构造函数初始化列表中编写的顺序。要确保构造函数代码不会导致混淆地指定不同的顺序。
详细:
1、违反的危害性很大,而且很难发现。
2、C++ 这么设计是为了确保销毁成员的顺序是唯一的,避免底层开销。
3、解决方案是,总是按成员声明的顺序编写成员初始化语句。

第48条 在构造函数中用初始化代替赋值
设置一次,到处使用:在构造函数中,使用初始化代替赋值来设置成员变量,能够防止发生不必要的运行时操作,而输入代码的工作量则保持不变。
详细:
1、并未显式初始化的对象将使用其默认构造函数自动初始化。
2、这不是不成熟的优化,这是在避免不成熟的劣化(见 C9)。
3、应该总是在构造函数体内而不是初始化列表中执行非托管资源获取,比如并不立即将结果传递给智能指针构造函数的 new 表达式。

第49条 避免在构造函数和析构函数中调用虚拟函数
虚拟函数仅仅“几乎”总是表现得虚拟:在构造函数和析构函数中,它们并不虚拟。更糟糕的是,从构造函数或析构函数直接或者间接调用未实现的纯虚拟函数,会导致未定义的行为。如果设计方案希望从基类构造函数或者析构函数虚拟分派到派生类,那么需要采用其他技术,比如后构造函数(post-constructor)。
详细:
1、有些设计方案需要“后构造”,即必须在构造了完整的对象之后立刻调用虚拟函数。有如下方式:推卸责任(以文档方式说明)、迟缓后初始化(通过布尔标志)、使用虚拟基类语义、使用工厂函数。
2、使用工厂函数实现“后构造”虚拟调用:

class B{protected:B() { /* ... */ }virtual void PostInitialize() { /* ... */ }// 在构造之后立即调用public:template<typename T>static std::shared_ptr<T> Create()// 用于创建对象的接口{std::shared_ptr<T> object = std::make_shared<T>();assert(object);object->PostInitialize();return object;}// VC2010 暂未支持可变参数模板,所以下面代码暂时无法编译,同时可能存在错误template<typename T, typename... Args>static std::shared_ptr<T> Create(Args... &&args){std::shared_ptr<T> object = std::make_shared<T>(std::forward(args...));assert(object);object->PostInitialize();return object;}};class D : public B { /* ... */ };// 某个派生类std::shared_ptr<D> objectD = D::Create<D>();// 创建一个 D 对象
3、上面的方案存在如下缺点:派生类不能公开公用构造函数,否则用户无需调用 PostInitialize() 就能够创建对象;分配局限于 operator new ,但是 B 可以改写 new(见 C45, C46);D 必须定义一个与 B 选择的参数相同的构造函数。

第50条 将基类析构函数设为公用且虚拟的,或者保护且非虚拟的
删除,还是不删除,这是个问题:如果允许通过指向基类 Base 的指针执行删除操作,则 Base 的析构函数必须是公用且虚拟的。否则,就应该是保护且非虚拟的。
详细:
1、当且仅当基类析构函数是公用的,才将其设为虚拟的。
2、编写一个基类,就是定义一个抽象(见 C35, C37)
3、总是为基类编写析构函数,因为隐含生成的析构函数是公用且非虚拟的。
4、类似 COM 和 CORBA 等组件架构没有使用标准的删除机制,需要遵循架构本身的模式与惯用法。
5、有些 STL 库的 std::unary_function 的析构函数是公用且非虚拟的,需要在文档中说明派生的对象不能被多态地使用,更好的设计应该遵循本条款的建议,给它一个保护的非虚拟析构函数(STLport 就是如此)。

第51条 析构函数、释放和交换绝对不能失败
它们的一切尝试都必须成功:决不允许析构函数、资源释放(deallocation)函数(如 operator delete)或者交换函数报告错误。说得更具体一些,就是绝对不允许将那些析构函数可能会抛出异常的类型用于 C++ 标准库。
详细:
1、析构函数发生异常而退出,将调用 terminate。
2、释放函数,包括特殊一些的重载 operator delete 和 operator delete[] 都属于同一范畴。
3、对于异常而言,可以将一切敏感操作都包装在一个 try/catch(...) 块中即可。
4、当使用异常作为错误处理机制时,建议用一个注释掉的空异常规范 /*throw()*/ 来声明这些函数,通过这种方式说明这一行为(见 C75)。

第52条 一致地进行复制和销毁
既要创建,也要清除:如果定义了复制构造函数、复制赋值操作符或者析构函数中的任何一个,那么可能也需要定义另一个或者另外两个。
详细:
1、复制构造函数、复制赋值操作符和析构函数,这三个函数是不对称相关的。
2、要优先使用编译器所生成的特殊成员,只有它们能够归类为“普通的(trivial)”,STL 将对含有“普通的”特殊成员的类进行优化。
3、如果声明这三个特殊函数之一,只是为了将它们设为私有或者虚拟的,而没有什么特殊语义的话,那么就意味着不需要其余两个函数。

第53条 显式地启用或者禁止复制
清醒地进行复制:在下述三种行为之间谨慎选择——使用编译器生成的复制构造函数和赋值操作符;编写自己的版本;如果不应允许复制的话,显式地禁用前两者。
详细:
1、显式地禁止复制和赋值,通过私有定义且不提供实现显式禁止。
2、显式地编写复制和赋值。
3、使用编译器生成的版本,最好是加上一个明确的注释。

第54条 避免切片。在基类中考虑用克隆代替复制
切片面包很好;切片对象则不然:对象切片是自动的、不可见的,而且可能会使漂亮的多态设计嘎然而止。在基类中,如果客户需要进行多态(完整的、深度的)复制的话,那么请考虑禁止复制构造函数和复制赋值操作符,而改为提供虚拟的 Clone 成员函数。
详细:
1、避免下面的代码:
class B { /* ... */ };class D : public B { /* ... */ };void Transmogrify(B object);// 糟糕:通过值接受了一个对象void Transubstantiate(B &object)// OK:接受了一个引用{Transmogrify(object);// 糟糕:将对象切片了// ......}D d;Transubstantiate(d);
2、将 B 的复制构造函数设为 explicit 是一种可行的,但不可移植的方法。
3、使用 Clone 函数(并且采用 NVI 模式,见 C39)更好:
class B{public:std::shared_ptr<B> Clone() const{std::shared_ptr<B> object = DoClone();assert(typeid(*object) == typeid(*this) && "DoClone 不正确重写!");return object;}protected:B(const B &);private:virtual std::shared_ptr<B> DoClone() const = 0;};class D : public B{protected:D(const D &rhs) : B(rhs) { /* ... */ }private:virtual std::shared_ptr<B> DoClone() const{return std::shared_ptr<D>(new D(*this));}};

第55条 使用赋值的标准形式
赋值,你的任务:在实现 operator= 时,应该使用标准形式——具有特定签名的非虚拟形式。
详细:
1、应该为具有如下签名的类型 T 声明复制赋值:
T & operator=(const T &); // 传统的
T & operator=(T); // 可能更方便的优化器(见 C27)
2、要避免将赋值操作符设为虚拟的,如果确实需要虚拟行为,最好改成提供一个命名函数:
virtual void Assign(const T &);
3、不要返回 const T &,否则不能把 T 对象放入标准容器。
4、要始终保证复制赋值是错误安全的,最好是提供强有力的保证(见 C71)。
5、要确保赋值操作符对于自我赋值是安全的。
6、要显式调用所有基类赋值操作符,并为所有数据成员赋值。

第56条 只要可行,就提供不会失败的 swap (而且要正确地提供)
swap 既可无关痛痒,又能举足轻重:应该考虑提供一个 swap 函数,高效且绝对无误地交换两个对象。这样的函数便于实现许多惯用法,从流畅地将对象四处移动以轻易地实现赋值,到提供一个有保证的、能够提供强大防错调用代码的提交函数(见 C51)。
详细:
1、对于原始类型和标准容器,可以使用 std::swap 实现。
2、如果成员的复制构造和复制赋值都不会失败时,那么 std::swap 将在成员对象上正常工作。
3、如果成员的复制构造或复制赋值有可能会失败时,可以使用(智能)指针来代替直接成员,或者使用 Pimpl 惯用法。

返回 目录

返回《C++ 编程规范及惯用法》