【C++】类和对象——六大默认成员函数
🏖️作者:@malloc不出对象
⛺专栏:C++的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
目录
- 前言
- 一、类的6个默认成员函数
- 二、构造函数
- 2.1 特性
- 三、析构函数
- 3.1 特性
- 四、拷贝构造函数
- 4.1 特性
- 4.2 构造函数与析构函数的调用顺序
- 五、运算符重载
- 六、友元
- 6.1 友元函数
- 6.2 友元类
- 七、赋值运算符重载
- 7.1 赋值运算符与运算符重载的关系
- 7.2 赋值运算符重载 VS 拷贝构造函数
- 八、const成员函数
- 九、取地址及const取地址操作符重载
前言
本篇文章讲解的是类的六大默认成员函数,它们的特性以及注意点非常多,学起来是有一定难度的,但只要我们认真一点相信什么困难都是可以克服的orz~
一、类的6个默认成员函数
Q:如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date {};
下面就让我们一起来了解一下这6个函数的特性吧!!
二、构造函数
概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
下面我们来看看日期类的例子:
#include <iostream>
using namespace std;class Date
{
public: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;d1.Init(2022, 11, 5);d1.Print();Date d2;d2.Init(2022, 11, 6);d2.Print();return 0;
}
对于上面的日期类Date,可以通过Init共有函数对对象设置日期,但如果每次创建对象时都要通过该函数设置信息,未免有点麻烦。那能否在创建对象时,就将信息设置进去呢?为此C++之父就设计出了构造函数来解决这一问题。
2.1 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1.函数名与类名相同。
2.无返回值。
3 对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
注:构造函数的无返回值就是类名后面直接跟形参列表,void不算做无返回值!!!构造函数可以重载的意思就是可以写多个构造函数,提供多种初始化方式。
下面我们就来利用构造函数解决上面日期类出现的问题:
#include <iostream>
using namespace std;
class Date
{
public:// 初始化对象// 无参构造函数Date(){_year = 2023;_month = 1;_day = 1;}// 有参构造函数,与无参构造函数构成函数重载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 d1; // 1.调用无参构造函数,对象实例化时会自动调用对应的构造函数d1.Print();Date d2(2022, 11, 5); // 2.调用有参构造函数 d2.Print();return 0;
}
我们来看看结果:
我们通过调试带大家感受一下这个过程:
通过上述的分析想必大家都知道了构造函数的用法,其实上述的两个构造参数可以合并成一个缺省构造函数,它也是以后我们最推荐的写法,下面我们一起来看看这段代码:
#include <iostream>
using namespace std;
class Date
{
public:// 初始化对象// 无参构造函数Date(){_year = 2023;_month = 1;_day = 1;}// 有参构造函数,与无参构造函数构成函数重载Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 缺省构造函数Date(int year = 2023, 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(2022, 11, 5);d1.Print();Date d2(2022, 11);d2.Print();Date d3(2022);d3.Print();Date d4;d4.Print();Date d5();d5.Print();return 0;
}
首先我想问一下这段代码存在哪些错误?
那么该如何解决这些问题呢?
我们实际上只需要定义一个缺省构造函数就可以解决这些问题了,因为无参和有参构造函数都是与缺省构造函数发生冲突了才产生的错误,我们一起来看看修改后的结果:
在以后的学习过程中,我们也是推荐采用缺省构造函数的方式来初始化对象,这样既可以初始化对象了又很好的利用了缺省参数的特性来方便我们初始化,可谓是一举俩得嘿嘿!!
我们来看看下面一组例子,为什么右图定义一个有参构造函数就发生了报错呢?并且报错信息为没有合适的默认构造函数可用???
首先我们在开头就已经讲到过,任何一个类中都有默认的6大成员函数,它们就是天选之子!!对于构造函数来说,只要发生了对象实例化就一定会调用构造函数,而如果我们没显示的定义构造函数的话就会自动的调用默认构造函数如左图所示,它是没有任何问题的。而对于右图来说只要类中实现了任意一种构造函数,此时编译器就不会自动生成一个默认构造函数了!!!换而言之我在右图中显式的定义了这个构造函数,那么在实例化对象时我就会去默认调用这个构造函数,但右图明显的未调用这个有参构造函数,而是无参构造函数,,所以编译器提示未找到合适的默认构造函数。
总结:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了任意一种构造函数,编译器将不再生成这个默认的无参构造函数,那么在对象实例化时默认调用的构造函数就是用户显式定义的构造函数(其他构造函数都不行)!!!
接下来我想问大家一个问题:在类中没有显式定义构造函数,此时自动生成的无参默认构造函数中的内容是什么???我们来看一个例子:
d对象调用了编译器生成的无参默认构造函数,但是d对象中_year/_month/_day成员变量依旧是随机值,也就说在这里编译器生成的默认构造函数其实并没有什么用??
解答:这其实是C++之父设计的不合理的地方。在C++中把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。默认生成的构造函数对内置类型成员不做处理,而对自定义类型的成员进行处理。对于上述_year、_month以及_day都是内置类型成员,默认构造函数不会对它们进行处理,所以它们打印的是随机值。
注意事项:默认生成的构造函数对内置类型成员不做处理,而对于自定义类型的成员,会去调用它的默认构造函数。默认构造函数:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为是默认构造函数。
接下来我们就来看看默认构造函数对自定义类型的处理:
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;
}
首先我们的对Date类进行了实例化对象,它一定会调用构造函数,在Date类中我们没有显式的定义一个构造函数,所以它会生成一个默认的构造函数,但是它只对自定义类型成员进行处理,我们的_t是Time类的一个实例化对象,此时它又会调用构造函数,Time类中显式的定义了一个无参构造函数,所以会一定会调用这个无参构造函数,结果会打印出Time()。
这个设计在当初本来就是有一点不合理的,于是C++之父在C++11中针对内置类型成员不初始化的缺陷又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
其实通过这些例子我们知道其实到最后使用最多的情况还是自己写构造函数,不用自己定义构造函数的情况还是比较少的。当然了即使这个地方设计的不那么尽如意,大家根据环境的不同来判断要不要自定义写构造函数是最靠谱的。
另外关于构造函数其实还有一点内容没讲完,我放在了下一篇文章给大家讲解。
三、析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.1 特性
学完了构造函数,学习析构函数就非常的轻松了,因为析构函数很多特性是与构造函数相反的!!
析构函数是特殊的成员函数,其特征如下:
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(与默认构造函数的特性相似,都是只对自定义类型成员处理,而不对内置类型成员处理)。 注意:析构函数不能重载。
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
下面我们一起来看看析构函数的例子:
#include <iostream>
#include <assert.h>
using namespace std;class Stack
{
public:// 初始化栈Stack(int capacity = 4){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;}void Push(int x){_a[_size++] = x;}bool Empty(){return _size == 0;}int Top(){return _a[_size - 1];}// 销毁栈~Stack() // 析构函数{cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:// 成员变量int* _a;int _size;int _capacity;
};int main()
{Stack st(4);st.Push(1);st.Push(2);st.Push(3);st.Push(4);return 0;
}
我们知道动态申请的空间在使用完之后需要进行释放,如果不释放会造成内存泄露,有时候我们经常可能会把这项事情忽略掉,那么在C++中我们显式的在类中定义一个析构函数就能很好的解决这个问题,它会在对象声明周期结束时自动调用,这样就达到了空间释放的功能!!妈妈再也不怕出现内存泄露了🙈🙈
我们通过调试让大家清楚整个过程:
关于析构函数的第三点特性析构函数未显式定义时生成默认的析构函数,这里我不再进行测试了,大家下来可以自行去尝试一下与构造函数是差不多的orz~
四、拷贝构造函数
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
答案是可以的,在C++中借助拷贝构造函数可以完成这个任务。拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.1 特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
我们来看一个例子,我们想创建一个新的对象d2它与d1对象内容一样:
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 2023, int month = 2, int day = 25){_year = year;_month = month;_day = day;}Date(Date d) // 将d1对象中的成员变量赋值给d2对象的成员变量,由此便完成了d1对象的拷贝{_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1); // 将要拷贝的对象作为实参进行传递return 0;
}
从上面注释的分析来说理论上这个拷贝构造函数是可行的,但为什么编译器会报错而且会出现无限递归调用拷贝构造函数的情况呢?
首先调用函数是不是要先进行传参操作,这个形参为自定义类型,那么我们知道自定义类型拷贝时需要去调用拷贝构造函数,而拷贝构造函数也是一个函数,它也是先要进行传参的,此时的形参还是为自定义类型,所以又会去调用拷贝构造函数,所以这样就引发了无穷递归调用。
这个地方确实是有点难懂的,但其实把思路理清了是非常好想的一件事,我们可以看看下图的分析:
我们来看一个例子让大家理清函数调用与拷贝构造函数之间的关联:
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 2023, int month = 2, int day = 25){_year = year;_month = month;_day = day;}Date(const Date& d) {cout << "调用了拷贝构造函数" << endl;_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};void Func1(Date d)
{cout << "Func1()" << endl;
}void Func2(Date& d)
{cout << "Func2()" << endl;
}int main()
{Date d1;Func1(d1);Func2(d1);return 0;
}
通过这个例子想必我已经讲清楚了这段代码中函数调用传参与拷贝构造函数之间的关联。
4.2 构造函数与析构函数的调用顺序
下面我们再来看一个比较难的例题,如果这个例题你能很好的消化或者独立做出来,那么对于引用、构造函数、析构函数、拷贝构造函数以及函数调用这部分的理解会更进一步:
#include <iostream>
using namespace std;class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}private:int _year;int _month;int _day;
};Date Test(Date d)
{Date temp(d);return temp;
}
int main()
{Date d1(2022, 1, 13);Test(d1);return 0;
}
构造函数与析构函数的调用顺序:
调用构造函数的顺序:基类(父类)构造函数、对象成员构造函数、派生类本身的构造函数。
调用析构函数的顺序:派生类本身的析构函数、对象成员析构函数、基类(父类)构造函数。
我们来看一组关于全局对象与静态对象的例子,大家想想看答案会是什么?
#include<iostream>
using namespace std;class A
{
public:A(){cout << "A的构造函数" << endl;}~A(){cout << "A的析构函数" << endl;}
};class B
{
public:B(){cout << "B的构造函数" << endl;}~B(){cout << "B的析构函数" << endl;}
};class C
{
public:C(){cout << "C的构造函数" << endl;}~C(){cout << "C的析构函数" << endl;}
};class D
{
public:D(){cout << "D的构造函数" << endl;}~D(){cout << "D的析构函数" << endl;}
};C c;
int main()
{static D d;A a;B b;return 0;
}
通过上图我们发现全局对象比静态局部变量先构造,再其次是构造静态局部对象?还是说跟静态局部对象的位置有关系?
我们发现静态局部对象跟局部成员对象的的构造顺序是一致的,先实例化就先进行构造,但是静态成员的析构不是按照局部成员对象的析构顺序来的,它与全局对象构成栈的关系,先构造的后析构。
下面我们再来看看全局静态对象与全局对象的构造与析构顺序:
从上图我们可以发现,静态全局对象与全局对象的构造析构顺序与定义的先后顺序有关!!
关于构造函数与析构函数的调用顺序有些知识点我们现阶段还未学到,到了后面我们再来进行详谈。
我们只需要记住,在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反:最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。简单的来说,先构造的后析构,后构造的先析构。 我们根据栈的性质(后进先出)来记住是最好不过的!!
拷贝构造函数的典型应用场景:拷贝一份已存在的对象去构造一个新对象、函数参数类型为自定义类类型对象、函数返回值类型为自定义类类型对象。
这道题涉及到的知识点还是很多的,相对来说还是较难的,但是我们只要吸收了对我们的收获是很大的!!实际上这道题如果使用引用作为函数参数类型、做返回值类型的话能减少很多的拷贝工作,所以根据实际场景,能用引用就尽量使用引用。
关于拷贝构造函数它其实还有其他的实现方式,下面我们一起来看看:
#include <iostream>
using namespace std;class Date
{
public:// 缺省构造函数Date(int year = 2023, int month = 2, int day = 25){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d) // 这里我们一般使用const修饰参数,这是为了防止写错赋值对象的情况,例如:写成d._year = _year,这样得出的答案就是随机值了{_year = d._year;_month = d._month;_day = d._day;}// 构造函数Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1);Date d3 = d1; // 写成'='的形式也是可以的,‘=’是一个赋值运算符,后续我们会提到六大默认函数之一赋值重载函数,但在这里用'='就表示拷贝构造,在文章后面我会将两者进行对比Date d4(&d1); // 写成传指针的构造函数之后,我们需要传对象的地址,这样其实是没有写成引用拷贝对象这么直观的并且使用起来也没引用这么方便Date d5 = &d1;return 0;
}
注意:拷贝构造函数的参数写成指针也能实现,但是这个函数就不再是拷贝构造函数了,而是构造函数。拷贝构造函数的引用一般要用const关键字修饰,这样可以避免将两个参数赋值行为写反!!
接下来我们来探究一下拷贝构造函数的第三个特性:为什么自定义类型一定要调用其拷贝构造函数来完成拷贝呢??
class Stack
{
public:// 初始化栈Stack(int capacity = 4){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;}void Push(int x){_a[_size++] = x;}bool Empty(){return _size == 0;}int Top(){return _a[_size - 1];}// 销毁栈~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:// 成员变量int* _a;int _size;int _capacity;
};int main()
{Stack st;st.Push(1);st.Push(2);st.Push(3);st.Push(4);Stack st2(st); // 将st对象拷贝给st2对象,未显式定义拷贝构造函数,默认的拷贝构造函数对象按内存存储按字节序完成拷贝return 0;
}
我们来看看结果:
我们发现程序已经崩溃了,这是为什么?
要想解决这个问题我们只能自己去实现这个拷贝构造函数进行拷贝,之前的内置类型拷贝也被叫做浅/值拷贝,它是按照一字节一字节来进行拷贝的,而对于自定义类型拷贝是去调用它的成员函数拷贝构造/赋值重载的,它也被叫做深拷贝!!!
下面我们就来简单实现一下这个深拷贝,通过上述例子我们知道值拷贝不适用于指向同一块空间的拷贝,那么我们就将两者分别指向不同的独立的空间,这样就不会发生上面的问题了:
Q:什么情况下需要我们实现拷贝构造函数?
如果类中没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,自己实现了析构函数释放空间,则拷贝构造函数是一定要写的,否则就是浅拷贝(按照字节序方式直接进行拷贝)。
我们再来谈一个题外话,为什么内置类型不会出现这种异常的情况呢?
因为内置类型是很简单的类型,编译器是完全能够驾驭它的拷贝方式的,而自定义类型可以认为是多样性的,编译器不能驾驭它的拷贝方式,由此就交给了拷贝构造函数去完成这项任务。
五、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator
后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如
operator@
- 重载操作符必须有一个自定义类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
指针。.*
::
sizeof
?:
.
注意以上5个运算符不能重载。.*
是极少出现的一种运算符重载
为什么会出现运算符重载?以及为什么增强了代码的可读性?
运算符重载的出现就是为了解决自定义类型无法直接使用运算符来进行运算的问题。假设我们用函数来实现各项运算符的功能,但使用函数我们就需要取各种各样的函数名,这样在调用的时候存在名字的规范性问题,而使用运算符重载我们不需要命名函数就可以直接使用运算符了。例如要实现俩个自定义对象的比较,我们使用 d1 < d2即可,这样是不是代码的可读性大大提高了!!!
下面我们就来针对日期类进行各项运算符重载的讲解:
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 2023, int month = 2, int day = 26){_year = year;_month = month;_day = 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._month;
}int main()
{Date d1(2023, 2, 25);Date d2(2023, 2, 26);cout << (d1 == d2) << endl; // 使用运算符会自动转换成去调用对应的运算符重载函数,operator==(d1,d2)operator==(d1, d2); // 显式调用了==运算符重载函数,但是它的可读性不高,这样写和我们自定义一个函数实现==的功能然后去调用这个函数差不多,这就体现不出运算符重载的优势所在了return 0;
}
从上图我们知道operator==
函数就是两个日期类对象的比较运算符重载函数,但是我们可以看到如果我们的operator==
函数是定义在类外面的也就是全局的,我们要想使用Date
类中的成员变量就必须将其放开到公有域中,但是这样写的话C++的封装性就体现不出来了。
这里有几种解决方式,下面我给出最简单的方式就是将operator==
函数写在Date
类中,但是我们不能直接复制粘贴到类中就完事了,我们来看看现象:
为什么这里编译器提示函数参数太多呢?难道我们不是利用两个对象来做比较的吗?
这是因为此时我们的
operator==
函数已经在类中了,它是一个成员函数,既然是成员函数那么参数列表中一定隐含了一个this指针,所以这里我们只需要传递一个对象就好了!!
下图为正确的表示形式:
operator==: 比较两个日期类对象是否相等
bool operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}
operator<: 判断第一个日期类对象是否小于第二个日期类对象
bool operator<(const Date& d)
{return _year < d._year|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);
}
有了上面两个运算符重载函数接下来其他的运算符重载函数都可以进行复用,不用我们一个个的去实现了。
operator<=: 判断第一个日期类对象是否小于或等于第二个日期类对象
bool operator<=(const Date& d)
{// 复用d1对象的operator<以及operator==运算符重载函数return (*this < d) || (*this == d);
}
operator>: 判断第一个日期类对象是否大于第二个日期类对象
bool operator>(const Date& d)
{ // 复用operator<=运算符重载函数return !(*this <= d);
}
operator>=: 判断第一个日期类对象是否大于或等于第二个日期类对象
bool operator>=(const Date& d)
{return !(*this < d);
}
operator!=: 判断第一个日期类对象是否不等于第二个日期类对象
bool operator!=(const Date& d)
{return !(*this == d);
}
operator+=:求第一个日期加上第二个日期得到的日期,返回的是两者加上之后得到的新的日期对象
// 由于每个月的天数是不确定的,所以我们要获取每个月的天数
int GetMonthDay(int year, int month)
{// static修饰数组避免频繁创建static int monthDayArray[13] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return monthDayArray[month];}
}Date& operator+=(int day)
{// 处理 day < 0的情况if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}
+=
运算符会修改变量的值,返回的是加上之后新的日期对象。出了函数的作用域*this
对象还存在,所以我们可以传引用返回日期类对象,这样可以减少拷贝提高程序效率。
operator+:求第一个日期加上第二个日期表示的日期对象,返回的是原来的日期对象
Date operator+(int day)
{Date ret(*this);ret += day;return ret;
}
不能传引用返回,因为ret对象是局部变量出了函数作用域会被销毁。
知道了日期对象+day的写法,对于日期对象-day就很简单了。
operator-=: 求第一个日期减去第二个日期得到的日期,返回的是两者相减之后得到的新的日期对象
Date& operator-=(int day)
{// 处理 day < 0的情况if (day < 0){return *this += -day;}_day -= day;while (_day <= 0){--_month;if (_month == 0){--_year;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;
}
operator-: 求第一个日期减去第二个日期表示的日期对象,返回的是原来的日期对象
Date operator-(int day)
{Date ret(*this);ret -= day;return ret;
}
下面我们来验证一下代码的正确性:
operator++: 前置++与后置++
前置++和后置++都是一元运算符,为了让前置++ 与 后置++ 能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
前置++
Date& operator++()
{*this += 1;return *this;
}
前置++:返回+1之后的新结果。出了函数作用域*this指向的对象还存在,此时我们可以传引用减少拷贝。
后置++
Date operator++(int) // int参数仅仅是为了占位跟前置重载区分,传不传参都无所谓
{Date tmp(*this);*this += 1;return tmp;
}
后置++是先使用后+1,所以需要返回+1之前的旧值。我们先将原值拷贝一份给tmp,*this指向的对象+1之后返回tmp;tmp为临时变量那么不能传引用返回。
注意:在内置类型中前置++与后置++的区别不大,但是在自定义类型中需要使用++时,我们建议使用前置++,因为后置++需要多调用两次拷贝构造函数!!
operator- -: 前置- -与后置- -
前置- -和后置- -跟前置++与后置++是一样的道理,这里我就不再对它们进行解释了。
前置- -
Date operator--()
{*this += 1;return *this;
}
后置–
Date operator--(int)
{Date tmp(*this);*this += 1;return tmp;
}
日期-日期
我们的思路是让一直让小的日期++直到它们两者相等。
int 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 (min != max){n++;min++;}return n * flag;
}
operator<< 与 operator>> : 输入与输出流重载
我们经常见到cout <<
用来输出内容,cin >>
用来输入内容,但你真的明白它实现的机理吗?下面就让我们一起来探究一下吧:
cin
是头文件istream
中的对象,cout
是头文件ostream
的对象;>>
表示流提取运算符,<<
表示流插入运算符。注:istream
与ostream
都是类。
Q:为什么C++要重载流插入、流提取运算符呢?
在C语言中printf其实是只能打印内置类型内容的,之所以能打印结构体成员是因为结构体成员是自由的不受任何限制;对于C++而言结构体/类中的成员变量是私有属性的,所以就要按照C++的语法规定来重载这个运算符。cin与cout为什么能够自动识别类型?就是因为标准库已经将内置类型全部构造完成了,所以它能够直接进行使用,而对于自定义类型来说,编译器并没有帮我们完成,我们需要自己重载流插入、流提取运算符。
题外话:为什么经常有人说printf的效率要比cout稍微高一些?
因为使用cout会涉及大量的函数调用并且C++为了保持与C语言的兼容性,它们的流需要保持同步!!
自定义类型需要运算符重载才能进行使用:
既然编译器没帮我们实现自定义类型的重载,那么同样的我们就自己来构造运算符重载函数!!
operator<<: 流提取日期类对象
// 以下代码放在Date类中
void operator<<(ostream& out)
{out << _year << "年" << _month << "月" << _day << "日" << endl;
}
我们将这段代码放在Date类中作为一个成员函数,但是我们的this指针会默认占据参数的第一个位置,这也就导致了与我们平时使用的习惯相斥使用起来特别的别扭,那么我们该怎么使cout对象作为第一个参数呢?
我们可以试着将它放在全局中进行函数重载,第一个参数位置就为cout
对象了,为了保证它可以连续输出多项内容,所以它的返回值要成ostream
类,我们一起来看看:
虽然成功的完成了任务,但是我们将类中私有成员变量放到了公有域中,这样封装性又不好了。
这里我们有两种方法:一种是定义一个在public中的函数来接收私有成员变量的值,我们直接使用对象调用这个函数就能使用私有属性的成员变量了;另外一种方式是借助友元函数来实现。下面我们讲解第二种方式,同时也顺便把友元的知识一并全部讲完。
六、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类。
6.1 友元函数
友元函数:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。 形象的来讲,就是在类中声明这个函数是友好的,把这个函数当成朋友充分信任它,让它能访问类中的私有成员。
operator>>: 流提取日期对象
实现了operator<<
重载定义这个重载函数就非常简单了,下面我们一起来看看:
istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
6.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。- 友元关系不能传递。
如果C是B的友元, B是A的友元,则不能说明C时A的友元- 友元关系不能继承。在继承位置再给大家详细介绍。
我们来看一个例子:
#include <iostream>class Time
{friend class Date; // 声明日期类为时间类的友元类,则在日期类中就可以直接访问Time类中的私有成员变量
public:Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second){}void Print(){std::cout << _hour << "时" << _minute << "分" << _second << "秒" << std::endl;}private:int _hour;int _minute;int _second;
};class Date
{
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}void SetTimeOfDate(int hour, int minute, int second){// 直接访问时间类私有的成员变量_t._hour = hour;_t._minute = minute;_t._second = second;_t.Print();}void Print(){std::cout << _year <<"年" << _month << "月" << _day << "日" << std::endl;}private:int _year;int _month;int _day;Time _t;
};int main()
{Date d1(2023, 3, 1);d1.Print();Time t1(23, 34, 20);t1.Print();d1.SetTimeOfDate(10, 24, 0);return 0;
}
在Date类中使用_Time类中非公有成员完成了对_Time类非公有成员值的修改!!
关于友元暂时就讲到这里了,后续我们在遇到一些特殊场景再来详谈。
七、赋值运算符重载
赋值运算符重载格式
- 参数类型:
const T&
,传递引用可以提高传参效率- 返回值类型:
T&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值- 检测是否自己给自己赋值
- 返回
*this
:要复合连续赋值的含义
赋值运算符重载既是默认成员函数又是运算符重载!!
函数重载:支持函数名相同,参数不同的函数可以同时使用。
运算符重载:自定义类型对象可以使用运算符。
operator=:赋值运算
// 第一种形式,对于单次赋值
void operator=(const Date& d)
{if (*this != d) // 避免出现同一个对象进行赋值操作,如d1 = d1这种无意义的赋值操作{_year = d._year;_month = d._month;_day = d._day;}
}
我们知道赋值是可以连续复合进行赋值的,例如:在内置类型中可以这样int x1 = x2 = x3,来进行连续赋值,对于赋值运算符来说是从右往左结合的,也就是x2 = x3进行赋值操作之后这里其实会产生一个临时变量,之后再将这个临时变量赋值给x1,,那么我们在实现复合连续赋值重载函数时就需要将对象进行返回,由此就写成了下面的形式:
Date& operator=(const Date& d)
{if (*this != d){_year = d._year;_month = d._month;_day = d._day;}return *this;
}
对于这个日期类对象赋值来说,实际上不需要定义也可以达到目的,因为编译器会默认生成一个赋值重载函数,它与内置类型赋值采取的都是值拷贝的形式,这里未涉及空间资源的申请,所以不会有任何问题产生。
Q:赋值运算符重载到底什么时候需要我们自己去定义呢?
像拷贝构造函数一样,如果该类需要写析构函数,那么就需要写赋值运算符重载函数;如果该类不需要写析构函数,那么就不需要写赋值运算符重载。
我们把赋值运算符写在类外,此时它为一个全局赋值运算符重载函数,我们看看它能不能实现赋值功能:
Date& operator=(Date& d1, Date& d2)
{if (&d1 != &d2){d1._year = d2._year;d1._month = d2._month;d1._day = d2._day;}
}
为什么赋值运算符只能重载成类的成员函数而不能重载成全局函数?
赋值运算符如果不显式实现,编译器会生成一个默认的(因为它是六大默认成员函数之一)。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
在《C++ prime》这本书中也特意提及到了这个语法项:
7.1 赋值运算符与运算符重载的关系
关系:
自定义类型使用运算符都需要重载,除了赋值运算符和&运算符重载(默认六大成员函数)!!!
关于运算符重载这里只讲了一部分,有些特殊一点的运算符(例如:[]它也是运算符)重载还没讲到过,以后碰见了我们再来谈吧!
7.2 赋值运算符重载 VS 拷贝构造函数
我们来看看下面这个例子:
#include <iostream>class Date
{
public:Date(int year = 2003, int month = 10, int day = 24){std::cout << this << ": Date(int, int, int)" << std::endl;_year = year;_month = month;_day = day;}bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}bool operator!=(const Date& d){return !(*this == d);}Date& operator=(const Date& d){std::cout << this << ": Date& operator=(const Date& )" << std::endl;if (*this != d){_year = d._year;_month = d._month;_day = d._day;}return *this;}Date(Date& d){std::cout << this << ": Date(Date& d)" << std:: endl;_year = d._year;_month = d._month;_day = d._day;}void Print(){std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 2, 27);// 构造d1.Print(); Date d2 = d1; //拷贝构造 d2.Print();Date d3(d2); //拷贝构造d3.Print();d3 = d1; //赋值重载d3.Print();Date d4 = d3 = d2; // 赋值重载 + 构造拷贝d4.Print();return 0;
}
从上图我们知道拷贝构造函数会构造出一个新的对象,这个新的对象与拷贝对象的内容一致;赋值运算符重载函数没有新的对象生成,两边使用的都是已经实例化过的对象,将右边对象的内容赋给左边对象,两者内容一致!!
我们还可以通过调试来检测一下我们的推测:
结论:赋值运算符和拷贝构造函数最大区别即是赋值运算符重载没有新的对象生成,而拷贝构造函数会生成新的对象(拷贝构造函数是构造函数的一种特殊重载函数,构造函数和拷贝构造函数一致,都有新的对象生成)。拷贝构造函数是将老对象的数据成员一一赋值给新的对象数据成员的一种构造函数;赋值运算符重载函数是将右边已经存在对象的数据成员一一赋值给左边另一个已经存在对象数据成员的一种操作符重载函数!!
其实说白了,只有在两边对象都已经存在的情况下使用=
才是赋值运算符重载,其他情况下都是拷贝构造函数!!!
八、const成员函数
将const
修饰的“成员函数”称之为const
成员函数,const
修饰类成员函数,实际是修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看下面一个例子,大家认为会打印出什么结果呢?
class A
{
public:void Print() const{cout << _a << endl;}private:int _a = 20;
};int main()
{const A aa;aa.Print(); return 0;
}
通过上图我们发现这段代码竟然报错了,这是为何?
这是因为我们的类A对象aa使用了const修饰,那么在调用成员函数时编译器会隐式的将&aa ==> const A*传递给成员函数Print的形参,但是成员函数Print默认是用A* this来接收实参,所以此时就发生了权限放大的问题,我们在之前讲过对于指针和引用来说权限放大是不行的。
那么该做如何修改呢?在C++中为了解决这个问题使用了const修饰成员函数,在成员函数后加上const表示它是一个const成员函数:
下面这种权限缩小的方式也是可行的:
内部不改变成员变量的成员函数最好加上const,const对象和普通对象都可以调用!!只能修饰成员函数因为它有this指针
下面有几个问题我们一起来看一下:
1.const对象可以调用非const成员函数吗? 不可以
2.非const对象可以调用const成员函数吗? 可以
3.const成员函数内可以调用其它的非const成员函数吗?
4.非const成员函数内可以调用其它的const成员函数吗?
第一二个问题我们已经解决了,接下来我们看看剩下的问题:
Q:const成员函数内可以调用其它的非const成员函数吗?
Q:非const成员函数内可以调用其它的const成员函数吗?
九、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会自动生成。
operator&: 取地址操作符重载
Date* operator&()
{return this;
}
const operator&: const取地址操作符重载
const Date* operator&() const
{return this;
}
如果这两个函数不写也没有什么问题,编译器生成也够用。如果你不想让别人拿到类对象的地址就可以像下面这样写。
Date* operator&()
{return nullptr;
}const Date* operator&() const
{return nullptr;
}
本篇文章的内容就到这里了,这篇文章有一定的难度,特别是细节方面需要大家去认真琢磨,最后如果文章有任何疑问或者错处,欢迎大家评论区一起交流orz~🙈🙈
相关文章:

【C++】类和对象——六大默认成员函数
🏖️作者:malloc不出对象 ⛺专栏:C的学习之路 👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈 目录前言一、类的6个默认成员函数二、构造…...

远程debug被arthas watch了的idea
开发工具idea端(2021.2.1) 远程调试 被 应用了 修改的arthas端 的 鸡idea端(2022.3.2) A. 鸡idea端 鸡idea: “D:\IntelliJ IDEA 2022.3.2\bin\idea64.exe” 中安装有目标插件 比如 RedisNew-2022.07.24.zip 对文件 “D:\IntelliJ IDEA 2022.3.2\bin\idea64.exe.vmoptions” 新…...

Cesium实现的光柱效果
Cesium实现的光柱效果 效果展示: 可以通过拼接两个entity来实现这个效果: 全部代码; index.html <!DOCTYPE html> <html><head><meta charset...

你最爱记混的slice()和splice()
slice()方法:选取数组的一部分,并返回一个新数组 该方法不会改变原始数组,而是将截取到的元素封装到一个新数组中返回 语法:array.slice(start,end),参数的介绍如下: 语法:array.slice(start,end),参数的介绍如下: 1.start:截取开始的位置的索引,包含开始索引 2.…...

【LeetCode】剑指 Offer(15)
目录 题目:剑指 Offer 32 - II. 从上到下打印二叉树 II - 力扣(Leetcode) 题目的接口: 解题思路: 代码: 过啦!!! 题目:剑指 Offer 32 - III. 从上到下打…...

【刷题笔记】之二分查找(搜索插入位置。在排序数组中查找元素的第一个和最后一个位置、x的平方根、有效的完全平方数)
1. 二分查找题目链接 704. 二分查找 - 力扣(LeetCode)给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -…...

一起Talk Android吧(第五百一十五回:绘制向外扩散的水波纹)
文章目录整体思路实现方法示例代码各位看官们大家好,上一回中咱们说的例子是"Java中的进制转换",这一回中咱们说的例子是"绘制向外扩散的水波纹"。闲话休提,言归正转, 让我们一起Talk Android吧! 整体思路 …...

基于粒子群改进的支持向量机SVM的情感分类识别,pso-svm情感分类识别
目录 支持向量机SVM的详细原理 SVM的定义 SVM理论 Libsvm工具箱详解 简介 参数说明 易错及常见问题 SVM应用实例,基于SVM的情感分类预测 代码 结果分析 展望 支持向量机SVM的详细原理 SVM的定义 支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型…...

【python中的列表和元组】
文章目录前言一、列表及其使用1.列表的特点2. 列表的使用方法二、元组及其特点1.元组的类型是tuple1.元组的查找操作2. 计算元组某个元素出现的次数3.统计元组内元素的个数总结前言 本文着重介绍python中的列表和元组以及列表和元组之间的区别 一、列表及其使用 1.列表的特点…...

