构造析构与拷贝赋值那些事

构造函数

关于构造函数,咱们耳熟能详,彷佛都没有必要成为一个知识点,或者说是重要的知识点拿出来特殊说明,毕竟C++的编译器都能帮咱们完成这个工做,只是,事情真的如想象的那么简单么;ios

可能不是。c++

本文试图挖掘关于构造函数,可能不是那么简单的一面,固然也不会很全面,权当一块儿学习了。ide

构造函数的概念:提供类的对象的初始化的方式,类经过一个或几个特殊的成员函数来控制对象的初始化过程。函数

有这个概念出发,咱们能够知道,全部的构造函数都是在类的对象初始化时由系统调用的,具体调用哪一个是按重载函数的调用规则来的。工具

备注:构造函数不能被声明为const。能够想一想为什么?学习

构造函数也不能是虚函数,这个应该好解释。测试

默认构造函数

这个最简单,在面向对象的世界里,万物皆是对象,由于万物皆须要构造函数,若是咱们没有定义一个构造函数,那么就由C++的编译器帮咱们完成,在《c++ primer》里叫作合成的默认构造函数。this

下面开始咱们的编码求学之旅:编码

首先,定义一个类设计者工具类:spa

#include <iostream>

using namespace std;
class ClassDesignTool
{
public:
    void printSp(){
    cout << sp_ << "\n";
    }
private:
    string *sp_;
        
};

在这样一个什么没有写构造函数的类里,默认构造函数依然会在编译阶段生成,测试代码以下:

ClassDesignTool tool;
tool.printSp();

在VS2010的编译环境下的结果是CCCCCCCC,看到这个你应该很熟悉,这是Windows环境下对全部未显式赋值变量的默认赋值,这也就能证实,Windows系统在编译后使用默认合成构造函数,将成员变量sp_赋值为CCCCCCCC了。

若是你不放心,能够把默认构造函数加上去,

ClassDesignTool(){};

测试的结果是同样的。

这说明,若是你不许备在类的对象初始化时作点什么,彻底能够把这件事交给编译器。反之,咱们须要作点别的工做了。

覆盖默认构造函数

可能,你认为默认的合成构造函数什么事也没作,对它心有怨恨,因此你决定出马把它改写(覆盖之)。

ClassDesignTool():sp_(new string("lcksfa")){
    cout << "use override default constructor " << "\n";
}

//打印函数同时修改
void printSp(){
    cout << "sp_ is " << sp_->c_str() << "\n";
}

测试结果:

use override default constructor
sp_ is lcksfa

如今,咱们覆盖(override)了默认构造函数,合成的默认构造函数不会被调用,而调用咱们本身的构造函数。

构造函数重载

函数重载(overload)的概念,我相信你们都不会陌生,对于构造函数,一样的也能将其重载。和调用普通的重载函数同样,系统会在初始化对象时,根据不一样的参数类型去调用不一样的重载构造函数:

在上面的代码里添加以下代码:

//overload constructor  
ClassDesignTool(const string& str)
    :sp_(new string(str)){
        std::cout << "use overload constructor " << "\n"; 
    }

以上,咱们重载了一个构造函数,其参数为一个const string&类型。

ClassDesignTool tool4(string("4"));
tool4.printSp();

测试结果以下:

use overload constructor
sp_ is 4

这说明,当咱们添加了构造函数的重载函数后,使用string("4")参数构造对象时,调用了咱们的string参数的构造函数。

拷贝构造函数

上面的东西都很简单,下面,咱们说下稍微复杂的。

从函数重载层面,拷贝构造函数也是构造函数的重载,只是其参数为本类的const引用,以下:

//copy constructor
ClassDesignTool(const ClassDesignTool&);
ClassDesignTool::ClassDesignTool(const ClassDesignTool& rhs)
{
    std::cout << "use copy constructor from " << rhs.sp_->c_str() << "\n";
    sp_ = new string(*(rhs.sp_));
}

何时调用?

ClassDesignTool tool("lcksfa");
ClassDesignTool tool2(tool);
tool2.printSp();

测试输出:

use overload constructor
use copy constructor from lcksfa
sp_ is lcksfa

以上代码说明,tool是使用的构造函数初始化,其参数为"lcksfa",而tool2是使用拷贝构造函数初始化,其参数为tool。

析构函数

说完构造函数,说下析构函数。咱们知道对象在建立时调用了构造函数,而在销毁时则会调用析构函数。

//destructor
~ClassDesignTool(){
    std::cout <<"use destructor "<<sp_->c_str()<<"\n";
    delete sp_;
}

以上是析构函数,事实上,我已经把默认的析构函数给覆盖了,缘由在于sp_的内存释放,若是使用合成的默认析构函数,系统将不会释放sp__的内存,从而致使内存泄漏。

和构造函数不一样,析构函数没有重载函数。这一点和人生很像啊。

执行方式

每个构造函数都是 由两部分组成的,一个是初始化部分,另外一个才是函数体,成员的初始化是在函数体执行以前完成的,因此你的代码里也须要作这两个部分的区分,不要把成员的初始化和函数体混为一体,由于,可能会影响析构函数的执行(只是,没有你想的那么严重)。由于一个析构函数,其也是由函数体和其析构部分组成的,析构时,先执行函数体,再执行销毁操做,成员按构造的初始化列表的逆序销毁。

由析构函数体引发的

若是你须要覆盖重写析构函数体,那么几乎能够确定你还须要拷贝构造函数和拷贝赋值运算符。

举例子,我在上面的程序中重写了析构函数,由于我须要显示释放sp_的内存,按上面的程序看,还可能出现什么问题呢?毕竟我没有拷贝赋值运算符函数。在测试函数中添加如下代码:

ClassDesignTool tool ;
{
    ClassDesignTool tool2("not me");

    tool2 = tool;
    // tool.printSp();
    tool2.printSp();
}

测试输出:

use override default constructor
use overload constructor
sp_ is lcksfa
use destructor lcksfa
use destructor
///奔溃了!!!

使用大括号{}将tool2的赋值部分封起来,确保tool2先析构。

程序输出后,到tool析构处就奔溃了!

缘由何在?

由于这里的系统默认的赋值运算是直接将sp_ 的值进行赋值,而没有去拷贝sp_ 指向的内存,tool2离开做用域时调用析构将sp_ delete掉了,等到tool离开做用域时,尝试delete的仍是同一块内存,因而就出现了double delete的问题!

赋值操做运算符

这种状况的解决方案之一就是咱们本身定义一个赋值操做运算符:

ClassDesignTool& 
ClassDesignTool::operator=(const ClassDesignTool& rhs)
{
    std::cout << "use copy-assignment operaotr"<<"\n";

    auto spNew = new string(*(rhs.sp_));
    delete sp_;
    sp_ = spNew;
    return *this;
}

本函数的写法颇为模式化:

  1. 将待拷贝的对象拷贝到新内存
  2. 释放sp_原来指向的内存
  3. 使用新拷贝的指针值给sp_赋值。
  4. 最后将 * this的引用返回(能够说凡是指望返回ClassDesignTool& ,最后都是返回 * this)

总结起来就是 综合了析构和构造函数的操做。销毁了左值运算对象的资源,而从右值运算对象中拷贝资源。

小结:本文初略的说明了构造函数、析构函数和拷贝赋值运算符的重载,能够做为入门者的参考。

相关文章
相关标签/搜索