二、类与对象(二)
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类中有Init与Print两个成员函数,函数体中并没有关于不同对象的区分,那么当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象的呢?
C++通过引入this指针来解决这个问题。实际上,C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
8.2 this指针的特性
this指针的类型:类的类型 const*,所以成员函数中,不能给this指针赋值。this指针只能在成员函数的内部使用。this指针本质上是成员函数的形参,所以this指针是存储在栈中的。当对象调用成员函数时,函数将对象地址作为实参传递给this形参。所以对象中不存储this指针。this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。

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相关操作函数有以下共性:
- 每个函数的第一个参数都是
Stack*。 - 函数中必须要对第一个参数检测,因为该参数可能会为
NULL。 - 函数中都是通过
Stack*参数操作栈的. - 调用时必须传递
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 构造函数的特性
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载,也就是说构造函数允许对象有多种初始化的方式。
例:
#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;
}
输出结果:

- 如果类中没有显式定义构造函数,则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;
}
放开前运行结果:

放开后运行结果:

- 由于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;
}
输出结果:

- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 也就是说,如果既写了无参构造函数又写了全缺省的构造函数,那么编译的时候编译器会报错。
例:
#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 析构函数的特性
- 析构函数名是在类名前加上字符
~。 - 无参数无返回值。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,也就是说,析构函数不能重载。
- 对象生命周期结束时,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;
}
- 由于内置类型成员的销毁不需要资源清理,是最后由系统直接将其内存回收,所以不需要调用析构函数;而对于自定义类型的成员则需要调用它的析构函数,不过这个自定义类型成员的析构函数不能被直接调用,而是由包含这个自定义类型成员的类的析构函数调用。换言之,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数即可;而有资源申请时,一定要写,否则会造成资源泄漏,比如
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三个是内置类型成员,销毁时不需要资源清理,而_t是Time类对象,所以在销毁d时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,所以编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,也就是说当Date的对象销毁时,要保证其内部每个自定义对象都能被正确销毁。
总结:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数。
13 拷贝构造函数
13.1 拷贝构造函数的概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
而在C++中,拷贝构造函数就可以实现创建一个与已存在对象一模一样的新对象。
13.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;
}
错误写法运行结果:

原因图解:

正确写法运行结果:

- 如果没有显式定义拷贝构造函数,那么编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。其中内置类型按照字节方式直接拷贝,而自定义类型则调用其拷贝构造函数完成拷贝。
例:
#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进行拷贝构造时,程序崩溃了,这是什么原因呢?

20231003203650522.png&pos_id=img-LibCRJDs-1700659738351)](https://img-blog.csdnimg.cn/acc2ea99a8284387af3ec91303b32683.png)
我们可以通过上图帮助我们理解崩溃的原因。在main函数中,s1对象通过调用构造函数创建,而在构造函数中,默认申请了10个元素的空间,然后将1、2、3、4存了进去。
在后续构造s2对象的过程中,由于s2对象使用s1拷贝构造,而Stack类没有显式定义拷贝构造函数,所以编译器会给Stack类生成一份默认的拷贝构造函数,而又因为默认拷贝构造函数是按照值进行拷贝的,也就是说默认拷贝构造函数会将s1中的内容原封不动地拷贝到s2中,所以s1和s2指向了同一块内存空间。
当程序退出时,s2和s1都要销毁。而根据析构“后进先出”(即后创建的先销毁)的原则,s2将先被销毁,此时s2销毁时调用析构函数已经将0x11223344的空间释放了,但是s1中仍然指向0x11223344这块空间,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,必然会造成程序崩溃。
结论:类中一旦涉及到资源申请时,一定要写拷贝构造函数,否则就是浅拷贝;而类中没有涉及资源申请时,写还是不写拷贝构造函数都可以。
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
例:
#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 +(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符,比如operator@。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型
+,不能改变其含义。 - 作为类成员函数重载时,其形参看起来比实际操作数数目少
1,但是成员函数里还隐藏了一个this参数。 - 特别注意:
.*、::、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的赋值运算符重载又是依靠编译器实现的。
图解:

结论:
- 赋值运算符重载格式:
- 参数类型:const T&,传递引用可以提高传参效率。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
- 检测是否自己给自己赋值。
- 返回
*this:要复合连续赋值的含义。
- 赋值运算符只能重载成类的成员函数而不能重载成全局函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
- 如果类中未涉及到资源管理,那么赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自行实现。
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修饰时,它能够顺利运行,但是当aa被const修饰后再运行编译器就报错了。
之所以会报错,是因为这里涉及到一个权限被放大的问题。
在这个例子中,当我们把aa传进Print函数时,本质上传的是aa的地址,aa在没有被const修饰前,&aa的类型为A*,而Print的隐藏参数this的类型为A* const,也就是说当把aa传进Print函数后this的指向是不能被改变的,这是个权限缩小的过程,所以编译器允许;而当aa被const修饰后,&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;
}
运行结果:

