【C++】类和对象(中):构造、析构、拷贝构造、运算符重载

目录

一.类的默认成员函数

二.构造函数

三.析构函数

四.拷贝构造函数

五.运算符重载

 六.赋值运算符重载

七.取地址运算符重载


一.类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数。一个类,在我们不写的情况下会自动生成以下六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数、两个取地址重载函数,C++11后新增了两个默认成员函数:移动构造和移动赋值,以后有机会再讲解,本文集中于介绍前六个默认成员函数。

二.构造函数

构造函数是特殊的成员函数,虽然名字是叫构造,但实际上构造函数的主要任务是初始化对象而不是开空间创建对象,通常我们使用的局部对象是栈帧创建时,空间就开好了。构造函数的本质是要替代我们以前Stack类中实现的Init函数的功能,构造函数的自动调用的特征完美替代了Init

构造函数有以下特点:

  1. 函数名与类名相同
  2. 无返回值(意思是什么也不要写,包括void)
  3. 对象实例化时系统会自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数
  6. 默认构造函数:不传实参就可以调用的构造就叫默认构造。有三种类型:无参构造函数、全缺省构造函数、编译器自动生成的无参构造函数。这三个函数有且只能存在一个,不能同时存在。
  7. 编译器自动生成的默认构造,对于内置类型的初始化没有要求,取决于编译器。对于自定义类型,会调用该成员变量的默认构造函数初始化,若没有默认构造函数,则会报错。

用Date类的实现来展现一下构造函数:

//Date类的实现
class Date
{
public:
	//无参构造函数
	//Date()
	//{
	//	_year = 0;
	//	_month = 0;
	//	_day = 0;
	//}

	//有参构造函数
	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	//全缺省构造函数,这个和第一个无参构造只能存在一个
    //同时也要屏蔽第二个,否则会报错,因为有参时不知道调用谁
	Date(int year = 1900, 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;
};

int main()
{
	//不能写为Date d1();这样会有歧义,分不清是不是函数声明
	Date d1;
	d1.Print();
	Date d2(2024, 1, 1);
	d2.Print();

	return 0;
}}

三.析构函数

析构函数与构造函数功能相反,析构函数不是完成对象本身的销毁,而是完成对象中资源的清理释放工作。局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们手动清理。析构函数的功能就类似于Stack实现中的Destroy功能,对于Date日期类而言,没有资源需要释放(没有用malloc等开辟出来的资源空间)因此Date是不需要析构函数的。

析构函数的特点:

  1. 析构函数要在类名前加上~
  2. 无参数无返回值(跟构造类似)
  3. 一个类只能有一个析构函数,若未显式定义,则由编译器自动生成默认的析构函数
  4. 对象生命周期结束,系统自动调用析构函数
  5. 编译器自动生成的构造函数对内置类型不做处理,对自定义成员调用它对应的析构函数
  6. 类中若没有资源申请,可以不写析构函数,直接使用编译器默认生成的,但若有资源申请时,一定要亲自去写,否则会造成资源泄露
  7. 一个局部域的多个对象,C++规定后定义的先析构

 例如,对于有资源申请的Stack而言,它的析构函数就是这样:

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = 0;
		_top = 0;
	}

四.拷贝构造函数

拷贝构造是一个特殊的构造函数,它是构造函数的一个重载,它的第一个参数是自身类类型的引用,且任何额外的参数都有默认值。

class Date
{
public:
	//全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数,是自定义类对象的引用
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 7, 13);
	d1.Print();
 
    //Date d2 = d1也是等价,都是拷贝构造
	Date d2(d1);
	d2.Print();
	return 0;
}

C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,在自定义类型传值传参和传值返回的时候都会调用拷贝构造函数。由于上述特性,在写拷贝构造时,参数一定要写正确,是类类型的引用,不是引用而是传值传参:Date(const Date d)则会引发无穷递归

当使用Date d2(d1)时是调用拷贝构造函数,调用函数先要传参,这里传参d1对应的是Date类型,也就是传值传参,要调用拷贝构造函数,而调用拷贝构造函数又要进行传值传参,接着又进行调用拷贝构造,一直重复下去形成了无穷递归.......

因此写拷贝构造函数时一定不能写传值传参,那样会继续调用拷贝构造形成无穷递归,应该使用引用,若是Date(const Date& d)就不是传值传参,该引用传参不会调用拷贝构造,因此传参后就直接开始执行拷贝构造函数内部代码了,完成拷贝构造。

