回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

时间:2022-09-28 07:54:01

???? 前情提要????

本章节是C++深度剖析封装细节&特性的相关知识~

接下来我们即将进入一个全新的空间,对代码有一个全新的视角~

以下的内容一定会让你对C++有一个颠覆性的认识哦!!!

以下内容干货满满,跟上步伐吧~


作者介绍:

???? 作者: 热爱编程不起眼的小人物????
????作者的Gitee:代码仓库
????系列文章&专栏推荐: 《刷题特辑》《C语言学习专栏》《数据结构_初阶》《C++轻松学_深度剖析_由0至1》《Linux - 感受系统美学》

????我和大家一样都是初次踏入这个美妙的“元”宇宙???? 希望在输出知识的同时,也能与大家共同进步、无限进步????
????这里为大家推荐一款很好用的刷题网站呀????点击跳转



????本章重点

  • 探索构造函数的奥秘&新型初始化方式

    • 初始化列表

    • 缺省值

  • 探索C++对类的“神奇优化”

  • 了解匿名对象的概念

  • 认识并深入了解“友元”

  • 认识并深入了解“内部类”


????一.回炉&剖析构造函数

????构造函数:

  • 在前文(可>点击<跳转食用呀)对构造函数的铺垫下,我们继续细致深入了解构造函数内部的细节

????让我们来意义揭晓构造函数还有什么不为人知的秘密吧~


????Ⅰ.初始化列表

????初始化&赋初值: 在了解新知识前,我们先来回忆一下这部分内容,以便后续的更好深入

  • 初始化:简单来说就是在定义一个变量的时候对其值进行初始化赋值,就叫做变量的初始化,Eg:

    • int a; 为定义了一个变量a,其中初始化的过程则由编译器将其初始化成随机值

    • int a = 0; 为定义了一个变量a,且数值被初始化成数值0

  • 赋初值:简单来说就是人为定义一个变量的初始值是多少,即给一个变量进行赋值

????构造函数初始化:

  • 在之前的学习中,我们已经了解了构造函数中的其中一种初始化方式:函数体内赋值

    即在构造函数内,我们显示的为成员变量赋上初值

    回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 特别注意: 上述过程只能称作赋初值,并不能称作初始化,这是因为成员变量已经在进入函数体内进行赋值前定义了,即当成员变量进入到函数体内时,已经有一个初值【编译器初始化的随机值】,这也就是为什么函数体内的赋值语句只能称作赋初值,而不是初始化

  • 总结: 初始化只能初始化一次,而构造函数体内可以多次赋值

  • 但有一种独特的方式,可以达到给成员变量初始化:初始化列表初始化

????初始化列表

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

  • Eg:回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

????补充:

  • 对于类来说,只要没有被实例化出对象,就没有真正给成员变量开辟空间

  • 也就是说,我们书写的类中的成员变量仅仅是声明而已,并没有真正被被定义出来

  • 只有在用类实例化出一个对象的时候,编译器才真正开辟了这个类的空间,而成员变量也在此时被定义出来且同时完成了初始化【若没有显示定义构造函数(传参构造……),则编译器会调用默认构造函数(即不用传参的构造函数:全缺省构造函数、无参构造函数、编译器默认生成的)】

????举个例子:

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 如上图,若成员为const所修饰的变量的时候,我们是无法实例化出一个对象的

  • 原因就在于: 我们无法通过默认构造函数中的函数体内赋值,去给成员变量const int _n赋上一个值,这是因为变量被const所修饰,我们只有在这个 变量定义的时候才可以初始化它的值 ,否则在后续代码中我们是无法修改它的值的

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 如上我们就可以知道如果我们使用的是初始化列表进行赋值的话,是可以改变const int _n这个值的

????所以:

  • 我们可以认为成员变量的定义阶段是在初始化列表处进行定义且初始化的

  • 这也就是为什么上述类型可以被初始化,是因为这个变量的 定义阶段 就是在初始化列表处进行定义的,所以我们可以在初始化列表(定义)处对其进行初始化

特别注意:

  • 初始化列表仅能在构造函数(构造函数、拷贝构造函数)中使用

  • 每个成员变量在 初始化列表 中只能出现一次【即初始化只能初始化一次】

  • 若类中包含以下成员变量,必须放在初始化列表位置进行初始化【因为以下成员需要在定义时就初始化】:

    • 引用成员变量

    • const成员变量

    • 自定义类型成员(该类没有默认构造函数)

