目录
一、类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为 默认成员函数 。⼀个类,我们不写的情况下编译器会 默认生成 以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个后面再说。
默认成员函数很重要,也很复杂,我们要从两个方面去学习:
• 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
• 第⼆:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
二、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是 对象实例化时初始化对象 。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
构造函数的特点(先大致看一下文字,后面会有代码辅助理解):
- 函数名与类名相同 。
- 无返回值 。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会 自动调用 对应的构造函数。
- 构造函数可以 重载 。
- 如果类中没有显式定义构造函数,则C++编译器会 自动生成 ⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做 默认构造函数 。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多人会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是 不传实参就可以调⽤的构造就叫默认构造 。
- 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们在类和对象(下)再来探讨。
大家可以结合代码来理解,基本上以上的内容在代码中都有体现。
cpp
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
// 构造函数
// 无返回值,函数名与类名相同
// 1.无参构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
// 2.带参构造函数(构造函数可以重载)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.半缺省构造函数
//Date(int year, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//} // 这里一定要注意函数重载的规则,参数不能都一样,这里是举例是想说明构造函数也可以使用缺省参数
// 4.全缺省构造函数
//Date(int year = 1900, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
// 其中1、4以及不自己定义而编译器自动生成的构造函数,不需要传递参数,称为默认构造
// 程序员自己定义构造函数后,编译器就不会自己生成
// 默认构造只能存在一个,所以这里把4注释掉了
// Init函数可以被构造函数替代,并且Init函数不会自动执行,显然构造函数更好一点
//void Init(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 d1;
// 对象实例化时系统会自动调用对应的构造函数
// 为什么不是Date d1(); ?
// 因为这样写就无法区分这是函数调用还是声明,大家记住是这样用就好
// 手动写的两个默认构造没有全部注释掉,会调用手动写的默认构造
// 都注释掉,会调用编译器生成的默认构造,结果未知,一般是随机值,取决于编译器
d1.Print();
// 运行结果是1900/1/1
Date d2(2024, 7, 24);
//这里直接调用了传参的构造函数,没有默认构造的事情
d2.Print();
// 运行结果是2024/7/24
return 0;
}
另外提一下,如果把默认构造的代码屏蔽掉,只写了一个需要传参的构造函数,编译器就不会生成默认构造函数,那么这样的写法就是有问题的,又因为编译器的默认构造不太好用,所以我们还是最好自己写出需要的默认构造和传参构造。
那么是否每个类我们都需要自己写构造函数?
不是的,我们上面提到,对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。也就是说一个类里面放着的都是自定义类型,那么这个类的默认构造就会调用类里面自定义类型实例化时的默认构造函数,如果类里面的自定义类型没有默认构造就会报错。
比如说用两个栈来实现队列:
cpp
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
// 这里我们只需要写出Stack的默认构造,下面MyQueue在实例化时就会自动调用Stack的默认构造
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ......各种操作省略
private:
STDataType * _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语⾔提供的原生数据类型,
如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
三、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中 资源的清理释放工作 。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点(与构造函数有相通之处,后面也会给出代码来辅助理解):
- 析构 函数名 是在类名前加上字符 ~。
- 无参数无返回值 。 (这里跟构造类似,也不需要加void)
- ⼀个类 只能有⼀个析 构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会 自动调用 析构函数。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理, 自定类型成员会调用他的析构函数 。
- 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说 ⾃定义类型成员无论什么情况都会自动调用析构函数 。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是 有资源申请 时,⼀定要 自己写析构 ,否则会造成资源泄漏,如Stack。
- ⼀个局部域的多个对象,C++规定后定义的先析构 ,这个用于判断对象的析构顺序
结合代码辅助一下理解:
cpp
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
// 析构函数
// 无参数无返回值,函数名为~加上类名
~Stack()
{
cout << "~Stack()" << endl;// 这里打印是为了在控制台显示出析构函数被调用过,正常情况下不用写
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType * _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
// 编译器默认生成MyQueue的析构函数调用了Stack的析构,完成了空间释放
// 就算这里写了MyQueue的析构函数,编译器也会自动调用Stack的析构
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
运行结果是调用了两次Stack的析构,不难理解,这两次是对pushst和popst的分别析构
对比一下之前的C语言代码,我们可以发现我们再也不用害怕忘记写Init和Destroy函数了,因为根本不用写,有构造函数和析构函数确实方便了很多。
四、拷贝构造函数
普通的构造函数是给对象默认或者自己指定的值,那么能不能在不知道一个类对象的各种值把它的值直接拷贝给另一个对象呢?
可以。
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做 拷贝构造函数 ,也就是说拷贝构造是⼀个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的⼀个 重载 。
- 拷贝构造函数的 参数只有⼀个 且必须是 类类型对象的引用 ,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。
- C++规定 自定义类型对象进行传值拷贝行为必须调用拷贝构造 ,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 若未显式定义拷贝构造,编译器会 自动生成 拷贝构造函数。自动生成的拷贝构造对内置类型成
员变量会完成 值拷贝/浅拷贝 (⼀个字节⼀个字节的拷贝),对 自定义类型 成员变量会调用他的拷贝构
造。 - 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是 _a指向了资源 ,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就
需要显示写拷贝构造,否则就不需要。 - 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个 野引用 ,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要 确保返回对象在当前函数结束后还在 ,才能用传引用返回。
结合代码来进行理解:
cpp
#include <iostream>
using namespace std;
//日期类
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)// 参数只有一个且必须为类类型对象的引用,加const是为了防止d被修改,造成错误拷贝
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 这个不是拷贝构造函数,这是一个普通的构造重载,传指针,一般不用
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 取地址并打印日期
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
// 返回野引用的情况举例
Date& Func2()
{
Date tmp(2024, 7, 24);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2024, 7, 24);
// C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造
// 所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以减少这里的拷贝
Func1(d1);
cout << &d1 << endl;
// 这里可以完成拷贝,但是不是拷贝构造,只是一个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针
Date d3(d1);
d3.Print();
// 也可以这样写,这里也是拷贝构造
Date d4 = d1;
d2.Print();
// Func2返回了⼀个局部对象tmp的引用作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引用
Date ret = Func2();
ret.Print();
return 0;
}
为什么拷贝构造只能传引用?
因为C++规定对于自定义类型的拷贝行为必须调用拷贝构造,如果拷贝构造传值,那么传值的过程中还会调用拷贝构造再次传值,结果就是永远都在调用拷贝构造和传值,无限递归下去,不能完成拷贝构造,程序就会崩溃。
cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
// 需要对_a指向资源创建同样大的资源再拷贝值
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);//空间拷贝
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显示实现拷贝构造,用编译器自动生成的拷贝构造完成值拷贝/浅拷贝
// 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃
// 其实不止有析构两次的问题,由于是两者共用同一片空间,还有数据覆盖...
// 所以有资源申请的自定义类型,都要自己实现深拷贝
Stack st2 = st1;
//Stack st2(st1); 都可以
MyQueue mq1;
// MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造完成pushst/popst的拷贝
// 只要Stack拷贝构造自己实现了深拷贝,那就没问题
MyQueue mq2 = mq1;
return 0;
}
五、运算符重载
1.基本知识
(别慌,这些在后面的代码基本上都有体现)
- 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
- 运算符重载是具有特殊名字的函数,他的名字是由 operator和后面要定义的运算符 共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
- 重载运算符函数的 参数个数 和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给 隐式的this指针 ,因此运算符重载作为成员函数时,参数比运算对象少⼀个。
- 运算符重载以后,其 优先级和结合性 与对应的内置类型运算符保持⼀致。
- 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@(错误) 。
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
- 重载操作符 至少有⼀个类类型参数 ,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)。
下面的代码用来解释 .* 操作符和运算符重载的形参条件(7,8):
cpp
#include<iostream>
using namespace std;
// 编译报错:"operator +"必须至少有一个类类型的形参,一定要注意运算符重载必须有类类型形参
//int operator+(int x, int y)
//{
// return x - y;
//}
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
typedef void(A::* PF)(); //成员函数指针类型 返回类型(*函数指针变量名)(参数类型和个数)
int main()
{
// C++规定成员函数要加&才能取到函数指针
PF pf = &A::func;
// 如果我们想调用func函数,怎么调用?
//(*pf)();
// 不对,因为类成员函数有一个特点就是有this指针,而this指针又不能显示传
// 所以可以这样,对象调用成员函数指针时,使用.*运算符
A a;
(a.*pf)();
// 运行结果打印出了A::func(),证明调用成功
return 0;
}
- ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意
义(计算日期间差了几天),但是重载operator+就没有意义。 - 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。
C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
这里是前置++和后置++的重载:
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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 _year == d._year
&& _month == d._month
&& _day == d._day;
}
Date& operator++()
{
cout << "前置++" << endl;
//...
return *this;//返回加之后
}
// C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分
Date operator++(int)
{
Date tmp;
cout << "后置++" << endl;
//...
return tmp;//返回加之前
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 25);
d1.operator++();
// 编译器会转换成 d1.operator++();
++d1;
d1.operator++(0);
d1.operator++(1);
d1.operator++(-1);// 括号里面int类型的值无所谓,但是我们一般给0
// 编译器会转换成 d1.operator++(0);
d1++;
return 0;
}
- 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
这里是流运算符的重载示例:
cpp
//重载流运算符输入和打印日期类
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
// 这个是友元函数声明,现在只需要知道有了这个声明,operator<<函数在全局就可以使用Date的私有成员了
// 这里是为了方便博主才使用的,在类和对象(下)博主会细说
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//void operator<<(ostream& out);// 如果在类里面实现,那么隐式传的this指针会占第一个形参位置
// 我们打印时就只能 d << cout 不符合使用习惯
// 所以我们最好在全局重载流运算符
private:
int _year;
int _month;
int _day;
};
// 打印
ostream& operator<<(ostream& out, const Date& d)// 参数传引用和传引用返回都是为了减少拷贝
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;// 传过来是cout其实返回的也就是cout,这里设置返回值主要是为了可以连续打印,不然就只能一个一个写
}
// 与打印类似
istream& operator>>(istream& in, Date& d)//这里的d千万不要加const!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
{
cout << "请在下方依次输入年月日:" << endl;
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1, d2;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
//程序运行会成功
return 0;
}
- 还是之前的日期类:如果直接将操作符重载为全局,会出现类类型内私有成员无法访问的问题。
一般来说解决这种问题有四种方法,这里只说三种,剩下一种放到类和对象(下)会好好说清楚。
cpp
#include<iostream>
using namespace std;
// 重载为全局的面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有
// 2、Date提供getxxx函数
// 3、友元函数(这里先不讲,放到类和对象下博主会写清楚)
// 4、重载为成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 2.在类里面提供Getxxx函数,把需要得到的成员变量值作为返回值返回,在全局重载函数内部接收
// 其实写起来有点不方便
int Getyear() const// 这里后面为什么加const? 是为了防止在该函数里面误修改成员变量,会造成权限放大
// 因为后面的全局重载函数参数有const,这里与那里相对应,const修饰成员函数后面一点我会细说
{
return _year;
}
int Getmonth() const
{
return _month;
}
int Getday() const
{
return _day;
}
private: // 1.直接把需要的成员放成公有,不建议
int _year;
int _month;
int _day;
};
// 直接将操作符重载为全局,会出现类类型内私有成员无法访问的问题
bool operator==(const Date& d1, const Date& d2)
{
return d1.Getyear() == d2.Getyear()
&& d1.Getmonth() == d2.Getmonth()
&& d1.Getday() == d2.Getday();
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显示调用
operator==(d1, d2);
// 编译器会转换成 operator == (d1, d2);
d1 == d2;
return 0;
}
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 4.不重载为全局函数,重载为成员函数
bool operator==(const Date& d2)// 为什么只需要传一个d2呢,因为this指针已经替我们传了d1,其实这里把d2写成d会更好看一点
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
private: // 1.直接把需要的成员放成公有,不建议
int _year;
int _month;
int _day;
};
// 直接将操作符重载为全局,会出现类类型内私有成员无法访问的问题
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1.Getyear() == d2.Getyear()
// && d1.Getmonth() == d2.Getmonth()
// && d1.Getday() == d2.Getday();
//}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显示调用
// 调用方式会略微变化一丢丢
d1.operator==(d2);
// 下面这个不变
// 编译器会转换成 operator == (d1, d2);
d1 == d2;
return 0;
}
2.赋值运算符重载
(这里和拷贝构造很相似,前面拷贝构造学明白这里会很容易理解)
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
赋值运算符重载的特点:
- 赋值运算符重载是⼀个运算符重载,规定 必须重载为成员函数 。赋值运算重载的参数建议写成
const 当前类类型引用,否则会传值传参会有拷贝 - 有 返回值 ,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成⼀个 默认赋值运算符重载 ,默认赋值运算符重载行为为跟默认构造函数类似,对内置类型成员变量会完成 值拷贝/浅拷贝 (⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造。
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是 _a指向了资源 ,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。这里还有⼀个小技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
下面是赋值重载的实现和运用举例:
cpp
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << " 拷贝构造 " << endl;// 打印是为了在运行结果可以看到函数调用
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引用返回减少拷贝
// d1 = d2;
Date& operator=(const Date& d)
{
cout << " 赋值重载 " << endl;// 打印是为了在运行结果可以看到函数调用
// 检查自己给自己赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
// 这样设计是为了可以支持连续赋值
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 25);
Date d2(d1);//拷贝构造
Date d3(2024, 7, 26);
d1 = d3;//赋值重载,因为d1之前已经存在了
Date d4 = d1;//拷贝构造
// 赋值重载完成两个已经存在的对象直接的拷贝赋值
// 而拷贝构造用于一个对象拷贝初始化给另一个要创建的对象
return 0;
}
3.取地址运算符重载
a.const成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
举一下上面用到const修饰成员函数的例子
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 为什么此函数后面要加const?
// 加上const后,this指针由 Date* const this 变为 const Date* const this
// 这样this指针指向的内容便不能被修改
// 我们以后在写不涉及到内容修改的成员函数时,都可以加上const修饰,可以避免不必要的错误
// 这里的this指针变成const Date* const this后,就会与下面全局重载函数的参数const Date& d1呼应
// 这样就避免了权限的放大
// 我们在日常写代码过程中用const修饰不需要修改的函数参数和不需要修改this指针指向内容的成员函数是一种非常好的习惯
int Getyear() const
{
return _year;
}
int Getmonth() const
{
return _month;
}
int Getday() const
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1.Getyear() == d2.Getyear()
&& d1.Getmonth() == d2.Getmonth()
&& d1.Getday() == d2.Getday();
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
operator==(d1, d2);
d1 == d2;
return 0;
}
b.取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非⼀些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。
cpp
// 取地址运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
Date* operator&()
{
//return this;
return nullptr;// 如果不想让别人取到地址,返回一个nullptr或随便其它一个地址就行了
}
const Date* operator&()const //这个是主要适用于const类型的变量取地址,因为上面那个会导致权限放大,编译器会自动选择
{
//return this;
return (const Date*)0x0060fa71;// 道理和上面那个一样,为了后面打印的时候和上面那个区分,博主就随便写了一个地址
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;//这里默认构造会初始化
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
终终终终终于写完了..............................
大家可以试试自己实现一个日期类,里面包含输入输出,差值,比较等等功能,博主会在类和对象(下)最后把代码附上。
点个赞吧〒_〒......