【初识C++】万字简单说懂 类与对象的默认成员函数(中)

1、前言


本章介绍类与对象中的默认成员函数。当一个类中没有任何成员时,我们称这个类为空类,而在空类中并不是真正的空,编译器会自动生成以下几个默认成员函数。

默认成员函数:用户不显示实现,编译器自动生成的成员函数(我们不写编译器自己生成,我们写编译器不生成)。

默认成员函数有:
1、【初始化和清理】:构造函数和析构函数。
2、【拷贝复制】:拷贝构造和赋值运算符重载函数
3、【取地址重载】:普通对象和const对象取地址

2、构造函数


  在C语言实现一些数据结构的时候,我们可能老是忘记初始化数据或者销毁数据,或者是每次对初始化和销毁都需要留心感到很麻烦。
  实现C++的大佬也可能有这样的顾虑,所以就设计了默认成员函数,使得在创建对象的同时编译器就自动调用了默认成员函数。

2.1 构造函数的特性


功能:构造函数在对象创建的同时,编译器通过调用构造函数完成对对象的初始化

特征:

  1. 构造函数的函数名和类名要一样
  2. 构造函数没有返回值
  3. 对象实例化后,编译器自动调用构造函数
  4. 构造函数可以重载

看代码:

class Test
{
public:
	//无参构造函数
	Test()
	{
		_e1 = 1;
		_e2 = 2;
		_e3 = 3;
	}
	//带参构造函数
	Test(int e1, int e2, int e3)
	{
		_e1 = e1;
		_e2 = e2;
		_e3 = e3;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};

int main()
{
	Test t1; // 调用无参构造函数
	Test t2(1, 2, 3); // 调用带参构造函数
	//Test t3(); //错误写法 会让编译器误以为是一个函数声明
	return 0;
}

由于构造函数是支持重载的,在上面代码中有一个无参构造函数和一个有参构造函数,我们可以给上面两个函数简化一下,毕竟我们学过缺省函数嘛。

这样写更能适应多个场景,同时也支持两种调用方式

class Test
{
public:
	Test(int e1 = 1, int e2 = 2, int e3 = 3)
	{
		_e1 = e1;
		_e2 = e2;
		_e3 = e3;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};

int main()
{
	Test t1; // 默认1,2,3
	Test t2(2,3,4); //2,3,4
	return 0;
}

2.2 编译器对构造函数的处理


当然对于构造函数,如果我们写了,编译器就不会生成了,下面看看如果我们不写呢?

先看一段代码:

class Test
{
public:
	void func1()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};

int main()
{
	Test t1;
	t1.func1();
	return 0;
}

在这里插入图片描述

再看以下代码

class Temp
{
public:
	Temp()
	{
		cout << "Time()" << endl;
		_t1 = 0;
		_t2 = 0;
		_t3 = 0;
	}
private:
	int _t1;
	int _t2;
	int _t3;
};


class Test
{
public:
	void func()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}
private:
	// 基本类型(内置类型)
	int _e1;
	int _e2;
	int _e3;
	// 自定义类型
	Temp _t;
};
int main()
{
	Test d;
	d.func();
	return 0;
}

在这里插入图片描述

结论是:

  当我们没有在类中写构造函数的时候,编译器会自动生成构造函数,自动生成的构造函数对内置类型不做处理,但会对自定义类型做处理,调用自定义类型的构造函数。

如果我们想要对内置类型做处理怎么办?
在C++11中打了个补丁,可以对成员变量声明时给个缺省值(不占物理空间,只是标识)。

class Temp
{
public:
	Temp()
	{
		cout << "Time()" << endl;
		_t1 = 0;
		_t2 = 0;
		_t3 = 0;
	}
private:
	int _t1;
	int _t2;
	int _t3;
};


class Test
{
public:
	void func()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}
private:
	// 基本类型(内置类型)
	int _e1 = 1;
	int _e2 = 2;
	int _e3 = 3;
	// 自定义类型
	Temp _t;
};
int main()
{
	Test d;
	d.func();
	return 0;
}

在这里插入图片描述

应用举例:
用栈实现队列
队列的构造函数就不用写了。

