=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
【C++初阶】三、类和对象
(面向过程、class类、类的访问限定符和封装、类的实例化、类对象模型、this指针)
-CSDN博客
=========================================================================
引入:类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
但空类中并不是什么都没有,任何类在什么都不写时,
编译器会自动生成以下六个默认成员函数,
默认成员函数:用户没有显式实现时,编译器会自动生成的成员函数称为默认成员函数
- 初始化和清理:
构造函数(1) -- 完成成员变量的初始化工作
析构函数(2) -- 完成一个对象结束生命周期后的资源清理工作
- 拷贝复制:
拷贝构造函数(3) -- 使用同类对象初始化创建对象
赋值重载(4) -- 把一个对象赋值给另一个对象
- 取地址重载:
主要是普通对象(5)和const对象取地址(6),这两个很少会自己实现
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一 . 构造函数(难)
构造函数的概念和特性:
C++构造函数的概念:
还是假设有以下Date类:
//日期类: 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(2023, 11, 16);d1.Print();Date d1;d1.Init(2023, 11, 17);d1.Print();return 0; }
- 对于以上的Date类,可以通过我们自己定义的 Init共有函数(方法)给对象设置日期,
但如果每次创建对象时都需要调用该方法初始化对象成员的话,
会有点麻烦而且可能会忘记初始化,那能否在对象创建时就自动进行初始化呢?
- C++为了优化C语言需要自己初始化的情况,有了一个新概念:构造函数。
构造函数是特殊的成员函数,其名字和类名相同,
创建类类型对象时由编译器自动调用进行对象的初始化,
以保证每个数据成员都有一合适的初始值,并且在对象整个声明周期内只会调用一次
- 构造函数分为有参构造函数和无参构造函数,
我们在创建对象时可以设置各成员变量初始化的值,
如果没有设置,则对象初始化时会调用无参构造函数,
如果设置了,则对象初始化时会调用相应的有参构造函数Date类 -- 图示:
主函数通过构造函数创建对象 -- 图示:
---------------------------------------------------------------------------------------------
C++构造函数特征:
- 构造函数名和类名相同,构造函数没有返回值,
对象实例化时编译器会自动调用对应的构造函数
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,
一旦用户显式定义构造函数,编译器将不再自动生成构造函数,
所以如果定义了有参构造函数,最好再定义一个无参构造函数,
防止创建对象时需要无参构造函数而又无法调用到
- 构造函数也是函数,可以有参数,所以也可以对其设置缺省参数,
将一个有参构造函数(初始化全部成员变量的构造函数)的
所有参数都设置一个缺省参数(全缺省构造函数),
这样该构造函数就既实现了有参构造函数的任务,
又实现了无参构造函数的任务,因为初始化对象时如果不给初始化值,
那么有参构造函数的缺省参数就会发挥作用,实现无参构造函数的任务
这样一个构造函数就可以替代有参和无参两个构造函数了
- 构造函数支持重载,虽然支持重载,
但如果已经定义了全缺省构造函数,已经能够实现无参构造函数的情况下,
这时如果再定义一个无参构造函数,虽然构成了构造函数重载,
但是实际调用时是会出错的,因为全缺省构造函数和无参构造函数的功能重复了,
编译器就会不知道该调用哪个构造函数了
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,
并且默认构造函数只能有一个(否则会有调用歧义)
注意:
无参构造函数、全缺省构造函数、编译器默认生成的构造函数,
都可以认为是默认构造函数
(不传参数还可以被调用的构造函数,都可以叫默认构造函数)全缺省构造函数 -- 图示:
编译器默认生成的构造函数的作用:
- C++中把类型分成了内置类型(基本类型)和自定义类型,
内置类型就是语言原生的数据类型(int、double、指针……);
自定义类型就是我们使用 class / struct / union 自己定义的类型。
关于编译器生成的默认构造函数,该构造函数会对我们未定义的成员变量进行初始化
- 不同编译器的初始化方式不同,
VS2013中:
如果对象的成员变量为内置类型,
默认生成构造函数不会对其进行处理(为随机值);
如果对象的成员变量为自定义类型,
默认生成构造函数则会调用该自定义类型的默认构造函数
VS2019中情况会更复杂:
如果对象的成员变量全是内置类型,
默认生成构造函数不会对其进行处理(为随机值);
如果对象的成员变量既有内置类型又有自定义类型,
则会对其中的内置类型进行处理(int类型成员变量会被初始化为0),
对其中的自定义类型,会调用该自定义类型的默认构造函数
- 所以默认生成的构造函数会根据对象的成员变量的情况来判断是否要对其进行处理,
如果对象的成员变量为自定义类型,就调用该自定义类型的默认构造函数;
如果是内置类型,则不进行处理(为随机值)
(会处理自定义类型,不一定处理内置类型(看编译器),建议统一当成不会进行处理)图示:
- 因此C++11中针对内置类型成员不初始化的缺陷,又打了一个补丁:
内置类型成员变量在类中声明时可以给默认值
(给了默认值又有定义显式构造函数的话,以显式构造函数为准)图示:
总结:
- 一般情况下,我们都要自己写构造函数
- 成员变量如果都是自定义类型,或者成员变量声明时给了缺省值,
那就可以考虑让编译器自己生成构造函数
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
二 . 析构函数
析构函数的概念和特性:
C++析构函数的概念:
- 通过前面对构造函数的了解,我们知道了一个对象是怎么来的,
可一个对象又是怎么没的呢?如果说构造函数是我们以前写的Init初始化函数,那么析构函数就是我们以前写的Destroy“销毁”函数
- 析构函数和构造函数的功能相反,但析构函数不是完成对对象本身的销毁,
局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
---------------------------------------------------------------------------------------------
C++析构函数的特性:
- 析构函数名 = 在类名前加上字符 “~” (按位取反符号)
- 析构函数没有返回值和函数参数
- 一个类只能有一个析构函数,若没有显式定义,编译器会自动生成默认的析构函数
(注:析构函数不支持重载)
- 对象声明周期结束时,C++编译系统会自动调用析构函数
析构函数 -- 图示:
编译器默认生成的构造函数的作用:
- 默认生成的析构函数,其行为跟构造函数的类似,
针对内置类型的成员变量,析构函数不会对其进行处理;
针对自定义类型的成员变量,析构函数也会调用该自定义类型的默认析构函数
- 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数,
比如之前写的Date日期类就可以不写;
而如果类中有申请资源,则一定要写析构函数,否则会导致资源(内存)泄漏,
比如Stack栈类就需要显式定义析构函数进行资源清理图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
三 . 拷贝构造函数(难)
拷贝构造函数的概念和特性:
C++拷贝构造函数的概念:
拷贝构造函数:
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),
在使用已存在的类类型对象拷贝创建新对象时由编译器自动调用图示:
---------------------------------------------------------------------------------------------
C++拷贝构造函数的特性:
- 拷贝构造函数也是特殊的成员函数,是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,
使用传值方式作为其参数编译器会直接崩溃,因为会引发无穷递归调用
- 如果没有显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数拷贝对象时会按内存存储按字节序完成拷贝,
这种拷贝叫做浅拷贝,或者值拷贝注:
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的(值拷贝),
而自定义类型则会调用该自定义类型的拷贝构造函数完成拷贝图示:
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值的拷贝(值拷贝)了,
当类中没有涉及资源申请(申请动态空间等)时,
浅拷贝已经足够使用了,是否显式定义拷贝构造函数都可以;
但是一旦涉及到了资源申请时,则拷贝构造函数是一定要显式定义的,进行深拷贝图示:
- 拷贝构造函数典型调用场景:
使用已存在的对象来“拷贝”创建新对象、函数参数类型为类类型对象、
函数返回值类型为类类型对象注:
为了提高效率,一般对象传参时,尽量使用引用类型返回,
返回时根据实际场景,能用引用返回尽量使用引用返回
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
四 . 赋值运算符重载
运算符重载的使用和注意事项
引言:
对于内置类型(int、double……)的数据,我们可以直接对其使用运算符,
假设我们有整型变量a和b,我们可以对其使用:
a == b(判断相等) 、a > b(判断大小),
但对自定义类型的数据而言,就不能直接对其使用运算符,
因为编译器不知道怎么判断我们自定义的类型,所以需要我们自己定义其判断的规则图示 -- 自定义类型判断规则:
---------------------------------------------------------------------------------------------
运算符重载的使用:
- C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,
也具有其返回值类型、函数名字以及参数列表,
其返回值类型与参数列表和普通的函数类似
- 函数名字:关键字operator后接需要重载的运算符符号
(如:加法运算符重载 -- operator+)
- 函数原型:返回值类型 operator操作符(参数列表)
图示 -- 类外运算符重载:
---------------------------------------------------------------------------------------------
运算符重载的注意事项:
- 不能通过连接其它符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,
例如:内置的整型+ ,不能改变其含义( + 和 += 是不一样的 )
- 重点:
作为类成员函数重载时,其形参看起来比操作数数目少一个,
因为成员函数的第一个参数为隐藏的this指针
- 注意以下五个运算符不能重载:
“ .* ” 、“ :: ” 、“ sizeof ” 、“ ?: ” 、“ . ”图示 -- 类中运算符重载:
- 一个类要重载哪些运算符,主要看这个运算符对这个类来说有没有意义,
有意义就可以重载,没有意义就不要重载,
对日期类来说,日期的 +(加) *(乘) /(除) 都没有意义,但 -(减) 是有意义的,
两个日期相减可以计算两日期相差了多少天;
日期+日期没有意义,但日期+整型是有意义的,
如:d1 + 100 ,计算d1日期的100天后的日期图示 -- 类中实现 += 和 + 运算符重载:
(注:“+=”运算符重载中要设置返回值 -- return *this ,这里忘了写了)
赋值运算符(=)重载
赋值运算符 -- "=" ,赋值运算符重载就是让自定义类型也能像内置类型一样使用”=“
赋值运算符重载格式:
- 参数类型:const T&
const修饰参数,能够防止赋值(拷贝)时左右值写反了,导致改变了原对象
T& 传参引用接收右值的“别名”,提高传参效率
- 返回值类型:T&
引用返回可以提高返回的效率,设置返回值还为了支持“=”的连续赋值
- 定义赋值运算符重载函数时,需要检测是不是“自己给自己赋值”的情况
- 最终返回*this(即返回被赋值对象本身),能够符合“=”连续赋值的含义
- 用户没有显式实现时,编译器会生成一个默认的赋值运算符重载函数,
其行为和拷贝构造函数类似:
针对内置类型成员变量:进行 值拷贝(浅拷贝)
针对自定义类型成员变量:会调用该自定义类型的 赋值运算符重载函数
注意:
如果类中没有“资源”(Date类),赋值运算符重载函数要不要显式定义都可以;
如果类中有“资源”(Stack类),赋值运算符重载函数必须要显式定义
赋值运算符只能重载成类的成员函数(只能在类中定义重载),不能重载为全局函数
原因:
赋值运算符重载函数如果不显式实现,编译器会生成一个默认的。
此时如果再在类外实现一个全局的赋值运算符重载函数,
就会和编译器在类中生成的默认赋值运算符重载函数冲突了,
所以赋值运算符重载函数只能是类的成员函数图示 -- 以Date类为例:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本篇博客相关代码
Test.cpp -- C++文件:
#define _CRT_SECURE_NO_WARNINGS 1#include <iostream> #include <assert.h> using namespace std;//class Date //{ //public: // // //(有参)构造函数 -- 用于初始化对象: // //Date(int year, int month, int day) // ///* // //* 有参构造函数: // //* 如果创建类对象时有传参数就自动调用 // //* 有参构造函数对其进行初始化 // //*/ // //{ // // _year = year; // // _month = month; // // _day = day; // //} // // /* // * 构造函数特征: // * 1、函数名和类名相同; // * 2、没有返回值,所以不用设置返回值类型; // * 3、对象示例化时编译器自动调用对应的构造函数; // * 4、构造函数可以重载 // * // * 构造函数实现的功能就是我们下面写的Init初始化函数的功能, // * 而且还会在创建该类对象时就自动帮你初始化该对象 // */ // // //构造函数重载(无参): // //Date() // ///* // //* 无参构造函数: // //* 如果创建类对象时没有传参数就自动调用 // //* 无参构造函数对其进行初始化 // //*/ // //{ // // _year = 1; // // _month = 1; // // _day = 1; // // // // //没有传参就初始化为 1年1月1日 // //} // // /* // * 有参数构成函数也可以使用(全)缺省参数, // * 设置后有参构造函数也能实现无参构造函数的效果, // * 因为如果不传参数,就默认使用全缺省参数, // * (在有参构造函数的基础上通过全缺省参数实现无参构造函数) // * // * 因为全缺省参数的特点,只传初始化参数的部分参数, // * 也可以实现只初始化其中一部分参数的效果 // */ // // //设置了缺省参数的有参构造函数: // Date(int year = 1 , int month = 1, int day = 1) // //有参构造函数的参数设置成缺省参数: // { // _year = year; // _month = month; // _day = day; // } // // 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); // /* // * 注意不要忘记初始化: // * 如果没有初始化,程序可能会出现随机值, // * 严重的情况下程序还会崩溃 // * // * 平常写代码时还是很容易忘记初始化的, // * 像是有时创建一个栈对象后,没有初始化然后 // * 就直接进行push出栈操作了 // * // * C++中为了防止出现忘记初始化的情况, // * 有了构造函数,构造函数可以在创建一个对象时 // * 就自动初始化该对象 // */ // // /* // * 创建对象后不调用自己写的Init函数, // * 让构造函数帮我们完成初始化, // * 此时调用的是无参构造函数, // * 调用打印函数查看自动初始化情况: // */ // cout << "通过无参构造函数初始化对象:"; // d1.Print(); // // /* // * 有了构造函数后,创建对象时可以自己设置 // * 对象初始化时的值(通过有参构造函数), // * 只需要在创建对象时后面加上(成员变量,成员变量,……)即可: // */ // //通过有参构造函数对类进行初始化: // Date d2(2023, 11, 16); // // cout << "通过有参构造函数初始化对象:"; // //打印通过有参构造函数初始化的对象: // d2.Print(); // // /* // * 注: // * 初始化对象时,如果想调用无参构造函数, // * 初始化时对象名后面是不需要加括号的, // * 如果加了的话,那可能就是声明了一个函数, // * 而不是初始化类对象 // * // * 如果是调用有参构造函数的话, // * 初始化对象时会在括号中写上初始化的值, // * 没有函数的声明会这样写,所以不用担心 // * 调用无参构造函数时的问题 // */ // // //通过设置了缺省参数的有参构造函数初始化部分参数: // Date d3(2023, 10); // // cout << "通过有参构造函数(设置了缺省函数)部分初始化对象:"; // d3.Print(); // // return 0; //}//class Date //{ //public: // void Print() // { // cout << _year << "-" << _month << "-" << _day << endl; // } // // /* // * 没有显式定义构造函数且没有自定义初始化函数的情况下, // * 编译器会有一个默认的无参构造函数, // * 但该默认的无参构造函数调用后初始化的值会是随机值 // */ // //private: // int _year = 1; // int _month = 1; // int _day = 1; // /* // * 注:这里虽然给出了默认值, // * 但也不是成员变量定义,还是成员变量声明, // * 只是在声明时给了一个缺省值,没有开空间, // * 在类中,它始终只是“设计图”的一部分, // * 示例化对象后,开了空间,成员变量才被定义 // */ //}; // // C++实现栈(部分): //class Stack //{ //public: // //栈初始化构造函数: // Stack(size_t capacity = 3) // /* // * 构造函数初始化对象时的情况要按实际情况而定, // * 像这里的栈初始化构造函数就不需要将所有的成员变量都初始化, // * 最多只用传栈容量大小即可,用于开辟对应容量的动态空间, // * 不传的话缺省参数默认容量为3 // */ // { // cout << "Stack(size_t capacity = 3)" << endl; // // _a = (int*)malloc(sizeof(int) * capacity); // if (nullptr == _a) // { // perror("malloc申请空间失败!!!"); // } // _capacity = capacity; // _top = 0; // } // //private: // int* _a; //栈底层数组指针 // int _capacity; //栈容量 // int _top; //栈顶值 //}; // // 定义一个类:通过两个栈实现一个队列 //class MyQueue //{ //private: // //成员变量: // Stack _pushst; //出栈值 // Stack _popst; //入栈值 // int _size = 1; //当前存储数据个数 //}; // // // //int main() //{ // Date d1; // d1.Print(); // //如果没有自己定义构造函数和其它初始化函数, // //使用编译器默认的无参构造函数后初始值为随机值 // // Stack st1; // //如果没有自己定义构造函数和其它初始化函数, // //使用编译器默认的无参构造函数后初始值为随机值 // // /*所以对与以上两个对象的情况,不自己定义构造函数是不合适的*/ // // MyQueue mq; // /* // * MyQueue类中对象中,需要生成栈的对象,_pushst和_popst, // * 所以当创建MyQueue对象mq时,就需要初始化这两个栈对象, // * // * C++中有内置类型和自定义类型: // * 内置类型:语言原生就有的类型(int、double、指针……) // * (注:指向自定义类型的指针也是内置类型) // * 自定义函数:我们自己通过class、struct定义的类型 // * // * 当调用默认生成的构造函数时(VS2013): // * 内置类型成员变量不会进行处理(为随机值) // * 自定义类型则会去调用它的默认构造函数 // * // * 当调用默认生成的构造函数时(VS2019): // * VS2019在这方面比较奇怪,要分两种情况。 // * 当所有成员变量都是内置类型时,不会进行处理(全为随机值) // * 当成员变量既有内置类型又有自定义类型时, // * 那么这两种类型都会被处理,自定义类类型变量会调用其构造函数, // * 自定义函数会被初始化为0(int) // * (所以VS2019编译器在这方面有种“半生不熟”的感觉) // * // * 所以默认生成的构造函数并不是什么都不做, // * 而是看成员的情况来进行要不要进行处理, // * 如果成员变量是自定义类型,那就会调用其默认构造函数; // * 如果是内置类型,则不进行处理(为随机值) // * (处理自定义类型,内置类型不确定(看编译器),建议当成不处理) // */ // // return 0; //}//class Date //{ //public: // // /* // * 默认构造函数: // * 1、编译器默认生成的构造函数,叫默认构造函数 // * 2、我们自己定义的无参构造函数也可以叫做默认构造函数 // * 3、我们定义的全缺省构造函数也可以叫默认构造函数 // * 不传参数还可以被调用的构造函数,都可以叫默认构造函数 // */ // // 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 = 1; // int _month = 1; // int _day = 1; //}; // //int main() //{ // Date d1; // d1.Print(); // // return 0; //}//析构函数: //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; // } // // //析构函数: // ~Date() // /* // * 对象销毁时会自动调用析构函数, // * 完成对象中资源的清理工作, // * 和构造函数相反的概念, // * 构造函数名和类名相同, // * 析构函数名则需要在类名前再加上一个 “~” 符号 // */ // { // //当前日期类中没有什么资源需要被清理, // //所以只能打印相关内容来证明对象销毁时会自动调用析构函数: // cout << "~Date()" << endl; // } // //private: // int _year = 1; // int _month = 1; // int _day = 1; //};//class Stack //{ //public: // //栈初始化构造函数: // Stack(size_t capacity = 3) // { // //cout << "Stack(size_t capacity = 3)" << endl; // // _a = (int*)malloc(sizeof(int) * capacity); // if (nullptr == _a) // { // perror("malloc申请空间失败!!!"); // } // _capacity = capacity; // _top = 0; // } // // //析构函数: // ~Stack() // /* // * 对象销毁时会自动调用析构函数, // * 完成对象中资源的清理工作, // * 和构造函数相反的概念, // * 构造函数名和类名相同, // * 析构函数名则需要在类名前再加上一个 “~” 符号 // * (按位取反符号),且没有参数和返回值 // * // * 构造函数相当于我们之前在数据结构中写的Init初始化函数 // * 析构函数则相当于之前写的Destroy“销毁”函数 // * 和Init函数相同,Destroy函数也很容易最后忘记调用 // * // * 构造函数和析构函数都是默认成员函数, // * 不显式定义的话,编译器会自动生成 // * 但自动生成的析构函数不会对动态空间进行释放, // * 也不会将指向该动态空间的指针置为空指针, // * 所以当一个类中有类似资源时就需要自己定义析构函数了 // */ // { // cout << "~Stack()" << endl; // //释放之前开辟的动态空间资源: // free(_a); // _capacity = _top = 0; //将栈容量和栈顶值都初始化为0 // _a = nullptr; //将底层数组指针置为空指针 // } // //private: // int* _a; //栈底层数组指针 // int _capacity; //栈容量 // int _top; //栈顶值 //};定义一个类:通过两个栈实现一个队列 //class MyQueue //{ //private: // //成员变量: // Stack _pushst; //出栈值 // Stack _popst; //入栈值 // int _size = 1; //当前存储数据个数 // // /* // * 和构造函数相同, // * 针对内置类型成员变量,析构函数不进行处理 // * 针对自定义类型成员变量, // * 析构函数也会调用该自定义类型的默认析构函数 // */ //};//int main() //{ // //Stack st1; // 出了生命周期后,会自动调用析构函数清理资源 // // MyQueue mq; // /* // * 默认生成的析构函数,其行为跟构造函数类似, // * 内置类型成员不做处理, // * 自定义类型成员会去调用它的默认析构函数 // */ // // return 0; //}//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 << endl; // } // // 指针传参: // //Date(Date* dd) // //{ // // _year = dd->_year; // // _year = dd->_year; // // _year = dd->_year; // //} // // //拷贝函数: // Date(Date& dd) //引用传参 // { // _year = dd._year; // _year = dd._year; // _year = dd._year; // } // //private: // int _year; // int _month; // int _day; //};//class Stack //{ //public: // //栈初始化构造函数: // Stack(size_t capacity = 3) // { // cout << "Stack(size_t capacity = 3)" << endl; // // _a = (int*)malloc(sizeof(int) * capacity); // if (nullptr == _a) // { // perror("malloc申请空间失败!!!"); // } // _capacity = capacity; // _top = 0; // } // // //析构函数: // ~Stack() // { // cout << "~Stack()" << endl; // //释放之前开辟的动态空间资源: // free(_a); // _capacity = _top = 0; //将栈容量和栈顶值都初始化为0 // _a = nullptr; //将底层数组指针置为空指针 // } // // Stack(const Stack& stt) //通过引用传参 // /* // * 拷贝构造函数中,在值拷贝时,即使等号两边写反了也不会报错, // * 所以为了防止出现不小心写反的情况,使用const修饰被拷贝对象, // * 让被拷贝对象不能被修改,此时如果值拷贝写反了就会报错了。 // */ // { // cout << "Stack(Stack& stt)" << endl; // /* // * 如果一个类中有“资源”, // * 在定义该类的拷贝函数时就需要进行深拷贝, // * 深拷贝本质是拷贝指向的资源, // * 让拷贝对象和被拷贝对象有一样大的空间和一样的值, // * 此时各自调用析构函数时析构的就是各自的空间, // * 而不是析构同一块空间两次,导致程序崩溃 // */ // //进行深拷贝 -- 让拷贝对象和被拷贝对象有一样大的动态空间: // _a = (int*)malloc(sizeof(int) * stt._capacity); // //检查是否开辟成功: // if (_a == nullptr) // { // perror("malloc fail"); // exit(-1); // } // //使用memcpy让拷贝对象空间中的值和被拷贝对象的一样: // memcpy(_a, stt._a, sizeof(int) * stt._top); // // _top = stt._top; //栈顶值拷贝 // _capacity = stt._capacity; //栈容量拷贝 // } // /* // * 拷贝函数是特殊的构造函数,是构造函数的重载, // * 其参数只有一个同类型对象的引用, // * 当需要拷贝一个对象时就会调用拷贝函数 // *(通过同类型对象拷贝出另一个对象、函数传值传参或返回都需要进行拷贝) // * // * 不能通过传值方式调用拷贝函数, // * 因为会引发无穷递归调用, // * 传值拷贝时,自定义类型调用其拷贝函数, // * 传参时,还未调用到就先拷贝了一份形参出来, // * 形参又需要调用拷贝函数,就形成了无穷递归调用 // *(调用拷贝构造函数->先传参->传值传参->形成新的拷贝构造函数) // * // * 解决方法:通过指针或者引用进行传参 // */ // //private: // int* _a; //栈底层数组指针 // int _capacity; //栈容量 // int _top; //栈顶值 //};创建一个类,参数是接收另一个类的对象: //void func1(Date d) //接收日期类对象 //{ // d.Print(); // // /* // * 这里将日期类对象(自定义类型)作为函数参数, // * 而且我们知道形参是实参的临时拷贝, // * // * C语言中也可以将结构体变量当作函数参数, // * 但是有两个缺陷: // * 1、实参拷贝时效率较低 // * 2、形参结构体变量改变不会影响实参结构体变量 // * 我们这里只考虑传值传参,需要拷贝实参, // * 也称 值拷贝 或者 浅拷贝 // * // * 为了解决第二个问题,可以使用指针或者引用(&) // */ //}//void func2(Stack st) //接收栈类 //{ // //}//class MyQueue //{ // Stack _pushst; // Stack _popst; // int _size = 0; //};//Stack& func3() //{ // static Stack st; // /* // * static修饰后st出了函数后不会销毁, // * 所以此时可以使用引用返回,返回st的“别名”, // * 返回时就不需要进行拷贝了,也就不用调用拷贝构造函数了 // */ // // return st; // /* // * 如果使用传值返回: // * 传值返回时,返回的是自定义类型st的拷贝, // * 所以也需要调用到该类型的拷贝构造函数 // */ //}//int main() //{ // Date d1(2023, 10, 22); // func1(d1); //接收日期类对象d1 // /* // * 实参拷贝时,拷贝成员变量:年、月、日, // * 共12个字节,要通过实参d1拷贝生成形参d, // * 拷贝时要先调用拷贝函数,拷贝函数中dd是d1的别名, // * 隐藏的this指针会指向形参d, // * 所以可以将d1的成员变量值拷贝到形参d。 // * 拷贝完成后再执行func1函数中的内容 // */ // Date d2(d1); //使用d1拷贝出一个d2 // //这种情况下也会进行拷贝 // /* // * 此时拷贝同样需要调用拷贝函数, // * 拷贝函数中dd是d1的别名,隐藏的this指针会指向对象d2 // * 所以可以通过对象d1拷贝生成对象d2 // */ // // //Date d3(&d1); // //指针传参,可以解决传值方式的拷贝构造函数问题, // //但是用起来没有引用传参方便 // // // Stack st1; // func2(st1); //接收栈类对象st1 // /* // * 实参拷贝时,拷贝成员变量: // * 指针_a、栈顶值_top、栈容量capacity // * 也是12个字节,但是这里有一个指针_a, // * 指向一块动态开辟的空间,拷贝指针后, // * 形参拷贝的指针也会指向该动态空间, // * 但是栈类中是有析构函数的, // * 形参st出了作用域,析构函数会先释放掉动态空间, // * 之后实参st1出了主函数,其析构函数又会再释放一次动态空间, // * 导致一块空间被释放了两次,程序会崩溃, // * 所以C++中使用值拷贝(浅拷贝)是有风险的 // * (C语言默认拷贝 -- 浅拷贝) // */ // // /* // * 此时就需要进行深拷贝,来解决浅拷贝的问题 // * C++中规定,自定义类型对象拷贝的时候, // * 需要调用一个函数,这个函数就叫做拷贝构造函数, // * 拷贝构造函数默认的行为:值拷贝,这也是导致上面会崩溃的原因, // * // * 对于内置类型的成员变量,值拷贝就够了, // * 但对于自定义类型的成员变量有“资源”的情况下,就需要进行深拷贝了 // */ // Stack st2(st1); //使用st1拷贝出一个st2 // /* // * 拷贝函数中stt时st1的别名,隐藏的this这种指向对象st2, // * 所以可以通过对象st1拷贝生成对象st2 // */ // // // MyQueue q1; // MyQueue q2(q1); //使用q1拷贝出一个q2 // // /* // * 默认生成的拷贝构造函数的作用: // * 1、内置类型成员 -- 默认生成拷贝构造函数会完成值拷贝 // * 2、自定义类型成员 -- // * 默认生成拷贝构造函数会调用该自定义类型的拷贝构造函数 // */ // // func3(); //传值返回一个自定义类型 // // return 0; //}运算符重载: //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 << endl; // } // // //运算符重载 // //判断对象是否相等的函数(以日期类为例): // bool operator==(const Date y) // /* // * (注:)在类中的话,隐藏的this指针是第一个参数, // * == 有两个操作数,this指针 和 y 就是两个操作数, // *(运算符重载,这个运算符操作数和要和函数的参数匹配) // */ // { // /* // * 实现前需要先把Date类的成员变量权限设置为共有, // * 才能在类外部进行调用: // */ // return _year == y._year // && _month == y._month // && _day == y._day; // //比较两日期类对象是否相等只要判断两者年月日是否相等即可 // // /* // * 成员变量虽然是私有的, // * 但在类中通过this指针访问私有成员变量是可以的 // */ // } // // //写一个函数判断自定义类型大小(以日期类对象为例): // bool operator>(Date y) // /* // * > 同样有两个操作数,隐藏的this指针是第一个参数 // */ // { // //“年大就大”: // if (_year > y._year) // { // return true; // } // //"年相等,月大就大": // else if (_year == y._year && _month > y._month) // { // return true; // } // //"年相等,月相等,天大就大": // else if (_year == y._year && _month == y._month && _day > y._day) // { // return true; // } // // //上面已经写出了所有大于的情况,执行到这说明是小于等于: // return false; //返回false // } // // /* // * 一个类要重载哪些运算符, // * 主要看这个运算符对这个类来说有没有意义, // * 有意义就可以重载,没有意义就不要重载, // * 对日期类来说,日期的 +(加) *(乘) /(除) 都没有意义, // * 但 -(减) 是有意义的, // * 两个日期相减可以计算两日期相差了多少天 // */ // //实现 d1 - d2,计算两日期相差多少天 : // int operator-(const Date& d) // { // //(目前还不太好实现)…… // return 0; // } // // //日期+日期没有意义,但日期+整型是有意义的, // //如:d1 + 100 ,计算d1日期的100天后的日期: // // /* // * 因为考虑到各月份日期可能不一样, // * 二月还需考虑闰年还是平年, // * 所以可以单独写一个函数处理月份的情况: // */ // int GetMonthDay(int year, int month) // { // //assert断言防止month传错: // assert(year >= 1 && month >= 1 && month <= 12); // // int monthArray[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; //返回闰年二月的29天 // } // // //把数组下标当作月份,返回对应月份: // return monthArray[month]; // } // // //d1 += 100(整数) ,计算d1日期的100天后的日期: // Date& operator+=(int day) // += 才会改变d1对象 // //为了防止拷贝返回,使用引用返回 // { // //思路:当月天数满了进位到月,月满了则进位到年: // // _day += day; //先将要加的天数加到当前的天数上 // // //开始进位(如果需要的话): // while (_day > GetMonthDay(_year, _month)) // /* // * 先通过GetMonthDay函数获得当月的天数, // * 再比较相加后的天数是否超过了当月的天数, // * 只要超过了则进行进位,进位到月: // * (while循环到没超过为止) // */ // { // //天数减去一轮当前月天数: // _day -= GetMonthDay(_year, _month); // // //减去的一轮当前月天数进位到月中: // ++_month; // // //如果当前月进位后超过了12个月: // if (_month == 13) // { // //将一轮月份进位到年: // ++_year; // // //将月份重置为1月: // _month = 1; // } // } // // return *this; // } // // //d1 + 100(整数) ,计算d1日期的100天后的日期: // Date operator+(int day) // + 不会改变d1对象 // //为了防止拷贝返回,使用引用返回 // { // Date tmp(*this); // /* // * 为了实现加法,加了后不改变d1对象(+=才会改变) // * 先通过*this(即d1)拷贝出一个tmp, // * 对tmp进行加法操作就不会改变d1对象了 // */ // // //思路:当月天数满了进位到月,月满了则进位到年: // // tmp._day += day; //先将要加的天数加到当前的天数上 // // //开始进位(如果需要的话): // while (tmp._day > GetMonthDay(tmp._year, tmp._month)) // /* // * 先通过GetMonthDay函数获得当月的天数, // * 再比较相加后的天数是否超过了当月的天数, // * 只要超过了则进行进位,进位到月: // * (while循环到没超过为止) // */ // { // //天数减去一轮当前月天数: // tmp._day -= GetMonthDay(tmp._year, tmp._month); // // //减去的一轮当前月天数进位到月中: // ++tmp._month; // // //如果当前月进位后超过了12个月: // if (tmp._month == 13) // { // //将一轮月份进位到年: // ++tmp._year; // // //将月份重置为1月: // tmp._month = 1; // } // } // // return tmp; // /* // * 这里tmp是d1的拷贝,出了函数就销毁了, // * 所以需要传值返回拷贝一份回主函数 // */ // } // // // d1+整型 加法运算符重载(优化): // Date operator+(int day) // { // Date tmp(*this); // /* // * 为了实现加法,加了后不改变d1对象(+=才会改变) // * 先通过*this(即d1)拷贝出一个tmp, // * 对tmp进行加法操作就不会改变d1对象了 // */ // // /* // * 复用 “+=运算符重载” ,只要 += 到d1的拷贝tmp上即可, // * 就不会改变到d1对象,通过tmp返回d1的加法结果: // */ // tmp += day; // // //通过tmp返回d1的加法结果: // return tmp; // /* // * 这里tmp是d1的拷贝,出了函数就销毁了, // * 所以需要传值返回拷贝一份回主函数 // */ // } // // private: // int _year; // int _month; // int _day; //};写一个函数判断自定义类型大小(以日期类对象为例): //bool Greater(Date x, Date y) //{ // //“年大就大”: // if (x._year > y._year) // { // return true; // } // //"年相等,月大就大": // else if (x._year == y._year && x._month > y._month) // { // return true; // } // //"年相等,月相等,天大就大": // else if (x._year == y._year && x._month == y._month && x._day > y._day) // { // return true; // } // // //上面已经写出了所有大于的情况,执行到这说明是小于等于: // return false; //返回false //}写一个函数判断自定义类型大小(以日期类对象为例): //bool operator>(Date x, Date y) ///* //* 为了在自己定义自定义类型比较函数时, //* 函数命名能够更规范,可以使用 operator> 表示 //* 这是比较自定义类型大小的函数 //*/ //{ // //“年大就大”: // if (x._year > y._year) // { // return true; // } // //"年相等,月大就大": // else if (x._year == y._year && x._month > x._month) // { // return true; // } // //"年相等,月相等,天大就大": // else if (x._year == y._year && x._month == x._month && x._day > y._day) // { // return true; // } // // //上面已经写出了所有大于的情况,执行到这说明是小于等于: // return false; //返回false //}判断对象是否相等的函数(以日期类为例): //bool Equal(Date x, Date y) //{ // /* // * 实现前需要先把Date类的成员变量权限设置为共有, // * 才能在类外部进行调用: // */ // return x._year == y._year // && x._month == y._month // && x._day == y._day; // //比较两日期类对象是否相等只要判断两者年月日是否相等即可 //}//判断对象是否相等的函数(以日期类为例): //bool operator==(Date x, Date y) ///* //* 为了在自己定义自定义类型比较函数时, //* 函数命名能够更规范,可以使用 operator== 表示 //* 这是判断自定义类型是否相等的函数 //*/ //{ // /* // * 实现前需要先把Date类的成员变量权限设置为共有, // * 才能在类外部进行调用: // */ // return x._year == y._year // && x._month == y._month // && x._day == y._day; // //比较两日期类对象是否相等只要判断两者年月日是否相等即可 //}//int main() //{ // Date d1; // Date d2(2023, 10, 22); // // /* // * 内置类型对象可以直接使用各种运算符, // * 那自定义类型呢? // */ // // //内置类型: // int x = 1, y = 2; // bool ret1 = x > y; // bool ret2 = x == y; // /* // * 内置类型都是简单类型,是编程语言自己定义的, // * 编译器能直接转换成指令(通过汇编指令cmp完成比较的各种行为)。 // * 自定义类型则不支持,需要自己定义比较规则 // */ // //自定义类型: // d1 == d2; //自定义类型无法使用 “==” 运算符 // d1 > d2; //自定义类型无法使用 “>” 运算符 // // //通过函数判断d1和d2的大小: // cout << Greater(d1, d2) << endl; // cout << operator>(d1, d2) << endl; // // //通过函数判断d1和d2是否相等: // cout << Equal(d1, d2) << endl; // cout << operator==(d1, d2) << endl; // // /* // * 为进行自定义类型对象的比较定义一个比较规则(函数) // * // * operator> 和 operator== 都是运算符重载, // * 使用运算符重载作为该函数的名字,更加规范 // */ // // /* // * 为了让自定义类型比较更接近内置类型比较, // * 可以直接写成:(d1 > d2)、(d1 == d2) // * 可以看起来就和内置类型的比较一样了, // * // * 这里加()是因为 <<流插入运算符 优先级比较高, // * 所以要加()调整优先级 // * // * 这里虽然看起来和内置类型比较一样, // * 但编译器实际还是调用了运算符重载的函数: // * d1 > d2 编译器会调用 operator>(d1, d2)函数 // * d1 == d2 编译器会调用 operator==(d1, d2)函数 // * // * 如果没有使用对应的运算符重载作为对应的比较函数名的话, // * 就不能像 d1 > d2 这样去比较自定义类型了, // * 因为编译器找不到对应的运算符重载函数 // */ // cout << (d1 > d2) << endl; //调用operator // cout << (d1 == d2) << endl; // // /* // * 写了运算符重载的好处: // * 1、可以像比较内置类型一样来比较自定义类型对象 // * 2、更加规范,不会说每个人定义的自定义比较函数名都不一样 // */ // // /* // * 运算符重载 和 函数重载 之间没有关联: // * 运算符重载:让自定义类型可以直接使用运算符(像内置类型一样) // * 函数重载:允许有参数不同的多个同名函数 // */ // // // cout << (d1 > d2) << endl; // /* // * 实际编译器执行: // * 调用Date类中的成员函数 -- d1.operator>(d2) // * 实际执行时 -- d1.operator>(&d1, d2) // * &d1 为隐藏的this指针存储的地址 // */ // // cout << (d1 == d2) << endl; // /* // * 实际编译器执行: // * 调用Date类中的成员函数 -- d1.operator==(d2) // * 实际执行时 -- d1.operator==(&d1, d2) // * &d1 为隐藏的this指针存储的地址 // */ // // d1 - d2; // // d2 += 50; // //调用:Date& operator+=(int day) // d2.Print(); // // Date ret = d1 + 50; //因为是传值返回所以需要一个对象接收 // //调用:Date operator+(int day) // d1.Print(); //d1本身不会改变 // ret.Print(); //ret 接收 d1+整型的结果 // // return 0; //}//运算符重载 -- 让自定义类型能够像内置类型一样使用运算符//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; // } // // //赋值运算符(=)重载 -- d1=d3 : // Date& operator=(const Date& d) // //this指针存储d1地址,形参d存储d3“别名”: // { // //应对“自己给自己赋值”的情况: // if (this != &d) // /* // * this 存储着d1的地址, // * &d 为形参接收的对象地址, // * 如果this == &d,说明是“自己给自己赋值”的情况, // * 就没必要进行赋值操作了,直接返回*this对象本身即可 // */ // { // //排除“自己给自己赋值”的情况后,进行赋值操作: // _year = d._year; // _month = d._month; // _day = d._day; // } // // return *this; // //返回*this“别名”,即d1“别名”,省略拷贝操作 // } // //private: // int _year; // int _month; // int _day; // //}; // //int main() //{ // Date d1(2023, 10, 24); // Date d2(2022, 1, 1); // //定义初始化对象 -- 调用构造函数 // // //拷贝构造: // Date d3(d1); // //用d1拷贝出d3 -- 调用拷贝构造函数 // //(一个已经存在的对象去拷贝初始化另一个对象出来) // // //赋值重载: // d1 = d3; // /* // * 把对象(d3)赋值拷贝给另一个对象(d1) // *(两个已经存在的对象拷贝) // * 这时就需要进行 赋值运算符(=)的重载, // * 调用:operator= // */ // // d2 = d1 = d3; // /* // * 连续赋值: // * 先将d3赋值给d1,d1作为返回值再赋值给d2 // * 所以调用operator=后要返回 d1(“别名”) // */ // // d1 = d1; //“自己给自己赋值” // /* // * 可以自己给自己赋值, // * 但是实际调用operator=时没必要赋值, // * 直接返回*this对象本身即可 // */ // // /* // * 我们不显式定义operator=赋值运算符重载的话, // * 编译器会自己生成一个默认的operator= // * // * 默认生成的operator=函数的行为和拷贝构造的行为类似, // * 针对内置类型成员变量,operator=函数会完成值拷贝(浅拷贝); // * 针对自定义类型成员变量, // * operator=函数会调用该自定义类型的operator=函数 // * // * 所以对于 Date类 和 MyQueue类,可以不显式写operator=函数, // * 默认生成的operator=函数就足够了,Date类只有内置类型只需浅拷贝, // * MyQueue类中会的自定义类型Stack类会调用Stack类的operator=函数 // * // * 而Stack类就必须自己实现operator=函数了,因为其中有“资源”, // * 需要对其进行深拷贝 // */ // // return 0; //}/* * 默认生成的: * * 构造函数 和 析构函数 的行为类似, * 针对内置类型成员变量:不进行处理(为随机值) * 针对自定义类型成员变量:会调用该自定义类型的 构造函数 或 析构函数 * * 拷贝构造函数 和 赋值运算符重载函数 的行为类似, * 针对内置类型成员变量:进行值拷贝(浅拷贝) * 针对自定义类型成员变量:会调用该自定义类型的 拷贝构造函数 或 赋值运算符重载函数 */