C++入门全集(3):类与对象【中】

news2024/9/28 5:58:47

一、前言

经过上一篇文章的学习,我们知道类中由成员变量和成员函数组成

本章的内容,就围绕类的6个默认成员函数展开讲解

如果一个类中什么都没有,简称为空类

class Date
{

};

然而,空类真的像表面一样什么都没有吗?不是的,编译器会自动生成上面的6个默认成员函数。

用户本身没有去显式实现,编译器会自己生成的成员函数就称为默认成员函数。


二、构造函数

2.1 概念

我们实现一个日期类

class Date
{
public:
	void Init(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;
};

对于这个Date类,我们可以使用公有成员函数Init来初始化对象

但是如果每次创建一个对象,就要调用一次Init来初始化,未免有点太麻烦了,有没有办法在创建对象的时候就可以进行初始化呢?

构造函数的功能能够实现这个需求,它是一个特殊的成员函数,它的函数名和类名相同,在创建对象时由编译器自动调用,并且只在创建对象时调用一次,其他时候不会调用。

2.2 特性

虽然构造函数名叫构造,但是它并不参与对象的创建,只用来初始化对象。

构造函数主要有以下几个特征:

(1)函数名与类名相同

(2)无返回值

(3)实例化对象时由编译器自动调用对应类的构造函数

(4)构造函数可以重载(你可以定义多种初始化的方法)

例如我定义了两个构造函数,一个不带参数,一个带参数

class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}

	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;
};

当我实例化对象时不传参,就会调用第一个无参的构造函数;如果带了参数,就会调用第二个。

有人可能会有疑问:为什么不传参时对象的后面不用带括号呢?

下面这种情况你能分得清是在实例化对象还是在声明函数吗?这就是原因

所以,使用无参的构造函数初始化对象时,不用带括号

(5)如果用户没有在类中显式定义构造函数,则编译器会自动生成一个无参的默认构造函数;如果用户定义了,则编译器不会生成。

例如我们把之前定义的无参构造函数删了,只留下带参的构造函数

这时如果还是无参的去实例化对象,就会报错。

因为已经有了显式定义的构造函数,编译器就不再生成。

那我们把前面显式定义的两个构造函数删了,试一试编译器自动生成的构造函数

这是怎么回事,不是说编译器会自动生成默认构造函数吗?怎么好像并没有什么用?

原来,C++把类型分为了内置类型(基本类型)和自定义类型。内置类型就是语言自己提供的例如int、char等类型,而自定义类型就是我们使用class/struct/union等自己定义的类型。

例如我们在日期类中添加一个自定义类型的成员函数_time

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	int _year;
	int _month;
	int _day;
	Time _time;
};

创建一个Date类的对象,输出如下:

可以看到,Date类中编译器生成的默认构造函数对自定义类型的变量_time起了作用,去调用了Time类的构造函数。 

编译器自动生成的默认构造函数,只对自定义类型起作用,而不处理内置类型。我们的日期类中原本的三个成员变量都是内置类型,所以才会出现随机值。

实际上,这是C++的一个缺陷,所以在C++11中针对内置类型成员不初始化的缺陷打了一个补丁,即内置类型的成员变量在声明时可以给默认值

(6)无参的构造函数和全缺省的构造函数也认为是默认构造函数,并且默认构造函数只能有一个

例如如下代码:

class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}

	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year = 1900;
	int _month = 1;
	int _day = 1;
};

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

一个无参的构造函数,一个全缺省的构造函数,那么此时实例化d1时该调用哪个函数呢?

最好就直接把无参的构造函数删了就行了~ 毕竟全缺省的构造函数也能完成无参的功能嘛


三、析构函数

3.1 概念

以前使用C语言写栈或者其他数据结构的时候,每次程序结束前都需要手动Destory一下开辟出的空间,避免内存泄漏,十分的麻烦。

既然C++可以自动初始化,那能不能自动清理内存空间呢?析构函数可以满足你的需求

析构函数与构造函数类似,但是它们的功能大不一样。在一个对象出了作用域被销毁后,编译器就自动调用析构函数,完成对象中资源的清理工作

3.2 特性

析构函数是特殊的成员函数,主要有以下几个特征:

(1)析构函数名是在类名前加上字符~

(2)析构函数没有参数,也没有返回值

