C++类中重要的四个默认成员函数

一、了解类的默认成员函数的概念

默认成员函数是类里面比较特殊的函数
默认成员函数是指:用户没有显示实现,编译器自动生成的成员函数叫做默认成员函数
(这里的用户是编写这个类的人员)
默认成员函数主要有六个:构造函数、析构函数、拷贝构造、赋值重载、两个取地址重载

构造函数:主要是完成对象的成员的初始化,在创建对象时自动调用
析构函数:主要完成动态内存申请的空间的释放,在对象生命周期结束时自动调用
拷贝构造:主要完成一个已创建对象初始化另外一个需要创建的对象,满足条件初始化对象时自动调用
赋值重载:主要完成两个相同对象间的成员变量赋值,也是重载运算符的一个函数,对象赋值时自动调用
两个取地址重载:主要是普通对象与const对象的取地址,通常不自己实现,不是重点

在这里插入图片描述
这些函数通常是在类内部实现的,编译器默认生成或者自己显示实现
这些函数是在自己书写一些语句的时候自动调用,因此不需要自己使用访问操作符 . 或者 -> 来调用

二、主要的四个默认成员函数

主要的四个默认成员函数是:构造函数、析构函数、拷贝构造、赋值重载

1.构造函数

构造函数是类默认函数的开始,用于类对象的成员变量的初始化
注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象

1)构造函数有以下几个特点:

语法规则上的特点:
1、构造函数的函数名与类名相同(不重要知识:在Java中叫构造器并且函数名与类名也相同)
2、构造函数无返回值(直接可以不写void)
3、构造函数可以重载(回顾:重载就是函数名相同,参数列表类型不同或参数个数不同)
4、对象实例化时编译器自动调用对应的构造函数(构造函数可以重载,注意是哪一个构造函数)
本质特征:
5、如果没有显示实现构造函数,则编译器会自动生成无参构造函数,一旦显示实现了就不会生成
(编译过程中生成,不会显示到代码上)
6、编译器默认生成的构造器的作用是对于内置类型不进行初始化,对于内置类型去调用它的默认构造器
(内置类型就是基础类型如int、char一类,自定义类型就是结构体、类一些自定义的类型)
7、无参构造函数、全缺省构造函数、编译器默认生成的构造函数都成为默认构造函数,每个类最多只存在一个
(如果显示实现构造函数,且不是全缺省或者不是无参,编译器也不会自动生成,此时类没有默认构造函数)

2)特点解析

因为构造函数通常需要自己实现,毕竟可以需要让指针指向动态开辟的区域,多写就记住了
构造函数是用来初始化对象的,从理论和结构上来说初始化之后不应该返回什么,也没人接收
(Date d1(…);谁接收,不应该类似于int a = int b = 10;吧,编译都无法通过)
默认构造函数和默认成员函数不是一个概念,需要区分
大概来说,不需要传参的构造函数就是默认构造函数
三个默认构造器,最好的建议就是,实现的是全缺省的构造函数(并且只能存在一个)
在这里插入图片描述
看到以上,Date构造函数没有返回值,也没有书写
Date函数体里用于对对象初始化时成员变量的赋值或动态开辟空间
咋知道它默认调用了构造函数?
反汇编下看到:
在这里插入图片描述
根据域作用限定符可看到,这一条语句也走了Date类域里的Date函数,也可以使用逐过程(F11)调试查看进入构造函数
并且这里也反面证实,不同对象会调用同一个函数(地址相同),因此函数独有一份并不存储在对象里
是吧,自动调用

2.析构函数

析构函数是在对象出了作用域时自动调用的,就是对象生命周期结束的时候自动调用
注意:析构函数不是销毁对象,对象出了作用域编译器自动销毁
析构函数主要作用是用来释放动态申请的空间,完成清理工作(如对象里指向malloc申请的空间)

为啥这样子,因为我们通常忘记释放空间,并且代码短小并不会产生什么麻烦

1)析构函数的特点:

1、是一个函数,函数名就是类名前面加~波浪号(如 ~Date() )
2、无参数无返回值类型(void直接不写)
3、一个类只能有一个构析函数,因此不能重载,如果没显示实现,则编译器自动生成默认析构函数
4、对象生命周期结束时自动调用析构函数
5、编译器自动生成的析构函数,自动调用时对应内置类型不做处理,对于内置类型调用它的析构函数(这一点和构造有点相似)
6、如果类中没有内存申请(没有指针指向动态申请的空间),可以不显示实现,如果有则显示实现并且完成内存的释放
(如数据结构中那些顺序表、栈一些内存申请的空间需要手动实现释放功能)

2)特点解析

析构函数是完成清理工作,如动态申请的释放,而需要动态内存申请通常在构造函数内
因此在有内存申请时会在构造函数先实现,那么配套销毁在析构函数内实现
因为析构函数函数名是类名前面加 ~波浪号,也就是在构造函数前面加 ~波浪号
在C/C++内都有按位取反的意思,可以记成是和构造函数反过来的
在这里插入图片描述
如果一个类里面没有动态内存开辟的空间,可以不实现析构函数,也可以就出现函数名而没有函数体
咋知道会不会在生命周期结束的时候自动调用析构函数
可以在一个函数内定义对象,在析构函数里面打印一句提示,通过调试查看出来函数是否打印
在这里插入图片描述
以上图可以看到,函数里创建对象,函数结束了析构函数也调用了,因为打印了析构函数内的语句
析构函数在局部域和全局域还有一个先创建对象后析构、后创建的对象先析构的规则
static修饰的呢?不在这规则之内
析构顺序是:局部对象(后定义先析构) >> 局部静态对象 >> 全局对象(后定义先析构)
如果是静态修饰的全局对象,也算是全局对象的一部分
这个析构顺序如同入栈出栈

3.拷贝构造

拷贝构造也是一种构造函数,可以说是构造函数的的重载
拷贝构造函数的主要功能是:一个已存在的对象,初始化创建另一个对象
一个对象初始化创建另一个对象有两种格式,例如:

Date d1(2023, 2, 10);//已创建对象
Date d2(d1);//第一种直接写括号里
Date d3 = d1;//第二种,也是初始化创建另一个对象

第一种方法是写括号里,第二种是用 =等于号连接

1)拷贝构造函数的特点:

1、拷贝构造函数是构造函数的一个重载形式,因此函数名也是类名(也是无返回值)
2、拷贝构造函数只有一个参数,并且必须是类类型对象的引用,如果是直接传值方式编译器会报错,因为会引发无穷递归调用
3、如果不显示实现,编译器会默认生成拷贝构造函数,默认的拷贝构造函数对象是按照内存字节来拷贝
(这种拷贝叫浅拷贝,也叫值拷贝,有点像内存函数的字节拷贝)
4、编译器默认生成的拷贝构造函数只能浅拷贝,对于指向动态开辟内存的指针,只会拷贝来那片内存空间的首地址,不会开一片新的空间来让另一个指针指向
两个对象两个指针共用一块空间,这本就不合理,如果有一方先释放了那片空间,则会造成另一个指针非法访问,并且无法通过二次释放
5、拷贝构造函数典型调用场景:使用已存在对象创建新对象、函数参数类型为类类型对象、函数返回值类型为类类型对象
(第二个的意思是如果函数参数是类型为类类型对象,不是引用不是指针,进入函数之前会先调用拷贝构造给函数参数那个对象拷贝一份)
(第三个也是一样,如果返回值是类类型,返回之前也会调用拷贝构造函数生成临时变量,当然这个生成可能会被编译器优化去掉)

2)特点解析:

先从第五点看,第一个我们知道是初始化对象用的
第二个使用场景是:
在这里插入图片描述
以这个方法,现在拷贝构造是类引用类型的对象,如果只是类类型对象呢?
(Date &d ----> Date d的转变)
在这里插入图片描述
我们知道形参是类类型对象就会调用拷贝构造函数
如果拷贝构造函数的形参也是类类型对象,则会无穷调用,当然这时候编译器已经报错了
我们也看到拷贝构造的函数名是类名,和构造函数一样,因此拷贝构造是构造函数的一种重载
第二点也能知道为什么拷贝构造函数参数只能是类引用类型对象
对于第三点第四点编译器默认生成拷贝构造进行的浅拷贝,在动态申请内存的情况下不使用
在这里插入图片描述
可以看到以上,没有显示实现拷贝构造函数,当使用拷贝构造初始化另一个对象时
s2里面的指针a和s1里面的指针a共用了一片空间(地址相同)
因此浅拷贝没有帮开空间,如果先释放一个却使用另一个就会非法访问
如果两个都释放的话,编译器不会同意一片空间释放两次的

