「C++」类和对象2

🎇个人主页Ice_Sugar_7
🎇所属专栏C++启航
🎇欢迎点赞收藏加关注哦!

🍉前言

C++中,为了方便操作对象,引入了六个默认成员函数,它们默认就有的,即使类为空,它们也是实际存在的。这六大函数各有特色,规则也与与常规函数有所不同,建议不要以理解常规函数的方式来理解这六大函数,不然你学起来会很难受的

🍉构造函数

你写好了一个栈,在定义一个变量之后就直接去进行入栈操作,由于你没初始化,top和capacity都为空,很可能导致程序崩溃。
没有初始化的后果挺严重的,但是老是会被忘记,于是C++中引进构造函数,可以帮你初始化,来看看它是怎么初始化的。

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;
};

可以看到:构造函数的函数名与类名相同,没有返回值
注意:没有返回值指的是函数返回值的位置什么都不用写,不要写成void

构造函数在创建类类型对象时由编译器自动调用,用来保证每个数据成员都有一个合适的初始值,而且在对象整个生命周期内只会调用一次
(虽然叫作构造,但是它并不创造什么东西,只是对对象成员进行初始化)

构造函数也支持重载,不过默认构造函数只有一个

上面说过,构造函数会自动调用,进行初始化

🍌参数

如果构造函数没有参数的话,那你在调用的时候,后面不能加括号(),因为这样会与函数声明混淆,所以规定不加了。带参的话就和正常函数一样写就ok了

🍌默认构造函数

如果一个构造函数不带参数或者全缺省,也就是说不传参就能调用的话,它就成了默认构造函数

🥝两种类型

编译器会将变量识别为两种类型:内置类型(如char、int…)和自定义类型
而对于自定义类型的指针,它也是内置类型,因为它只是指向自定义类型,但本质还是一个指针变量

🥝编译器生成的默认构造函数(附调试小技巧)

如果你没写默认构造函数,那么编译器会自动生成一个默认构造函数(有写的话就不会生成了),它会初始化自定义类型的变量(调用这个成员的默认构造函数,如果没有,那编译器就会生成该成员的默认构造函数),而对于内置类型变量,有的编译器会进行初始化(vs2019、vs2022会),有的不会,也就是说不确定是否会初始化,建议一般当成没有初始化

总结一下,一般情况下都要自己写构造函数,除非所有成员都是自定义类型,或者给了缺省值。你不写的话就自动用系统的默认构造函数,不过系统自带的有点不靠谱

