=========================================================================
相关代码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类为例: