《类和对象:基础原理全解析(下篇)》

news2025/1/5 14:53:11

目录

  • 一、类的构造函数的初始化列表
    • 1. 初始化列表的使用
    • 2. 初始化列表的初始化顺序
    • 3. 使用初始化列表的注意事项
  • 二、类的自动类型转换
    • 1. 类的自动类型转换的使用
    • 2. 关闭类的自动类型转换
  • 三、静态类成员
    • 1. 静态成员的特性
    • 2. 使用静态成员计算类创建了多少个对象
    • 3. 使用静态类成员的注意事项
  • 四、友元函数
    • 1. 友元函数的使用
    • 2. 使用友元函数的注意事项

一、类的构造函数的初始化列表

  使用函数时需要传递对应的参数,而在传参的过程中就已经完成了形参的创建并使用实参对其初始化。而构造函数不同于其他函数,因为构造函数是在创建类对象时编译器自动调用的,我们传递的参数只是用来给形参初始化,然后把形参赋值给类对象的成员变量。而在进入函数体时,类对象的成员变量就已经创建好了但并没有初始化,此时类对象的成员变量中存储的都是垃圾值(上一次使用遗留的值),只有在执行完构造函数之后,该类对象的成员变量中存储的才是我们想要的值。

  说的直白一点,在构造函数的函数体中对类对象成员变量进行修改都是赋值,不是初始化。但是有的成员变量必须初始化,如:常量成员变量(const 成员)和引用成员等。而针对这些成员就需要使用构造函数的初始化列表,在初始化列表中可以对类的成员变量进行初始化。

1. 初始化列表的使用

  初始化列表的使用方法是在构造函数定义的圆括号后面使用冒号(:),然后列出需要初始化的成员变量,中间用逗号(,)隔开,且圆括号中是用来初始化的值。

// A 类声明
class A
{
private:
	const int a;  // const 成员变量
	const int& ra;  // 引用成员变量

public:
	A(int b, int c);
};

// A 类的构造函数
A::A(int b, int c)
	: a(b)
	, ra(c)
{

&esmp; 虽然上述的初始化列表使用正确了,但是成员变量 ra 引用了一个局部变量,出了该函数以后会导致 ra 成为野引用。

2. 初始化列表的初始化顺序

  观察下面的 Stakc 类的构造函数:

// 类型声明
typedef int DataType;

// Stack 类声明
class Stack
{
private:
	DataType* _pdata;
	size_t _top;
	size_t _capacity;

public:
	Stack(size_t capacity = 4);
	void push(const DataType& data);
};

Stack::Stack(size_t capacity)
	: _capacity(capacity)
	, _top(1)
	, _pdata((DataType*)malloc(sizeof(DataType)*_capacity))
{

}

void Stack::push(const DataType& data)
{
	// 入栈
	_pdata[_top++] = data;
}

  如果按照构造函数初始化列表中的初始化顺序来说,该构造函数没有问题。但是如果进行调试的话,可以发现初始化的顺序与之并不一致。
(1)刚进入构造函数
  作者使用的是 VS2022,编译器自动把成员变量设置为 0 了。
在这里插入图片描述
(2)初始化第一个成员变量
  可以看到下面的代码中第一个初始化的是成员变量 _pdata,并不是构造函数定义中的 _capacity。
在这里插入图片描述
(3)初始化剩下两个成员变量
在这里插入图片描述
在这里插入图片描述
  通过对上面调试结果的分析,可以得出初始化列表的初始化顺序并不是按照构造函数中的顺序进行初始化的,而是按照类声明中成员变量的声明顺序进行初始化的。

  这样就会产生一个问题,如果先初始化 _pdata,但是此时 _capacity 还没有初始化,它的值可能会是随机值(并不是所有的编译器都会在进入构造函数的瞬间把成员变的值设置为 0),这样就会开辟一个随机大小的空间,很容易造成严重的问题。

  所以,初始化列表中的初始化顺序需要和类声明中成员变量的声明顺序一致。防止由于初始化的顺序和预期的顺序不一致,从而产生难以预估的影响。

3. 使用初始化列表的注意事项

(1)对于下面四种类型必须使用初始化列表:常量成员变量(const 成员)、引用成员变量、没有默认构造函数的自定义成员变量和基类构造(基类在后面的博客中会介绍);

  当自定义类型作为其他类的成员变量时,该类的构造函数必须在初始化列表中使用该自定义类型的构造函数对该自定义类型初始化。如果不在初始化列表中显式使用,那么编译器会在初始化列表中隐式调用其默认构造函数。如果该自定义类型没有默认构造函数且又没有在初始化列表中显式调用其构造函数,编译器会报错。

  为什么必须在初始化列表中调用自定义类型的构造函数?
1)首先只要创建类对象就必须调用其构造函数,这是 C++ 规定;
2)其次该类对象也可能包含 const 成员变量或者引用等必须初始化的成员变量,这些成员变量必须在它们自己类的构造函数中通过初始化列表进行初始化;
3)最后,该自定义类型可能涉及动态内存开辟,使用它自己的构造函数可以正确地开辟动态内存。

