拷贝和复制是一个意思,对应的英文单词都是copy。对于计算机来说,拷贝是指用一份原有的、已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据。例如,将 Word 文档拷贝到U盘去复印店打印,将 D 盘的图片拷贝到桌面以方便浏览,将重要的文件上传到百度网盘以防止丢失等,都是「创建一份新数据」的意思。
在 C++ 中,拷贝并没有脱离它本来的含义,只是将这个含义进行了“特化”,是指用已经存在的对象创建出一个新的对象。从本质上讲,对象也是一份数据,因为它会占用内存。
严格来说,对象的创建包括两个阶段,首先要分配内存空间,然后再进行初始化:
分配内存很好理解,就是在堆区、栈区或者全局数据区留出足够多的字节。这个时候的内存还比较“原始”,没有被“教化”,它所包含的数据一般是零值或者随机值,没有实际的意义。
初始化就是首次对内存赋值,让它的数据有意义。注意是首次赋值,再次赋值不叫初始化。初始化的时候还可以为对象分配其他的资源(打开文件、连接网络、动态分配内存等),或者提前进行一些计算(根据价格和数量计算出总价、根据长度和宽度计算出矩形的面积等)等。说白了,初始化就是调用构造函数。
很明显,这里所说的拷贝是在初始化阶段进行的,也就是用其它对象的数据来初始化新对象的内存。
它一共有3个场合使用:
通过使用另一个同类型的对象来初始化新创建的对象。
复制对象把它作为参数传递给函数。
复制对象,并从函数返回这个对象。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
classname (const classname &obj) {
// 构造函数的主体
}
在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。
#include <iostream>
#include <string>
using namespace std;
class Student{
public:
Student(string name = "", int age = 0, float score = 0.0f); //普通构造函数
Student(const Student &stu); //拷贝构造函数(声明)
public:
void display();
private:
string m_name;
int m_age;
float m_score;
};
Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//拷贝构造函数(定义)
Student::Student(const Student &stu){
this->m_name = stu.m_name;
this->m_age = stu.m_age;
this->m_score = stu.m_score;
cout<<"Copy constructor was called."<<endl;
}
void Student::display(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
Student stu1("小明", 16, 90.5);
Student stu2 = stu1; //调用拷贝构造函数
Student stu3(stu1); //调用拷贝构造函数
stu1.display();
stu2.display();
stu3.display();
return 0;
}
运行如下:
Copy constructor was called.
Copy constructor was called.
小明的年龄是16,成绩是90.5
小明的年龄是16,成绩是90.5
小明的年龄是16,成绩是90.5
第 8 行是拷贝构造函数的声明,第 20 行是拷贝构造函数的定义。拷贝构造函数只有一个参数,它的类型是当前类的引用,而且一般都是 const 引用。
- 为什么必须是当前类的引用呢?
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
2) 为什么是 const 引用呢?
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。