深入计算机语言之C++:类与对象(中)
🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:从C语言到C++语言的渐深学习
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、默认成员函数
如果一个类中什么成员都没有,我们称之为 "空类" 。但是空类中真的什么都没有吗?答案是否定的!
类有六个默认成员函数,默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
默认成员函数很重要,也⽐较复杂,我们要从两个⽅去学习:
- 我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
二、构造函数
2.1 为什么要有构造函数
为了能够更好地讲解,我们来写一个简单的日期类,通过日期类来讲解!
#include <iostream>
using namespace std;class Date {
public:void SetDate(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(void)
{Date d1;d1.SetDate(2022, 3, 8);d1.Print();Date d2;d2.SetDate(2022, 3, 12);d2.Print();return 0;
}
对于 Date 类,我们可以通过我们写的成员函数 SetDate 给对象设置内容。但是每次创建对象都要调用这个 SetDate ,是不是感觉太麻烦了?
❓ 那有没有什么办法能在创建对象时,自动将我们要传递的内容放置进去呢?
那就要用到我们构造函数了
2.2 定义
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值(对象实例化时初始化对象),并且在对象整个生命周期内只调用一次。其特点如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
构造函数是特殊的成员函数,主要任务是初始化,而不是开空间(虽然构造函数的名字叫构造)
2.3 构造函数的使用
- 构造函数的功能就相当于我们之前书写的初始化函数(Init),但由于其自动调用的特性,大大提升了代码的容错率。
构造函数分为三种:不带参数的,带参数的,全缺省的
我们先来看看前两种的运行结果:
#include<iostream>
using namespace std;class Date
{
public:// 1.⽆参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 2. 带参数构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 3.全缺省构造函数//Date(int year = 1, 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;
};int main()
{Date d1;//不带参数构造d1.print();Date d2(2024,10,11);//带参数构造d2.print();return 0;
}
🔑 解读:不给参数时就会调用 无参构造函数,给参数则会调用 带参构造函数。
用起来很简单,但也有很多需要注意的地方:
📌 注意事项:
- 构造函数是特殊的,不是常规的成员函数,不能直接调 d1.date();
- 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。
2.4 默认构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
那什么是默认构造,有的同学会误以为不定义构造函数,系统自己生成的就叫默认构造,其实不然:
- 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
- 这三个函数有且只有⼀个存在,不能同时存在。
总结:不传实参就可以调⽤的构造就叫默认构造
📌 注意事项:
全缺省和无参的不能同时使用,虽然构成函数重载,但会造成调用歧义。
🔑 解读:无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,虽然语法上允许它们们两个可以同时存在,但是如果有对象定义去调用就会报错。
并且同时,有了全缺省的构造函数,就不需要使用带参数的和不带参数的构造函数,强烈推荐实现全缺省或者半缺省,因为真的很好用:
2.5 默认构造函数的特性
通过刚才的讲解我们知道如果你没有自己定义构造函数,C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。
那我们就让编译器自己生成一个试试:
#include<iostream>
using namespace std;class Date
{
public:void print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1; // 这里调用的是默认生成的无参的构造函数d1.print();return 0;
}
在我们不是先构造函数的情况下,编译器生成的默认构造函数。
"似乎这看起来没有什么鸟用啊,这不就是一堆随机值嘛……"
d1 对象调用了编译器生成的默认函数,但 d1 对象 year / month / day 依旧是随机值
🔑 解答:C++ 把类型分成内置类型(基本类型)和自定义类型。
- 内置类型就是语法已经定义好的类型:如 int / char...
- 自定义类型就是我们使用 class / struct / union / stack 自己定义的类型。
C++ 规定:我们不写编译器默认生成构造函数对于内置类型的成员变量不做初始化处理。
class A
{
public:A(){cout << "hello" << endl;}
private:int _a;
};class Date
{
public:void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:A b;int _year;int _month;int _day;
};
int main()
{Date d;d.Print();return 0;
}
上图中编译器对于class 类型的 b 进行了初始化(自定义类型(class) b 的初始化就是调用 b 的构造函数),而对于内置类型就没有进行初始化
而如果自定义类型 b 没有默认构造函数就会报错
"你要写就写好了,要么就别写,不写我默认生成的能保底"
三、析构函数
通过前面构造函数的学习,我们知道了一个对象是怎么来的
那一个对象又是怎么没的呢?既然构造函数的本质是初始化,那清理的工作交给谁来干呢?那就要交给析构函数了。
3.1 定义
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象是存在栈帧的,函数结束栈帧销毁。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,类似我们之前Stack实现的Destroy功能。其特点与构造函数类似,如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(但常常不能满足我们的需要)。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
3.2 析构函数的使用
我们知道了,如果没写析构函数编译器会自动生成一个。那默认生成的析构函数会做什么事情呢?它会帮我们 destroy 嘛?
哪有这种好事,不能什么都帮你做啊!
我们刚才在构造函数中讲了:
📌 如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:
- 对于 "内置类型" 的成员变量:不会做初始化处理。
- 对于 "自定义类型" 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
而我们的析构函数也是这样的,也与之类似!
📌 如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:
- 对于 "内置类型" 的成员变量:不作处理 (不会帮你清理的.)
- 对于 "自定义类型" 的成员变量:会调用它对应的析构函数 (虽然大多数时候并没有什么用,但已经仁至义尽了) 。
" 编译器:哈哈哈,给你默认生成个用用就不错了,你都懒得写了,不要挑三拣四滴!"
难道就不能帮我把这些事都干了吗?帮我都销毁掉不就好了?不不不,举个最简单的例子,迭代器,析构的时候是不释放的,因为不需要他来管,所以默认不对内置类型处理是正常的,万一误杀了怎么办,对吧。
有人可能又要说了,这么一来默认生成的析构函数不就没有用了吗?
有用!他对内置类型的成员类型不作处理,会在一些情况下非常的有用!比如说: 两个栈实现一个队列,用 C++ 可以非常的爽。
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://自动调用构造函数和析构函数private:Stack pushst;Stack popst;
};
注意:⼀个局部域的多个对象,C++规定后定义的先析构,参考上述代码即:先析构 _top ,后析构 _capacity,与栈的规则类似(栈先进后出)。
需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员(下列代码中的Stack)无论什么情况(哪怕对MyQueue进行析构)都会自动调用析构函数(~Stack),防止造成严重的内存泄漏问题。至于其他资源,比如指针(STDataType* _a)等需要我们自己进行释放(在~Stack中写的free)。
如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
四、拷贝构造
4.1 定义
我们在创建对象的时候,能不能创建一个与某一个对象一模一样的新对象呢?当然可以,这时我们就可以用拷贝构造函数。
📚 拷贝构造函数:拷贝构造函数是构造函数的一种重载形式,它可以用来创建一个与已存在的对象一模一样的新对象。对于拷贝构造,它只有单个形参,且该形参必须是对本类类型对象的引用,因为要引用,所以一般要加const修饰
拷贝构造也是一个特殊的成员函数,所以他符合构造函数的一些特性::
- 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 拷贝构造函数的参数只有一个,并且 必须要使用引用传参。
- 使用传值方式会引发无穷递归调用!
"拷贝构造函数的引用是必不可少的!"
4.2 拷贝构造的使用
💬拷贝构造的代码示例如下:
class Date {
public:Date(int year = 0, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}/* Date d2(d1); */Date(const 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;
};int main(void)
{Date d1(2022, 3, 9);Date d2(d1); // 拷贝复制//Date d2 = d1; 和 Date d2(d1); 的效果是一样的// 看看拷贝成功没d1.Print();d2.Print();return 0;
}
其中 Date d2(d1); 和 Date d2 = d1; 达成的效果是完全一样的,可以选择自己喜欢的写法
4.3 为什么要使用引用传参
❓ 拷贝构造的时候直接使用 Date d 来传值传参可以吗?
编译器会直接报错
但这是为什么呢?
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
……
一直在传参这里出不去了,一直在进行自己调用自己,所以这里是一个无穷递归。
如上图所示,执行date d2(d1); d1传参给拷贝构造的形参d,即需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参,那么现在的对象只有d1了,所以会出现 date d(d1),而拷贝的过程中又会调用自身的拷贝构造函数,传值方式会继续传进一个A的对象作为实参,会无休止的递归下去。
而加上引用,则形参d是d1的别名,就不需要继续进行拷贝构造。
❓ 拷贝构造函数加 const:如果函数内不需要改变,建议把 const 也给它加上!
万一你不小心写反了怎么办?
/* Date d2(d1); */
Date(Date& d) {d._year = _year;d._month = _month;d._day = _day;
}
这样会产生一个很诡异的问题,这一个可以被编译出来的 BUG ,结果会变为随机值。所以,这里加一个 const 就安全多了,这些错误就会被检查出来了。
4.4 默认生成的拷贝构造
📚 默认生成拷贝构造:
- 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去),也叫做浅拷贝。
- 自定义类型成员,会再调用它的拷贝构造。
#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(Date& d) {// _year = d._year;// _month = d._month;// _day = d._day;// }void Print() {printf("%d-%d-%d\n", _year, _month, _day);} private:int _year;int _month;int _day;
};int main(void)
{Date d1(2002, 4, 8);// 拷贝复制Date d2(d1);// 没有写拷贝构造,但是也拷贝成功了d1.Print();d2.Print();return 0;
}
🔑 他这和之前几个不同了,这个他还真给我解决了。
所以为什么要写拷贝构造?写他有什么意义?没有什么意义。
默认生成的一般情况下就够用了!当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的,来查看如下代码:
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);//默认拷贝构造return 0;
}
这段程序会发生崩溃,让我们来调试以下看看什么情况
我们会发现s1和s2的成员变量_array都指向同一块空间,也就说编译器调用系统生成的默认构造时,把s1._array指向的地址也拷贝给了s2._array,这看起来没什么,等到程序结束完成时,调用析构函数会出现大问题!
程序即将结束时,调用析构函数,会先析构s2,第一次析构完成后,s2._array指向的空间会被释放,其他置为0,也就是等价于s1._array指向的空间也被释放(同一块空间),此时再执行s1的析构函数时,导致他们指向的空间被析构两次,导致程序崩溃然而问题不止这些……
其实这里的字节序拷贝是浅拷贝,下面几章我会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。
🔺 总结:对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于自定义类型中需要资源申请时,都需要手动写拷贝构造,默认生成的拷贝构造不能用。
五、赋值运算符重载
5.1 运算符重载
5.1.1 运算符重载的定义
C++为了增强代码的可读性引入了运算符重载,那么什么是运算符重载呢?
运算符重载是由运算符 operator 定义具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。简单来说就是:能让自定义类型和内置类型一样使用运算符。5.1.2 运算符重载的注意事项
① 不能通过连接其他符号来创建新的操作符, 你只能对已有的运算符进行重载,你也不能对内置类型进行重载。
operator@ ❌
② 重载操作符必须有一个类类型的操作数。
③ 用于内置类型的操作符,其含义不能改变。比如内置的 整型 +,你不能改变其含义。
④ 作为类成员的重载函数时,其形参看起来比操作数数目少 1,成员函数的操作符有一个默认的形参 this,限定为第一个形参。
5.1.2 重载函数的使用
💬 代码演示:运算符重载 ==,下面实现了简单判断日期是否相当的运算符重载:
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;
};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(2024, 1, 1);Date d2(2024, 1, 1);if (d1 == d2)//也可以显示调用operator==(d1,d2);{cout << "日期相等" << endl;}else{cout << "日期不相等" << endl;}return 0;
}
这里会发现运算符重载成全局的,不得不将成员变量是共有的,得把 private 撤掉:
但这种情况下会导致我们想要私有化的内容公开出来
5.1.3 如何保证封装性
❓ 那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决!
当然,我们现在还没有学习到 "友元",可以通过将 operator 重载成成员函数来解决
class Date
{
public:Date(int year, int month, int day){_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;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 1, 1);Date d2(2024, 1, 1);if (d1 == d2)//也可以显示调用operator==(d1,d2);{cout << "日期相等" << endl;}else{cout << "日期不相等" << endl;}return 0;
}
5.1.4 不支持重载的运算符
注意,有五个运算符不支持重载!慎之慎之!
. (点运算符):: (域运算符).* (点星运算符,)?: (条件运算符)sizeof
值得一提的是:虽然点运算符 . 不能重载,但是箭头运算符 -> 是支持重载的,解引用 * 是可以重载的,不能重载的是点星运算符 .*
"网络上不知名的冲浪大神发明的,个人感觉挺不错。"
5.2 赋值运算符重载
5.2.1 定义
赋值运算符重载是将运算符 =
进行运算符重载。但是它相较于其他运算符重载有着自己独特的特点。
需要注意的是,赋值运算符重载也是默认成员函数。
📚作为默认成员函数的 operator 有着如下特点:
- 参数类型:const T& ,传递引用可以提高传参效率。
- 返回值类型:T& ,返回引用可以提高返回的效率,支持连续赋值。
- 检测是否自己给自己赋值。
- 返回* this :要复合连续赋值的值。
5.2.2 赋值运算符重载的使用
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date& operator=(const Date& d)//赋值运算符重载{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};int main(void)
{Date d1(2022, 3, 10);Date d2(2022, 7, 1);d1 = d2;return 0;
}
5.2.3 使用引用可以有效减少拷贝
返回值为什么要用引用?因为出了作用域 *this 还在,所以我们可以使用引用来减少拷贝!
我们先把引用返回去掉:
class Date
{
public:Date(int year = 1900, 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;}Date operator=(const Date& d)//赋值运算符重载{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};int main(void)
{Date d1(2022, 3, 10);Date d2(2022, 7, 1);Date d3(d2); // 拷贝构造d1 = d2;return 0;
}
🚩 运行结果如下:
我们发现,调用了两次拷贝构造函数。
我们来进行调试一下:
① 第一句 "调用了一次拷贝构造" 是因为 Date d3(d2) ,我们自己调用的。
② 第二句则出自 d1 = d2
因为传值返回不会直接返回对象,而是会生成一个拷贝的对象。
我们这里出了作用域,对象还在,就可以使用引用返回:
Date& operator=(const Date& d)//赋值运算符重载
{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;
}
成功减少了拷贝!
我们也就可以得出结论:
- 赋值传参如果不加引用,会先去调用拷贝构造,再进入operator进行赋值,operator本身不像拷贝构造一样无穷递归
- 但operator传参加入引用,就不会先去调用拷贝构造
5.3 实践:日期类的实现
我们学习了上面的知识,来尝试实现一个日期类的代码吧
class Date
{
public:// 获取某年某月的天数int GetMonthDay(int year, int month);// 全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1);// 拷贝构造函数// d2(d1)Date(const Date& d);// 赋值运算符重载// d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d);// 析构函数~Date();// 日期+=天数Date& operator+=(int day);// 日期+天数Date operator+(int day);// 日期-天数Date operator-(int day);// 日期-=天数Date& operator-=(int day);// 前置++Date& operator++();// 后置++Date operator++(int);// 后置--Date operator--(int);// 前置--Date& operator--();// >运算符重载bool operator>(const Date& d);// ==运算符重载bool operator==(const Date& d);// >=运算符重载bool operator >= (const Date& d);// <运算符重载bool operator < (const Date& d);// <=运算符重载bool operator <= (const Date& d);// !=运算符重载bool operator != (const Date& d);// 日期-日期 返回天数int operator-(const Date& d);private:int _year;int _month;int _day;
};
我们获取当月日期的函数经常需要调用,所以我们就将其写在定义里面。
int GetMonthDay(int year, int month)
{assert(month > 0 && month < 13);static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,31, 31, 30, 31, 30, 31 };// 365天 5h +if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year% 400 == 0)){return 29;}else{return monthDayArray[month];}
}
#include"Date.h"bool Date::CheckDate()
{if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)){return false;}else{return true;}
}// 全缺省的构造函数
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "⽇期⾮法" << endl;}
}void Date::Print()
{cout << _year << "-" << _month << "-" << _day << endl;
}
// 拷贝构造函数
// d2(d1)
Date::Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;
}// 析构函数
Date::~Date()
{_year = 0;_month = 0;_day = 0;
}// 日期+=天数
Date& Date::operator+=(int day)
{if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this;
}// 日期+天数
Date Date::operator+(int day)
{Date tmp = *this;tmp += day;return tmp;
}// 日期-天数
Date Date::operator-(int day)
{Date tmp = *this;tmp -= day;return tmp;
}// 日期-=天数
Date& Date::operator-=(int day)
{if (day < 0){return *this += -day;}_day -= day;while (_day < 0){_day += GetMonthDay(_year, _month);_month--;if (_month == 0){_month = 12;_year--;}}return *this;
}// 前置++
Date& Date::operator++()
{*this += 1;return *this;
}// 后置++
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}// 后置--
Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}// 前置--
Date& Date::operator--()
{*this -= 1;return *this;
}// >运算符重载
bool Date::operator>(const Date& d)
{if (_year > d._year){return true;}else if(_year == d._year){if (_month > d._month){return true;}else if(_month == d._month){return _day > d._day;}}return false;
}// ==运算符重载
bool Date::operator==(const Date& d)
{return _year == d._year && _month == d._month && _day == d._day;
}// >=运算符重载
bool Date::operator >= (const Date& d)
{return *this < d || *this == d;}// <运算符重载
bool Date::operator < (const Date& d)
{return !(*this >= d);
}// <=运算符重载
bool Date::operator <= (const Date& d)
{return !(*this > d);return true;
}// !=运算符重载
bool Date::operator != (const Date& d)
{return !(*this == d);
}// 日期-日期 返回天数
int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (max != min){min++;n++;}return n * flag;
}
六、取地址重载
6.1 const 修饰成员函数
首先我们得知道一个规则就是,const修饰的常变量不能赋值给普通变量,因为这样造成const权限的放大,但是普通变量可以赋值给const修饰的常变量。所以让我们来看看这段代码:
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}
private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{const Date d1(2022, 1, 13);d1.Print();//errorreturn 0;
}
这段代码会出错,因为 d2 调用 Print 函数是将 const Date* 作为参数类型传过去,而函数接受的类型是 Date* ,这样就会造成权限的放大。为了解决这个问题,就需要使用const修饰原函数:
void Print() const{cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}
6.2 取地址运算符
取地址运算符和 const取地址运算符重载也是默认成员函数:
#include <iostream>
using namespace std;class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date* operator&(){return this;}const Date* operator&()const{return this;}private:int _year;int _month;int _day;
};
💬这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到你的地址:
#include <iostream>
using namespace std;class Date {
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date* operator&(){// return this; 我不想让你取我的地址return nullptr;}private:int _year;int _month;int _day;
};int main(void)
{Date d1(2022, 2, 2);cout << &d1 << endl;return 0;
}
或者这样:
Date* operator&() {// return this; 我想误导你return 0xFE087460;}
返回一个看似正常的地址,让你的同学调用你的库的时候摸不着头脑,但需要慎用!
相关文章:

深入计算机语言之C++:类与对象(中)
🔑🔑博客主页:阿客不是客 🍓🍓系列专栏:从C语言到C语言的渐深学习 欢迎来到泊舟小课堂 😘博客制作不易欢迎各位👍点赞⭐收藏➕关注 一、默认成员函数 如果一个类中什么成员都没有&…...

51单片机快速入门之 IIC I2C通信
51单片机快速入门之 IIC 总线通信 协议: 空闲时 SCL/SDA 为高电平SCL高时 SDA下降沿 为开始信号开始信号之后: SCL高电平时 SDA不能变化 , SCL低电平时 SDA才可变 SDA 传数据时 从高到低按位传输 SCL一个脉冲高电平对应一位数据 4.SCL高电平时 SDA上升沿 为停止信号 数…...

腾讯推出ima.copilot智能工作台产品 由混元大模型提供技术支持
腾讯公司近期推出了一款名为ima.copilot(简称ima)的智能工作台产品,它由腾讯混元大模型提供技术支持。这款产品旨在通过其会思考的知识库,为用户开启搜读写的新体验。ima.copilot的核心功能包括知识获取、打造专属知识库以及智能写…...
1024是什么日子
【1024程序员日数字编织梦想的赞歌】 在这个由二进制构建的宇宙里,每一行代码都是通往未来的桥梁,每一位程序员都是这浩瀚数字海洋中的航海家。今天,10月24日,不仅是一个简单的日期,它是属于我们的节日——程序员日&a…...
驱动开发系列20 - Linux Graphics Xorg-server 介绍
一: 概述 X.Org Server 是由 X.Org 基金会管理的 X Window System (X11) 显示服务器的自由开源实现。客户端 X Window System 协议的实现以 X11 库的形式存在,这些库作为与 X 服务器通信的有用 API。有两个主要的 X11 库。第一个库是 Xlib,它是最初的 C 语言 X11 API;…...

晶台推出SOP5封装的高速光耦KLM45X,提供1MBit/s超快速率
KLM452 和 KLM453 器件均由一个红外发射二极管与一个高速光电检测晶体管组成,两者之间光学耦合。光电二极管偏置和输出晶体管集电极的独立连接可以通过减少输入晶体管的基极-集电极电容来使速度比传统的光电晶体管耦合器提高几个数量级。它们采用行业内标准的 5 引脚…...

软物质流变探究:从宏观微观差异,到水凝胶界面特性
大家好!今天我们要探讨的是一篇关于纳米级界面水凝胶粘弹性的研究论文——《Nanoscopic Interfacial Hydrogel Viscoelasticity Revealed from Comparison of Macroscopic and Microscopic Rheology》发表于《Nano Letters》,该研究通过比较宏观和微观流…...

Axure中继器单选、多选和重置
亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢! 课程主题:Axure中继器单选、多选和重置 主要内容:根据查询条件,通过单选、多选和重置,从中继器中得到数据 应用场景&…...
微软公司用没有使用证据的商标申请驰名商标,该怎么维权?
收集证据:首先需要收集微软公司商标使用的证据,包括但不限于销售记录、广告宣传材料、市场调查报告等,以证明商标的实际使用情况和知名度。如果微软公司的商标确实没有在市场上使用,或者使用证据不足以证明其商标的知名度…...

学习分布式系统我来助你!【基本知识、基础理论、设计模式、应用场景、工程应用、缓存等全包含!】
基本知识 什么是分布式 分布式系统是一种通过网络连接多个独立计算机节点,共同协作完成任务的系统架构,具有高度的可扩展性、容错性和并发处理能力,广泛应用于大数据处理、云计算、分布式数据库等领域。 通俗来讲:分布式系统就…...

ubuntu查看系统版本命令
查看系统版本指令 在 Ubuntu 操作系统中,您可以使用多个命令来查看系统版本。以下是一些常用的命令: lsb_release -a 这个命令会显示详细的 Ubuntu 版本信息,包括发行版名称、版本号、代号等。lsb_release -acat /etc/os-release 这个命令会显…...

使用yield压平嵌套字典有多简单?
我们经常遇到各种字典套字典的数据,例如: nest_dict {a: 1,b: {c: 2,d: 3,e: {f: 4}},g: {h: 5},i: 6,j: {k: {l: {m: 8}}} } 有没有什么简单的办法,把它压扁,变成: {a: 1,b_c: 2,b_d: 3,b_e_f: 4,g_h: 5,i: 6,j_k_l_…...
express中使用morgan打印请求数据日志文件,按日期分割
使用morgan可以打印日志,但是要分割日志文件就需要使用file-stream-rotator,下面介绍使用方法: 1.安装2个依赖 npm i morgan file-stream-rotator 2.在入口文件app.js中引入相关插件 var express require("express"); var fs require("fs"); var pat…...

干货 | 2024 AI+智慧城市安全解决方案白皮书(免费下载)
导读:新型智慧城市是推动城市治理体系和治理能力现代化、提升城市居民幸 福感和满意度的新理念和新路径,也是网络强国建设和数字经济发展的重要载体。随着 AI 技术的不断发展和在智慧城市智领域广泛的应用,人们享受技 术红利的同时࿰…...
超越 React Query:探索更高效的数据请求策略
我们常常遇到组件间通信的难题。你是否也曾为如何优雅地在组件间传递信息而头疼?今天,我想和大家分享一个让我眼前一亮的解决方案——使用 alova。 跨组件触发请求的挑战 如果你正在构建一个电商应用,用户在更新了购物车后,需要…...

Scala trait
一.trait 基本使用 idea实例 二.实现单个特质 三.实现多个特质 idea实例 四.特质成员的处理方式...
AI大法之C语言哈希表算法比较两个文件去重
最近朋友在工作上遇到了一个问题,经常需要比对两个文件,筛选出文件中不同的订单号。比如有两个文件:计费.txt 和 受理.txt,文件中每一行都是一个订单号,需要找出计费.txt文件中有而受理.txt文件中没有的单号和计费.txt…...
Scala 提取器(Extractor)
Scala 提取器(Extractor) Scala 提取器(Extractor)是一个非常有用的特性,它允许你为任何类型定义自定义的解构赋值语法。在Scala中,提取器是一种用于从对象中提取值的工具,它可以帮助你以一种更直观和声明式的方式处理数据。本文将详细介绍Scala提取器的工作原理、使用场景…...

【主机漏洞扫描常见修复方案】:Tomcat安全(机房对外Web服务扫描)
文章目录 引言I SSL/TLS Not ImplementedTomcat 服务器 SSL 证书安装部署(JKS 格式)Tomcat 服务器 SSL 证书安装部署(PFX 格式)HTTP 自动跳转 HTTPS 的安全配置(可选)修复SSL证书版本低II 主机漏洞扫描常见修复方案Apache JServ protocol serviceSlow HTTP DEnial of Ser…...

MySQL数据库之——事务(Transaction)详解
一、MySQL 事务定义 MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在银行管理系统中,用户张三向李四账户转账的操作,账户转账是一个完整的业务,最小的单元,不可再分,这样,…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论
路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中(图1): mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...