Cpp类和对象(下)(6)

news2024/11/23 12:41:46

文章目录

  • 前言
  • 一、初始化列表
    • 概念
    • 使用注意
    • 实际运用
    • explicit关键字
    • 初始化列表的总结
  • 二、static成员
    • static成员的概念
    • static成员的特性
    • static的一个实用场景
  • 三、友元
    • 友元函数
    • 友元类
  • 四、内部类
    • 概念
    • 特性
  • 五、匿名对象
  • 六、再次理解封装和面向对象
  • 总结


前言

  Hello,本篇应该是类和对象系列里面较为轻松的一篇了,尤其是在经历了中篇的洗礼之后(,但是不可掉以轻心,要说这个下篇比上篇简单,那也是没有的
  继续加油!


一、初始化列表

概念

  前文我们讲构造函数的时候,提了一嘴初始化列表,不如我们先来看一下其概念吧:

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

我们来对比一下,应该能让你有更加深刻的认识:

首先是我们之前的初始化方式

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
 
private:
	int _year;
	int _month;
	int _day;
};

 这种方法赋初值当然有效,可是严格意义上来说并不能称其为初始化,因为初始化只能初始化一次,而函数体内却可以多次赋值

现在再来看看用初始化列表来赋初值

#include <iostream>
using namespace std;

class Date
{
public:
    //构造函数
    Date(int year = 1900, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

使用注意

  1. 每个成员变量在初始化列表中只能出现一次
    原因是初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现

  2. 类中包含以下成员,必须放在初始化列表进行初始化
    引用成员变量、const成员变量、自定义类型成员(该类没有默认构造函数)
    对于引用成员变量,引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化
    对于const成员变量,被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化
    对于自定义类型成员(该类没有默认构造函数),若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,否则就会报编译错误

  3. 尽量使用初始化列表初始化
    因为语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,所以无论你是否使用初始化列表,都会走这么一个过程

  4. 成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
    我后面会举个具体例子说明,但是建议你声明顺序和初始化列表顺序保持⼀致。

实际运用

这个真的很复杂,要梳理清楚很困难

先来看一连串的代码吧:

int a = 10;
int& b = a;// 创建时就初始化

const int a = 10;// correct 创建时就初始化
const int b;// error 创建时未初始化

class A //该类没有默认构造函数 
{
public:
	A(int val) //注:这个不叫默认构造函数(需要传参调用)
	{
		_val = val;
	}
private:
	int _val;
};

class B
{
public:
	B()
		:_a(2021) //必须使用初始化列表对其进行初始化
	{}
private:
	A _a; //自定义类型成员(该类没有默认构造函数)
};

以上代码很好的解释了为什么三种特殊情况下必须要用初始化列表来初始化

接着看以下代码:

// 使用初始化列表
int a = 10

// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

// 对于自定义类型使用初始化列表
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};

class Test
{
public:
	// 使用初始化列表
	Test(int hour)
		:_t(hour)// 调用一次Time类的构造函数
	{}
	// 在构造函数体内初始化(不使用初始化列表)
	Test(int hour)
	//初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
	{ 
		Time t(hour);// 调用一次Time类的构造函数
		_t = t;// 调用一次Time类的赋值运算符重载函数
	}
private:
	Time _t;
};

 通过以上代码,你就明白了为什么说尽量使用初始化列表来初始化,其目的就是为了节省效率
 更明确的说对于内置类型,两种方式没差,如上;对于自定义类型,因为不管如何都会走一遍初始化列表,所以直接在初始化列表初始化能最大化地提高效率,如上

再看如下代码:

#include<iostream>
using namespace std;

class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa(1); // _a1是1,_a2是随机值
	aa.Print();
}

 我们分析下为什么会是这个输出,其实,你只要记着上面说的,成员变量的初始化跟定义顺序有关系,跟初始化列表的出现顺序毫无关系,一切就明白了

首先初始化列表里_a1,_a2都有明确的初始化值,于是缺省失效,然后我们再来看定义顺序,_a2先定义,那么_a2先初始化,初始化为_a1的值,可是这时候_a1并没有被赋值,只能是随机值,而紧接着_a1开始初始化,被赋予我们传过去的a,即为1

 其实在这里,我们还提到了缺省值的概念,我们也可以来研究一下,其实我觉得你也可以自己写个程序,跑个调试,看一看缺省值、初始化列表之间的关系

#include<iostream>
using namespace std;

class Test
{
public:
	Test()
		:_a(2)
	{}

	void GetRet()
	{
		cout << _a << endl;
	}
private:
	int _a = 1;
};

