C++:类和对象(中)
文章目录
- 1 类的6个默认成员函数
- 2 构造函数
- 2.1 概念
- 2.2 特性
- 3 析构函数
- 3.1 概念
- 3.2 特性
- 4 拷贝构造函数
- 4.1 概念
- 4.2 特性
- 5 赋值运算符重载
- 5.1 运算符重载
- 5.2 赋值运算符重载
- 5.3 前置++重载和后置++重载
- 6 日期类的实现
- 7 const成员
- 8 取地址及const取地址操作符重载
1 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类(如:
class Date{};)。那么空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。

2 构造函数
2.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;
}
我们可以通过Init公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,而且有时还可能因为忘记使用该方法进行对象信息初始化设置而直接使用对象造成未预期的后果(如:我们实现一个栈,如果忘记初始化而直接使用,就可能造成越界访问等问题)。
基于此,C++中引入了构造函数的概念,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但其主要任务并不是开空间创建对象,而是初始化对象。
其特征包括:
- 函数名与类名相同。
- 无返回值(这里无返回值并不是指返回类型为void,事实上,构造函数不需要加返回类型,直接类名即可)。
- 对象实例化时编译器
自动调用对应的构造函数。 - 构造函数可以重载。 表示一个类中可以有多个构造函数,或者说可以有多种初始化方式。
示例代码:
class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};void TestDate()
{Date d1; // 调用无参构造函数Date d2(2023, 1, 1); // 调用带参的构造函数//Date d3();//不是创建对象,而是函数声明//d4.Date();//?
}int main() {TestDate();return 0;
}
注意:
- 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如对于代码
Date d3();,其表示的是声明了d3函数,该函数无参,返回一个日期类型的对象,如果运行该代码,则会报出警告如下:warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)

- 不能用对象去调用构造函数,如
d4.Date(),且不说在调用之前d4对象是否已经创建了出来(没有创建的话就无法调用成员函数),就算d4是一个已经创建好的对象,如果再用对象去调用构造函数是否是多此一举了,也违背了对于优化C语言中需要自己调用初始化函数问题的初衷。
- 如果类中没有显式定义构造函数,则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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用Date d1;return 0;
}
- 关于编译器生成的默认成员函数,或许有人会疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象的成员变量_year/_month/_day依旧是随机值。也就是说这里编译器生成的默认构造函数并没有什么用?那究竟这个编译器生成的默认构造函数中实现了什么?
解释: C++把类型分成了内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…;自定义类型就是我们使用class/struct/union等自己定义的类型。我们看下面的程序,运行后可以发现编译器生成的默认构造函数会调用自定义类型成员 _t 的默认构造函数。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour;int _minute;int _second;
};class Date
{
public:void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d;d.ShowInfo();return 0;
}
程序运行结果:

既然编译器默认生成的构造函数都对自定义类型进行处理了,那为什么不对内置类型做处理呢?关于这点,可以说这是C++的一个缺陷,基于此,C++11中针对编译器生成的默认构造器不处理(或者说不初始化)内置类型成员的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。 此时,如果在对象实例化时调用的构造函数(无论是编译器默认生成的构造函数还是自己重载的构造函数)中没有对成员变量进行初始化,则就以默认值进行初始化。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour;int _minute;int _second;
};class Date
{
public:void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year = 2023;int _month = 2;int _day = 3;// 自定义类型Time _t;
};int main()
{Date d;d.ShowInfo();return 0;
}
程序运行结果:

- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数
最多只有一个。注意:无参构造函数、全缺省构造函数、我们没写而编译器默认生成的构造函数,都可以认为是默认构造函数。
① 类中没有默认构造函数情况:
class Date
{
public://构造函数//由于我们自己定义了构造函数,因此编译器不会生成默认的无参构造函数,也就是说类中没有默认构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};// 以下测试函数能通过编译吗?
void Test()
{Date d1;
}
程序运行结果:

② 类中有多个默认构造函数情况:
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;
}
程序运行结果:

