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

news2024/11/25 7:02:41

目录

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类同样没有,但他们的结果却截然不同。

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

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

 

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

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

下期再见!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据挖掘实战-基于决策树算法构建北京市空气质量预测模型

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

nginx slice模块的使用和源码分析

文章目录 1. 为什么需要ngx_http_slice_module2. 配置指令3. 加载模块4. 源码分析4.1 指令分析4.2 模块初始化4.3 slice模块的上下文4.2 $slice_range字段值获取4.3 http header过滤处理4.4 http body过滤处理5 测试和验证 1. 为什么需要ngx_http_slice_module 顾名思义&#…

云计算市场分析

目录 一、云计算市场概述 1.1 概述 二、国外云计算厂商 2.1 亚马逊AWS 2.2 微软AzureAzure 2.3 Apple iCloud 三、国内云计算厂商 3.1 阿里云 3.2 腾讯云 3.3 华为云 3.4 百度智能云 一、云计算市场概述 1.1 概述 云计算从出现以来&#xff0c;其发展就非常迅速。以…

【SeaArt】免费生成图像——功能和使用方法解析

SeaArt 关于SeaArtSeaArt的特点和功能1. SeaArt每天最多可以免费生成150次2. SeaArt生成的图片可以用于商业用途 如何使用登录创作 总结 关于SeaArt SeaArt&#xff08;海艺&#xff09;是由总部位于新加坡的“STAR CLUSTER PTE. LTD.”运营的图像生成AI工具。 它基于Stable …

7.Vue面试题

一、Vue面试基础知识 在这一小节中&#xff0c;我们先把一些常见的Vue的基础的面试题&#xff0c;总结出来。这些基础的知识点都是在面试的时候经常会被问到的一些内容。 当然关于基础的一些内容在前面的课程总咱们都已经讲解过来&#xff0c;所以这里我们只是把一些常见的内…

C++PythonC# 三语言OpenCV从零开发(8):图像平滑处理

文章目录 相关链接前言图像资源图像平滑处理图像学知识补充(重点)什么是卷积什么是图像滤波什么是方框滤波和均值滤波 代码PythonCCsharp 总结 相关链接 C&Python&Csharp in OpenCV 专栏 【2022B站最好的OpenCV课程推荐】OpenCV从入门到实战 全套课程&#xff08;附带课…

UE4运用C++和框架开发坦克大战教程笔记(十七)(第51~54集)

UE4运用C和框架开发坦克大战教程笔记&#xff08;十七&#xff09;&#xff08;第51~54集&#xff09; 51. UI 框架介绍UE4 使用 UI 所面临的问题以及解决思路关于即将编写的 UI 框架的思维导图 52. 管理类与面板类53. 预加载与直接加载54. UI 首次进入界面 51. UI 框架介绍 U…

