本文主要内容:
一.拷贝构造函数(很重要)
在有的场景下,我们需要用一个已经存在的对象来构造一个新对象。换言之,我们需要构造一个和已有对象一模一样的对象。那么这里就是我们要介绍的拷贝构造函数。
1.拷贝构造函数也是构造函数,函数名和类名相同,没有返回值,函数的参数是你想要拷贝的对象。
2.如果没有显式提供拷贝构造函数,那么编译器会自动生成一个拷贝构造函数。
3.拷贝构造函数的参数必须是引用类型!
首先我们来看一看为什么拷贝构造函数的参数必须是引用类型,如果不是会发生什么
class Date
{
public:
Date(int year=1900,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//只有内置类型成员,不需要提供析构
~Date()
{}
//拷贝构造函数
Date(const Date& d)
{
}
private:
int _year;
int _month;
int _day;
};
拷贝构造函数传值传参可能会引发无穷拷贝:

所以,为了解决无穷拷贝的问题,所以拷贝构造函数的参数类型必须是引用类型!
而如果我们没有涉及到对对象的属性的内容进行修改的话,最好在前面加一个const(如果不加const,可能会有意想不到的编译失败的问题存在)
//正确的拷贝构造函数
Date(const Date& d)
{
}
那么和前面的默认构造函数一样,如果我们没有显式提供,那么编译器会默认生成一个拷贝构造函数。和默认构造函数不同的地方是,对于内置类型,拷贝构造函数会进行值的拷贝(可以理解成把一个一个字节的内容拷贝过来)也叫做浅拷贝。对于只有内置类型成员的类型,进行浅拷贝就已经足够了。而对于像栈这种直接管理堆区内存的类就需要进行深拷贝。而关于深浅拷贝的具体内容后续会详细介绍,这里稍微了解一下就可以了。
所以这里的Date类我们就可以直接使用编译器生成的拷贝构造函数就可以了,显示写出拷贝构造函数就要这样写
//使用引用返回可以减少拷贝
Date& (const Date& d)
{ //防止自己给自己拷贝
if(this !=&d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
return *this;
}
2.赋值运算符重载
那么接下来我们就来看赋值运算符的重载。首先先来谈谈运算符重载。我们知道C++里面有很多运算符,那么对于内置类型来说,运算符的行为已经被定义好不能变了。而对于自定义类型成员来说,运算符的行为是不确定的。为了能够让自定义类型成员也能用上运算符,C++提供了运算符重载的机制
//运算符重载--->operator关键字
//重载+号运算符
Date operator+(int day)
{
//do something
}
而运算符重载本质就是一个函数,所以在使用+号的是后,编译器会自动做这一步转换
//编译器做的等价转换
d1+d2;//等价于d1.operator+(d2);
了解了运算符重载,我们接下来再来看看赋值运算符的重载。显然易见,赋值运算符的重载完成的工作是完成对一个已存在的对象进行改变,并不是只要使用=就是赋值
//易混淆的拷贝构造和赋值运算符重载
class Date
{
public:
Date(int year=1900,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
//只有内置类型成员,不需要提供析构
~Date()
{}
//拷贝构造函数
Date(const Date& d)
{
//防止自己给自己拷贝
if(this !=&d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,6,26);
//这里是调用拷贝构造函数,虽然是=号,因为是创建d2
Date d2=d1;
Date d3;
//这里才是赋值运算符重载,这里d3已经存在,所以是赋值
d3=d2;
}
和默认构造函数一样,如果没有显示提供的话,编译器会自动生成一个,而这个自动生成的赋值运算符重载函数也是完成浅拷贝。
那么在现在的编译器来看,对于拷贝构造的优化十分厉害,来看这样一段代码,看一看发生了几次拷贝构造:
class Widget
{
public:
//默认构造函数
Widget(int a = 0)
{
_a = a;
}
//拷贝构造函数
Widget(const Widget& w)
{
_a = w._a;
}
private:
int _a;
};
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
int main()
{
Widget x;
Widget y = f(f(x));
return 0;
}

然而,现实的情况却是:编译器会对整个代码进行优化,也就是它会把连续的拷贝构造合并成一步,在上面的过程里面,图中的4和5,还有8和9就会被合并,所以最终我们看到只有7次的拷贝构造:
//验证拷贝构造只有7次
class Widget
{
public:
Widget(int a = 0)
{
_a = a;
cout << "Widget(int a=0)" << endl;
}
Widget(const Widget& w)
{
_a = w._a;
cout << "Widget(const Widget& w)" << endl;
}
private:
int _a;
};
Widget f(Widget u)
{
Widget v(u);
Widget w = v;
return w;
}
int main()
{
Widget x;
Widget y = f(f(x));
return 0;
}

补充:
关于拷贝构造和赋值运算符的const参数问题:
前面我们提到,如果不涉及对于对象属性的更改,那么我们最好使用const来修饰对象。有的时候不用const修饰形参可能会出现不可预测的编译错误!而具体可能出现的原因我们会在下一篇博客里面讨论。
总结: