文章目录

一、浅拷贝和深拷贝

对于普通类型的对象来说,它们之间的复制是很简单的,而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。

在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,假设对象B中有一个成员变量指针已经申请了内存,此时执行:A=B。其实只是新建了一个代指对象A的指针,这个指针指向B的地址,这时,A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝(a)和浅拷贝(b)可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

C++(十一)拷贝构造函数(复制构造函数)_c++


深拷贝示例

#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
char * str;
public:
String() :str(NULL) { }
String(String & s);
const char * c_str() const { return str; };
String & operator = (const char * s);
String & operator = (const String & s);
~String();
};
String::String(String & s)
{
if (s.str) {
str = new char[strlen(s.str) + 1]; // 添加\0空间
strcpy(str, s.str);
}
else
str = NULL;
}
String & String::operator = (const String & s)
{
if (str == s.str)
return *this;
if (str)
delete[] str;
if (s.str) { //s. str不为NULL才执行复制操作
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
return *this;
}
String & String::operator = (const char * s) //重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};

=======================
String s1, s2;
s1 = "this";
s2 = "that";
s2 = s1;

二、复制构造函数

复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用

复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。

如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。

注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。

默认复制构造函数

#include<iostream >
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r, double i) {
real= r; imag = i;
}
};
int main(){
Complex cl(1, 2);
Complex c2 (cl); //用复制构造函数初始化c2
cout<<c2.real<<","<<c2.imag; //输出 1,2
return 0;
}

编译器自动生成的那个默认复制构造函数的参数才能和 c1 匹配。
调用默认复制构造函数进行初始化的。初始化的结果是 c2 成为 c1 的复制品,即 c2 和 c1 每个成员变量的值都相等。

自定义复制构造函数

#include<iostream>
using namespace std;
class Complex{
public:
double real, imag;
Complex(double r,double i){
real = r; imag = i;
}
Complex(const Complex & c) // 最好加上const,这样复制构造函数才能接受常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。
{
real = c.real; imag = c.imag;
cout<<"Copy Constructor called"<<endl ;
}
};
int main(){
Complex cl(1, 2);
Complex c2 (cl); //调用复制构造函数
cout<<c2.real<<","<<c2.imag;
return 0;
}

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。
例如:类X的拷贝构造函数的形式为X(X& x)。

三、复制构造函数被调用的三种情况

复制构造函数在以下三种情况下会被调用。

3.1 用一个对象去初始化同类的另一个对象

当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。
例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
Complex c2(c1);
Complex c2 = c1;
这两条语句是等价的。

注意,第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
Complex c1, c2; c1 = c2 ;
c1=c2;

3.2 函数的参数是类的对象

那么当 Func 被调用时,类 A 的复制构造函数将被调用。
换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。

#include<iostream>
using namespace std;
class A{
public:
A(){};
A(A & a){
cout<<"Copy constructor called"<<endl;
}
};

void Func(A a){ }

int main(){
A a;
Func(a);
return 0;
}
==========输出结果=============

Func 函数的形参 a 在初始化时调用了复制构造函数。
函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 a 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。

以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。

如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。例如:
void Function(const Complex & c)
{

}
这样,Function 函数中出现任何有可能导致 c 的值被修改的语句,都会引发编译错误。

3.3 函数的返冋值是类的对象

函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。
换言之,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:

#include<iostream>
using namespace std;
class A {
public:
int v;
A(int n) { v = n; };
A(const A & a)
{
v = a.v; // 复制构造函数在此完成了复制的工作
cout << "Copy constructor called" << endl;
}
};
A Func() {
A a(4);
return a; //返回值是一个对象,该对象就是用复制构造函数初始化的
}
int main() {
cout << Func().v << endl; //调用了 Func 函数
return 0;
}
===========输出结果:==============
Copy constructor called
4

需要说明的是,有些编译器出于程序执行效率的考虑,编译的时候进行了优化,函数返回值对象就不用复制构造函数初始化了,这并不符合 C++ 的标准。上面的程序,用 Visual Studio 2010 编译后的输出结果如上所述,但是在 Dev C++ 4.9 中不会调用复制构造函数。把第 14 行的 a 变成全局变量,才会调用复制构造函数。