说明: 上述程序运行中断,编译器表示对重载函数的调用不明确,即不知道该调用哪一个构造函数(从语法上来说,无参构造函数和全缺省构造函数是构成函数重载的,如果我们没有调用函数,程序是可以编译通过的)。这是因为对于函数调用来说,无参构造函数和全缺省构造函数都不需要传入参数,这就意味着无论我们本意是调用哪个构造函数,由于调用时参数列表都是无参,所以经过修饰后的函数名是相同的,此时就会导致链接器在链接查找函数时不明确到底是要找哪个函数。
3 析构函数
3.1 概念
通过前面对构造函数的学习,可以知道一个对象是怎么来的,如何初始化的,那如果要销毁一个对象呢?就好比我们定义一个栈,进行栈的初始化,然后使用,最后还需要将栈销毁,对应也就是栈的销毁函数。那既然编译器都可以帮我们自动调用构造函数完成对象初始化了,那是不是也可以帮我们在使用完对象后自动进行销毁呢?没错,除了构造函数,C++中还引入了析构函数的概念。
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁(对象本身是存储在栈区的,出了作用域会自动销毁),局部对象销毁工作是由编译器完成的(就好比我们调用栈的销毁函数并不是销毁栈本身,而是释放栈中开辟的空间,同时将栈容量等信息归置)。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数 是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符
~。 - 析构函数无参数无返回值类型(以
~类名())。 - 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载** 。
- 对象生命周期结束时,C++编译系统自动调用析构函数。
typedef int DataType;
class Stack
{
public://全缺省构造函数Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");exit(-1);}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;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;
}
程序运行结果:

- 那编译器自动生成的析构函数是否会完成一些事情呢?从下面的程序我们可以看到,与编译器默认生成的构造函数类似,编译器生成的默认析构函数不会处理内置类型成员,而对自定义类型成员会调用它的析构函数。**
class Time
{
public:~Time(){cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 2023;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d;return 0;
}
程序运行结果:

分析: 程序运行后输出了 ~Time()。而在 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 类对象销毁时,要保证其内部每个自定义对象都可以正确销毁。这样 main 中并没有直接调用 Time 类的析构函数,而是显示调用编译器为 Date 类生成的默认析构函数。 注意:创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如上述 Date 类;如果有资源申请时,则一定要自己编写,在析构函数中通过指针(内置类型,如果是编译器生成的默认析构函数不会对其进行处理)等将申请的空间释放,否则会造成资源泄漏, 比如 Stack栈 中就有指向动态开辟的空间的指针,在指针被销毁前(对象生命周期结束前)应用指针释放申请的空间。
4 拷贝构造函数
4.1 概念
在现实生活中,可能存在两个一模一样的人,我们称之为双胞胎。

那在创建对象时,是否可以创建一个与已存在对象一模一样的新对象呢?
基于此,C++中引入了拷贝构造函数的概念。
拷贝构造函数:只有单个形参,该形参是对本类类型对象(待拷贝对象)的引用(一般常用 const 修饰,避免原型对象信息被修改),在用已存在的类类型对象创建新对象时由编译器 自动调用。
4.2 特性
拷贝构造函数 是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式(函数名与类名相同,无返回值类型)。
- 拷贝构造函数的参数只有一个,且必须是类类型对象的引用(规定),使用传值方式编译器直接报错(语法不通过),否则若是语法同意使用传值方式,将会引发无穷递归调用。此外,拷贝构造函数除了可以通过
目标对象(拷贝原型对象)的方式调用,还可以直接通过目标对象 = 拷贝原型对象的方式调用。
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d) //正确写法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);return 0;
}
程序运行结果:

解释:函数的形参是实参的一份临时拷贝,如果采用传值方式传参,如果参数是内置类型,那这函数声明并没有什么问题,可以正常运行;但如果参数是自定义类型对象,有些细节就需要注意:C++规定,函数采用传值传参时,对于内置类型,编译器可以直接拷贝;而对于自定义类型,需要调用其拷贝构造函数。 此时如果拷贝构造函数本身采用的是传值传参(参数是自定义类类型),那就意味着在真正调用到拷贝构造函数前,还需要先调用实参对象的拷贝构造函数得到形参对象,如此就陷入了无穷递归调用拷贝构造函数的困境。而如果是采用传引用方式传参,那形参对象就是实参对象的一个别名,不需要再调用一次拷贝构造函数。那可不可以用传指针传参呢(如:Date(const Date* pd))?这当然是可以的,传指针传参也不会有无穷递归的问题,但是采用传指针传参的方式,还需要对指针进行一次拷贝,显然还是传引用传参的方式效率更高。事实上如果采用传指针传参的方式,此时函数相当于是重载的构造函数,并不能称为拷贝构造函数。

- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数按对象存储字节大小完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public://全缺省构造函数Time(int hour = 0, int minute = 0, int second = 0){_hour = hour;_minute = minute;_second = second;}//拷贝构造函数Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}private:int _hour;int _minute;int _second;
};class Date
{
public://全缺省构造函数Date(int year = 2023, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d1(2023, 2, 3);Date d2(d1);cout << "d1:";d1.ShowInfo();cout << "d2:";d2.ShowInfo();return 0;
}
程序运行结果:

说明: 在上述代码中,我们使用已存在的日期类对象 d1 拷贝构造 d2 ,此处会调用 Date 类的拷贝构造函数,但 Date 类中并没有显式定义拷贝构造函数,则编译器会给 Date 类生成一个默认的拷贝构造函数。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的(浅拷贝),而自定义类型是调用其拷贝构造函数完成拷贝的。
- 既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那还需要自己显式实现吗?当然像日期类这样的类是没必要自己显式实现,那么下面的栈类呢?
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;
}
程序运行结果:


结果说明: 对于类中涉及资源申请的对象,调用编译器默认生成的拷贝构造函数进行值拷贝将造成一块空间被多个同类类对象使用的情况,这样对象之间对空间数据的修改会相互影响,且在最后会造成一块空间被多次释放的问题。
注意:对象本身作为局部变量也是存储在栈空间中的,其析构的顺序为:先构造的后析构,后构造的先析构

结论: 类中如果没有涉及资源申请时,拷贝构造函数是否自己显式实现都可以;一旦涉及到资源申请时,拷贝构造函数必须自己显式实现,否则就是浅拷贝。
那如果是深拷贝要如何实现呢?还是以上面的栈类为例,如下自己编写可实现深拷贝的拷贝构造函数:
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;}//拷贝构造函数Stack(const Stack& st) {_array = (int*)malloc(_capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}memcpy(_array, st._array, _capacity * sizeof(DataType));_size = st._size;_capacity = st._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;
}
程序运行结果:

- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
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;
}

建议:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能使用传引用返回就尽量使用传引用返回。
5 赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名及参数列表,其返回值类型及参数列表与普通的函数类似。
函数名: 关键字 operator 后面接需要重载的运算符符号。
函数原型: 返回值类型 operator运算符符号(参数列表)
注意:
- 不能通过连接其它符号来创建新的操作符:比如 operator@ 。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能变,例如:内置的整型
+,不能改变其含义。 - 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的
this指针。其中如果有两个操作数,则第一个参数为左操作数,第二个参数为右操作数。 .*::sizeof?:(三目比较运算符).这 5 个运算符不能重载。
示例1(全局实现):
// 全局的operator==
class Date
{
public:Date(int year = 2023, 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(2022, 9, 26);Date d2(2022, 9, 27);cout << (d1 == d2) << endl; //注意要记得加括号,因为流插入运算符 << 的优先级高于 ==//cout << operator(d1, d2) << endl; //也可采用函数调用的方式
}int main() {Test();return 0;
}//结果输出:0
示例2(作为成员函数实现):
class Date
{
public:Date(int year = 2023, 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(2022, 9, 26);Date d2(2022, 9, 27);cout << (d1 == d2) << endl; //注意要记得加括号,因为流插入运算符 << 的优先级高于 ==//cout << d1.operator(d2) << endl; //也可采用函数调用的方式
}int main() {Test();return 0;
}
//结果输出:0
5.2 赋值运算符重载
1 赋值运算符重载格式及注意事项
- 参数类型:
const 类型&,传递引用可以提高传参效率。 - 返回值类型:
类型&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。 - 检测是否自己给自己赋值。
- 返回
*this:要符合连续赋值的含义。
示例:
class Date
{
public:Date(int year = 2023, 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;}//赋值运算符重载Date& operator=(const Date& d){//检测是否给自己赋值,如果是给自己赋值,直接返回即可,避免多余操作if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}private:int _year;int _month;int _day;
};
2 赋值运算符只能重载成类的成员函数,不能重载成全局函数
class Date
{
public:Date(int year = 2023, 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;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
//时间类
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;}cout << "Time::operator=()" << endl;return *this;}private:int _hour;int _minute;int _second;
};//日期类
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d1(2022, 12, 25);Date d2(2022, 1, 1);cout << "赋值前:" << endl;d1.ShowInfo();d2.ShowInfo();cout << endl;d1 = d2;cout << endl;cout << "赋值后:" << endl;d1.ShowInfo();d2.ShowInfo();return 0;
}
程序运行结果:

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,那还需要自己实现吗?与编译器生成的默认拷贝构造函数的问题类似,或许像日期类这样的类没有必要自己实现赋值运算符重载,那下面的栈类呢?
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:DataType* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2;s2 = s1;return 0;
}
程序运行结果:程序运行中断

结果说明:

总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自己实现。
以下实现栈类的赋值运算符重载(深拷贝):
//赋值运算符重载Stack& operator=(const Stack& st) {memcpy(_array, st._array, st._size * sizeof(DataType));_size = st._size;_capacity = st._capacity;return *this;}
将上述赋值运算符重载添加到栈类中,重新运行程序,结果如下:

5.3 前置++重载和后置++重载
注意:由于前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定,后置++重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,由编译器自动传递。
以日期类为例:
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);// d = d1.operator++(0); //后置++由编译器自动传入一个整型值d = d1++; // d: 2022,1,13 d1:2022,1,14// d = d1.operator++();d = ++d1; // d: 2022,1,15 d1:2022,1,15return 0;
}
可以看到:相比于前置的 ++ 与 - - ,后置的 ++ 与 - - 的实现要多一次拷贝构造,因此建议,在不影响最终结果的情况下,尽量使用前置的 ++ 与 - - 。
6 日期类的实现
Date.h
#pragma once
#include <iostream>using namespace std;class Date {
public://获取某年某月的天数int GetMonthDay(int year, int month) {static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int day = days[month];if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {day++;}return day;}//全缺省构造函数Date(int year = 2023, int month = 1, int day = 1);//打印信息void Print();拷贝构造函数//Date(const Date& date);赋值运算符重载//Date& operator=(const Date& date);析构函数//~Date();//日期+=天数Date& operator+=(int day);//日期+天数Date operator+(int day);//日期-天数Date operator-(int day);//日期-=天数Date& operator-=(int day);//前置++Date& operator++();//后置++Date operator++(int);//后置--Date& operator--(int);//前置--Date& operator--();//>运算符重载bool operator>(const Date& date);//==运算符重载bool operator==(const Date& date);//>=运算符重载bool operator>=(const Date& date);//<运算符重载bool operator<(const Date& date);//<=运算符重载bool operator<=(const Date& date);//!=运算符重载bool operator!=(const Date& date);//日期-日期 返回天数int operator-(const Date& date);private:int _year;int _month;int _day;
};
Date.cpp
#include "Date.h"//全缺省构造函数
Date::Date(int year, int month, int day) {if (month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month)) {_year = year;_month = month;_day = day;}else {cout << "日期非法!" << endl;}
}void Date::Print() {cout << _year << "-" << _month << "-" << _day << endl;
}拷贝构造函数
//Date::Date(const Date& date) {
// _year = date._year;
// _month = date._month;
// _day = date._day;
//}
//
赋值运算符重载
//Date& Date::operator=(const Date& date) {
// if (this != &date) {
// _year = date._year;
// _month = date._month;
// _day = date._day;
// }
// return *this;
//}
//
析构函数
//Date::~Date() {
// _year = 2023;
// _month = 1;
// _day = 1;
//}//日期+=天数
Date& Date::operator+=(int day) {if (day < 0) {return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)) {_day -= GetMonthDay(_year, _month);_month++;if (_month > 12) {_year++;_month = 1;}}return *this;
}//日期+天数
Date Date::operator+(int day) {if (day < 0) {return *this - (-day);}Date temp(*this);temp += day;return temp;
}//日期-天数
Date Date::operator-(int day) {if (day < 0) {return *this + (-day);}Date temp(*this);temp -= day;return temp;
}//日期-=天数
Date& Date::operator-=(int day) {if (day < 0) {return *this += -day;}_day -= day;while (_day <= 0) {_month--;if (_month == 0) {_month = 12;_year--;}_day += GetMonthDay(_year, _month);}return *this;
}//前置++
Date& Date::operator++() {*this += 1;return *this;
}//后置++
Date Date::operator++(int) {Date temp(*this);*this += 1;return temp;
}//后置--
Date& Date::operator--(int) {Date temp(*this);*this -= 1;return temp;
}//前置--
Date& Date::operator--() {*this -= 1;return *this;
}//>运算符重载
bool Date::operator>(const Date& date) {if (_year > date._year) {return true;}else if (_year == date._year && _month > date._month) {return true;}else if (_year == date._year && _month == date._month && _day > date._day) {return true;}else {return false;}
}//==运算符重载
bool Date::operator==(const Date& date) {return _year == date._year && _month == date._month && _day == date._day;
}//>=运算符重载
bool Date::operator>=(const Date& date) {return *this > date || *this == date;
}//<运算符重载
bool Date::operator<(const Date& date) {return !(*this >= date);
}//<=运算符重载
bool Date::operator<=(const Date& date) {return !(*this > date);
}//!=运算符重载
bool Date::operator!=(const Date& date) {return _year != date._year || _month != date._month || _day != date._day;
}//日期-日期 返回天数
int Date::operator-(const Date& date) {Date max = *this;Date min = date;int flag = 1;if (max < min) {max = date;min = *this;flag = -1;}int count = 0;while (min != max) {++min;++count;}return count * flag;
}
7 const成员
将 const 修饰的 “成员函数” 称为 const成员函数, const 修饰类成员函数,实际修饰该成员函数隐含的
this指针,表明在成员函数中不能对类的任何成员进行修改。

我们再看看下面的代码:
class Date {
public:Date(int year = 2023, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}void Print() {cout << "Print()" << endl;cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}void Print()const {cout << "Print()const" << endl;cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}private:int _year;int _month;int _day;
};void Test() {Date d1(2022, 1, 13);cout << "d1:" << endl;d1.Print();cout << endl;const Date d2(2022, 2, 5);cout << "d2(const):" << endl;d2.Print();
}int main() {Test();return 0;
}
运行程序可以发现: 虽然调用函数一样,但const 对象会调用对应的 const 成员函数,而非 const 对象则调用普通成员函数。当我们将const成员函数注释掉后,此时类中只有普通成员函数,再运行程序,编译器报错:error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”,这是因为,对于编译器自动传递的隐含形参 this指针 ,其在普通成员函数参数列表中的完整声明为:类型* const this , 以日期类为例就是 Date* const this , 表示指针指向不能改变,但是并没有约束指向的日期对象中的内容不能改变,而用 cosnt 修饰的对象的成员是不能被改变的,如果再用其去调用普通成员函数的话,相当于权限被放大了,所以无法通过编译。基于 this指针 作为隐含参数是不能被更改的,即我们无法再直接用 const 去显示的修饰 *this,因此为了使 const 修饰的对象也能够调用相应的函数,引入了 const 成员函数的概念,在函数最后用 const 修饰,实际上是用 const 修饰了 *this (const Date* const this)。此外,当我们将普通成员函数注释掉,只保留对应的 const 成员函数时,非 const 修饰的对象就会去调用 const 成员函数,这里传参时则是权限缩小了,因此可以正常运行,也就是说,在有普通成员函数时,非 const 对象会优先调用非 const 成员函数,如果没有,再去调用 const 成员函数。