(3)一个类只能有一个析构函数,如果用户没有显式定义,编译器会自动生成默认的析构函数。

(4)在对象生命周期结束时,编译器会自动调用析构函数

我们以栈为例子,写一个析构函数看看

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	// ...

	~Stack() //析构函数
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

有了析构函数,我们对栈进行各种增删查改后再也不用手动去释放空间了,编译器都会帮你完成。

(5)编译器自动生成的默认析构函数也只对自定义类型的成员有效

因为内置类型的成员变量出了作用域就销毁了,不需要回收空间等操作,而自定义类型的成员则需要额外的清理。

我们还是添加一个Time类型作为例子

可以看到,虽然我们创建的是Date类的对象,但是还是调用了Time类的析构函数。

因为在Date类中有类型为Time的成员变量,在销毁对象d1时要先销毁Time类的成员变量_time

编译器会在Date类中生成一个默认析构函数并调用,这个函数自身再去调用Time类的析构函数。

对于三个int类型的成员变量,因为是内置类型,所以析构函数不作处理。

所以,如果类中只有内置类型的成员变量,没有进行资源申请时,可以不用显式定义析构函数,例如Date类;有资源申请时就一定要写,例如Stack类


四、拷贝构造函数

4.1 概念

我们在创建一个内置类型的变量时,可以用另一个变量去初始化

int b = 1;
int a = b;

对于变量,我们也想这样很方便快捷的去创建一个和已存在对象一模一样的新对象,于是就有了拷贝构造函数。

在用已存在的类类型对象去创建新对象时,编译器就会自动调用拷贝构造函数。

4.2 特性

拷贝构造函数也是特殊的成员函数,主要有以下几个特征:

(1)拷贝构造函数是构造函数的一个重载形式

(2)拷贝构造函数只有一个参数,并且必须是类对象的引用

正确写法:

用法:

 

为什么形参前要加const呢?

看看下面这种情况

如果有人粗心大意写反了,而我们又没加const,是不会报任何错误的。

并且,如果实参本身就是被const修饰的,而形参又没加const,就会造成权限放大。

为什么形参一定要用引用呢?传值传参不行吗? 

如果对于自定义类型的对象,我们在拷贝构造函数中使用传值传参的话,会引发无限递归。

我们知道,传值传参时,形参是实参的拷贝

编译器对于内置类型直接进行浅拷贝,即按字节拷贝;对于更复杂的自定义类型,编译器不敢擅自拷贝,此时就需要调用类的拷贝构造函数

对于自定义类型为什么不能浅拷贝呢?例如栈这种类型,其内部存放的是指向空间的指针,我们对栈进行拷贝,想要的效果是两个栈各自拥有自己的空间,空间内的元素相同;如果我们对其进行浅拷贝的话,就只是对地址进行了拷贝,两个栈就指向了同一块空间,最后两个栈调用析构函数时就对这块空间清理了两遍,程序会崩溃。

但是如果你的拷贝构造函数中使用了传值传参,那么就会造成死递归,即拷贝构造需要传值传参,但每次传值传参又要调用拷贝构造。

(3)若用户未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数只会进行浅拷贝(值拷贝),即按字节拷贝

我们把日期类中显式定义的拷贝构造函数删除,看看编译器生成的默认拷贝构造函数是否有效

由于日期类中只有内置类型的成员变量,所以使用编译器生成的默认拷贝构造也可以

所以,如果类中没有涉及资源申请时,可以选择用编译器生成的默认拷贝构造;一旦涉及到资源申请时,就一定要自己写拷贝构造函数。

 


五、赋值运算符重载

5.1 运算符重载

概念

我们可以使用大于号、小于号等运算符来比较两个内置类型的变量;但是对于像日期类的对象而言,当我们想比较两个日期时,无法使用这些运算符来进行比较

因此,C++引入了运算符重载

运算符重载是具有特殊函数名的函数,这里要提到关键字operator

函数名:operator + 待重载的运算符

例如我要对相等运算符==进行重载,相等返回true,不相等返回false,那么函数名就是

bool operator==(参数列表)

 

特性

(1)我们只能在operator后加上已经存在的运算符进行重载,不能凭空创造一个新的操作符

例如你不能写一个operator@并定义它,不存在名为@的运算符

(2)关于运算符重载的参数和参数类型:

首先我们在类中定义运算符重载函数

