C++类和对象(中)

C++教学总目录点此

注:本篇内容很多且复杂,建议动手实验+理解。

1、类的6个默认成员函数

在类和对象上,我们计算过空类的大小,为1byte——为了占位,表示对象存在。
但是空类中真的什么都没有吗?
实际上,任何类在什么都不写的情况下,编译器会自动生成以下六个默认成员函数。
在这里插入图片描述

2、构造函数

在C语言阶段,我们如果定义一个栈,必须手动初始化和销毁,否则程序可能崩溃和内存泄漏。

#include <iostream>
using namespace std;
int main()
{
	Stack st;
	st.Init();    // 必须手动初始化

	// ...

	st.Destroy(); // 必须手动销毁,否则内存泄漏
	return 0;
}

实际上这两步还是很烦人的,而且如果忘记写了就会出问题。
在C++中,有了构造函数和析构函数,我们就不需要手动调用了。


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

构造函数的特性:
1、无返回值。
2、名字与类名相同。
3、构造函数可以重载。
4、对象实例化时编译器自动调用对应的构造函数。

#include <iostream>
using namespace std;

class Date
{
public:
	// 构造函数
	Date()
	{
		cout << "Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}

	// 构造函数可以重载
	// 这里可以给成全缺省,这样更好用,但是如果是无参调用,就会存在二义性。
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1; // 实例化时自动调用构造函数
	Date d2(); // 无法很好的区分是函数调用还是函数声明
	Date d3(2024, 4, 18); // 调用带参的构造函数;
	return 0;
}

5、如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认构造函数。如果我们自己写了,编译器就不会生成。
6、默认生成的构造函数对于内置类型不做处理(a.有些编译器可能会处理。b.C++11打补丁,声明成员变量的时候可以给缺省值,这个我们到C++11再谈。),对于自定义类型会去调用它的默认构造函数,如果自定义类型没有默认构造函数就会报错。
7、默认构造函数:编译器默认生成的、无参的构造函数、全缺省的构造函数。——也就是不用参数就可以调用的。并且默认构造函数只能有一个。

#include <iostream>
using namespace std;

class Test
{
public:
	// 这是无参的默认构造函数
	Test()
	{
		cout << "Test()" << endl;
	}
private:
	int _a;
};

class Date
{
public:
	// 这里不写构造函数,编译器就会默认生成。
private:
	int _year;
	int _month;
	int _day;
	Test _t;
};
int main()
{
	Date d1;
	return 0;
}

在这里插入图片描述

在上面这个例子中,我们不写Date类的默认构造函数,所以编译器会生成无参的默认构造函数。
编译器生成的无参默认构造函数对于内置类型_year、_month、_day不做处理,对于自定义类型Test会去调用它的默认构造函数,所以输出了Test()。
C/C++中的内置类型:int/char/double/指针/内置类型数组 等等
C/C++中的自定义类型:struct/class定义的类型
对于上面的Test类,如果没有默认构造函数,那么就会报错,因为Date类编译器生成的默认构造要去调用Test类的默认构造,所以Test类必须要有默认构造(不用参数就可以调用的:编译器生成的、无参的、全缺省的)。


#include <iostream>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "Test()" << endl;
	}
private:
	int _a;
};