若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造会对内置类型进行值拷贝(浅拷贝),也就是一个字节一个字节的拷贝,对自定义类型成员变量则调用它自身的拷贝构造。这种浅拷贝方式适用于Date类这种没有资源申请的类类型,但不适用于Stack这种有资源申请的,因为浅拷贝只能将值一一拷贝,例如Stack中int* a的地址都拷贝过来,那么这两个栈就都指向同一个区域了,这显然不符合我们的要求,此时就需要我们自己手动写拷贝构造来完成深拷贝的任务了。

除了上述传值传参时调用的拷贝构造,在传值返回时也会调用拷贝构造,例如:

//传值返回
Stack func()
{
	Stack st;
	return st;
}

在这种情况下,由于函数结束,该局部域中创建的对象st生命周期就已经到了,对象就已经释放了,此时返回的就是st的拷贝构造,是一个临时对象。注意:此时就不能使用引用返回了,若写为:Stack& func(),返回的就是st这个已经释放的对象的别名,这就是野引用了,需要注意。 

五.运算符重载

当运算符被运用于类类型的对象时,C++允许我们通过运算符重载的形式定义新的含义。例如:平常经常使用+-等运算符适用于内置类型,却不适用于自定义的类类型,此时就需要使用运算符重载:运算符重载是具有特殊名字的函数,它的名字是由operator和后面要定义的运算符组成,也具有返回值和参数列表,例如:

class Date
{
public:
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
 
	int _year;
	int _month;
	int _day;
};

//全局定义的==运算符重载,判断日期是否相等
bool operator==(const Date& d1,const Date& d2)
{
	return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

其中operator==就是函数名,使用的时候可以直接调用,如:operator==(d1,d2),当然也可以直接写成:d1 == d2,编译器会自动转化为上述那种形式。

不过以上运算符重载是在全局定义的,因为类中的成员变量是公有的才能如此轻松访问,但一般成员变量都是定义为私有,那么此时解决方法就有两个:创建get成员变量的公有函数,或者更为简单地将运算符重载直接定义到类中,其实运算符重载函数就成为了类的成员函数,应有了隐含的this指针,能够访问类中私有成员,此时就只需要传参另一个类的引用即可,例如:

//在类中定义,隐含Date* const this
bool operator==(const Date& d)
{
    return _year == d._year && _month == d._month && _day == d._day;
}

运算符重载的特性有:

  1. 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数(顺序不能调换)
  2. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致
  3. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@
  4.  .*    ::    sizeof    ?:    .(点)   注意以上5个运算符不能重载
  5. 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
  6. 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义(日期-日期=间隔天数),但是重载operator+就没有意义

 六.赋值运算符重载

赋值运算符重载是一个默认成员函数,用于两个已经存在的对象直接的拷贝赋值,这里要跟拷贝构造区分,拷贝构造是构造函数,用于初始化一个新创建的对象。

赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成const当前类类型的引用,否则传值传参会有拷贝,效率降低。同时,应当具有类类型引用的返回值,引用是为了提高效率,有返回值是为了支持连续赋值的场景

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

    //拷贝构造函数
	Date(const Date& d)
	{
		cout << " Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

    //赋值运算符重载
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	
        //this是d1的地址,*this就是d1
		// d1 = d2表达式的返回对象应该为d1,也就是*this
		return *this;
	}

    void Print()
	{
		cout << _year <<"/"<< _month <<"/"<< _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

没有显式实现时,编译器会自动生成一个默认赋值运算符重载,它的行为跟默认拷贝构造函数类似,都是值拷贝(浅拷贝),也就是一个字节一个字节地拷贝,对于自定义类型也是调用其对应的赋值运算符重载。那么写不写也是同理,对于有资源调用的应当自己写,没有的就可以不写。

七.取地址运算符重载

在此先介绍一个const成员函数,使用const关键字修饰的成员函数,相当于用const修饰了this指针,例如在Date类中:原本的Date* const this变味了const Date* const this

//Date类中的Print成员函数
//隐含的Date* const this变为const Date* const this
void Print() const
{
	cout << _year <<"/"<< _month <<"/"<< _day << endl;
}

新增的const修饰在*号的左边,表明修饰的是this指向的值不能被修改,也就是类中成员都不能被修改。

若不加const修饰,在如下场景就会出错,d1是const Date类型,那么d1调用Print时,传过去d1的地址就是const Date*类型,但现在Print没有用const修饰,隐含的this就是Date*类型,这就是典型的权限放大,会报错。

void Print() 
{
	cout << _year <<"/"<< _month <<"/"<< _day << endl;
}

const Date d1(2024, 7, 14);
d1.Print();

因此为了应对这些情况,建议在不改变对象的前提下,尽量在函数后都加上const 

至于取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。

	Date* operator&()
	{
		return this;
		// return nullptr;
	}
	const Date* operator&()const
	{
		return this;
		//return nullptr;
	}
  • 15
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值