世界顶级五大女程序媛,不仅技术强还都是美女
文章目录1.计算机程序创始人:勒芙蕾丝伯爵夫人2.首位获得图灵奖的女性:法兰艾伦3.谷歌经典首页守护神:玛丽莎梅耶尔4.COBOL之母:葛丽丝穆雷霍普5.史上最强游戏程序媛-余国荔说起程序员的话,人们想到的都会是哪些理工科…...

Linux- 系统随你玩之--文件管理-双生姐妹花
文章目录1、前言2、文件管理-双生姐妹花2.1、 df2.1.1、 df 语法2.1.1 、常用参数2.2、 du2.2.1、du 语法2.1.1、 常用参数2.3、双生姐妹花区别2.3.1、 查看文件统计 的计算方式不同2.3.2 、删除文件情况下统计结果 不同2.3.3 、针对双生姐妹花区别 结语3、双生姐妹花实操3.1 、…...

18、多维图形绘制
目录 一、三维图形绘制 (一)曲线图绘制plot3() (二)网格图绘制 mesh() (三)曲面图绘制 surf() (四)光照模型 surfl() (五)等值线图(等高线图)绘制 cont…...

【C++】30h速成C++从入门到精通(STL介绍、string类)
STL简介什么是STLSTL(standard template libaray-标准模板库):是C标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。STL的版本原始版本Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本&…...