class Date
{
public:
	// 
	Date(int year = 1, int month = 1, int day = 1)
	{
		cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	Test _t;
};
int main()
{
	Date d1;
	return 0;
}

在这里插入图片描述

再来看这段代码:我们写了Date类的构造函数,所以编译器不会再生成Date类的构造函数了。并且在Date类的构造函数中也会去调用Test类的默认构造函数(所以输出了Test())。如果Test类没有默认构造函数,那么编译器也会报错。因为无论是编译器默认生成的,还是你自己写的构造函数,对于自定义类型都会去调用它的默认构造函数,没有就会报错,只不过编译器默认生成的对于内置类型不做处理。

并且:编译器会先调用自定义类型的默认构造函数,然后再执行Date类构造函数体的内容。这实际上是在初始化列表先调用了自定义类型的构造函数,这个我们类和对象下会讲。


总结:编译器生成的默认构造函数对于内置类型不做处理,对于自定义类型会去调用它的默认构造函数。我们写了构造函数编译器就不会生成,对于自定义类型也会去调用它的默认构造函数。并且先调用自定义类型的构造函数,然后才会初始化内置类型的成员变量,因为调用自定义类型的构造函数是在初始化列表进行的。
一般情况下,我们需要自己写默认构造函数。除非:1、内置类型成员变量都给了缺省值,且初始化符合我们的要求。2、全都是自定义类型的构造,并且这些自定义类型都有默认构造函数。


3、析构函数

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

析构函数的特性:
1、析构函数名是:类名并且在前面加上~。
2、无参数无返回值。
3、一个类只能有一个析构函数,析构函数不能重载。
4、对象声明周期结束时由编译器自动调用析构函数。

#include <iostream>
using namespace std;

class Date
{
public:
	// 构造函数
	Date()
	{
		cout << "Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}

	// 析构函数
	// 由于Date类都是内置类型,没有需要清理的资源,所以析构函数什么都不做
	// 这里打印是为了演示现象——在Date类对象声明周期结束时,编译器自动调用析构。
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

在这里插入图片描述


5、如果类中没有显示定义析构函数,编译器会自动生成一个析构函数。如果我们自己写了,编译器就不会生成。
6、编译器生成的析构函数,对于内置类型不做处理,对于自定义类型会去调用它的析构函数。
7、如果类中没有申请资源,那么就不需要显示定义析构函数,用编译器默认生成的就可以。如果类中有申请资源,那么一定要写析构函数将资源释放,否则会内存泄漏。

#include <iostream>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "Test()" << endl;
	}

	~Test()
	{
		cout << "~Test()" << endl;
	}
private:
	int _a;
};

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

	// 不写编译器会自动生成析构函数,会去调用自定义类型的析构函数
private:
	int _year;
	int _month;
	int _day;
	Test _t;
};
int main()
{
	Date d1;
	return 0;
}

在这里插入图片描述


#include <iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 10)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}

	// 对于Stack类,必须写析构清理资源
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

// 用两个栈实现一个队列
class MyQueue
{
public:
	// 不需要写构造和析构
private:
	Stack _push_st;
	Stack _pop_st;
};
int main()
{
	MyQueue q;
	return 0;
}

在这里插入图片描述
上面这个例子MyQueue就不需要写构造和析构,因为编译器默认生成和构造和析构对于自定义类型会去调用它的构造和析构。但是Stack类必须写析构,因为必须释放掉_a的空间,否则会内存泄漏。


#include <iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 10)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}

	// 对于Stack类,必须写析构清理资源
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

// 用两个栈实现一个队列
class MyQueue
{
public:
	MyQueue()
	{
		cout << "MyQueue()" << endl;
		_p = (int*)malloc(sizeof(int) * 10);
	}

	~MyQueue()
	{
		cout << "~MyQueue()" << endl;
		free(_p);
		_p = nullptr;
	}
private:
	int* _p;
	Stack _push_st;
	Stack _pop_st;
};
int main()
{
	MyQueue q;
	return 0;
}

在这里插入图片描述
上面的代码在MyQueue加了一个int*类型的变量,那就必须手写构造进行初始化,手写析构释放_p的空间。
并且我们写的构造会在初始化列表调用自定义类型的构造函数,然后执行构造函数体的内容。我们写的析构会先执行析构函数体的内容,最后再去调用自定义类型的析构函数。


总结:编译器生成的析构函数对于内置类型不做处理,对于自定义类型会去调用它的析构函数。我们写了析构函数编译器就不会生成,对于自定义类型也会去调用它的析构函数。并且是先执行析构函数体的内容,最后再调用自定义类型的析构函数。
一般情况下,有动态资源申请,就需要显示写析构函数释放资源。没有动态申请的资源,就不需要析构。如果需要释放的资源都是自定义类型,也不需要写析构。