❓有同学可能会疑惑 自定义类型成员 用到初始化列表的是什么情况呢

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 补充: 对于自定义类型成员变量也一样,在初始化列表阶段其实就是这个自定义成员变量的定义阶段,所以在定义的时候自定义成员变量便会自动调用其 默认构造函数 进行初始化,也就是说:自定义成员变量的默认构造函数其实是在初始化列表初始化阶段就调用了

  • 在以上的这种情况中,我们可以发现是错误的,原因是在于自定义成员变量t2是不存在 默认构造函数的 而是 传参构造函数,即此时编译器是无法自动调用其构造函数去初始化其成员变量的,需要我们传参调用

  • 于是在这种情况下,我们便可以很好的利用初始化列表的特性,在自定义成员变量被定义的时候显示的去调用其构造函数,这样就能达到 传参构造 的效果

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

????易混点: 若同时存在 初始化列表初始化函数体内赋值,最终会采用哪个地方的值呢?

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 以上便可以看到: 对于 内置类型 来说,最终采取的函数体内赋值作为变量值

  • 总结:

    • 对于 内置类型 来说,在经过初始化列表初始化后,还是会经过函数体内赋值

    • 对于 自定义类型成员(存在默认构函数) 来说,建议使用初始化列表进行初始化,效率更高

????️重要内容:

  • 成员变量在类中 声明次序 就是其在初始化列表中的 初始化顺序

  • 简单来说: 就是成员变量的 定义顺序 是由 成员变量在类中的 声明次序 所决定的

  • 与其在初始化列表中的初始化语句先后次序无关

  • 建议: 养成 类中成员变量声明的顺序初始化列表中初始化的顺序 保持一致

综上:

  • 初始化列表是成员变量定义的地方

  • 所以可以尽量多使用初始化列表进行成员的初始化,因为即使我们不写成员也需要进行定义,也还是会经历初始化列表这一步的

  • 上述就是初始化列表的全部内容啦~


???? Ⅱ.神奇的构造优化

????构造函数中的优化:

  • 在C++中,对于对象的构造存在一种神奇的构造优化,接下来就让我们一同揭秘吧~

  • 在此之前,先补充一个小知识点:隐式类型转换

    • 在之前有提到过,只要发生类型偏差,编译器都会自动生成一个临时变量去接收类型变换后的值,再进行赋值,而非直接转换本身变量的类型去进行赋值【这里可以点击>回顾<跳转食用呀】

    • 而对于对象的构造,也存在一种隐式类型转换

????举个例子:

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

  • 上述可以发现,对于aa2这个对象竟然可以支持这种构造方式

  • 本质就是:因为100是整形,而aa2是自定义类型,是不可能直接赋值的,所以会发生隐式类型转换

    • 1️⃣因为发生了类型偏差,编译器会自动产生一个类型与接收对象相同的类型去接收拷贝数据

    • 【即编译器相当于拿100去构造了一个临时对象:A tmp(100);

    • 2️⃣然后再利用这个临时对象去赋值给接收对象

    • 【即编译器再将刚创建的临时对象去赋值:A aa2(tmp);

    • 综上:这里的对象构造看似也只有一条语句,实则编译器做了以上两步操作:构造临时对象➕拷贝构造【语法意义上】

  • 但真实的情况是: 编译器对隐式类型转换其实做了特别的优化

    • 将上述的两步操作优化为一步操作了,优化成A aa2(100);一条语句了

    • 这也就是为什么上述的两步操作被称为语法意义上,因为实际情况是编译器发生优化了,直接进行拿值进行构造了

如果不想让编译器发生如上的隐式类型转换:

  • 可以给构造函数加上explicit关键字修饰

  • explicit修饰构造函数,将会禁止单参构造函数的隐式转换

  • 回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

综上:

  • 以上就是发生在构造函数中的神奇优化的全部内容啦~

???? Ⅲ.匿名对象

????匿名对象:

  • 简单来说:就是没有名字的对象

  • 而且匿名对象的生命周期仅仅只有在构造匿名对象的一行代码中,并不是随着主函数的销毁而销毁的

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

匿名对象的意义:

  • 在某种场景下,需要调用类中的一个函数去“搞事情”,且只需要调用此函数一次

  • 如果是平常的方法,我们构造一个普通对象,从而去调用函数,但函数我们只需要调用一次,此时这个普通对象就会一直占用空间,直至程序结束而调用析构

  • 而此时,匿名对象就可以起到很大的作用,因为我们仅调用这个函数一次,所以匿名对象就可以在一行代码中帮我们去调用,执行完这行代码去执行下一句的时候,匿名对象生命周期结束,便会自动析构

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