不过像这样,有的变量会初始化,而有的不进行初始化确实很不好,所以C++11支持声明时给缺省值,用缺省值对内置类型进行初始化,完善编译器自带的默认构造函数的功能(注意:此时仍然是声明而非定义

class Date {
public:
	//...
private:
	int _year = 2023;
	int _month = 1;
	int _day = 1;
};

学到这里,可以知道默认构造函数有三种:

①没有显式定义默认构造函数时,编译器默认生成的构造函数;
②无参构造函数也可以叫做默认构造
③全缺省也可以叫默认构造

不传参数就调用构造,都可以叫默认构造

注意:这三个不能同时存在,因为它们都可以无参调用,同时存在的话调用会产生歧义

另外补充一个调试的小技巧:在调试时想查看对象里面的成员,在监视窗口输入this就可以全部看到了,不用去一个一个敲了。


🍉析构函数

如果你信誓旦旦地说“我肯定不会忘记初始化的”,那么我猜你肯定还是会忘记销毁的。(doge)
与构造函数相对,析构函数,类似我们自己写的destroy函数,它的特征其实和构造函数差不多:

①函数名就是类名前面加上~
②没有参数(说是说没有参数,但实际上参数部分自带一个this),也没有返回值类型。因此,它不支持重载(也就是说你自己写的析构函数只有一个)
③一个类只能有一个析构函数。若未显式定义,那么系统会自动生成默认的析构函数

析构函数清理的是对象当中动态开辟的空间(也就是用free把它们给释放掉,不然会导致内存泄漏),而非对象本身。对象及其他非静态局部变量的空间都是编译器建立栈帧时开的。这些空间在作用域结束后也跟着销毁了。
下面是一个栈的析构函数:

class Stack {
public:
	~Stack() {
		free(array);
		array = nullptr;
	}

private:
	int* array;
	int top;
	int capacity;
};

🍌默认析构函数

刚才上面说过,你自己不能写多个析构函数,只能写一个,那其实这个也就是默认析构函数了
如果你不写,那编译器也会自动给你生成一个。
默认生成的析构函数,它的行为和构造函数相似,也是不处理内置类型,而对于自定义类型,则会去调用该类型的析构函数
如果对象的成员都是内置类型,且不包括指针,那你靠编译器生成的这个就ok了,因为像是int a、char b这些其实没有清理的必要。真正需要清理的是动态内存开辟的空间

🥝调用

默认析构函数会在对象销毁时自动调用
如果在一个函数中,那么函数栈帧销毁时就会调用。
(如果是在主函数中,也是等到主函数结束时才调用,但注意这里的“结束”并不是真正的结束,此时会调用所有全局变量和静态变量的析构函数,直到所有全局变量和静态变量的析构函数完成后,程序才算真正结束)


🍉拷贝构造函数

拷贝构造函数长得和构造函数差不多,不过它只有一个参数(算上this就是两个)

Date(const Date& d) {
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
int main() {
	Date d1;
	d1.Print();
	Date d2(2023,11,28);
	d2.Print();

	Date d3(d1);

	return 0;
}

这里就是把对象d1的成员拷贝给this,this指向实例化的对象,即d3。相比于构造函数,拷贝构造只需在构造函数后面加上拷贝对象就ok了

自定义类型变量传值调用(值拷贝),先通过拷贝构造函数进行拷贝,然后才调用函数

编译器生成的默认拷贝构造函数,对内置类型完成值拷贝,对自定义类型,调用其拷贝构造函数

对象成员若全为内置类型,那直接值拷贝没有问题;不过如果有自定义类型,这时候还值拷贝,就会导致无穷递归了

🍌无穷递归

在这里插入图片描述

d1传参,得先生成一份临时拷贝,然后传给形参,这就要调用拷贝构造函数,我们假设这份临时拷贝叫d4,那就有Date d4(d1),但是为了生成d4这份拷贝,d1又得再传参,就得再生成一份临时拷贝,假设这份拷贝叫d5,那就要Date d5(d1)……这样下去子子孙孙无穷匮也,陷入死循环了

所以传值是不行滴,得传引用(因为你要拷贝,拷贝的对象已经存在),为防止拷贝对象被更改,记得加个const修饰

Date(const Date& d) {
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

🍌浅拷贝

上面讲的无穷递归便是浅拷贝带来的问题,除此之外,指针如果浅拷贝的话会出现共用空间的情况,会导致同一块空间多次析构的问题。
比如对于一个栈(如下图)
在这里插入图片描述
因为是浅拷贝,所以d1和d2的int*的值是一样的,说明指向同一个数组。当d1的生命周期结束后,自动调用析构函数清理这块空间,然后d2同样调用析构函数,会free释放相同的空间,后果很严重。
(如果free函数释放的指针不是通过malloc等函数动态申请的内存空间,或者已经释放过了,会导致不可预料的后果,比如程序崩溃或者产生非法操作,因为系统无法判断这块内存是否可以被释放。)

除了重复析构之外,如果d2通过指针向该空间存储数据的话,可能会把d1的数据覆盖掉

🍌构造与拷贝构造

写拷贝构造函数的前提是已经存在构造函数。如果只写拷贝构造函数的话,那这个类根本就没法创建对象(因为定义对象和初始化对象这两步是绑定在一起的)


🍉运算符重载

在讲第四个默认函数——赋值重载函数之前,得先了解下运算符重载
C++为了增强代码的可读性引入了运算符重载,它使得自定义类型可以直接使用运算符
运算符重载是具有特殊函数名的函数,既然是函数,那就有返回值、函数名和参数列表,它的返回值和参数列表与普通函数差不多
有区别的点在于它的函数名,函数名为:关键字operator后面接需要重载的运算符符号比如想比较两个自定义类型是否相等,那就是operator=,具体名字要根据我们的需求起的,而且要有实际意义
注意:

①不能重载非运算符。如operator@
②重载操作符必须有一个类类型的参数
③运算符重载如果作为类的成员函数,那么它的形参会比操作数少1个,因为形参部分有一个隐藏的this
.*::(域作用限定符)sizeof? :(三目操作符).(访问成员操作符)这五个运算符不能重载(笔试题常考)

🍌一元运算符重载

一元运算符分为前置和后置两种形式,比如++a和a++,在参数位置分别用无参数和一个int参数来区分前置和后置

Date operator++();  //前置
Date operator++(int); //后置

🍌二元运算符重载

类的对象默认占据第一个位置(this),所以对象只能作为左操作数

🍌运算符重载和函数重载的关系

二者毫无关系,只是称呼相近而已

🍌全局运算符重载

运算符重载如果是全局的,那么我们就需要让成员变量都是公有的,那这样的话该如何保证封装性呢?
有两个办法:

①使用友元函数
在函数声明前加上friend就是友元函数了,这样类外的函数定义就可以使用类的成员了,下篇文章中将详细讲解
②直接在类中重载为成员函数


🍉赋值重载函数

了解运算符重载的机制之后,学习赋值重载就很简单了
如果现在有一个对象d1,我们想创建和它一样的对象d2,那就用拷贝构造函数Date d2(d1),但如果d1和d2都已经存在,那只要赋值拷贝就行了

Date& operator=(const Date& d)  //加const防止不小心修改了右值
 {
 	if(this != &d)
 	{
 		_year = d._year;
		_month = d._month;
		_day = d._day;
 	}
 		return *this;  //传引用返回,提高返回效率
 }

返回值设为Date&而非void的目的是确保有返回值,以支持连续赋值

🍌默认赋值重载函数

前面讲到的构造和析构函数都有编译器生成的默认函数,而赋值重载它也有默认函数,这个函数对于内置类型采用值拷贝,对于自定义类型会调用它的赋值函数

对于日期类这种类型,显然值拷贝就够用了,但如果对于栈这种需要动态开辟内存的类就不行了(值拷贝会发生重复析构的问题),这时候就要进行深拷贝

然后有一个重要的结论:因为类中不显式实现赋值运算符重载的话,编译器会生成默认的赋值重载,此时你在类外实现一个全局的赋值运算符重载就会和默认生成的产生冲突,所以赋值运算符只能重载成类的成员函数,不能重载成全局函数


🍉两个取地址重载函数

这两个函数指的是普通对象取地址重载函数和const对象取地址重载函数,由于编译器生成的默认函数能覆盖大部分使用场景,所以一般很少自己实现这两个函数
普通对象的取地址和结构体的取地址差不多,重点来讲const对象
我们将const修饰的成员函数称为const成员函数,const修饰类的成员函数,实际修饰的是该成员函数隐藏的this指针,表明在这个成员函数中不能对类的任何成员进行修改。

	 const Date* operator&()const {  //返回值类型为const Date*;修饰this的const加在参数部分的括号后面
		return this;
	}

(这里只是示例,一般不用自己实现)

🍌const与非const

const对象可以调用非const成员函数吗?
非const对象可以调用const成员函数吗?
const成员函数内可以调用其它非const成员函数吗?
非const成员函数内可以调用其它const成员函数吗?

解决这些问题的关键在于分析权限的变化,即权限是被放大还是被缩小,亦或是不变(权限平移)。记住权限只能缩小或平移,不能被放大
先看第一个问题,const对象权限小,而成员函数的this没有const修饰,权限比较大,所以不能调用
第二个问题你就自己分析咯,答案是可以
第三、四问,const成员函数一定不会修改对象的状态,所以可以放心调用非const成员函数;而非const成员函数可能修改成员状态,所以不能调用const成员函数


🍉写在最后

以上就是本篇文章的全部内容,如果你觉得本文对你有所帮助的话,那不妨点个小小的赞哦!(比心)

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值