所以构造函数和析构函数很相似:
1、我们不写,编译器自动生成的构造和析构对于内置类型不做处理,对于自定义类型会去调用它的构造和析构。
2、我们写了构造函数,在初始化列表也会自动调用自定义类型的构造函数,先构造自定义类型。
3、我们写了析构函数,也会自动调用自定义类型的析构函数,最后析构自定义类型。
4、无论我们写不写构造和析构,都会自动调用自定义类型的构造和析构。


4、拷贝构造函数

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
C++规定,内置类型直接拷贝,自定义类型必须调用拷贝构造完成拷贝。

拷贝构造函数的特性:
1、拷贝构造函数是构造函数的一个重载形式
2、拷贝构造函数的参数只有一个且必须是对本类对象的引用,否则会引发无穷递归。

#include <iostream>
using namespace std;

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;
	}
public:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 4, 18);

	// 使用同类对象来对新创建的对象初始化—调用拷贝构造函数
	Date d2(d1); 
	// 这里虽然是赋值,但是本质上也是用同类对象来对新对象进行初始化,所以也是调用拷贝构造函数
	Date d3 = d1; 
	return 0;
}

在这里插入图片描述
可以看到,上面创建d2和d3的方式都是调用拷贝构造函数,这里的d3虽然用的是赋值,但是本质上也是拷贝构造。只要是用本类对象来对新对象进行初始化,那就是拷贝构造。这一点与我们后面要讲的赋值运算符重载以示区分。


那么为什么拷贝构造的参数必须是对类类型对象的引用呢?
在这里插入图片描述


3、若未显式定义,编译器会生成默认的拷贝构造函数。编译器生成的默认拷贝构造函数对于内置类型完成按字节序的值拷贝——浅拷贝。对于自定义类型会去调用它的拷贝构造函数。

#include <iostream>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "Test()" << endl;
	}
	Test(const Test& t)
	{
		cout << "Test(const Test& t)" << endl;
	}
private:
	int _a;
};

class Date
{
public:
	// 使用全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	// 不需要写析构,无需释放资源

	// 不写拷贝构造函数,由编译器自动生成
public:
	int _year;
	int _month;
	int _day;
	Test _t;
};
int main()
{
	Date d1(2024, 4, 18);

	// 使用同类对象来对新创建的对象初始化—调用拷贝构造函数
	Date d2(d1); 
	// 这里虽然是赋值,但是本质上也是用同类对象来对新对象进行初始化,所以也是调用拷贝构造函数
	Date d3 = d1; 
	return 0;
}

在这里插入图片描述

这里需要注意一个点:我们在日期类用了Test类,并且写了构造函数,在构造函数的初始化列表中会自动调用Test的默认构造函数,如果我们在Test类中只写了拷贝构造就会报错,因为拷贝构造函数也属于构造函数,我们写了,编译器就不会再生成构造函数了,那么就没有默认构造函数了(无参、全缺省、编译器默认生成的),所以就会报错。

这里我们看到,编译器默认生成的拷贝构造函数是会去调用自定义类型的拷贝构造的。


#include <iostream>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "Test()" << endl;
	}
	Test(const Test& t)
	{
		cout << "Test(const Test& t)" << endl;
	}
private:
	int _a;
};

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;
	}
public:
	int _year;
	int _month;
	int _day;
	Test _t;
};
int main()
{
	Date d1(2024, 4, 18);

	// 使用同类对象来对新创建的对象初始化—调用拷贝构造函数
	Date d2(d1); 
	// 这里虽然是赋值,但是本质上也是用同类对象来对新对象进行初始化,所以也是调用拷贝构造函数
	Date d3 = d1; 
	return 0;
}

在这里插入图片描述

可以看到,当我们写了拷贝构造函数之后,并不会自动调用自定义类型的拷贝构造,这点和构造函数以示区分


拷贝构造的深浅拷贝问题

先来看一段代码

#include <iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这里我们先定义了一个st1,然后用st1来拷贝构造st2,由于是编译器默认生成的拷贝构造,所以完成的是值拷贝——也就是浅拷贝。st1的_a和st2的_a指向的是堆上的同一块空间。当对象生命周期结束了进行析构时,会析构两次,导致程序崩溃。另外,其中一个修改了,也会影响另一个。如下图:

在这里插入图片描述


所以我们需要自己完成拷贝构造函数,进行深拷贝,让st1的_a和st2的_a指向不同的空间,但是他们的数据是一样的。如下图:
在这里插入图片描述
所以正确的写法是:

#include <iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

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

	Stack(const Stack& s)
	{
		_a = (int*)malloc(sizeof(int) * s._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		memcpy(_a, s._a, sizeof(int)*s._top);
		_top = s._top;
		_capacity = s._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

总结:编译器默认生成的拷贝构造函数对于内置类型会完成按字节序的值拷贝——浅拷贝,对于自定义类型会去调用它的拷贝构造函数。如果我们写了拷贝构造函数,就不会自动调用自定义类型的拷贝构造函数了。
如果是日期类这种类就不需要实现拷贝构造,用编译器生成的就行,如果是stack这种类就必须实现深拷贝。


5、赋值运算符重载函数

5.1、运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类
型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)


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


C++中内置类型可以直接用> <来进行比较,但是自定义类型就不行了。例如我们想比较两个日期,由于日期是自定义类型,无法直接用> <来比较,要重载运算符才行。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//private:
	int _year;
	int _month;
	int _day;
};

bool operator<(const Date& d1, const Date& d2)
{
	if (d1._year < d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month < d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
	{
		return true;
	}
	return false;
}
int main()
{
	Date d1(2024, 4, 18);
	Date d2(2024, 7, 1);
	cout << (d1 < d2) << endl; // 转换为:operator(d1, d2);
	return 0;
}

但是这个代码还是有点问题,由于重载operator<函数是在类外的,Date类的成员变量必须是公有的,否则无法访问。这里可以利用友元函数来解决(类和对象下会讲解,但这个方法不好,会破坏封装)。也可以直接写在类内。


#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 不能写成:bool operator(const Date& d1, const Date& d2)
	// 有隐藏的this指针,实际上还是两个参数。
	// 实际上是:bool operator(Date* const this, const Date& 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;
		}
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 4, 18);
	Date d2(2024, 7, 1);
	d1 < d2;
	cout << (d1 < d2) << endl;  // 转换为调用d1.operaotr<(d2);
	return 0;
}

在这里插入图片描述


5.2、赋值运算符重载函数

1、赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date& operator=(const Date& d)
	{
		// 判断是否自己给自己赋值
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 4, 18);
	Date d2(2024, 7, 1);
	d2.Print();
	d2 = d1; // 两个已经存在的对象进行赋值——运算符重载
	d2.Print();
	Date d3(d1); // 用已存在的类对象去初始化新对象——拷贝构造
	Date d4 = d1; // 用已存在的类对象去初始化新对象——拷贝构造
	return 0;
}

在这里插入图片描述


2、赋值运算符只能重载成类的成员函数不能重载成全局函数(原因:类内不写编译器会生成一个默认的,此时在类外的与类内的会发生冲突)
3、用户没有显式实现时,编译器会生成一个默认赋值运算符重载。默认生成的对于内置类型完成按字节序的值拷贝——浅拷贝,对于自定义类型会去调用它的赋值运算符重载函数。

#include <iostream>
using namespace std;
class Test
{
public:
	Test& operator=(const Test& t)
	{
		cout << "Test& operator=(const Test& t)" << endl;
		return *this;
	}
private:
	int _a;
};
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;
	Test _t;
};

int main()
{
	Date d1(2024, 4, 18);
	Date d2(2024, 7, 1);
	d2 = d1;
	return 0;
}

在这里插入图片描述


#include <iostream>
using namespace std;
class Test
{
public:
	Test& operator=(const Test& t)
	{
		cout << "Test& operator=(const Test& t)" << endl;
		return *this;
	}
private:
	int _a;
};
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date& operator=(const Date& d)
	{
		cout << "Date& operator=(const Date& d)" << endl;
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Test _t;
};

int main()
{
	Date d1(2024, 4, 18);
	Date d2(2024, 7, 1);
	d2 = d1;
	return 0;
}

