二、类与对象(二)

8 this指针

8.1 this指针的引入

我们先来定义一个日期的类Date

	#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, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}

对于上面一个类,有这样一个问题:

Date类中有InitPrint两个成员函数,函数体中并没有关于不同对象的区分,那么当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象的呢?

C++通过引入this指针来解决这个问题。实际上,C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

8.2 this指针的特性

  1. this指针的类型:类的类型 const*,所以成员函数中,不能给this指针赋值。
  2. this指针只能在成员函数的内部使用。
  3. this指针本质上是成员函数的形参,所以this指针是存储在中的。当对象调用成员函数时,函数将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. this指针

例1:下面程序编译运行的结果是什么?

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
void test1()
{A* p = nullptr;//空指针p->Print();
}
void test2()
{A* p = nullptr;//空指针(*p).Print();
}
int main()
{test1();test2();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,程序正常运行了,这是为什么呢?

这是因为成员函数Print实际上在公共的代码段而并不在对象里面,所以虽然p是一个空指针,但p->Print()在这里并不代表解引用,而是直接去公共区域调用了函数Print(*p).Print()也同理。

如果是这样的话,那能不能不用对象直接调用Print函数呢?

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{Print();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,编译器报错了。这是因为Print会受到类域的限制,如果不用对象直接调用Print函数那么编译器将无法找到Print函数。

例2:下面程序编译运行的结果是什么?

#include <iostream>
using namespace std;
class A
{
public:void PrintA(){cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,程序崩溃了。这是因为PrintA函数体内部的cout << _a << endl语句等价于cout << this->_a << endl而此时PrintA函数的参数为空指针,那么对空指针进行解引用自然就会发生崩溃了。

9 C语言和C++实现Stack的对比

9.1 C语言实现

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int DataType;
typedef struct Stack
{DataType* array;int capacity;int size;
}Stack;void StackInit(Stack* ps)
{assert(ps);ps->array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == ps->array){assert(0);return;}ps->capacity = 3;ps->size = 0;
}void StackDestroy(Stack* ps)
{assert(ps);if (ps->array){free(ps->array);ps->array = NULL;ps->capacity = 0;ps->size = 0;}
}void CheckCapacity(Stack* ps)
{if (ps->size == ps->capacity){int newcapacity = ps->capacity * 2;DataType* temp = (DataType*)realloc(ps->array,newcapacity * sizeof(DataType));if (temp == NULL){perror("realloc申请空间失败!!!");return;}ps->array = temp;ps->capacity = newcapacity;}
}void StackPush(Stack* ps, DataType data)
{assert(ps);CheckCapacity(ps);ps->array[ps->size] = data;ps->size++;
}int StackEmpty(Stack* ps)
{assert(ps);return 0 == ps->size;
}void StackPop(Stack* ps)
{if (StackEmpty(ps))return;ps->size--;
}DataType StackTop(Stack* ps)
{assert(!StackEmpty(ps));return ps->array[ps->size - 1];
}int StackSize(Stack* ps)
{assert(ps);return ps->size;
}int main()
{Stack s;StackInit(&s);StackPush(&s, 1);StackPush(&s, 2);StackPush(&s, 3);StackPush(&s, 4);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackPop(&s);StackPop(&s);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackDestroy(&s);return 0;
}

可以看到,在用C语言实现Stack时,Stack相关操作函数有以下共性:

  1. 每个函数的第一个参数都是Stack*
  2. 函数中必须要对第一个参数检测,因为该参数可能会为NULL
  3. 函数中都是通过Stack*参数操作栈的.
  4. 调用时必须传递Stack结构体变量的地址。

结论:C语言中结构体只能定义存放数据的结构,而操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相对复杂,涉及到大量指针操作,稍不注意可能就会出错。

9.2 C++实现

#include <iostream>
#include <stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
public:void Init(){_array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = 3;_size = 0;}void Push(DataType data){CheckCapacity();_array[_size] = data;_size++;}void Pop(){if (Empty())return;_size--;}DataType Top() { return _array[_size - 1]; }int Empty() { return 0 == _size; }int Size() { return _size; }void Destroy(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:void CheckCapacity(){if (_size == _capacity){int newcapacity = _capacity * 2;DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));if (temp == NULL){perror("realloc申请空间失败!!!");return;}_array = temp;_capacity = newcapacity;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);printf("%d\n", s.Top());printf("%d\n", s.Size());s.Pop();s.Pop();printf("%d\n", s.Top());printf("%d\n", s.Size());s.Destroy();return 0;
}

在C++中,通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装。在使用时就像使用自己的成员一样,更符合人对一件事物的认知。 而且和C语言相比,每个方法不需要传递Stack*的参数,编译器在编译之后会将该参数自动还原,即C++中Stack*参数是编译器维护的,C语言中需要用户自己维护。

10 类的默认成员函数

之前我们说过,如果一个类中什么成员都没有,简称为空类。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数指的就是用户没有显式实现,但是编译器会生成的成员函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11 构造函数

11.1 构造函数的概念

我们以下面一个描述日期的类Date为例:

#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, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

对于Date类,可以通过公有方法Init给对象设置日期,但如果每次创建对象时都调用该方法设置信息,还是有点麻烦。那能否在对象创建时,就将信息设置进去呢? C++中,引入了构造函数来解决这个问题。

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

需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象

11.2 构造函数的特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载,也就是说构造函数允许对象有多种初始化的方式。

例:

#include <iostream>
using namespace std;
class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数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; // 调用无参构造函数Date d2(2015, 1, 1); // 调用带参的构造函数Date d3();d1.Print();d2.Print();// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象//d3.Print(); // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

例:

#include <iostream>
using namespace std;
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;d1.Print();return 0;
}

放开前运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

放开后运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 由于C++把类型分成内置类型(如:int/char等)和自定义类型(如使用class/struct/union等自己定义的类型),而C++的语法又规定编译器生成的默认构造函数不会对内置类型进行处理,也就是说对于内置类型的成员,虽然调用了默认构造函数但是依旧是随机值,而对于自定义类型的成员则会去调用它的默认构造函数。

注意:不传参数就可以调用的构造函数就叫默认构造函数,一般建议每个类都提供一个默认构造函数。

例:

#include <iostream>
using namespace std;
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}/*Date(){cout << "Date()" << endl;}*/
private://基本类型int _year;int _month;int _day;//自定义类型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

注意:C++11中针对内置类型成员不初始化的缺陷打了补丁,打了补丁后内置类型成员变量在类中声明时可以给默认值。

例:

#define _CRT_SECURE_NO_WARNINGS	1
//构造函数缺陷
#include <iostream>
using namespace std;
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}/*Date(){cout << "Date()" << endl;}*/
private://基本类型型(内置类型)int _year = 2023;int _month = 10;int _day = 3;//自定义类型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 也就是说,如果既写了无参构造函数又写了全缺省的构造函数,那么编译的时候编译器会报错。

例:

#include <iostream>
using namespace std;
class Date
{
public://无参的构造函数Date(){_year = 1900;_month = 1;_day = 1;}//全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};void Test()
{Date d1;
}
int main()
{Test();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

12 析构函数

12.1 析构函数的概念

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

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

12.2 析构函数的特性

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

例:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}
int main()
{TestStack();return 0;
}
  1. 由于内置类型成员的销毁不需要资源清理,是最后由系统直接将其内存回收,所以不需要调用析构函数;而对于自定义类型的成员则需要调用它的析构函数,不过这个自定义类型成员的析构函数不能被直接调用,而是由包含这个自定义类型成员的类的析构函数调用。换言之,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数即可;而有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

例:

#include <iostream>
using namespace std;
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;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,在main函数中根本没有直接创建Time类的对象,但是最后还是调用了Time类的析构函数,这就是因为main函数中创建了Date类对象d,而d中包含了4个成员变量,其中_year_month, _day三个是内置类型成员,销毁时不需要资源清理,而_tTime类对象,所以在销毁d时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,所以编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,也就是说当Date的对象销毁时,要保证其内部每个自定义对象都能被正确销毁。
总结:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数。

13 拷贝构造函数

13.1 拷贝构造函数的概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

而在C++中,拷贝构造函数就可以实现创建一个与已存在对象一模一样的新对象。

13.2 拷贝构造函数的特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数只有单个形参,该形参只能是对本类类型对象的引用(一般常用const修饰),而且在用已存在的类类型对象创建新对象时由编译器自动调用,如果使用传值方式进行传参那么编译器会直接报错,因为C++规定自定义类型的传值需要去调用拷贝构造函数,也就是在使用传值方式进行传参的过程中会调用拷贝构造函数,而由于这个拷贝构造函数是以传值方式实现的受C++语法的限制会又调用拷贝构造函数,层层调用最终导致无穷递归调用。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date d)       //错误写法:编译报错,会引发无穷递归//{//    _year = d._year;//    _month = d._month;//    _day = d._day;//   cout << "Date(const Date d)" << endl;//}Date(const Date& d)      // 正确写法{_year = d._year;_month = d._month;_day = d._day;cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(d1);return 0;
}

