【C++记忆站】类和对象(二)

news2024/11/24 22:40:03

类和对象(二)

如果一个类中什么成员都没有,简称为空类
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

在这里插入图片描述

1. 构造函数

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

构造函数的主要任务是初始化对象

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

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

	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void TestDate()
{
	Date d1; // 调用无参构造函数
	Date d2(2024, 1, 21); // 调用带参的构造函数

	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
	// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	Date d3();
}

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	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 d;
	return 0;
}

注意
● 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
● 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
● 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用

不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
C++把类型分成 内置类型(基本类型)自定义类型
● 内置类型就是语言提供的数据类型,如:int/char…
● 自定义类型就是我们使用class/struct/union等自己定义的类型

看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数

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

class Date
{
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	//自定义类型
	Time _t;
};

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

内置类型成员变量在类中声明时可以给默认值
所以Time类里面的_hour、_minute、_second 都会被初始化为0

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

注意 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数

2. 析构函数

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

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

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}

下面来看一下默认析构函数

class Time
{
public:
	~Time()
	{
		std::cout << "~Time()" << std::endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

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

程序运行结束后输出:~Time() 在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类生成的默认析构函数

注意 :创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

3. 拷贝构造函数

3.1 概念

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

class Date
{
public:
	Date(int year = 1, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024,1,22);
	Date d2(d1);

	return 0;
}

3.2 特性

一、拷贝构造函数是构造函数的一个重载形式

二、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
在这里插入图片描述
要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用

在这里插入图片描述
注意 自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但每次传参时都会调用拷贝构造函数,引发无穷递归

三、若未自己定义拷贝构造函数,系统将生成默认的拷贝构造函数

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)// 构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024, 1, 22);
	Date d2(d1); // 用已存在的对象d1创建对象d2
	d1.Print();
	d2.Print();
	return 0;
}

程序结果
在这里插入图片描述
可以从结果看出编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造

编译器自动生成的拷贝函数和自己写的拷贝函数有什么区别?
这就要知道编译器自动生成的拷贝构造函数机制

1. 编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)
2. 对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数

四、编译器自动生成的拷贝构造函数不能实现深拷贝

浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2,所以说浅拷贝也叫做值拷贝,但某些场景下浅拷贝并不能达到我们想要的效果。
例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_ps = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		std::cout << _ps << std::endl;// 打印栈空间地址
	}
private:
	int* _ps;
	int _size;
	int _capacity;
};

int main()
{
	Stack s1;
	s1.Print();// 打印s1栈空间的地址
	Stack s2(s1);// 用已存在的对象s1创建对象s2
	s2.Print();// 打印s2栈空间的地址
	return 0;
}

看看以上代码运行结果
在这里插入图片描述
结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈
而且这种情况下,还会出现对同一块空间释放多次的问题。若我们自己定义的析构函数是正确的情况下,当程序运行结束,s2栈将被析构,此时那块栈空间被释放,然后s1栈也要被析构,再次对那一块空间进行释放
总结

  • 1 像Date这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,我们不需要自己写。
  • 2 像Stack这样的类,浅拷贝会导致析构两次、程序崩溃等问题,需要我们自己写对应的拷贝构造函数。

4. 赋值运算符重载

4.1 运算符重载

C++为了增强代码的可读性了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

  • 函数名字为:关键字operator后面接需要重载的运算符符号
  • 函数原型:返回值类型 operator操作符(参数列表)

注意

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。

重载 == 运算符作为例子:

