【C++指南】一文总结C++类和对象【中】

发布于:2025-03-01 ⋅ 阅读:(8) ⋅ 点赞:(0)

🌟 各位看官好,我是egoist2023

🌍 种一棵树最好是十年前,其次是现在!

🚀 今天来学习C++类和对象的语法知识

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦

目录

类的默认成员函数

构造函数

内置类型和自定义类型

特点

析构函数

特点

拷贝构造函数

特点

传值返回注意事项


类的默认成员函数

默认成员函数就是在用户没有显现实现的情况下, 编译器会自动生成的成员函数称为默认成员函数 。一个类中不显现写的情况下编译器会默认生成 6个默认成员函数 ,需要注意的是这6个中最重要的是 构造、析构、拷贝构造、赋值重载这4个 。(在C++11后还增加了两个默认成员数, 移动构造和移动赋值)

在学习默认成员函数时需围绕这两点出发:
第一:我们不写时,编译器默认生成的函数行为是什么,是否符合我们的需求。
第二:编译器默认生成的函数不满足我们的需求时,如何自己实现呢?

构造函数

构造函数 是特殊的成员函数,其主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了), 而是对象实例化时初始化对象 。构造函数的本质是要替代我们以前StackSeqlist类中写的Init函数的功能,其自动调用的特点完美的替代了Init。

内置类型和自定义类型

C++把类型分为内置类型和自定义类型。内置类型即语言提供的原生数据类型,如:int/char/double/指针等自定义类型就是使用的class/struct等关键字自己定义的类型

特点

1. 函数名与 类名相同 ,且 无返回值 ,也 不需要写void (C++规定)
2. 对象实例化时系统会 自动调用对应的构造函数
3. 构造函数 支持重载
4. 如果类中 没有显式定义 构造函数,则C++编译器会 自动生成一个无参的默认构造函数 (若显式定义编译器不再生成)
5. 编译器默认生成的构造,对内置类型成员变量的初始化没有要求 ,即是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要借助初始化列表才能解决。
在如下一段程序中,我们写了一个日期Date类,想打印其年月日,会发现里面的值是随机给的(编译器并未对内置类型初始化),可以提供默认值。

6. 无参构造函数、全缺省构造函数、编译器默认生成的构造函数 --> 默认构造函数 。有且只能有一种存在。(默认构造函数并不是指编译器默认生成的构造函数,简单来说不传实参就可以调用的构造就叫默认构造)
7. 无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
原因有两点:
1.语法上默认构造函数有且只能有一种存在;
2.写了一个无参一个全缺省,那么构造 Date  d( ) 时编译器怎么知道走哪个构造函数呢。
注意:调用无参构造函数时,对象后面不要跟(),否则会报错,因为编译器无法判断是函数声明还是实例化对象。
//声明和实例化混淆
Date Func();//函数声明
Date d1();
class Date
{
public:
	// 1.⽆参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	// 2.全缺省构造函数
	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;
};

析构函数

析构函数与构造函数功能相反, 析构函数 不是完成对对象本身的销毁(比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要管),C++规定对象在销毁时会自动调用析构函数, 完成对象中资源的清理释放工作 。析构函数的功能类比我们之前Stack实现的Destroy功能(若没有向堆申请空间之类其实就是没有资源需要释放,如Date实现是不需要析构函数的。

特点

1. 析构函数名是在 类名前加上字符 ~
2. 无参数无返回值 。(跟构造类似)。
3. 一 个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(对于有资源申请就需要显式定义).
如下我们对堆区申请了4字节的空间,当对象生命周期结束时,则调用析构函数回收资源。
int* arr=(int*)malloc(sizeof(int));
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数 对内置类型成员不做处理 (因为自己会释放),自定类型成员会调用他的析构函数。
6. 需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说 自定义类 型成员无论什么情况都会自动调用析构函数
7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数。
如Date;如下一段程序Date的实现都是内置类型,不涉及向资源的申请,因此使用编译器默认生成的析构函数即可。
class Date
{
public:
    //带参构造函数
	Date(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;
};
有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack;
typedef int STDataType;

class Stack
{
public:
	Stack(int n = 4)
	{
        //向堆空间申请了n*STDataType字节空间
		STDataType* _a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == NULL)
		{
			perror("malloc fail!");
			exit(1);
		}
		_capacity = n;
		_top = 0;
	}

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

如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue --> 即两个栈实现一个队列

那么显式写了析构函数,编译器是否还会调用默认生成的析构?

会的,这里通过调试来一步步观察。

对象生命周期结束时,通过调试可以发现编译器先调用了显式的析构函数,观察运行窗口确实打印了"~MyQueue()"。接着,继续调试发现编译器调用了MyQueue默认生成的析构(调用了stack的析构函数),完成了资源的回收。在这里,我们就可以发现,显式析构函数的实现并未达到我们想要的目的,编译器还会调用MyQueue默认生成的析构从而达到了资源回收的目的。

因此,可以总结编译器不放心仍会调用不显示的析构(尽管写了显式析构),怕的就是内存泄露等问题。

8. 一个局部域的多个对象,C++规定后先定义后析构。(注意:这里指的是局部域的情况下)


拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用(因为需要此参数来拷贝),且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

特点

1. 拷贝构造函数 是构造函数的一个重载 。(即拷贝构造也是一种构造函数)
2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 (拷贝构造函数支持多个参数,但后面的参数必须有缺省值)
3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,自定义类型传值传参和传值返回都会调用拷贝构造完成。
为何使用传值方式会报错呢?
调用拷贝构造就需要先传参,把实参d1传给形参d,而此时Date d(d1)又形成了一个新的拷贝构造,调用新的拷贝构造就需要先传参,把d1传给形参d,此时再次形成新的拷贝构造,一直这样下去,永远没办法回来,就形成了无穷递归。

传值返回注意事项

在上面两段代码中,一个是传值返回,一个是传引用返回,分别运行后运行窗口都是没问题的。但是代码2这种情况实际上是不对的,不符合预期。

传值返回会产生临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),可以减少拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似⼀个野指针一样。因此传引用返回一定要确保返回对象,在当前函数结束后还在,才能用引用返回

4. 若未显式定义拷贝构造,编译器会成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

默认生成的拷贝构造

在Func1()函数中,为传值返回,此时我们并没有写拷贝构造函数,编译器默认生成拷贝构造函数。但这种拷贝仅仅是浅拷贝,如果是针对有资源申请的成员则无法满足要求。上面这段程序并不涉及资源的申请,因此运行不会出错(一旦涉及资源申请,切记显式实现拷贝构造实现目的)。

5. 像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造(浅拷贝)不符合我们的需求,因此我们自己要实现深拷贝(对指向的资源也进行拷贝)。
编译器默认生成的拷贝(浅拷贝)
此时运行窗口会崩溃,这是为什么呢?这是 浅拷贝并不会向内存申请空间,在析构函数时二次析构同一块空间,从而导致报错。
显式拷贝构造(深拷贝)
向内存申请一块空间供d2使用
	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样大的资源再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (_a = NULL)
		{
			perror("malloc fail!");
			exit(1);
		}
		//malloc 成功
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

像MyQueue这样的类型内部主要是自定义类型 Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显式实现 MyQueue的拷贝构造。
我们还会发现MyQueue简直是人生赢家,无论是析构还是拷贝构造,编译器都会默认调用自动生成的默认成员函数。