本系列文章主要介绍C++编程语言中类对象的赋值操作和复制操作,以及两者之间的区别,另外还会介绍“深拷贝”与“浅拷贝”的相关知识。
本文为系列文章的第二篇,主要介绍C++编程语言中类对象的复制的相关知识。
1 对象的复制
1.1 What
相对于“对已声明的对象使用赋值运算符进行的对象赋值”操作,使用拷贝构造函数操作对象的方式,称为“对象的复制”。
类的拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用为:在创建一个新对象时,使用一个已经存在的对象去初始化这个新对象。例如语句“ClassA obj2(obj1);”就使用了拷贝构造函数,该语句在创建新对象obj2时,利用已经存在的对象obj1来初始化对象obj2。
1.2 拷贝构造函数的特点
拷贝构造函数有以下特点:
- 拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值类型;
- 拷贝构造函数只有一个参数,并且该参数是其所属类对象的引用;
- 每一个类都必须有一个拷贝构造函数,可以根据需要重载默认的拷贝构造函数(自定义拷贝构造函数),如果没有重载默认的拷贝构造函数,系统就会生成产生一个默认的拷贝构造函数,默认的拷贝构造函数将会复制出一个数据成员完全相同的新对象。
1.3 自定义拷贝构造函数
这里展示一个自定义拷贝构造函数的示例代码(object_assign_and_copy_test2.cpp),内容如下:
#include <iostream>
using namespace std;
class ClassA
{
public:
// 普通构造函数
ClassA(int i, int j)
{
m_nValue1 = i;
m_nValue2 = j;
}
// 自定义的拷贝构造函数
ClassA(const ClassA& obj)
{
m_nValue1 = obj.m_nValue1 * 2;
m_nValue2 = obj.m_nValue2 * 2;
}
// 打印成员变量的值
void ShowValue()
{
cout << "m_nValue1 is: " << m_nValue1 << ", m_nValue2 is: " << m_nValue2 << endl;
}
private:
int m_nValue1;
int m_nValue2;
};
int main()
{
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2(obj1);
obj1.ShowValue();
obj2.ShowValue();
return 0;
}
编译并执行上述代码,结果如下:
上述执行结果表明,通过调用自定义的拷贝构造函数,在创建对象obj2时,根据对象obj1的成员变量的值,完成了自定义的初始化过程。
1.4 两种调用形式
可以从调用内容上,对“对象的赋值”和“对象的复制”进行区分。两者的调用内容的对应关系如下:
- 对象的赋值:指的是调用了类的赋值运算符,进行的对象的拷贝操作;
- 对象的复制:指的是调用了类的拷贝构造函数,进行的对象的拷贝操作。
上面的对应关系是不严谨的,因为有些情况下,即使使用了赋值运算符“=”,但实际上最终使用的仍然是类的拷贝构造函数,这就引出了拷贝构造函数的两种调用形式。
拷贝构造函数的调用语法分为以下两种:
- 类名 对象2(对象1)。例如:“ClassA obj2(obj1);”,这种调用拷贝构造函数的方法称为“代入法”;
- 类名 对象2 = 对象1。例如:“ClassA obj2 = obj1;”,这种调用拷贝构造函数的方法称为“赋值法”。
拷贝构造函数的“赋值法”就很容易与“对象的赋值”场景混淆,二者之间的区别是:对象的赋值场景必须是建立在源对象与目标对象均已声明的基础上;而拷贝构造函数的赋值法,必须是针对新创建对象的场景。
下面给出简单的示例代码说明两者的区别。
【对象的赋值】:
// 声明对象obj1和obj2
ClassA obj1;
ClassA obj2;
obj1.SetValue(1, 2);
// 对象赋值场景 —— 将obj1的值赋给obj2
obj2 = obj1;
【拷贝构造函数的“赋值法”】:
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2 = obj1;
当然,为了代码的可读性,建议使用拷贝构造函数的“代入法”,可以让人一眼就看出调用的是拷贝构造函数。
1.5 调用拷贝构造函数的三个场景
1.5.1 类对象初始化
当使用类的一个对象去初始化另一个对象时,会调用拷贝构造函数(包括“代入法”和“赋值法”)。示例代码内容如下:
// 创建并初始化对象obj1,此处调用了普通构造函数
ClassA obj1(1, 2);
// 创建并初始化对象obj2,此处调用了自定义的拷贝构造函数
ClassA obj2(obj1); // 代入法
ClassA obj3 = obj1; // 赋值法
1.5.2 类对象作为函数参数
当类对象作为函数形参时,在调用函数进行形参和实参转换时,会调用拷贝构造函数。
示例代码内容如下:
// 形参是类ClassA的对象obj
void funA(ClassA obj)
{
obj.ShowValue();
}
int main()
{
ClassA obj1(1, 2);
// 调用函数funA时,实参obj1是类ClassA的对象
// 这里会调用拷贝构造函数,使用实参obj1初始化形参对象obj
funA(obj1);
return 0;
}
说明:在上面的main函数内,语句“funA(obj1);”就会调用拷贝构造函数。
1.5.3 类对象作为函数返回值
当函数的返回值是类的对象时,在函数调用完毕将返回值(对象)带回函数调用处,此时会调用拷贝构造函数,将函数返回的对象赋值给一个临时对象,并传到函数的调用处。
示例代码内容如下:
// 函数funB()的返回值类型是ClassA类类型
ClassA funB()
{
ClassA obj1(1, 2);
// 函数的返回值是ClassA类的对象
return obj1;
}
int main()
{
// 定义类ClassA的对象obj2
ClassA obj2;
// funB()函数执行完成、返回调用处时,会调用拷贝构造函数
// 使用obj1初始化obj2
obj2 = funB();
return 0;
}
说明:在上面的main函数内,语句“obj2 = funB();”就会调用拷贝构造函数。由于对象obj1是在函数funB中定义的,当函数funB结束时,obj1的生命周期就结束了,因此在函数funB结束之前,执行语句"return obj1"时,会调用拷贝构造函数将obj1的值拷贝到一个临时对象中,这个临时对象是系统在主程序中临时创建的。funB函数结束时,对象obj1消失,但是临时对象将会通过语句“obj2 = funB()”赋值给对象obj2,执行完这条语句后,临时对象也自动消失了。
关于本系列文章的第三篇,“深拷贝”与“浅拷贝”,以及赋值运算符的重载、拷贝构造函数的重载的相关内容,请点击此处。