在这里插入图片描述
当我们显示写了Date类的赋值运算符重载函数,那么就不会自动去调用自定义类型的赋值运算符重载了。这一点和拷贝构造函数是一样的。与构造函数和析构函数有所区别。


5.3、赋值运算符的深浅拷贝问题

赋值运算符重载也同样涉及深浅拷贝问题,对于日期类,我们可以不写默认浅拷贝就行,但对于stack这样的类就必须实现深拷贝。

#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}

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

	Stack(const Stack& s)
	{
		_a = (int*)malloc(sizeof(int) * s._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		memcpy(_a, s._a, sizeof(int) * s._top);
		_top = s._top;
		_capacity = s._capacity;
	}

	Stack& operator=(Stack s)
	{
		cout << "Stack& operator=(Stack s)" << endl;
		swap(_a, s._a);
		swap(_top, s._top);
		swap(_capacity, s._capacity);
		return *this;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2;
	st2 = st1;
	return 0;
}

解释:Stack类的赋值运算符重载必须实现深拷贝,如果是编译器默认生成的是完成按字节序的值拷贝,这样会导致析构两次以及一个修改了会影响另一个的问题。
而由于必定要开空间,且被赋值的st2的空间也必定要释放掉,所以直接在传参的时候进行拷贝构造s,然后把s的内容和st2的内容交换,这样st2的内容就变成了st1的内容,并且函数栈帧销毁时s指向的原来是st2的空间也会被释放掉,一举两得。


总结:编译器生成的赋值运算符重载函数对于内置类型完成按字节序的值拷贝——浅拷贝,对于自定义类型会去调用它的拷贝构造函数。如果我们写了赋值运算符重载函数,对于自定义类型就不会再去自动调用。
如果是日期类这种类就不需要实现赋值运算符重载,如果是stack这种类就必须实现深拷贝。


5.4、赋值运算符重载的前置++和后置++

由于前置++和后置++都是单操作数,所以如果直接重载operator++()的话,无法区别是前置++还是后置++。因此有规定后置++增加一个int参数表示占位,跟前置++构成函数重载

	Date& operator++(); // 前置++

	// d1++; 后置++为了跟前置++进行区分,
	//增加一个int参数占位,跟前置++构成函数重载
	Date operator++(int);

6、四个默认成员函数的总结

构造函数和析构函数类似,编译器默认生成的构造和析构对于内置类型不做处理,对于自定义类型会去调用它的构造和析构。如果我们写了构造函数和析构函数,对于自定义类型也会自动去调用它的构造和析构。先在初始化列表中调用自定义类型的构造函数,再进行初始化。先进行析构,最后调用自定义类型的析构函数。

拷贝构造函数和赋值运算符重载类似,编译器默认生成的拷贝构造和赋值运算符重载对于内置类型完成按字节序的值拷贝——浅拷贝,对于自定义类型会去调用它的拷贝构造和赋值运算符重载。如果我们写了拷贝构造和赋值运算符重载,那么就不会自动调用自定义类型的拷贝构造和赋值运算符重载了——这里和构造与析构有所区别。并且要注意深浅拷贝的问题。


7、const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。

class Date{
	void Print()  // 这里默认是 Date* const this
	{
		//...
	}
};

// 由于this所指向的Date类对象在Print函数中只读,所以可以加上const,表明该对象不可修改
// 这样无论是const对象还是非const对象,都可以调用Print函数。
class Date{
	void Print() const // 现在是 const Date* const this
	{
		//...
	}
};
// Date d1; d1.Print();  可以调用
// const Date d1; d1.print(); 也可以调用,但是如果函数后面没有加const就不能调用。
// 所以如果成员函数中不对成员变量进行修改,最好都在后面加上const,这样const和非const对象都可以调用。

8、取地址和const取地址操作符重载

这两个操作符一般不需要重载,用默认的就行,除非有特殊情况,比如你不想让别人获取到对象的地址。

class Date
{
public :
	Date* operator&()
	{
		return this ;
	}
	const Date* operator&()const
	{
		return this ;
	}
private :
	int _year ; // 年
	int _month ; // 月
	int _day ; // 日
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值