PMP是什么意思?适合哪些人学呢?
PMP简而言之,就是提高项目管理理论基础和实践能力的考试。 官方一点的说明呢,就是:PMP证书全称为Project Management Professional,也叫项目管理专业人士资格认证。 PMP证书由美国项目管理协会(PMI)发起,是严格评估项…...

【SpringBoot 事务不回滚?怎么解决?】
SpringBoot 事务不回滚可能有多种原因,下面列举一些常见的原因和对应的解决方法: 异常被捕获处理了 如果方法中抛出了异常,但是在方法中被捕获并处理了,那么事务不会回滚。解决方法是让异常继续抛出,或者使用 Transa…...

软件研发管理经验总结 - 技术管理
软件研发管理经验总结 - 技术管理 技术管理主要负责有技术团队建设、管理团队成员技术相关事务、帮助团队成员成长、负责团队成员交付的代码质量、以及负责产品技术方向、以及产品相关前沿技术调研;管理团队成员技术相关事务有代码Review、故障率跟踪、分析及根据分…...

项目实战典型案例19——临时解决方案和最终解决方案
临时解决方案和最终解决方案一:背景介绍二:思路&方案四:总结五:升华一:背景介绍 本篇博客是对项目开发中出现的临时解决方案和最终解决方案进行的总结和改进。目的是将经历转变为自己的经验。通过博客的方式分享给…...