还是以相等运算符==为例,我们知道它有两个操作数,那么是不是意味着相等运算符的重载函数也要有两个参数呢? 

可以看到,报错的原因是参数过多

你是否忘记了一点:成员函数的第一个参数为隐式的this指针?

所以实际上的确有两个参数,但是一个是隐式的this指针,一个是显式的类类型对象

正确的函数如下:

 

如果在类外定义运算符重载函数呢?

因为类外定义的函数没有隐式的this指针,所以我们需要写两个参数

问题又来了,我们在类外无法访问私有的成员变量啊

 

这里可以用我们后面会学到的友元解决

至于相等运算符的重载函数用法如下: 

编译器会将 d1==d2 转换成 d1.operator==(d2) 

因为流插入运算符的优先级较高,我们需要用括号把待比较的对象和运算符括起来

(3)重载后的运算符,其特性应和原本的运算符一样

例如加法运算符+,我们想要实现在一个日期的基础上加上一个天数,获得一个新的日期,就需要对其进行运算符重载

我们来统计一下+的特性有哪些

  • 可以+一个负数
  • 可以进行连续的+,例如 a + b + c
  • 一个变量+一个数,变量本身不会改变

所以在重载的加法运算符函数中,我们需要对形参的正负进行判断,并且要返回日期类的对象保持连续运算的特性,还要用拷贝构造函数创建一个临时的对象来进行运算,避免修改对象本身

如果天数为负数,我们就可以调用减法运算符的重载函数(虽然这里还没实现)

因为tmp出了作用域就销毁了,所以我们无法传引用返回,只能传值返回tmp的拷贝

(4)重载运算符函数中必须至少有一个类类型的参数

重载运算符本来就是用来运算自定义类型的,如果参数全是内置类型,还有必要重载吗?

(5).*    ::    sizeof    ?:    .  这五个运算符不能重载(常在笔试题中出现)

5.2 赋值运算符重载

概念

赋值运算符重载和拷贝构造有点相似,不过一个是针对两个已经实例化好的对象,另一个是在创建对象时用已经存在的对象去初始化。

我们平时可以用一个变量赋值给另一个变量,例如

int a = 1 , b = 2 , c = 3;

a = b = c;

如果想让自定义类型的对象也能够做到这一点,就需要使用赋值运算符的重载函数

赋值运算符的重载函数格式如下:

  • 参数类型:const 类名& 参数名,用引用可以减少拷贝,提高传参效率
  • 返回值类型:类名&,返回的对象出了作用域不销毁,所以选择传引用返回
  • 检测*this是否和形参一样,避免自己给自己赋值
  • 返回*this:因为要支持连续赋值,保持运算符的特性

现在,我们可以按照上面的格式写一个日期类的赋值运算符重载函数了

 

特性

赋值运算符重载是类的6大默认成员函数之一,所以它有着一些独特的特性

(1)赋值运算符只能重载为类的成员函数,不能在类外实现

这是因为,赋值重载函数是默认成员函数,如果我们不实现,编译器就会自己生成一个。

此时如果我们再在类外去实现一个全局的赋值运算符重载,就和编译器在类中实现的赋值重载冲突了,所以赋值重载函数只能在类中实现

(2)当用户没有显式实现赋值运算符重载时,编译器会生成一个默认的,并逐字节拷贝

对于内置类型的成员变量直接拷贝即可,对于自定义类型的成员变量,则需要调用对应类的赋值运算符重载。

可以看到,在调用Date类的默认赋值重载函数时,对于自定义类型的成员变量_t,会去调用它的赋值运算符重载。

和拷贝构造函数类似的,如果类中没有涉及到资源管理,我们就可以不实现赋值重载;一旦涉及资源管理就必须要自行实现。

5.3 前置++和后置++重载

前置++的重载函数:

	Date& operator++()
	{
		_day += 1;
		return *this;
	}

this指向的对象在函数结束后不会销毁,所以可以使用传引用返回提高效率 

后置++的重载函数:

	Date operator++(int)
	{
		Date tmp(*this);
		_day += 1;
		return tmp;
	}

因为后置++是先使用后+1,要返回未+1之前的旧值,所以需要tmp来保存*this的值

tmp出了作用域后就会销毁,所以只能传值返回

后置++重载函数中的参数int没有实际作用,只是为了与前置++构成函数重载,以便区分

