1、引入
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
意思是:你不写,编译器自动帮你写一个,你写了,编译器就不写了
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
显式实现 就是自己写
2、构造函数(自动的 Init函数:约等于初始化)
2.1 前言:
在我们 平时写程序时,可能会出现一种情况,定义一个变量,对着这个变量操作半天,一运行程序,发现程序崩溃了,原来是因为该变量没有初始化
这个忘记初始化的操作,是很正常的事,C++之父本贾明 也会出这个Bug,因此,他就在C++ 中加上了 构造函数,通过编译器自动帮我们初始化(这样就不会“忘记”了 doge)
这个“构造函数“ ,类似于 Init函数,(但这个不是真正意义上的初始化,暂且当作初始化)
至于为什么叫做 ”构造“,没有为什么,祖师爷喜欢(doge)
2.2 概念
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << '\n';
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
d1.Init(2022, 1, 11); // 实例化一个对象后,要自己设置一个日期数据,初始化对象
d1.Print();
return 0;
}
对于Date类,如果每次创建对象时都调用 Init 函数设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
这就要用到 构造函数,具体往下看
2.3 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
其特征如下:
1、函数名与类名相同。
2、无返回值。 (不用写 void,看下面的演示图)
3、对象实例化时编译器自动调用对应的构造函数。
4、构造函数可以重载。 (注意演示图)
可以重载的使用是说:可以设计几种构造函数,供你有选择性的使用
2.4 构造函数重载的例子:
写一个无参构造函数 和 一个 带参构造函数
使用方式:
想要不改变原来的数值(原来的数值可能是随机值):就不传参,调用 无参构造函数就好
若想要更改数值:对象后面括号写入形参,调用 带参构造函数
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数:即想要初始化为什么数据,就直接括号写在实例化对象后面
注意事项:无参构造函数创建对象 后面跟 括号会变成 函数声明
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
为什么会成了函数声明?
答:Date 可看作类型,d3 可看作 函数名字,看下面例子演示,就知道原因了
Date d3();
int fun(); // 例子
完整的构造函数 重载演示代码
// 构造函数
class Date {
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数:即想要初始化为什么数据,就直接括号写在实例化对象后面
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
2.5 全缺省和无参函数构成歧义
重载构造函数写多少个都行,满足重载规则就好
也可以加入 缺省参数
通常情况下,写一个带有全缺省的函数就够了
下面两个函数构成重载,但是无参调用时会产生歧义
例如:
因此,一般不写无参构造函数,全缺省可以直接替代无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
2.6 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。**
意思是:你不写,编译器自动帮你写一个,你写了,编译器就不写了
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
2.7 什么是默认构造函数
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,默认构造函数只能有一个。 都可以认为是默认构造函数。
总结成一句:不传参数就可以调用的函数就是默认构造函数
一定要结合第 7 点,就是上面这一点:理解默认构造函数是什么!! 才能方便理解下面第 8 点
2.8 编译器默认生成的构造函数 会给成员变量赋什么值?
解答:C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类型,如:int / char…,
自定义类型就是我们使用 class / struct / union 等自己定义的类型,
自己运行下面的程序,就会发现编译器生成默认的构造函数会对自定义类型成员_t 调用的它的默认成员函数:即控制台窗口会输出 语句 “Time()” ,同时通过调试可知,基本数据类型 _year / _month / _day 都是随机值
总结:内置类型默认生成构造函数(处理后一般变为随机值或者可能是 0 (有些编译器自己优化了这个”缺陷“)),自定义类型调用默认构造函数
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意点一:
注意一:自定义类型 调用默认构造函数,若没有默认构造函数,则会报错
(注意:默认构造函数有三种,前面第 7 点)
演示一种情况:显式的写一个带参构造函数(非缺省)(这个构造函数就不属于默认构造函数,则这里报错)
注意点二:
注意二:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
这个也是一种缺省值
class Time {
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private: // 基本类型(内置类型) :可以直接先初始化
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main() {
Date d;
return 0;
}
在类里面这些是 声明,不是定义(定义就代表开空间,这里并没有开空间) 因此,给类型 “初始化” 了一个值:这个是 缺省值
有了缺省值,在没有显式构造函数时,可以不用调用 默认的构造函数(就不会被初始化成随机值了)
另外,类里面的变量类型,是先初始化成 缺省值,再看有没有构造函数
例如下面这段代码:_ year / _ month / _day 是先初始化成 1970/1/1 再初始化成 2024/5/2
可以自己调试看一下(这里暂不演示)
class Date
{
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d(2024, 5, 2);
return 0;
}
注意点三:
注意三:当 自定义类型调用默认构造函数是编译器自动生成的构造函数 时,会出现 ”套娃现象“(这一点的内核也是帮助理清前面的知识)
想想可以套3层、4层….多层吗? 可以的
通过这个套娃现象,我们可以思考出:所有自定义类型最终都要回到内置类型(内置类型是本源),因为总要有变量类型要初始化
总结
总结:有了构造函数,不用自己写 Init函数,不用自己调用,编译器自动完成初始化操作
1、一般情况下构造函数需要我们自己显式的写出来(编译器内部自动生成的一般为随机值)
2、只有少部分情况会让编译器自动生成构造函数
3、析构函数(自动的 Destroy函数)
3.1 前言
这个函数刚好和 构造函数的功能相反
这个函数相当于 平时写的 Destroy函数,这个和 Init初始化函数一样,比较容易忘记写
包括 祖师爷本贾尼 自己也可能忘记:自己淋过雨,然后给孩子们撑起了一把伞,发明了 析构函数
注意:Init初始化函数不写会报错,Destroy函数不写可大概率不会报错,从而极易导致 内存泄漏等问题,危害较大,却内存泄漏不容易被直接检测出来
C++最怕内存泄漏了(doge)
3.2 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作(一般有开辟空间、连接了数据库(关闭连接的链接)…这种资源就需要清理工作,才需要析构函数)。
(好好体会这句话,重新梳理一下:完全销毁对象包括 自己本身的销毁 和 对象中的资源销毁,对象会调用析构函数 完成对象中资源的清理工作,而本身的销毁是编译器完成的)
3.3 特性
析构函数是特殊的成员函数,其特征如下:
1、析构函数名是在类名前加上字符 ~。 (这个是取反符号,即在构造函数的结构下,在函数名前加上 ~ )
2、无参数无返回值类型。 (不写 void)
3、一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4、对象生命周期结束时,C++编译系统系统自动调用析构函数。
什么叫做生命周期结束?
全局变量在 main 函数结束时销毁
局部变量在 函数结束时销毁:可以是 main 函数,外部定义的函数….
// 析构函数的作用:free指针、恢复top等变量原有值.....等资源清理工作
~Date()
{
free(_a);
_a = nullptr;
top = 0;
}
5、下面的程序我们会看到,编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private: // 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
解析:
程序运行结束后输出:~Time()
问题:在main函数中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
回答:main函数中创建了Date 对象d,而 d 中包含4个成员变量,其中_year, _month, _day 三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
而 _t 是 Time类对象,所以在 d 销毁时,要将其内部包含的 Time类的_t 对象销毁,所以先要调用 Time类的析构函数。
但是:main函数中不是直接调用 Time类的析构函数
编译器会先调用Date类的析构函数(而Date没有显式提供析构函数,则编译器会给Date类生成一个默认的析构函数),这个Date析构函数的作用:调用自定义类型的析构函数 ,这里就调用Time 类的析构函数
总结:当没有显式析构函数时,编译器会给当前这个类生成一个默认的析构函数,这个析构函数会 调用当前这个类中所有自定义类型的各自析构函数,用于销毁所有自定义类型
3.4 实践中总结出来:
1、有资源需要显式清理,需要写析构函数:例如 List 链表类(你每一个节点要自己 free)
2、有两种场景不需要显式写析构函数,默认生成的就可以了
a、没有资源需要清理,如:日期类 Date
b、内置类型成员没有资源需要清理,剩下的都是自定义成员,如:下面代码中的 MyQueue
(第 b 点怎么理解:编译器生成的默认析构函数,对内置函数不做处理,对自定类型成员调用它的析构函数,这样使整个类达成析构的目的。)
第 b 点是最为常见的,也是项目中经常使用到的一点
class Stack {
// .....
};
class MyQueue {
// 自定义类型
Stack st1;
Stack st2;
// 内置类型
int top = 1; // 没有资源要清理,其他的都是 自定义类型了
};
int main()
{
return 0;
}
3.5 在底层,多个对象的析构的顺序如何?
这里是栈的存储:后进先出,则析构也是后进先出
后定义的先析构
int main() {
A a1;
A a2;
return 0;
}
这里就是 先析构 a2 再析构 a1
3.6 演示 构造函数 和 析构函数 被自动调用:
class Date {
public:
// 构造函数
Date()
{
cout << "自动调用构造函数:Date" << '\n'; // 打印出来为了演示:自动调用构造函数
_a = (int*)malloc(sizeof(int) * 4);
top = 100;
}
// 析构函数
~Date()
{
cout << "自动调用析构函数:~Date" << '\n'; // 打印出来为了演示:自动调用析构函数
free(_a);
_a = nullptr;
top = 0;
}
private:
int* _a;
int top;
};
int main()
{
Date d;
return 0;
}
4、拷贝构造函数(也是构造函数)
这个是构造函数的一个细分,因为较为特殊,因此单独讨论
构造函数的本质是初始化某个值
拷贝构造是将一个对象的值拷贝到另一个同类型对象里,即拷贝初始化
4.1 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
4.1.1 拷贝构造函数是构造函数的一个重载形式。
4.1.2 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
class Date
{
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(Date& d) {
// 将对象d的数值赋给 本对象的类型,实现拷贝
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
**
演示拷贝构造的使用:
class Date
{
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(Date& d) {
// 将对象d的数值赋给 本对象的类型,实现拷贝
cout << "Date(Date& d)" << '\n'; // 这里的打印是为了方便看有没有调用到这个拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
// 打印函数:给 func函数调用
void Print()
{
cout << _year << "/" << _month << "/" << _day << "\n";
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2024, 5, 1);
Date d2(d1); // 将 d1 拷贝给 d2 :调用拷贝构造函数
d2.Print(); // 打印d2 里面的日期值
return 0;
}
结果:拷贝成功
拷贝构造函数的参数必须是引用不写引用会引发无穷递归调用
// 拷贝构造函数
Date(Date& d) {
// 将对象d的数值赋给 本对象的类型,实现拷贝
_year = d._year;
_month = d._month;
_day = d._day;
}
4.2 为什么不写引用会引发无穷递归调用?
1、先理解下面这条:
调用函数func前得先传参,内置类型传值传参,需要拷贝,自定义类型如对象传值传参,也要拷贝(调用相应的拷贝构造函数)
下面的代码举例:要调用 func函数,先传对象 d, 而 Date func(Date d) 该函数使用一个类对象来接收,就需要调用对象相应的 拷贝构造函数
重点部分:
void func(Date d) {
d.Print();
}
int main()
{
Date d(2024, 5, 1);
func(d);
return 0;
}
完整代码:
class Date
{
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(Date& d) {
// 将对象d的数值赋给 本对象的类型,实现拷贝
cout << "Date(Date& d)" << '\n'; // 这里的打印是为了方便看有没有调用到这个拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
// 打印函数:给 func函数调用
void Print()
{
cout << "Print()" << '\n'; // 这里的打印是为了显示该函数被调用
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
void func(Date d) {
d.Print();
}
int main()
{
Date d(2024, 5, 1);
func(d);
return 0;
}
这段代码运行的结果是:先调用拷贝构造函数再调用打印函数
2、再讲为什么会无穷递归
// 拷贝构造函数
Date(Date d) { // 错误写法:不写引用
_year = d._year;
_month = d._month;
_day = d._day;
}
// ......
int main()
{
Date d1(2024, 5, 1);
Date d2(d1);
return 0;
}
对象 d1 作为参数传递到拷贝构造函数 ,用 (Date d) 接收,而这个 d 不是引用,根据前面第 1 点的知识,这个 属于传值传参,d 需要调用 拷贝构造函数 进行拷贝,而调用的那个 拷贝构造函数 自己也需要 调用拷贝构造函数 ……. 就这样嵌套循环下去
这样就形成了 无穷的递归(还是只有递没有归,回不来的死龟(doge))
无穷递归的根源是 拷贝构造函数的参数直接是一个 类类型,使得这个了类类型接收参数时需要调用拷贝构造
有没有方法可以不用调用拷贝构造?
写成 引用 或 指针 :本质是直接传址或传对象本体过来,不用进行拷贝 (理解本质就好)
4.3 拷贝构造函数的参数为什么要加 const?
正常书写的拷贝构造函数是:
Date(Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
有一种可能会发生的错误写法:赋值对象写反了
使自己没有成功拷贝给别人,反倒被别人反噬了
Date(Date& d) {
d._year = _year;
d._month = _month;
d._day = _day;
}
为了避免这种情况:可以加上 const ,让编译器报错提醒你
因为 Date d 只是用于拷贝给你,不涉及修改,则这里缩小权限是合理的
4.4 拷贝构造的两种写法
Date d2(d1);
Date d3 = d1;
4.5 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意一个特别点:认真理解
你自己显式写了一般的构造函数(不是拷贝构造函数),编译器还是会自动生成 默认拷贝构造函数
但如果显式写了拷贝构造函数,编译器 不会自动生成 默认构造函数,同时会报错(因为拷贝构造函数
也是构造函数的一种,自动生成的默认构造函数生成的前提是你自己没有显式的写构造函数)
4.5.1 使用生成默认的拷贝构造函数的浅拷贝有什么坏处?
浅拷贝是 按内存存储按字节序一点点顺着拷贝下来的
// C语言实现的数组栈
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2 = st1; // 将 st1 拷贝给 st2
将 st1 拷贝给 st2 此时编译器会报错:其中根源是 浅拷贝将 数组栈中的 int* a 也直接拷贝了,这样就使两个栈指向了同一块空间,而前面讲解过对象会自动调用析构函数(指针 a 需要 free),当main函数结束时,对象 st1 和 st2 会调用各自的析构函数,从而导致 指针 a free 了两次 -----> 同一块空间 free 两次,会报错
因此,当类中只有内置类型的,直接用默认的拷贝构造即可, 而像栈这样有指针这种浅拷贝风险的就需要自己显式的写拷贝构造函数 -------> 深拷贝(即自己平时会写的 Destory函数)
深拷贝:自己显式的写拷贝构造函数
class Stack {
public:
// 数组栈的 深拷贝
Stack(const Stack& st) {
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL) {
perror("mallco fail !");
return;
}
_capacity = st._capacity;
_size = st._size;
}
private:
int* _a;
int _capacity = 4;
int _size = 0;
};
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的(浅拷贝),而自定义类型是调用其拷贝构造函数完成拷贝的。
这个执行流程和析构函数很像
总结一下:
1、若没有管理资源,一般情况无需写拷贝构造,使用默认的就可以了,如日期类Date
2、若内置类型没有指向资源,剩下的都是自定义类型,也是使用默认的就可以,如 MyQueue
3、一般情况下,无需写析构函数的 类,一般也不用写 拷贝构造函数
4、如果内部有指针或一些值指向资源(如指向某块空间),就需要 显式写析构释放,通常也需要显式写构造完成深拷贝:如 Stack,Queue, List……
总结:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。
(为什么说使用引用 可以提高程序效率?:引用相当于 传指针,不用拷贝)
5、运算符重载
5.1.概念引入
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
5.2.注意事项
-
不能通过连接其他符号来创建新的操作符:比如operator@ (即重载的符号 需要是运算符,而不能随便指定一个符号)
-
重载操作符必须有一个类类型参数
即重载函数必须要有一个参数要和类相关的,不能没有,例如错误例子: int operator<(int a, int b)
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
即 + 的含义是 相加,你不能改变成 相减的意思
其实也不是不能,在实践中其实是可以改变含义的,不会报错,万一你就是有什么特殊的需求呢,这一点其实是考虑了规范性和可读性
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
-
注意以下5个运算符不能重载。这个经常在笔试选择题中出 现。
-
-
域限定符: 【 :: 】
-
三目操作符:【 ? : 】
-
点操作符:【 . 】
-
sizeof
-
点星操作符:【 .* 】
比较少见,记住它不能重载就好
用法如下:
class A { public: void func() { // .. } int *p; }; // 定义函数指针类型:void (*)() 指针名写在里面 typedef void (A::* Ptr)(); int main() { Ptr p = &A::func; // 取类A 中的函数func 的指针:需要加上取地址符& // 定义一个 类A的对象 A tmp; // 两种方式调用成员函数 tmp.func(); // 直接 点操作符调用 (tmp.*p)(); // 通过函数指针+解引用+点操作符 调用 :星操作相当于对这个函数指针解引用,然后点操作符调用成员函数 tmp.(*p); // 这种写法不存在:没有这种写法,必须要用 点星操作符 /* 因此,【.* 点星操作符】的作用是,通过函数指针让对象调用相应的成员函数 */ return 0; }
-
为什么自定义类型需要运算符重载,而内置类型不需要(也不能)?
因为内置类型的所有东西都被计算机给明确规定好了,不需要也不能修改其规定
而自定义类型是你自己定义的,其本身的 内部结构和大小比较等规则,可以由你自己定义,例如两个自定义类型比较大小,编译器是不知道你自己是如何定义的,需要你自己规定好规则,即通过运算符重载
普通函数:函数名就是地址
成员函数:要在前面加上一个取地址符 &(规定)
运算符重载例子:使用运算符重载函数的两种方式
operator==(d1, d2) // 显式调用
d1 == d2 // 转换调用
class Date
{
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
//private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
// 重载双等号:判断两个日期对象是否相等
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(1999, 9, 9);
Date d2(2024, 7, 8); // 将 d1 拷贝给 d2 :调用拷贝构造函数
// 显式调用运算符重载函数:和正常的函数调用相同
// 但是这样子使用就不能体现运算符重载函数的便利性了,因此一般使用第二种方式(下面)
bool flag = operator==(d1, d2);
if (flag) cout << "相等" << '\n';
else cout << "不相等" << '\n';
// 或者 直接使用重载的函数的重载运算符
if (d1 == d2) cout << "相等" << '\n';
else cout << "不相等" << '\n';
return 0;
}
在汇编底层两者等价的,都是调用这个运算重载函数
有没有发现我把类的成员变量 的 private 给注释掉了
因为需要 把成员变量变成公有的,否则外部的运算符重载函数无法访问私有的成员函数
有没有其他的解决办法
(因为成员变量一般都要写成私有的,不会把它变成公有的)
1、提供成员的 get 和 set(Java中尤其喜欢用):通过公共 get 函数访问私有的成员
2、友元函数:可以访问对象的私有成员(后面会讲解)
3、将运算符重载函数写为成员函数(C++常用)
写成成员函数示例
class Date
{
public:
// 构造函数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
// 重载双等号:判断两个日期对象是否相等
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(1999, 9, 9);
Date d2(2024, 7, 8); // 将 d1 拷贝给 d2 :调用拷贝构造函数
// 显式调用
d2.operator==(d1);
// 转换调用
(d1 == d2);
return 0;
}
5.3.赋值运算符重载
就是 将 等于号 重载了,使一个 已经存在的对象赋值给 另一个对象
5.3.1 注意区分 [ 赋值运算符重载 ] 和 [ 构造函数的初始化 ]
分清楚 对象当前的状态:是 刚刚创建出来,还是 已经创建出来
5.3.2 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率 (T 为数据类型)
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要符合连续赋值的含义
// 赋值运算符重载:以日期类 Date举例
Date& operator=(const Date& d)
5.3.3 什么叫做:有返回值的目的是为了支持连续赋值 ?
举一个连续赋值的例子:
赋值表达式的返回值是左操作数,为了连续赋值
int i, j, k;
i = j = k = 0;
0 先赋值给变量 k
左操作数 k 返回, 赋值给 j
左操作数 j 返回, 再赋值给 i
因此,我们赋值运算符重载 同理,则需要写返回值,返回左操作数,即对应 this指针所指对象
赋值运算符重载标准写法:
// 赋值运算符重载
Date& operator=(const Date& d) {
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
结合代码再分析一下:
// 对象d2 赋值给 d1
d1 = d2
d2 传到函数中(即别名 d ),赋值给 this 所指的对象 d1
最后 return *this 表示将 d1 return(即将左操作数 d1 返回)
5.3.4 为什么 函数返回类型 的是 Date& (引用),不是 Date ?两者有什么区别?
看这个博客:较为详细的解释了
【C++修炼之路 第二章】类和对象学习 中,函数传值返回 和 传引用返回 有什么区别?
5.3.5 赋值运算符只能重载成 类的成员函数 不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
即 你自己不写在类里面,而是写在类外,编译器还是会生成一个默认的,此时就冲突了
5.3.6 默认赋值运算符重载函数是 值拷贝(赋值重载函数的本质还是 类的默认函数)
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
值拷贝就要考虑某些风险了 , 这里 前面的【拷贝构造函数】处有讲过
5.3.7 自己给自己赋值的情况
某些情况下,可能会出现 自己给自己赋值的 情况(doge)
可以在函数中加入 if 的判断,以免进行无谓的赋值(提升一下效率:因为赋值的过程可能非常长,若自己还要给自己赋值,一定程度影响了效率)
// 赋值运算符重载
Date& operator=(const Date& d) {
if (this != &d) // 先判断是否相等
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
6. const 修饰对象 和 修饰成员函数
由于本文篇幅过长,这个知识点请点击这个博客学习:
【C++修炼之路 第二章】类和对象:const 修饰对象与 const 修饰成员函数 及其之间的调用关系(权限的放大与缩小)
7. 取地址及const取地址操作符重载
这两个函数一般不用自己写,没什么意义,编译器自己生成的就已经达到目的了
这两个函数就是为了形成学习知识的逻辑闭环
class Date {
public:
// 取地址及const取地址操作符重载
Date* operator&() {
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
或者说:你想做一些坏事,不想让这个类型的地址被取到(doge)或者 想让别人获取到指定的内容!
防御性编程+1(doge)
class Date
{
public :
Date* operator&()
{
return nullptr; //做坏事,返回空指针
}
const Date* operator&()const
{
return 0xffffff; // 或者给你一个假地址
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};