C++ 拷贝构造函数

在实际开发中,我们经常会遇到这样的需求:程序里已经有一个创建好的对象,现在想以它为模板,再复制出一个数据相同的新对象。比如已经定义好了一个 Student 类的对象 stu1,接着写下下面这行代码:

Student stu2 = stu1;

很多初学的小伙伴第一次看到 Student stu2 = stu1; 时,容易因为中间有一个等号(=),就以为这是普通的赋值操作。

其实并不是这样。在这行代码之前,对象 stu2 还不存在,这里是在创建新对象 stu2 并使用已有的 stu1 初始化它。这种 “使用同类型的已有对象创建并初始化新对象” 的过程,调用的就是 “拷贝构造函数(Copy Constructor)”。

C++ 拷贝构造函数的基本语法

在 C++ 中,拷贝构造函数也是一种特殊的构造函数。因此它的函数名必须与类名相同,也没有返回值类型,只是形参固定写成 const 类名&。

语法:

class 类名 {
public:
    类名(const 类名& other) : 成员(other.成员) {
        // 函数体
    }
};

说明:

编写拷贝构造函数时,我们通常会配合前面学过的初始化列表,直接拿来源对象 other 的各个成员去初始化新对象的成员。这里的形参名 other 只是习惯叫法,表示 “另一个已有对象”,你也可以取其他合法的变量名。

对于形参 const 类名& other 来说,有以下 2 个细节需要特别留意:

  • 引用符号(&)不能省略:形参必须声明为引用。因为在 C++ 中,把对象按值传给形参时,本身就要调用拷贝构造函数来复制实参。如果拷贝构造函数自己的形参也写成值传递,调用时为了传参又得先调用它自己,就会陷入无限循环,编译器会直接报错。
  • 推荐加上 const 修饰:加上 const 既能防止在复制过程中不小心修改来源对象的数据,也能保证 const 常对象和临时对象都可以正常完成复制。

比如对于 Student 类,声明拷贝构造函数时一定要带上引用符号(&):

// 正确方式
Student(const Student& other);

// 错误方式
Student(const Student other);

定义好拷贝构造函数后,使用已有对象 stu1 创建新对象时,既可以用括号写成 Student stu2(stu1);,也可以用等号写成 Student stu3 = stu1;。只要左边的对象是在当前行才声明创建的,编译器都会自动调用拷贝构造函数。下面我们来看一个例子。

示例 1:定义与调用拷贝构造函数

#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int score;

public:
    // 带参构造函数
    Student(const std::string& n, int s)
        : name(n), score(s) {}

    // 拷贝构造函数
    Student(const Student& other)
        : name(other.name), score(other.score) {
        std::cout << "调用拷贝构造函数\n";
    }

    void setScore(int s) {
        score = s;
    }

    void show() const {
        std::cout << name << ",成绩:" << score << "\n";
    }
};

int main() {
    Student stu1("阿莫", 95);
    Student stu2(stu1);     // 括号法调用拷贝构造
    Student stu3 = stu1;    // 等号法调用拷贝构造

    stu2.setScore(100);     // 修改 stu2 的成绩

    stu1.show();
    stu2.show();
    stu3.show();
    return 0;
}

运行结果如下。

调用拷贝构造函数
调用拷贝构造函数
阿莫,成绩:95
阿莫,成绩:100
阿莫,成绩:95

分析:

在这个例子中,创建 stu1 时传入了姓名和成绩,匹配的是带参构造函数。随后执行 Student stu2(stu1); 和 Student stu3 = stu1; 时,由于 stu2 和 stu3 都是用同类型对象初始化的新对象,因此两次都自动调用了拷贝构造函数。

可能有些小伙伴会觉得奇怪:name 和 score 明明是 private 私有成员,为什么在拷贝构造函数里能直接访问 other.name 和 other.score 呢?

其实原因很简单:C++ 的 private 访问限制是以 “类” 为单位,而不是以 “单个对象” 为单位的。拷贝构造函数本身属于 Student 类的成员函数,因此在它内部可以直接访问同类型另一个对象 other 的私有成员。

此外从运行结果还能看出,调用 stu2.setScore(100); 修改 stu2 的成绩后,stu1 和 stu3 的成绩依然保持 95 不变。这说明复制完成后,它们是内存中 3 个各自独立的对象,并不是同一个对象的别名。

拷贝构造函数的调用时机

除了在定义新对象时写出 Student stu2(stu1); 或 Student stu3 = stu1; 之外,在函数传参和返回结果的过程中,编译器也会在背后自动调用拷贝构造函数。常见的调用时机主要有以下 3 种:

  • 使用已有对象初始化新对象:如示例 1 所示,通过括号法 Student stu2(stu1);、等号法 Student stu3 = stu1; 或花括号 Student stu4{stu1}; 创建新对象时自动调用。
  • 对象按值传递给函数形参:当函数的形参是普通对象而不是引用时,调用函数会把实参对象复制一份交给形参,此时会自动调用拷贝构造函数。
  • 函数按值返回局部对象:当函数按值返回一个局部对象时,在语法规则上也会通过拷贝或移动把结果交回给调用处。

在这 3 种情况里,第二种在平时写代码时最容易产生多余的复制开销。下面我们通过一个例子,对比一下按值传递与引用传递传入对象时有什么区别。

示例 2:按值传参与按引用传参

#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int score;

