当前位置: 首页 > news >正文

【C++】类和对象(中篇)

类和对象

  • 类的六大默认成员函数
  • 一、构造函数
    • 1. 构造函数的概念
    • 2. 构造函数的特性
  • 二、析构函数
    • 1. 析构函数的概念
    • 2. 析构函数的特性
  • 三、拷贝构造函数
    • 1. 拷贝构造函数的概念
    • 2. 拷贝构造函数的特征
  • 四、赋值运算符重载
    • 1. 运算符重载
    • 2. 赋值运算符重载
  • 五、取地址及 const 取地址操作符重载
    • 1. const 成员
    • 2. 取地址及 const 取地址操作符重载

在往期 类和对象(上篇) 中,我们初步接触了C++中的类和对象,接下来我会和大家分享有关这个知识点更多的内容~

类的六大默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数,下面我会带大家介绍类的六大默认成员函数。

一、构造函数

1. 构造函数的概念

我们在类和对象上篇的时候,我们写了一个对日期初始化的函数 Init,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

		// 日期类class Date{public:// 初始化void Init(int year, int month, int day){_year = year;_month = month;_day = day;}private:// 声明int _year;int _month;int _day;};

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

2. 构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。开空间是我们创建对象的时候,栈帧已经帮我们开好空间了。

构造函数有以下的特征:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