class Stack
{
public:
	Stack()
	{
		_a = NULL;
		_capacity = _top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

class Queue
{
public:
	void push(size_t x)
	{}
private:
	Stack _PushSt;
	Stack _PopSt;
};

int main()
{
	Queue q1;
	return 0;
}

总结:

如果默认生成的能满足你的需求,那就不用写。

2.3 默认构造函数


让我们再跳回这个代码,如果只留带参构造函数,这时编译器不会自己生成构造函数了,如果我们调用的时候不传参,这个时候就会出现问题了。

class Test
{
public:
	//带参构造函数
	Test(int e1, int e2, int e3)
	{
		_e1 = e1;
		_e2 = e2;
		_e3 = e3;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};

int main()
{
	Test t1(1, 2, 3); // 正常运行
	//Test t2; //编译错误
	return 0;
}

在这里插入图片描述
这里说的默认构造函数是什么?
默认构造函数分为这几种:

  1. 无参的构造函数。
  2. 全缺省的构造函数。
  3. 编译器生成的构造函数。

所以创建对象没有默认传参的话,一定得有默认构造函数。

3、析构函数


3.1 析构函数的特征

前面学习的构造函数是用来初始化对象的,而析构函数是用来清除对象所占资源(不是销毁对象)的函数。

特征:

  1. 析构函数名是在类名前加个~
  2. 无参数无返回值,所以不能重载
  3. 一个类只能有一个析构函数,如果写了编译器就不生成,不然编译器会生成
  4. 析构函数在对象生命周期结束时自动调用

看代码了解特性:

class Temp
{
public:
	Temp()
	{
		cout << "Time()" << endl;
		_t1 = 0;
		_t2 = 0;
		_t3 = 0;
	}

	~Temp()
	{
		cout << "~Temp()" << endl;
	}
private:
	int _t1;
	int _t2;
	int _t3;
};

class Test
{
public:
	void func()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}

	~Test()
	{
		cout << "~Test()" << endl;
	}
private:
	// 基本类型(内置类型)
	int _e1 = 1;
	int _e2 = 2;
	int _e3 = 3;
	// 自定义类型
	Temp _t;
};
int main()
{
	Test d;
	d.func();
	return 0;
}

在这里插入图片描述

  对象d销毁的时候,清除Test类的资源,所以先调用~Test(),在清除到Temp类型数据的时候再进入Temp类调用 ~Temp(),最后完成整个对象d的资源清除。

但实际上对于栈区里的数据,编译器在程序结束也就自动销毁了,所以这里的析构函数可写可不写。

3.2 编译器对析构函数的处理

在上面我们了解了通过自己写的析构函数程序是怎么运行的。
那么接下来让我们看看编译器生成的析构函数。

下面是一个用栈实现队列的部分:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc fail");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{

		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

class Queue
{
public:
	void Push(DataType x)
	{
		_Pushst.Push(x);
	}
private:
	Stack _Pushst;
	Stack _Popst;
};


int main()
{
	Queue q;
	q.Push(1);
	q.Push(2);
	return 0;
}

初始化过程:

  在创建对象q的时候,调用编译器生成的类Queue构造函数,在这个构造函数中再调用类Stack的构造函数最后完成了q的初始化。

释放资源过程:

  在对象q的生命周期结束时,调用编译器生成的类Queue析构函数,在这个析构函数中再调用类Stack的析构函数完成了q的资源释放。

总结:
 当只有栈中的数据需要消除时,析构函数可写可不写,当有资源(如动态内存、关闭文件)需要释放的时候编译器的生成只是一个"连接",最终总有一个类中是需要自己写析构函数的。

有资源释放的需求要写,默认可以处理的不写。

4、拷贝构造函数


拷贝构造函数是一个特殊的构造函数,用已有的对象创建新的对象时,编译器自动调用对新的对象进行初始化。

4.1 拷贝构造函数的特征


拷贝构造函数特征:

  1. 函数名和类名一样,是构造函数的重载形式
  2. 没有返回值,参数只有一个且必须是类类型已有的对象的引用
  3. 用类类型对象创建新对象时,编译器自动调用。

看代码看特征:

class Test
{
public:
	Test(int e1 = 1, int e2 = 2, int e3 = 3)
	{
		_e1 = e1;
		_e2 = e2;
		_e3 = e3;
	}

	//其实注释了这个函数也是一样的结果
	//加const 防止d的值被更改
	Test(const Test& d)
	{
		cout << "Test(const Test& d)" << endl;
		_e1 = d._e1;
		_e2 = d._e2;
		_e3 = d._e3;
	}

	void func()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};
int main()
{
	Test d1(2, 3, 4);
	Test d2(d1);
	d2.func();
	return 0;
}

在这里插入图片描述

Test d1(2, 3, 4); 构造 初始化
Test d2(d1); 构造 给d2初始化

4.2 拷贝构造函数的引用参数


为什么拷贝构造函数的参数必须用引用?

先分析一下这段代码

class Test
{
public:
	Test(int e1 = 1, int e2 = 2, int e3 = 3)
	{
		cout << "Test()" << endl;
	}

	//其实注释了这个函数也是一样的结果
	//加const 防止d的值被更改
	Test(const Test& d)
	{
		cout << "Test(const Test& d)" << endl;
	}

private:
	int _e1;
	int _e2;
	int _e3;
};

void func1(const Test d)
{
	cout << "func1(const Test d)" << endl;
}

void func2(const Test& d)
{
	cout << "func1(const Test& d)" << endl;
}

int main()
{
	Test d1;
	func1(d1);
	func2(d1);
	return 0;
}

运行结果:
在这里插入图片描述
实际步骤:

  1. 创建d1对象,调用构造函数。
  2. 调用func1传值,传值先对实参d1拷贝,第一次调用拷贝构造函数后将形参传给func1,打印。
  3. 引用传参不需要拷贝,所以直接打印。

结论:
对象传值,需要调用拷贝构造,所以如果拷贝构造函数传值,那么就会反复调用自己,最后造成死循环。

4.3 编译器对拷贝构造函数的处理


如果未显示定义拷贝构造函数,编译器会生成默认的拷贝构造。

对于内置类型

还是这段代码

class Test
{
public:
	Test(int e1 = 1, int e2 = 2, int e3 = 3)
	{
		_e1 = e1;
		_e2 = e2;
		_e3 = e3;
	}

	//其实注释了这个函数也是一样的结果
	//加const 防止d的值被更改
	//Test(const Test& d)
	//{
		//cout << "Test(const Test& d)" << endl;
		//_e1 = d._e1;
		//_e2 = d._e2;
		//_e3 = d._e3;
	//}

	void func()
	{
		cout << _e1 << endl;
		cout << _e2 << endl;
		cout << _e3 << endl;
	}
private:
	int _e1;
	int _e2;
	int _e3;
};
int main()
{
	Test d1(2, 3, 4);
	Test d2(d1);
	d2.func();
	return 0;
}

对于一些内置类型就算没有写拷贝构造函数,结果还是一样的,因为编译器生成的拷贝构造函数完成了值拷贝。

但有一些内置类型,如果靠编译器生成的拷贝构造就会出问题。

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc fail");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{

		_array[_size] = data;
		_size++;
	}


	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack st;
	st.Push(1);
	st.Push(2);
	Stack st1(st);
	return 0;
}

