金三银四C++面试考点之复制构造函数

151 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情

一、复制构造函数

1. 定义

复制构造函数又称为拷贝构造函数,它是一种特殊的构造函数。它的作用就是用一个已经生成的对象 来初始化 另一个同类的对象

以前我们有

  • 变量的初始化:
    int a = 10; 
    int b = a; 
    

那么可不可以用一个对象初始化另一个对象呢?下面的这个表达式会调用构造函数吗?还是其他?

  • 对象的初始化:
    Point pt1(10,20); 
    Point pt2 = pt1;
    

我们带着问题一起往下看。

2. 一般形式

类名(const 类名& obj)//同类的引用类型的类
{
    函数体
}

3. 实战

下面是一个点类,里面有个默认构造函数。

class Point 
{ //Point类 
public: 
    Point() : x(0), y(0) { } //默认构造函数
private: 
    int x,y; 
};

我们尝试自己写一个复制构造函数,看看和下面的一不一样?

Point(const Point& r) : x(r.x), y(r.y) { } //复制构造函数 

前面不要写成下面参数构造函数,仔细的区分一下吧!

Point(int a,int b) : x(a), y(b) { } //带参数构造函数

4. 注意

  • 复制构造函数有且只有一个本类类型对象的引用形参,通常使用const限定。因为复制构造函数只是复制对象,不改变传递来的对象的值。
  • 复制构造函数的功能是利用一个已知的对象来初始化一个被创建的同类的对象。
  • 与复制构造函数对应的对象的定义形式为:
    类名 对象名1(类对象1), 对象名2(类对象2),...
    
    也就是说如何用复制构造函数创建一个对象呢?按照上面的定义形式我们可以写出:
    Point pt1(10,20); 
    Point pt2(pt1);//用已经生成的对象pt1初始化另一个同类的对象pt2
    

5. 对象赋值和对象复制的区别

  • 对象的赋值

    是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。

     Point pt1(10,20); 
     Point pt2(20,20);
     pt1 = pt2;
    

    上面的代码就是对一个已经存在的对象 pt1 赋值,赋值成 pt2。这里应该调用拷贝赋值。因此我们还应该在原类中增加拷贝赋值的操作,如下所示。

    Point& operator=(const Point& other) 
    { 
        std::cout << "Copy assignment operator called" << std::endl; 
        if (this != &other) //避免自我赋值
        { 
            x = other.x; 
            y = other.y;
        } 
        return *this; 
    }
    
  • 对象的复制

    则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。下面的两种情况都是对象的复制。都是从无到有地建立一个新对象。

    Point pt1(10,20); 
    Point pt2 = pt1;//用已经生成的对象pt1初始化另一个同类的对象pt2
    

    在创建pt2的时候就对其赋值,将pt1中的数据复制到pt2中。是用一个已经生成的对象 pt1 来初始化 另一个同类的对象 pt2。也会调用复制构造函数。

    Point pt1(10,20); 
    Point pt2(pt1);//用已经生成的对象pt1初始化另一个同类的对象pt2
    

    代码Point pt2(pt1) 也是原来不存在对象pt2,创建这行代码的同时,新对象pt2创建的时刻就有了值,和pt1完全一样,就用已经生成的对象pt1初始化类一个同类的对象pt2,也就是调用了复制构造函数创建了pt2。 这里也算是总结了两种调用拷贝构造函数的情况。

二、合成复制构造函数

1. 定义

每个类必须有一个复制构造函数。如果类没有定义复制构造函数, 编译器就会自动合成一个,称为合成复制构造函数(synthesized copy constructor)。