例如以下的日期类:

		class Date{public:// 无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 有参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:// 声明int _year;int _month;int _day;};

根据我们以前学的缺省参数,可以将上面两个构造函数合成一个,例如:

		// 全缺省构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}

在这个时候我们可以引入第五点:

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

这里所指的默认构造函数是指,我们不传参就可以调用的。如果我们将无参的构造函数和全缺省的构造函数都放在类的内部实现,并实例化一个对象,会发生什么呢?我们来看结果:

在这里插入图片描述
在这里插入图片描述

在这里出现的主要问题是 d1,实例化时没有参数,属于无参对象,而上面我们有三个构造函数,虽然它们构成函数重载,但是 d1 在调用构造函数时,无参的构造函数全缺省的构造函数存在歧义,编译器不知道该调用哪一个,对于 d1 来说,它可以调用无参的构造函数,也可以调用全缺省的构造函数,所以这很好地说明了,多个构造函数并存会存在调用二义性。

所以上面这段代码应该把无参的构造函数和有参的构造函数去掉,只保留全缺省的构造函数,因为全缺省的构造函数是它们两个的合并,使用于两种情况,如以下代码:

		class Date{public:// 全缺省构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:// 声明int _year;int _month;int _day;};
  1. 如果类中没有显式定义构造函数,则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 d1;d1.Print();return 0;}

这里的 d1 是无参的对象,我们写了一个没有缺省值的构造函数,按正常来说编译是不会通过的,因为一旦显式定义任何构造函数,编译器将不再生成,编译报错如下:

在这里插入图片描述
这里说明我们一旦显式写了任何一个构造函数,编译器都不会生成默认构造函数。

如果我们将我们自己写的构造函数屏蔽掉,代码可以通过编译,因为编译器生成了一个无参的默认构造函数,但是会有以下的现象:

在这里插入图片描述
我们可以看到,编译器默认生成的构造函数并没有对对象进行初始化,这就引出了我们的第七点:

  1. 关于编译器生成的默认成员函数,我们会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1 对象调用了编译器生成的默认构造函数,但是 d1 对象 _year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用吗?

答案是并不是的,C++ 把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,包括所有的指针都是内置类型;自定义类型就是我们使用 class/struct/union 等自己定义的类型。

我们可以观察下面的程序,我们在日期类的成员变量中,添加了一个时间类的自定义类型 _t ,我们会发现编译器生成默认的构造函数会对自定类型成员 _t 调用了它的默认成员函数。

		// 时间类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;}

我们在时间类的构造函数中打印了它的构造函数的字符串,证明编译器默认生成的构造函数确实调用了 _t 对应类的默认成员函数,执行结果如下:
在这里插入图片描述
我们看到它确实打印出来了,说明自定义类型确实调用它对应的类的默认成员函数。

  1. 编译器不会对内置类型的成员进行处理,但是 C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值,即缺省值。

例如以下代码:

		// 日期类class Date{public:void Print(){cout << _year << '.' << _month << '.' << _day << endl;}private:// 内置类型// 声明给的缺省值int _year = 1;int _month = 1;int _day = 1;};int main(){// 实例化对象Date d;d.Print();return 0;}

我们没有对它显式地写构造函数,默认生成的构造函数也不会对内置类型进行处理,但是我们在声明的时候给了缺省值,执行结果如下:

在这里插入图片描述

最后对构造函数进行总结:一般情况都需要我们自己写构造函数,决定初始化方式,例如成员变量中有指针类型的,需要我们自己决定如何初始化;如果成员变量全是自定义类型的,可以考虑不写构造函数。

二、析构函数

1. 析构函数的概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

2. 析构函数的特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
  4. 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。

例如日期类,日期类中的析构函数虽然没有资源需要清理,最后系统直接将其内存回收即可,但是在对象销毁时还是会自动调用;我们在析构函数中打印了它的函数名,证明它有调用析构函数:

		// 日期类class Date{public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 日期类的析构函数~Date(){cout << "~Date()" << endl;_year = 0;_month = 0;_day = 0;}void Print(){cout << _year << '.' << _month << '.' << _day << endl;}private:// 内置类型int _year;int _month;int _day;};

我们实例化一个对象 d,什么都不做,直到函数结束,对象销毁,观察它是否自动调用了析构函数:

在这里插入图片描述
很明显,它确实自动调用了析构函数。

那么对于日期类我们没有什么资源需要清理,我们是可以不用显示写的,默认生成的析构函数就够用,默认生成的会做什么,我们后面再看;现在我们再实现一个栈的析构函数;

因为栈我们是用数组实现的,我们先需要向堆申请一块空间,最后对象销毁时,堆上的空间并没有释放,如果没有释放会造成内存泄漏,所以需要我们实现一个析构函数去完成这块的清理工作:

		class Stack{public:// 栈的构造函数Stack(int capacity = 4){cout << "Stack(int capacity = 4)" << endl;_a = (int*)malloc(sizeof(int) * capacity);assert(_a);_top = 0;_capacity = capacity;}// 栈的析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:int* _a;int _top;int _capacity;};

例如上面是我们栈的析构函数,我们再观察一下它的调用情况:

在这里插入图片描述
我们可以看到,它确实调用了构造函数和析构函数。

  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?其实,编译器生成的默认析构函数,对内置类型的成员不做处理,对自定义类型成员调用它的析构函数。

下面的代码我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数:

			// 时间类class Time{public:~Time(){cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;};// 日期类class Date{private:// 内置类型int _year = 2023;int _month = 7;int _day = 21;// 自定义类型Time _t;};

我们写了时间类的析构函数,并在里面打印它的函数名字,证明它确实被调用;然后我们在日期类的成员变量中添加自定义类型 _t,我们观察它是否会自动调用 Time 类的析构函数:

在这里插入图片描述
我们可以看到,对于自定义类型,编译器确实调用了它的析构函数。

最后对这部分的析构函数进行总结,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如日期类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

  1. 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象

例如,设已经有A, B, C, D4个类的定义,程序中A, B, C, D构造函数和析构函数调用顺序是什么呢,看以下程序:

		C c;int main(){A a;B b;static D d;return 0;}

我们分别在对应类的构造函数和析构函数中打印对应的构造或者析构函数的信息,以便我们观察谁先调用和析构;我们先观察谁先调用构造函数:

在这里插入图片描述

如上图,我们可以观察到,构造函数的调用是按照从全局域先构造,再到局部域顺序进行构造的;

然后我们观察谁先调用析构函数:

在这里插入图片描述

我们可以观察到,是局部域先调用析构函数,而且是按照栈的结构顺序,先构造的后析构;当局部域全部析构完,再到静态区的对象析构;最后是全局域的对象析构。

三、拷贝构造函数

1. 拷贝构造函数的概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

2. 拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。

  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

     	class Date{public:// 构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(Date d){_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;};
    

例如上面代码的拷贝构造函数,我们使用传值的方式,会引发下面的问题:

在这里插入图片描述
所以在写拷贝构造函数的时候,我们应该在参数那里加上引用,这样调用拷贝构造就是实参的别名,不会造成无穷递归,应该写成下面的形式:

			// 拷贝构造函数Date(Date& d){_year = d._year;_month = d._month;_day = d._day;}
  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

例如下面这段代码,我们实现一个时间类和一个日期类,在日期类的成员变量中加入时间类的对象,即一个自定义对象,观察在拷贝构造的时候是否会调用它的拷贝构造;同时,我们的日期类没有写拷贝构造,我们也观察编译器生成的默认拷贝构造能否对内置类型完成浅拷贝:

		// 时间类class Time{public:// 时间类的构造函数Time(){_hour = 1;_minute = 1;_second = 1;}// 时间类的拷贝构造Time(const Time& t){cout << "const Time& t" << endl;_hour = t._hour;_minute = t._minute;_second = t._second;}private:int _hour;int _minute;int _second;};

上面是一个时间类,我们对它写了拷贝构造函数,下面在日期类中定义一个时间类的自定义类型,观察它是否调用时间类的拷贝构造函数:

		// 日期类class Date{public:void Print(){cout << _year << '.' << _month << '.' << _day << endl;}private:// 内置类型int _year = 2023;int _month = 7;int _day = 21;// 自定义类型Time _t;};

执行的结果如下:

在这里插入图片描述
从结果我们可以看出,自定义类型确实调用了它自己的拷贝构造;并且我们没有显式写的拷贝构造,编译器默认生成的拷贝构造会对内置类型完成浅拷贝的工作。

  1. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝,会造成内存泄漏。

为什么说涉及到资源申请时一定要写拷贝构造呢?又怎么会发生内存泄漏呢?理由如下:

例如我们有一个栈的类如下:

		class Stack{public:Stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);assert(_a);_top = 0;_capacity = capacity;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}private:int* _a;int _top;int _capacity;};

但是我们没有对它写拷贝构造函数,现有以下程序:

		void func(Stack s){}int main(){Stack st;func(st);return 0;}

func(st) 在传参的过程中就是对 Stack 类调用拷贝构造的过程,但是我们没有对 Stack 类写拷贝构造,所以编译器默认生成的拷贝构造会完成浅拷贝的过程,具体过程如下图:

在这里插入图片描述
例如上图所示,func 中的 _a 和 main 中的 _a 指向了同一个空间,当 fun 函数结束时,编译器会调用析构函数对 func 函数中的临时拷贝对象进行析构,也就是说,_a 指针指向的空间被释放了,而这个空间同时也是 main 函数中对象 st 的成员变量 _a 的,当 main 函数也结束时,会再次调用析构函数对 st 对象进行析构,可是它的成员变量 _a 指向的空间已经被释放了,现在再次释放就是同一个空间释放两次,程序会崩溃。

所以我们应该自己写一个拷贝构造函数,例如:

			// 拷贝构造函数Stack(const Stack& s){// 深拷贝_a = (int*)malloc(sizeof(int) * s._capacity);assert(_a);memcpy(_a, s._a, sizeof(int) * s._top);_capacity = s._capacity;_top = s._top;}

其实我们对 _a 指针的处理就是深拷贝的问题,关于深拷贝问题我们以后会学习。

四、赋值运算符重载

1. 运算符重载

C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。其作用是使对象之间可以使用某种运算符,这种运算符需要我们重载。

假设我们实现的日期类要进行大小比较,是无法直接使用 >,<,= 进行比较的,因为这个类是我们实现的,编译器无法知道比较的规则,所以需要我们直接写。

函数名字为:关键字 operator 后面接需要重载的运算符符号。返回类型根据需要返回。

假设我们在日期类中重载一个 >== 的运算符:

			// >bool operator>(const Date& d){if (_year < d._year){return false;}else if (_year == d._year && _month < d._month){return false;}else if (_year == d._year && _month == d._month && _day < d._day){return false;}return true;}// == bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}

如上就是两个运算符重载的示例,我们需要根据运算符的特性和类的需求实现运算符的重载,有了以上两个运算符重载,我们可以复用以上的代码,实现剩下的比较运算符,例如 <,!=,>=,<= :

			// >=bool operator>=(const Date& d){return ((*this) > d) || ((*this) == d);}// <bool operator<(const Date& d){return !(*this >= d);}// <=bool operator<=(const Date& d){return (*this < d) || (*this == d);}// != bool operator!=(const Date& d){return !((*this) == d);}

我们还可以实现一些对日期类有意义的运算符,例如我需要知道 x 天以后的日期,或者 x 天以前的日期,其实就是对 +=,+,-=,- 运算符的重载;

假设我们从现在的时间开始加 100 天,首先我们得知道这个月一共有几天才可以进行加减,还需要判断闰年非闰年的二月,所以我们需要写一个函数判断每个月的天数,代码如下:

			// 获得月份天数int GetMonthDay(int year, int month){static int Day[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;}return Day[month];}

首先我们定义了一个 static 的数组,因为这个函数有可能会被大量调用,每次调用都需要创建一个数组,效率会变低;这个数组就是每个月的天数,其中二月默认是 28 天,我们在下面判断如果是闰年并且是二月,再返回二月的 29 天。

然后我们实现 += 的运算符重载:

			// 日期 += 天数Date& operator+=(const int day){_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_year++;_month = 1;}}return *this;} 

我们直接先让日期中的天数直接加上需要加的天数,再判断这个日期的天数是否符合这个月的天数,不符合的话就减去这个月的天数,然后月份+1,+1后还要判断月份是否合理,如果大于十二月,年份就+1,月份置为一月,逻辑就如上;

我们可以根据上面的逻辑再实现一些运算符的重载,例如以下:

			// 日期 + 天数Date operator+(const int day){Date tmp(*this);tmp += day;return tmp;}

注意,日期 + 天数的重载是不改变日期本身的,所以返回的时一个日期 + 天数之后的临时变量,所以这里的返回也不可以用引用返回。

			// 日期 -= 天数Date& operator-=(const int day){_day -= day;while (_day <= 0){--_month;if (_month < 1){_month = 12;--_year;}_day += GetMonthDay(_year, _month);}return *this;}// 日期 - 天数Date operator-(const int day){Date tmp(*this);tmp -= day;return tmp;}

注意,C++ 在对前置++和后置++进行运算符重载的时候进行了特殊处理以便进行区分,前置++括号内没有参数类型,而后置++在括号内添加了一个参数类型,如下:

			// 前置++Date& operator++(){(*this) += 1;return *this;}// 后置++Date operator++(int){Date tmp(*this);++(*this);return tmp;}

前置- -和后置 - - 也类似:

			// 前置--Date& operator--(){(*this) -= 1;return *this;}// 后置--Date operator--(int){Date tmp(*this);--(*this);return tmp;}

我们还可以重载一个日期 - 日期的运算符重载,以便知道两个日期相差的天数,代码如下:

			// 日期 - 日期int operator-(const Date& d){Date max = *this;Date min = d;int n = 0;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}while (min != max){++min;++n;}return n * flag;}

想要求出两个日期相差的天数,我们先假设第一个日期是大的那个,即 *this ,小的是 d;如果假设错了就换过来,并且用一个 flag 变量控制正负;然后使用一个计数变量 n ,统计两个日期的相差天数,即两个日期不相等,就让小的那个 ++ ,然后 n++ 统计。

.* :: sizeof ?: . 注意以上5个运算符不能重载。

假设我们需要将一个对象赋值给另外一个对象呢,所以这里就引入我们的赋值运算符重载。

2. 赋值运算符重载

(1)赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义

日期类的赋值运算符重载代码如下:

		// 赋值运算符重载Date& operator=(const Date& d){// 判断是否是给自己赋值if (*this != d){_year = d._year;_month = d._month;_day = d._day;}return *this;}

这里返回类型 Date& 是为了支持连续赋值,例如 d1 = d2 = d3;而且出了作用域 *this 还在,所以可以使用引用返回。

(2)赋值运算符重载也是类的默认成员函数,我们不显式写,编译器也会默认生成一个,和拷贝构造一样,编译器默认生成的赋值运算符重载对内置类型进行值拷贝(浅拷贝),对自定义类型调用它自己的赋值运算符重载。

		class Time{public:Time(){_hour = 1;_minute = 1;_second = 1;}// 时间类的赋值运算符重载Time& operator=(const Time& t){cout << "Time& operator=(const Time& t)" << endl;if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}private:int _hour;int _minute;int _second;};

例如上面是一个时间类,我们在下面日期类中定义一个时间类的自定义类型,观察它是否调用自己的赋值运算符重载:

		class Date{private:// 基本类型(内置类型)int _year = 1;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){Date d1;Date d2;d1 = d2;return 0;}

结果如下:

在这里插入图片描述
所以验证了结论是正确的。

但是既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,是不是我们只用编译器默认生成的就够了呢?对于日期类确实是的,但是对于 Stack 类,我们还是需要自己显式写的,具体的原因和拷贝构造的一样,参考拷贝构造函数。

(3)拷贝构造赋值运算符重载的区别:拷贝构造是一个已经存在的对象对一个创建的对象进行实例化,例如以下代码:

		int main(){Date d1;// Date d2(d1);Date d2 = d1;return 0;}

其中,Date d2 = d1; 是 d1 对 d2 进行拷贝构造,所以会调拷贝构造函数;其实它等价于 Date d2(d1);

赋值运算符重载是两个已经存在的对象进行赋值操作,例如以下代码:

		int main(){Date d1;Date d2;d1 = d2;return 0;}

其中,d1 和 d2 都是已经存在的两个对象,所以 d1 = d2; 会调用赋值运算符函数。

五、取地址及 const 取地址操作符重载

到目前为止我们已经学了类的四个默认成员函数,那么还有两个就是取地址及 const 取地址操作符重载,为什么取地址操作符重载分为 const 和非 const 类型呢?下面带大家了解了解。

1. const 成员

我们将 const 修饰的 “成员函数” 称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针指向的对象,表明在该成员函数中不能对类的任何成员进行修改。

我们通常使用 const 修饰成员函数是用以下方式:

		void Date::Print() const{cout << _year << "年" << _month << "月" << _day << "日" << endl;}

实际上,这种写法相当于下图:

在这里插入图片描述

因为函数名后面的 const 就是修饰 this 指针的。

那么为什么要有这种 const 修饰的成员函数呢?我们来看以下的代码:

		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;}private:int _year; // 年int _month; // 月int _day; // 日};int main(){Date d1(2023, 7, 28);d1.Print();const Date d2(2023, 6, 28);d2.Print();return 0;}

上面的代码是一个日期类,我们只写了一个普通的 Print() 函数,让对象可以调用其打印数据,但是我们在主函数中创建了一个 const 修饰 d2 对象,我们观察是否可以调用 Print() 函数:

在这里插入图片描述

很明显不能调用,编译器报错了,因为对 d2 取地址后是一个 const Date* 类型,到 Print() 函数中的 this 指针却是 Date* 类型,很明显这是权限的放大的问题。

所以针对这个问题,我们引入了 const 修饰成员函数的问题,正确的代码应该是以下代码:

		class Date{public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 给非 const 对象调用void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}// 给 const 对象调用void Print() const{cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year; // 年int _month; // 月int _day; // 日};int main(){Date d1(2023, 7, 28);d1.Print();const Date d2(2023, 6, 28);d2.Print();return 0;}

我们观察是否能正常运行:

在这里插入图片描述

从上图可以看出,是可以正常运行的,说明 const 对象是调用了 const 修饰的成员函数。

那么是不是所有的成员函数都可以使用 const 修饰呢?并不是的,一般来说,只读函数可以加 const,只读函数就是内部不涉及修改成员的函数。 例如我们实现的日期类中,以下成员函数都可以使用 const 修饰:

			//运算符重载bool operator==(const Date& d) const;bool operator!=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator<(const Date& d) const;bool operator<=(const Date& d) const;Date operator+(int day) const;Date operator-(int day) const;

注意,这里是函数的声明,如果成员函数要想被 const 修饰,我们不仅要在声明给 const ,函数的定义处也要给 const 修饰。

而以下几个都涉及内部修改成员的函数,就不适合用 const 修饰:

			Date& operator+=(int day);Date& operator-=(int day);Date& operator++(); //前置Date operator++(int); //后置Date& operator--(); //前置Date operator--(int); //后置

2. 取地址及 const 取地址操作符重载

这两个默认成员函数相对于上面我们所学的其他四个,是非常简单的;这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

编译器默认生成的一般就是以下的形式:

		class Date{public:// 非 const 对象取地址运算符重载Date* operator&(){return this;}// const 对象取地址运算符重载const Date* operator&()const{return this;}private:int _year; // 年int _month; // 月int _day; // 日};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人取到对象的地址,可以重载成以下形式:

			// 非 const 对象取地址运算符重载Date* operator&(){return nullptr;}// const 对象取地址运算符重载const Date* operator&()const{return nullptr;}

如果不想让别人取到对象的地址,返回空指针即可。

最后,我们类的六大默认成员函数就学完啦!感觉有用的小伙伴帮忙点个赞吧~

预告:类和对象(下篇)将会是类和对象的最后一篇文章,我会和大家补充一下类和对象有关的知识,并会完善我们的日期类噢!~

相关文章:

【C++】类和对象(中篇)

类和对象 类的六大默认成员函数一、构造函数1. 构造函数的概念2. 构造函数的特性 二、析构函数1. 析构函数的概念2. 析构函数的特性 三、拷贝构造函数1. 拷贝构造函数的概念2. 拷贝构造函数的特征 四、赋值运算符重载1. 运算符重载2. 赋值运算符重载 五、取地址及 const 取地址…...

大数据处理架构详解:Lambda架构、Kappa架构、流批一体、Dataflow模型、实时数仓

前言 本文隶属于专栏《大数据理论体系》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见大数据理论体系 姊妹篇 《分布式数据模型详解&#xff1a;OldSQL &…...

双指针解决n数之和问题

1. 两数之和 1. 两数之和 将时间复杂度降到O(n)&#xff1b; class Solution {// 双指针public int[] twoSum(int[] nums, int target) {int nnums.length;int l0;while(l<n){int rn-1;// 找到第一个可能nums[l]nums[r]target的位置while(r>l){if(nums[l]nums[r]targe…...

安全学习DAY07_其他协议抓包技术

协议抓包技术-全局-APP&小程序&PC应用 抓包工具-Wireshark&科来分析&封包 TCPDump&#xff1a; 是可以将网络中传送的数据包完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤&#xff0c;并提供and、or、not等逻辑语句来帮助你去掉无用…...

electron的electron-packager打包运行和electron-builder生产安装包过程,学透 Electron 自定义 Dock 图标

electron的electron-packager打包运行和electron-builder生产安装包过程 开发electron客户端程序&#xff0c;打包是绕不开的问题。 macOS 应用构建&#xff0c;看似近在咫尺&#xff0c;实则坑坑致命。 场景&#xff1a;mac笔记本打包&#xff0c;以及生产出可交付的软件安装…...

【无标题】深圳卫视专访行云创新马洪喜:拥抱AI与云原生,深耕云智一体化创新

人工智能&#xff08;AI&#xff09;是引领新一轮科技革命和产业变革的重要驱动力。因此&#xff0c;深圳出台相关行动方案&#xff0c;统筹设立规模1,000亿元的人工智能基金群&#xff0c;引导产业集聚培育企业梯队&#xff0c;积极打造国家新一代人工智能创新发展试验区和国家…...

jenkins通过流水线进行构建jar包

前言 最近项目上需要进行CICD,本篇博客主要分享各种骚操作 目录 前言操作如下:构建触发器测试哈哈操作如下: 1.下载Jenkins.war包上传到服务器上面,然后在同级目录下面创建如下脚本: #!/bin/bash# Jenkins安装目录 JENKINS_HOME=/usr/local/jenkins# Jenkins日志文件 LO…...

Android开发:通过Tesseract第三方库实现OCR

一、引言 什么是OCR&#xff1f;OCR(Optical Character Recognition&#xff0c;光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符&#xff0c;通过检测暗、亮的模式确定其形状&#xff0c;然后用字符识别方法将形状翻译成计算机文字的过程。简单地说&#…...

合并两个有序链表——力扣21

题目描述 法一 递归 class Solution { public:ListNode* mergeTwoLists(ListNode *l1, ListNode*l2){if(l1 nullptr){return l2;} else if (l2nullptr){return l1;} else if (l1->val<l2->val){l1->next mergeTwoLists(l1->next, l2);return l1;} else {l2-&g…...

企业数据,大语言模型和矢量数据库

随着ChatGPT的推出&#xff0c;通用人工智能的时代缓缓拉开序幕。我们第一次看到市场在追求人工智能开发者&#xff0c;而不是以往的开发者寻找市场。每一个企业都有大量的数据&#xff0c;私有的用户数据&#xff0c;自己积累的行业数据&#xff0c;产品数据&#xff0c;生产线…...

LabVIEW使用支持向量机对脑磁共振成像进行图像分类

LabVIEW使用支持向量机对脑磁共振成像进行图像分类 医学成像是用于创建人体解剖学图像以进行临床研究、诊断和治疗的技术和过程。它现在是医疗技术发展最快的领域之一。通常用于获得医学图像的方式是X射线&#xff0c;计算机断层扫描&#xff08;CT&#xff09;&#xff0c;磁…...

kafka面试题

kafka基本概念 Producer 生产者&#xff1a;负责将消息发送到 BrokerConsumer 消费者&#xff1a;从 Broker 接收消息Consumer Group 消费者组&#xff1a;由多个 Consumer 组成。消费者组内每个消费者负责消费不同分区的数据&#xff0c;一个分区只能由一个组内消费者消费&am…...

树的遍历(一题直接理解中序、后序、层序遍历,以及树的存储)

题目如下&#xff1a; 一个二叉树&#xff0c;树中每个节点的权值互不相同。 现在给出它的后序遍历和中序遍历&#xff0c;请你输出它的层序遍历。 输入格式 第一行包含整数 N&#xff0c;表示二叉树的节点数。 第二行包含 N 个整数&#xff0c;表示二叉树的后序遍历。 第…...

JVM系统优化实践(22):GC生产环境案例(五)

您好&#xff0c;这里是「码农镖局」CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e; 除了Tomcat、Jetty&#xff0c;另一个常见的可能出现OOM的地方就是微服务架构下的一次RPC调用过程中。笔者曾经经历过的一次OOM就是基于Thrift框架封装出来的一个RPC框架导…...

DevOps系列文章 之GitLabCI模板库的流水线

目录结构&#xff0c;jobs目录用于存放作业模板。templates目录用于存放流水线模板。这次使用​​default-pipeline.yml​​作为所有作业的基础模板。 作业模板 作业分为Build、test、codeanalysis、artifactory、deploy部分&#xff0c;在每个作业中配置了rules功能开关&…...

spring扩展点ApplicationContextAware解释

ApplicationContextAware是Spring框架中的一个扩展接口&#xff0c;用于获取和操作应用程序上下文&#xff08;ApplicationContext&#xff09;。通过实现ApplicationContextAware接口&#xff0c;可以在Bean中获取对应用程序上下文的引用&#xff0c;并进行进一步的操作。 具…...

力扣热门100题之最大子数组和【中等】【动态规划】

题目描述 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 示例 1&#xff1a; 输入&#xff1a;nums [-2,1,-3,4,-1,2,1,-5,4] 输出&a…...

导出为PDF加封面且分页处理dom元素分割

文章目录 正常展示页面导出后效果代码 正常展示页面 导出后效果 代码 组件内 <template><div><div><div class"content" id"content" style"padding: 0px 20px"><div class"item"><divstyle"…...

【C++入门】浅谈类、对象和 this 指针

文章目录 一、前言二、类1. 基本概念2. 类的封装3. 使用习惯成员函数定义习惯成员变量命名习惯 三、对象1. 基本概念2. 类对象的存储规则 四、this 指针1. 基本概念2. 注意事项3. 经典习题4. 常见面试题 一、前言 在 C 语言中&#xff0c;我们用结构体来描述一个事物的多种属性…...

【Linux命令200例】indent对C语言代码进行缩进和格式化

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;本文已收录于专栏&#xff1a;Linux命令大全。 &#x1f3c6;本专栏我们会通过具体的系统的命令讲解加上鲜活的实操案例对各个命令进行深入…...

shell脚本--常见案例

1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件&#xff1a; 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分

一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计&#xff0c;提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合&#xff1a;各模块职责清晰&#xff0c;便于独立开发…...

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

无人机侦测与反制技术的进展与应用

国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机&#xff08;无人驾驶飞行器&#xff0c;UAV&#xff09;技术的快速发展&#xff0c;其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统&#xff0c;无人机的“黑飞”&…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...

02.运算符

目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&&#xff1a;逻辑与 ||&#xff1a;逻辑或 &#xff01;&#xff1a;逻辑非 短路求值 位运算符 按位与&&#xff1a; 按位或 | 按位取反~ …...

【实施指南】Android客户端HTTPS双向认证实施指南

&#x1f510; 一、所需准备材料 证书文件&#xff08;6类核心文件&#xff09; 类型 格式 作用 Android端要求 CA根证书 .crt/.pem 验证服务器/客户端证书合法性 需预置到Android信任库 服务器证书 .crt 服务器身份证明 客户端需持有以验证服务器 客户端证书 .crt 客户端身份…...

聚六亚甲基单胍盐酸盐市场深度解析:现状、挑战与机遇

根据 QYResearch 发布的市场报告显示&#xff0c;全球市场规模预计在 2031 年达到 9848 万美元&#xff0c;2025 - 2031 年期间年复合增长率&#xff08;CAGR&#xff09;为 3.7%。在竞争格局上&#xff0c;市场集中度较高&#xff0c;2024 年全球前十强厂商占据约 74.0% 的市场…...