拷贝构造函数引出
先给出日期类的定义:
class Date
{
public:
Date(int year=1900,int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
有一种场景,我们需要另一个对象和之前对象所初始化的值一模一样。
第一种方法:每次创建对象写成一样的值,但是如果d1的值改变了呢,其他在跟着修改,数据量很大时很不方便
Date d1(2020,2,1);
Date d2(2020,2,1);
第二种方法:采用拷贝构造函数
拷贝构造函数
拷贝构造函数:只有单个形参(不算那个隐含的this指针),该形参是对本类类型的一个引用(一般用const修饰),再用已存在的类类型对象创建新对象时由编译器自动调用。
特点:
- 拷贝构造函数是构造函数的一个重载。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值会引发无穷的递归调用。
传值的话编译不通过。
分析一下不通过的原因:
假如我们要给d2对象拷贝d1的值Date d2(d1);,
根据以前的知识,传值传参需要发生拷贝把d1传给
Date(const Date d)中的d,就是传值传参,所以,d1拷贝给d就又要调用拷贝构造函数
Date d(d1)`,在调用。引发无穷递归。
所以需要传引用
Date(const Date& d)
{
_year = d.year;
_month = d.month;
_day = d.day;
}
在和传值对比一下:
传引用时不需要拷贝,调用函数Date d2(d1);
然后看函数Date(const Date& d)
的形参d,d是d1的一个别名。就完成了对 对象d1内成员的全部拷贝,但默认的拷贝构造函数只拷贝值,所以也被我们成为值拷贝,或者浅拷贝。
只拷贝值的含义是什么呢,在这个例子可能理解的不是很清楚,我们看到这两个的值虽然是完全一样的,但是另一种场景呢?
class stack
{
public:
stack(int capacity=4)
{
_a = (int*)malloc(sizeof(int)*capacity);
_capacity = capacity;
_size = 0;
}
~stack
{
free(-a);
_capacity=0;
_size=0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
stack s1;
stack s2(s1);
return 0;
}
这里我们想干的是在建一个对象s2,这个对象也想s1一样有一样的一个一样大小的数组,但是我们发现
其他一样是一样了,但是s2的指针,和s1的指针竟然指向了同一块空间,这就是浅拷贝。比如说,我看见了舍友有一个很好用的手机,我想要一个跟他内存一样大的手机,而不是和他共用一个手机。
而且这个程序最后会崩溃,创建s2,s2后进系统上的栈,先出所以s2对象生命周期先结束,会调用我们写的析构函数(自己生成的析构函数不释放)释放对上的空间,而s1此时是浅拷贝,和他共用一个空间,生命周期结束,再次调用析构函数,又free了一次,完蛋。
由于日期类里面没有指针什么的,所以默认生成的就可以用,因为它只需要浅拷贝所有字节, 而栈里,则是必须我们要自己写一个拷贝构造实现深拷贝(要一块跟你一样大的空间,让指针指向自己的),因为不写,就出现了上述举例的情况运算符重载引出
C++为了增加代码的可读性引入了运算符重载。自定义类型就可以像内置类型一样去用运算符。需要我们自己定义运算符重载
Date d;
d+1;//日期加一天
运算符重载
运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字,及参数列表,其返回值类型与参数列表与普通函数类似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型: 返回值类型 operator 后面接需要重载的运算符符号
注意:
- 不能通过连接其他符号创造新的操作符
- 冲在操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符其含义不能改变,例如内置的整形+,不能改变其含义
- 作为类成员函数,其形参看起来比操作数数目少一,因为成员函数有个默认的形参this指向调用对象,限定为第一个形参。
.* :: sizeof ?: .
以上五个运算符不能重载(面试选择题经常考)- 想用什么操作符去自己重载
第一次写了这样,忘记了作为成员函数的时候,第一个参数已经有了就是隐含的this指针,指向调用对象。
不需要改变所以加上const,传值的话外面会调用拷贝构造函数,传引用就不调用了。
bool operator==(const Date& d2)
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
开始的时候运算符函数形参写的是这样 const Date d2
,虽然也可以,但是传递过去会调用拷贝构造函数。
但是脑子一混,把operator==
函数当成了拷贝构造函数,还在想拷贝构造函数的形参是const Date d2
的话不就无穷调用了吗。(zzz,你不重新写拷贝构造函数,人家默认生成的的参数肯定是引用类型的,不用你操心)
赋值运算符重载
把d2的值赋值给d1,需要注意的是它与拷贝构造是不一样的。赋值说明的是,两个对象已经定义出来了。
Date d1;
Date d2(2021,2,6);
d1=d2;
而拷贝构造则是将d1的值直接初始化给d2。两者是不一样的
Date d1;
Date d2(d1);
//赋值重载
void operator=(const Date& d)
{
//自己赋值自己没有意义
//开始写成这样,让对象比较。。还要写一个运算符重载函数比较 if(*this!=d)
if(this!=&d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
但是在我们的内置类型中是支持连续赋值的
比如说:
int i,j,k;
i=j=k=10;
也就是说k=10,有一个返回值k,让j=k,返回j,又让i=j。
所以给我们的函数加上返回值,类型为类名,返回this所指向的对象
//赋值重载
Date operator=(const Date& d)
{
//自己赋值自己没有意义
if(this!=&d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
当返回值为Date类型,*this传递给外面需要两次拷贝构造(先传给临时变量(Date类型),临时变量再给外面用于接收的变量)
当返回值为Date&
,临时变量是返回值的别名,减少一次拷贝构造。形参做引用(总共一次)也减少一次拷贝构造。
所以下面这样写一共减少了两次拷贝构造。
//最终版赋值重载
Date& operator=(const Date& d)
{
//自己赋值自己没有意义
if(this!=&d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
注意:返回值做引用有一个条件就是看你返回值的生命周期,假如this所指向的对象生命周期在本函数体内,那么this传给中间变量,此时this销毁,中间变量是个引用也跟着销毁。外面的变量接收不到它。但是在这里*this是d1对象,生命周期在main函数里,所以不用担心
当我们不写,他也会自动生成,不过默认生成的他也是只进行浅拷贝,只拷贝值,像之前写拷贝构造函数里的Static,里面的指针变量。当d1=d2时,两个对象里面的两个指针指向同一块空间,析构的时候释放两次,程序崩溃。