我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		std::cout << _year << "年" << _month << "月" << _day << "日";
	}
	bool operator==(const Date& d1)
	{
		return _year == d1._year && _month == d1._month && _day == d1._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

4.2 赋值运算符重载

以重载 = 运算符作为例子

class Date
{
public:
	Date(int year = 1, int mouth = 1, int day = 1)
	{
		_year = year;
		_month = mouth;
		_day = day;
	}
	void Print()
	{
		std::cout << _year << "年" << _month << "月" << _day << "日";
	}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

总结
一、参数类型设置为引用,并用const进行修饰

  • 赋值运算符重载函数的第一个形参默认是this指针,第二个形参是赋值运算符的右操作数。
  • 由于是自定义类型传参,若使用传值传参,会额外调用一次拷贝构造函数,所以最好使用引用传参作为函数的第二个参数(第一个参数是默认的this指针,我们无法改变)。
  • 其次,第二个参数,即赋值运算符的右操作数,在函数体内不会被修改,所以最好加上const修饰。

二、函数的返回值使用引用返回

  • 为了避免不必要的拷贝,我们最好还是使用引用返回

三、赋值前检查是否是给自己赋值

  • 判断是否是给自己赋值,避免不必要的赋值操作。

四、引用返回的是*this

  • 赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。

五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

  • 赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
  • 对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是需要自己编写赋值运算符函数。

5. const成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

  • 例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:
void Print()const// cosnt修饰的打印函数
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

6. 取地址及const取地址操作符重载

取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了:

class Date
{
public:
	Date* operator&()// 取地址操作符重载
	{
		return this;
	}
	const Date* operator&()const// const取地址操作符重载
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

在这里插入图片描述

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

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

相关文章

使用DTS实现TiDB到GaiaDB数据迁移

1 概览 本文主要介绍通过 DTS 数据迁移功能&#xff0c;结合消息服务 for Kafka 与 TiDB 数据库的 Pump、Drainer 组件&#xff0c;完成从TiDB迁移至百度智能云云原生数据库 GaiaDB。 消息服务 for Kafka&#xff1a;详细介绍参见&#xff1a;消息服务 for Kafka 产品介绍百度智…

Ubuntu Desktop 隐藏 / 显示文件和文件夹

Ubuntu Desktop 隐藏 / 显示文件和文件夹 1. GUI hot key2. Show hidden and backup filesReferences 1. GUI hot key Ctrl H: 隐藏 / 显示文件和文件夹 2. Show hidden and backup files Edit -> Preferences -> Views References [1] Yongqiang Cheng, https://yo…

AI大概不会很快抢走你的饭碗哦!

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

交互式AI百舸争流,声通科技要再次破题实现IPO?

仅隔半年&#xff0c;声通科技两次递表可以窥见其上市势在必行的决心。 事实也确实如此&#xff0c;由于对赌期限在即&#xff0c;声通科技上市迫在眉睫。 招股书显示&#xff0c;若声通科技未能于2024年12月31日之前完成合资格首次公开发售。那么声通科技及其创始人将赎回已…

Redis服务端优化(持久化配置、慢查询、命令及安全配置、内存配置)

文章目录 持久化配置慢查询命令及安全配置内存配置 持久化配置 慢查询 命令及安全配置 漏洞&#xff1a;Redis未授权访问配合SSH key文件利用分析-腾讯云开发者社区-腾讯云 (tencent.com) 漏洞出现的核心的原因有以下几点 Redis未设置密码利用了Redis的config set命令动态修…

Go 基本数据

第 2 章 基本数据类型 Go 的数值类型包括了不同大小的整数 、浮点数 、复数&#xff1b; 各种数值类型分别有自己的大小&#xff0c;对正负号支持也各不相同&#xff1b; 1. 整数&#xff08;OK&#xff09; 整数类型&#xff08;整型&#xff09;整数类型Go 语言同时支持 有…

VUE---插槽

一、插槽的作用&场景 1、在封装组件的时候&#xff0c;将可变的结构设计为插槽&#xff08;<slot></slot>&#xff09; 2、使用上述组件的时候&#xff0c;可以按需为插槽提供自定义的结构&#xff0c;以达到复用组件且高度自定的效果 二、基本语法 1、组件内…

关于网络安全 的 ARP欺骗 实验操作

实验设备&#xff1a; Windows server 2008 kali 1. vmware--上面菜单栏--虚拟机--设置--网络--NAT 模式 确定靶机与攻击机的连通性&#xff08;互相能 ping 通&#xff09; 靶机查看 arp 表&#xff08;arp -a&#xff09; 查看攻击机(kali)物理地址&#xff08;ip addr&…

MIT 6s081 lab4.xv6进程调度

xv6进程调度 在xv6中&#xff0c;调度发生的两种情况&#xff1a; 时钟中断导致的进程切换&#xff08;也叫时间片轮转&#xff09;睡眠锁&#xff0c;当进程调用sleep时&#xff0c;发生cpu的调度 xv6进程相关概念 xv6用struct proc来描述进程 // Per-process state stru…

uvicorn日志清空问题以及uvicorn日志配置

uvicorn日志清空问题 1、配置&#xff1a; uvicorn starlette 2、现象描述&#xff1a; 当我使用uvicorn starlette进行Python web开发的时候&#xff0c;本来想把所有的日志都打印到一个文件里面&#xff0c;于是我写了一个启动脚本&#xff0c;所有的日志都输出到log.t…

最大流-Dinic算法,原理详解,四大优化,详细代码

文章目录 零、前言一、概念回顾(可略过)1.1流网络1.2流1.3最大流1.4残留网络1.5增广路径1.6流网络的割1.7最大流最小割定理1.7.1证明 1.8Ford-Fulkerson方法 二、Dinic算法2.1EK算法的可优化之处2.2Dinic算法的优化策略2.3Dinic算法原理2.3.1找增广路2.3.2更新剩余容量 2.4算法…

浏览器无网

目录 1.运行网络诊断&#xff0c;确认原因 原因A.远程计算机或设备将不接受连接(该设备或资源(Web 代理)未设置为接受端口“7890”上的连接 原因B.DNS服务器未响应 场景A.其他的浏览器可以打开网页&#xff0c;自带的Edge却不行 方法A&#xff1a;关闭代理 Google自带翻译…

<C++>STL->vector

vector的介绍 vector的使用文档 vector是一个可改变数组大小的序列容器vector和数组一样采取连续的空间存放数据&#xff0c;可以使用方括号访问vector的元素&#xff0c;和数组一样高效。但是vector的大小可以动态增长&#xff0c;而数组不行实际上vector内部使用一个动态分…

MySQL-SQL-DQL

DQL-介绍 DQL-语法 基本查询 1、查询多个字段 2、设置别名 3、去除重复记录 条件查询 1、语法 2、条件 聚合函数 1、介绍 2、常见的聚合函数 3、语法 分组查询 1、语法 2、where与having区别 排序查询 1、语法 2、排序方式 分页查询 1、语法 DQL-执行顺序

多维时序 | Matlab实现WOA-TCN-Multihead-Attention鲸鱼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测

多维时序 | Matlab实现WOA-TCN-Multihead-Attention鲸鱼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测 目录 多维时序 | Matlab实现WOA-TCN-Multihead-Attention鲸鱼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测效果一览基本介绍程序设计参考资料 效…

C++大学教程(第九版)6.29素数

题目 (素数)素数是只能被1和自已整除的整数。例如,235和7是素数而468和9不是素数 a)编写一个函数&#xff0c;确定一个数是否是素数。 b)在程序中使用这个函数&#xff0c;该程序确定和打印2 ~10000之间的所有素数。在确信已找到所有的素数之前&#xff0c;实际需测试这些数中…

五邑大学餐厅网络点餐系统设计与实现(包含完整源码详细开发过程)

博主介绍&#xff1a;✌专研于前后端领域优质创作者、本质互联网精神开源贡献答疑解惑、坚持优质作品共享、掘金/腾讯云/阿里云等平台优质作者、擅长前后端项目开发和毕业项目实战&#xff0c;深受全网粉丝喜爱与支持✌有需要可以联系作者我哦&#xff01; &#x1f345;文末获…

你不知道的git如何撤销回退版本

简言之&#xff1a;从1 回退到 3&#xff0c;在3版本通过回退记录(git reflog)找到它的上一条回退记录的hash值&#xff0c;复制1的hash值进行回退&#xff0c;执行git reset --hard 粘贴1的hash值进来&#xff0c;此时就回到1的版本了&#xff0c;执行git log即可看到1、2、3、…

安装pytorch GPU的方法,一次安装成功!!win10、win11皆可用!!

前提—查看是否有NVIDIV英伟达显卡&#xff01;&#xff01; 在控制面板打开设备管理器 一、查看电脑的显卡驱动版本 方法一&#xff1a;在cmd命令窗口中输入nvidia-smi&#xff0c;可以发现版本为12.2 方法2&#xff1a;点击NVIDIA控制面板→系统信息 二、安装CUDA 方法1…

算法(4)——前缀和

目录 一、前缀和的定义 二、一维前缀和 三、一维前缀和OJ题 3.1、前缀和 3.2、寻找数组中心下标 3.3、除自身以外数组的乘积 3.4、和为K的数组 3.5、和可被K整除的子数组 3.6、连续数组 四、二位前缀和 4.1、二维前缀和 4.2、矩阵区域和 一、前缀和的定义 对于一个…