错误写法运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原因图解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正确写法运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果没有显式定义拷贝构造函数,那么编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。其中内置类型按照字节方式直接拷贝,而自定义类型则调用其拷贝构造函数完成拷贝

例:

#include <iostream>
using namespace std;
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}void Print(){cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}
private://基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;//自定义类型Time _t;
};int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);d2.Print();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝,那么对于所有的类是不是都不需要自己来显式实现呢?我们可以通过下面的类来感受一下:

#include <iostream>
using namespace std;
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;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们以同样的方式对Stack类的对象s2进行拷贝构造时,程序崩溃了,这是什么原因呢?

![![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=C%3A%5CUsers%5CHackerKevin%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-](https://img-blog.csdnimg.cn/64088cfcaf1a48e4bbae1b869341bd97.png)
20231003203650522.png&pos_id=img-LibCRJDs-1700659738351)

我们可以通过上图帮助我们理解崩溃的原因。在main函数中,s1对象通过调用构造函数创建,而在构造函数中,默认申请了10个元素的空间,然后将1、2、3、4存了进去。

在后续构造s2对象的过程中,由于s2对象使用s1拷贝构造,而Stack类没有显式定义拷贝构造函数,所以编译器会给Stack类生成一份默认的拷贝构造函数,而又因为默认拷贝构造函数是按照值进行拷贝的,也就是说默认拷贝构造函数会将s1中的内容原封不动地拷贝到s2中,所以s1s2指向了同一块内存空间。

当程序退出时,s2s1都要销毁。而根据析构“后进先出”(即后创建的先销毁)的原则,s2将先被销毁,此时s2销毁时调用析构函数已经将0x11223344的空间释放了,但是s1中仍然指向0x11223344这块空间,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,必然会造成程序崩溃。

结论:类中一旦涉及到资源申请时,一定要写拷贝构造函数,否则就是浅拷贝;而类中没有涉及资源申请时,写还是不写拷贝构造函数都可以。

  1. 拷贝构造函数典型调用场景:
    1. 使用已存在对象创建新对象
    2. 函数参数类型为类类型对象
    3. 函数返回值类型为类类型对象

例:

#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;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

程序解读:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结:为了提高程序效率,一般对象传参时,尽量使用引用类型;返回时根据实际场景,能用引用尽量使用引用。

14 赋值运算符重载

14.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,让自定义类型对象也可以使用运算符。

运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名为:operator + 需要重载的运算符符号

函数原型:返回值类型 + operator +(参数列表)

注意:

  1. 不能通过连接其他符号来创建新的操作符,比如operator@
  2. 重载操作符必须有一个类类型参数。
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
  4. 作为类成员函数重载时,其形参看起来比实际操作数数目少1,但是成员函数里还隐藏了一个this参数。
  5. 特别注意:.*::sizeof?:.这5个运算符不能重载,这个经常在笔试选择题中出现。

例:用全局的operator==实现判断Date类相等:

//全局的operator==
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_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._day;
}
void Test()
{Date d1(2023, 9, 27);Date d2(2023, 9, 27);Date d3(2023, 9, 27);Date d4(2023, 9, 26);cout << (d1 == d2) << endl;//d1 == d2会被转换成operator==(d1,d2)cout << (d3 == d4) << endl;
}int main()
{Test();return 0;
}

