【C++】类和对象(三)

news2024/11/29 20:53:58

目录

一、构造函数补充

1、初始化列表

1.1、初始化列表概念

1.2、初始化列表性质

2、explicit关键字

二、static成员

1、概念及使用

2、性质总结

三、友元

1、友元函数

2、友元类

四、内部类

五、拷贝对象时的一些编译器优化


一、构造函数补充

 在《类和对象(二)》中,我们已经学习了关于构造函数的大部分内容,然而这些内容还不足以解决全部的问题。

例如,当我们写下如下代码时,运行程序:

class Test
{
public:

private:
	int _a;
	int _b;
};

int main()
{
	Test t;

	return 0;
}

程序运行成功:

 但是我们再在成员变量中增加一个 const 类型变量时:

 程序报错了,这是因为我们在定义 const 类型变量时,必须要进行初始化。但是由于默认生成的构造函数对于内置类型不做处理,也就无法对 const 类型变量进行初始化,因此而报错。

 虽然这里的问题可以通过 C++11 新增的针对内置类型成员打的补丁来解决,即:内置类型成员变量在类中声明时可以给默认值。但是在 C++11 之前这个问题是如何解决的呢?

 为了解决这个问题,我们引入一个新的概念:初始化列表

1、初始化列表

1.1、初始化列表概念

 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表每个 "成员变量" 后面跟
一个放在括号中的初始值或表达式

形如:

class Test
{
public:
	Test()
		:_a(1)  //初始化列表
		, _b(2)
		, _c(3)
	{
        //构造函数的函数体
	}
private:
	int _a;
	int _b;
	const int _c;
};

int main()
{
	Test t;

	return 0;
}

 初始化列表是成员变量定义的地方。不管我们写没写初始化列表,编译器都会自己过一遍初始化列表。编译器会把我们在初始化列表中写了的成员变量按照我们写的来进行初始化,把我们没在初始化列表中写的成员变量初始化成默认值。

 对于普通的内置类型成员变量是可以初始化成默认值的,因为之后还可以随意修改。但是对于 const 类型的成员变量因为无法再修改,所以必须要被初始化为一个有意义的初始值。

与之同理的还有引用类型的成员变量:

 除了以上两类成员变量,还有一类成员变量也必须要放在初始化列表位置进行初始化,那就是自定义类型成员(且该类没有默认构造函数时)

 因为该类没有默认构造函数,所以在初始化时必须要传参,否则会报错。这里可以结合《类和对象(二)》中默认构造函数对于自定义类型成员变量会调用它的默认构造函数来理解,原因就是编译器会自动走一遍初始化列表,遇到自定义类型成员变量会做出对应的处理。

总结类中包含以下成员,必须放在初始化列表位置进行初始化:

  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有默认构造函数时)

 对于其他类型的成员变量,则就算不在初始化列表中写出来也不会报错。

1.2、初始化列表性质

 我们推荐尽量使用初始化列表进行初始化。因为不管你是否使用初始化列表,编译器对于所有成员变量都一定会走一遍初始化列表,并使用初始化列表进行初始化。

 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

我们来看如下代码:

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
		//构造函数的函数体
	}
	void Print() 
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

int main() 
{
	A aa(1);
	aa.Print();
}

大家可以看出最终的结果是什么吗?

 运行程序后发现 _a1 被初始化为了 1 ,但是 _a2 却被初始化为了随机值。这是为什么呢?

 原因是在初始化列表中,变量初始化的顺序并不是看在初始化列表中排列的顺序,而是看在类中声明的顺序。因为在类中是先声明的 _a2 ,所以在变量初始化时就先初始化了 _a2 。又因为在初始化列表中,_a2 是使用 _a1 来初始化的,而此时 _a1 尚且还是一个随机值,所以就导致 _d2 被初始化成了随机值。

2、explicit关键字

我们来看如下代码:

class A
{
public:
	A(int a)
		:_a1(a)
	{}
	void Print()
	{
		cout << _a1 << endl;
	}
private:
	int _a1;
};

