目录
一、拷贝构造函数
1.概念
2.特性
二、运算符重载
1.运算符重载
2.运算符重载实现的形式
3.赋值运算符重载
一、拷贝构造函数
1.概念
拷贝构造函数是一种特殊的构造函数,它在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于通过使用另一个同类型的对象来初始化新对象,或者在函数间传递对象时复制对象的状态。
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2.特性
①拷贝构造函数是构造函数的一个重载形式
②拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错,因为会引发无穷递归调用
③若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)
④编译器生成的默认拷贝构造函数中,内置类型直接拷贝,自定义类型调用其拷贝构造函数(与析构函数类似)
接下来对这些特性进行详细讲解:
关于第①点:
我们知道,构造函数的作用就是初始化,而我们在实例化出对象时是可以有多种初始化方式的,即构造函数支持重载,而拷贝构造函数就是将一个已经创建好的对象来初始化一个即将创建的对象,调用拷贝构造函数后这两个对象中的成员变量的值就是一样的。
关于第②点(重点):
我们先来看看拷贝构造函数的一般形式:
类名(const 类名& 其他对象)
参数为什么一定要是引用呢?直接传对象不可以吗?
我们知道,形参是实参的临时拷贝,如果是传值的话,在执行函数体中的语句前需要先拷贝实参给形参,而在拷贝构造函数中,参数是一个对象,要拷贝实参给形参就需要再次调用拷贝构造函数,而在这个函数中参数又是对象,就又要调用拷贝构造函数,就会一直这样无穷递归调用下去。
接下来看草图来帮助理解:
在拷贝构造函数中,我们不希望所传入的参数(对象)被修改,所以要加上const。
关于第③点:
当我们没有写拷贝构造函数时编译器会自动生成一个拷贝构造函数,接下来我们看一段代码来验证:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date today(2024, 4, 17);
Date _today(today);
today.Print();
_today.Print();
return 0;
}
运行结果:
可以看到,_today最后初始化为与today一样了,说明调用了编译器自动生成的拷贝构造函数。
既然我们不写拷贝函数时,编译器可以帮助我们生成一个拷贝函数,那么是否我们就不需要写拷贝构造函数了呢?
答案肯定是否定的,因为编译器生成的拷贝构造函数只能实现浅拷贝,接下来就要详细讲解一下浅拷贝和深拷贝的区别了。
浅拷贝仅复制对象中的数据成员,而不涉及动态内存分配;深拷贝则会创建一个新的对象并在堆上分配内存,以复制原始对象中的动态内存部分。
也就是说,浅拷贝只能拷贝数据成员,如果对象申请了空间,是无法自动在堆区中申请空间的。
总结:
①如果不涉及到申请空间(资源),可以不用写拷贝构造,编译器自动生成的就可以
②如果全是自定义类型成员,内置类型成员没有指向资源,用生成的默认拷贝构造函数就可以
③如果内部有指针或者一些值指向资源,就需要显式写析构函数,一般需要显式写析构函数的就需要显式写拷贝构造函数
二、运算符重载
1.运算符重载
运算符重载(Operator Overloading)是C++语言的一个强大特性,它允许程序员为自定义类型重新定义或重载已有的运算符,使得这些运算符能够用于自定义类型的对象。通过运算符重载,我们可以使得自定义类型的对象操作起来像内置类型(如int、float等)一样直观和方便。
运算符重载是具有特殊函数名的函数,也具有其返回类型,函数名字以及参数列表,其返回值类型是与参数列表与普通的函数类似。
函数名字为:关键字operator需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:
①重载运算符时,只能重载现有的运算符
②不能改变运算符的性质,如:运算符的优先级、结合性、操作数的个数及其语法结构
③五个不能重载的运算符:. .* :: ?: sizeof
2.运算符重载实现的形式
要重载一个运算符,可以把该运算符函数定义为类的成员函数,也可以把该运算符函数定义为类的非成员函数。非成员函数的形式通常包括友元函数和全局函数等两种形式。一般来说,把运算符函数定义为类的成员函数和友元函数,具有更简单的成员设置要求和更高的成员访问效率。
大多数运算符可以同时用成员函数形式和友元函数形式重载,如加法、减法、乘法和除法等算术运算符。但是= () [] ->只能重载为类的成员函数。
需要注意的是,如果重载为成员函数,实际参数数要少一个,因为有一个隐含的this指针,指向当前对象。
例如有一个Date类,重载>并且将其作为成员函数,实际只需要传一个参数:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool operator>(const Date& x)
{
//年大就大
if (_year > x._year)
{
return true;
}
else if (_year == x._year)
{
//年相同,月大就大
if (_month > x._month)
{
return true;
}
else if (_month == x._month)
{
return _day > x._day;
}
}
return false;
}
3.赋值运算符重载
同样,我们先把所需要的注意点放前边总结,然后再详细讲解。
①参数类型:const T&,传引用可以提高效率(T是类名)
②返回类型:T&,避免再一次拷贝,提高效率
③避免自赋值
④赋值运算符只能重载为类的成员函数
⑤我们平时使用的赋值运算符=是可以连等的,重载时也应该保留这个特点
⑥用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,内置类型成员变量是直接赋值的,而自定义类型需要调用对应类的赋值运算符重载完成赋值
关于第①②点:
在传参和值返回时会创建临时变量,也就是说,如果我们返回类型是类类型,那么就会再一次调用拷贝构造函数,这种是不必要的,所以用引用的形式返回更高效。
需要注意的是,并不是什么时候都可以用引用返回,出了作用域,返回对象还没有析构,就可以用引用返回,减少拷贝,如果返回对象已经析构,就不能用引用返回。
我们来看下边的代码,在Test函数中,我们创建了一个日期对象d,并且返回它的引用,然后在主函数中用dd再来接收(相当于Date& tmp = d; Date& dd = tmp;tmp是d的别名,dd是tmp的别名,即dd也是d的别名),按道理说d是2024年4月19日,而Test返回的是引用即别名,dd应当与d相同才是,实际上并不是这样的。原因在于d在执行完Test函数后就被析构了,然后这块内存就会被释放回栈中,当我们再调用其他函数(在这个例子中是Add)时就可能会用到那块空间,这时候就会出现如下的情况:dd中的数据成员全是随机值。
关于第④点:
赋值运算符如果不显式实现,编译器会生成一个默认的,此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,所以赋值运算符重载只能是类的成员函数。
关于第⑤点:
对于基本数据类型,是支持连等的,例如有两个int类型的数据a、b,我们可以写a=b=1;赋值运算符=的运算顺序是从右至左,所以实际返回的是左操作数,即我们重载函数时也要返回左操作数,即*this。
接下来我们来实现赋值运算符重载:
在类中再加一个成员函数:
Date& operator=(const Date& x)
{
if (this != &x)
{
_year = x._year;
_month = x._month;
_day = x._day;
}
return *this;
}
关于第⑥点:
与拷贝构造函数非常类似,如果未涉及到资源管理,赋值运算符重载是否实现都可以,一旦涉及到资源管理就必须自己实现。