成员变量为私有时运行结果:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

成员变量为公有时运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里会发现运算符重载成全局的就需要成员变量是公有的,但如果这样的话封装性就无法得到保证了。这里其实可以用我们后面学习的友元解决,或者干脆重载为成员函数。

例:

//重载为成员函数
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,成员函数都有一个默认的隐藏参数,即左操作数是this,指向调用函数的对象bool operator==(const Date & d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2(2023, 9, 27);Date d3(2023, 9, 27);Date d4(2023, 9, 26);cout << (d1 == d2) << endl;cout << (d3 == d4) << endl;
}
int main()
{Test();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.1.1 运算符重载的复用

刚才我们实现了判断Date类相等的函数operator==,那当我们还想实现诸如operator>operator<operator>=这样逻辑相似的函数时,如果每一个函数都要单独写一段代码进行实现,那未免也太麻烦了,有没有什么简化的方法呢?

这里我们就可以通过对运算符重载的复用来实现,还是以Date类为例,要实现所有的比较关系的话,我们实际上只需在实现operator==的基础上,再实现一个operator<或者operator>即可:

	bool operator==(const Date & d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator<(const Date& d){return _year < d._year|| (_year == d._year && _month < d._month)|| (_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);}bool operator!=(const Date& d){return !(*this == d);}

可以看到,上面的代码只具体实现了operator==operator<,其他的关系直接通过这两个函数的复用就实现了,以operator>为例,operator>就是通过复用operator<=,然后对它的判断结果进行取反来进行实现的。

实际上,上面这一套判断逻辑,对所有的类均适用

14.2 赋值运算符重载

以往赋值运算符=只能在内置类型之间使用,而如果要让自定义类型也能通过=进行赋值,就需要对赋值运算符进行重载。

有了刚才实现运算符重载的经验,那我们实现赋值运算符的重载实际上也没有什么难度。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void operator=(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;
};
void Test()
{Date d1(2023, 9, 27);Date d2;d1.Print();d2.Print();d2 = d1;d1.Print();d2.Print();
}
int main()
{Test();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,我们设计的赋值运算符重载实现了它的功能,但实际上当前设计的还是存在缺陷的,比较突出的一点就是它不支持连续赋值,因为它的返回类型是void

要实现连续赋值,那么它应该返回当前被赋值的对象,也就是返回左操作数的值。除此之外,我们还应该考虑到自己给自己赋值的情况,尤其在需要深拷贝时,会降低程序运行的效率,所以遇到这种情况时,我们直接返回即可。那么对于刚才的operator=函数我们可以进行如下改造:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}/* void operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;}*/bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator!=(const Date& d){return !(*this == d);}Date& operator=(const Date& d)//支持连续赋值的重载赋值运算符{if (this != &d)//地址不一样时才赋值{_year = d._year;_month = d._month;_day = d._day;return *this;} }void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2;Date d3;d1.Print();d2.Print(); d3.Print();d3 = d2 = d1;d1.Print();d2.Print();d3.Print();
}
int main()
{Test();return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,改造后的operator=函数就支持连续赋值了。

需要注意的是,赋值运算符只能重载成类的成员函数而不能重载成全局函数,原因在于赋值运算符如果不显式实现,那么编译器就会生成一个默认的赋值运算符重载,此时如果用户再在类外自己实现一个全局的赋值运算符重载,那么就和编译器在类中生成的默认赋值运算符重载冲突了,所以赋值运算符重载只能是类的成员函数

例:

#include <iostream>
using namespace std;
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;
}
void Test()
{Date d1(2023, 9, 27);Date d2;Date d3;d3 = d2 = d1;
}
int main()
{Test();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里还需要注意的是,由编译器生成的默认赋值运算符重载,是以值的方式逐字节拷贝,也就是说,对于内置类型成员变量是直接赋值的,但是对于自定义类型成员变量则需要调用对应类的赋值运算符重载才能完成赋值。

例:

#include <iostream>
using namespace std; 
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}//Time& operator=(const Time& t)  ////{//	if (this != &t)//	{//		_hour = t._hour;//		_minute = t._minute;//		_second = t._second;//	}//	return *this;//}void Print(){cout << _hour << "时" << _minute << "分" << _second << "秒" << 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 Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2(2023, 10, 4);d1.Print();d2.Print();d1 = d2;d1.Print();d2.Print();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以,虽然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,但是对于一些涉及到资源管理的类,则必须要自己实现赋值运算符的重载,否则会出现无法预料的结果。

例:

#include <iostream>
using namespace std;
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;s2 = s1;return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们以同样的方式对Stack类的对象s2进行拷贝构造时,程序崩溃了,原因就在于Stack类中涉及到了资源管理,而Stack的赋值运算符重载又是依靠编译器实现的。

图解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论:

  1. 赋值运算符重载格式:
    1. 参数类型:const T&,传递引用可以提高传参效率。
    2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
    3. 检测是否自己给自己赋值。
    4. 返回*this:要复合连续赋值的含义。
  2. 赋值运算符只能重载成类的成员函数而不能重载成全局函数。
  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
  4. 如果类中未涉及到资源管理,那么赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自行实现。
14.2.1 赋值运算符重载和拷贝构造之间的辨析

我们通过下面这段代码来感受一下赋值运算符重载和拷贝构造的区别:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator!=(const Date& d){return !(*this == d);}Date& operator=(const Date& d)//支持连续赋值的重载赋值运算符{if (this != &d)//地址不一样时才赋值{_year = d._year;_month = d._month;_day = d._day;return *this;} }void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2 = d1;//拷贝构造d1.Print();d2.Print();Date d3;d3 = d1;//赋值重载cout << "--------------------------------" << endl;d1.Print();d2.Print();d3.Print();
}
int main()
{Test();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于赋值重载是在两个已经定义好的对象之间进行的,虽然Date d2 = d1;这条语句中用了赋值重载运算符=,但是这条语句的意思是用d1来初始化d2,也就是用一个已经定义好的对象来初始化一个正在定义的对象,所以Date d2 = d1;这条语句实际上是拷贝构造,而d3 = d1;这条语句才是赋值重载。

14.3 前置++和后置++重载

前置++和后置++的重载之所以要单独拎出来讲,是因为它们和运算符+-相比,有需要注意的地方。

由于前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}//后置++:返回+1之前的结果// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d;Date d1(2022, 1, 13);d = d1++;d.Print();d1.Print();cout << "-------------------------" << endl;d = ++d1;d.Print();d1.Print();return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