c#string方法对比

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a; using System; namespace ConsoleApp23{ class Progra…

【计算机网络】物理层概述|通信基础|奈氏准则|香农定理|信道复用技术

目录 一、思维导图 二、 物理层概述 1.物理层概述 2.四大特性&#xff08;巧记"械气功程") 三、通信基础 1.数据通信基础 2.趁热打铁☞习题训练 3.信号の变身&#xff1a;编码与调制 4.极限数据传输率 5.趁热打铁☞习题训练 6.信道复用技术 推荐 前些天发…

flutter开发实战-可扩展popup弹窗template模版样式

flutter开发实战-可扩展popup弹窗template模版样式 最近在看到一个flutter_beautiful_popup&#xff0c;可以美化弹窗窗口样式。该插件通过一个template模版的类BeautifulPopupTemplate作为抽象的base类。 一、基类BeautifulPopupTemplate 在BeautifulPopupTemplate中&…

前端面试题——Vue的双向绑定

前言 双向绑定机制是Vue中最重要的机制之一&#xff0c;甚至可以说是Vue框架的根基&#xff0c;它将数据与视图模板相分离&#xff0c;使得数据处理和页面渲染更为高效&#xff0c;同时它也是前端面试题中的常客&#xff0c;接下来让我们来了解什么是双向绑定以及其实现原理。…

可解释性对人工智能发展的影响

文章目录 每日一句正能量前言可解释AI已成热点可解释性人工智能的重要性可解释性人工智能的研究现状推动可解释模型构建未来展望后记 每日一句正能量 不好等待运气降临&#xff0c;就应去发奋掌握知识。 前言 随着人工智能技术的快速发展&#xff0c;越来越多的应用场景需要人…

神经网络激活函数到底是什么?

激活函数 其实不是很难啦&#xff0c;归结一下就是大概这样几个分类&#xff0c;详情请参考【神经网络】大白话直观理解&#xff01;_哔哩哔哩_bilibili神经网络就是干这个事的~ 如果队伍不长&#xff0c;一个ykxb就可以了&#xff0c;如果 如果 队伍太长 就加一个激活函数也…

HBase相关面试准备问题

为什么选择HBase 1、海量存储 Hbase适合存储PB级别的海量数据&#xff0c;在PB级别的数&#xff0c;能在几十到几百毫秒内返回数据。这与Hbase的极易扩展性息息相关。正是因为Hbase良好的扩展性&#xff0c;才为海量数据的存储提供了便利。 2、列式存储 这里的列式存储其实说的…

Verilog实现2进制码与BCD码的互相转换

1、什么是BCD码&#xff1f; BCD码是一种2进制的数字编码形式&#xff0c;用4位2进制数来表示1位10进制中的0~9这10个数。这种编码技术&#xff0c;最常用于会计系统的设计里&#xff0c;因为会计制度经常需要对很长的数字做准确的计算。相对于一般的浮点式记数法&#xff0c;…

微信小程序 --- 腾讯地图线路规划

目录 微信小程序JavaScript 简介 Hello world&#xff01; geocoder(options:Object) 微信小程序插件 简介 路线规划插件 入驻腾讯位置服务平台 申请开发者密钥&#xff08;Key&#xff09;&#xff1a;申请秘钥 Key的作用与注意事项 微信公众平台绑定插件 方式一&a…

新型IT运维管理,基础设施和数据两手都要硬

编前语&#xff1a;数据是AI的基石&#xff0c;缺数据无AI。 AI大模型时代&#xff0c;数据赋予IT人“新使命” 当下IT人在企业中扮演着运营支撑的角色。说到运维管理&#xff0c;相信每人都是一把辛酸泪&#xff0c;每天承担着繁琐、高负荷且又高风险的运维工作&#xff0c;但…

开源软件全景解析:驱动技术创新与行业革新的力量

目录 什么是开源 开源的核心 开源软件的特点 为什么程序员应该拥抱开源 1.学习机会&#xff1a; 2.社区支持&#xff1a; 3.提高职业竞争力&#xff1a; 4.加速开发过程&#xff1a; 5.贡献和回馈&#xff1a; 开源软件的影响力 开源软件多元分析&#xff1a; 开源…

机器学习中常用的性能度量—— ROC 和 AUC

什么是泛化能力&#xff1f; 通常我们用泛化能力来评判一个模型的好坏&#xff0c;通俗的说&#xff0c;泛化能力是指一个机器学期算法对新样本&#xff08;即模型没有见过的样本&#xff09;的举一反三的能力&#xff0c;也就是学以致用的能力。 举个例子&#xff0c;高三的…

为什么说TiDB在线扩容对业务几乎没有影响

作者&#xff1a; 数据源的TiDB学习之路 原文来源&#xff1a; https://tidb.net/blog/e82b2c5f 当前的数据库种类繁多&#xff0c;墨天轮当前统计的所有国产数据库已经有 290个 &#xff0c;其中属于关系型数据库的有 166个 。关系型数据库从部署架构上又可以分为集中式…