前置++和后置++的使用:

遇到后置++重载函数时,编译器会自动在参数中放一个整型用来匹配函数


 六、const成员

如果一个被const修饰的对象去调用一个普通的成员函数,因为this指针没有被const修饰,就会造成权限放大

针对这种情况,我们需要用const对this指针进行修饰,但是this指针是隐式的,该怎么去修饰它呢

只需要在成员函数的后面加上const即可

我们将const修饰的成员函数称为const成员函数, 看上去是修饰成员函数,实际上修饰的是隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

我们看看下面这段代码

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
	void Print() const //void Print(const Date* this)
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; 
	int _month; 
	int _day;
};

int main()
{
	Date d1(2024, 3, 2);
	d1.Print();
	const Date d2(2024, 3, 2);
	d2.Print();
	return 0;
}

其中,对象d1没有被const修饰,对象d2被const修饰,它们都调用了Print函数

而一个Print函数没有被const修饰,一个被const修饰,函数的调用情况如何呢?

可以看到,d1调用了第一个Print,d2调用了第二个被const修饰的Print

那么问题来了

1.非const对象可以调用const成员函数吗?

我们将第一个Print删除,运行程序

此时两个对象都能调用const成员函数

2.const成员函数内可以调用其他非const成员函数吗?

我们恢复第一个Print,并在第二个Print内调用第一个Print,运行程序

程序正常运行

3.非const成员函数内可以调用其他const成员函数吗?

程序正常运行

如果成员函数内部不需要修改成员变量时,都可以在后面加上const,这样普通对象和const对象都可以调用


七、取地址操作符重载

这两个默认成员函数一般不需要我们定义,编译器会默认生成。

它们起到对普通对象和const对象取地址的作用

class Date
{
public:
	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}
private:
	int _year; 
	int _month; 
	int _day; 
};

完.

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

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

相关文章

HBM(High Bandwidth Memory)

选择正确的高带宽内存 构建高性能芯片的选择越来越多&#xff0c;但附加内存的选择却几乎没有变化。为了在汽车、消费和超大规模计算中实现最大性能&#xff0c;选择取决于一种或多种 DRAM&#xff0c;而最大的权衡是成本与速度。 尽管多年来人们一直在努力用更快、更便宜或更…

【MySQL】索引(重点)-- 详解

一、索引 没有索引&#xff0c;可能会有什么问题&#xff1f; 索引 &#xff1a;提高数据库的性能&#xff0c;索引是物美价廉的东西了。不用加内存&#xff0c;不用改程序&#xff0c;不用调 sql &#xff0c;只要执行正确的 create index &#xff0c;查询速度就可能提高成…

06.QT信号和槽-1

一、信号和槽概述 在Qt中&#xff0c;用户和控件的每次交互过程称为一个事件。比如"用户点击按钮"是一个事件&#xff0c;"用户关闭窗口"也是一个事件。每个事件都会发出一个信号&#xff0c;例如用户点击按钮会发出"按钮被点击"的信号&#xff…

武汉灰京文化:多样化推广与创新引领游戏行业

作为专业的游戏推广服务商&#xff0c;武汉灰京文化注重多样化的推广策略&#xff0c;通过与各大媒体、社交平台和游戏社区建立紧密的合作关系&#xff0c;为游戏企业提供全方位的推广服务。他们通过精确的广告投放、内容创作和社交媒体互动等方式&#xff0c;将游戏信息传播给…

MySQL 核心模块揭秘 | 07 期 | 二阶段提交 (1) prepare 阶段

二阶段提交的 prepare 阶段&#xff0c;binlog 和 InnoDB 各自会有哪些动作&#xff1f; 本文基于 MySQL 8.0.32 源码&#xff0c;存储引擎为 InnoDB。 1. 二阶段提交 二阶段提交&#xff0c;顾名思义&#xff0c;包含两个阶段&#xff0c;它们是&#xff1a; prepare 阶段。…

xxl-job--02--可视化界面各功能详细介绍

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 可视化界面1 新增执行器2.新增任务**执行器**&#xff1a;**任务描述**&#xff1a;**路由策略**&#xff1a;**Cron**&#xff1a;cron表达式**运行模式**JobHandl…

YOLO算法