➡️什么场景使用:

  • 定义一个对象要用,但仅在这一行的代码中使用,其他地方不再使用

综上:

  • 匿名对象的使用可以使程序更加灵活、方便、便捷

  • 上述就是匿名对象的概念啦~


????二.static成员

????静态成员:

  • 声明为 static的类成员 称为 类的静态成员

  • static修饰的 成员变量,称之为 静态成员变量

  • static修饰的 成员函数,称之为 静态成员函数

  • 简单来说:就是被static所修饰的成员都为静态成员,即被修饰的成员不再属于单个对象的,而是属于这个类的,这个类中的所有对象【即所有对象共用此成员,多个对象中的静态成员其实都是在同一块空间上的】,而且静态成员的生命周期是全局的

????静态成员的特征:

  • 静态成员为所有类对象所共享,不属于某个具体对象的实例

  • 静态成员变量必须在类外定义(初始化),定义时不需要添加static关键字

  • 类静态成员可用类名::静态成员或者对象.静态成员(静态变量为私有成员就不可以了)来访问

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

  • 静态成员和类的普通成员一样,也有publicprotectedprivate这三种访问级别,也可以具有返回值

特别注意:

  • 1️⃣静态成员变量生命周期是全局的,但变量并不能在构造函数内初始化,因为变量存储于静态区【即静态变量属于这个类的成员变量,但并不存储于这个类的存储区域】

  • 2️⃣编译器专门给 静态成员变量的定义初始化,开了个后门:可以在全局中通过::(域作用限定符)去突破类域,初始化静态成员变量(即使静态成员变量是私有成员也是可以的,在这里不受访问限制符限制,因为这是编译器专门留给初始化设置的后门)

  • 3️⃣静态成员函数:只需要突破类域(即告诉编译器要找的函数是属于哪个类的)就可以访问得到,并不需要构造一个对象才能调用

  • 4️⃣静态成员变量:若想访问,会受到访问限定符的限制,若为私有成员,则需要设立一个静态成员函数去获取【所以一般访问静态成员变量的话,会给一个静态成员函数】

????面试题: 实现一个类,去计算中程序中创建出了多少个类对象

  • 这个题目,就很好体现了静态成员的存在的意义

    即我们可以创建一个计数器,把每一次类对象的创建都下统计下来,计算一共创建了多少个类对象

  • 有了以上想法,便有了以下的想法实现:

    类外面 创建一个计数器去记录:但此方法有个问题就是如何去设定条件判定类对象创建的时候记录下来,所以对于此方法是 不好实现的

    类里面 创建一个计数器(成员变量)去记录:此方法就 很好实现 了,因为类对象创建时一定会调用构造函数的,于是可以在构造函数内都做一个记录,只要调用就统计一次,但这个计数器是需要属于这个类的,即这个类的所有对象都是共享这个计数器的,而不是分开记录,这样最终才能获得总的类的对象个数

  • 此时就可以利用上静态成员的特性了,将计数器用static修饰,这样计数器就能在这个类中的所有对象中被同时共用

class A
{
public:
	A()
	{
		n++;
	}

	A(const A& a)
	{
		n++;
	}

	static int GetN()
	{
		return n;
	}

private:
	static int n;
};

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

综上:

  • 就是static成员的全部内容啦~

  • 静态成员函数 不可以 调用非静态成员函数

  • 非静态成员函数 可以 调用类的静态成员函数


????Ⅰ.成员初始化新方式

????C++11成员初始化新方式:

  • C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量 缺省值

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节

????如上:

  • 给了成员变量缺省值后,如果类调用的是默认构造函数的话,就会在 初始化列表阶段缺省值 去初始化自己【若存在函数体内赋值,最终的赋值还是以函数体内赋值为主】

  • 对于自定义类型成员来说,缺省值仅支持单成员变量的自定义类型,且必须是公有成员【本质:这里其实发生了隐式类型转换,被编译器优化成了传值构造】

综上:

  • 就是新型的初始化方式啦~

  • 给缺省值本质就是最后一层预防未初始化的保障,不到万不得已才使用的

  • 在有缺省值初始化列表初始化函数体内赋值的情况下,成员变量最终使用的值是函数体内赋值,若没有函数体内赋值其次使用的是初始化列表初始化的值,最后才是缺省值