public:
    Student(const std::string& n, int s)
        : name(n), score(s) {}

    Student(const Student& other)
        : name(other.name), score(other.score) {
        std::cout << "调用拷贝构造函数\n";
    }

    void show() const {
        std::cout << name << ",成绩:" << score << "\n";
    }
};

// 按值传递:触发拷贝构造
void printByValue(Student s) {
    s.show();
}

// 按引用传递:不触发拷贝构造
void printByRef(const Student& s) {
    s.show();
}

int main() {
    Student stu("Jack", 92);

    std::cout << "1. 调用 printByValue:\n";
    printByValue(stu);

    std::cout << "2. 调用 printByRef:\n";
    printByRef(stu);
    return 0;
}

运行结果如下。

1. 调用 printByValue:
调用拷贝构造函数
Jack,成绩:92
2. 调用 printByRef:
Jack,成绩:92

分析:

调用 printByValue(stu) 时,由于形参 Student s 采用的是值传递,程序在进入函数时需要在内存中新建形参对象 s,相当于执行了 Student s = stu;,因此控制台多输出了一行 “调用拷贝构造函数”。

而调用 printByRef(stu) 时,形参 const Student& s 只是原对象 stu 的只读引用,不需要创建新对象,自然也就省去了一次拷贝构造函数的调用开销。

因此在实际开发中,如果一个函数只需要读取类对象的数据而不需要修改它,我们通常会将形参写成 const 类名&,既能省去复制对象的开销,又能防止原对象的数据被误改。

返回值优化(RVO)

前面提到拷贝构造函数的第三种调用时机是 “函数按值返回局部对象”。不过小伙伴们如果在本地编写一个返回局部 Student 对象的函数,运行时往往会发现控制台并没有打印 “调用拷贝构造函数”。

这是因为现代 C++ 编译器默认开启了 “返回值优化(Return Value Optimization)”,从 C++17 标准起在许多场景下已成为强制规则。编译器会直接在接收结果的内存位置上构造对象,省去中间临时对象的复制,因此平时我们可以放心地按值返回局部对象。

默认拷贝构造与 = default / = delete

在前面的章节中,我们定义类时并没有手写拷贝构造函数,为什么同样可以使用已有对象创建新对象呢?这是因为编译器会自动生成默认的拷贝构造函数,并且从 C++11 标准起,我们还可以使用 = default 和 = delete 明确控制类的拷贝行为。

1. 默认拷贝构造函数与 = default

如果我们没有在类中显式定义拷贝构造函数,C++ 编译器会自动生成一个公有的 “默认拷贝构造函数”,按照成员变量在类中的声明顺序,把来源对象的每个成员依次复制给新对象。

比如对于包含 x 和 y 两个整型成员的 Point 类,编译器自动生成的默认拷贝构造函数,效果就相当于下面这段代码:

// 编译器自动生成的默认拷贝构造
Point(const Point& other) : x(other.x), y(other.y) {}

如果想在代码中明确表达 “使用编译器默认生成的拷贝规则”,我们也可以在类内部显式写上 Point(const Point& other) = default;。

对于只包含基本类型或 std::string、std::vector 等标准库容器成员的类,默认拷贝就已经够用了。只有当类内部直接管理通过 new 分配的指针成员时,才需要手动编写支持深拷贝的拷贝构造函数,这一点后面章节会详细介绍。

2. 使用 = delete 禁止拷贝

在有些场景下,某个类的对象代表独占的资源,比如前面学过的 std::unique_ptr,或者管理唯一网络连接、唯一许可证的类。这时我们希望从语法上直接禁止别人复制这个类的对象。

遇到这种需求时,我们只需要在拷贝构造函数声明的末尾加上 = delete;,告诉编译器禁用该类的拷贝构造函数即可。请看下面例子。

示例 3:控制类的拷贝行为

#include <iostream>

// 普通坐标类:保留默认拷贝构造
class Point {
private:
    int x;
    int y;

public:
    Point(int px, int py) : x(px), y(py) {}

    // 显式保留默认拷贝构造
    Point(const Point& other) = default;

    void show() const {
        std::cout << "(" << x << ", " << y << ")\n";
    }
};

// 独占许可证类:禁止拷贝
class UniqueLicense {
private:
    int id;

public:
    UniqueLicense(int n) : id(n) {}

    // 禁用拷贝构造
    UniqueLicense(const UniqueLicense& other) = delete;

    void show() const {
        std::cout << "许可证编号:" << id << "\n";
    }
};

int main() {
    Point p1(10, 20);
    Point p2 = p1;    // 调用默认拷贝构造
    p2.show();

    UniqueLicense lic1(1001);
    lic1.show();
    return 0;
}

运行结果如下。

(10, 20)
许可证编号:1001

分析:

Point(const Point& other) = default; 表示显式保留编译器默认生成的拷贝构造函数,因此 Point p2 = p1; 可以正常把 p1 的两个坐标值 10 和 20 复制给 p2。

而 UniqueLicense 类通过 = delete 禁用了拷贝构造函数。一旦我们在代码中尝试复制 UniqueLicense 对象,或者把它按值传给函数,编译器就会直接报错:

UniqueLicense lic1(1001);
UniqueLicense lic2 = lic1;    // 报错:拷贝构造已被禁用

注意 如果一个类原本没写任何构造函数,一旦显式声明了拷贝构造函数,编译器就不再自动生成默认无参构造函数。此时如果还需要无参创建对象,记得一并加上 类名() = default;。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号