目录
1.拷贝构造函数:
1.1 为什么要引入拷贝构造:
1.2 拷贝构造函数的定义及特性:
1.3 什么类可以不用编写拷贝构造:
2. 赋值运算符重载:
2.1 为社么要引入运算符重载:
2.2运算符重载的定义以及特性:
在前面的文章中,引入了C++中类的概念,对于一个类而言,如果其中不存在成员,则该类被称之为空类。但是空类中并不是不存在任何内容,而是编译器会自动生成以下个默认成员函数,即:用户没有显性显示,编译器会生成的函数。
这个函数大致可以分为以下三类,分别是用于初始化和清理的构造函数和析构函数,用于拷贝赋值的拷贝构造函数和赋值运算符重载,以及
在上一篇文章中,对于用于初始化构造函数的定义其特点进行介绍,即:函数名和类名相同、没有返回值、对象实例化时编译器会自动调用构造函数,并且针对于自定义类型和内置类型的作用不同、可以构成重载。并且介绍了用于清理的析构函数的定义及其特点,即:函数名是类名之前加~,无参数无返回值类型、一个类中只能由一个析构函数(所以析构函数不能构成重载)并且在未显性显示的情况下,编译器会自动生成析构函数、编译器会自动调用析构函数。
在本文中,将继续介绍默认成员函数中的其他函数:
1.拷贝构造函数:
1.1 为什么要引入拷贝构造:
在正式介绍拷贝构造函数的定义以及性质之前,需要先说明为什么要引入拷贝构造,为了解释此问题,首先提及数据结构中,对于函数的传参方式。例如在栈中,向各个功能函数传递栈这个数据结构的参数时,一般采用传址调用而非传值调用,这是因为传址调用在速度和大小方面都优于传值调用。但是,这并不意味着传值调用不可以使用,例如在下面的代码中:
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int year = 2023, int month = 11, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void func(Date dd)
{
cout << "func(Date dd)" << endl;
dd.Print();
}
int main()
{
//类的实例化
Date d1;
func(d1);
return 0;
}
运行结果如下:
不难发现,向函数传递参数时,并没有传递指针或者采用引用,而是直接将类作为参数传递。对于这种直接传值的方式,可以称为浅拷贝或者值拷贝。对于上述代码所给出的日期类,浅拷贝并不会造成程序的错误。
但是在不同的情况下,浅拷贝可能会造成程序的错误,例如上篇文章中提到的栈:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
_array = nullptr;
_capacity = 0;
_top = 0;
}
private:
DataType* _array;
int _capacity;
int _top;
};
为了方便演示,这里专门创建一个函数用于检测,具体如下:
void func2(Stack s1)
{
//......
}
int main()
{
//类的实例化
Date d1;
func(d1);
Stack s;
func(s);
return 0;
}
运行上述程序,编译器会显示错误:
通过对上述日期类和栈类的调用,会发现,在日期类进行传值调用或者说进行浅拷贝时,并不会出现错误,而对于栈这个类则会报错。导致两者不同的原因,就在于栈这个类中,有一个成员变量是指针_。 对于传值调用,是直接将变量的值进行传递,对于指针也不例外,通过监视窗口,可以观察和中指针_的地址。
通过图片不难发现,再向函数传递参数时,直接将对象作为参数传递, 因此,对象中的成员变量的值也传给了形参。所以,和中的指针_指向同一块地址,具体可以有下面的图片表示:
在上一篇文章及文章开头,提及了析构函数的一个特点:对象生命周期结束时,会自动调用析构函数。因此,当函数调用结束后,此时对象的生命周期结束,因此,析构函数会清理对象中指针_指向的空间。
当函数运行结束后,当主函数运行结束时,此时对象的生命周期结束,编译器会再次调用析构函数,清理对象中指针_指向的空间。上面提到,两个对象中的指针指向了同一块空间,因此,本次清理时,会造成错误,因为指针_指向的空间被清理了两次。
在C++中,为了解决浅拷贝这种方式在上述情况下会引起错误的问题,因此,C++规定自定义对象在进行拷贝时,需要调用拷贝构造函数
1.2 拷贝构造函数的定义及特性:
拷贝构造函数的定义如下:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
为了便于解释特性中,为什么采用传值方式会引发无穷递归,文章首先给定下面一个构造函数:
Date da(d1);
//拷贝构造函数:
Date(Date dd)
{
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
在采用上述拷贝方式的情况下,首先传递参数,由于C++规定自定义类型传值或者值拷贝需要调用拷贝构造,因此在第一次传参后,并没有直接去调用拷贝构造,而是编译器额外新生成一个拷贝构造函数,并且去调用新生成的拷贝构造函。为了调用拷贝构造函数,首先需要传递参数,但是在传递参数时,又会生成一个新的拷贝构造函数。。。。。。因此会引发无限递归。
在特性中提到,拷贝构造函数的参数只有一个,并且必须是引用的方式,即:
//拷贝构造函数:
Date(Date& dd)
{
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
在这种情况下,再次运行上面的代码,就不会造成无穷递归,具体原理如下:
void func(Date d3)
{
cout << "func(Date dd)" << endl;
d3.Print();
}
func(d1);
在调用函数时,首先需要传递参数,此时传递参数的方式为传值拷贝,因为,会调用拷贝构造函数,由于拷贝构造函数的参数是类型对象的引用,因此参数就是的别名,此时的指针指向,所以,在拷贝构造函数对日期类进行赋值时,通过指针,直接将对象的成员变量赋值给指针指向的对象的成员变量,完成赋值。
并且,由于拷贝构造函数的参数是类型对象的引用,不是传值调用,所以,在向拷贝构造函数传递参数时,不会引发无穷递归(同理,传递指针也可以避免无穷递归)。
在基本了解了拷贝构造函数的定义以及特性后,可以利用拷贝构造函数来解决上面栈类的问题,即:开辟的空间会被释放两次。解决问题的方法就是通过拷贝构造函数来实现深拷贝,即在拷贝时不只拷贝值,还将被拷贝对象的资源一起进行拷贝。代码如下:
Stack s2(s);
//拷贝构造函数:
Stack(Stack& stt)
{
_array =(int*)malloc(sizeof(int)*stt._capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, stt._array, sizeof(int) * stt._capacity);
_capacity = stt._capacity;
_top = stt._top;
}
对于上述代码,指针指向对象,是对象的引用,所以,根据深拷贝,需要将被拷贝对象的资源一起拷贝的原则,对对象中的指针_再开辟一块空间,大小和对象中的指针指向的空间大小相同,但是两块空间的地址不同,即:
由于两块空间的地址不同,因此,不会出现析构函数将同一块空间释放两次的情况。
1.3 什么类可以不用编写拷贝构造:
针对这个问题,可以通过一个例子进行说明:
首先,将日期类中的拷贝构造删除,即:
class Date
{
public:
//构造函数
Date(int year = 2023, int month = 11, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void func(Date d3)
{
cout << "func(Date dd)" << endl;
d3.Print();
}
int main()
{
//类的实例化
Date d1;
func(d1);
return 0;
}
运行上述程序,通过监视窗口观察对象
不难发现,即使没有人为编写拷贝构造函数,两个对象依然完成了值拷贝。这是因为拷贝构造函数属于默认成员函数,在没有人为编写的情况下,针对内置类型会自动完成值拷贝。针对自定义类型会去调用此类型的拷贝构造,如果没有人为编写或者显性显示的拷贝构造,则编译器会自动生成。
例如,对于下面的自定义类型,类中并没有人为给出拷贝构造函数
int main()
{
violent p1;
violent p2(p1);
return 0;
}
class violent
{
Stack pp1;
Stack pp2;
int size;
};
此时运行程序,通过监视窗口观察类中的成员变量
可以发现,主函数中对象中的成员变量都被进行了拷贝,并且还进行了深拷贝。由于成员的类型是,因此编译器自动调用了成员相对类型的拷贝函数,这一点,可以通过下面的代码进行验证。
即在拷贝构造函数的开头加上一行打印,如果编译器会自动调用成员相对类型的拷贝函数,即调用中的拷贝函数。则会打印一次。
//拷贝构造函数:
Stack(Stack& stt)
{
cout << "Stack(Stack& stt)" << endl;
_array =(int*)malloc(sizeof(int)*stt._capacity);
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, stt._array, sizeof(int) * stt._capacity);
_capacity = stt._capacity;
_top = stt._top;
}
运行结果如下:
通过上面的例子,不难看出, 针对和这两种类,使用编译器默认生成的拷贝函数即可。不过二者有稍有差别。因为中所有成员的类型都是内置类型,编译器默认生成的拷贝构造函数来完成值拷贝已经满足了的需求。针对这种类的成员变量的类型是自定义类型,需要调用该成员的拷贝构造函数,即。种已经存在了人为编写的拷贝构造函数,编译器直接调用即可。
2. 赋值运算符重载:
2.1 为社么要引入运算符重载:
在C++中,针对内置类型的变量,可以通过等运算符来判断他们之间的关系。但是针对类这种这种较为复杂的类型,却不能通过运算符来判断他们之间的大小关系,例如:
bool ret = d1 > d2;
在C++中,如果需要使用运算符来判断类之间的关系,需要利用函数来完成,即:
bool Compare(Date x, Date y)
{
return x._year == y._year &&
x._month == y._month &&
x._day == y._day;
}
bool Comparebig(Date x, Date y)
{
if (x._year > y._year)
{
return true;
}
else if (x._year == y._year && x._month > y._month)
{
return true;
}
else if (x._year == y._year && x._month == y._month && x._day > y._day)
{
return true;
}
else
{
return false;
}
}
不过由于不同用户的使用及命名习惯不同,会导致函数的函数名可读性及规范性差。因此,在C++中,为了规范性以及可读性,引入了运算符重载
2.2运算符重载的定义以及特性:
定义如下:运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。函数原型:返回值类型 operator操作符(参数列表)
例如,将上面给出的用于比较两个类较大的函数进行改写:
bool operator>(Date x, Date y)
{
if (x._year > y._year)
{
return true;
}
else if (x._year == y._year && x._month > y._month)
{
return true;
}
else if (x._year == y._year && x._month == y._month && x._day > y._day)
{
return true;
}
else
{
return false;
}
}
判断两个类是否相等的函数改写为:
bool operator==(Date x, Date y)
{
return x._year == y._year &&
x._month == y._month &&
x._day == y._day;
}
虽然利用关键字 规范了函数名的书写方式后,使得代码的可读性变高,但是在接收函数判断的结果时,例如:
bool ret = operator>(d1, d2);
bool ret1 = operator==(d1, d2);
cout << operator>(d1, d2) << endl;
cout << operator==(d1, d2) << endl;
代码的可读性仍然不高,因此,C++在此时再次进行了优化,即:
bool ret = d1 > d2;
bool ret1 = d1 == d2;
cout << (d1 > d2) << endl;
cout << (d1 == d2) << endl;
在这种情况下,编译器会去寻找,代码中是否存在相应的函数,即:,,如果存在则会自动调用,不存在则会报错。
虽然代码的可读性再一次提高,但是针对上述函数依旧存在两个问题:
1. 调用日期类的成员变量时,需要将类的访问限定符由改为。
2. 函数的参数在传参时,由于传递的参数是自定义类型,并且传参的方式是传值(浅拷贝),因此需要调用拷贝构造函数。
针对问题一,只需要将函数都放在类种便可以解决,针对第二个问题,将函数的传参方式由传值拷贝改为传引用即可,即:
class Date
{
public:
//构造函数
Date(int year = 2023, int month = 11, int day = 21)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数:
Date(Date& dd)
{
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator==(Date& x, Date& y)
{
return x._year == y._year &&
x._month == y._month &&
x._day == y._day;
}
bool operator>(Date& x, Date& y)
{
if (x._year > y._year)
{
return true;
}
else if (x._year == y._year && x._month > y._month)
{
return true;
}
else if (x._year == y._year && x._month == y._month && x._day > y._day)
{
return true;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
不过此时运行代码,编译器会显示如下错误:
这是因为,对于成员函数的参数,都会有一个隐藏的参数,即指针,因此,需要将上述函数的参数改为:
bool operator==(Date& y)
bool operator>(Date& y)
再进行函数的调用时,即
bool ret = d1 > d2;
bool ret1 = d1 == d2;
编译器会自动将上述调用的形式进行转换,转换为:
bool ret = d1 > d2;
//d1.operator>(&d1,d2)
bool ret1 = d1 == d2;
//d1.operaotr(&d1,d2);
对于上述形式,可以理解为,函数内部的参数由两个,一个是指向的指针,另一个则是上述函数中传递的参数& 。
在函数调用时,也可以用上述方式进行调用,即:
bool ret3 = d1.operator>(d2);
因此,对于上述函数,其正确写法为:
bool operator==(Date& y)
{
return _year == y._year &&
_month == y._month &&
_day == y._day;
}
bool operator>(Date& y)
{
if (_year > y._year)
{
return true;
}
else if (_year == y._year && _month > y._month)
{
return true;
}
else if (_year == y._year && _month == y._month && _day > y._day)
{
return true;
}
else
{
return false;
}
}
此时,两个函数内部均有两个参数,即上面所说的传递的参数和一个指向的指针。编译器会通过指针自动完成函数的整个运行过程。