相关文章:
二、类与对象(二)
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 <&l…...
Pytorch从零开始实战10
Pytorch从零开始实战——ResNet-50算法实战 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——ResNet-50算法实战环境准备数据集模型选择开始训练可视化模型预测总结 环境准备 本文基于Jupyter notebook,使用Python3.8,…...
设计模式-单例模式实战
目录 一、引言二、适用场景三、代码实战饿汉式单例模式懒汉式单例模式双重检查锁定单例模式静态内部类单例模式 四、实际应用举例Runtime解析 五、结论 一、引言 单例模式是一种创建型设计模式,用于确保一个类只有一个实例,且提供全局访问点以访问该实例…...
requests库出现AttributeError问题的修复与替代方法
在使用App Engine时,开发者们通常会面临需要发送爬虫ip请求的情况,而Python中的requests库是一个常用的工具,用于处理爬虫ip请求。然而,在某些情况下,开发者可能会遇到一个名为AttributeError的问题,特别是…...
opencv-2D直方图
cv2.calcHist() 是 OpenCV 中用于计算直方图的函数。它可以计算一维或多维直方图,用于分析图像中像素值的分布。 基本的语法如下: hist cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])参数说明: images:…...
读像火箭科学家一样思考笔记06_初学者之心
1. 专业化是目前流行的趋势 1.1. 通才(generalist)是指博而不精之人 1.2. 懂得的手艺越多,反而会家徒四壁 1.2.1. 希腊谚语 1.3. 这种态度代价很大,它阻断了不同学科思想的交融 2. 组合游戏 2.1. 某个行业的变革可能始于另一…...
中职组网络安全 Server-Hun-1.img Server-Hun-2.img
一串密码 smbuser用户和密码登录ssh还是失败提示需要密钥,尝试ftp登录成功 发现密钥存放在.ssh/下,在kali上生成一个密钥,通过上传到.ssh/下,将其替换掉 使用kali生成密钥 登录成功,但是无法拿到root目录下的flag 获取root用户权限…...
基于区域划分的GaN HEMT 准物理大信号模型
GaN HEMT器件的大信号等效电路模型分为经验基模型和物理基模型。经验基模型具有较高精度但参数提取困难,特别在GaN HEMT器件工艺不稳定的情况下不易应用。相比之下,物理基模型从器件工作机理出发,参数提取相对方便,且更容易更新和…...
laravel引入element-ui后,blade模板中使用elementui时,事件未生效问题(下载element-ui到本地直接引入项目)
背景 重构公司后台项目,使用了dcat-admin,但是dcat-admin有些前端功能不能满足需求。因此引入element-ui进行相关界面的优化 具体流程 1.下载element-ui到本地 2.进入如下目录 打开 node_modules\element-ui\lib 复制index.js 打开 node_modules/ele…...
【计算机网络笔记】路由算法之层次路由
系列文章目录 什么是计算机网络? 什么是网络协议? 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能(1)——速率、带宽、延迟 计算机网络性能(2)…...
【华为OD机试python】分糖果【2023 B卷|100分】
【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述 小明从糖果盒中随意抓一把糖果,每次小明会取出一半的糖果分给同学们。 当糖果不能平均分配时,小明可以选择从糖果盒中(假设盒中糖果足够) 取出一个糖果或放回一个糖果。 小明最少需要多…...
ARM 汇编基础
我们在学习 STM32 的时候几乎没有用到过汇编,可能在学习 UCOS 、 FreeRTOS 等 RTOS 类操作系统移植的时候可能会接触到一点汇编。但是我们在进行嵌入式 Linux 开发的时候是绝 对要掌握基本的 ARM 汇编,因为 Cortex-A 芯片一上电 SP 指针还…...
虹科Pico汽车示波器 | 汽车免拆检修 | 2017款东风本田XR-V车转向助力左右不一致
一、故障现象 一辆2017款东风本田XR-V车,搭载R18ZA发动机,累计行驶里程约为4万km。车主反映,车辆行驶或静止时,向右侧转向比向左侧转向沉重。 二、故障诊断 接车后试车,起动发动机,组合仪表上无故障灯点亮&…...
阿里云服务器ECS经济型e实例优惠99元性能怎么样?
阿里云服务器ECS经济型e实例优惠99元性能怎么样?阿里云服务器优惠99元一年,配置为云服务器ECS经济型e实例,2核2G配置、3M固定带宽和40G ESSD Entry系统盘,CPU采用Intel Xeon Platinum架构处理器,2.5 GHz主频࿰…...
vue3引入vuex基础
一:前言 使用 vuex 可以方便我们对数据的统一化管理,便于各组件间数据的传递,定义一个全局对象,在多组件之间进行维护更新。因此,vuex 是在项目开发中很重要的一个部分。接下来让我们一起来看看如何使用 vuex 吧&#…...
C++二维数组中的查找
4. 二维数组中的查找 题目链接 牛客网 题目描述 给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 Consider the following matrix: [[1, 4, 7, 11, 15],[2, 5, 8, 12, 19],[3, 6, 9, 16, 22],[1…...
【计算思维】蓝桥杯STEMA 科技素养考试真题及解析 2
1、兰兰有一些数字卡片,从 1 到 100 的数字都有,她拿出几张数字卡片按照一定顺序摆放。想一想,第 5 张卡片应该是 A、11 B、12 C、13 D、14 答案:C 2、按照下图的规律,阴影部分应该填 A、 B、 C、 D、 答案&am…...
Qt+sqlite3使用事务提升插入效率
参考: 【精选】SQLite批量插入效率_sqlite 批量插入_PengX_Seek的博客-CSDN博客 (1)不使用事务时: clock_t t_start clock();QSqlQuery query(db);QString sql("insert into test(col1,col2) values(1,2);");for (int i 0; i < 1000; i…...
【深度学习】不用Conda在PP飞桨Al Studio三个步骤安装永久PyTorch环境
在 PaddlePaddle AI Studio 中使用 Python 虚拟环境安装 PyTorch 免责声明 在阅读和实践本文提供的内容之前,请注意以下免责声明: 侵权问题: 本文提供的信息仅供学习参考,不用做任何商业用途,如造成侵权,请私信我&am…...
SpringBoot:kaptcha生成验证码
GitHub项目地址:GitHub - penggle/kaptcha: kaptcha - A kaptcha generation engine. kaptcha介绍 kaptcha官网(Google Code Archive - Long-term storage for Google Code Project Hosting.)对其介绍如下, kaptcha十分易于安装…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
DAY 47
三、通道注意力 3.1 通道注意力的定义 # 新增:通道注意力模块(SE模块) class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...
MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
