【C++学习】类和对象(中)

目录

一、类的默认成员函数

 二、构造函数

 🍔如何理解构造函数

 🍟构造函数的书写和特点

三、析构函数

🍔如何理解析构函数

  🍟构造函数的书写和特点

四、拷贝构造

🍔如何理解拷贝构造

 🍟默认拷贝构造函数特点

🌮拷贝构造函数书写

🥪拷贝构造书写时注意

五、赋值运算符重载

🍔运算符重载

🍟赋值运算符重载 

🌮拷贝构造和赋值运算符重载

🥪 前置++和后置++重载

六、const成员

七、取地址运算符重载

八、结语


 

一、类的默认成员函数

🐥 类的默认成员函数任何类中,如果我们在类中什么都不写,由编译器自动生成的成员函数


接下来就以日期类为例,分开学习每一个默认成员函数、由浅入深、生动易懂

六个默认成员函数:

1️⃣构造函数

2️⃣析构函数

3️⃣拷贝构造函数

4️⃣赋值重载

5️⃣6️⃣两个取地址重载 


 二、构造函数

 🍔如何理解构造函数

class Date
{
public:
	void Init(int year, int month, int day)
	{
		year_ = year;
		month_ = month;
		day_ = day;
	}
private:
	int year_;
	int month_;
	int day_;
};

这是一个日期类,我们在创建了对象后,需要用 Init函数 给对象初始化

在C++中, 构造函数 的 作用 就是 代替了 Init函数 构造函数➡️初始化)

 🍟构造函数的书写和特点

构造函数虽然叫“构造”,但是主要的任务是初始化而不是开空间创建对象

无返回值(不写返回值) 类名(形参列表)

                                         {

                                          //初始化工作
                                          }

构造函数特殊的成员函数,其特点是

1️⃣函数名称和类名相同
2️⃣没有返回值
3️⃣创建类对象时由编译器自动调用
4️⃣并且在对象整个生命周期内只调用一次
5️⃣支持函数重载

示例:

class Date
{
public:
	// !!!函数名和类名相同
	// !!!无返回值
	Date(int year, int month, int day)
	{
		year_ = year;
		month_ = month;
		day_ = day;
		cout << "构造函数调用:" << year_ << endl;
	}
	// !!!支持重载
	Date()
	{
		//...
	}
private:
	int year_;
	int month_;
	int day_;
};

6️⃣如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认成员函数,一旦显示定义,编译器就不会再生成

验证6️⃣:

class A
{
public:
//	A(int a)
//	{
//		_a = a;
//	}
private:
	int _a;
};

void test()
{
	A a;
}


🌟解释:我们显示定义后,编译器就不会生成默认的成员函数,在创建对象时,传参不够,编译失败
注释我们显示定义的构造函数后,即可使用默认生成的成员函数,成功编译

7️⃣编译器生成的默认成员函数特点

        👉内置类型不做处理
        👉自定义类型成员会调用这个成员的构造函数

验证7️⃣

class A
{
public:
	A()
	{
		cout << "A构造函数调用:" << endl;
	}
private:
	int _a;
};

class Date
{
public:
	void Print()
	{
		cout << year_ << "年" << month_ << "月" << day_ << "日" << endl;
	}
private:
	//内置类型
	int year_;
	int month_;
	int day_;

	//自定义类型
	A _a;
};


8️⃣默认构造函数

还记得这个错误提示吗?

默认构造函数指的不止是由编译器自动生成的无参成员函数
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且只能有一个,多个存在会造成调用二义性的问题


三、析构函数

🍔如何理解析构函数

析构函数的学习可以类比构造函数的学习

学习了构造函数后,我们知道构造函数的作用是初始化对象
那么
清理资源的工作又是谁来做呢?

析构函数:析构函数的作用和构造函数相反,析构函数的工作是在对象销毁时自动调用析构函数,完成对象中资源的清理

以一个简单的栈来举例:

class Stack
{
public:
	//构造函数(完成初始化)
	Stack(int capcity=4)
	{
		_a = (int*)malloc(sizeof(int) * capcity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		_capcity = capcity;
		_size = 0;
	}
	//析构函数(完成资源的清理)
	~Stack()
	{
		if (_a!=nullptr)
		{
			free(_a);
			_a = nullptr;
			_capcity = 0;
			_size = 0;
		}
	}

private:
	int* _a;
	int _size;
	int _capcity;
};

  🍟构造函数的书写和特点

析构函数的写法:

~类名 (形参列表)

{
//资源清理

}