YOLO介绍 YOLO&#xff0c;全称为You Only Look Once: Unified, Real-Time Object Detection&#xff0c;是一种实时目标检测算法。目标检测是计算机视觉领域的一个重要任务&#xff0c;它不仅需要识别图像中的物体类别&#xff0c;还需要确定它们的位置。与分类任务只关注对…

【Python】进阶学习:pandas--query()用法详解

&#x1f4da;【Python】进阶学习&#xff1a;pandas–query()用法详解 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希…

Docker知识点总结

二、Docker基本命令&#xff1a; Docker支持CentOs 6 及以后的版本; CentOs7系统可以直接通过yum进行安装&#xff0c;安装前可以 1、查看一下系统是否已经安装了Docker: yum list installed | grep docker 2、安装docker&#xff1a; yum install docker -y -y 表示自动确认…

RH850P1X芯片学习笔记-Generic Timer Module -ATOM

文章目录 ARU-connected Timer Output Module (ATOM)OverviewGLOBAL CHANNEL CONTROL BLOCK ATOM Channel architectureATOM Channel modesSOMP-Signal Output Mode PWMSOMP - ARUSOMC-Signal Output Mode CompareSOMC - ARUSOMC – COMPARE COMMANDSOMC – OUTPUT ACTIONATOM …

【如何像网吧一样弄个游戏菜单在家里】

GGmenu 个人家庭版游戏、应用管理 桌面图标管理器

【笔试强训错题选择题】Day5.习题(错题)解析

文章目录 前言 错题题目 错题解析 总结 前言 错题题目 1. ​ ​ 2. 3. ​ 4. ​ 5. ​ 错题解析 1. 移位运算符的使用 2. 3. 4. 5. 总结

应用机器学习回归离群值处理

异常值可能会破坏机器学习模型的运转&#xff0c;导致结果出现偏差并影响准确性。在这篇博文中&#xff0c;我们将深入研究应用机器学习领域&#xff0c;并探索使用 Python 识别和处理异常值的有效技 了解异常值 离群值是与数据集其余部分显着偏差的数据点。它们可能是错误、异…

土壤类型数据

国家地球系统科学数据中心

3、Linux-命令提示符与常用命令(一)

目录 一、命令提示符 二、命令格式 三、常用命令&#xff08;一&#xff09; 0、clear&#xff1a;清空终端窗口的内容。 1、ls&#xff1a;列出当前目录或指定目录下的文件和子目录 2、pwd&#xff1a;显示当前所在工作目录的完整路径。 3、cd&#xff1a;切换目录。 …

【MySQL】深入解析 Buffer Pool 缓冲池

文章目录 1、前置知识1.1、Buffer Pool介绍1.2、后台线程1.2.1、Master Thread1.2.2、IO Thread1.2.3、Purge Thread1.2.4、Page Cleaner Thread 1.3、重做日志缓冲池 2、Buffer Pool 组成2.1、数据页2.2、索引页2.3、undo页2.4、插入缓冲2.5、锁空间2.6、数据字典2.6、自适应哈…

金三银四求职攻略:如何在面试中脱颖而出

随着春天的脚步渐近&#xff0c;对于众多程序员来说&#xff0c;一年中最繁忙、最重要的时期也随之而来。金三银四&#xff0c;即三月和四月&#xff0c;被广大程序员视为求职的黄金时段。在这段时间里&#xff0c;各大公司纷纷开放招聘&#xff0c;求职者们则通过一场又一场的…

搜索算法(算法竞赛、蓝桥杯)--双向DFS+二分查找

1、B站视频链接&#xff1a;B26 双向DFS 送礼物_哔哩哔哩_bilibili #include <bits/stdc.h> using namespace std; int n,m; int g[46];//存储所有物品的质量 int w[1<<23];//存储所有能凑出来的重量 int ans,cnt;//w的个数是cnt//搜索第u个数&#xff0c;和为s; …

Java数据类型(八种基本数据类型 + 四种引用类型)、数据类型转换

1.总览 Java的数据类型只有两大类&#xff1a;8大基本数据类型与引用数据类型。其中基本数据类型又被称为值类型 基本数据类型&#xff1a;6种数字类型&#xff08;byte/short/int/long/float/double&#xff09;、1种字符型&#xff08;char&#xff09;、1种布尔型&#xff…

Java中常见延时队列的实现方案总结

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&…