int main()
{
	A aa1(1);
	A aa2 = 1;
	aa1.Print();
	aa2.Print();
}

在编译器中运行:

 可以发现对象 aa1 aa2 的成员变量都被初始化为了 1

 这是不是说明这两种写法的意义是一样的呢?其实不是的,第一种写法是调用了构造函数来初始化的。而第二种写法实际上是一个隐式类型转换,把整型数字 1 进行类型转换,转换成类类型存储到类类型临时变量中,再把该临时变量赋值给 aa2

写一行代码来证明一下:

A& ref = 10;

 代码报错,显示 int 无法转换为 A& 类型,这是因为临时变量具有常性,不可更改,所以我们要把 ref 改为 const 类型:

const A& ref = 10;

 程序运行成功。

 如果我们不希望发生这种隐式类型转换,则可以使用关键字: explicit

 此时,第二种写法就已经不被允许了。 explicit 修饰构造函数,将会禁止构造函数的隐式转换。

补充说明: 类型转换针对的是单参数构造函数,C++98 不支持多参数构造函数。但是在C++11 中对此进行了拓展,使多参数构造函数也支持隐式类型装换了。形如:

A aa3 = {2, 2};

二、static成员

1、概念及使用

 声明为 static 的类成员称为类的静态成员。用 static 修饰的成员变量,称之为静态成员变量。用
 static 修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

 静态成员不属于某个对象,而是属于所有对象,属于整个类。

 例如,我们实现了一个类,现在想要计算程序中创建出了多少个类对象,就可以使用静态成员变量来计算:

class A
{
public:
	A(int a = 0)
	{
		++count;
	}

	A(const A& aa)
	{
		++count;
	}

    //读取私有的成员变量 count 
	int GetCount()
	{
		return count;
	}
private:
	static int count; //声明
};

int A::count = 0; //定义初始化

int main()
{
	A aa1;
	A aa2(aa1);

	return 0;
}

 静态变量是被存放在静态区的,被所有类对象共用。又因为静态变量 count 是在类域中声明的,所以变量名也不会与外界的变量名相互冲突。

 需要注意的是,静态变量的初始化不能在类内进行,只能放在类外。这是因为 count 作为静态变量被所有对象共用,不应该在初始化列表中被初始化,在初始化列表中进行初始化的变量是单独属于某个对象的。

所以静态变量的声明放在类内,而定义是放在全局的。在全局定义的时候要加上 域名: :

 因为我们实例化了两个对象 aa1aa2 ,所以 count 的值为,所有对象都共用一个 count

 因为静态变量 count 是存放在静态区的,而不是对象内,所以蓝色方框框起的 "->" 符号没有访问到对象内部的数据,只起到了提示域名的作用,不属于解引用。具体相关知识可以参考《类和对象(一)》。


但是如果当前函数作用域内没有对象的话,使用起来就会有些麻烦,像下面这样:

 由于 main 函数中没有对象,也就无法通过对象调用 GetCount 函数来读取 count 的值。于是只能专门实例化出一个对象来读取,同时还要把读取到的 count 减去一,去掉这个我们新定义出来的没有其他实际意义的对象。


补充内容:因为我们实例化出对象 aa ,仅仅只是为了在这个地方使用一次,一次过后就不会再去使用它。所以这里可以使用一个特殊的对象:匿名对象来简化代码。

在《类和对象(二)》中,我提到过,在实例化对象时,不可以写成这种形式:

A aa();

因为编译器无法区分这段代码是一个函数的声明,还是调用默认构造函数。但是下面这种写法是可以的:

A();

意为实例化了一个匿名对象,他的特点是生命周期只存在于这一行,刚好符合我们只调用一次的需求,所以读取 count 的值时,我们也可以这样写:


 但是这样写起来的话还是太过于麻烦,也不够优雅。为了解决这个问题,我们再来学习一个东西:静态成员函数

