C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】

目录

1.默认(缺省)成员函数:析构函数

当带有static时,析构和构造函数的创建/销毁顺序是?

在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

如果在类的析构函数中调用delete this,会发生什么?

2.拷贝构造函数

2.1 内置类型和自定义类型

3.运算符重载

前置++和后置++重载

 3.3.友元

4.赋值运算符重载:=

5.const成员

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


1.默认(缺省)成员函数:析构函数

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作由编译器完成

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

析构函数特性

1. 析构函数名是在类名前加上字符 ~

2. 无参数无返回值类型

3.一个类只能有一个析构函数。若未定义,系统会自动生成默认的析构函数。析构函数不能重载

4.对象生命周期结束,C++自动调用析构函数

5.手动开辟的例如Stack中malloc,fopen等需要析构函数,Date日期类不需要析构函数

6.编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数

7.析构函数可以定义在类外

	~Stack()
	{
		free(...);
	}

析构函数顺序

先定义的先构造,后定义后构造; 先定义的后析构,后定义的先析构(栈和栈帧里面的对象都要符合后进先出)

class A
{
public:
	A(int a = 0)
	{
      _a = a;
		cout << "A(int a = 0)->" <<_a<< endl;
	}

	~A()
	{
		cout << "~A()->" <<_a<

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第1张图片

当带有static时,析构和构造函数的创建/销毁顺序是?

两个局部静态对象,一个全局对象

class A
{
public:
	A(int a = 0)
	{
      _a = a;
		cout << "A(int a = 0)->" <<_a<< endl;
	}

	~A()
	{
		cout << "~A()->" <<_a<

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第2张图片

全局变量最先被初始化(main函数之前初始化,全局和静态都在静态区),局部静态特点是第一次运行后初始化

析构是aa2和aa1中最先析构,原因在于剩余三个生命周期在程序结束后才销毁,main函数栈帧结束清理在栈帧中的aa2和aa1;main函数结束,再调用全局和静态(符合先定义后析构)

 如果调用两次f()函数,结果又是如何?

 C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第3张图片

静态变量在第一次执行后初始化,第一次函数调用结束,aa4和aa5不会销毁

在成员函数中调用delete this会出现什么问题?对象还可以使用吗?

在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放 在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。 当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

为什么是不可预期的问题?

delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生野指针之类的令崩溃的问题。这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。


2.拷贝构造函数

有时候我们需要对一个对象进行拷贝,就会调用拷贝构造函数

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

int main()
{
	Date d1(2022, 7, 31);
	Date d2(d1);//拷贝构造两个写法
    Date d3 = d1;
	return 0;
}

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

1. 拷贝构造函数是构造函数的一个重载形式。

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

3.拷贝构造函数:函数名和类名相同,没有返回值;同类型对象构造

 错误的写法

	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
非法的复制构造函数

传值传参:一份临时拷贝,开辟新空间

传引用传参:别名,原空间

d1实例化调用的是构造函数;用d1初始化d,对象实例化要调用拷贝构造函数,(同类型对象拷贝初始化)

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第4张图片

如果不是引用调用,同类型调用拷贝构造要传参,传参又是一个拷贝构造,层层传值引发对象的拷贝的递归调用

正确的写法

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

同时建议拷贝构造函数加const (权限缩小),防止以下情况发生(逻辑写反,原数据被修改,并不是修改原数据而是进行拷贝)

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


2.1 内置类型和自定义类型

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序(memcpy)完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调 用其拷贝构造函数完成拷贝的(日期类不需要写拷贝构造,默认生成够用)。

以下情况默认生成的拷贝构造函数无法使用,必须自己实现:

深浅拷贝问题

拷贝构造st2(st1)程序崩溃,原因在于两个指针指向了同一块malloc开辟的空间,结束时free释放了两次同一片空间,原因就在于浅拷贝造成的(指针地址拷贝)

typedef int DataType;
class Stack
{
public:
	Stack(int capacity=4)
	{ 
		cout << "Stack(int capacity = 4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_size = 0;
		_capacity = capacity;
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

 解决方法:深拷贝,具体在string模拟实现中

总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数一定要写,否则就是浅拷贝


3.运算符重载

 一个类可以重载哪些运算符取,决于运算符对类有无意义

内置类型可以使用运算符运算,但当自定义类型,例如日期类想完成日期-日期、比较日期、日期加天数等操作,可以使用运算符重载

//日期类构造函数需要写;析构和拷贝构造默认生成够用
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		if (!CheckDate())
		{
			print();
			cout << "日期非法" << endl;
		}
	} 
	bool CheckDate()
	{
		if (_year >= 1 
                &&_month >0 &&  _month <13 
                    && _day >0 && _day <=GetMonthDay(_year,_month))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数类型:返回值类型 operator需要重载的运算符符号(参数列表)

注意:返回值类型由运算符决定;参数列表由操作数决定(d1==d2,两个参数,并规定第一个参数为左操作数,第二个参数为右操作数)

比较运算符重载

技巧:任何一个类,写比较运算符重载,只需要写大于和等于或者小于和等于,剩下的比较运算符重载复用即可

在类外,成员变量访问受到限制,要么使用友元,要么取消private,但是都会破坏封装

bool operator==(const Date& d1,const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

bool operator!=(const Date& d)
{
	return !(*this == d);
}

bool operator>(const Date& d)
{
	if (_year > d._year)
	{
		return true;
	}
	else if(_year == d._year && _month > d._month)
	{
		return true;
	}
	else if (_year == d._year && _month == d._month && _day > d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

bool operator>=(const Date& d)
{
	return (*this > d) || (*this == d);
}

bool operator<(const Date& d)
{
	return !(*this >= d);
}

bool operator<=(const Date& d)
{
	return !(*this > d);
}

int main()
{
	Date d1(2022,5,20);
	Date d2(2022,8,1);
	cout<<(d1 == d2)<

在类中,提示运算符参数太多,原因在于:this指针

	operator==(d1, d2);//全局时,编译器其实是处理成这样
	d1.operator==(&d1, d2);//类中时,编译器处理成这样
    d1.operator==(d2);//类中实际情况
    //但是this指针不能显示传参,不能显示声明参数,但是类中可以使用

所以最好写成:成员函数

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

可以在类中只声明,定义放到另一个文件中(防止太多内联造成代码膨胀,除非频繁调用)


+ 和 +=(天数)

天满了进月,月满进年

复用的情况下,先写+=更好(+=没有看对象构造),同时类不关心上下顺序(作为一个整体,上下都搜索)

int GetMonthDay(int year,int month)//涉及闰年
{
	static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//频繁调用用static
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		return 29;
	}
	else
	{
		return days[month];//拿到每个月天数
	}
}

Date& operator+=(int day)
{
    	if (day < 0)//+= -100
	{
		return *this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))//如果大于这个月天数非法,进位
	{
		_day -= GetMonthDay(_year, _month);//
		++_month;
		if (_month == 13)
		{
			_month = 1;
			++_year;
		}
	}
	return *this;//this指向当前对象的指针,*this就是当前对象
}

Date operator+(int day)
{
    Date ret(*this);
    ret += day;
    return ret;
}

//不复用的+
Date operator+(int day)
{
	Date ret = (*this);
	ret._day += day;
	while (ret._day > GetMonthDay(ret._year, ret._month))//如果大于这个月天数非法,进位
	{
		ret._day -= GetMonthDay(ret._year, ret._month);//
		++ret._month;
		if (ret._month == 13)
		{
			ret._month = 1;
			++ret._year;
		}
	}
	return ret;
}

前置++和后置++重载

前置++返回值为++后的值,后置++返回值为++前的值

运算符重载为了区分前后置++,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

Date operator++(int)//后置++
{
	Date ret = *this;
	*this += 1;
	return ret;
}

Date& operator++()
{
	*this += 1;
	return *this;
}

前置--和后置--重载

Date& operator--()//日期-天数.前置--
{
	return (*this -= 1);
}

Date operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

日期-天数;日期-=天数;

Date operator-(int day)
{
	Date ret = *this;
	ret -= day;
	return ret;
}


Date& operator-=(int day)
{
    if (day < 0)//-= -100
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			_year -= 1;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

日期-日期

不复用思路:算出当前年月离当年1.1号差多少天,再算出年之间差距(闰年366天)

复用思路:累加

int operator-(const Date& d)//日期-日期
{
	int flag = 1;
	Date max = *this;//默认第一个大第二个小
	Date min = d;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	 }
	 //小的不断++,加到跟大的相等为止
	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n*flag;
}

<<流插入运算符重载

cout能自动识别类型在于cout写了运算符重载<<,依靠函数重载来实现自动识别类型

当我们想写以下函数时确保错,由于cout是ostream对象的成员,处理内置类型,却不处理自定义类型,我们可以重载<<来实现日期类的流插入<<

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第5张图片

	cout << (d1 + 100);
	cout << d1;

原cout<<中一个是隐含的cout,一个是int/float等;在Date中一个是隐藏的Date,另一个传cout即可

错误的返回值写法:cout << d1;

(报错:没有找到接收Date类型的右操作数的运算符)

当使用原生的d1.operator<<(cout)却可以调的到,原因在于运算符有多个操作符,而第一个操作数为d1,第二个操作数为cout,写法其实是d1 << cout

ostream& operator<<(ostream& out,const Date& d)
{
	//支持年月日输出
	out << d._year << "年" << d._month << "月" << d._day <<"日" << endl;
	return out;
}

解决方法:不能是成员函数(日期类对象抢占了第一个操作数),写在类外(使用友元)

返回值使用ostream做返回值的对象,用来支持连续cout等操作

>>流提取运算符重载

为什么scanf要取地址而>>不用,原因在于没有引用,cin转换成调用operator流提取,把cin和d1引用传入(默认输入多个值以空格或者换行去间隔)

istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	if (!d.CheckDate())
		cout << "日期非法" << endl;
	return in;
}

friend ostream& operator<<(ostream& out, const Date& d);

运算符重载总结:

 .*(matlab计算矩阵型号匹配)    ::(域作用限定符)    sizeof   ?:    .  注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

2.内置类型的运算符,其含义不能改变

判断日期是星期几

	Date d1(1840,11,1);
    cin >> d1;
	Date start(1, 1, 1);
	int n = d1 - start; 
	int weekDay = 5;//默认从0开始,1.1.1星期1,0相当于周天
	weekDay += n;
	cout << "周" << weekDay % 7 + 1  << endl;

 3.3.友元

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

缺点:友元破坏了封装

friend ostream& operator<<(ostream& out, const Date& d);

友元函数不能用const修饰

友元类:

1.友元关系是单向的,不具有交换性

2.友元关系不能传递(A=B B=C A!=C)


4.赋值运算符重载:=

日期类初始化时d2(d1)为拷贝构造,当两个类已经定义好时,把值赋值给另一个类,就叫赋值运算符重载

参数类型:const T&,传递引用可以提高传参效率

日期类的赋值运算符重载:返回值是Date是为了支持连续赋值,&可以让赋值一次拷贝构造都没发生(并不是静态和全局才能用引用返回,只要对象除了作用域还在即可);加引用减少拷贝构造,同时加const缩小权限;if判断是防止自己给自己赋值的无意义行为(地址比较)

总结:能用就用引用传参和引用返回

	Date&  operator=(const Date& d)//d1 = d3;
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

赋值运算符只能重载成类的成员函数不能重载成全局函数(写在类外,类中生成默认赋值重载,造成重载冲突)

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值

类似Stack需要自己写赋值运算符重载来避免浅拷贝(复制拷贝一样的问题)

其特性和复制拷贝一样


5.const成员

在对象加了const,调用print会遇到问题,原因在于隐藏的this指针权限放大,从const Date转换为Date*const this(const修饰的是this指针本身不能被改变,指针的内容可以改变)

&d1 是 Date*

&d2 是const Date*(*之前意味着指向的内容不能被修改,传给Date*是权限放大)

d1 < d2编译通过  d2 < d1编译报错,原因和上面情况一样

只有指针和引用涉及权限缩小放大问题

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第6张图片

 解决方法:让this指针变成const修饰即可,由于this指针隐含不能轻易修改,需要加在后面

变成const Date* const this

void print() const
{
	cout << _year << "/" << _month << "/" << _day<


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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容

const对象取地址调用const A*,A对象调用A*

普通对象和const对象要分开处理,就需要写两个;如果不需要例如只需要打印,写一个即可

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

	const A* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
 {
	A a;
	const A b;
	&a;
	&b;
	return 0;
}

C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】_第7张图片

特殊场景使用:不想让别人取到这个类型对象的地址,返回nullptr即可;或者转换为私有,无法取地址

你可能感兴趣的:(C++,c++,开发语言)