c++阶梯之类与对象(中)

目录

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

2. 构造函数 

2.1 构造函数概念的引出 

2.2 构造函数的特性

3. 析构函数

3.1 析构函数的概念 

3.2 特性

未使用构造与析构的版本 

使用了构造与析构函数的版本

4. 拷贝构造函数 

4.1 拷贝构造函数的概念 

4.2 特性 

结语


本节我们来认识一些类的默认成员函数。

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

如果一个类中什么都不写,我们简称它为空类

但空类中真的什么都没有吗?

不是这样的,任何类在我们什么都不写的情况下,编译器会自动生成六个默认成员函数。

默认成员函数:当用户没有显式定义时,编译器自动生成的成员函数。

2. 构造函数 

构造函数是六个默认成员函数中最为重要的成员函数。

2.1 构造函数概念的引出 

为什么要有构造函数呢?

我们来看看这段示例代码:

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

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

int main()
{
	Date d1;
	d1.Init(2024, 2, 1);
	d1.showinfo();
	Date d2;
	d2.Init(1997, 1, 1);
	d2.showinfo();
	return 0;
}

对于Date类,创建对象时我们可以调用公有方法Init() 来初始化对象,但我们每次创建对象时都需要调用它,会显得有一些麻烦,那么有没有一种方法,在我们创建对象的同时就能完成对他的初始化呢?

答案是有的。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2 构造函数的特性

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

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

我们将上面的Date类代码进行改造:

class Date
{
public:
	Date()//无参构造函数
	{}

	Date(int year, int month, int day)//带参构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void showinfo()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

int main()
{
	Date d1;//调用无参构造函数
	d1.showinfo();

	Date d2(1997, 1, 1);//调用带参构造函数
	d2.showinfo();

	Date d3();
	return 0;
}

在这里我们看到,d3的用法是调用无参函数,调用无参函数是不能在对象后加括号的。 

当我们调用无参构造函数时,发现输出的成员变量都是随机值,我们接着往下看。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。

我们将显式定义的构造函数全部注释,然后再调用无参默认构造,结果如下:

6. 关于编译器生成的默认成员函数,很多朋友会有疑惑:不实现构造函数的情况下,编译器会
生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默
认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的
默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数。
 

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 30;
		_second = 47;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void showinfo()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

int main()
{
	Date d1;
	d1.showinfo();

	return 0;
}

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值


 使用如下:

class Time
{
public:
	Time()
	{
		
		_minute = 30;
		_second = 47;
	}
private:
	int _hour=1;
	int _minute;
	int _second;
};
class Date
{
public:
	void showinfo()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year=1997;
	int _month=10;
	int _day=9;
	Time _t;
};

int main()
{
	Date d1;
	d1.showinfo();

	return 0;
}

 7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数,所以默认构造函数有三种。(不需要传参就可以调用的,都可以称为默认构造函数。)

 下面这段代码就无法正常执行。

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

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

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

 

 因为全缺省构造函数与无参构造函数都属于默认构造函数,调用不明确。默认构造函数只能有一个。

3. 析构函数

3.1 析构函数的概念 

学习了上面的构造函数,我们知道了对象是如何创建的,那么对象是如何销毁的呢?

简单来说,对象通过调用析构函数清空对象的内容,然后由编译器销毁空间。

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

3.2 特性

析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
4. 对象生命周期结束时,编译系统自动调用析构函数。

这里我们借用c++实现Stack的部分代码做实例。如果想看完整代码的同学可以点击链接查看。

目录九:c++实现类封装Stack

未使用构造与析构的版本 

typedef int DataType;
class Stack
{
public:
	//初始化
	void STInit()
	{
		_a = (DataType*)malloc(sizeof(DataType) * 4);
		if (_a == NULL)
		{
			perror("malloc fail");
			return;
		}
		_size = 0;
		_capacity = 4;
	}
	void STPush(DataType x)
	{
		_a[_size] = x;
		_size++;
	}
	//销毁
	void STDestory()
	{
		if (_a == NULL)
			return;
		free(_a);
		_a = NULL;
		_size = 0;
		_capacity = 0;
	}

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

int main()
{
	Stack ST;//创建对象
	ST.STInit();//初始化对象
	ST.STPush(1);//压栈
	ST.STPush(2);
	ST.STDestory();//清空对象
	return 0;
}

使用了构造与析构函数的版本

typedef int DataType;
class Stack
{
public:
	//初始化
	Stack()
	{
		_a = (DataType*)malloc(sizeof(DataType) * 4);
		if (_a == NULL)
		{
			perror("malloc fail");
			return;
		}
		_size = 0;
		_capacity = 4;
	}
	void STPush(DataType x)
	{
		_a[_size] = x;
		_size++;
	}
	//销毁
	~Stack()
	{
		if (_a == NULL)
			return;
		free(_a);
		_a = NULL;
		_size = 0;
		_capacity = 0;
	}

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

int main()
{
	Stack ST;//创建对象,同时系统自动调用构造函数进行初始化
	ST.STPush(1);//压栈
	ST.STPush(2);
	return 0;//对象生命周期结束,系统自动调用析构函数清空对象内容。
}

我们看这段代码:

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
	}
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};


class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	Date d1;

