• C++雾中风景6:拷贝构造函数与赋值函数


    在进行C++类编写的过程之中,通常会涉及到类的拷贝构造函数与类的赋值函数。初涉类编写的代码,对于两类函数的用法一直是挺让人困惑的内容。这篇文章我们会详细来梳理拷贝构造函数与赋值函数的区别。

    1.调用了哪个函数?

    上述两种函数的使用和C++之中类的定义紧密相关,所以我们先定义一个类:

    class Line {
    public:
        int getLength( void );
        Line( int len );             //简单的构造函数
    
        Line( const Line &obj)  {   //拷贝构造函数
            cout << "调用拷贝构造函数" << endl;
            ptr = new int;
            *ptr = *obj.ptr;
        };
    
        Line& operator=(const Line &obj) { //赋值函数
            cout << "调用赋值函数" << endl;
            if(this == &obj) {
                return *this;
            }
    
            delete ptr;
            ptr = new int;
            *ptr = *obj.ptr;
    
            return *this;
    
        };
        ~Line();                     // 析构函数
    
    private:
        int *ptr;
    };
    

    这里我们显式声明了拷贝构造函数与赋值构造函数,接下来我们用一小段代码测试一下上面定义的类。(其他函数的定义并不完整,读者可以之行补全)

    int main() {
        Line l1(10);
        Line l2 = l1;
    
        Line l3(5);
        l3 = l2;
    }
    

    输出结果如下:

    调用拷贝构造函数
    调用赋值函数
    

    看似很相似的两个表达式,却调用了不同的两个函数。初学C++时,这样的结果让我很困惑,所以我们接下来梳理一下这两个函数。

    2.拷贝构造函数

    上面的代码我们可以看到代码 Line l2 = l1调用了拷贝构造函数。
    拷贝构造函数,顾名思义,是一个构造函数,但是它特殊的点就在于在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。所以对于它的使用场合也很简单,只有在构造对象时才会调用到拷贝构造函数,显然Line l2 = l1是一个对象初始化的过程。我们知道每个类都会有构造函数,在对象初始化的过程之中,拷贝构造函数提供了一个通过一个同类型的对象对它进行初始化。

    C++支持两种初始化形式:拷贝初始化(int a = 5;)和直接初始化(int a(5);)对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数,也就是说:

    A x(2);    //直接初始化,调用构造函数
    A y = x;  //复制初始化,调用拷贝构造函数
    

    在C++中,下面几个场景中,拷贝构造函数会被调用:

    • 一个对象需要通过另一个对象进行初始化
    • 一个对象以值传递的方式作为参数传入函数
    • 一个对象以值传递的方式作为返回值从函数返回

    如果我们没有显式声明定义对应类的拷贝构造函数,C++编译器会默认生成对应的拷贝构造函数。既然C++编译器会自动生成拷贝构造函数,为什么我们又需要显式的去定义它呢?
    因为由C++编译器提供的拷贝构造函数工作方式是浅拷贝。它单纯的使用了=操作符来拷贝类中的成员。但是如果类中用到了需要动态分配内存的成员(如Line类之中的ptr指针),则会出现内存安全的问题。同时对于类的封装性也是一种变相破坏,因为浅拷贝只是单纯拷贝了该成员的内存地址,但所指向的空间内容并没有复制,而是由两个对象共用,就容易出现double free等问题。所以此时就要手动重载拷贝构造函数,实现深拷贝

    3.赋值函数

    许多文章,博客喜欢把Line& operator=(const Line &obj)重载=操作符的函数称之为赋值构造函数。个人认为其实是不准确的,会产生一个理解误区。其实重载的=操作符就是一个赋值函数。

    赋值函数:是把一个新的对象赋值给一个原有的对象,如果原来的对象中有内存分配需要先把内存释放掉。如果我们没有在类之中显式重载对应类的赋值函数,C++编译器也会默认生成对应的赋值函数。生成的规则与拷贝构造函数类似,也是一种浅拷贝的形式。所以我们重载赋值函数的原因也与拷贝构造函数类型,需要实现深度赋值。
    由上文的代码也可以看出,赋值函数与拷贝构造函数定义的内容之中,所做的工作大同小异。唯一需要注意的点是:在赋值函数之中需要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。

    x = x   //可能会出现赋值给自身的操作,所以需要对赋值对象进行检查
    

    所以现在回头再来看看前文的代码,我们应该可以理解C++编译器调用不同函数的理由了:

    int main() {
        Line l1(10);
        Line l2 = l1;
    
        Line l3(5);
        l3 = l2;
    }
    

    当对象不存在,且用别的对象来初始化,此时调用的便是拷贝构造函数。而当对象已经存在,用别的对象来给它进行赋值操作时,调用的就是赋值函数了。

    最后的小Tips:一旦在类之中声明了拷贝构造函数与赋值函数,编译器将不会生成缺省的对应函数。所以我们可以通过将拷贝构造函数和赋值函数声明为私有函数,来阻止编译器生成的缺省函数,在编译阶段来拒绝不安全的浅拷贝。

    class Line {
    public:
        int getLength( void ) {
            return *ptr;
        };
        Line( int len ) {
            ptr = new int;
            *ptr = len;
        }             //简单的构造函数
    //    ~Line();                     // 析构函数
    
    private:
        int *ptr;
        Line( const Line &obj);
        Line& operator=(const Line &obj);
    };
    
    int main() {
        Line l1(10);
        Line l2 = l1;  //编译器报错,无法通过编译
    
        Line l3(5);
        l3 = l2; //同样的的编译器报错,无法通过编译
    }
    

    好的,关于拷贝构造函数与赋值函数,就先写到这里。下一篇雾中风景系列,来聊一聊命名空间吧~~

  • 相关阅读:
    聊聊Flame Graph(火焰图)的那些事
    Dynamometer:HDFS性能扩展测试工具
    论分布式系统中单一锁控制的优化
    聊聊磁盘数据的损坏
    分级副本存储:一种更具效益成本的数据容错策略
    分布式存储系统中的Data Scrubbing机理
    论一个成熟分布式系统的工具类设计
    聊聊Raft一致性协议以及Apache Ratis
    ListenableFuture的状态同步和原子更新
    2018-9-1-win10-uwp-轻量级-MVVM-框架入门-2.1.5.3199
  • 原文地址:https://www.cnblogs.com/happenlee/p/8302334.html
Copyright © 2020-2023  润新知