int main()
{
	Test().GetRet(); // 这个涉及到匿名对象,我们等下会讲

	return 0;
}

最后输出_a的值为2,其实你调试一下就会发现一些巧妙的地方
_a一开始为1,后面才为2,最后才输出,所以,这其中奥妙就显然了

如果一个成员变量既有缺省值又在初始化列表中定义了,那么就按照初始化列表中的值进行初始化
如果一个成员变量有缺省值,但是没在初始化列表中定义,那么就用它的缺省值初始化
如果一个成员变量既没有缺省值又没在初始化列表中定义,那么就给一个随机值

explicit关键字

先来看两行代码:

int a = 1;
double& b = a; // err,内置类型变量在发生类型转换的时候会生成一个临时的常性变量

其实,内置类型也可以转换成自定义类型,这里就和构造函数扯上关系了

一个类的构造函数,不仅起到初始化成员变量的作用,对于单个参数或第一个参数无缺省值的半缺省构造函数来说,它还具有类型转换的作用。(其实多个参数也行,只是要在C++11标准后,用{ }括起来内置类型即可)

在这里插入图片描述
如下,第一个a1构造函数无可厚非,第二个首先1被作为参数构造了一个临时常性变量,再拷贝构造给a2

有什么办法可以禁止构造函数类型转换呢?
有,引入explicit关键字,在构造函数的前面加上它,即可禁止类型转换了

explicit A(int a)

初始化列表的总结

前面讲了那么多,下面用一张表来尝试捋清一下
在这里插入图片描述
一言以蔽之:
无论是否显示写初始化列表,每个构造函数都有初始化列表!
无论是否在初始化列表显式初始化,每个成员变量都要走初始化列表初始化!

二、static成员

static成员的概念

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

static成员的特性

一、静态成员为所以类对象所共享,不属于某个具体的对象
 举例来说,请看以下代码:

#include <iostream>
using namespace std;

class Test
{
private:
	static int _n;
};

int main()
{
	cout << sizeof(Test) << endl; // 1
	return 0;
}

  结果显示计算Test类的大小为1,因为静态成员 _n 是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和

二、静态成员变量必须在类外定义,定义时不添加static关键字

class Test
{
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

  这里静态成员变量 _n 虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了,本质上是因为_n属于整个类,不单独属于某个类的对象

三、静态成员函数没有隐藏的this指针,不能访问任何非静态成员

class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

其实,含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量

四、静态成员和类的普通成员一样,也有public、private和protected这三种访问级别
 所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问

你不妨思考一下以下两个问题
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?

答案是:
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制

static的一个实用场景

 请问如何实现⼀个类,计算程序中创建出了多少个类对象?

请看如下代码:

#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		++_scount;
	}
	
	A(const A& t)
	{
		++_scount;
	}
	
	~A()
	{
		--_scount;
	}
	
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类⾥⾯声明
	static int _scount;
};

// 类外⾯初始化
int A::_scount = 0;

int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	
	// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)
	//cout << A::_scount << endl;
	
	return 0;
}

三、友元

  这个我们前面讲解日期类的时候提到过,输出日期类的时候,因为d1 << _cout 与我们的常用习惯不符合,所以我们把<<操作符重载为全局函数,可这样就无法访问到日期类的成员变量,于是,我们采用了友元这一方案,现在我们来详细学习下这一概念

友元本质上提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

友元函数

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

如上,我们再来回顾以下代码:

class Date
{
	// 友元函数的声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day<< endl;
	return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

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

友元类

  和友元函数类似,我们也可以在类中声明一个友元类,友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员
在这里插入图片描述

请注意:
友元关系是单向的,不具有交换性 -> 比如上面,Date类是Time类的友元,所以可以直接在Date类中访问Time类的私有成员变量;但是不代表Time类是Date类的友元,不能在Time类中访问Date类的私有成员变量

就像爱情一样,“执子之手,与子偕老”实为可遇不可求
相比之下,“我本将心向明月,奈何明月照沟渠”才是人间常态

友元关系不能传递 -> 例如A是B的友元,B是C的友元,不代表A就是C的友元了
友元关系不能继承 -> 这里在讲到继承后再给大家详细介绍

四、内部类

概念

如果一个类定义在另一个类的内部,这个定义在内部的类就称为内部类

请注意:
1、此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。
2、外部类对内部类没有任何优越的访问权限。
3、内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性

一、 内部类受外部类的类域限制
假如我们想创建一个内部类类型的变量,需要用作用域限定符
在这里插入图片描述

二、外部类的大小不包括内部类

在这里插入图片描述

外部类A的大小并没有包括内部类B,所以也可以体现内部类的空间也是独立的

五、匿名对象