	return 0;
}

在这段代码里有两个类,Date类的成员变量中有Time类的对象,对于两个类的构造与析构我们都只是做了打印函数名的操作。

 我们看到,只是创建了一个Date类的d1对象,但却调用了两个类的构造与析构,根据打印内容,我们可以明晰函数的调用顺序。那么,为什么会出现上面这个结果呢?

内置类型成员的销毁不需要资源清理,而销毁类中的自定义类型变量时,需要调用其析构函数进行清理。

因为Time类的对象是Date类的成员变量,因此必须先创建Time类的对象,才能创建Date类对象。同时必须先调用Date类的析构函数,然后调用Date类中自定义类型的析构函数。

也可以这样理解:

//在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
//_day三个是
// 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对

// 所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
main函数
// 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date
类的析构函
// 数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部
调用Time
// 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析
构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

 6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

有兴趣的同学可以研究一下不同存储区对象的析构。

4. 拷贝构造函数 

4.1 拷贝构造函数的概念 

在现实生活中,我们见过两个一模一样的人,并称其为双胞胎。

那么在创建对象时,能不能创建一个与已存在对象一模一样的新对象呢?

在之前的学习中,我们可以用拷贝来实现,在c++中同样可以。

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

4.2 特性 

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

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

 下面这段代码是正确示范:

class Date
{
public:
	Date(int year = 2020, 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 showinfo()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year=1997;
	int _month=10;
	int _day=9;
};

int main()
{
	Date d1(2022, 9, 13);
	Date d2(d1);
	d2.showinfo();
	return 0;
}

关于第二点,为什么传值方式会无穷递归呢?

这是因为形参就是原对象的拷贝,需要调用拷贝函数,但拷贝函数需要传入对象的形参,因此构成了无限递归拷贝的现象。 

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

class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void showinfo()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year=1997;
	int _month=10;
	int _day=9;
};

int main()
{
	Date d1(2022, 9, 13);
	Date d2(d1);
	d2.showinfo();
	return 0;
}

这段代码里我们并没有显式实现拷贝构造函数,但结果依旧。

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的

既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
我们来看看Stack类是怎么做的

错误版