在这里插入图片描述

程序运行结果:程序崩溃
默认拷贝构造是浅拷贝,属于值拷贝,两个对象的指针都指向一个空间。
当浅拷贝两个动态空间的指针值时,就会发生错误,因为最后会对一个空间析构两次,程序也就崩溃了。

深拷贝通过将建立新的空间,拷贝相同的数据放入新空间,就能实现有效拷贝。

//深拷贝的拷贝构造函数写法
	Stack(const Stack& st)
	{
		_array = (int*)malloc(sizeof(int) * st._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, st._array, sizeof(int) * st._size);
		_size = st._size;
		_capacity = st._capacity;
	}

结论:
需要写析构函数的类,需要拷贝构造函数。
不需要写析构函数的类,默认拷贝构造函数也够用。

对于自定义类型:

#include <iostream>
using namespace std;

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		cout << "stack构造" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc fail");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{

		_array[_size] = data;
		_size++;
	}
	Stack(const Stack& st)
	{
		cout << "stack拷贝构造" << endl;
		_array = (int*)malloc(sizeof(int) * st._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, st._array, sizeof(int) * st._size);
		_size = st._size;
		_capacity = st._capacity;
	}


	~Stack()
	{
		cout << "stack析构" << endl;
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};


class Queue
{
public:
	void Push(DataType x)
	{
		_Pushst.Push(x);
	}
private:
	Stack _Pushst;
	Stack _Popst;
};