机器学习模型的可解释性算法汇总!
模型可解释性汇总简 介目前很多机器学习模型可以做出非常好的预测,但是它们并不能很好地解释他们是如何进行预测的,很多数据科学家都很难知晓为什么该算法会得到这样的预测结果。这是非常致命的,因为如果我们无法知道某个算法是如何进行预测&…...

什么是着色器/Threejs如何使用着色器/Threejs使用着色器实现平面网格的动态效果案例
1,什么是着色器着色器(Shader)是计算机图形学中的一个重要概念,它是在 GPU 上运行的程序,用于计算三维场景中每个像素的颜色和其他属性。着色器通常分为两种类型:顶点着色器和片元着色器。顶点着色器主要用…...

191、【动态规划】AcWing ——AcWing 900. 整数划分:完全背包解法+加减1解法(C++版本)
题目描述 参考文章:900. 整数划分 解题思路 因为本题中规定了数字从大到小,其实也就是不论是1 2 1 4,还是2 1 1 4,都会被看作是2 1 1 4这一种情况,因此本题是在遍历中不考虑结果顺序。 背包问题中只需考虑…...

Java 比较器
public interface Comparable Comparable 接口位于 java.lang 包下,对实现它的每个类的对象强加一个总排序,这种排序被称为类的自然顺序,compareTo 方法被称为其自然比较方法。 实现此接口的对象的列表(和数组)可以由…...