????三.友元

????友元:

  • 友元分为:友元类 & 友元函数

  • 友元本质是提供了一种 突破封装 的方式,有时提供了便利

  • 但也存在副作用,友元会增加耦合度,破坏了封装,所以友元不宜多用

????接下来我们就深入“友元”吧~


????Ⅰ.友元函数

????友元函数:

  • 简单来说:可以让 非本类的函数 访问到 本类的私有成员变量

  • 即友元函数可访问类的私有和保护成员,但不是类的成员函数

  • 友元函数 是定义在类外部的普通函数,不属于任何类,但需要在 类的内部 有一个函数声明:frined ➕ 函数声明

????举个例子:

友元函数 就可以适用在如下场景中:

class Date
{
public:
	Date(int year, int month, int day)
		 : _year(year)
		 , _month(month)
		 , _day(day)
	 {}
	
	void operator<<(ostream& out) 
	{
		out << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
  • 我们想重载<<(输出)操作符,对类直接进行输出

  • 但此时我们可以发现,如果运算符重载函数为成员函数的话,默认是自带一个参数Date* this,所以左操作数变为Date* this,右操作数为out

  • 这样就会导致上述的运算符重载函数其实是调不动的,需要_year << out才调得动,但这样写代码就 失去可读性了

  • 于是,我们就可以自己实现一个全局的运算符重载函数,去重新排操作数的顺序(即参数传参的顺序),解决上述问题:

ostream& operator<<(ostream& out,const Date& d ) 
{
	out << d._year << "-" << d._month << "-" << d._day << endl;

	return out;
}
  • 但上述又会出现新的问题,就是在类外无法访问类中的私有成员,而如果这个函数成为类的 友元函数 的话,就可以很好的解决这个问题
class Date
{
public:
	friend ostream& operator<<(ostream& out, const Date& d);

	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	void operator<<(ostream& out)
	{
		out << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;

	return out;
}
  • 此时就相当于把朋友(外部函数)邀请进家(类)里做客,这样朋友就可以无限制访问私有成员等等【因为类里没有访问限定符的限制,可以直接访问】

  • 这也就是为什么说友元函数不是类的成员函数,因为朋友始终是朋友,无法成为家人

特别注意:

  • 友元函数不能用 const修饰

  • 一个函数可以是多个类的友元函数

  • 友元函数的调用与普通函数的调用和原理相同

综上:

  • 就是友元函数的全部内容啦~

  • 友元函数解决了可读性的问题(抢位置问题)

  • 友元函数可以访问到类中的私有成员变量


???? Ⅱ.友元类

????友元类:

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员

  • 友元类声明:在想要成为其友元类的类中写上friend ➕ 类的声明

特别注意:

  • 友元关系是单向的,不具有双向性

    AB的友元类,那A中的所有成员函数都是B的友元函数,可以访问B中所有私有成员变量,但B不可以反过来访问A

  • 友元关系不能传递

    如果AB的友元类,B也是C的友元类,不代表AC的友元

综上:

  • 以上就是友元类的全部内容啦~

???? Ⅲ.总结

  • 如非迫不得已,不建议使用友元,因为其本质是一种破坏封装的行为

????四.内部类

????内部类:

  • 如果一个类定义在另一个类的内部,这个内部类就叫做 内部类

  • 此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类【因为外部类对内部类没有任何优越的访问权限】

  • 本质:内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元

特别注意:

  • 内部类(友元类、函数也可以)可以定义在外部类的publicprotectedprivate都是可以的

  • 注意内部类(友元类也可以)可以直接访问外部类中的static枚举成员,不需要外部类的对象/类名【因为已经突破类域了】

  • sizeof(外部类)=外部类,和内部类没有任何关系

综上:

  • 就是内部类的全部内容啦~

  • 使用起来基本和友元类无差别,因为其本质就是友元类

  • 只不过友元类是声明在类内部,定义在类外部

  • 内部类则直接定义在类内部


????总结

综上,我们基本了解了C++中的 “封装细节&特性” ???? 的知识啦~

恭喜你的内功又双叒叕得到了提高!!!

感谢你们的阅读????

后续还会继续更新????,欢迎持续关注????哟~

????如果有错误❌,欢迎指正呀????

✨如果觉得收获满满,可以点点赞????支持一下哟~✨

回炉与剖析C++封装特性 - 重新认识C++,完满呈现全部内部细节