博主:一个努力敲代码的小粉象
收录专栏:《c++初阶》
欢迎点赞、评论、收藏鼓励三连~~
前言
大家学习拷贝构造函数的时候常常无法理解其深层的含义以及和构造函数之间的关系。今天这篇博文将从拷贝构造函数的概念、特征、与构造函数之间的关系这三个方面来进行讲解~~
浅拷贝与深拷贝的简单理解
在了解拷贝函数之前,我们来简单拓展一个新知识:浅拷贝与深拷贝。
我们在c语言的学习过程中了解到形参是实参的拷贝这个说法,传参的方式也有传值传参和传址传参,而浅拷贝的一个别名就叫做值拷贝,所以这里我们就能简单联想一下:浅拷贝就是简单地将数据拷贝过去。那么如果是传一个数组呢?我们来看一看这个问题。

a中存储的是数组的首地址,那么将a传给形参a‘之后,a’中存储的是数组中的数据还是地址呢?显而易见是数组的地址。所以传递地址本质上没有开辟新的空间,也是浅拷贝的范畴。而a’和a是指向同一个位置的。
我们先来看一个简单的传值拷贝的例子~~
class date
{
public:
date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//浅拷贝/值拷贝
void print(date d1)
{
cout << "print(date d1)" << endl;
}
int main()
{
date d1(2023,10,25);
print(d1);
}

我们看到,浅拷贝在这里使用是完全可以的,程序运行成功了。但是,浅拷贝在任何情况下都可以使用吗?答案当然是否定的。
在这里老铁们注意,当我们想传递一个数组的时候,我们一般传递的是数组的首地址,而当函数结束跳出作用域的时候,c++会自动调用析构函数,这会导致什么呢?同样的一个空间被释放了两次。
老铁们肯定还是没有太理解,我们上代码!
#include<iostream>
using namespace std;
//栈空间问题
class stack
{
public:
stack(int capacity=10)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
~stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
}
//private:
int* _a;
int _size;
int _capacity;
};
void Print(stack st)
{
cout << "Print(stack st)" << endl;
}
int main()
{
stack st1;
Print(st1);
}
老铁们可以自行复制代码去vs上实现一下,会发现上述代码获得的结果是什么呢?

出现了警告。原因就是我们开辟的空间连续被释放了两次,导致出现问题。
我们通过图像来直观感受一下。

这里我们浅显总结一下深拷贝和浅拷贝的区别:浅拷贝是一种简单的复制,将数据复制过去,或者将地址传过去,而没有另开空间,所以函数中各种操作也是在原本空间上进行操作的;而深拷贝则是重新开辟了一块空间供函数使用,这样函数在结束时调用析构函数时就不会影响原本的空间。
所以此时,我们发现浅拷贝(值拷贝)在c++中会出现问题,那么应该怎么办呢?这里的方法就是调用拷贝构造函数。
拷贝构造函数
概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const来修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 编译器生成的默认拷贝函数已经可以完成字节序的值拷贝了,不需要自己显示实现。
- 拷贝构造函数的典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
传值引发无穷递归
我们先来讲解一下为什么直接传值会引发无穷递归。
这里不妨用日期类函数举个例子。
class date
{
public:
date(int year = 2023, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
date(const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//private:
int _year;
int _month;
int _day;
};
void print(date d) {
cout << "print(date d2)" << endl;
}
int main()
{
date d1;
date d2(d1);
print(d2);
return 0;
}

我们看到,这里传的引用,程序成功运行了。
而当我们去掉引用符,用传值引用呢?

这里便出现报错了。
下面我们用图像来理解一下。

自定义类型传参之前,要先进入拷贝构造函数当中,而在该拷贝构造函数当中,参数还是自定义类型,所以还需要传递到下一次的拷贝构造函数当中。这就导致了函数的无穷递归调用。
内置类型无需拷贝,自定义类型需要拷贝
而拷贝构造函数和构造函数之间有着相似的地方,便是**内置类型是按照字节方式直接拷贝的,自定义类型是调用其拷贝构造函数完成拷贝的。**通俗来讲,只有自定义类型才需要程序员自己写拷贝构造函数,而内置类型是不需要的。比如像日期类这样的类,是没有必要的。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
后言
今天我们集中讲解了关于拷贝函数的相关知识,具体解决了浅拷贝和深拷贝的简单理解,拷贝构造函数的概念、拷贝构造函数的特征、传值引发的无穷递归、内置类型无需拷贝,自定义类型需要拷贝,这几个问题,基本覆盖了关于拷贝构造函数的基本知识点。
后续博主还会继续更新c++以及数据结构,linux等知识点的学习!博主的知识有限,属于新人,如果有存在错误的地方,望各位佬们可以批评指正!
关于拷贝函数的相关知识,具体解决了浅拷贝和深拷贝的简单理解,拷贝构造函数的概念、拷贝构造函数的特征、传值引发的无穷递归、内置类型无需拷贝,自定义类型需要拷贝,这几个问题,基本覆盖了关于拷贝构造函数的基本知识点。
后续博主还会继续更新c++以及数据结构,linux等知识点的学习!博主的知识有限,属于新人,如果有存在错误的地方,望各位佬们可以批评指正!