 析构函数特殊的成员函数,其特点是
1️⃣析构函数名是在类名前加
~
2️⃣没有返回值没有参数
3️⃣对象生命周期结束时,由编译器自动调用
4️⃣一个类只能有一个析构函数
5️⃣若未显示定义,编译器自动生成默认的析构函数

6️⃣编译器生成的默认析构函数特点

        👉内置类型不做处理
        👉自定义类型成员会调用这个成员的析构函数


🫵总结:
如果类中没有申请资源(malloc...)析构函数可以不写,因为在对象销毁后,我们也无法访问那块空间,可以直接使用编译器生成的默认析构函数,比如Date类
如果类中
有资源的申请,就一定要写析构函数,否则就会造成内存泄漏,如Stack类


四、拷贝构造

🍔如何理解拷贝构造

在一些场景下我们需要用一个已存在的对象来创建一个新的对象:
🫗就像这样:

int a=10;
int b=a;

类类型的对象有时也有这样的需求,
 

Date d1(2024,1,1);
Date d2=d1;


拷贝构造已存在的类类型对象创建新对象

 🍟默认拷贝构造函数特点

默认拷贝构造特点:

👉内置类型:值拷贝

👉自定义类型:调用它的拷贝构造
🫗代码示例:

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;
	}
private:
	//内置类型
	int year_;
	int month_;
	int day_;
};

测试结果:


问题:默认生成的拷贝构造似乎就能完成我们期望的工作,为何有时还要自己写?


 对于Date类确实可以,但如果是Stack类,我们再来看结果~


🫗解释:默认生成的拷贝构造函数对于内置类型会进行简单的值拷贝(浅拷贝),直接将a对象的值复制给b,所以ab对象中的  _a 指向同一块空间,这也就造成了问题,两个对象在销毁时都会调用析构函数,造成了同一块空间被释放两次的问题

🫵总结:
如果类中没有申请资源(malloc...)拷贝构造函数可以不写,如果类中有资源的申请,就一定要写析构函数,否则就会在析构时出现二次析构程序崩溃,如Stack类

🌮拷贝构造函数书写

拷贝构造函数也是特殊的成员函数,特征如下:

1️⃣拷贝构造函数是构造函数的一个重载形式

2️⃣拷贝构造函数的参数只有一个,并且必须是同类型对象的引用

3️⃣若未显示定义,编译器会生成默认的拷贝构造函数

加入拷贝构造后的Stack类:

class Stack
{
public:
	//构造函数(完成初始化)
	Stack(int capcity=4)
	{
		_capcity = capcity;
		_size = 0;
		cout << "Stack()构造函数" << endl;
		_a = (int*)malloc(sizeof(int) * capcity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}
	
	}

	//拷贝构造函数
	Stack(const Stack& s)
	{
		_capcity = s._capcity;
		_size = s._size;
		_a =(int*) malloc(sizeof(int) * _capcity);
		if (_a == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		memcpy(this->_a, s._a, sizeof(int)*_size);
	}

	void Push(const int& data)
	{
		_a[_size] = data;
		_size++;
		//...
	}

	//析构函数(完成资源的清理)
	~Stack()
	{
		cout << "~Stack()析构函数" << endl;
		if (_a!=nullptr)
		{
			free(_a);
			_a = nullptr;
			_capcity = 0;
			_size = 0;
		}
	}

private:
	int* _a;
	int _size;
	int _capcity;
};

🫗运行结果:正常运行

🥪拷贝构造书写时注意

拷贝构造的参数必须是类类型对象的引用,如果不是引用,将会直接报错

问题‼️:

小知识👓:

拷贝构造函数调用的场景

1️⃣使用已存在的对象创建新对象

2️⃣函数参数类型为类类型参数

3️⃣函数返回值为类类型


五、赋值运算符重载

🍔运算符重载

学习赋值运算符重载之前,我们先来学习运算符重载
C++为了增加代码的可读性,引入了
运算符重载
其字面意思就是让运算符能够有不同的意义不过仅对类类型对象而言
拿日期类来讲,有了运算符重载,我们可以进行两个日期的比较、两个日期相减-,日期加天数+,减天数的操作

可以写出下面的代码:

虽然是函数,但是使用起来十分明了

Date d1(2024,1,1);
Date d2(2024,10,1);

int day=d2-d1;
cout<<"距离国庆还有"<<day<<"天"<<endl;

cout<<(d1>d2)<<endl;

Date d3=d1+100;

函数原型: 返回值类型 operator操作符(参数列表) 

注意:

👉不能连接其他符号,创造操作符 例如:operator@

👉重载操作符中必须有一个类类型参数

👉不能重载内置类型间的操作符

👉作为类成员函数重载时,第一个参数this被隐藏

👉.* ::  sizeof  ?:  .  这5个运算符不能重载

🫗示例:

	bool operator>(const Date d)
	{
		if (year_ < d.year_)
		{
			return false;
		}
		else if (year_ == d.year_ && month_ < d.month_)
		{
			return false;
		}
		else if (year_ == d.year_ && month_ == d.month_ && day_ <= d.day_)
		{
			return false;
		}
		else
		{
			return true;
		}
	}

	bool operator==(const Date d)
	{
		if (year_ == d.year_ && month_ == d.month_ && day_ == d.day_)
			return true;
		return false;
	}

有了 >== 后,我们可以复用== 实现>= 、<、 <= 这些函数

调用时:

🍟赋值运算符重载 

📗

赋值运算符重载即让类类型对象也可以使用

赋值运算符的重载形式:

参数类型:const T& ,传递引用可以提高传参效率
返回值类型:T& 返回引用可以提高返回效率,有返回值就可以支持连续赋值
 

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			year_ = d.year_;
			month_ = d.month_;
			day_ = d.day_;
		}
		return *this;
	}

📘

‼️赋值运算符只能重载成类的成员函数不能重载成全局函数

🫗原因:

赋值运算符是默认构造函数,如果不显示实现,编译器会生成一个默认的。

如果定义全局的赋值运算符重载,就和编译器在类中自动生成的赋值运算符重载冲突。因此,赋值运算符只能是类的成员函数。

📙

赋值运算符重载作为默认成员函数,如果用户没有显示定义,编译器就会自动生成,以值的方式逐字节拷贝

默认赋值运算符重载作用:

👉内置类型:值拷贝

👉自定义类型:调用它的赋值运算符重载

📕

何时需要我们来显示定义呢?
其实和拷贝构造的定义的情况十分类似:

如果类中没有申请资源(malloc...)赋值运算符重载可以不写,如果类中有资源的申请,就一定要写析构函数,否则就会在析构时出现二次析构程序崩溃,如Stack类


🌮拷贝构造和赋值运算符重载

大家是否学习完之后,就把这两个函数混淆了

我来带大家理清一下🌟

拷贝构造:已存在的对象给未存在的对象拷贝

赋值运算符重载:已存在的两个对象间的拷贝

🫗代码验证:

class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}
	//拷贝构造
	A(const A& data)
	{
		cout << "拷贝构造" << endl;
		_a = data._a;
	}
	//赋值运算符重载
	A& operator=(const A& data)
	{
		cout << "赋值运算符重载"<< endl;
		_a = data._a;
		return *this;
	}
private:
	int _a;
};

A func(A data)
{
	return data;
}

int main()
{
	A a1;
	A a2;
	A a3 = a1;//拷贝构造
	a2 = a1;//赋值运算符重载
	a3 = func(a1);
	return 0;
}

🥪 前置++和后置++重载

前置++和后置++都是一元运算符

🔥

由于无法通过返回值类型构成重载

为了能区分前置++和后置++,我们这样规定:

后置++在重载时多增加一个int类型的参数,但调用该函数时参数不用传递

//后置++ 返回调用之前的值
Date operator++(int)
{
	Date tmp = *this;
	day_++;
	++*this;
	return tmp;
}


//前置++
Date& operator++()
{
	day_++;
	if (day_ > GetMonthDay(year_, month_))
	{
		day_ = 1;
		month_++;
		if (month_ == 13)
		{
			year_++;
			month_ = 1;
		}
	}
	return *this;
}

 

六、const成员

const成员:将const修饰的“成员函数”称之为const成员函数,实际修饰该成员函数隐含的this指针


❓问题引出:


那么如何来修饰隐含的this指针:在成员函数后加const

🫗几个小问题:

1️⃣const对象可以调用非const成员函数吗?

答:不可以,属于权限的放大

2️⃣非const对象可以调用const成员函数吗?

答:可以,属于权限的缩小

3️⃣const成员函数内可以调用其他非const成员函数吗?

答:不可以,属于权限的放大

4️⃣非const成员函数内可以调用其他的const成员函数吗

答:可以,属于权限的放大


 

七、取地址运算符重载

一般对象和const对象取地址重载

🌟最后两个默认成员函数一般不用重新定义,使用场景非常少

class Date
{
public:
	Date* operator& ()
	{
		return this;
	}

	const Date* operator&()const
	{
		return this;
	}
};

 

八、结语

🫡你的点赞和关注是作者前进的动力!

🌞最后,作者主页有许多有趣的知识,欢迎大家关注作者,作者会持续更新有意思的代码,在有趣的玩意儿中成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值