C++类与对象第二篇:(构造函数析构函数拷贝构造运算符重载取地址及const取地址操作符重载)

Posted 无聊的马岭头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类与对象第二篇:(构造函数析构函数拷贝构造运算符重载取地址及const取地址操作符重载)相关的知识,希望对你有一定的参考价值。

前言

在这里插入图片描述

1. 构造函数

1.1构造函数的引出

写个日期(Date)类

class Date
{
public:
	void SetDate(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.SetDate(2021,5,25);
	d1.print();
	
	Date d2;
	d2.SetDate(2021,6,25);
	d2.print();
	
	return 0;
}

对于Date类来说,可以通过公有的SetDate的方法给对象初始化,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那么是否在创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字和类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。

1.2 构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

特性如下

  1. 函数名和类名相同

  2. 没有返回值

  3. 在对象实例化时编译器自动调用对应的构造函数
    在这里插入图片描述
    构造函数的调用也比较特殊:在创建对象时候直接加参数。

  4. 构造函数可以重载
    在这里插入图片描述

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
    在这里插入图片描述

  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
    在这里插入图片描述
    问题来了】下面代码从语法上是没有错误的,但是在调用的时候,编译器不知道调用哪个构造函数,对构造函数的调用不明确。所以写构造函数的时候最好写一个全缺省的构造函数,这种方式能适应各种场景。
    在这里插入图片描述

  7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d1对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??
    在这里插入图片描述
    【原因】
    如果类中没有显式定义的构造函数,编译器会自动生成一个默认构造函数,且编译器自动调用。但是,编译器自己生成的默认构造函数不会对类中内置类型初始化(这也是C++的一个缺陷)。会对类中自定义类型初始化。
    内置类型(基本类型):语言原生定义的类型 如:int、char、double还有指针等。
    自定义类型:我们使用class、struct等定义的类型,编译器回去调用他们的默认构造函数初始化。
    在这里插入图片描述
    【大总结】
    1、构造函数的名字和类一样
    2、构造函数没有返回值
    3、对象实例化时,编译器会自动调用构造函数(可以自己去调试一下看看)
    4、构造函数也可以重载
    5、在类中没有显式定义构造函数,则c++编译器会自己生成默认构造函数
    6、编译器默认生成的构造函数不会对自己类中的内置类型初始化
    7、构造函数是保证每个数据成员都有一个合适的初始值

2. 析构函数

2.1 概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

2.2 析构函数的特性

析构函数是特殊的成员函数。

特征如下:

  1. 析构函数名是在类名前面加上字符~
  2. 无参无返回值
  3. 一个类有且只有一个析构函数(不能重载)。若没显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统自动调用析构函数
    在这里插入图片描述
  5. 编译器自动生成的析构函数和编译器自动生成的构造函数有一样,只会对自定义类型成员调用它的析构函数,内置类型不处理

【问题】
是不是所以类中我们都写一个析构函数呢?
在这里插入图片描述

【答】不是所以类中都要显式定义析构函数,如Date类中的显式定义析构函数没有意义。而对于stack类具有意义。当然,没有显式定义析构函数,编译器会默认生成一个析构函数。

【附加问题1】
在这里插入图片描述
上面谁先析构?
【答】A先析构,因为对象是定义在函数中,函数调用会建立栈帧,栈帧中的对象构造和析构也符合后进先出。

【附加问题2】
数据结构的栈和堆和内存分段区域中的栈和堆有什么区别?
【答】
1、他们之间没有绝对的联系,因为他们属于两个学科的各自的一些命名。
2、数据结构栈和系统分段栈(函数栈帧)中的对象都符合后进先出。

【大总结】
1、析构函数名:~类名
2、无参无返回值
3、不能进行函数重载,有且只有一个析构函数
4、类中没有显式定义析构函数,编译器会默认生成一个析构函数
5、对象生命周期结束,C++编译系统自动调用析构函数
6、析构函数是完成类的一些资源清理工作

3. 拷贝构造函数

3.1 概念

拷贝构造函数:只有单个行参,该行参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day =day;
	}


	//拷贝构造函数
	Date(const Date& date)//该行参是对本类类型对象的引用(一般用const修饰)
	{
		cout << "自动调用Date(Date& d)" << endl;
		_year = date._year;
		_month = date._month;
		_day = date._day;
		cout << "拷贝成功" << endl;
	}
	void print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;

	}
	
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2021,5,25);
	Date d2(d1);//自定义类型拷贝的写法,把d1拷贝给d2
	d1.print();
	d2.print();
}

在这里插入图片描述

3.2 拷贝构造的特性

拷贝构造函数也是特殊的成员函数。

其特征如下:

  1. 拷贝构造是构造函数的一个重载类型
  2. 拷贝构造函数的参数只有一个且必须使用引用传参使用传值传参方式会引发无穷递归调用
    在这里插入图片描述
    在用传值方式时,d1要拷贝给d2,就要传值(Date d2(d1)),要调用拷贝构造函数,但是传值也也是一种拷贝,所以要先进行(Date date(d1)),调用拷贝构造函数,d1要拷贝给date,就要进行(Date date(d1)),如此一直递归。
    总之,要调用拷贝构造函数,就先传参,传参使用传值方式,就又要调用拷贝构造函数,循环往复。
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝(对内置类型也会处理)。
    在这里插入图片描述
    类中没有显式定义拷贝构造函数,编译系统会自动默认生成一个拷贝构造函数,且该编译系统默认生成的拷贝构造函数对内置类型也会处理