typedef int DataType;
class Stack
{
public:
	//初始化
	Stack()
	{
		_a = (DataType*)malloc(sizeof(DataType) * 4);
		if (_a == NULL)
		{
			perror("malloc fail");
			return;
		}
		_size = 0;
		_capacity = 4;
	}
	void STPush(DataType x)
	{
		_a[_size] = x;
		_size++;
	}
	//销毁
	~Stack()
	{
		if (_a == NULL)
			return;
		free(_a);
		_a = NULL;
		_size = 0;
		_capacity = 0;
	}

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

int main()
{
	Stack ST;
	ST.STPush(1);
	ST.STPush(2);
	ST.STPush(3);
	Stack st(ST);
	return 0;
}

在这段代码里,我们并没有显式实现Stack类的拷贝构造,上面的Date类同样没有,但他们的结果却截然不同。

很明显,这样做是错的。那这是为什么呢?

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

 

在浅拷贝中,s2将s1中的内容逐字节拷贝,包括s1中_a的地址,因此s1与s2指向同一空间。

但在深拷贝中,我们开辟了与s1同样大小的一块空间,然后将s1中的内容拷贝给这块空间。

改良版 

typedef int DataType;
class Stack
{
public:
	//初始化
	Stack()
	{
		_a = (DataType*)malloc(sizeof(DataType) * 4);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_size = 0;
		_capacity = 4;
	}
	Stack(const Stack& s)
	{
		DataType* temp = (DataType*)malloc(sizeof(DataType) * s._size);
		if (temp == nullptr)
		{
			perror("copy malloc fail");
			return;
		}
		_a = temp;
        memcpy(_a, s._a, s._capacity*sizeof(DataType));
		_size = s._size;
		_capacity = s._capacity;
	}
	void STPush(DataType x)
	{
		_a[_size] = x;
		_size++;
	}
	//销毁
	~Stack()
	{
		if (_a == NULL)
			return;
		free(_a);
		_a = NULL;
		_size = 0;
		_capacity = 0;
	}

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

int main()
{
	Stack s1;
	s1.STPush(1);
	s1.STPush(2);
	s1.STPush(3);
	Stack s2(s1);
	return 0;
}

 

 

5. 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象

小建议:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

下期再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/372849.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue中keep-alive的作用、原理及应用场景

在进行Vue开发的过程中&#xff0c;我们经常会遇到需要进行组件缓存的场景&#xff0c;这时候Vue提供的keep-alive组件就派上了用场。keep-alive组件是Vue内置的一个抽象组件&#xff0c;它可以将其包裹的组件进行缓存&#xff0c;提高组件的性能&#xff0c;同时也可以节省服务…

Python学习路线 - Python高阶技巧 - 拓展

Python学习路线 - Python高阶技巧 - 拓展 闭包闭包注意事项 装饰器装饰器的一般写法(闭包写法)装饰器的语法糖写法 设计模式单例模式工厂模式 多线程进程、线程并行执行多线程编程threading模块 网络编程Socket客户端和服务端Socket服务端编程实现服务端并结合客户端进行测试 S…

使用Java实现HTTP持久连接:一次与网络的“长聊“

大家都知道&#xff0c;传统的HTTP连接就像是一次性的餐具&#xff0c;每发送一个请求&#xff0c;就得重新建立一个连接&#xff0c;然后快速用完就扔。这对于网络资源来说&#xff0c;简直就是一场"大肆挥霍"的派对。但幸好&#xff0c;我们有HTTP持久连接&#xf…

【字符串】字典树

字典树就是利用一个这样的树状结构&#xff0c;可以记录字符串有没有出现过 放个板子 int nxt[100000][26], cnt; bool st[100000]; // 该结点结尾的字符串是否存在 void insert(string s, int l) // 插入字符串&#xff0c;l是字符串长度 { int p 0;for (int i 0; i < …

BGP邻居故障检测

第一种情况:如果AR2和AR4采用直连建立邻居,则排查步骤如下: 1)在AR2和AR4上使用ping x.x.x.x命令检查AR2和AR4用于建立EBGP邻居关系的直连地址连通性是否正常。如果不能ping通。则需要使用二分法从网络层向下层逐层进行排查,首先检查接口地址及路由的可达性,修改完成后,如…

Architecture Lab:Part C【流水线通用原理/Y86-64的流水线实现/实现IIADDQ指令】

目录 任务描述 知识回顾 流水线通用原理 Y86-64流水线实现&#xff08;PIPE-与PIPE&#xff09; 开始实验 IIADDQ指令的添加 优化 ncopy.ys 仅用第四章知识&#xff0c;CEP11.55 8x1展开&#xff0c;CPE9.35 8x1展开2x1展开消除气泡&#xff0c;CPE8.10 流水线化通过…

中小学信息学奥赛CSP-J认证 CCF非专业级别软件能力认证-入门组初赛模拟题一解析(选择题)

CSP-J入门组初赛模拟题一&#xff08;选择题&#xff09; 1、以下与电子邮件无关的网络协议是 A、SMTP B、POP3 C、MIME D、FTP 答案&#xff1a;D 考点分析&#xff1a;主要考查小朋友们网络相关知识的储备&#xff0c;FTP是文件传输协议和电子邮件无关&#xff0c;所以…

vscode debug无法直接查看eigen变量的问题(解决方法)

主要是给gdb添加一个Eigen相关的printer即可, 网上其他教程都搞太复杂了, 我整理成了一个仓库, 把仓库克隆下来直接运行 ./setup.sh脚本即可配置好 git clone gitgithub.com:fandesfyf/EigenGdb.git cd EigenGdb ./setup.sh 然后在vscode中重新debug即可。 效果 …

2.2-学成在线内容管理之课程分类查询+新增课程

文章目录 内容管理模块4 课程分类查询4.1 需求分析4.2 接口定义4.3 接口开发4.3.1 树型表查询4.3.2 开发Mapper 4.4 接口测试4.4.1 接口层代码完善4.4.2 测试接口 5 新增课程5.1 需求分析5.1.1 业务流程4.1.2 数据模型 5.2 接口定义5.3 接口开发5.3.1 保存课程基本信息5.3.2 保…

深度学习系列55:深度学习加速技术概述

总体有两个方向&#xff1a;模型优化 / 框架优化 1. 模型优化 1.1 量化 最常见的量化方法为线性量化&#xff0c;权重从float32量化为int8&#xff0c;将输入数据映射在[-128,127]的范围内。在 nvdia gpu&#xff0c;x86、arm 和 部分 AI 芯片平台上&#xff0c;均支持 8bit…

嵌入式系统中的电磁兼容和电磁干扰问题如何解决?

嵌入式系统在现代科技领域中发挥着越来越重要的作用&#xff0c;无论是在智能手机、汽车、医疗设备还是工业控制系统中&#xff0c;嵌入式系统都扮演着关键的角色。然而&#xff0c;随着嵌入式系统功能的不断扩展和集成度的增加&#xff0c;电磁兼容性(EMC)和电磁干扰(EMI)问题…

SpringBoot集成axis发布WebService服务

文章目录 1、使用maven-web项目生成server-config.wsdd文件1.1、新建maven-web项目1.1.1、新建项目1.1.2、添加依赖 1.2、编写服务接口和实现类1.2.1、OrderService接口1.2.2、OrderServiceImpl实现类 1.3、配置deploy.wsdd文件deploy.wsdd文件 1.4、配置tomcat1.4.1、配置tomc…

交友系统---让陌生人变成熟悉人的过程。APP小程序H5三端源码交付,支持二开。

随着社交网络的发展和普及&#xff0c;人们之间的社交模式正在发生着深刻的变革。传统的线下交友方式已经逐渐被线上交友取而代之。而同城交友正是这一趋势的产物&#xff0c;它利用移动互联网的便利性&#xff0c;将同城内的人们连接在一起&#xff0c;打破了时空的限制&#…

【node】Node.js的常用内置模块:

文章目录 一、os模块&#xff1a;【1】常用的OS模块方法包括&#xff1a;【2】案例&#xff1a; 二、path模块&#xff1a;【1】常用的path模块方法包括&#xff1a;【2】案例&#xff1a; 三、url模块&#xff1a;【1】常用的url模块方法包括&#xff1a;【2】案例&#xff1a…

LeetCode--代码详解 2.两数相加

2.两数相加 题目 难度&#xff1a;中等 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数…

数字孪生:智慧城市的核心技术与发展

一、引言 随着城市化进程的加速&#xff0c;智慧城市的概念和实践逐渐成为全球关注的焦点。智慧城市利用先进的信息通信技术&#xff0c;提升城市治理水平&#xff0c;改善市民的生活质量。而数字孪生作为智慧城市的核心技术&#xff0c;为城市管理、规划、应急响应等方面提供…

【数据分享】1929-2023年全球站点的逐日平均能见度(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;说到常用的降水数据&#xff0c;最详细的降水数据是具体到气象监测站点的降水数据&#xff01; 有关气象指标的监测站点数据&#xff0c;之前我们分享过1929-2023年全…

[Angular 基础] - 数据绑定(databinding)

[Angular 基础] - 数据绑定(databinding) 上篇笔记&#xff0c;关于 Angular 的渲染过程及组件的创建&简单学习&#xff1a;[Angular 基础] - Angular 渲染过程 & 组件的创建 Angular 之中的 databinding 是一个相对而言更加复杂&#xff0c;以及我个人觉得相对而言比…

《MySQL》超详细笔记

目录 基本知识 主流数据库 数据库基本概念 MySQL启动 数据库基本命令 数据库 启动数据库 显示数据库 创建数据库 删除数据库 使用数据库 查询当前数据库信息 显示数据库中的表 导入数据库脚本 表 查看表的结构 查看创建某个表的SQL语句 数据库的查询命令 查询…

设计模式学习笔记(一):基本概念;UML

文章目录 参考面向对象的设计原则创建型模式结构型模式行为型模式 UML视图图&#xff08;Diagram&#xff09;模型元素(Model Element)通用机制类之间的关系关联关系复杂&#xff01;&#xff01;聚合关系组合关系 依赖关系泛化关系接口与实现关系 参考 https://github.com/fa…