int main()
{
	Queue q;
	q.Push(1);
	q.Push(2);
	Queue q1(q);
	return 0;
}

在这里插入图片描述

Queue类可以不写拷贝构造函数,对于自定义类型编译器自动生成的拷贝构造函数会自动调用Stack类的拷贝构造函数

5、运算符重载和赋值重载


5.1 运算符重载

为什么要对运算符进行重载?

  如果有一个日期的对象,我们需要计算距今100天后的具体日期,简单内置的+肯定是不能完成这个工作的,对于类似情况,就需要我们对运算符进行重载。

5.1.1 运算符重载的使用

运算符重载是一个特殊的函数
函数名:operator后面接需要重载的运算符
原型:返回值 operator操作符(参数)

如:

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

运算符重载很注重代码的可读性,所以调用的形态很易懂。
如:

class Date
{
public:	
	//构造
	Date(int year = 2022, int month = 10, int day = 9)
	{
		_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()
	{
		cout << "~Date()" << endl;
	}
	
	//打印
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	
	//==重载函数 写类里的方式
	//bool operator==(const Date& d)
	//{
	//	return _year == d._year
	//		&& _month == d._month
	//		&& _day == d._day;
	//}
	
	//==重载函数声明
	bool operator==(const Date& d);

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

//==重载函数 写类外的方式
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

int main()
{
	Date d1(2022, 10, 8);
	d1.Print();

	Date d2(d1);
	//写法和平时一样 但实际d1 == d2 是 d1.operator==(d2);
	cout << (d1 == d2) << endl;

	return 0;
}

在这里插入图片描述

让我们通过==重载函数的实现看一些东西

bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}
  1. 为什么参数只有一个d,不是(const Date& d1, const Date& d2),这是因为在参数中有一个隐藏的this指针。
  2. &的使用相较传值可以避免d的拷贝构造,从而提高效率,相较传指针可读性更强。
  3. const避免d的值被改变,并且使得函数调用适合多种情况(可以传Date类型,也可以传const Date类型),因为权限可以缩小不能扩大。

注意:

  1. 不能通过连接其它符号来创建新的操作符,比如operator@。
  2. 重载操作符必须有一个类类型参数,不然就失去了重载的意义。
  3. 运算符重载后不能改变运算符本该有的含义。
  4. sizeof 、? : 、:: 、. 、*. 这5个操作符不能进行重载。

5.1.2 实现多种运算符重载

在实现一定的操作符重载函数后,对于实现一些其它的操作符重载函数我们可以复用

比如:

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

bool Date::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;
	}

	return false;
}

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

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

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

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

以下函数都定义在类外,所以需要添加类作用域限定符。

int Date::GetMonthDay(int year, int month)
{
	int MonthArr[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (year % 4 == 0 && year % 400 != 0)
	{
		MonthArr[2] = 29;
	}

	return MonthArr[month];
}

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

	return *this;
}

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


前置++和后置++的实现与区别
前置++,直接返回+1后的结果。
后置++,拷贝一个临时变量,对象+1,再返回临时变量。

//后置++
//C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
//自动传递
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}
//前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

日期相减的重载

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;

	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (max != min)
	{
		++min;
		++n;
	}

	return n*flag;
}

5.2 赋值运算符重载


当我们想完成一个d1 = d2 的赋值重载

	Date d1(2022, 10, 8);
	d1.Print();

	Date d2;
	d2 = d1;

赋值运算符重载也有自己的格式

  • 参数类型:const 类类型& 提高效率
  • 返回值类型:类类型& 提高效率,并且使得赋值能够连续
  • 考虑自己给自己赋值的情况
  • 返回*this使得符合赋值连续
5.2.1 赋值运算符的赋值连续

下面我们通过代码理解它的格式
这里重点是理解连续赋值是怎么实现的

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 2022, int month = 10, int day = 9)
	{
		_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()
	{
		cout << "~Date()" << endl;
	}

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

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

		return *this;
	}



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

int main()
{
	Date d1(2022, 10, 8);
	d1.Print();

	Date d2;
	Date d3(2022, 10, 9);
	d2 = d3 = d1; //先右到左 (d3 = d1)的返回值和d2比 等价 d2 = (d3 = d1);

	return 0;
}