【问题来了】
既然编译器生成的默认拷贝构造函数已经可以对内置类型进行拷贝,那我们还需要自己实现吗?
】当然需要,对于Date日期类没有必要,但是对像Stack栈类是有必要的。

在这里插入图片描述

在拷贝完了后,d1和d2都指向同一块空间。
在这里插入图片描述
【会出现的问题】
1、在析构的时候,同一块空间被析构两次,第一次析构,这块空间已经free了,然后这块空间可能又被系统分配到了其他地方,然后又来一次free,就相当于把别人的给释放了。
2、如果我们还有对d1和d2进行一些操作,但是d1和d2指向同一块空间,对d1操作也就同时对d2操作,这不是我们想要的结果。

所以:像Stack这样的类,需要我们自己实现深拷贝(深拷贝以后说)

【大总结】
1、拷贝构造函数只有单个新参,该形参是对本类类型对象的引用
2、拷贝构造函数是构造函数的一种重载形式
3、编译系统默认生成的拷贝构造会对内置类型处理
4、系统会自动调用拷贝构造函数

4.赋值操作符重载

4.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函类似

函数名字:关键字operator后面连接需要的重载运算符符号

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

注意:

  • 不能通过连接其他符号创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现

【注意】 运算·符重载和构造函数中的重载没有关联。

1、构造重载时支持定义同名函数
2、运算符重载时为了让自定义类型可以像内置类型一样使用运算符

class Date
{

public:
	Date(int year = 2021, int month = 5, int day = 25)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //判断相等的运算符重载
	int operator==(Date &d)//--->int operator==(Date* this,Date &d)
	{
		return _year == d._year
			&&_month == d._month
			&&_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;

};

int main()
{
	Date d1;
	Date d2(d1);
	d1 == d2;//----->   d1.operator==(&d1,d2);

	cout << (d1==d1) << endl;	//	注意优先级的问题,要用括号括起来,<<优先级大于==
}

在这里插入图片描述
【注意】
在运算符重载函数中最好用引用,用传值方式会调用拷贝构造函数。

4.2 赋值运算符重载

特点

  1. 返回值:类类型

  2. 返回*this

  3. 检查1是否自己给自己赋值

  4. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
    在这里插入图片描述

  5. 参数类型

class Date
{

public:
	Date(int year = 2021, int month = 5, int day = 25)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

		return *this;
	}

	void prit()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;

};

int main()
{
	Date d1;
	Date d2(2021,6,6);
	cout << "d1:" << endl;
	d1.prit();
	cout << "d2:" << endl;
	d2.prit();
	d1 = d2;//----->   d1.operator=(&d1,d2);
	cout << "赋值完成后" << endl;
	cout << "d1:" << endl;
	d1.prit();
}

在这里插入图片描述
【注意】那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。但是对于Stack这样的类会崩掉的哦,这里以后会在深拷贝里面讲。

【这里是我用上面的内容和上篇的内容写的日期类,对这些内容的理解有帮助】
👉日期类的实现

5. const成员

5.1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
在这里插入图片描述

【思考几个问题】

  1. const对象可以调用非const成员函数吗?
    【答】:不能,因为这样属于扩大权限。
    在这里插入图片描述

  2. 非const对象可以调用const成员函数吗?
    【答】:能,缩小权限可以。
    在这里插入图片描述

  3. const成员函数内可以调用其它的非const成员函数吗?
    【答】:不能,因为,因为属于扩大权限。
    在这里插入图片描述

  4. 非const成员函数内可以调用其它的const成员函数吗?
    【答】:能
    在这里插入图片描述

6. 取地址及const取地址操作符重载

这两个默认成员函数一般不能重新定义,编译器会默认生成。

他们也是运算符重载函数,和赋值运算符重载函数一样,类中,没有显式定义,编译器会默认自动生成。

class Date
{
public:
	//.取地址运算符重载
	Date* operator&()
	{
		return this;
	}
	//const取地址操作符重载
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

没有什么意义哦。
真要有点作用!就!!!

class Date
{
public:
	//.取地址运算符重载
	Date* operator&()
	{
		return nullptr;
	}
	//const取地址操作符重载
	const Date* operator&()const
	{
		return nullptr;
	}
private:
	int _year;
	int _month;
	int _day;
};

总结

本次的对所学内容的总结,如果有错误的地方,请懂哥指出,共同学习。爆赞👍

以上是关于C++类与对象第二篇:(构造函数析构函数拷贝构造运算符重载取地址及const取地址操作符重载)的主要内容,如果未能解决你的问题,请参考以下文章

[C++潜心修炼]类与对象:构造与析构与拷贝构造

[ C++ ] C++类与对象之 类中6个默认成员函数

C++从入门到入土第二篇:类和对象基础

初始c++——类与对象

C++初阶第五篇——类和对象(中)(构造函数+析构函数+拷贝构造函数+赋值操作符重载)

C++ ----类与对象(上)