• EC笔记:第二部分:12、复制对象时勿忘其每一个成分


    EC笔记:第二部分:12、复制对象时勿忘其每一个成分

     

    1.场景

    某些时候,我们不想使用编译器提供的默认拷贝函数(包括拷贝构造函数和赋值运算符),考虑以下类定义:

     

    代码1:

    class Point{

        private:

            double x;

            double y;

        public:

            Point()=default;

            Point(const Point &other){

                x=other.x;

                y=other.y;

            }

            const Point& operator=(const Point &other){

                x=other.x;

                y=other.y;

                return *this;

            }

    };

     

    Point类重写了拷贝构造函数和赋值运算符,这并没有什么问题,但是,假设现在要新增一个成员:

    代码2:

    class Point{

        private:

            double x;

            double y;

            double r; //新增的成员

        public:

            Point()=default;

            Point(const Point &other){

                x=other.x;

                y=other.y;

            }

            const Point& operator=(const Point &other){

                x=other.x;

                y=other.y;

                return *this;

            }

    };

     

    而且我们的程序员很不巧地忘了更新拷贝构造函数和赋值运算符,那么在复制的时候,新增的1成员r就保持原值,新得到的对象与原对象不相等,与要赋值的对象也不相等。

     

    考虑另外一种场景:

    代码3:

    class Point{

        private:

            double x;

            double y;

    };

     

    class MyPoint:public Point{

        private:

            double r;

        public:

            Point()=default;

            MyPoint(const MyPoint& other){

                r=other.r;

            }

            const MyPoint& operator=(const MyPoint& other){

                r=other.r;

                return *this;

            }

    };

     

    当MyPoint继承Point类的时候,重写了拷贝构造函数和赋值运算符,但是忘了赋值基类中的成员,也会导致和前面一样的后果。

    2.解决办法

    为了解决上面的问题,首先,确保拷贝构造函数和赋值运算符确实将所有的成员都进行了拷贝(除非你是真的想保留一些成员)。例如,代码2中增加了成员r,那么拷贝构造函数和赋值运算符就应该对应更新:

    代码4:

    class Point{

        private:

            double x;

            double y;

            double r; //新增的成员

        public:

            Point()=default;

            Point(const Point &other){

                x=other.x;

                y=other.y;

                r=other.r;//新增

            }

            const Point& operator=(const Point &other){

                x=other.x;

                y=other.y;

                r=other.r;//新增

                return *this;

            }

    };

     

    其次,子类中重写了赋值运算符或者拷贝构造函数,一定要将父类中隐藏的成员一起复制,通常是调用父类的赋值运算符或者拷贝构造函数。代码3中可以修改为以下代码:

    代码5:

    class Point{

        public:

            Point()=default;

        private:

            double x;

            double y;

    };

     

    class MyPoint:public Point{

        private:

            double r;

        public:

            MyPoint()=default;

            MyPoint(const MyPoint& other):Point(other){

                r=other.r;

            }

            const MyPoint& operator=(const MyPoint& other){

                Point::operator=(other);

                r=other.r;

                return *this;

            }

    };

     

    有些童鞋可能注意到,大多数拷贝构造函数与赋值运算符的操作类似,那么,是否可以在拷贝构造函数中调用赋值运算符呢?类似下面的代码:

     

    代码6:

    class Point{

        public:

            Point()=default;

        private:

            double x;

            double y;

    };

     

    class MyPoint:public Point{

        private:

            double r;

        public:

            MyPoint()=default;

            MyPoint(const MyPoint& other){

                operator=(other);

            }

            const MyPoint& operator=(const MyPoint& other){

                Point::operator=(other);

                r=other.r;

                return *this;

            }

    };

    对此,《Effective C++》一书给出的解释是:

        这段解释比较不是那么易懂,总体说来,就是在执行operator=的时候,拷贝构造函数其实已经执行结束,赋值运算符只作用于已经构造好的对象上,所以此时再使用并无意义(本质上相当于调用了默认构造函数+赋值运算符)。

        反过来的话(在赋值运算符中调用拷贝构造函数),由于根本没有类似的语法,所以不做讨论。事实上,构造函数主要目的是"构造",但是调用赋值运算符的时候,对象肯定已经构造完毕,所以,不会有这样的语法支持。

     

        对于拷贝构造函数与赋值运算符有类似代码的场景,《Effective C++》给出的建议是,将公共代码提到一个单独的函数中,各自调用,以上代码可修改为:

    代码7:

    class Point{

        public:

            Point()=default;

        private:

            double x;

            double y;

    };

     

    class MyPoint:public Point{

        private:

            double r;

            void init(const MyPoint& other){

                r=other.r;

            }

        public:

            MyPoint()=default;

            MyPoint(const MyPoint& other):Point(other){

                init(other);

            }

            const MyPoint& operator=(const MyPoint& other){

                Point::operator=(other);

                init(other);

                return *this;

            }

    };

     

        但是对于以上的修改方法,如果是对于这个类,我仍觉得有瑕疵。虽然拷贝构造函数中直接调用了父类的拷贝构造函数,但是在调用init函数的时候,成员r其实已经被构造,整个对象此时已经构造完毕,再对r进行复制,依然会产生额外的开销(当然,这个开销比拷贝构造函数直接调用赋值运算符小)。

        所以我建议的修改方式为直接调用每个成员的拷贝构造函数,代码如下:

     

    代码8:

    class Point{

        public:

            Point()=default;

        private:

            double x;

            double y;

    };

     

    class MyPoint:public Point{

        private:

            double r;

        public:

            MyPoint()=default;

            MyPoint(const MyPoint& other):Point(other),r(other.r){

            }

            const MyPoint& operator=(const MyPoint& other){

                Point::operator=(other);

                r=other.r;

                return *this;

            }

    };

     

    此时Point和r都调用了各自的拷贝构造函数完成构造,比代码7的效率要好一些。

    当然,这个修改只是针对这个类,《Effective C++》中指的是拷贝构造函数与赋值运算符中有相同代码部分,此处的修改已经不属于"相同代码"了,因为r=other.r已经被优化为了r(other.r),相当于拷贝构造函数与赋值运算符中已经没有相同代码了。

     

    针对代码7代码8的分析,可以知道,只有当r没有提供拷贝构造函数的时候,代码7才不能优化为代码8,通常有两种情况:

    1. r是一个类对象,而且此对象中有些成员未提供拷贝构造函数
    2. r未提供拷贝构造函数

    其实总的就是一句话:r无法通过拷贝构造函数构造,这时才需要用自定义代码完成r的构造。所以为了避免效率上的损失,尽量每个成员都提供拷贝构造函数(除非确实不希望成员被复制,当然,此时对象也不应该有拷贝构造函数)。

    3.建议

    1. 确保每个成员都被复制
    2. 将不同copy函数中相同部分提取出来,重新封装一个函数,各自调用,两个copy函数不应该存在调用关系。
  • 相关阅读:
    URL提交之前对数据编码
    软件工程概论第三章概括
    软件工程概论第七章概括
    软件工程概论第四章概括
    软件工程概论第五章概括
    软件工程概论第一章概括
    《人月神话》观后感
    软件工程概论第六章概括
    软件工程概论第二章概括
    MySQL语句
  • 原文地址:https://www.cnblogs.com/SkyFireITDIY/p/6206166.html
Copyright © 2020-2023  润新知