初识C++ · 类和对象(中)

目录

1 类的6个默认成员函数

2 构造函数

3 析构函数

3 拷贝构造函数


1 类的6个默认成员函数

class Date
{
public:

private:

};

这是一个空类,试问里面有什么?
可能你会觉得奇怪,明明是一个空类,却问里面有什么。其实一点也不奇怪,这就像文件操作章节,系统默认有三个流一样,标准输出流(stdout),标准输入流(stdin),标准错误流(stderr),类里面系统是有默认的函数的,一共有6个默认函数。

默认函数是指用户没有显式实现,系统会自己生成的函数,下面依次介绍。


2 构造函数

class Date
{
public:
	void Init(int year = 2020,int month = 1,int day = 17)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

当我们写了一个日期类之后,我们想要对它进行初始化,我们通常都会写一个函数叫做Init()函数,用来初始化里面的成员变量,这是一般写法。

那么有疑问了,我们介绍的不是构造函数吗,为什么会涉及到构造函数?
这是因为构造函数就是专门用来作为初始化函数的,至于为什么取名为构造函数呢?咱也不知道,咱也不敢问。

构造函数应遵行一下几个点:

1 函数名和类名应相同,并且没有返回值

class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

里面的Date()函数就是构造函数,因为没有返回值,所以不用加void,只有默认成员函数如果没有返回值就可以不用加上void,其他函数就不可以,可以用print函数试验一下。

2 类实例化的时候编译器自动调用构造函数

这里就这里结合调试:

是会自动跳到构造函数的,留个疑问,如果我们没有显式写默认构造函数会怎么样呢?

3 构造函数支持函数重载

这里就复习一下函数重载的概念,函数名相同,函数的参数不同,包括类型不同,个数不同,顺序不同,就构成函数重载:

class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	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;
};

int main()
{
	Date d1;
	Date d2(2024,4,10);
	d1.Print();
	d2.Print();

	return 0;
}

构造函数可以有多个,只要支持函数重载就行,并且不存在调用歧义

class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

这种代码就会存在调用歧义,两个函数都构成构造函数的函数重载,但是调用的时候会出现问题,传参的时候如果是无参,则两个函数都行,就会存在调用歧义,所以编译器就会报错。

使用构造函数的时候一般有无参调用和带参调用:

Date d1;
Date d2(2024,4,10);

两种调用方式都可以,取决于带不带参数,都是没有问题的。

4 如果用户没有显示调用构造函数,编译器就会调用默认的构造函数,一旦用户显示定义构造函数,系统就不会生成默认构造函数。
 

