[TOC]

概述

我们先说一下对象的创建过程。严格来说,对象的创建包括两个阶段,首先要分配内存空间,然后再进行初始化:

  • 分配内存很好理解,就是在堆区、栈区或者全局数据区留出足够多的字节。这个时候的内存还比较“原始”,没有被“教化”,它所包含的数据一般是零值或者随机值,没有实际的意义。
  • 初始化就是首次对内存赋值,让它的数据有意义。注意是首次赋值,再次赋值不叫初始化。初始化的时候还可以为对象分配其他的资源(打开文件、连接网络、动态分配内存等),或者提前进行一些计算(根据价格和数量计算出总价、根据长度和宽度计算出矩形的面积等)等。说白了,初始化就是调用构造函数。

很明显,这里所说的拷贝是在初始化阶段进行的,也就是用其它对象的数据来初始化新对象的内存。

当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数(Copy Constructor)。

定义与使用

以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数(Copy Constructor)。

我们先看拷贝构造函数的声明

1
2
3
4
5
/**
* 拷贝构造函数(声明)
* @param stu
*/
Student(const Student &stu);

我们接着看拷贝构造函数的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 拷贝构造函数的定义。
* @param student 入参是对应本身的引用
*/
Student::Student(const Student &student) {
cout << ">>>>>调用类的拷贝构造函数" << endl;
// 我们使用this指针来访问当前对应的属性
this->stuID = student.stuID;
this->stuAge = student.stuAge;
this->stuName = student.stuName;
// 如果类的某个属性,没有进行初始化,那么这个属性就是随机值
// 所以我们要进行针对下面totalScore进行拷贝初始化
this->totalScore = student.totalScore;
}

我们可以看到。拷贝构造函数只有一个参数,它的类型是当前类的引用,而且一般都是 const 引用。

为什么参数是当前类的引用呢?

如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

为什么是const引用呢。

拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。

另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

以上面的 Student 类为例,将 const 去掉后,拷贝构造函数的原型变为:

1
2
3
4
5
6
7
8
Student::Student(Student &stu);

// 此时,下面的代码就会发生错误:
const Student stu1("小明", 16, 90.5);
Student stu2 = stu1;
Student stu3(stu1);


stu1 是 const 类型,在初始化 stu2、stu3 时,编译器希望调用Student::Student(const Student &stu),但是这个函数却不存在,又不能将 const Student 类型转换为 Student 类型去调用Student::Student(Student &stu),所以最终调用失败了。

当然,你也可以再添加一个参数为 const 引用的拷贝构造函数,这样就不会出错了。换句话说,一个类可以同时存在两个拷贝构造函数,一个函数的参数为 const 引用,另一个函数的参数为非 const 引用。

默认拷贝构造函数

在前面的教程中,我们还没有讲解拷贝构造函数,但是却已经在使用拷贝的方式创建对象了,并且也没有引发什么错误。这是因为,如果程序员没有显式地定义拷贝构造函数,那么编译器会自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数很简单,就是使用“老对象”的成员变量对“新对象”的成员变量进行一一赋值,和上面 Student 类的拷贝构造函数非常类似。

对于简单的类,默认拷贝构造函数一般是够用的,我们也没有必要再显式地定义一个功能类似的拷贝构造函数。但是当类持有其它资源时,如动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认拷贝构造函数就不能拷贝这些资源,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。

源码解析

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
42
43
44
45
46
47
class Student {
private:
std::string stuName;
int stuID;
int stuAge;
float totalScore;
protected:
float englishScore, mathScore, articleScore;

public:
/**
* 无参构造函数
*/
Student() {
}

/**
* 普通含参构造函数的声明
* @param name
* @param age
* @param score
*/
Student(int id, std::string name, int age, float score); //普通构造函数

Student(int id, std::string name, int age); //普通构造函数

/**
* 拷贝构造函数(声明)
* @param stu
*/
Student(const Student &stu);

/**
* 析构函数的声明
* TODO .H文件里面是不是可以把类的定义也写在里面
*/
~Student() {
std::cout << "=====调用类的析构函数==========" << std::endl;
}

void input_data(); //声明成员函数的原型

void show_data(); //声明成员函数的原型

void display(); //声明成员函数的原型
};

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//
// Created by Frewen.Wong on 2021/2/17.
//
#include <iostream>
#include <cstdlib>
#include "01_demo_object_oriented.h"

using namespace std;

/**
* Student的类的声明与定义
* 类在使用前必须要进行声明。我们一般情况将类的声明放在.H文件里面
*/
/**
*
* @param id
* @param english
* @param math
* @param article
*/
Student::Student(int id, string name, int age, float score) {
cout << ">>>>>调用类的含参构造函数" << endl;
stuID = id;
stuAge = age;
stuName = name;
totalScore = score;
}

/**
* 可以通过冒号运算符直接对变量进行赋值
* @param id
* @param name
* @param age
*/
Student::Student(int id, string name, int age) : stuID(id), stuAge(age), stuName(name) {
cout << ">>>>>调用类的直接进行变量赋值含参构造函数" << endl;
}

/**
* 拷贝构造函数的定义。
* @param student 入参是对应本身的引用
*/
Student::Student(const Student &student) {
cout << ">>>>>调用类的拷贝构造函数" << endl;
// 我们使用this指针来访问当前对应的属性
this->stuID = student.stuID;
this->stuAge = student.stuAge;
this->stuName = student.stuName;
// 如果类的某个属性,没有进行初始化,那么这个属性就是随机值
// 所以我们要进行针对下面totalScore进行拷贝初始化
this->totalScore = student.totalScore;
}


// 如果是在类外面编写成员函数,只要在外部定义时函数名称前面加上类名称与范围解析运算符(::)即可。
// 范围解析运算符的主要作用就是指出成员函数所属的类。
void Student::input_data() {
cout << "请输入您的成绩:";
cin >> totalScore;
}

void Student::show_data() { //实现show_data函数
cout << "成绩是:" << totalScore << endl;
}

void Student::display() { //实现display函数的定义
cout << stuName << "的年龄是:" << stuAge << ",成绩是:" << totalScore << endl;
}

int main() {

cout << "===================普通对象实例化=================" << endl;
Student stu1(10010, "张三", 18, 100);
stu1.input_data();
stu1.show_data();
stu1.display();
// 直接实例化的对象使用完毕就会进行回收。调用类的析构函数
// 而使用指针进行动态内存申请的对象。如果不调用delete 则对应不会回收

cout << "===================指针方式类的实例化=================" << endl;

Student *stuPtr = new Student();
stuPtr->input_data();
stuPtr->show_data();
// 使用指针进行动态内存申请的对象。如果不调用delete 则对应不会回收
// delete指针对象。可以让对象调用析构函数,就加快资源释放。
// delete stuPtr;

cout << "===================指针方式类的含参数构造函数实例化=================" << endl;

Student *stuParamPtr = new Student(1001, "李四", 20, 596);
stuParamPtr->input_data();
stuParamPtr->show_data();

cout << "===================通过类的拷贝构造函数类进行实例化=================" << endl;
Student student4 = *stuParamPtr;
student4.display();
// 使用拷贝构造函数实例化的对象使用完毕就会进行回收。调用类的析构函数
// 而使用指针进行动态内存申请的对象。如果不调用delete 则对应不会回收

return 0;

}