15 const成员函数

在引出const函数之前,我们先来看下面这种情况:

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << _a << endl;}
private:int _a = 10;
};
int main()
{A aa;//const A aa;aa.Print();return 0;
}

const修饰aa前的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

const修饰aa后的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当对象aa没有被const修饰时,它能够顺利运行,但是当aaconst修饰后再运行编译器就报错了。

之所以会报错,是因为这里涉及到一个权限被放大的问题。

在这个例子中,当我们把aa传进Print函数时,本质上传的是aa的地址,aa在没有被const修饰前,&aa的类型为A*,而Print的隐藏参数this的类型为A* const,也就是说当把aa传进Print函数后this的指向是不能被改变的,这是个权限缩小的过程,所以编译器允许;而当aaconst修饰后,&aa的类型就成了const A*,也就是说这个时候aa是不能被修改的,但传进Print函数后却反而可以被修改了,这个过程就把this的权限放大了,而这是不被编译器所允许的。

又由于this是隐藏的参数,我们没有办法进行修改,所以我们只能对函数用const进行修饰,那么我们就将const修饰的成员函数称为const成员函数,这个const实际修饰的是成员函数隐藏的this指针,修饰后this的类型就变成了const A*

虽然我们实际情况下很少在定义的时候用const修饰变量,但是像下面的情况却并不少见:

#include <iostream>
using namespace std;
class A
{
public:void Print() const{cout << _a << endl;}
private:int _a = 10;
};
void Func(const A& x)
{x.Print();
}
int main()
{A aa;Func(aa);return 0;
}

const修饰Print函数前的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