8 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。同时,这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况才需要重载,如想让别人获取到指定的内容。
示例:
class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}private:int _year; // 年int _month; // 月int _day; // 日
};
以上是我对C++中类和对象相关的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!
相关文章:
C++:类和对象(中)
文章目录1 类的6个默认成员函数2 构造函数2.1 概念2.2 特性3 析构函数3.1 概念3.2 特性4 拷贝构造函数4.1 概念4.2 特性5 赋值运算符重载5.1 运算符重载5.2 赋值运算符重载5.3 前置重载和后置重载6 日期类的实现7 const成员8 取地址及const取地址操作符重载1 类的6个默认成员函…...
53. 最大子数组和
文章目录题目描述暴力法动态规划法分治法参考文献题目描述 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 子数组 是数组中的一个连续部分。 示例 1: 输入&…...
基于Java+SpringBoot+SpringCloud+Vue前后端分离医院管理系统设计与实现
博主介绍:✌全网粉丝3W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建、毕业项目实战、项目定制✌ 博主作品:《微服务实战》专栏是本人的实战经验总结,《S…...
QT基础入门【环境配置篇】linux桌面QT开发环境的构建以及问题解决
目录 一、下载QT的安装包 二、安装 1.执行以下命令开始安装 2.选择配置 三、启动...
Linux系统之部署企业内部静态导航页
Linux系统之部署企业内部静态导航页 一、本次实践目的二、检查本地系统环境1.检查系统版本2.检查内核版本三、下载静态导航页资源包1.创建下载目录2.下载资源包四、安装apache服务1.安装httpd2.复制网页文件3.重启httpd服务4.检查httpd服务状态五、访问导航页六、修改导航页网站…...
2023备战金三银四,Python自动化软件测试面试宝典合集(四)
接上篇:11、点击塞钱进红包,选择使用新卡付款,按照流程添加新卡,此时同样需要考虑金额>新卡余额,金额<新卡余额,金额新卡余额三种情况12、使用指纹确认付款(正确的/不正确的指纹)13、使用密码确认付款…...
算法训练营 day43 动态规划 不同路径 不同路径 II
算法训练营 day43 动态规划 不同路径 不同路径 II 不同路径 62. 不同路径 - 力扣(LeetCode) 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达…...
关联查询的SQL有几种情况
1、内连接:inner join … on 结果:A表 ∩ B表 2、左连接:A left join B on (2)A表全部 (3)A表- A∩B 3、右连接:A right join B on (4)B表全部 &#…...
查缺补漏三:事务隔离级别
什么是事务? 事务就是一组操作的集合,事务将整组操作作为一个整体,共同提交或者共同撤销 这些操作只能同时成功或者同时失败,成功即可提交事务,失败就执行事务回滚 MySQL的事务默认是自动提交的,一条语句执…...
没有她的通讯录(C语言实现)
🚀write in front🚀 📝个人主页:认真写博客的夏目浅石. 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝 📣系列专栏:夏目的C语言宝藏 💬总结:希望你看完之…...
Spring Security 从入门到精通
前言 Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与Spr…...
微信小程序Springboot vue停车场车位管理系统
系统分为用户和管理员两个角色 用户的主要功能有: 1.用户注册和登陆系统 2.用户查看系统的公告信息 3.用户查看车位信息,在线预约车位 4.用户交流论坛,发布交流信息,在线评论 5.用户查看地图信息,在线导航 6.用户查看个…...
看完这篇 教你玩转渗透测试靶机vulnhub——Hack Me Please: 1
Vulnhub靶机Hack Me Please: 1渗透测试详解Vulnhub靶机介绍:Vulnhub靶机下载:Vulnhub靶机安装:Vulnhub靶机漏洞详解:①:信息收集:②:漏洞利用③:获取反弹shell:④&#x…...
nodejs+vue地铁站自动售票系统-火车票售票系统vscode
地铁站自动售票系统主要包括个人中心、地铁线路管理、站点管理、购票信息管理、乘坐管理、用户信息管理等多个模块。它使用的是前端技术:nodejsvueelementui 前后端通讯一般都是采取标准的JSON格式来交互。前端技术:nodejsvueelementui,视图层其实质就是…...
Spring Security in Action 第十二章 OAuth 2是如何工作的?
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
天工开物 #5 我的 Linux 开发机
首先说一下结论:最终我选择了基于 Arch Linux[1] 的 Garuda Linux[2] 发行版作为基础来搭建自己的 Linux 开发机。Neofetch 时刻发行版的选择在上周末的这次折腾里,我一共尝试了 Garuda Linux 发行版,原教旨的 Arch Linux 发行版,…...
【沁恒WCH CH32V307V-R1开发板输出DAC实验】
【沁恒WCH CH32V307V-R1开发板输出DAC实验】1. 前言2. 软件配置2.1 安装MounRiver Studio3. DAC项目测试3.1 打开DAC工程3.2 编译项目4. 下载验证4.1 接线4.2 演示效果5. 小结1. 前言 数字/模拟转换模块(DAC),包含 2 个可配置 8/12 位数字输入…...
Linux进程控制详解
目录前言一、进程创建1.1 fork函数初识1.2 写时拷贝1.3 fork常规用法1.4 fork调用失败的原因二、进程终止2.1 进程终止时,操作系统做了什么??2.2 进程终止的常见方式有哪些??2.3 如何用代码终止一个进程三、进程等待3.…...
C语言深度剖析之程序环境和预处理
1.程序的翻译环境和执行环境 第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令 第二种是执行环境,它用于实际执行代码 2.翻译环境 分为四个阶段 预编译阶段 ,编译,汇编,链接 程序编译过程:多个…...
【Spark分布式内存计算框架——Spark Core】9. Spark 内核调度(上)
第八章 Spark 内核调度 Spark的核心是根据RDD来实现的,Spark Scheduler则为Spark核心实现的重要一环,其作用就是任务调度。Spark的任务调度就是如何组织任务去处理RDD中每个分区的数据,根据RDD的依赖关系构建DAG,基于DAG划分Stag…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
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 开发者设计的强大库ÿ…...
搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...