  有时候我们可能只需要调用一次某个类的成员函数,为此如果特意去创建一个对象的话就太麻烦了,这时候就可以考虑运用匿名对象

  匿名对象的特点在于,它的生命周期只在这一行,一旦程序走到了下一行,就会自动调用析构函数销毁,且在创建的时候是不用取名字的

在这里插入图片描述
所以对于各种一次性的对象创建,我们都可以使用匿名对象

六、再次理解封装和面向对象

  C++是基于面向对象的程序,面向对象有三大特性:封装、继承、多态

  C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起。通过访问限定符的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。

所以说,封装的本质是一种更高效的管理

举个例子,我们来看一下火车站
 售票系统:负责售票—用户凭票进入,对号入座。
 工作人员:售票、咨询、安检、保全、卫生等。
 火车:带用户到目的地

乘客不需要知道火车的构造、票务系统是如何运作的,只要能正常方便的应用即可,知道了反而还增加管理成本,而工作人员的协调配合,才能使得让大家坐车井然有序的进行
从这可以看出,面向对象实际上是在模拟世界,这个思路打通了计算机世界和现实世界的桥梁
在这里插入图片描述


总结

  怎么样!是不是感概终于撑过来了,现在终于可以缓口气了,接下来的内存管理会相对较为简单,且能让你在本篇的一些疑惑得到更好的解答,所以休整一下,我们马上继续出发!

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

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

相关文章

redis学习(013 实战:黑马点评:优惠券秒杀——超卖问题解决方案)

黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目 总时长 42:48:00 共175P 此文章包含第52p-第p53的内容 文章目录 问题演示使用jmeter测试两百个并发请求 超卖的原因分析解决方案 加锁悲观锁介绍乐观锁介绍乐观锁…

XXL-Job 监控消息队列消息数量预警

1、什么是Basic Authentication认证 Basic Authentication 是一种常用的 HTTP 认证机制&#xff0c;用于保护 Web 资源免受未授权访问。在这种认证方式中&#xff0c;客户端&#xff08;通常是浏览器&#xff09;需要在 HTTP 请求头中提供用户凭据&#xff08;通常是用户名和密…

Leetcode 最小覆盖子串

解题思路&#xff1a; 哈希表存储字符频率&#xff1a;首先统计字符串 t 中每个字符出现的次数。滑动窗口&#xff1a;用两个指针 left 和 right 来标记当前窗口的左右边界&#xff0c;不断右移 right&#xff0c;直到包含了所有 t 中的字符。然后尝试右移 left&#xff0c;缩…

python爬虫/引用requests/基本使用

1.安装requests 进入控制台使用该命令安装requests pip3 install requests 2.对网站使用get请求 这里用对网站进行get请求&#xff0c;然后打印。 import requests //引用requestsresponse requests.get(urlhttps://www.bilibili.com/)print(response.text) 3.对网站使用…

2024全国研究生数学建模竞赛(数学建模研赛)ABCDEF题深度建模+全解全析+完整文章

全国研究生数学建模竞赛&#xff08;数学建模研赛&#xff09;于9月21日8时正式开赛&#xff0c;赛程4天半&#xff0c;咱这边会在开赛后第一时间给出对今年的6道赛题的评价、分析和解答。包括ABCDEF题深度建模全解全析完整文章&#xff0c;详情可以点击底部的卡片来获取哦。 …

座椅空置状态检测系统源码分享

座椅空置状态检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer…

大模型之基准测试集(Benchmark)-给通义千问2.0做测评的10个权威测基准测评集

引言 在去年(2023)云栖大会上&#xff0c;阿里云正式发布千亿级参数大模型通义千问2.0。据现场介绍&#xff0c;在10个权威测评中&#xff0c;通义千问2.0综合性能超过GPT-3.5&#xff0c;正在加速追赶GPT-4。以下是通义千问在MMLU、C-Eval、GSM8K、HumanEval、MATH等10个主流…

基于Springboot共享充电宝管理系统JAVA|VUE|SSM计算机毕业设计源代码+数据库+LW文档+开题报告+答辩稿+部署教+代码讲解

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

openEuler普通用户su root时Permission denied

openEuler普通用户su root时Permission denied 背景&#xff1a; openEuler默认普通用户是不能通过su切换到root用户的 如果想通过su切换到root&#xff0c;有以下两个解决办法 1、修改/etc/pam.d/su 文件 [rootlocalhost ~]# vim /etc/pam.d/su #修改21行&#xff0c;将“…

视频怎么制作成二维码?视频轻松生成二维码的3步操作

现在很多人为了能够更快捷的实现视频内容的分享&#xff0c;会通过将视频生成二维码的方式&#xff0c;让其他人可以通过扫描二维码来查看视频内容。这种方式不需要用户存储视频&#xff0c;扫码就能够在设备上查看视频&#xff0c;有利于提升查看视频的便捷性&#xff0c;可以…

图片压缩工具免费怎么找?归纳了这几个压缩工具

有哪些图片压缩工具免费&#xff1f;在数字化时代&#xff0c;图像已成为我们生活中不可或缺的一部分。无论是网站设计、社交媒体分享还是文件传输&#xff0c;高质量的图片都扮演着重要的角色。但高质量往往意味着大文件体积&#xff0c;这可能会导致加载速度变慢或存储空间不…

打造以太坊数据监控利器:InfluxDB与Grafana构建Geth可视化分析平台

前言 以太坊客户端收集大量数据&#xff0c;这些数据可以按时间顺序数据库的形式读取。为了简化监控&#xff0c;这些数据可以输入到数据可视化软件中。在此页面上&#xff0c;将配置 Geth 客户端以将数据推送到 InfluxDB 数据库&#xff0c;并使用 Grafana 来可视化数据。 一…

Android13中Android.mk和Android.bp预编译多种架构文件

需求&#xff1a; 1&#xff0c; 当前有多个架构的config文件&#xff0c;但是需要不同架构使用不同config文件 2&#xff0c; 必须将config文件拷贝到out/host目录下 常规思路 在Android.bp中&#xff0c; 一般在编译多架构文件时&#xff0c;都会使用arch属性&#xff…

Tauri 应用 input 输入自动大写问题定位解决

使用 Tauri React 开发 MinApi(http api接口测试工具) 时&#xff0c;在 Mac 系统中遇到一个很奇怪的问题&#xff1a;在 input 输入框中输入内容时&#xff0c;如果输入的是全小写英文字母&#xff0c;会自动将首字母转换为大写&#xff0c;效果如下图所示。 问题定位 经过排…

WebRTC关键技术及应用场景:EasyCVR视频汇聚平台高效低延迟视频监控解决方案

众所周知&#xff0c;WebRTC是一项开源的实时通信技术&#xff0c;它通过集成音频、视频和数据传输到Web浏览器中&#xff0c;使得实时通信变得简单且无需任何插件或第三方软件。WebRTC不仅是一个API&#xff0c;也是一系列关键技术和协议的集合&#xff0c;它的出现改变了传统…

代码随想录算法训练营Day14 | 226.翻转二叉树、101. 对称二叉树、104.二叉树的最大深度、111.二叉树的最小深度

目录 226.翻转二叉树 101. 对称二叉树 104.二叉树的最大深度 111.二叉树的最小深度 226.翻转二叉树 题目 226. 翻转二叉树 - 力扣&#xff08;LeetCode&#xff09; 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例1&#…

Android下反调试与反反调试

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 反调试检测 反调试检测的几种方式。 1. TrackerId 首先&#xff0c;通过 IDA Pro 的调试器附加到当前 app 进程 关于IDA Pro调试android app的详细教程可以…

必应广告投放推广收费标准和流程

在当今竞争激烈的商业环境中&#xff0c;如何精准高效地推广产品与服务&#xff0c;成为企业面临的重大挑战。微软必应Bing广告平台&#xff0c;凭借其强大的技术实力和精准的数据分析能力&#xff0c;已成为众多企业广告推广的首选。云衔科技作为业界领先的数字化营销服务商&a…

【机器学习-无监督学习】聚类

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈Python机器学习 ⌋ ⌋ ⌋ 机器学习是一门人工智能的分支学科&#xff0c;通过算法和模型让计算机从数据中学习&#xff0c;进行模型训练和优化&#xff0c;做出预测、分类和决策支持。Python成为机器学习的首选语言&#xff0c;…

安卓系统升级后,关于Fiddler工具不能抓取https接口问题

问题原因&#xff1f; 目前安卓手机可以抓取的https接口都在安卓7.0版本以下&#xff0c;有时候抓取Android7.0版本或以上的接口抓取不到 因为Android7.0之后常规手段不能抓Https的包&#xff0c;应用会默认不信任用户安装的证书(手机里自己安装的证书)&#xff0c;只信任系统…