技术交流QQ群:1027579432,欢迎你的加入!
1.拷贝构造函数
- 拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- a.当用类的一个对象去初始化该类的另一个对象(或引用)时系统自动调用拷贝构造函数实现拷贝赋值
- b.若函数的形参为类对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数
- c.当函数的返回值是类对象时,系统自动调用拷贝构造函数
-
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
类名 (const 类名 &obj){ // obj 是一个对象引用,该对象是用于初始化另一个对象的 拷贝构造函数的主体; }
- 实例如下:
#include "iostream" using namespace std; class Line{ public: int getLength(); Line(int len); // 普通的构造函数 Line(const Line &obj); // 拷贝构造函数 ~Line(); // 析构函数 private: int *ptr; }; // 构造函数定义 Line::Line(int len){ cout << "调用构造函数..." << endl; ptr = new int; // 为指针分配内存 *ptr = len; } // 拷贝构造函数定义 Line::Line(const Line &obj){ // &obj是对象line的一个引用,用这个对象的引用来初始化另一个对象 cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷贝值 } // 析构函数定义 Line::~Line(){ cout << "释放内存\n"; delete ptr; } // 普通成员函数定义 int Line::getLength(){ return *ptr; } void display(Line obj){ cout << "line大小: " << obj.getLength() << endl; } int main(){ Line line(10); display(line); return 0; }
- 下面的实例是对上面的例子进行修改,通过使用已有的同类型的对象来初始化新创建的对象:
#include "iostream" using namespace std; class Line{ public: int getLength(); Line(int len); // 普通的构造函数 Line(const Line &obj); // 拷贝构造函数 ~Line(); // 析构函数 private: int *ptr; }; // 构造函数定义 Line::Line(int len){ cout << "调用构造函数..." << endl; ptr = new int; // 为指针分配内存 *ptr = len; } // 拷贝构造函数定义 Line::Line(const Line &obj){ // &obj是对象line的一个引用,用这个对象的引用来初始化另一个对象 cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷贝值 } // 析构函数定义 Line::~Line(){ cout << "释放内存\n"; delete ptr; } // 普通成员函数定义 int Line::getLength(){ return *ptr; } void display(Line obj){ cout << "line大小: " << obj.getLength() << endl; } int main(){ Line line(10); display(line); cout << "-------------------------------\n"; Line line2 = line; // 调用拷贝构造函数 display(line2); return 0; }
2.什么是拷贝构造函数
- 首先对普通类型的对象来说,它们之间的赋值是简单的,如:int a = 100;int b = a;而类的对象与普通对象是不同的,类的对象内部结构一般比较复杂,存在各种成员变量,下面是一个简单的类对象拷贝的例子:
// 类的对象拷贝的简单例子 class C{ private: int a; public: // 构造函数 C(int b): a(b){ cout << "这是构造函数....\n"; } // 拷贝构造函数:一种特殊的构造的函数,函数的名称必须与类名一样,它必须的一个参数是本类类型的一个引用变量 C(const C &C_rename){ // 这里没有自定义拷贝构造函数的话,必须使用系统默认的拷贝构造函数 a = C_rename.a; cout << "这个是拷贝构造函数......\n"; } // 一般的成员函数 void show(){ cout << "a = " << a << endl; } }; C c1_obj(666); C c2_obj = c1_obj; // 等价于C c2_obj(c1_obj); c2_obj.show();
3.拷贝构造函数的调用时机
3.1 对象以值传递的方式传入作为函数参数
// 1.对象以值传递的方式传入函数作为参数
class D{
private:
int a;
public:
// 构造函数
D(int b): a(b){
cout << "这是D的构造对象...\n";
}
// 拷贝构造函数
D(const D& d_reference){
a = d_reference.a;
cout << "这是拷贝构造函数...\n";
}
// 析构函数
~D(){
cout << "这是析构函数,删除a = " << a << endl;
}
// 普通成员函数
void show(){
cout << "a = " << a << endl;
}
};
// 全局函数,传入的函数参数是D的某个对象
void d_Fun(D d){
cout << "test D\n";
}
D d_obj1(999);
d_Fun(d_obj1);
- 调用函数d_Fun()时,会产生以下几个重要的步骤:
- 对象d_obj1传入d_Fun()的形参时,会先产生一个临时变量temp;
- 然后调用拷贝构造函数会把d_obj1的值给temp,整个过程类似于D temp(d_obj1);
- 等d_Fun()执行完后,析构掉temp对象
3.2 对象以值传递的方式从函数返回
class E{
private:
int a;
public:
// 构造函数
E(int b) : a(b){
cout << "这是E的构造函数...\n";
}
// 拷贝构造函数
E(const E& e_reference){
a = e_reference.a;
cout << "这是E的拷贝构造函数...\n";
}
// 析构函数
~E(){
cout << "这是析构函数,删除a = " << a << endl;
}
// 普通成员函数
void show(){
cout << "a = " << a << endl;
}
};
// 全局函数
E e_fun(){
E temp(3);
return temp;
}
int main(){
e_fun();
return 0;
}
- 当调用e_fun()函数执行到return时,会产生几个重要的步骤:
- 先产生一个临时变量xxx
- 然后调用拷贝构造函数把temp的值给xxx,整个过程类似于E xxx(temp);
- 在函数执行到最后先析构temp局部变量
- 等e_fun()执行完后再析构掉xxx对象
3.3 对象需要通过另一个对象来初始化
C c_obj1(100);
C c_obj2(c_obj1); // 类似于C c_obj2 = c_obj1;
4.浅拷贝与深拷贝
4.1 默认拷贝构造函数
- 很多时候我们都不知道拷贝构造函数的情况下,传递对象参数或从函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是默认拷贝构造函数,这个构造函数很简单,仅仅使用老对象的数据成员的值来对新的对象的数据成员一一赋值,它具有以下的形式:
Rect::Rect(const Rect& r){ width = r.width; length = r.length; }
- 当然,上面的代码不用我们自己写,可以由编译器自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了!看下面的代码:
class Rect{ public: Rect(){ count++; } ~Rect(){ count--; } static int getCount(){ // 静态成员函数 return count; } private: int width; int length; static int count; // 静态成员变量count来计数 }; Rect rect1; cout << "The count of Rect: " << Rect::getCount() << endl; Rect rect2(rect1); // 新的对象需要使用老的对象来进行初始化 cout << "The count of Rect: " << Rect::getCount() << endl;
- 上面的代码对Rect类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数。按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。说白了,就是默认的拷贝构造函数没有处理静态数据成员。出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:
class Rect{ public: Rect(){ count++; } // 自定义拷贝构造函数 Rect(const Rect& r){ width = r.width; length = r.length; count++; } ~Rect(){ count--; } static int getCount(){ // 静态成员函数 return count; } private: int width; int length; static int count; // 静态成员变量count来计数 }; // 静态成员变量初始化 int Rect::count = 0;
4.2 浅拷贝
- 所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:
// 浅拷贝 class BB{ public: BB(){ // 构造函数,p指向堆中分配的一空间 p = new int(100); } ~BB(){ // 析构函数,释放动态分配的空间 if(p!=NULL) delete p; } private: int width; int length; int *p; // 指针成员 }; BB rect1; BB rect2 = rect1; // 复制对象
- 在上面的代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
-
在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:
-
-
在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:
-
- 3.当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
4.3 深拷贝
- 在深拷贝的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
class DD{ public: DD(){ p = new int (100); cout << "这是DD的构造函数\n"; } // 自定义默认拷贝构造函数 DD (const DD& dd){ width = dd.width; length = dd.length; p = new int; // 为新对象重新动态分配空间 *p = *(dd.p); cout << "这是DD的拷贝构造函数\n"; } ~DD(){ if (p!=NULL) delete p; cout << "这是DD的析构函数\n"; } private: int width; int length; int *p; }; DD rect1; DD rect2(rect1);
-
此时,在完成对象的复制后,此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”,内存的一个大致情况如下:
4.4 防止默认拷贝的发生
- 通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递即声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
// 防止默认拷贝的发生 class FF{ private: int a; public: FF(int b): a(b){ cout << "这是FF的构造函数\n"; } private: FF(const FF& ff){ a = ff.a; cout << "这是FF的拷贝构造函数\n"; } public: ~FF(){ cout << "这是FF的析构函数,删除a = " << a << endl; } // 普通成员函数 void show(){ cout << "a = " << a << endl; } }; // 全局函数 void ff_fun(FF ff_param){ cout << "test\n"; } FF test(1); // ff_fun(test); 按值传递将会报错!
5.拷贝构造函数的几个细节知识
- 拷贝构造函数里能调用private成员变量吗?
- 拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。
- 以下函数哪个是拷贝构造函数,为什么?
X::X(const X&); X::X(X); X::X(X&, int a=1); X::X(X&, int a=1, int b=2);
- 解释:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X& b) const X& c) volatile X& d) const volatile X&
X::X(const X&); //是拷贝构造函数 X::X(X&, int=1); //是拷贝构造函数 X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数
- 解释:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
- 一个类中可以存在多于一个的拷贝构造函数吗?
- 类中可以存在超过一个拷贝构造函数
class X { public: X(const X&); // const 的拷贝构造 X(X&); // 非const的拷贝构造 };
- 注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
class X{ public: X(); X(X&); }; const X cx; X x = cx; // error
- 如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数;这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。
- 类中可以存在超过一个拷贝构造函数