c++详细讲解构造函数的拷贝流程
作者:panamera12
#include <iostream> #include <string> using namespace std; void func(string str){ cout<<str<<endl; } int main(){ string s1 = "http:www.biancheng.net"; string s2(s1); string s3 = s1; string s4 = s1 + " " + s2; func(s1); cout<<s1<<endl<<s2<<endl<<s3<<endl<<s4<<endl; return 0; }
运行结果:
http:www.biancheng.net
http:www.biancheng.net
http:www.biancheng.net
http:www.biancheng.net
http:www.biancheng.net http:www.biancheng.net
s1、s2、s3、s4 以及 func() 的形参 str,都是使用拷贝的方式来初始化的。
对于 s1、s2、s3、s4,都是将其它对象的数据拷贝给当前对象,以完成当前对象的初始化。
对于 func() 的形参 str,其实在定义时就为它分配了内存,但是此时并没有初始化,只有等到调用 func() 时,才会将其它对象的数据拷贝给 str 以完成初始化。
当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数(Copy Constructor)。
#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(){ const 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 引用。
1) 为什么必须是当前类的引用呢?
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
2) 为什么是 const 引用呢?
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
当然,你也可以再添加一个参数为非const 引用的拷贝构造函数,这样就不会出错了。换句话说,一个类可以同时存在两个拷贝构造函数,一个函数的参数为 const 引用,另一个函数的参数为非 const 引用。
class Base{ public: Base(): m_a(0), m_b(0){ } Base(int a, int b): m_a(a), m_b(b){ } private: int m_a; int m_b; }; int main(){ int a = 10; int b = a; //拷贝 Base obj1(10, 20); Base obj2 = obj1; //拷贝 return 0; }
b 和 obj2 都是以拷贝的方式初始化的,具体来说,就是将 a 和 obj1 所在内存中的数据按照二进制位(Bit)复制到 b 和 obj2 所在的内存,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。
对于简单的类,默认的拷贝构造函数一般就够用了,我们也没有必要再显式地定义一个功能类似的拷贝构造函数。但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认的拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。
下面我们通过一个具体的例子来说明显式定义拷贝构造函数的必要性。
#include <iostream> #include <cstdlib> using namespace std; //变长数组类 class Array{ public: Array(int len); Array(const Array &arr); //拷贝构造函数 ~Array(); public: int operator[](int i) const { return m_p[i]; } //获取元素(读取) int &operator[](int i){ return m_p[i]; } //获取元素(写入) int length() const { return m_len; } private: int m_len; int *m_p; }; Array::Array(int len): m_len(len){ m_p = (int*)calloc( len, sizeof(int) ); } Array::Array(const Array &arr){ //拷贝构造函数 this->m_len = arr.m_len; this->m_p = (int*)calloc( this->m_len, sizeof(int) ); memcpy( this->m_p, arr.m_p, m_len * sizeof(int) ); } Array::~Array(){ free(m_p); } //打印数组元素 void printArray(const Array &arr){ int len = arr.length(); for(int i=0; i<len; i++){ if(i == len-1){ cout<<arr[i]<<endl; }else{ cout<<arr[i]<<", "; } } } int main(){ Array arr1(10); for(int i=0; i<10; i++){ arr1[i] = i; } Array arr2 = arr1; arr2[5] = 100; arr2[3] = 29; printArray(arr1); printArray(arr2); return 0; }
运行结果:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
0, 1, 2, 29, 4, 100, 6, 7, 8, 9
本例中我们显式地定义了拷贝构造函数,它除了会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来。这样做的结果是,原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据不会影响另外一个对象,本例中我们更改了 arr2 的数据,就没有影响 arr1。
这种将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。深拷贝的例子比比皆是,除了上面的变长数组类,使用的动态数组类也需要深拷贝;此外,标准模板库(STL)中的 string、vector、stack、set、map 等也都必须使用深拷贝。
读者如果希望亲眼目睹不使用深拷贝的后果,可以将上例中的拷贝构造函数删除,那么运行结果将变为:0, 1, 2, 29, 4, 100, 6, 7, 8, 9
0, 1, 2, 29, 4, 100, 6, 7, 8, 9
可以发现,更改 arr2 的数据也影响到了 arr1。这是因为,在创建 arr2 对象时,默认拷贝构造函数将 arr1.m_p 直接赋值给了 arr2.m_p,导致 arr2.m_p 和 arr1.m_p 指向了同一块内存,所以会相互影响。
另外需要注意的是,printArray() 函数的形参为引用类型,这样做能够避免在传参时调用拷贝构造函数;又因为 printArray() 函数不会修改任何数组元素,所以我们添加了 const 限制,以使得语义更加明确。
- 到底是浅拷贝还是深拷贝
如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针,一般浅拷贝足以。
另外一种需要深拷贝的情况就是在创建对象时进行一些预处理工作,比如统计创建过的对象的数目、记录对象创建的时间等,请看下面的例子:
#include <iostream> #include <ctime> #include <windows.h> //在Linux和Mac下要换成 unistd.h 头文件 using namespace std; class Base{ public: Base(int a = 0, int b = 0); Base(const Base &obj); //拷贝构造函数 public: int getCount() const { return m_count; } time_t getTime() const { return m_time; } private: int m_a; int m_b; time_t m_time; //对象创建时间 static int m_count; //创建过的对象的数目 }; int Base::m_count = 0; Base::Base(int a, int b): m_a(a), m_b(b){ m_count++; m_time = time((time_t*)NULL); } Base::Base(const Base &obj){ //拷贝构造函数 this->m_a = obj.m_a; this->m_b = obj.m_b; this->m_count++; this->m_time = time((time_t*)NULL); } int main(){ Base obj1(10, 20); cout<<"obj1: count = "<<obj1.getCount()<<", time = "<<obj1.getTime()<<endl; Sleep(3000); //在Linux和Mac下要写作 sleep(3); Base obj2 = obj1; cout<<"obj2: count = "<<obj2.getCount()<<", time = "<<obj2.getTime()<<endl; return 0; }
运行结果:
obj1: count = 1, time = 1488344372
obj2: count = 2, time = 1488344375
运行程序,先输出第一行结果,等待 3 秒后再输出第二行结果。Base 类中的 m_time 和 m_count 分别记录了对象的创建时间和创建数目,它们在不同的对象中有不同的值,所以需要在初始化对象的时候提前处理一下,这样浅拷贝就不能胜任了,就必须使用深拷贝了。
到此这篇关于c++详细讲解构造函数的拷贝流程的文章就介绍到这了,更多相关c++构造函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!