const修饰Print函数后的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们把对象传给某个参数被const修饰的函数,而这个函数的内部所调用的函数却没有被const修饰时,就容易出错。

因此,只要函数内部不对成员变量进行改变一般都建议用const修饰一下,加上之后const对象和普通对象都可以调用。

16 取地址及const取地址操作符重载

对于这两个操作符一般不需要重载,使用编译器生成的默认取地址的重载即可。

例:

#include <iostream>
using namespace std;
class A
{
public:/*A* operator&(){cout << "My &:";return this;}const A* operator&() const{cout << "My const&:";return this;}*/
private:int _a = 10;
};
int main()
{A aa;const A bb;cout << &aa << endl;cout << &bb << endl;return 0;
}

使用编译器默认生成的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用自己写的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

只有特殊情况,才需要重载,比如想让别人获取到指定的内容:

#include <iostream>
using namespace std;
class A
{
public:A* operator&(){return nullptr;//拒绝取地址}const A* operator&() const{return nullptr;//拒绝取地址}
private:int _a = 10;
};
int main()
{A aa;const A bb;cout << &aa << endl;cout << &bb << endl;return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/160180.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python BDD之Behave测试报告

behave 本身的测试报告 behave 本身提供了四种报告格式&#xff1a; pretty&#xff1a;这是默认的报告格式&#xff0c;提供颜色化的文本输出&#xff0c;每个测试步骤的结果都会详细列出。plain&#xff1a;这也是一种文本格式的报告&#xff0c;但没有颜色&#xff0c;并且…

电动汽车充放电V2G模型MATLAB代码

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 主要内容&#xff1a; 本程序主要建立电动汽车充放电V2G模型&#xff0c;采用粒子群算法&#xff0c;在保证电动汽车用户出行需求的前提下&#xff0c;为了使工作区域电动汽车尽可能多的消纳供给商场基础负荷…

【办公常识】写好的代码如何上传?使用svn commit

首先找到对应的目录 找到文件之后点击SVN Commit

五大资源之Service(可以固定IP)

Service可以看作是一组同类Pod对外访问接口,借助Service应用可以方便的实现服务发现与负载均衡 创建集群内部可以访问Service #暴露Service(也创建在了namespace dev下) [root@master ~]# kubectl expose deployment(pod控制器) nginx --name=svc-nginx1 --type=Cluste…

基于原子轨道搜索算法优化概率神经网络PNN的分类预测 - 附代码

基于原子轨道搜索算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于原子轨道搜索算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于原子轨道搜索优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xf…

Python入门教学——输入任意长度的int整型一维数组

使用python输入一个任意长度的整型一维数组&#xff1a; nums input("请输入整数数组&#xff0c;用空格分隔&#xff1a; ") nums [int(i) for i in nums.split( )] # 将每个数转换为整型后输出 运行结果&#xff1a; 【注】如果不强制转换类型&#xff0c;数字…

如何使用YOLOv8代码框架中的RT-DETR

1. RT-DETR RT-DETR是由由此&#xff0c;百度推出了——RT-DETR (Real-Time DEtection TRansformer) &#xff0c;一种基于 DETR 架构的实时端到端检测器&#xff0c;其在速度和精度上取得了 SOTA 性能。 RT-DETR开源的代码在百度自己的飞桨paddlepaddle上&#xff0c;因此非…

基于天鹰算法优化概率神经网络PNN的分类预测 - 附代码

基于天鹰算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于天鹰算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于天鹰优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

新手必看!!附源码!!STM32通用定时器输出PWM

一、什么是PWM? PWM&#xff08;脉冲宽度调制&#xff09;是一种用于控制电子设备的技术。它通过调整信号的脉冲宽度来控制电压的平均值。PWM常用于调节电机速度、控制LED亮度、产生模拟信号等应用。 二、PWM的原理 PWM的基本原理是通过以一定频率产生的脉冲信号&#xff0…

实时错误’-2147217887‘多步OLB DB 操作产生错误。如果可能,请检查OLE DB状态值

目录 背景问题问题分析问题解决 错误解决与定位技巧总结 背景 仍旧是学生信息管理系统的问题&#xff0c;当时做的时候没发现这么多问题呢&#xff0c;只能说明一件事&#xff0c;做的时候没有站在用户的角度考虑需求&#xff0c;设置了什么内容&#xff0c;就按照设置好的去测…

AIGC ChatGPT4总结SQL优化细节操作

数据库SQL优化是一个复杂的过程,它通常涉及到许多不同的技术和方法。以下是一些常用的SQL优化策略: 1. **索引使用**:索引可以极大地加速查询速度。但是,索引并不总是有好处的,因为它们需要额外的空间来存储,并且在插入和更新数据时可能会减慢速度。因此,选择正确的字段…

Unity中Shader纹理的过滤

文章目录 前言一、为什么要过滤&#xff1f;二、过滤方式1、Point(no filter) 无过滤2、Bilinear 双线性过滤3、Trilinear 三线性过滤 前言 Unity中Shader纹理的过滤 一、为什么要过滤&#xff1f; 事实上没有一个纹理上的纹素是与屏幕上的像素是一一对应的。 屏幕上的 一个…

redis安装(Windows和linux)

如何实现Redis安装与使用的详细教程 Redis 简介 Redis是一个使用C语言编写的开源、高性能、非关系型的键值对存储数据库。它支持多种数据结构&#xff0c;包括字符串、列表、集合、有序集合、哈希表等。Redis的内存操作能力极强&#xff0c;其读写性能非常优秀&#xff0c;且…

万字解析:十大排序(直接插入排序+希尔排序+选择排序+堆排序+冒泡排序+快速排序+归并排序+计数排序+基数排序+桶排序)

文章目录 十大排序排序算法复杂度及稳定性分析一、 排序的概念1.排序&#xff1a;2.稳定性&#xff1a;3.内部排序&#xff1a;4.外部排序&#xff1a; 二、插入排序1.直接插入排序2.希尔排序 三、选择排序1.直接选择排序方法一方法二直接插入排序和直接排序的区别 2.堆排序 四…

【蓝桥杯省赛真题45】Scratch九宫格游戏 蓝桥杯scratch图形化编程 中小学生蓝桥杯省赛真题讲解

目录 scratch九宫格游戏 一、题目要求 编程实现 二、案例分析 1、角色分析

轻量封装WebGPU渲染系统示例<37>- 多个局部点光源应用于非金属材质形成的效果(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/rendering/src/voxgpu/sample/BasePbrMaterialMultiLights.ts 当前示例运行效果: 此示例基于此渲染系统实现&#xff0c;当前示例TypeScript源码如下&#xff1a; export class BasePbrMaterial…

2023年09月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 点击绿旗,运行程序后,舞台上的图形是?( ) A:画笔粗细为4的三角形 B:画笔粗细为5的六边形 C:画笔粗细为4的六角形 D:画笔粗细为5的三角形 答案:D 第2题 如下图所示,从所给…

缓存雪崩、击穿、穿透_解决方案

文章目录 缓存雪崩、击穿、穿透1.缓存雪崩造成缓存雪崩解决缓存雪崩 2. 缓存击穿造成缓存击穿解决缓存击穿 3.缓存穿透造成缓存穿透解决缓存穿透 缓存雪崩、击穿、穿透 一般用户数据存储于磁盘&#xff0c;读写速度慢。 使用redis作为缓存&#xff0c;相当于数据缓存在内存&a…

年底了,我劝大家真别轻易离职...

年底了&#xff0c;一些不满现状&#xff0c;被外界的“高薪”“好福利”吸引的人&#xff0c;一般就在这时候毅然决然地跳槽了。 在此展示一套学习笔记 / 面试手册&#xff0c;年后跳槽的朋友可以好好刷一刷&#xff0c;还是挺有必要的&#xff0c;它几乎涵盖了所有的软件测试…

银河麒麟V10-ARM架构-postgresql安装与部署指南

提示&#xff1a;本人长期接收外包任务。 前言 本文详细介绍应用源码进行pgsql的安装步骤&#xff0c;本文以postgresql-12.0为例。 一、下载并解压安装包 ☆下载地址&#xff1a;https://ftp.postgresql.org/pub/source/ 解压安装包&#xff0c;创建安装路径&#xff1a; …