class Date
{
public:
	Date(int x)
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

这里定义了一个默认构造函数,系统就不会默认生成构造函数,所以这里编译器会报错,说没有合适的默认构造函数,主要就是因为我们已经显式定义了默认构造函数。 

5 构造函数只会对自定义类型进行初始化,C++标准没有规定对内置类型要有所处理,初始化自定义类型的时候会调用该自定义类型自己的构造函数

这个点可能有点绕,我们分开来看,一是没有规定对内置类型有所处理, 如下:

class Date
{
public:

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

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

如上,打印出来都是些随机值,说明编译器对这三个内置类型没有进行处理,但是不乏有些编译器会将它们初始化为0,这也不用惊讶,因为对内置类型没有规定要处理,所以可处理可不处理,取决于编译器心情咯。

那么什么是调用自定义类型的构造函数呢?

class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

当我们进行调试的时候,我们会发现编译器会自动进入到Time类的构造函数,随即初始化Time类的三个内置类型为0,但是如果Time类中我们没有显式定义构造函数呢?

那么就会:

那么Time类的内置类型的成员都会是随机值,有点类似无限套娃,只要我们没有显式定义构造函数,就会被定义为随机值,是不是看起来很鸡肋?

先不着急,C++11的标准中为了给内置成员初始化,添加了一个补丁,即可以在声明的时候给上缺省值:

class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
	Time _t;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

打印出来的时候即都是1,这就补上了不给内置成员初始化的缺陷。

那么构造函数是不是很鸡肋没有用处呢?

实际上并不是,如下:

class Stack
{
public:

private:
	int* arr;
	int _size;
	int _capacity;
};

class MyQueue
{
public:


private:
	Stack _st1;
	Stack _st2;
};

在两个栈实现队列的时候,当我们调用MyQueue的时候,调用到MyQueue的构造函数的时候,我们不需要对队列进行初始化,因为使用的是栈,所以在栈里面初始化,队列类里面就不需要了,这个时候就不需要在Queue里面显式构造函数了。

默认构造函数有三种,无参构造函数,全缺省构造函数,系统自动生成的默认构造函数

总结来说就是不需要传参的构造函数就是默认构造函数,而且默认构造函数只能有一个,不然存在调用歧义的问题。


3 析构函数

构造函数是用来初始化的,那么析构函数就是用来做类似销毁的工作的,但是不是对对象本身进行销毁,对象本身是局部变量,局部变量进行销毁是编译器完成的,析构函数是用来进行对象中的资源清理的。

析构函数应遵循如下特点:
函数名是类型前面加个~,没有返回值没有参数

class Date
{
public:
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

析构函数不能函数重载,如果用户显式定义了析构函数,系统就不会默认生成析构函数

当代码执行到这一步的时候,系统就会开始执行析构函数的代码,下一步语句就会跳转到~Date函数执行代码清理工作,因为析构函数没有参数,所以不支持函数重载,即只能有一个析构函数。

对象的声明周期结束的时候编译器会自己调用析构函数

也就是上图了,因为声明周期一结束,就会自己调用析构函数,如果没有显式定义析构函数的话,就会调用系统自己生成的析构函数。

当我们调用系统给的析构函数的时候就会发现:

内置类型并没有进行处理,这就是析构函数和构造函数相同的点:
对于内置类型没有要求要进行处理,处理自定义类型的时候会调用自定义类型自己的析构函数

class Time
{
public:
	~Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;

};
class Date
{
public:

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

同构造函数一样。

那么总结起来也是,比如碰到两个栈实现一个队列的时候,就可以不用写析构函数,其他情况用户都是要显式定义析构函数的。

在类中,如果没有资源申请,那么就可以不用写析构函数,如果有资源申请,那么一定要写析构函数,不然就会导致内存泄露.

内存泄露是一件很恐怖的事,因为它不会报错,内存一点点的泄露,最后程序崩溃了,然后重启一下程序发现又好了,如此往复,就会导致用户的体验很不好

class Stack
{
public:
	Stack(int capacity = 4)
	{
		int* tem = (int*)malloc(sizeof(int) * capacity);
		if (tem == nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}
		arr = tem;
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		free(arr);
		arr = nullptr;
		_capacity = _size = 0;
	}
private:
	int* arr;
	int _size;
	int _capacity;
};

像这种在堆上申请了空间的,就一定要写析构函数,不然就会导致内存泄露。


3 拷贝构造函数

拷贝构造函数,拷贝就是复制,像双胞胎一样,复制了许多特征,拷贝构造函数就是用来复制对象的,应遵行如下特点:
拷贝构造函数是构造函数的一个重载形式
既然是构造函数的重载形式,那么拷贝构造函数的函数名也应该是类名,当然,也是没有返回值的。

拷贝构造函数的参数只有一个,是类类型的引用,如果采用传值调用就会触发无限递归,程序就会崩溃
这个点的信息量有点大,我们一个一个解释

第一个,函数参数只有一个引用类型的参数,使用的时候如下:

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2020, 1, 17);
	d1.Print();
	Date d2(d1);
	d2.Print();
	Date d3 = d1;
	d3.Print();
	return 0;
}

其中参数是Date& dd的就是拷贝构造函数,拷贝构造函数一共有两种拷贝方法:
一是Date d2 = d1,二是Date d3(d1),两种方式都可以的,最后打印出来的结果都是2020-1-17。

那么,为什么使用传值调用就会触发无限递归呢?
这是因为在传值调用的时候,形参也是一个对象,对象之间的赋值都会涉及到拷贝构造函数的调用,我们结合以下代码:

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Func(Date pd)
{
	cout << "Date pd" << endl;
}
int main()
{
	Date d1(2020, 1, 17);
	Func(d1);
	return 0;
}

当代码段执行到Func的时候,语句就会先跳到拷贝构造函数,赋值完了才会进入到函数Func里面,这时候我们监视形参pd,就会发现pd已经赋值了d1的数值。

也就是说,传值调用的时候,就会自动跳到拷贝函数,那么如果拷贝构造函数也是传值调用的话呢?就会造成拷贝构造函数的形参调用拷贝构造函数的形参,一直循环往复,从而导致了无限递归。

这就是为什么拷贝构造函数的参数必须是引用类型了,但是我们拷贝构造的时候,因为是引用类型,我们不希望引用类型被修改,所以常加一个const进行修饰。

如果用户没有显式定义拷贝构造函数,系统会默认生成拷贝构造函数,拷贝构造函数按字节序进行拷贝,这种拷贝被叫做浅拷贝,与之对应的是深拷贝

默认成员函数都有个特点,如果用户没有显式定义函数,系统都会默认生成该函数。

那么,什么是浅拷贝呢?对于日期类,无非就是赋值,我们不必太过在乎,但是对于Stack这种,我们就需要注意一下了,先看代码:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		int* tem = (int*)malloc(sizeof(int) * capacity);
		if (tem == nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}
		arr = tem;
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		free(arr);
		arr = nullptr;
		_capacity = _size = 0;
	}
private:
	int* arr;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

对于Stack这种有资源申请的类,我们拷贝构造之后,生成解决方案的时候是成功的,但是当我们

运行程序的时候就会报错:

报错位置是在空指针那里,那么我们可以把重心放在空指针这里,既然是空指针报错,是我们越界访问了吗?还是说我们free了两次空指针?

看这个:

在拷贝构造完成之后,发现s1 和 s1的arr指向的空间居然是一样的:

因为拷贝构造函数内置类型是按照字节序拷贝的,所以拷贝的时候就会出现两个指针指向空间是同一个的情况,那么在析构函数,释放空间的时候,就会free掉空间两次,所以会报错。

浅拷贝对应的就是深拷贝,所以解决方法就是深拷贝,对于这种有空间申请的类,我们进行拷贝构造的时候都要深拷贝,不然析构的时候就会出现问题:

	Stack(const Stack& ss)
	{
		arr = (int*)malloc(sizeof(int) * ss._capacity);
		if (arr == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		memcpy(arr, ss.arr, sizeof(int) * ss._size);
		_size = ss._size;
		_capacity = ss._capacity;
	}

深度拷贝构造无非就是两个指针指向不同的空间,但是里面的数据是一样的,那么拷贝数据我们就可以用到memcpy,然后自己开辟一块空间给s2,最后赋值相关的数据就可以了,这样就不会报错了。

总结:

如果是日期类的拷贝构造,是没有必要进行深拷贝的,用系统默认生成的拷贝构造函数就行

拷贝构造函数报错常常因为析构函数,所以一般情况下拷贝构造函数不用写的话,析构函数也不用写

如果内置成员都是自定义类型,如MyQueue,没有指向资源,默认的拷贝构造函数就可以。

如果内部资源有申请的话,如Stack类,就需要用户自己显式定义拷贝构造函数,防止空间多次释放


感谢阅读!

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

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

相关文章

HLOOKUP函数结合数据验证实现的动态图表

小伙伴们&#xff0c;大家好啊&#xff01;今天我们分享一个使用HLOOKUP函数结合数据验证实现的动态图表技巧&#xff1b; 接下来我们具体说说操作步骤吧。 步骤1&#xff1a;选中A列的【路口车辆通过数】单元格区域&#xff0c;复制粘贴后到右边的空白区域&#xff0c;如I列…

通过调用Vcenter-Api获取Vcenter中服务器信息

通过调用Vcenter-Api获取Vcenter中服务器信息 文章目录 通过调用Vcenter-Api获取Vcenter中服务器信息1. 获取Vmware API帮助文档2. 获取访问凭证3. 获取服务器清单4. 获取服务器更多信息5. 获取虚机更多信息6. 获取磁盘信息7. 获取操作系统相关 1. 获取Vmware API帮助文档 htt…

建模设计软件 Archicad 27 for mac激活版

在建筑设计领域&#xff0c;每一次技术的革新都意味着设计效率和质量的飞跃。Archicad 27 for Mac&#xff0c;就是这样一款引领行业变革的设计软件。 Archicad 27凭借出色的性能优化和强大的功能更新&#xff0c;为Mac用户带来了前所未有的建筑设计体验。它支持BIM&#xff08…

深入理解Cortex-M7 SVC和PendSV

1前言 1.1 PendSV 在ARM V7上&#xff0c;PendSV用来作为RTOS调度器的御用通道&#xff0c;上下文切换&#xff0c;任务调度都是在其ISR中实现的。所谓pend&#xff0c;字面意思即有悬起等待的意思&#xff0c;ARM官方也明确说明&#xff0c;PendSV应该在其他异常处理完毕后执…

GPT国内怎么用?4月最新版本来了

ChatGPT镜像 今天在知乎看到一个问题&#xff1a;“平民不参与内测的话没有账号还有机会使用ChatGPT吗&#xff1f;” 从去年GPT大火到现在&#xff0c;关于GPT的消息铺天盖地&#xff0c;真要有心想要去用&#xff0c;途径很多&#xff0c;别的不说&#xff0c;国内GPT的镜像…

Datax,hbase与mysql数据相互同步

参考文章&#xff1a;datax mysql 和hbase的 相互导入 目录 0、软件版本说明 1、hbase数据同步至mysql 1.1、hbase数据 1.2、mysql数据 1.3、json脚本&#xff08;hbase2mysql.json&#xff09; 1.4、同步成功日志 2、mysql数据同步至hbase 1.1、hbase数据 1.2、mysql…

Linux 快问快答

如果对于找 Java 后端开发的话&#xff0c;我感觉会这几个差不多了&#xff0c;面试官应该不会问的这么详细吧。一般就问问 Linux 的几个常用的命令&#xff0c;然后做一些简单的性能排查就好了。如果面试被问到另外的问题&#xff0c;那我再补充进来&#xff0c;现在先掌握这么…

ZISUOJ 数据结构-线性表

题目列表&#xff1a; 问题 A: 逆序链表建立 思路&#xff1a; 可以使用头插法插入所有元素后正序遍历输出或者使用尾插法逆序遍历&#xff0c;推荐使用双链表。这是链表系列的第一个题&#xff0c;那这个题下面的参考题解的各种解法我会尽可能写全一些。 参考题解1&#xff0…

《前端面试题》- JS基础 - 防抖和节流

在界面触发点击&#xff0c;滚动&#xff0c;输入校验等事件时&#xff0c;如果对事件的触发频率不加以限制&#xff0c;会给浏览器增加负担&#xff0c;且对用户不友好。防抖和节流就是针对类似情况的解决方案。 防抖 防抖(debounce)&#xff1a;当连续触发事件时&#xff0…

攻防世界:level2[WriteUP]

用checksec、file命令查看文件属性与防护 32位小端序二进制文件 把文件丢进IDA进行逆向分析 在main函数中可以看到一个vulnerable_function函数&#xff1a;易受攻击的函数 双击该函数名称查看里面的内容 按F5显示vulnerable_function的伪代码 可以看到v1数组长度136但最后…

什么是JAVA面向对象

一&#xff0c;什么是面向对象&#xff1a; 我们以前的项目都是面向过程的&#xff0c;一个完整的项目所有的代码都写在一个类里 这就叫面向过程。 面向对象&#xff0c;是指在写大型项目时&#xff0c;多人分工合作&#xff0c;为了代码看上去简洁美观&#xff0c;会将不同的…

量子信息产业生态研究(一):关于《量子技术公司营销指南(2023)》的讨论

写在前面。量子行业媒体量子内参&#xff08;Quantum Insider&#xff09;编制的《量子技术公司营销指南》是一本实用的英文手册&#xff0c;它旨在帮助量子科技公司建立有效的营销策略&#xff0c;同时了解如何将自己定位成各自的行业专家。本文对这篇指南的主要内容进行了翻译…

ubuntu安装vulnhub

文章目录 1.下载docker2.申请加速器3.安装pip4.安装docker-compose5.安装git6.安装vulnhub文件7.运行vulhub中的靶机TypeError: kwargs_from_env() got an unexpected keyword argument ssl_version报错8.tomcat-----CVE-2017-12615(任意文件上传)1.访问192.168.9.101:80802.bp…

C++实现AVL树

文章目录 一、平衡树的优势二、二叉平衡搜索树的节点定义三、二叉搜索树的插入3.1 寻找插入位置3.2 开始判定平衡因子&#xff0c;平衡因子有变就开始旋转3.2.1 左旋的情况3.2.2 左旋代码&#xff08;一定要考虑平衡因子为2或者-2的节点是否有父节点&#xff09;3.2.2 右旋的情…

mmdetection模型使用mmdeploy部署在windows上的c++部署流程【详细全面版】

0. 前置说明: 该文档适用于:已经使用mmdetection训练好了模型,并且完成了模型转换。要进行模型部署了。 1. 概述 MMDeploy 定义的模型部署流程,如下图所示: 模型转换【待撰写,敬请期待…】 主要功能是:把输入的模型格式,转换为目标设备的推理引擎所要求的模型格式…

2440LED点灯、K1~K6按键、24401中断、2440时钟

我要成为嵌入式高手之4月12日ARM第七天&#xff01;&#xff01; ———————————————————————————— 2440GPIO GPIO&#xff1a;通用目的输入输出 LED 要把GPBCON初始化 地址是固定的&#xff0c;可以当做无符号int型 控制所有灯就需要初始化GPBCO…

还原matlab编辑器窗口和主窗口分开的问题

问题 matlab不知道早点的&#xff0c;点击运行后会弹出新的窗口&#xff0c;咋整都恢复不了 解决方案 首先&#xff0c;在编辑器窗口下&#xff0c;按ctrlshiftD&#xff0c;此时编辑器窗口和主窗口就合并了&#xff0c;问题解决。

Andriod设置背景颜色简单教学

首先&#xff0c;打开Android studio的程序应用&#xff0c;找到File&#xff0c;然后点击Settings. 第二步&#xff0c;弹出窗口,点击Appearance,进入Background image... 第三步&#xff0c;选择本地文件夹的图片 选择一张自己合适的图片设为该界面的背景图&#xff0c;完成…

PyQt5结合Yolo框架打包python为exe文件完整流程

一、准备 1.安装 pyinstaller pip install pyinstaller 更新&#xff08;初次安装忽略&#xff09; pip install --upgrade pyinstaller 2.安装 auto-py-to-exe 安装 pip install auto-py-to-exe 打开工具 auto-py-to-exe.exe auto-py-to-exe 可视化转换工具&#xff1…

LeetCode 2924.找到冠军 II:脑筋急转弯——只关心入度

【LetMeFly】2924.找到冠军 II&#xff1a;脑筋急转弯——只关心入度 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-champion-ii/ 一场比赛中共有 n 支队伍&#xff0c;按从 0 到 n - 1 编号。每支队伍也是 有向无环图&#xff08;DAG&#xff09; 上的一个节…