zoukankan      html  css  js  c++  java
  • 构造析构与拷贝赋值那些事

    构造函数

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

    可能不是。

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

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

    有这个概念出发,我们可以知道,所有的构造函数都是在类的对象初始化时由系统调用的,具体调用哪个是按重载函数的调用规则来的。

    备注:构造函数不能被声明为const。可以想想为何?

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

    默认构造函数

    这个最简单,在面向对象的世界里,万物皆是对象,因为万物皆需要构造函数,如果我们没有定义一个构造函数,那么就由C++的编译器帮我们完成,在《c++ primer》里叫做合成的默认构造函数。

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

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

    #include <iostream>
    
    using namespace std;
    class ClassDesignTool
    {
    public:
        void printSp(){
    	cout << sp_ << "
    ";
    	}
    private:
    	string *sp_;
            
    };
    

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

    ClassDesignTool tool;
    tool.printSp();
    

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

    如果你不放心,可以把默认构造函数加上去,

    ClassDesignTool(){};
    

    测试的结果是一样的。

    这说明,如果你不准备在类的对象初始化时做点什么,完全可以把这件事交给编译器。反之,我们需要做点别的工作了。

    覆盖默认构造函数

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

    ClassDesignTool():sp_(new string("lcksfa")){
        cout << "use override default constructor " << "
    ";
    }
    
    //打印函数同时修改
    void printSp(){
        cout << "sp_ is " << sp_->c_str() << "
    ";
    }
    

    测试结果:

    use override default constructor
    sp_ is lcksfa
    

    现在,我们覆盖(override)了默认构造函数,合成的默认构造函数不会被调用,而调用我们自己的构造函数。

    构造函数重载

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

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

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

    以上,我们重载了一个构造函数,其参数为一个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() << "
    ";
    	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()<<"
    ";
        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"<<"
    ";
    
    	auto spNew = new string(*(rhs.sp_));
    	delete sp_;
    	sp_ = spNew;
    	return *this;
    } 
    

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

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

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

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

  • 相关阅读:
    js动画(三)
    js动画(二)
    css内容生成器
    css选择器基本属性
    css样式图片、渐变、相关小知识
    wed网页开发面试笔试必备小知识
    html5.边框属性相关知识点
    伪类选择符
    窗口尺寸小用法
    css3选择符使用个人理解。
  • 原文地址:https://www.cnblogs.com/Stultz-Lee/p/10054209.html
Copyright © 2011-2022 走看看