上一篇我们讲了构造函数,就是对象实例化时会自动调用,那么,我们这里的拷贝构造在形式上是构造函数的一个重载,拷贝构造其实也是一种构造函数,那么我们就可以引出这里的规则
1.拷贝构造函数的函数名必须与类名相同。
2.拷贝构造函数的参数必须为一个引用(否则会引起无穷递归),通常是 const 类型的引用,用来指定被拷贝的对象。
3.拷贝构造函数用来初始化一个新的对象,新的对象与被拷贝的对象应该属于同一类。
4.如果不手动定义拷贝构造函数,那么编译器会自动生成一个默认的拷贝构造函数,该函数进行浅拷贝。
其实一个日期的类进行浅拷贝就行了,只需要把值给复制过去;但是对于一个栈的话,因为有了自动调用析构函数这个特性,那么我们就不能只去浅拷贝,而是把栈在堆上申请的空间包括值都要去拷贝一份(这里就是指拷贝指针背后的资源),这里也就是我们所说的深拷贝
我们看一下第四条规则,它是会生成一个默认的拷贝构造函数的,对于内置类型是这样的
当然对于自定义类型来说会去调用它的拷贝构造函数
根据第一二条规则我们可以看出拷贝构造函数的基本形式是这样的
当然了,根据第四条,我们其实不写日期类的拷贝构造函数的话编译器是会默认生成的。这个拷贝构造函数其实在栈这一些类中才是有价值的
我们看下面这样一个例子
class stack {
public:
stack(int capacity=3) {
cout << "stack" << endl;
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == nullptr) {
perror("malloc failed");
return;
}
_a = tmp;
_top = 0;
_capacity = capacity;
}
~stack()
{
cout << "~stack" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
void func(stack x) {
cout << "func" << endl;
}
int main() {
stack s1;
func(s1);
return 0;
}
这个代码其实是会报错的,因为s1传给x时是要调用一次拷贝构造的(因为我们一直说形参是实参的一份临时拷贝),而我们没有写拷贝构造函数,那么此时编译器就会默认进行浅拷贝,也就是把s1的_a指针的值去给了x,当要出x的作用域时,x就会调用它的析构函数,此时就释放了指针指向的空间,函数出来后要出s1的作用域时又要调用析构函数,这就导致一块空间被释放(free)了两回,这时就会报错,这也就解释了为什么开头我说跟析构函数有关
那么写上栈的拷贝构造函数就可以了
stack(const stack& x) {
int* tmp = (int*)malloc(sizeof(int) * _capacity);
if (tmp == nullptr) {
perror("malloc fail");
exit(-1);
}
memcpy(tmp, x._a, sizeof(int) * x._top);
_a = tmp;
_top = x._top;
_capacity = x._capacity;
}
下边的赋值运算符重载也是类的默认成员函数中的一个,说这个之前,我们还是先解释一下什么叫运算符重载
我们平常在用运算符时(> + = >= 等),只能对于内置类型进行使用,对于自定义类型不能使用,因为编译器认识内置类型,它知道怎么去运行。但是对于自定义类型它也不知道怎么去操作。
这时呢?为了方便,我们就要去写一个类的函数(这里以==为例),我们写是会写,但是可能写的函数名可能会使别人不认识,这时,我们的C++祖师爷就制定了一个标准,函数名就是operator操作符
比如说,我们写一个日期类的比较相等的函数
bool operator==(const Date&d1,const Date& d2) {
if (d1._year == d2._year && d1._month == d2._month && d1._day == d2._day) {
return true;
}
return false;
}
但是有一个问题,就是我们的成员变量一般是私有的,也就是在类外是访问不了的,我们当然访问有很多种方式,但几乎都是用一个类里面的函数把值给弄出来,那我们还不如直接把运算符重载的函数放到类中,把它变成一个成员函数
那么此时,它的形式就会有所变化
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1,int day=1) {
_year = year;
_month = month;
_day = day;
}
bool operator==( const Date& d) {
if (_year == d._year && _month == d._month && _day == d._day) {
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2023, 3, 1);
Date d2;
int ret = d1.operator==(d2);
cout << ret << endl;
return 0;
}
这里的d1是利用构造函数去初始化了,d2没有传值就是去调用默认构造函数,实际上就是利用缺省值去进行初始化。调用成员函数就像主函数第三行一样,因为编译器会默认传一个this指针
既然我们都这么写了,那么我们能不能再简化一点呢?
其实我们可以直接用符号去比较的
int main() {
Date d1(2023, 3, 1);
Date d2;
int ret = d1 == d2;
//int ret = d1.operator==(d2);
cout << ret << endl;
return 0;
}
这样写就会让编译器默认去调用下面的函数
有了运算符重载的知识,我们下面写一下赋值运算符重载,
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
这里要注意如果=左右是一个对象的话是不需要去赋值的,并且用的是引用返回不需要拷贝,有返回值是因为有可能要连续赋值,就像下边
d1=d1;
d1=d2=d3;
赋值运算符重载函数和拷贝构造函数是一样的,对于内置类型会去调用默认的函数,对于自定义类型会去调用自定义类型定义好的函数