4.赋值重载

赋值重载是一种运算符重载
赋值重载主要是用于两个已经存在的对象,其中一个对象成员变量对另一个对象成员变量赋值

1)了解运算符重载规则

运算符重载就是让运算符可以执行除了内置类型的数据还能执行自己的自定义类型
例如加减乘除可以计算整型浮点型,进行运算符重载之后可以对于自定义类型里面某一个成员变量进行加减乘除
运算符重载的规则:

1、只能有两个参数不多不少
(如果是类里因为隐含了一个this指针就只需要一个参数,如果是全局或命名空间函数则需要两个参数)
2、关键字operator加符号来当函数名,例如:operator=(参数1 ,参数2)
3、可以有返回类型,通常参数列表两个参数类型相同,返回的是内置类型或自定义类型都行
4、不能改变重载的运算符之前的含义,你不能说重载的是加+而函数内部实现的是减-
5、(.*) 、(::) 、(sizeof)、 (?:) 、 (.)注意这5个运算符不能重载
(第一个是在类或结构体调用函数时需要解引用函数的操作符,第二个是域作用限定符,第三个是计算大小运算符)
(第四个是三目运算符,第五个是类或结构体引用成员函数或者成员变量的操作符)

2)了解运算符重载使用

例如下面使用类里面的运算符重载,并且看到隐含this指针:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d1)
	{
		return _year == d1._year
			&& _month == d1._month
			&& _day == d1._day;
		/*两种方法都行
		return this->_year == d1._year
			&& this->_month == d1._month
			&& this->_day == d1._day;
		*/
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 2, 10);
	Date d2(2024, 2, 11);
	cout<<(d1 == d2)<<endl;
	cout<<d1.operator==(d2)<<endl;
	//两种使用方法
}

在这里插入图片描述
看两个调用方法,可以直接使用运算符的形式,也可以对象点方法使用
如果是全局或者命名空间函数也是可以的,只不过不需要对象点方法了,需要像调用平常函数一样调用了

3)延伸类赋值重载

类里面的赋值重载也是一个普通的运算符重载,如何实现和使用与运算符重载一样
赋值运算符重载特点:

1、 赋值运算符重载格式
	1)形参列表参数类型通常使用const修饰不能改变,并且使用引用可以提高传递效率(例如const Date&)
	2)返回值类型使用引用类型,也可以不经过拷贝构造,提高返回效率
	3)检测是否存在自己给自己赋值(不做无用功)
	4)返回*this,要复合连续赋值的含义
2、赋值运算符只能重载成类的成员函数不能重载成全局函数,因为如果类里面没有显示实现,编译器自动生成
(如果全局中实现了赋值运算符重载,这时和编译器自动生成的产生冲突)
3、没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,也叫浅拷贝
(还是应付动态申请的指针无法实现,不然两个对象的指针指向同一片空间了)
4、如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

以上的特点也是类里面实现赋值重载的规则
看第一点,因为普通的赋值是支持连续赋值的,因此需要返回同类类型也需要支持连续赋值
使用const是为了防止修改,使用类引用类型是为了传参效率

第二第三点因为是默认成员函数,因此不显示实现时编译器会默认生成, 涉及到动态内存申请的空间需要自己实现
第四点通常也是在说动态内存申请的

总结

默认成员函数主要有六个:构造函数、析构函数、拷贝构造、赋值重载、两个取地址重载
重要的通常需要自己实现的有四个:构造函数、析构函数、拷贝构造、赋值重载

1、构造函数的主要任务并不是开空间创建对象,而是初始化对象
2、析构函数不是销毁对象,对象出了作用域编译器自动销毁
	析构函数主要作用是用来释放动态申请的空间,完成清理工作(如对象里指向malloc申请的空间)
3、拷贝构造函数的主要功能是:一个已存在的对象,初始化创建另一个对象
4、赋值重载主要是用于两个已经存在的对象,其中一个对象成员变量对另一个对象成员变量赋值

新的一年加油吧少年!
在这里插入图片描述

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值