1、什么是·C++拷贝构造函数?
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。这句话的意思是在利用拷贝构造函数创建一个新的对象时,是通过复制现有对象的数据(比如成员变量和成员函数)来初始化新对象。所以说,新对象将成为现有对象的副本。
C++中存在拷贝构造函数的主要原因是为了在对象复制时,正确地初始化新对象的状态。具体来说,拷贝构造函数允许程序员定义如何通过现有对象来创建一个新的对象。
具体解释
在C++中,当你创建一个新的对象,并且使用一个同类的已有对象来进行初始化时,就会调用拷贝构造函数。例如:
ClassName obj1; // 先创建一个对象 obj1
ClassName obj2 = obj1; // 使用 obj1 来初始化新的对象 obj2,调用拷贝构造函数
这里,obj2
是新创建的对象,它是通过使用 obj1
的数据来进行初始化的。这就意味着 obj2
的初始状态是 obj1
的副本。 使用 obj1 来初始化新的对象 obj2,就会调用拷贝构造函数,如下面拷贝构造函数代码示例所示。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
classname (const classname &obj) {
// 构造函数的主体
}
自己构造拷贝构造函数时,其形参必须是引用,但并不限制为const,一般会加上const限制。
拷贝构造函数的形参必须是引用类型原因:这是为了避免无限递归和不必要的拷贝,从而提升效率。
如果拷贝构造函数的形参不是引用而是值传递,那么在调用拷贝构造函数时,传递的对象参数会试图通过拷贝构造函数来创建其自身的副本。这就会导致递归调用拷贝构造函数,而每次递归调用又会创建一个新的对象副本,最终导致无限递归,程序无法结束。
例如,假设拷贝构造函数的形参是按值传递的:
class MyClass {
public:
MyClass(MyClass obj) { // 形参是值传递
// ... 拷贝操作
}
};
当调用 MyClass obj2 = obj1;
时,为了传递 obj1
,拷贝构造函数会被调用来创建 obj1
的副本,然后副本的创建又会触发拷贝构造函数,导致无限递归。
在C++中,拷贝构造函数的参数通常会使用 const
修饰符来限制引用,是为了防止修改原对象。当我们使用拷贝构造函数时,通常希望仅仅读取被拷贝对象的数据,而不希望在拷贝过程中修改这个对象。const
关键字确保拷贝构造函数中的代码不会修改传入的原对象,提供了额外的安全性和明确性。
拷贝构造函数代码示例:
#include "iostream"
using namespace std;
class Person{
public:
Person(int a){
m_age = a;
cout << "有参构造函数调用" << endl;
}
Person(const Person &p){//拷贝构造函数定义
cout << "拷贝构造函数调用" << endl;
m_age = p.m_age;
}
int m_age;
};
int main(){
Person p1(18);
Person p2 = p1;//拷贝构造函数调用
cout << "年龄:" << p2.m_age << endl;
return 0;
}
运行结果如下:
有参构造函数调用
拷贝构造函数调用
p2的年龄:18
从上述代码的运行结果可以看出,当你创建一个新的对象,并且使用一个同类的已有对象来进行初始化时,就会调用拷贝构造函数。
2、拷贝构造函数的通常使用
拷贝构造函数通常在以下情况下调用:
-
通过使用另一个同类型的对象来初始化新创建的对象。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
int value;
// 构造函数
MyClass(int v) : value(v) {}
// 拷贝构造函数
MyClass(const MyClass &obj) {
value = obj.value;
cout << "拷贝构造函数被调用" << endl;
}
};
int main() {
MyClass obj1(10); // 使用构造函数
MyClass obj2 = obj1; // 使用拷贝构造函数
cout << "obj2.value: " << obj2.value << endl;
return 0;
}
运行结果如下:
拷贝构造函数被调用
obj2.value: 10
在这个例子中,当我们使用MyClass obj2 = obj1;
时,拷贝构造函数被调用来创建obj2
。
-
复制对象把它作为参数传递给函数。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
int value;
// 构造函数
MyClass(int v) : value(v) {}
// 拷贝构造函数
MyClass(const MyClass &obj) {
value = obj.value;
cout << "拷贝构造函数被调用" << endl;
}
};
// 通过值传递参数,会调用拷贝构造函数
void display(MyClass obj) {
cout << "obj.value: " << obj.value << endl;
}
int main() {
MyClass obj1(20);
display(obj1); // 传递对象到函数
return 0;
}
运行结果如下:
拷贝构造函数被调用
obj.value: 20
在这个例子中,当我们通过值传递obj1
到display
函数时,拷贝构造函数被调用。
-
复制对象,并从函数返回这个对象。
示例:
#include <iostream>
using namespace std;
class MyClass {
public:
int value;
// 构造函数
MyClass(int v) : value(v) {}
// 拷贝构造函数
MyClass(const MyClass &obj) {
value = obj.value;
cout << "拷贝构造函数被调用" << endl;
}
};
// 返回对象会调用拷贝构造函数
MyClass createObject() {
MyClass temp(30);
return MyClass(temp); // 显式调用拷贝构造函数
}
int main() {
MyClass obj1 = createObject(); // 调用函数,返回对象
cout << "obj1.value: " << obj1.value << endl;
return 0;
}
运行结果如下:
拷贝构造函数被调用
obj1.value: 30
在这个例子中,当我们从createObject
函数返回一个对象时,拷贝构造函数被调用来创建obj1
。
3、浅拷贝与深拷贝
(1). 默认拷贝构造函数
很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
Rect::Rect(const Rect& r)
{
width=r.width;
height=r.height;
}
当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来思考以下一段代码:
#include<iostream>
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
~Rect()
{
count--;
cout << "析构函数被调用" << endl;
cout << "cout: " << count << endl;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,此时应该有两个对象存在,理论上讲,结果如下:
Rect对象的个数: 1
Rect对象的个数: 2
析构函数被调用
cout: 1
析构函数被调用
cout: 0
但实际程序运行时,结果如下:
The count of Rect:1
The count of Rect:1
析构函数被调用
cout: 0
析构函数被调用
cout: -1
此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。
说白了,就是拷贝构造函数没有处理静态数据成员。这是因为,在上述代码中,没有显式定义拷贝构造函数,因此编译器会生成一个默认的拷贝构造函数,该构造函数会逐位拷贝对象的非静态成员。由于静态成员不属于任何特定对象,而是属于类本身,默认拷贝构造函数不会对静态成员进行拷贝。
出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:
#include<iostream>
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
Rect(const Rect& r)
{
width=r.width;
height=r.height;
count++;
}
~Rect()
{
count--;
cout << "析构函数被调用" << endl;
cout << "cout: " << count << endl;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<"Rect对象的个数: "<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"Rect对象的个数: "<<Rect::getCount()<<endl;
return 0;
}
此时的输出与我们期望的一致,结果如下:
Rect对象的个数: 1
Rect对象的个数: 2
析构函数被调用
cout: 1
析构函数被调用
cout: 0
(2). 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:
#include "iostream"
using namespace std;
/*
* 如果利用编译器提供的拷贝构造函数,会做浅拷贝操作,浅拷贝带来的问题就是堆区的内存重复释放
*浅拷贝的问题要利用深拷贝进行解决
* */
class Person{
public:
Person(){
cout <<"Person的默认构造函数调用" << endl;
}
//有参构造函数
Person(int age,int height){
cout <<"Person的有参构造函数的调用"<<endl;
m_age = age;
m_height = new int(height);
}
// Person(const Person& p){
// cout << "Person拷贝构造函数调用" << endl;
// m_age = p.m_age;
m_Height = p.m_height;
编译器默认实现的就是这行代码
// //深拷贝操作
// m_height = new int(*p.m_height);
// }
~Person(){
//析构代码,将堆区开辟数据做释放操作
if(m_height != NULL){
delete m_height;
// m_height = NULL;
}
cout << "Person的析构函数调用" << endl;
}
public:
int m_age;//年龄
int *m_height;//身高
};
void test01()
{
Person p1(18,198);
cout << "p1的年龄为:"<< p1.m_age<<"身高为:"<< *p1.m_height<<endl;
Person p2(p1);
cout << "p2的年龄为:"<< p2.m_age<<"身高为:"<< *p2.m_height<<endl;
}
int main(){
test01();
return 0;
}
运行结果如下:
Person的有参构造函数的调用
p1的年龄为:18身高为:198
p2的年龄为:18身高为:198
Person的析构函数调用
进程已结束,退出代码为 -1073740940 (0xC0000374)
在这段代码运行出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
在运行定义对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:
在使用p1复制p2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 p1.p = p2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因,因为你不能将一个已经释放了的内存空间再次释放。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
(3). 深拷贝
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
#include "iostream"
using namespace std;
/*
* 如果利用编译器提供的拷贝构造函数,会做浅拷贝操作,浅拷贝带来的问题就是堆区的内存重复释放
*浅拷贝的问题要利用深拷贝进行解决
* */
class Person{
public:
Person(){
cout <<"Person的默认构造函数调用" << endl;
}
//有参构造函数
Person(int age,int height){
cout <<"Person的有参构造函数的调用"<<endl;
m_age = age;
m_height = new int(height);
}
Person(const Person& p){
cout << "Person拷贝构造函数调用" << endl;
m_age = p.m_age;
// m_Height = p.m_height;
// 编译器默认实现的就是这行代码
//深拷贝操作
m_height = new int(*p.m_height);
}
~Person(){
//析构代码,将堆区开辟数据做释放操作
if(m_height != NULL){
delete m_height;
// m_height = NULL;
}
cout << "Person的析构函数调用" << endl;
}
public:
int m_age;//年龄
int *m_height;//身高
};
void test01()
{
Person p1(18,198);
cout << "p1的年龄为:"<< p1.m_age<<"身高为:"<< *p1.m_height<<endl;
Person p2(p1);
cout << "p2的年龄为:"<< p2.m_age<<"身高为:"<< *p2.m_height<<endl;
}
int main(){
test01();
return 0;
}
运行结果如下:
Person的有参构造函数的调用
p1的年龄为:18身高为:198
Person拷贝构造函数调用
p2的年龄为:18身高为:198
Person的析构函数调用
Person的析构函数调用
进程已结束,退出代码为 0
小结:
拷贝有两种:深拷贝,浅拷贝。
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一对 一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
4、拷贝函数的三种调用方法
(1)括号法
#include "iostream"
using namespace std;
class Person{
public:
//构造函数
Person(){
cout << "Person的无参构造函数调用" << endl;
}
Person(int a){
age = a;
cout << "Person的有参构造函数调用" << endl;
}
~Person(){
cout << "Person 的析构函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p){
//将传入的人身上的所有属性拷贝到我身上
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
int age;
};
int main()
{
Person p2(10);
Person p3(p2);
cout << "年龄:" << p3.age << endl;
return 0;
}
运行结果:
Person的有参构造函数调用
Person的拷贝构造函数调用
年龄:10
Person 的析构函数调用
Person 的析构函数调用
(2)显示法
#include "iostream"
using namespace std;
class Person{
public:
//构造函数
Person(){
cout << "Person的无参构造函数调用" << endl;
}
Person(int a){
age = a;
cout << "Person的有参构造函数调用" << endl;
}
~Person(){
cout << "Person 的析构函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p){
//将传入的人身上的所有属性拷贝到我身上
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
int age;
};
int main()
{
Person p2 = Person(10);
Person p3 = Person(p2);
cout << "年龄:" << p3.age << endl;
return 0;
}
运行结果如下:
Person的有参构造函数调用
Person的拷贝构造函数调用
年龄:10
Person 的析构函数调用
Person 的析构函数调用
(3)、隐式转换法
#include "iostream"
using namespace std;
class Person{
public:
//构造函数
Person(){
cout << "Person的无参构造函数调用" << endl;
}
Person(int a){
age = a;
cout << "Person的有参构造函数调用" << endl;
}
~Person(){
cout << "Person 的析构函数调用" << endl;
}
//拷贝构造函数
Person(const Person &p){
//将传入的人身上的所有属性拷贝到我身上
age = p.age;
cout << "Person的拷贝构造函数调用" << endl;
}
int age;
};
int main()
{
Person p4 = 10;
Person p5 = p4;
cout << "年龄:" << p5.age << endl;
return 0;
}
运行结果如下:
Person的有参构造函数调用
Person的拷贝构造函数调用
年龄:10
Person 的析构函数调用
Person 的析构函数调用
5、拷贝构造函数的几个细节
(1).为什么拷贝构造函数必须是引用传递,不能是值传递?
避免无限递归:
- 如果拷贝构造函数使用值传递,则在调用拷贝构造函数时,需要将参数按值传递,这意味着需要拷贝这个参数对象。这又会调用拷贝构造函数,从而导致无限递归,最终导致栈溢出。
Rect(const Rect p) {
// 这将导致无限递归,因为传递参数 p 时需要调用拷贝构造函数
}
效率问题:
- 按值传递会创建参数对象的副本,这涉及调用拷贝构造函数和分配内存等操作,这比按引用传递要慢得多,效率较低。
Rect(const Rect p) {
// 按值传递会创建 p 的副本,涉及更多的内存分配和拷贝操作
}
引用传递更符合拷贝构造函数的语义:
拷贝构造函数的语义是创建一个新的对象作为参数对象的副本。通过引用传递参数,可以直接访问原始对象,而不会创建多余的副本,更符合其设计意图。
Rect(const Rect& p) {
// 通过引用传递参数 p,可以直接访问并拷贝原始对象的数据
}
(2). 拷贝构造函数的作用。
作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
(3).参数传递过程到底发生了什么?
将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!
i)值传递:
对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
ii)引用传递:
无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).
(4). 在类中有指针数据成员时,拷贝构造函数的使用?
如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题 了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。
问题1和2回答了为什么拷贝构造函数使用值传递会产生无限递归调用的问题;
问题3回答了回答了在类中有指针数据成员时,拷贝构造函数使用值传递等于白显式定义了拷贝构造函数,因为默认的拷贝构造函数就是这么干的。