(2)初始化列表的初始化顺序应该和类声明中的成员变量的声明顺序保持一致,避免出现由于初始化顺序产生的错误;

(3)尽量使用初始化列表,哪怕不显式使用初始化列表,编译器也会把所有的成员变量在初始化列表中走一遍,因为初始化列表是成员变量的创建过程。并且一般情况下,使用初始化列表的效率更高。

二、类的自动类型转换

  对于自动类型转换相信大家都不陌生,如:int a = 1.1;,该语句就是常见的 double 值自动类型转换为 int 值,只不过要默认舍弃后面的小数。

  而在类中也存在类似的自动类型转换,在类中,把只有一个参数的构造函数用来自动类型转换。

1. 类的自动类型转换的使用

  下面依旧是 Stack 类的声明:

// 类型声明
typedef int DataType;

// Stack 类声明
class Stack
{
private:
	size_t _capacity;
	size_t _top;
	DataType* _pdata;

public:
	Stack(size_t capacity = 2);
};

Stack::Stack(size_t  capacity)
	: _capacity(capacity)
	, _top(0)
	, _pdata(nullptr)
{
	_pdata = (DataType*)malloc(sizeof(DataType) * capacity);
	// 申请判断
	if (nullptr == _pdata)
	{
		perror("Stack::Stack::malloc: ");
		return;
	}
}

  再来看下面的 Stack 类对象的创建语句:
在这里插入图片描述
  为什么该语句可以通过编译?首先,编译器检测到该语句的右侧需要 Stack 类型的值,然后就会尝试把 int 类型的常量值 2 转换为 Stack 类型。而我们编写的 Stack 类的构造函数刚好包含一个 size_t 的参数,且 int 类型转换成 size_t 类型(满足隐式转换条件)。所以,编译器会先把 int 类型的 2 隐式转换成 size_t 类型,然后用这个 size_t 类型的 2 去构造一个 Stack 类型的临时对象,再用这个临时的 Stack 类型的对象去拷贝构造 sk1。

  但是现在的编译器很智能,编译器会优化上述的构造过程。编译器会把 2 转换为 size_t 类型之后直接去构造 sk1,因为编译器觉得调用两次构造函数太浪费了。当在一条语句中使用两次构造函数时,编译器会尝试把这个过程优化成一次构造函数。

2. 关闭类的自动类型转换

  自动类型转换有时候也会带来问题,比如当我们误写了 Stack sk1 = 10; 这种语句时,编译器并不会报错,而是进行隐式类型转换从而构造出一个容量为 10 的 Stack 类对象,因为我们编写了一个只包含一个 size_t 类型参数的构造函数——Stack(size_t capacity);。

  可以在类的只有一个参数的构造函数的声明之前加上 explicit 关键字来关闭自动类型转换的特性。
在这里插入图片描述
  可以看到当我们在只有一个参数的构造函数的声明前面加上了 explicit 关键字之后,该类的自动类型转换特性就消失了,无法从 int 类型转换为 Stack 类型。

  通常情况下不建议使用类的自动类型转换,因为这样如果代码错了,把一个 int 值用来初始化或者赋值给 Stack 类对象,编译器不会报错,而是会进行自动类型转换。

三、静态类成员

  如果我们想要计算一个类创建了多少个类对象,那么常规的方法肯定是创建一个全局变量或者静态变量,然后再构造函数中对该变量 +1,在析构函数中对该变量 -1。

  但是这样会破坏类的封装特性,因为不管是全局变量还是静态变量都可以被直接访问,这样很不安全。这时就需要使用类的静态成员(static 成员)。

  在类成员前面加上 static 就可以使该成员成为静态成员,成员变量前面加上 static 就变成了静态成员变量,成员函数前面加上 static 就成为了静态成员函数。

1. 静态成员的特性

