构造、析构、赋值运算

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_18218335/article/details/84680685

构造、析构、赋值运算

了解C++ 默认编写并调用哪些函数

如果没有声明,编译器将为C++ 类声明(编译器版本的)构造函数、一个拷贝构造函数、拷贝赋值操作符和一个析构函数,它们都是public、inline

  • 默认行为

    • 拷贝构造函数、拷贝赋值操作符

      • 单纯的将来源对象的每个非静态成员变量拷贝到目标对象

      • 内置数据类型

        • 字节拷贝
      • 非内置数据类型

        • 存在拷贝构造函数则调用,否则就执行同样的,编译器生成的拷贝构造函数
      • 一般而言,只有当生出的代码合法且有适当机会证明它有意义,编译器才会为class 生出operator=

        • 如果打算在一个”内含reference 成员或const 成员“的class 内支持赋值操作,必须定义自己的拷贝赋值操作符
    • 构造函数、析构函数

      • 给编译器一个地方用来放置“藏身其后“的代码

      • 默认构造函数和析构函数

        • 调用基类和非静态成员变量的构造函数和析构函数
        • 编译器产出的析构函数是非虚函数,除非这个基类自身声明有虚析构函数

唯有这些函数被需要(被调用),它们才会被编译器创建出来

若不想使用编译器自动生成的函数,就应该明确拒绝

所有编译器产出的函数都是public,为了阻止这些函数被创建,需要自行声明它们

将不需要的函数,声明为private

  • 成员函数和友元函数,也会也可以调用这个私有函数
  • 直接不定义该函数,当引用的时候,会产生,找不到符号的链接错误
  • ios_base、basic_ios 它们的拷贝构造函数和拷贝赋值函数都被声明为私有,没有定义

这里提供了一个阻止copying 动作的基类,其拷贝构造函数和拷贝赋值函数为私有,且未实现,当子类继承,尝试进行拷贝构造操作,生成默认的拷贝构造函数,其访问父类的拷贝构造函数时,发现为私有,因此失败

为驳回编译器自动提供的机能,可将相应的成员函数声明为私有,且不予实现。使用像Uncopyable 的基类也是一种做法

为多态基类声明virtual 析构函数

核心:以析构子类特有的资源,而这是必然的需求

  • 否则,当子类没有定义其析构函数,或者,定义了,但,通过delete 父类指针,的方式,析构,不会调用,非虚的子类函数,导致,子类未析构完全

如果类不含虚函数,通常表示它并不意图被用做一个基类。当类不企图被当作基类,其析构函数为虚函数则是不对的

  • 一个是占用更大的空间(虚表指针的原因)
  • 其对象不能再和其它语言(如C)内的相同声明有同样的结构(虚表指针的原因,当然,可以通过定制C 接口实现交互,但不直观)

当类中至少含有一个虚函数,才为它声明虚析构函数

细节

  • 当希望类成为抽象类,但没有纯虚函数,可以将析构函数构建为纯虚析构函数

    • 但同时,需要为该析构函数提供一份定义,否则,其派生类的析构函数无法引用它
  • STL 或标准库或已经存在的,不包含虚函数的类,我们不应该继承它们

    • 比如STL 容器、std::string

总结

  • 带多态性质的(polymorphic)基类应该声明一个虚析构函数
  • 类的设计目的不是作为基类使用,或不是为了具备多态,就不应该声明虚析构函数

别让异常逃离析构函数

简单场景

  • 包含10 个对象指针的数组,析构到中间,产生异常,如果抛出,剩下的不析构,程序状态不稳定
  • 如果,已经产生了异常, 而栈展开时调用某个对象的析构函数,此时抛出异常, 程序状态不稳定

析构函数中产生错误怎么办

  • 产生异常就记录并退出进程

  • 记录该异常,程序继续执行

    • 即使记录了问题的产生,程序继续执行可能破坏更多的数据
  • 比较好的一种办法是,自己在析构函数中,可以选择,记录并退出,或者记录并继续执行。但同时提供一个public 的close 函数,让客户调用,给客户一个,捕获该异常,并选择自己需要的处理的方式

原则

  • 析构不要抛出异常,如果可能产生异常,应该捕捉,记录,然后继续执行,或结束进程
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class 应该提供一个普通函数执行该操作

绝不在构造函数和析构函数中调用虚函数

基类构造期间,虚函数不会下降到继承类的阶层, 对象的行为此时就像基类一样:基类构造期间,虚函数不是虚函数,是自己的

  • 此时即使,RTI 都认为它是基类

不仅要确保其自身没有调用虚函数,其所调用的所有下层函数都不能调用虚函数(下层函数调用虚函数时,编译器甚至连警告都不给了)

如何,保证,子类构建函数一定要提供某个特有属性

  • 书中的例子,要求子类给父类构造函数传递自己的,string 参数,以,输出特定的信息
  • 令继承类将必要的构造信息向上传递到基类构造函数,以此弥补这种多态的不足

构造函数和析构期间,不要调用虚函数,这类调用从不下降到派生类(比起当前执行构造函数和析构函数的那层)

令operator= 返回一个reference to *this

是一个简单的,协议,这份协议被所有内置类型、标准程序库提供的类型:string、vector、complex、等遵守

在operator= 中处理”自我赋值“

if(this == & rhs) return *this;

  • 需要判断操作

  • 影响程序执行逻辑

    • 预加载,缓存,pipelining?

T* pOrig = pb;
pb = new T(rhs.pb);
delete pOrig;
return *this;
  • 这种做法,不用判断,但对某些对象来说,可能,这里的T 的构造比判断的开销更大
  • 不够高效

swap 函数

  • copy and swap

  • 就是,拷贝构造函数,然后,交换数据

    • 参数引用,内部显式的构造函数
    • 参数直接值传递,内部,省去构造函数调用,直接swap

原则

  • 确保当对象自我赋值时operator= 有良好的行为。技术包括:比较来源对象和目标对象的地址、精心设计的语句顺序、copy-and-swap
  • 确定任何函数如果操作到一个以上的对象,其中多个对象是同一个对象时,其行为仍然正确

复制对象时,勿忘其每一个成分

当提供自己的拷贝构造函数、拷贝赋值函数时尤其注意

新添加成员变量时,计得向函数中添加它

继承关系中的复制

  • 拷贝构造函数

    • 初始值表中添加:父类(参数),比如:
Son::Son(para1,para2):Far(para1),m_m1(para2){}
  • 拷贝赋值函数
Son& Son::operator=(const Son& rhs){
Far::operator=(rhs);}
  • 拷贝构造函数和拷贝赋值函数不可互相调用,概念不同

    • 可以提供一个共有的成员函数,提供共有的拷贝操作

原则

  • Copying 函数应该确保复制”对象内的所有成员变量“及“所有基类成分”
  • 不要尝试以某个copying 函数实现另一个copying 函数。应该将共同机能放进第三个函数中,并由两个copying 函数共同调用

猜你喜欢

转载自blog.csdn.net/qq_18218335/article/details/84680685