• C++可以这么学----->类和对象(下)


    本文主要内容:

    • 拷贝构造函数
    • 赋值运算符重载

    一.拷贝构造函数(很重要)
    在有的场景下,我们需要用一个已经存在的对象来构造一个新对象。换言之,我们需要构造一个和已有对象一模一样的对象。那么这里就是我们要介绍的拷贝构造函数。

    1.拷贝构造函数也是构造函数,函数名和类名相同,没有返回值,函数的参数是你想要拷贝的对象。
    2.如果没有显式提供拷贝构造函数,那么编译器会自动生成一个拷贝构造函数。
    3.拷贝构造函数的参数必须是引用类型!

    首先我们来看一看为什么拷贝构造函数的参数必须是引用类型,如果不是会发生什么

    class Date
    {
      public:
        Date(int year=1900,int month=1,int day=1)
        { 
              _year=year;
              _month=month;
              _day=day;
        }
        //只有内置类型成员,不需要提供析构
        ~Date()
        {}
        //拷贝构造函数
        
        Date(const Date& d)
        {
            
        }
      private:
         int _year;
         int _month;
         int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    拷贝构造函数传值传参可能会引发无穷拷贝:
    无穷拷贝
    所以,为了解决无穷拷贝的问题,所以拷贝构造函数的参数类型必须是引用类型!
    而如果我们没有涉及到对对象的属性的内容进行修改的话,最好在前面加一个const(如果不加const,可能会有意想不到的编译失败的问题存在)

    //正确的拷贝构造函数
    Date(const Date& d)
    {
       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么和前面的默认构造函数一样,如果我们没有显式提供,那么编译器会默认生成一个拷贝构造函数。和默认构造函数不同的地方是,对于内置类型,拷贝构造函数会进行值的拷贝(可以理解成把一个一个字节的内容拷贝过来)也叫做浅拷贝。对于只有内置类型成员的类型,进行浅拷贝就已经足够了。而对于像栈这种直接管理堆区内存的类就需要进行深拷贝。而关于深浅拷贝的具体内容后续会详细介绍,这里稍微了解一下就可以了。
    所以这里的Date类我们就可以直接使用编译器生成的拷贝构造函数就可以了,显示写出拷贝构造函数就要这样写

    //使用引用返回可以减少拷贝
    Date& (const Date& d)
    {    //防止自己给自己拷贝
         if(this !=&d)
         {
            _year=d._year;
            _month=d._month;
            _day=d._day;
         }
         return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.赋值运算符重载
    那么接下来我们就来看赋值运算符的重载。首先先来谈谈运算符重载。我们知道C++里面有很多运算符,那么对于内置类型来说,运算符的行为已经被定义好不能变了。而对于自定义类型成员来说,运算符的行为是不确定的。为了能够让自定义类型成员也能用上运算符,C++提供了运算符重载的机制

    //运算符重载--->operator关键字
    //重载+号运算符
    Date operator+(int day)
    { 
       //do something 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    而运算符重载本质就是一个函数,所以在使用+号的是后,编译器会自动做这一步转换

    //编译器做的等价转换
    d1+d2;//等价于d1.operator+(d2);
    
    • 1
    • 2

    了解了运算符重载,我们接下来再来看看赋值运算符的重载。显然易见,赋值运算符的重载完成的工作是完成对一个已存在的对象进行改变,并不是只要使用=就是赋值

    //易混淆的拷贝构造和赋值运算符重载
    class Date
    {
      public:
        Date(int year=1900,int month=1,int day=1)
        { 
              _year=year;
              _month=month;
              _day=day;
        }
        //只有内置类型成员,不需要提供析构
        ~Date()
        {}
        //拷贝构造函数
        
        Date(const Date& d)
        {
            //防止自己给自己拷贝
         
         if(this !=&d)
         {
            _year=d._year;
            _month=d._month;
            _day=d._day;
         }
         return *this;
        }
      private:
         int _year;
         int _month;
         int _day;
    };
    int main()
    {  
       Date d1(2022,6,26);
       //这里是调用拷贝构造函数,虽然是=号,因为是创建d2
       Date d2=d1;
       Date d3;
       //这里才是赋值运算符重载,这里d3已经存在,所以是赋值
       d3=d2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    和默认构造函数一样,如果没有显示提供的话,编译器会自动生成一个,而这个自动生成的赋值运算符重载函数也是完成浅拷贝。
    那么在现在的编译器来看,对于拷贝构造的优化十分厉害,来看这样一段代码,看一看发生了几次拷贝构造:

    class Widget
    {
    public:
    //默认构造函数
        Widget(int a = 0)
        {
            _a = a;
        }
        //拷贝构造函数
        Widget(const Widget& w)
        {
            _a = w._a;
        }
    private:
        int _a;
    };
    Widget f(Widget u)
    {
        Widget v(u);
        Widget w = v;
        return w;
    }
    int main()
    {    
        Widget x;
        Widget y = f(f(x));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    理论上应该拷贝的次数
    然而,现实的情况却是:编译器会对整个代码进行优化,也就是它会把连续的拷贝构造合并成一步,在上面的过程里面,图中的4和5,还有8和9就会被合并,所以最终我们看到只有7次的拷贝构造:

    //验证拷贝构造只有7次
    class Widget
    {
    public:
        Widget(int a = 0)
        {
            _a = a;
            cout << "Widget(int a=0)" << endl;
        }
        Widget(const Widget& w)
        {
            _a = w._a;
            cout << "Widget(const Widget& w)" << endl;
        }
    private:
        int _a;
    };
    Widget f(Widget u)
    {
        Widget v(u);
        Widget w = v;
        return w;
    }
    int main()
    {    
        Widget x;
        Widget y = f(f(x));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    拷贝构造优化


    补充:
    关于拷贝构造和赋值运算符的const参数问题:
    前面我们提到,如果不涉及对于对象属性的更改,那么我们最好使用const来修饰对象。有的时候不用const修饰形参可能会出现不可预测的编译错误!而具体可能出现的原因我们会在下一篇博客里面讨论。


    总结:

    • 拷贝构造函数参数必须是引用类型,否则会引发无穷拷贝
    • 拷贝构造函数对于内置类型完成值拷贝工作。
    • 不显示提供拷贝构造,编译器会默认生成一个,完成浅拷贝
    • 赋值运算符是默认成员函数,不显式提供编译器会默认生成一个。
      本文的主要内容就到这里结束了。如果有不足的地方,还希望能够指出。希望大家一起共同进步。
  • 相关阅读:
    Vue使用脚手架出现问题 2
    Vue动态绑定class
    记一次manjaro-i3系统sogoupinying候选词无法正常显示中文(变方框了)问题解决方案
    Oracle中国C/C++软件工程师代码编写规范2017公开版
    springboot毕设项目车位预定管理系统76ov7(java+VUE+Mybatis+Maven+Mysql)
    ES的基础概念
    Spring Security OAuth2的基本使用
    npm install 报错ERESOLVE unable to resolve dependency tree
    CSRF漏洞
    RabbitMQ【直连、主题、扇形交换机实战】
  • 原文地址:https://blog.csdn.net/qq_56628506/article/details/125466495