(1)静态成员只有一个实例,被所有类对象共享,存放在静态区;
(2)静态成员必须在类外定义,定义时不添加关键字,类中只是声明;
(3)静态成员可以通过类名::静态成员或者对象.静态成员来访问;
(4)静态成员函数没有 this 指针,不能访问任何其他非静态成员;
(5)静态成员也是类成员,受 private、public 和 protected 访问限定符的限制。

2. 使用静态成员计算类创建了多少个对象

  下面通过静态成员变量和静态成员函数计算 A 类创建了多少个对象。

// A 类声明
class A
{
private:
	static int _num_object;
	int _a;

public:
	A(int a = 0);
	~A();
	// 静态成员函数
	static int GetObjectNum();
};

// A 类静态成员变量定义
int A::_num_object = 0;

// A 类成员函数定义
A::A(int a)
{
	_a = a;
	// 对象数量 +1
	++_num_object;  // 实际上是 ++this->_num_object
}

A::~A()
{
	// 对象数量 -1
	--_num_object;
}

int A::GetObjectNum()
{
	cout << "当前对象个数: " << _num_object << endl;

	return _num_object;
}

int main()
{
	{
		A a1;
		A::GetObjectNum();
		{
			A a2;
			A::GetObjectNum();
			A a3;
			A::GetObjectNum();
		}
		A a4;
		A::GetObjectNum();
	}
	A::GetObjectNum();

	return 0;
}

  程序运行结果如下:
在这里插入图片描述
  上述代码通过把静态成员变量 _num_object 放在私有(private)中实现了封装,只能通过公有的静态成员函数访问。

  且静态函数中没有 this 指针,不能访问类的其他非静态成员变量。
在这里插入图片描述

3. 使用静态类成员的注意事项

(1)静态类成员变量在类中声明(前面加 static),在类外定义(不加 static,需要加类名::限定)。静态类成员函数也是如此;

(2)公有的静态成员可以直接通过类名和作用域解析运算符进行访问,或者通过类对象进行访问,但是建议使用类名和作用域解析运算符进行访问;
  因为静态类成员属于整个类,被所有对象共享,使用类名::来进行访问更加能提现其是一个静态成员。

(3)由于静态类成员函数不包含 this 指针,所以不能访问其他非静态成员变量;

(4)通常把静态成员变量放在私有区域,然后通过公有区域的静态类成员函数进行访问。

四、友元函数

  友元函数在类中声明,在类外定义。它不是类的成员函数,但是可以访问类的私有成员和保护成员。因为友元函数不是类的成员函数,所以它没有 this 指针,需要把类对象当作参数传递。友元函数不受访问限定符的限制,可以在类的任何地方定义,但是需要在前面加上关键字 friend。但是在类外定义时不需要加上 friend 关键字,也不需要使用类名::限定,因为友元函数不属于类的成员函数。

  为什么需要友元函数?比如在 Date 类中,我们想要直接使用流插入运算符(<<)打印 Date 类对象,那么我们就必须重载流插入运算符(<<)。但是这里有个问题,如果把流插入运算符当作类的成员函数,那么就需要把类对象放在前面,如:

Date d1(2025, 1, 2);
d1 << cout;

  我们平常都习惯把 cout 放在前面,如果像上面这样使用就导致可读性很差,所以我们需要使用友元函数来把 cout 放在前面。

1. 友元函数的使用

  下面使用友元函数重载了加法运算符和流插入运算符。

// Date 类声明
class Date
{
private:
	size_t _year;
	size_t _month;
	size_t _day;

public:
	Date(size_t year = 1949, size_t month = 10, size_t day = 1);
	size_t GetMonthDay() const;
	// 友元函数
	friend Date operator+(size_t days, const Date& date);
	friend std::ostream& operator<<(std::ostream& os, const Date& date);
};

// Date 类成员函数定义
Date::Date(size_t year, size_t month, size_t day)
	: _year(year)
	, _month(month)
	, _day(day)
{

}

size_t Date::GetMonthDay() const
{
	// 十二个月份的天数
	static const size_t MONTH_DAYS[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	// 闰年判断
	if ((2 == _month) && ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0)))
		return 29;
	else
		return MONTH_DAYS[_month];
}

// 友元函数
Date operator+(size_t days, const Date& date)
{
	Date tmp(date);
	// 计算
	tmp._day += days;
	while (tmp._day > tmp.GetMonthDay())
	{
		// 减去当月的天数
		tmp._day -= tmp.GetMonthDay();
		// 12 月
		if (12 == tmp._month)
		{
			++tmp._year;
			tmp._month = 1;
		}
		else  // 其他月
		{
			++tmp._month;
		}
	}
	// 返回
	return tmp;
}