静态成员函数是在成员函数前面使用 static 修饰。他没有 this 指针,于是我们可以直接调用该函数:

同时,由于静态成员函数没有 this 指针,也就没有办法访问非静态成员。可以说静态成员函数就是为了静态成员变量而生的。


有了上面的知识,我们来看一下下面这段代码创建了多少了对象:

void func()
{
	A aa1;
	A aa2(aa1);

	A aa3[10];
}
int main()
{
	func();
	cout << A::GetCount() << endl;	

	return 0;
}

 答案是 12 个,因为 aa3[10] 是一个容量为 10 的自定义类型数组,也就调用了 10 次构造函数。

2、性质总结

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

三、友元

 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。友元分为:友元函数友元类。

1、友元函数

 问题:现在尝试去重载 operator<< ,然后发现没办法将 operator<< 重载成成员函数。因为 cout  的输出流对象和隐含的 this 指针在抢占第一个参数的位置。 this 指针默认是第一个参数也就是左操作数了。但是实际使用中 cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。 operator>> 同理。

 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加 friend 关键字。

关于友元函数的说明: 

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

2、友元类

 除了函数可以是类的友元之外,类也可以是类的友元。友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

关于友元类的说明:

  1. 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  2. 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  3. 友元关系不能继承,在继承位置再给大家详细介绍

四、内部类

 内部类的概念:如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。

 我们写一个内部类来观察一下:

 B类定义在A类内部,但是A类对象 aa 的大小为 4 个字节,只占了一个整型的空间。这是因为A类里面只有 a ,而没有 b

 其实内部类仅仅只是定义在了另一个类的里面而已,和定义在全局并没有什么区别只是内部类受到了外面这个类的类域的限制

如果想要使用内部类,则需要在前面说明内部类的域:

A::B bb;

如果内部类是外部类的私有类型,则无法直接使用内部类:

 需要注意的是:内部类是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元类。

五、拷贝对象时的一些编译器优化

首先是我们上面介绍过的隐式类型转换:

 按照正常的逻辑,编译器会首先调用构造函数来创建一个类类型的临时变量,然后再调用拷贝构造函数使 aa1 变为类类型临时变量的拷贝。为了简化过程,编译器把拷贝构造 + 构造优化为了直接构造。

需要注意的是,这种优化只能存在于同一表达式,构造完成之后直接把获得的临时变量用于拷贝构造的情况。

适用这种情况的除了赋值、连续赋值之外,还有一些其他的情况,如传值传参:

这两行代码中,传值传参时所进行的构造 + 拷贝构造被优化为直接构造。

传引用传参则因为无需拷贝而无需优化。

当拷贝构造用于返回值时,例如以下场景:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{}
private:
	int _a;
};

A func()
{
	A aa;
	return aa;
}

int main()
{
	func();

	A aa1 = func();

	return 0;
}

按照正常逻辑,这里共需要一次构造,两次拷贝构造:

 而编译器进行优化时,会把这个过程优化为一个构造加一个拷贝构造,去除同一表达式中的冗余部分。

如果我们直接返回匿名对象时,例如:

 编译器同样会把同一表达式中多余的步骤优化掉。

 从以上各种例子中,我们可以知道在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。

 同学们需要注意的是,这里有一个容易弄混的地方:

这两种方式是不同的,第一个方框框起的代码属于拷贝构造,而第二个方框框起的属于赋值重载。编译器可以优化第一种而没有办法优化第二种。

了解了以上知识,我们日后写代码时就可以有意识的遵守三点规则:

  1. 接收返回值对象,尽量用拷贝构造方式接收,而不要赋值重载方式接收。
  2. 函数中返回对象尽量返回匿名对象。
  3. 传参时尽量使用传引用传参。

关于类和对象的相关知识就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢! 

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

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

相关文章

GeoTools:FeatureShapefile之CRUD操作

