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

news2024/12/23 23:33:15

目录

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类,就需要用户自己显式定义拷贝构造函数,防止空间多次释放


感谢阅读!

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

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

相关文章

PE文件(一)PE结构概述

PE结构简述 Windows操作系统是只能运行以内存4D 5A开头&#xff0c;翻译是MZ的可执行文件&#xff0c;也叫做PE结构文件&#xff0c;是以exe&#xff0c;.sys&#xff0c;.dll等等作为后缀的文件。而不同的操作系统能运行的可执行文件都是各自特有的&#xff0c;比如Linux可运…

zabbix 自定义模板,邮件报警,代理服务器,自动发现与自动添加及snmp

目录 一. 自定义监控内容 1. 在客户端创建自定义 key 2. 在 web 页面创建自定义监控项模块 2.1 创建模板 2.2 创建应用集 2.3 创建监控项 2.4 创建触发器 2.5 创建图形 2.6 将主机与模板关联起来 登录测试 2.7 设置邮件报警 测试邮件报警 3. nginx 服务状况的检测…

2024年腾讯云服务器价格一览表

随着云计算技术的快速发展&#xff0c;越来越多的企业和个人开始选择使用云服务器来满足他们的数据存储和计算需求。腾讯云作为国内领先的云服务提供商&#xff0c;其服务器产品因性能稳定、安全可靠而备受用户青睐。那么&#xff0c;2024年腾讯云服务器的价格情况如何呢&#…

状态压缩DP题单

P1433 吃奶酪&#xff08;最短路&#xff09; dp(i, s) 表示从 i 出发经过的点的记录为 s 的路线距离最小值 #include<bits/stdc.h> #define int long long using namespace std; const int N 20; signed main() { int n; cin >> n;vector<double>x(n 1),…

matplotlib plt.gca()学习

之前一直在这个代码里看到plt.gca()的使用&#xff0c;但是一直没搞明白这个怎么用&#xff0c;今天总结一下 gca是get current axis的首字母的缩写&#xff0c;就是控制坐标轴的&#xff0c;也是比较简单的&#xff0c;并不是一个很复杂的函数 移动坐标轴 import matplotlib…

Bacnet 入门参考资料 (一)

[OTC] 在网上整理的一些BACnet的相关资料&#xff0c;在这里作一个整理&#xff0c;方便自己食用。 参考博文 ① BACnet基础入门&#xff1a;https://blog.csdn.net/li1197538342/article/details/128341198 ② BACnet网络讲义 &#xff08;建议先看这个&#xff0c;第1章&a…

LeetCode 1.两数之和(HashMap.containsKey()、.get、.put操作)

给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回…

代码随想录第39天|62.不同路径 63. 不同路径 II

62.不同路径 62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划中如何初始化很重要&#xff01;| LeetCode&#xff1a;62.不同路径_哔哩哔哩_bilibili 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标…

spring-数据处理及跳转

结果跳转方式 ModelAndView 设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 . 页面 : {视图解析器前缀} viewName {视图解析器后缀} <!-- 视图解析器 --> <bean class"org.springframework.web.servlet.view.InternalResourceViewRes…

flink network buffer

Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一&#xff0c;是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask)&#xff0c;因此&#xff0c;对于 Flink 作业的性能包括吞吐与延迟都至关重要。与 TaskManager 和 JobManager 之间通过基于 A…

一些重新开始面试之后的八股文汇总

一、内存中各项名词说明 1、机器内存概念说明 linux中的free命令可以查看机器的内存使用情况&#xff0c;vmstat命令也可以 其中不容易被理解的是&#xff1a; 内存缓冲/存数&#xff08;buffer/cached&#xff09; 1.buffers和cache也是RAM划分出来的一部分地址空间 2.buff…

鸿蒙入门05-真机运行“遥遥领先”

如果你有一台真的 "遥遥领先"那么是可以直接在手机上真机运行你的项目的我们也来尝试一下运行 一、手机设置开发者模式 打开手机设置 打开手机设置界面 向下滑动到关于手机位置 快速连续点击版本号位置 下图所示位置快速连续点击 打开 3 - 5 次即可 会提示您已经进…

CoFSM基于共现尺度空间的多模态遥感图像匹配方法--论文阅读记录

目录 论文 Multi-Modal Remote Sensing Image Matching Considering Co-Occurrence Filter 参考论文&#xff1a;SIFT系列论文&#xff0c; SIFT Distinctive Image Features from Scale-Invariant Keypoints&#xff0c;作者&#xff1a;David G. Lowe 快速样本共识算法…

H264标准协议基础3

参考博文 上一篇H264标准协议基础2 1.解码视频帧的poc计算 2.残差4x4 矩阵中的trailingones & numcoeff 2.1查表 trailingones 表达出尾部one(1,-1)系数的个数,按照zigzag扫描出(1,-1)个数,trailingones的最大为3; numcoeff 表达非零值系数的个数,最多为16个…

初识ansible核心模块

目录 1、ansible模块 1.1 ansible常用模块 1.2 ansible-doc -l 列出当前anisble服务所支持的所有模块信息&#xff0c;按q退出 1.3 ansible-doc 模块名称 随机查看一个模块信息 2、运行临时命令 2.1 ansible命令常用的语法格式 3、常用模块详解与配置实例 3.1命令与…

内部类

一.概念 当一个事物内部&#xff0c;还有一个部分需要一个完整的结构进行描述&#xff0c;而这个内部的完整的结构又只为外部事物提供服务&#xff0c;那么将这个内部的完整结构最好使用内部类。在Java中&#xff0c;可以将一个类定义在另一个类或者一个方法内部&#xff0c;前…

如何进行开关电源温升极限测试?

开关电源温升极限测试是指开关电源在没有过温保护的条件下&#xff0c;逐渐升高电源的测试温度&#xff0c;直到开关电源损坏。温升极限测试是为了研究开关电源能够承受的最高环境温度&#xff0c;从而评估开关电源的性能&#xff0c;优化提升开关电源的工艺设计。 温升极限测试…

【Canvas技法】六种环状花纹荟萃

【图例】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>使用HTML5/Canvas绘制六种环状花纹</title><style type&quo…

SimCLR v2(NeurIPS 2020)论文解读

paper&#xff1a;Big Self-Supervised Models are Strong Semi-Supervised Learners official implementation&#xff1a;https://github.com/google-research/simclr 本文的创新点 本文在SimCLR的基础上做了一些改进&#xff0c;提出了SimCLR v2&#xff0c;进一步提升了…

车机系统与 Android 的关系概述

前言&#xff1a;搞懂 Android 系统和汽车到底有什么关系。 文章目录 一、基本概念1、Android Auto1&#xff09;是什么2&#xff09;功能 2、Google Assistant3、Android Automotive1、Android Auto 和 Android Automotive 的区别 4、App1&#xff09;App 的开发2&#xff09;…