std::ostream& operator<<(std::ostream& os, const Date& date)
{
	os << date._year << "-" << date._month << "-" << date._day;
	return os;
}

int main()
{
	Date d1(2025, 1, 2);

	cout << d1 << endl;
	cout << "10000 天以后: ";
	cout << 10000 + d1 << endl;

	return 0;
}

  程序的运行结果如下:
在这里插入图片描述
  可以看到使用了友元函数之后,不仅可以直接使用 cout 和流插入运算符直接打印日期类对象,还可以实现数字在前和类对象相加。

2. 使用友元函数的注意事项

(1)友元函数在类内声明,声明时前面加上关键字 friend,在类外定义,定义时前面不加关键字 friend;

(2)友元函数可以直接访问类的私有成员和保护成员,但是友元函数不是类的成员函数;

(3)由于友元函数不是类的成员函数,所有定义时不需要使用类名::限定;

(4)友元函数不受访问限定符的限制,可以在类的任意位置声明;友元函数可以重载;

(5)尽量少使用友元函数,因为友元函数破坏了类的封装性。

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

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

相关文章

分析服务器 systemctl 启动gozero项目报错的解决方案

### 分析 systemctl start beisen.service 报错 在 Linux 系统中&#xff0c;systemctl 是管理系统和服务的主要工具。当我们尝试重启某个服务时&#xff0c;如果服务启动失败&#xff0c;systemctl 会输出错误信息&#xff0c;帮助我们诊断和解决问题。 本文将通过一个实际的…

Crosslink-NX应用连载(12):如何复用特殊功能管脚

作者&#xff1a;Hello,Panda 大家早上好。 昨天有朋友私信我&#xff0c;如何复用Crosslink-NX的特殊功能引脚如PROGRAMN、DONE、INITN诸如这些。熊猫君在这里简单介绍下&#xff1a; 以LIFCL-33U-8CTG104C为例&#xff0c;我们建立一个简单的指示灯LED周期闪烁的工程&…

go项目使用gentool生成model的gen.go问题

Gen Tool 是一个没有依赖关系的二进制文件&#xff0c;可以用来从数据库生成结构。 使用方法&#xff1a; go install gorm.io/gen/tools/gentoollatest在项目根目录,执行连接的数据库中指定某几张表结构生成数据库model层 gentool -dsn "root:123456tcp(localhost:330…

家政上门小程序如何创建?家政服务怎么能少了小程序帮手

在如今这个“忙到没时间打扫”的时代&#xff0c;家政服务变得越来越受欢迎。为了提高效率、减少沟通成本&#xff0c;很多家政公司都已经开始借助小程序的力量。那么&#xff0c;家政上门小程序到底该如何创建呢?小程序又是如何帮助家政服务更好地满足客户需求的呢?本文将为…

破解密码

rhel8/centos8 重置 root 密码 方法 1 &#xff1a; rd.break 第 1 步 重启系统&#xff0c;在下图所示界面按 e 键 第2步 找到linux这行&#xff0c;末尾空格后 输入 rd.break 第3步 查看&#xff0c;可选步骤 这里 sysroot 是以只读的形式挂载的&#xff0c;所以要以可读可…

本地小主机安装HomeAssistant开源智能家居平台打造个人AI管家

文章目录 前言1. 添加镜像源2. 部署HomeAssistant3. HA系统初始化配置4. HA系统添加智能设备4.1 添加已发现的设备4.2 添加HACS插件安装设备 5. 安装cpolar内网穿透5.1 配置HA公网地址 6. 配置固定公网地址 前言 大家好&#xff01;今天我要向大家展示如何将一台迷你的香橙派Z…

自学记录鸿蒙API 13:实现多目标识别Object Detection

起步&#xff1a;什么叫多目标识别&#xff1f; 无论是生活中的动物识别、智能相册中的场景分类&#xff0c;还是工业领域的检测任务&#xff0c;都能看到多目标识别的身影。这次&#xff0c;我决定通过学习HarmonyOS最新的Object Detection API&#xff08;API 13&#xff09…

javaEE-多线程进阶-JUC的常见类

juc:指的是java.util.concurrent包&#xff0c;该包中加载了一些有关的多线程有关的类。 目录 一、Callable接口 FutureTask类 参考代码&#xff1a; 二、ReentrantLock 可重入锁 ReentrantLock和synchronized的区别&#xff1a; 1.ReentantLock还有一个方法&#xff1a…