因为需要返回值和下一个对象进行比较,所以需要返回*this。

为什么不能返回d呢?

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

		return *this;
	}

依然是权限的问题,d返回类型变成了Date&,权限扩大,如果要返回d返回类型需要改为const Date&,但这可能不是我们需要得到的类型。

5.2.2 赋值和拷贝的区别

赋值需要在对象都存在时才能调用。

Date d1(2022, 10, 8);
Date d2;
d2 = d1;

拷贝是用一个对象创建一个新的对象

Date d1(2022, 10, 8);
Date d2(d1);

那么这样写呢?

Date d1(2022, 10, 8);
Date d2=d1;

本质是创建一个新的对象,再对其初始化,所以是拷贝。

5.2.3 赋值运算符重载不能写在类外

首先,如果我们在类中不自己实现一个赋值运算符重载,在调用时,编译器会自动生成并且以字节序进行值拷贝,所以如果写在类外就会就会与编译器生成的冲突。

比如这种情况我们不写赋值重载

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 2022, int month = 10, int day = 9)
	{
		_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;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 10, 8);
	Date d2;
	Date d3(2022, 10, 9);
	d2 = d3 = d1; //先右到左 (d3 = d1)的返回值和d2比 等价 d2 = (d3 = d1);

	return 0;
}

就算没有写赋值重载,编译器生成的也能实现。

但对于这种情况,编译器自动生成的就会出问题

class stack
{
public:
	//构造
	stack(int capacity = 4)
	{
		cout << "stack(int capacity = 4)" << endl;
		_a = (int*)malloc(sizeof(int*) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_capacity = capacity;
		_top = 0;
	}
	
	//拷贝构造
	stack(const stack& st)
	{
		cout << "stack(const stack& st)" << endl;
		_a = (int*)malloc(sizeof(int*) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int) * st._top);
		_capacity = st._capacity;
		_top = st._top;
	}
	//入栈
	void Push(int x)
	{
		//扩容
		_a[_top++] = x;

	}
	
	//析构
	~stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	stack st1(100);
	st1.Push(1);
	st1.Push(2);

	stack st2(st1);
	st2.Push(10);
	st2.Push(20);

	stack st3(20);
	st3.Push(100);
	st3.Push(200);

	st1 = st2 = st3;
}

结果就是程序崩溃,对一个空间多次析构,还是值拷贝的问题,所以我们需要自己实现赋值重载。

大致和拷贝构造函数一样,值得注意的一个细节就是考虑自己赋值给自己的情况。

	stack& operator=(const stack& st)
	{
		//自己等于自己
		if (this != &st)
		{
			free(_a);
			_a = (int*)malloc(sizeof(int*) * st._capacity);
			if (_a == nullptr)
			{
				perror("malloc fail");
				exit(-1);
			}
			memcpy(_a, st._a, sizeof(int) * st._top);
			_capacity = st._capacity;
			_top = st._top;
		}
		
		return *this;

	}

6、const成员和取地址重载


被const修饰的对象,表示不能对该对象进行任何修改,只能拥有可读的权限,所以const对象的调用是我们需要讨论的问题。

6.1 const成员


当我们用一个const对象调用一个函数时,由于编译器生成的this指针默认类型是 类* 类型,当我们传const 类* 类型,就会发生权限扩大,这是禁止的。

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 2022, int month = 10, int day = 9, int tmp = 4)
	{
		_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()
	{
		cout << "~Date()" << endl;
	}

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

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

int main()
{
	const Date d1;
	d1.Print();
	return 0;
}

在这里插入图片描述
而解决这个问题只需要在Print函数右边加上const,编译器会自动将this指针调整成const Date* 类型。

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

因为函数加const后,带上const的对象和不加const的对象都能正常调用,所以如果函数没有修改对象的操作,那么最好是加上const。

6.2 取地址重载


还是实现日期类的取地址重载,注意对加const的对象要有特别处理。

	//一般不用自己定义 除非要让别人得到特定内容
	// 如:
	// 要求这个类的对象不让取地址
	Date* operator&()
	{
		/*return nullptr;*/
		return this;
	}

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

但其实对于取地址重载编译器生成的都能用,我们一般不用写,除非是想让调用的人看不到,这时候可以在重载中返回nullptr。

本章完~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值