配置本地 python GEE、geemap环境
1.安装anconda 百度搜索anconda清华镜像,从清华镜像中选择最新的anconda安装包,国内镜像网站下载速度较快,如果从国外官网下载速度相当慢,详细安装教程请参考: anconda安装教程https://blog.csdn.net/lwbCUMT/article…...

cmd命令教程
小提示: 在本文中,我将向您展示可以在 Windows 命令行上使用的 40 个命令 温馨提示:在本教程中学习使用适用于 Windows 10 和 CMD 网络命令的最常见基本 CMD 命令及其语法和示例 文章目录为什么命令提示符有用一、cmd是什么?如何在…...

深圳大学计软《面向对象的程序设计》实验15 函数模板和类模板
A. 有界数组模板类(类模板) 题目描述 编写有界数组模板BoundArray(即检查对数组元素下标引用并在下标越界时终止程序的执行),能够存储各种类型的数据。要求实现对数组进行排序的方法sort,及对数组进行查找…...

组播详解及示例代码
写在前面 由于公司业务需要用到组播实现,这里就记录下学习过程。在学习组播之前,我们先来看看另外两种数据包传输方式:单播和广播。 单播:简单来说就是数据一对一发送,如果需要给多个主机发送数据时,就需…...

C语言-qsort函数示例解析
一.qsort函数是什么stdlib.h头文件下的函数qsort()函数:是八大排序算法中的快速排序,能够排序任意数据类型的数组其中包括整形,浮点型,字符串甚至还有自定义的结构体类型。qsort函数实现对不同元素的排序主要就是通过对compar函数…...