之前在《GIS开源框架&#xff1a;ArcGIS文件地理数据库(GDB)解析与入库》中&#xff0c;从地理数据库的角度对Feature要素进行了解释&#xff0c;接下来&#xff0c;我们将从GeoTools库的角度&#xff0c;重新认识Feature要素&#xff0c;并通过GeoTools实现Shapefile文件在Fea…

产品、技术:如何编写有效的流程文档?

流程文档是指一系列的、连续的、有规律的活动过程&#xff0c;而这些活动以特定的方式进行&#xff0c;并导致特定结果&#xff08;创造价值&#xff09;的产生。流程梳理是指围绕企业的内部要素与外部要素&#xff0c;对整个企业的业务特点和现状进行深入细致的分析、整理、提…

Fiddler使用笔记(柠檬班)

Fiddler笔记 内部功能 Replay 重放请求。 号 移除请求&#xff0c;可以通过Shift Delete来选择要保存的请求&#xff0c;其他的都删掉。 Go 断点的时候使用&#xff0c;作用是转到下一步。 Stream 流模式&#xff0c;一般不用它。 Decode 用于解码信息。通常对响应…

Ambire AdEx 2023 年路线图

Ambire AdEx 是为简化 web3 显示广告而建立的&#xff0c;领先于时代。到 2023 年&#xff0c;它将专注于服务用户需求&#xff0c;同时保持其作为区块链隐私解决方案的核心&#xff0c;反对传统的数字广告模式。 回顾 2022 年 2022 年&#xff0c;AdEx 网络处理了超过 1 亿次展…

Hudi-并发控制

并发控制 Hudi支持的并发控制 MVCC Hudi的表操作&#xff0c;如压缩、清理、提交&#xff0c;hudi会利用多版本并发控制来提供多个表操作写入和查询之间的快照隔离。使用MVCC这种模型&#xff0c;Hudi支持并发任意数量的操作作业&#xff0c;并保证不会发生任何冲突。Hudi默…

小米电视安装 Plex 打造家庭影院

背景 最近突然想重温教父&#xff0c;本来想着直接投屏就可以&#xff0c;后来看了别人搭建的基于 NAS 的家庭影院很动心&#xff0c;也想依葫芦画瓢做一个&#xff0c;跟对象申请经费的时候被拒了&#xff0c;理由是有这钱还不如开个会员直接看。 我寻思不同电影在不同的平台…

遥感反演叶面积指数 (LAI)

叶面积指数 叶面积指数&#xff08;Leaf Area Index, LAI&#xff09;是反映一个生态系统中单位面积上的叶面积综合的一半&#xff0c;是模拟陆地生态过程、水热循环和生物地球化学循环的重要参数。 本文主要介绍LAI的遥感反演方法&#xff0c;其主要分为统计方法、植被辐射传输…

传奇私服搭建网站的几种方法

搭建网站的几种方法&#xff1a;一些人&#xff0c;连简单的搭建网站都不会&#xff0c;还要请技术帮忙&#xff0c;真是牛B&#xff0c;这里简单介绍下几种办法一&#xff1a;2003系统下&#xff0c;直接使用IIS&#xff0c;这个太简单了&#xff0c;桌面上就有IIS&#xff0c…

权威报告!这五个消费趋势,告诉你如何抓住中国消费者的心和钱包

有人说2023年是消费复苏的一年&#xff0c;市场回暖趋势明显&#xff1b;也有人说之前的亏空太大&#xff0c;想要短时间追上来不太可能&#xff0c;因此2023的消费市场最多是不低迷&#xff0c;达不到火热。这可把做生意的各位老板整纠结了&#xff0c;究竟今年要不要投个大手…

mysql 跳过事务 gtid

企业生产场景mysql主从复制故障原因 企业生产场景mysql主从复制故障原因 实验一&#xff1a; 目的&#xff1a;解决主从不同步&#xff08;本例中sql线程出现问题&#xff09; 方法&#xff1a;模拟故障场景 1.在SLAVE上建立一个名为yingying数据库。…