2. 注意

  • 与合成默认构造函数不同,即使定义了其他构造函数,编译器也会合成复制构造函数。
  • 合成复制构造函数的操作是:执行逐个成员初始化,将新对象初始化为原对象的副本。
    • 所谓“逐个成员” ,指的是编译器将现对象的每个非静态数据成员, 依次复制到正创建的对象中。每个成员的类型决定了复制该成员的含义:
      • 内置类型成员直接复制其值;

        class Test {
        public:
            Test() {
                num_ = 10;
            }
        private:
            int num_;
        };
        
        int main() {
            Test t1;
            Test t2 = t1;
            return 0;
        }
        

        在这个例子中,t1 中的 num_ 成员是内置类型 int,因此 t2 的合成复制构造函数将直接复制 t1 的 num_ 成员值给 t2。 这里补充说明一下C++内置类型:

        • 整数类型:bool、char、short、int、long、long long等
        • 浮点类型:float、double、long double等
        • 空类型:void
        • 指针类型:任何类型的指针
        • 枚举类型:enum

        注意,C++标准库中的类型string、vector等不属于内置类型。

      • 类类型成员使用该类的复制构造函数进行复制;

        class Inner {
        public:
            Inner() {
                num_ = 5;
            }
        private:
            int num_;
        };
        
        class Test {
        public:
            Test() {
                inner_ = new Inner();
            }
            ~Test() {
                delete inner_;
            }
        private:
            Inner* inner_;
        };
        
        int main() {
            Test t1;
            Test t2 = t1;
            return 0;
        }
        

        在这个例子中,t1 中的 inner_ 成员是类类型 Inner 的指针,因此 t2 的复制将使用其类类型成员 Inner 这个类的复制构造函数进行复制。

      • 如果一个类具有数组成员,则合成复制构造函数将复制数组,即 复制数组的每一个元素到新对象中。

        class Test {
        public:
            Test() {
                for (int i = 0; i < 5; i++) {
                    arr_[i] = i;
                }
            }
        private:
            int arr_[5];
        };
        
        int main() {
            Test t1;
            Test t2 = t1;
            return 0;
        }
        

        在这个例子中,t1 中 的 arr_ 成员是一个由 5 个 int 类型元素组成的数组,因此 t2 的合成复制构造函数将复制 t1 的 arr_ 成员数组。也就是将复制 t1 的数组的每一个元素(0、1、2、3、4)到新对象中。

    • 逐个成员初始化可以这样理解:将合成复制构造函数看作是每个数据成员在构造函数初始化列表中进行初始化的构造函数。 上面的三种类型的成员可以同时出现在一个类中,合成复制构造函数会对每个成员一次初始化,也就是类似于构造函数初始化列表中进行初始化的构造函数对num_、inner_、arr_[5]逐个初始化,每个成员初始化又符合上一条的规则。
      class Test 
      {
      private:
          int num_;
          Inner* inner_;
          int arr_[5];
      };
      

三、以下3种情况会使用复制构造函数

1. 用一个对象显式或隐式初始化另一个对象。

C++支持两种初始化形式:复制初始化和直接初始化。

(1)复制初始化

使用等号(=),而直接初始化将初始化式放在圆括号中。 ►复制初始化和直接初始化是有区别的:

(2)直接初始化

会调用与实参匹配的构造函数;而复制初始化总是调用复制构造函数。

Point pt1(10,20); 
Point pt2=pt1; //复制初始化 
Point pt3(pt1); //直接初始化

2. 函数参数按值传递对象时或函数返回对象时。

当函数形参为对象类型,而非指针和引用类型时,函数调用按值传递对象,即编译器调用复制构造函数产生一个实参对象副本传递到函数中。

类似地,以对象类型作为返回值时,编译器调用复制构造函数产生一个return语句中的值的副本返回到调用函数。

void printPoint(Point p) 
{ 
    std::cout << "Point(" << p.x() << ", " << p.y() << ")" << std::endl; 
} 

Point createPoint() 
{ 
    Point p(1.0, 2.0); 
    return p; 
}
int main() 
{ 
    // 调用复制构造函数 
    Point p1(3.0, 4.0); 
    Point p2 = p1; 
    
    // 调用复制构造函数 
    Point p3 = createPoint(); 
    printPoint(p3);
}

printPoint 函数按值传递了一个 Point 对象,这会调用复制构造函数。createPoint 函数返回一个 Point 对象,这也会调用复制构造函数。

注: 用到的函数x()y()可以在类的公有成员中增加

double x() const { return x_; } 
double y() const { return y_; }

3. 根据元素初始化式列表初始化数组元素时。

如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的大括号的数组初值列表形式来初始化数组时,则使用复制初始化来初始化每个元素。

总的来说,正是有了复制构造函数,函数才可以传递对象和返回对象,对象数组才能用初值列表的形式初始化。

int main() 
{ 
    // 初始化数组元素
    Point arr[3] = { Point(1.0, 2.0), Point(3.0, 4.0), Point(5.0, 6.0) }; 
    for (int i = 0; i < 3; i++) 
    { 
        std::cout << "arr[" << i << "] = (" << arr[i].x() << ", " << arr[i].y() << ")" << std::endl; 
    }
}

在初始化 arr 数组元素时,使用了元素初始化式列表,这会调用 Point 类的构造函数来创建数组元素。