一些Linux内核内存性能调优笔记!
前言 在工作生活中,我们时常会遇到一些性能问题:比如手机用久了,在滑动窗口或点击 APP 时会出现页面反应慢、卡顿等情况;比如运行在某台服务器上进程的某些性能指标(影响用户体验的 PCT99 指标等)不达预期…...

【JVM】逃逸分析
开发者都知道,基本上所有对象都是在堆上创建。但是,这里还是没有把话说绝对哈,指的是基本上所有。昨天一位朋友在聊天中,就说了所有对象都在堆中创建,然后被朋友一阵的嘲笑。 开始我们的正文,我们今天来聊聊…...

C51---震动传感器控制LED灯亮灭
1.example #include "reg52.h" sbit led1 P3^7;//原理图中led1指向P3组IO口的P3.7口 sbit vibrate P3^3;//Do接到了P3.3口 void Delay3000ms() //11.0592MHz { unsigned char i, j, k; //_nop_(); i 22; j 3; k 227; do { …...

使用 JaCoCo 生成测试覆盖率报告
0、为什么要生成测试覆盖率报告 在我们实际的工作中,当完成程序的开发后,需要提交给测试人员进行测试,经过测试人员测试后,代码才能上线到生产环境。 有个问题是:怎么能证明程序得到了充分的测试,程序中所…...