Webstorm 代码没有提示,uniapp 标签报错

问题 项目是用脚手架创建的&#xff1a; vue create -p dcloudio/uni-preset-vue my-project 打开之后&#xff0c;添加view标签警告报错的。代码也没有提示&#xff0c;按官方说法&#xff1a;CLI 工程默认带了 uni-app 语法提示和 5App 语法提示。 但是我这里就是有问题。…

Oracle实现高可用性的工具(负载均衡/故障切换)

Oracle实现高可用性的工具&#xff08;负载均衡/故障切换&#xff09;1 Oracle RAC故障转移负载均衡2 Data Guard负载均衡-读写分离Data Guard Broker3 GDSGSM&#xff1a;连接管理工具主要功能Data Guard Broker功能是监控Data Guard状态&#xff0c;当主库异常时自动切换角色…

idea2021版本新建maven项目

首先我们需要下载maven版本(maven下载地址Maven – Download Apache Maven)&#xff0c;并且配置好maven仓库与环境变量&#xff0c;这里不细述了。打开idea选择新建项目&#xff0c;选择maven&#xff0c;效果如下图 我们选择maven-archetype-webapp类型。 下一步&#xff0c;…

4.9 内部类

文章目录1.内部类概述2.特点3.练习 : 内部类入门案例4.成员内部类4.1 练习 : 被private修饰4.2 练习 : 被static修饰5.局部内部类6.匿名内部类1.内部类概述 如果一个类存在的意义就是为指定的另一个类&#xff0c;可以把这个类放入另一个类的内部。 就是把类定义在类的内部的情…

MQ中间件概念一览

一、概述 1. 大多应用中&#xff0c;可通过消息服务中间件来提升系统异步通信、扩展解耦能力 2. 消息服务中两个重要概念&#xff1a; 消息代理&#xff08;message broker&#xff09;和目的地&#xff08;destination&#xff09; 当消息发送者发送消息以后&#xff0c;将由…

有了ChatGPT 微软对元宇宙不香了?

押注ChatGPT是微软最近的主要发力点&#xff0c;另一边&#xff0c;它开始向元宇宙业务挥出裁员“大刀”。海外消息称&#xff0c;微软解散了成立仅四个月的工业元宇宙团队&#xff0c;约100名员工被全被解雇。 这只是微软放缓元宇宙战略的长尾动作&#xff0c;此前&#xff0…

【MFC】模拟采集系统——界面设计(17)

功能介绍 启动界面 开始采集&#xff1a; PS&#xff1a;不涉及 数据保存&#xff0c;重现等功能 界面设计 界面分为三块&#xff1a;顶部黑条带关闭按钮、左边对话框&#xff0c;右边的主界面 资源&#xff1a; 顶部黑条 top.bmp 2* 29 &#xff08;宽 * 高 像素点&…

SAS应用入门学习笔记7

代码说明&#xff1a; 1&#xff09;distinct 想获得region变量有的多少种&#xff1f; 2&#xff09;如果是常规语句&#xff0c;我们是使用proc freq 语句&#xff1a; where for filter&#xff1a; 然后有一个escape语句的概念&#xff1a; 这是一个简单的语法&#xff…

Redis集群离线安装

近日&#xff0c;由于客户的系统运行环境在一个封闭的网络内&#xff0c;不能与互联网联通&#xff0c;也不能提供yum库&#xff0c;所以运行环境只能采用离线安装的方式&#xff0c;我总结了一下本次的安装经过&#xff0c;希望对需要的人有所帮助。一、安装gcc查看gcc版本要求…

牛客网Python篇数据分析习题(五)

1.现有牛客网12月每天练习题目的数据集nowcoder.csv。包含如下字段&#xff08;字段之间用逗号分隔&#xff09;&#xff1a; user_id:用户id question_id&#xff1a;问题编号 result&#xff1a;运行结果 date&#xff1a;练习日期 请你统计答对和答错的总数分别是多少。 imp…