fpga系列 HDL:ModelSim显示模拟波形+十进制格式数值(临时方法和设置持久化的默认值)

模拟波形 FPGA中使用数字滤波器时&#xff0c;可通过观察模拟波形更好地查看滤波效果。可以通过ModelSim中的波形格式设置来实现更直观的波形显示。右键波形->Format-> Analog 效果 数值格式显示 不同的数值格式显示&#xff1a;右键波形->Radix-> Decimal 效果…

Linux 中 sysctl 和 systemctl 有什么区别?

sysctl 和 systemctl 是两个不同的命令行工具&#xff0c;它们在 Linux 系统中分别用于不同的目的。理解这两个命令的区别对于系统管理和配置非常重要。 1. sysctl 功能 用途&#xff1a;sysctl 用于动态地修改内核参数&#xff0c;这些参数控制着操作系统的某些行为。配置文…

【ArcGISPro/GeoScenePro】检查并处理高程数据

数据 https://arcgis.com/sharing/rest/content/items/535efce0e3a04c8790ed7cc7ea96d02d/data 数字高程模型 (DEM) 是一种栅格,可显示地面或地形的高程。 数字表面模型 (DSM) 是另一种高程栅格,可显示表面的高度,例如建筑物或树冠的顶部。 您需要准备 DEM 和 DSM 以供分析…

Redis数据库主要数据结构类型

Redis数据库提供了丰富多样的数据结构类型&#xff0c;以满足不同场景下的数据存储需求。以下是Redis中的主要数据结构类型&#xff1a; 一、五种基础数据结构 字符串&#xff08;String&#xff09; 简介&#xff1a;字符串是Redis最基本的数据类型&#xff0c;可以存储字符串…

基于Springboot + vue实现的校园周边美食探索及分享平台

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

Oracle Dataguard(主库为 Oracle 11g 单节点)配置详解(1):Oracle Dataguard 概述

Oracle Dataguard&#xff08;主库为 Oracle 11g 单节点&#xff09;配置详解&#xff08;1&#xff09;&#xff1a;Oracle Dataguard 概述 目录 Oracle Dataguard&#xff08;主库为 Oracle 11g 单节点&#xff09;配置详解&#xff08;1&#xff09;&#xff1a;Oracle Data…

mapbox基础,测面功能实现

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️Turf 框架二、🍀测面功能实现1. ☘️实现思路2. ☘️代码样例一、🍀…

基于下垂控制的构网变换器功率控制【微电网变流器】【Simulink】

目录 主要内容 理论研究 整体模型 PQ计算模块 功率控制模块 PWM反馈模块 结果一览 下载链接 主要内容 该仿真针对微电网中分布式电源接入后产生的谐波影响&#xff0c;除了污染网络外&#xff0c;还会恶化微电网变流器输出电流&#xff0c;为了消除谐波影响&a…

2025差旅平台推荐:一体化降本30%

医药行业因其高度专业化的特点&#xff0c;同时在运营过程中又极为依赖供应链和销售网络&#xff0c;因此差旅管理往往成为成本控制的重要环节。本期&#xff0c;我们以差旅平台分贝通签约伙伴——某知名药企为例&#xff0c;探讨企业如何通过差旅一体化管理&#xff0c;在全流…

【漫话机器学习系列】027.混淆矩阵(confusion matrix)

混淆矩阵&#xff08;Confusion Matrix&#xff09; 混淆矩阵是机器学习中评估分类模型性能的一种工具&#xff0c;特别是在多类别分类问题中。它通过对比模型预测结果和真实标签&#xff0c;帮助我们理解模型的分类效果。 1. 混淆矩阵的结构 混淆矩阵通常是一个二维表格&am…

【AIGC】 ChatGPT实战教程:如何高效撰写学术论文引言

&#x1f4a5; 欢迎来到我的博客&#xff01;很高兴能在这里与您相遇&#xff01; 首页&#xff1a;GPT-千鑫 – 热爱AI、热爱Python的天选打工人&#xff0c;活到老学到老&#xff01;&#xff01;&#xff01;导航 - 人工智能系列&#xff1a;包含 OpenAI API Key教程, 50个…

redis的学习(二)

4 哈希表 哈希类型中的映射关系通常称为field-value&#xff0c;⽤于区分Redis整体的键值对&#xff08;key-value&#xff09;&#xff0c; 注意这⾥的value是指field对应的值&#xff0c;不是键&#xff08;key&#xff09;对应的值&#xff0c; 4.1 操作命令 hset&#xff…