当前位置: 首页 > article >正文

类和对象(中)---默认函数

一、类的默认函数

默认函数就是我们不写编译器会默认生成的,一共有六个

学习默认函数我们主要从两个方面来学习:

默认函数我们不写时,生成的函数会做出那些行为,是否符合我们的要求

如果不符合我们应该,我们自己如何实现符合要求的函数。

二、构造函数

2.1 构造函数的定义

构造函数的功能是实例化对象时初始化,并不是创建对象。类似于我们用c语言来写栈和堆时的Init

2.2 构造函数的特点

  • 函数名与类名相同。
  • 无返回值(也不需要写void)
  • 对象实例化时系统自动调用对应的构造函数(不需要我们去调用)
  • 构造函数可以重载(构造函数可以有多个)
  • 如果类中没有显示定义构造函数,则c++编译器会自动生成一个无参的默认函数,一旦用户显示定义编译器将不在生成。
  • 默认构造函数分为无参构造函数、全缺省构造函数、我们不写时编译器默认生成的默认构造函数(三者有一个共同特点没有参数)三者只能出现其一,不能同时存在,值得注意的是虽然无参构造函数和全缺省函数构成重载,写的时候没有任何问题,但是调用的时候会出现歧义。(通过以上两点我们可以发现当用户显示定义一个有参构造函数时,那么整个类将没有构造函数)
  • 但我们不写时,编译器自动生成的构造函数,对内置类型成员变量(如:int char .......)没有要求,也就是说是否初始化是不确定的看编译器,对于自定义类型成员变量(如:struct,class),要求调用这个成员的默认构造函数初始化,如果这个成员没有默认构造函数就会报错,需要初始化列表(下面会说)。

2.3 从两个角度分析构造函数

1.默认函数我们不写时,生成的函数会做出那些行为,是否符合我们的要求

根据构造函数特点第七点知道,默认函数对于内置类型成员变量不做处理,自定义类型成员变量会调用他的默认构造函数,由此可知当我们的类中的成员变量如果存在内置类型就默认函数就不符合我们的要求需要我们自己实现,只有类中的成员变量只有自定义类型才不需要我们自己实现

2.如果不符合我们应该,我们自己如何实现符合要求的函数。

我们可以像在c语言中实现栈时的初始化一样来书写,也可以根据构造函数的特点来书写(会更加简单不需要写返回类型,不需要调用)

如上图Date类的成员变量为内置类型,如果我们不自己实现初始化,那么会产生随机值。

#include <iostream>
using namespace std;
class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << "/" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

如何自己实现如上图

补充:再探深造函数--函数列表
  • 我们之前实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式就是初始化列表,初始化列表的使用方式是一个冒号开始的,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  • 引用成员变量(必须初始化),const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。

  • C++11支持在成员 变量声明的位置给缺省值,这个缺省值主要是给没有显示在列表初始化的成员使用

 

  • 请注意这里不是初始化,这里给的是缺省值,这个缺省是给初始化列表的
  • 如果初始化列表没有显示初始化,默认就会用这个缺省初始化
  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置成员是否初始化取决于编译器,对于没有显示在初始化列表初始化的自定义类型会调用这个成员类型的默认构造函数 ,如果没有默认构造函数编译错误。
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。(建议声明顺序和初始化列表顺序保持一致)

ps:本题的是 1随机值,首先我们在初始化列表中给了初始值,此时两个缺省值_a2 = 2,_a1 = 1失去作用,接着初始化的顺序是按照在类中的顺序,所以_a2(根据初始化列表_a2(a1))先开始初始化但此时_a1还未初始化,所以_a2是随机值,_a1(根据初始化列表_a1(a))所以_a1为1;

大总结:
无论是否显示写初始化列表,每个函数都有初始化列表;

无论是否在初始化列表显式初始化成员变量都要走初始化列表初始化;

三、析构函数

3.1 析构函数的含义

析构函数与构造函数的功能相反,析构函数不是对空间的销毁,而是清理,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就销毁了,不需要我们管,C++规定对象在销毁时会自动调用析构函数。析构函数跟我们实现栈时的Destroy功能。

3.2 析构的特点:

析构函数名是在类名前加上字符~。

无参数返回值(跟构造类似)

一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。

对象生命周期结束时,系统会自动调用析构函数。

跟构造函数类似,我们不写编译器会自动生成的析构函数对内置类型不成员处理,对自定义类型会调用他自己的析构。

值得注意的时但我们显示写析构类型的时候,自定义成员变量也会调用他自己的析构函数(也就是说无论如何自定义成员变量都会调用自己的析构函数)

如上图我们在Stack的析构中加了一句cout<<stack;在MyQuque的析构中加了一句cout<<myqueue

如果上述的特点成立那么会显示myqueue和两个stack;

结果:

如果类中没有申请资源时,析构可以不用写,用编译器生成的析构函数,比如Date;如果默认生成的析构就可以用,也就不用显示写析构,如:MyQueue;但是如果申请了资源(空间),一定有自己写析构,否则会造成资源泄露,如Stack.

一个局部域的多个对象,c++规定后定义的先析构。

3.3 两个角度解析析构函数

1.默认析构函数我们不写时,生成的析构函数会做出那些行为,是否符合我们的要求

由析构函数特点第五点可知,编译器自动生成的析构函数对内置成员不做处理,对自定义类型会自动调用他的析构函数。当类中申请资源(空间时)我们需要自己写析构函数,当没有则不需要。如果成员变量

2.如果不符合,我们自己如何实现符合要求的函数。

我们可以像在c语言中实现栈时的销毁一样来书写,也可以根据析构函数的特点来书写(会更加简单不需要写返回类型,不需要调用)

四、拷贝函数

4.1 拷贝函数的含义

如果一个构造函数的第一个参数是它自身类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,也就说拷贝构造函数是一个特殊的构造函数。

4.2 拷贝函数的特点:

  • 拷贝构造函数是构造函数的一个重载。
  • 拷贝构造函数的第一个参数必须是自身类型对象的引用,如果使用传值传参会引发无穷递归调用。
  • C++规定自定义类型对象拷贝必须调用拷贝构造,所以自定义类型对象传值传参和传值返回都要调用拷贝构造。
  • 若未显示拷贝构造,编译器自动生成的拷贝构造对内置类型成员对象会进行浅拷贝即一个一个字节的拷贝,对内置类型成员对象会调用他的拷贝构造
  • 像Date这样的类他的成员变量全是内置类型并没有指向资源(申请空间),编译器自动生成的拷贝构造就够了。但像Stack这样的类他的成员变量虽然都是内置类型但是里面的int*a指向了资源(申请了空间)如果不写拷贝构造就会进行浅拷贝会一个个字节的拷贝,那么拷贝后两个指向的资源就一样了(空间),如果我们改变一个对象这个空间的值那么另一个对象这个空间的值就会改变,像MyQueue这样的类,他的成员变量都是内置类型,编译器自动生成的拷贝构造会自动调用Stack的拷贝构造。
  • 传值返回会产生一个临时变量从而会调用拷贝构造,但传值返回时返回的是返回对象的别名,则不会调用拷贝构造,但值得注意的是如果返回对象是局部函数的局部对象,当这个函数结束时就销毁了,这时候的引用会出现大问题,相当于野指针的解引用。

4.3 从两个角度解析拷贝构造

1.默认拷贝函数我们不写时,生成的构造函数构造会做出那些行为,是否符合我们的要求

编译器默认生成的构造函数对内置类型成员对象不做处理,对自定义类型成员变量会调用它的

拷贝构造。如果该类不指向资源自动生成的符合我们的要求,反之不符合(小技巧:如果该类需要写析构函数,纳闷大概率要写拷贝构造

2.如果不符合,我们自己如何实现符合要求的函数。

根据函数的特点来书写

4.4 拷贝构造的几个问题

1.如何调用拷贝构造

一共有两个方式:

2.为什么拷贝构造的第一个函数必须是自身类型的引用 ,指针是否也可以

如果我们使用的是传值传参,那么我们将d2拷贝给d1时要调用拷贝构造即线1,进行拷贝构造之前我们需要把实参传给形参根据拷贝构造函数的特点3会自动调用拷贝构造即线2,拷贝构造又要传参,传值传参又调用拷贝构造即线3。。。如此会构成无穷递归调用。根据上述用指针也是可以的但是没有引用方便。

五、赋值运算符重载

5.1 运算符重载

  • 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转化调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有返回类型和参数列表和参数列表以及函数体(注意如下图他的形参使用了引用,原因是如果使用传值传参会调用拷贝构造,引用不会这样效率更高,也可以在形参的前面加上const)。

类似于上图的形式,但是因为我们是在类外通过类调用类的私域是不允许的,有两种方法来解决

第一种Date 提供get函数(如下图),第二种即在类中书写,第三种就是友元函数

补充:友元函数

1. 友元提供了一种突破类访问限定符封装的方式,友元分为友元函数和友元类,在函数声明或者类声明的前面加上friend,并且把友元声明放到一个类的里面

例子:

友元函数

  

友元类:

 2. 外部友元函数可以访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。

3. 友元函数可以在类的而任何地方声明,不受类访问限定符限定。

 4. 一个函数可以是多个类的友元函数。

5. 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员

6. 友元函数是单向的,不具有交换性和传递性

7.有时提供了便利。但是友元会加强耦合度,破坏了封装。 

  • 重载运算符的参数个数和该运算符作用的运算对象数量一样多。即一元运算符有一个参数,二原参数有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
  • 上述我们叙说了第二种方法是在类中书写,但是类的成员函数会自动的加一个this指针的参数,所以如果我们如果要在类中写运算符重载就需要换一种写法

  •  运算符重载以后优先性和结合性与对应的内置类型运算符保持一致。
  • 不能通过连接语法中没有的符号来创建新的操作符:比如opertor@.
  • .*  ::   sizeof  ?: . 以上五个运算符不能重载
  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x,int y)
  • 一个类需要重载那些运算符,是看那些运算符重载后有意义,比如Date类重载opertor-就有意义,但是重载operator+就没有意义。
  • 重载++运算时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。c++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。

  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一形参位置是左侧运算对象,调用时就变成了<<cout,不符合使用习惯和可读性。重载为全局函数把全局函数ostream/istream 放在第一个参数位置就好了,第二个形参位置当类类对象。

 如果我们将他写成成员函数:

那么他调用的形式:

虽然也能完成功能,但是他不太符合我们从前的用法,所以我们只能把他写成全局函数但是我们就就不能通过类来调用类的成员变量 我们可以使用两种方法:友缘函数和在类中写get函数。

4.2 赋值运算符重载

(1) 赋值运算符的定义

赋值运算符是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值(与拷贝构造最大的不同,拷贝构造是对一个对象拷贝初始化给另一个要创建的对象

(2) 赋值运算符重载的特点
  • 赋值运算符重载是一个运算符重载,规定必须为成员函数。赋值运算重载的参数建议写成const       当前类类引用,否则会传值传参会有拷贝。

当我们自己给自己赋值的时候也会走一样的步骤这显然有一些繁琐。所以升级版:

  • 有返回值建议写成当前类类型引用,引用返回可以提高效率,有返回值支持连续赋值场景。
  • 没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会进行浅拷贝,对自定义类型成员变量会调用他的赋值重载函数。

  • 像Date这样的类他的成员变量全是内置类型并没有指向资源(申请空间),编译器自动生成的赋值预算符重载就够了。但像Stack这样的类他的成员变量虽然都是内置类型但是里面的int*a指向了资源(申请了空间)如果不写赋值预算符重载就会进行浅拷贝会一个个字节的拷贝,那么拷贝后两个指向的资源就一样了(空间),如果我们改变一个对象这个空间的值那么另一个对象这个空间的值就会改变,像MyQueue这样的类,他的成员变量都是内置类型,编译器自动生成的赋值预算符重载会自动调用Stack的。赋值预算符重载。

我们能很清楚的看见如果stack浅拷贝,当我们改变s1的_a时s2的_a也会改变。

(3) 两个角度解析赋值运算符重载

1.默认赋值运算符我们不写时,生成的函数构造会做出那些行为,是否符合我们的要求

同拷贝函数相似。

2.如果不符合,我们自己如何实现符合要求的函数。

根据函数的特点来书写

六、取地址运算符重载

6.1 const成员函数

  • 将 const修改的成员函数称之为const成员函数,const 修改成员函数放到成员函数列表的后面
  • const 实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改
  • const实际修饰Date类的Print成员函数,Print隐含的this 指针由Date* const this 变为了const Date*const this

  • 因为print 里面的this指针的格式Date*const this 在本次的调用中this指针指向d1(const Date),d1的地址要传给this指针,涉及权限的放大只需要在print 成员函数后加一个const就行了;

6.2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就够我们用了,不需要去显示实现。除非一些很特殊的场景,如:我们不想让别人取到当前类的地址,就可以自己实现一份,胡乱返回一个地址


http://www.kler.cn/a/528616.html

相关文章:

  • 开源智慧园区管理系统对比五款主流产品探索智能运营新模式
  • Java篇之继承
  • pytorch实现循环神经网络
  • 创建前端项目的方法
  • Windows程序设计9:文件的读写操作
  • SSM开发(三) spring与mybatis整合(含完整运行demo源码)
  • Linux命令入门
  • Python 模块导入问题终极解决指南
  • 土地覆盖产品批量下载(GLC_FCS30 、Esri_GLC10、 ESA_GLC10 、FROM_GLC10)
  • 深度学习 DAY3:NLP发展史
  • 网络工程师 (11)软件生命周期与开发模型
  • vscode命令面板输入 CMake:build不执行提示输入
  • Mono里运行C#脚本39—mono_jit_runtime_invoke函数
  • mac 手工安装OpenSSL 3.4.0
  • Linux02——Linux的基本命令
  • 水瓶加水时的重心变化,MATLAB计算与可视化
  • Day24 洛谷普及2004(内涵前缀和与差分算法)
  • 【上篇】-分两篇步骤介绍-如何用topview生成和自定义数字人-关于AI的使用和应用-如何生成数字人-优雅草卓伊凡-如何生成AI数字人
  • MySQL 如何深度分页问题
  • 论文阅读(十):用可分解图模型模拟连锁不平衡
  • 第25节课:前端缓存策略—提升网页性能与用户体验
  • 早期车主告诉后来者,很后悔买电车,一辈子都被车企拿捏了
  • kamailio-ACC_JSON模块详解
  • 【算法设计与分析】实验7:复杂装载及0/1背包问题的回溯法设计与求解
  • 快速了解Java虚拟机(JVM)以及常见面试题(持续更新中
  • python学习——常用的内置函数汇总