目录
6个默认成员函数介绍
构造函数
构造函数是什么?
构造函数的6种特性
析构函数
析构函数是什么?
析构函数的特性
拷贝构造函数
什么是拷贝构造函数
拷贝函数的特性
四.默认生成的拷贝构造实行的是浅拷贝(值拷贝)(重点)
赋值操作符重载
运算符重载:
赋值运算符重载(默认成员函数)
赋值运算符的tips
const和普通取地址操作符重载
6个默认成员函数介绍
class Date {
}; //空类
之前有说到如果一个类什么都没有是一个空类,但是空类为什么和只有函数的类一样只有1字节的占位符?这就说明了空类只是看着是空,其实其中大有乾坤。
顾名思义默认成员函数,那就表示了这些函数如果你没写就自动生成,当然你也可以自己写。
构造函数
构造函数是什么?
名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
下面的代码是一个有关日期的类:
class Date
{
public:
Date(int year = 0, 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;
};
注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。(开空间是创建类对象时进行的,构造函数是在空间开好后进行的)
构造函数的6种特性
一.构造函数的函数名和类名相同
二.构造参数没返回值
不是void!!!!!!,而是什么返回值都没有,可以这样理解,构造函数是构造这个对象。可以想象成返回的就是一个实例(默认的而不是)
三.对象实例化时对象自动调用对应的构造函数
创建一个对象时会自动调用一个适合的构造函数 (对应四)
四.构造函数可以重载
重载就是可以有同名函数只要参数不同即可(不同的参数,用不同的构造)
五.无参的构造函数和全缺省的构造函数以及不写构造函数都是默认构造函数,默认构造函数只有一份。(三选一)
默认构造函数只能有1份。但四说的重载是指的除了默认构造函数。
六.如果类中没有写出构造函数,则自动生成一个无参的构造函数并使用,如果写了则不生成
那这样不是都用默认的就好了,这样固然方便,但是初始化的值是随机值。
析构函数
析构函数是什么?
与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
默认析构函数可以释放局部资源的空间,但是如果在函数内有动态开辟的new的空间,则需要程序员自己写delete释放对应的空间。
析构函数的特性
一.析构函数名和构造函数名类似,在前面+'~'
class Date
{
public:
Date()// 构造函数
{}
~Date()// 析构函数
{}
private:
int _year;
int _month;
int _day;
};
二 .无返回值 无参数。
与构造函数不同的是析构函数不能有参数。因为不需要参数。
三.使用对象的生命周期结束,自动调用
int main()//main 函数的生命周期在return 后 结束
{
Date d1(1, 1, 1);//创造d1变量
return 0;
}
main函数在return后生命周期结束,d1自动调用析构函数。
四.一个类有且只有一个析构函数,如果没写会自动生成默认析构函数。
因为析构函数没有参数,所以实现不了重载和缺省。所以析构函数只有一个。显示写出来则用写出来的,如果没写则用自动生成的。
五.类似栈的构造析构调用
构造时把对象放入栈,析构时根据弹出顺序析构
拷贝构造函数
什么是拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)// 拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 31);
Date d2(d1); // 用已存在的对象d1创建对象d2
return 0;
}
由上述代码看出,拷贝构造函数和类名相同,参数是类对象的引用。顾名思义就是用另一个对象初始化当前对象。
拷贝函数的特性
一.拷贝函数是构造函数的重载
和构造函数只有参数不同,所以是一个重载
二.拷贝函数的参数只有一个并且必须是引用参数
Date(const Date d)// 拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
函数传参时,如果没有限定是指针或者引用,那就是传值传参,对于这类传参,系统会先对传入的参数进行一个拷贝,然后拷贝出来的这一份复制体在函数中使用,对于拷贝函数而言,如果传值传参在传入参数时它又要拷贝一遍,这个拷贝调用的还是拷贝构造函数,这样就陷入了死循环包出错的。
tips:传值传指针传引用详解(之后加上)
三.未显示定义则会默认生成
默认成员函数:如果没有就用默认的
四.默认生成的拷贝构造实行的是浅拷贝(值拷贝)(重点)
深拷贝和浅拷贝:这样的情况通常在于成员变量中存在指针和用自己开辟空间的时候。对于普通的内置类型变量(int char等等)浅拷贝和深拷贝没区别,内置类型在栈上开辟空间,而new的空间是在堆上。
shallow copy浅拷贝:由图可知我们浅拷贝时对应 的新对象和老对象指向的node是一样的。说明了浅拷贝就是把老对象的东西从根本上挖过来,属于是两人一起用。
class ShallowCopyExample {
private:
int* data;
public:
ShallowCopyExample(int value) {
data = new int(value);
}
// 浅拷贝构造函数(默认实现)
ShallowCopyExample(const ShallowCopyExample& other) {
data = other.data; // 浅拷贝,只是复制了指针
}
~ShallowCopyExample() {
delete data;
};
上述代码也表示了因为是引用传参所以复制出来的data指针是相同的。
deep copy深拷贝:则是重新拷贝出来一份新的。一对双胞胎,老大买了一份吃的,老二就也要买一份一模一样的。两人用的是相同的,但不是一起用,这就是深拷贝。
class DeepCopyExample {
private:
int* data;
public:
DeepCopyExample(int value) {
data = new int(value);
}
// 深拷贝构造函数
DeepCopyExample(const DeepCopyExample& other) {
data = new int(*other.data); // 分配新内存并复制值
}
~DeepCopyExample() {
delete data;
}
};
上述代码中,和浅拷贝不同的点在于有指针的情况下, 我们new了一个新的int。这样空间就是新的,二者用的就是不一样的空间。
两种形式可以按需使用,但如果有自己使用new等函数开辟的空间时一定要使用深拷贝,如果是浅拷贝因为大伙用的是一片空间在free时会出现多次free的情况。
赋值操作符重载
运算符重载:
之前对于两个自定义类型要做比较时,都是做一个函数,然后调用这个函数来进行比较就像strcmp
运算符重载就是为了让自定义类型也能够通过 == + - 等等运算符来进行比较和运算。
d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)
运算符重载函数长什么样子:和普通函数类似。只是函数名要注意:operator + 符号构成函数名
注意事项:
1.不能新建符号,自定义类型有的才能创建比如 operator~,没有这个用法
2.重载操作符要操作的对象至少要存在一个自定义类型
3.本身作用于内置类型(int,char等等)的操作符,重载过后效果不变
4.如果操作符重载放在类内,那也是作为成员函数也会有this指针。
5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。
以operator==为例:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
bool operator==(const Date& d)// 运算符重载函数
{
return this._year == d._year
&&this._month == d._month
&&this._day == d._day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
return d1._year == d2._year
&&d1._month == d2._month
&&d1._day == d2._day;
}
在类内时参数只用传一个作为右比较数,因为还有一个this指针指向左比较数。
赋值运算符重载(默认成员函数)
和其他的运算符重载不同,赋值运算符是默认的。所以不写的话会由系统生成。
Date& operator=(const Date& d)// 赋值运算符重载函数
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符的tips
1.参数引用,并且加上const。
参数引用,如果不是引用而是传值的话,会在调用一次拷贝构造,造成多余损耗。
但因为是引用的参数,我们是要让右边对象(参数)的值赋值给左边的对象(this指针),所以参数+const,可以防止右边的对象在使用时被更改,提高安全性。
2.返回值用引用
返回值引用,因为这是一个函数,最终还是需要一个返回值,对于赋值而言,我们把参数的值全都复制了一遍,那现在这个this就是一个d的复制品,所以返回*this,就等于是把复制体返回了。如果传值返回,和参数引用一样会再拷贝一份,损耗变高
综上12条用引用都是为了降低损耗。
3.本体判断
如果传入的参数的地址 == this,那还赋值啥直接返回就可以了。
4.如果没写赋值重载,则使用系统生成的浅拷贝赋值重载
和拷贝构造一样,如果我们对象中包含自己new的空间或者指针,使用浅拷贝会让对象指向一块内存。
比较两份代码:
Date d1(2021, 6, 1);
Date d2(d1);
Date d3 = d1;
对于d2而言 就是纯正的拷贝构造函数。d3看似是赋值。但是拷贝和赋值都有一定的前提
拷贝构造:是用已存在的对象的值拷贝给一个新的对象。 老对新
赋值重载:是用已存在的对象的值赋值给一个存在的对象。老对老
const和普通取地址操作符重载
class Date
{
public:
Date* operator&()// 取地址操作符重载
{
return this;
}
const Date* operator&()const// const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
也是用operator加符号当做函数名。但一般取地址重载不用自己写,系统默认的即可,因为 取地址返回的是一个地址,返回this指针即可。因为取地址不会出现浅拷贝,因为没有涉及值的赋值,所以没有风险。
前面的const是保证传出去的值的内容不被更改,函数后面的const是保证成员变量在这个函数不能被更改。