Re:从零开始的 C++ 入門篇(十)类和对象·最终篇下:类型转换、static成员、友元、匿名对象、内部类、拷贝编译优化

Re:从零开始的 C++ 入門篇(十)类和对象·最终篇下:类型转换、static成员、友元、匿名对象、内部类、拷贝编译优化

◆ 博主名称: 晓此方-ZEEKLOG博客

大家好,欢迎来到晓此方的博客。

⭐️C++系列个人专栏:

Re:从零开始的C++_晓此方的博客-ZEEKLOG博客

 ⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰


目录

0.1概要&序論

一,类型转换

1.1类型转换的定义

1.2类型转换的使用

1.2.1常见隐式类型转换

1.2.2C++11新增特性

1.2.3类型转换的局限

1.3类型转换的意义

1.3.1节省代码量

1.3.2编译器的优化

1.4explicit关键字

1.3.1官方定义

1.3.2使用方式

二,static成员

2.1static成员的定义

2.2static成员的使用

2.3static成员的初始化

Tips:静态成员的访问与初始化

2.4sistic成员函数

2.5静态计数器

三,友元

3.1友元的定义

3.2友元函数

Tips:少数特殊情况

3.3友元类

3.4友元的特性

四,内部类

4.1内部类的定义

4.2内部类的特性

五,匿名对象

5.1匿名对象的定义

5.1.1官方定义

5.1.2通俗解释和使用

5.2匿名对象的特性

六,编译器在拷贝时做的优化

6.1编译器拷贝优化概述

6.2拷贝优化一:类型转换

6.3拷贝优化二:值传参

6.4拷贝优化三:值返回

Tips:我们先补充一种写代码的方式

6.5高度激进的优化操作一

6.6高度激进的优化操作二


0.1概要&序論

         大家新年快乐!这里是此方,本篇是类和对象的最终篇下本文将详细介绍类和对象剩余的全部概念同时揭示对象拷贝过程中编译器为你偷偷做了哪些优化。内容干货满满!「此方」です。让我们现在开始吧!

一,类型转换

1.1类型转换的定义

官方定义

        类型转换(Type Conversion)类型转换又名隐式类型转换,是指在 C++ 程序中,将一个表达式的类型转换为另一种类型的过程。该过程可以由语言规则隐式地自动完成,也可以由程序员通过显式转换语法明确指定。类型转换的结果是一个具有目标类型的值或对象,其值由源类型的值按照 C++ 语言定义的转换规则产生。

1.2类型转换的使用

1.2.1常见隐式类型转换

#include <iostream> using namespace std; class A { public: A(int a) : _a1(a){} private: int _a1; }; int main(){ A aa2 = 1; return 0; }

         代码中:直接将1赋值给A类型对象,实际上发生了隐式类型转换。该隐式类型转换的发生实际上是基于A类类型对象存在一个单参数构造函数。1利用这个单参数构造函数构造生成了临时对象再拷贝构造给对象aa2。

1.2.2C++11新增特性

         如果这个类支持一个n参数构造函数,那么就传递一个n元值集合,就像这样:

class A { public: A(int a,int b) : _a1(a) ,_a(b){} private: int _a1; int _a2; }; int main(){ A aa2 = {1,2}; return 0; }

1.2.3类型转换的局限

         但是这种类型转换并不是所有类型都支持:如下,我们将一个常量字符串传递给A类类型变量aa2,实际上会发生报错。

class A { public: A(int a) : _a1(a){} private: int _a1; }; int main(){ A aa2 = "abcde"; return 0; }

1.3类型转换的意义

1.3.1节省代码量

A aa3(3); st.Push(aa3); //类型转换 st.Push(3);

          对比这两者。显然,适当使用类型转换能减少代码量。前面的方法你还要定义一个对象再插入,后面你直接传递一个3。

1.3.2编译器的优化

        到这里,一定会有读者问到:类型转换增加了构造临时对象和拷贝构造的过程,岂不是影响了运行效率?这点下文会详细介绍:编译器对拷贝的优化:编译器会把拷贝构造优化掉,和二为一,直接使用3进行构造。

       如何证明是编译器优化了而不是本就如此?

         临时对象是具有常性的,直接给临时对象一个非常性的别名会报编译错误(如图)所以的确是先使用类型转换转换成一个临时对象再拷贝构造。

1.4explicit关键字

1.3.1官方定义

          explicit用于修饰构造函数或类型转换函数,示该函数不能用于隐式类型转换,只能在需要显式指定的上下文中被调用。

1.3.2使用方式

class A { public: explicit A(int x) {} }; A a1 = 10; // ❌ 编译错误 

如上代码,编译器发生报错,说明不可以使用explicit修饰的构造函数去进行类型转换。

二,static成员

2.1static成员的定义

  • 用static修饰的成员变量,称之为静态成员变量静态成员变量一定要在类外进行初始化
  • 静态成员变量为同一个类的所有对象所共享,不属于某个具体的对象,不存在于对象中存放在静态区

2.2static成员的使用

静态对象不可以给缺省值。因为缺省值是给初始化列表用的,静态成员变量是不走初始化列表的

private: // 类里面声明 static int _scount;

2.3static成员的初始化

        static对象可以认为是一个全局变量但是在访问上受到类域和域访问操作符的限制。初始化方法如下:

private: // 类里面声明 static int _scount; }; // 类外面初始化 int A::_scount = 0;

Tips:静态成员的访问与初始化

        读者看到这里一定会发出疑问:static修饰的变量_scount不是在private的作用域中吗?为什么可以在类外初始化?一句话结论:“定义(初始化)” ≠ “访问”。private影响的是不可以在类外面访问成员变量而不是能否在类外面定义。

2.4sistic成员函数

          用 static 修饰的成员函数,称之为静态成员函数。静态成员函数没有 this 指针。在静态成员函数中,可以访问其他的静态成员,但是不能访问非静态成员,因为静态成员函数没有 this 指针。

  • 非静态的成员函数可以访问任意的静态成员变量和静态成员函数。
  • 而静态的成员函数只能访问任意的静态成员变量和静态成员函数。
        突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。(前提是不被private限制

2.5静态计数器

如何利用静态函数和静态成员变量来实现查找一共有多少个对象已经被创建?

         非常简单:利用静态成员变量的声明周期,
在每一个构造和拷贝狗杂函数中放一个该静态成员变量的++。生命周期还在的:就在析构里面加给--。

牛客传送门求1+2+3+...+n_牛客题霸_牛客网https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

          如图,题目要求计算累加和,但是不得使用迭代(限制循环),递归(无法设置结束条件),公式(限制乘法运算)三大常用方法。

class Sum { public: Sum() { ++cnt; sum += cnt; } static int GetSum() { return sum; } private: static int cnt; static int sum; }; int Sum::cnt = 0; int Sum::sum = 0;

        首先,我们创建构造函数并确保每一次调用构造函数的时候静态变量cnt就会++,sum每次都会加上cnt,基于两者静态变量的性质,可以持续累加实现目的。

 int main() { int n; cin >> n; Sum* p = new Sum[n]; cout << Sum::GetSum() << endl; delete[] p; return 0; } 

        利用get函数法调用这个静态成员变量使用new创建n个变量(后面会讲)实现连续调用n此构造函数,每一次构造都是一次累加操作。

        理解上述机制后,可进一步分析对象生命周期。

题目:

设已经有 A、B、C、D 4 个类的定义,程序中 A、B、C、D 构造函数调用顺序为?( )

设已经有 A、B、C、D 4 个类的定义,程序中 A、B、C、D 析构函数调用顺序为?( )
  • 第一题:CABD,静态变量是在第一次运行到这个地方的时候才会初始化。只有全局的静态才会在main函数之前初始化。
  • 第二题:BADC后定义先析构。但是局部变量先析构,静态变量后析构

三,友元

3.1友元的定义

        友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类。在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
class A { // 友元声明 friend void func(const A& aa, const B& bb); private: int _a1 = 1; int _a2 = 2; };

3.2友元函数

        外部友元函数可访问类的私有和保护成员友元函数仅仅是一种声明,他不是类的成员函数。 友元函数可以在类内部的任何地方声明,不受类访问限定符限制。一个函数可以是多个类的友元函数。 

Tips:少数特殊情况

        如图,我们有一个函数同时是A类和B类的友元函数,但是基于编译器的向上查找原则,当编译器运行到A类中的友元函数声明时,在查找B这个类的时候会出现报错。因此我们必须在整个程序的开头添加一个B类型的声明。class B

3.3友元类

         友元类是一种通过friend声明,​​​​​​被授予访问另一个类的私有和保护成员权限的类。其成员函数都可以是另一个类的友元函数。

        如上图,我们在类A中加入一个B类的友元声明,然后我们的B类中的函数就视为A类的友元函数。可以直接访问A类的成员变量。

3.4友元的特性

  1. 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  2. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  3. 友元增加了程序的耦合度,破坏了封装,不建议多用。

四,内部类

4.1内部类的定义

        如果一个类定义在另一个类的内部,这个类就叫做内部类内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。在计算外部类的时候不包含内部类。

#include<iostream> using namespace std; class A{ private: static int _k; int _h = 1; public: class B{ // B默认就是A的友元 public: void foo(const A& a){ cout << _k << endl; cout << a._h << endl; } private: int _b = 1; }; };
int main(){ cout << sizeof(A) << endl; A::B b; // 红框标注部分 return 0; }

        内部类必须指定类域搜索,如果内部类私有,则该内部类是该外部类的专属类。B受到A的限制,但是B不是A的成员

4.2内部类的特性

         内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。   
  1. 内部类默认是外部类的友元类,就是说:A的变量B可以随便使用。
  2. 内部类可以多层嵌套,但是一般不建议。
  3. 相对而言C++不大喜欢使用内部类,Java比较喜欢。
  4. 内部类与友元类,前者可以看作是从属但又相互独立关系,后者可以看作是朋友关系。

五,匿名对象

5.1匿名对象的定义

5.1.1官方定义

         A temporary object is an object created by a prvalue, which is not bound to a reference and has its lifetime limited to the evaluation of the full-expression in which it was created.(临时对象是由 prvalue 表达式创建、且未绑定到任何引用的对象,其生命周期限制在创建它的完整表达式的求值过程中。

5.1.2通俗解释和使用

匿名对象就是一个用构造函数临时创建的没有名字的对象,用完马上销毁。
int main(){ A aa1; //有名对象 A aa2();// 不能这么定义对象,因为编译器无法识别这是函数声明还是对象定义 A(); // 不传参匿名对象 A(1); //传参匿名对象 }

        没有设置名称只在后面加一对()(有参数传递参数),这样就完成了匿名对象的创建

//有名对象 Solution st; cout << st.Sum_Solution(10) << endl; // 匿名对象 cout << Solution().Sum_Solution(10) << endl;

        如上代码,可见采用匿名对象在一些一次性的对象参数传递的时候可以节约代码量

5.2匿名对象的特性

  • 匿名对象的声明周期只有一行,匿名对象就是一次性的,后面会讲const引用会延长它的声明周期。
  • 匿名对象 ≠ 没有 this,匿名对象只是没有名字,但它依然是一个完整的对象。成员函数是否有 this,只取决于它是不是非静态成员函数,和对象有没有名字完全无关。
  • 匿名对象会调用构造函数和析构函数会开辟空间。

六,编译器在拷贝时做的优化

6.1编译器拷贝优化概述

        现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下尽可能减少一些传参和传参过程中可以省略的拷贝

        如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化

6.2拷贝优化一:类型转换

        测试类(一个简易的验证机器)如果我们调用了哪个函数,构造还是拷贝构造,我们可以通过打印结果判断出来

#include<iostream> using namespace std; class test{ public: test(int a, int b) :_a(a) , _b(b){ cout << "test::test(int a, int b)" << endl; } ~test(){ _a = 0; _b = 0; cout << "test::~test()" << endl; } test(test& test1){ _a = test1._a; _b = test1._b; cout << "test::test(test& test1)" << endl; } private: int _a; int _b; };

测试类型转换:如下

test testA = { 1,2 };

对比结果:

基于逻辑我们所想的编译器优化后实际做的
一、{1,2}传递给构造函数创建临时变量一,直接用{1,2}传递参数给构造函数构造testA
二、该临时变量传递给testA实现拷贝构造/
三、析构二,析构

所以i我们可以得出结论:编译器在连续的拷贝工作中会优化掉其中一步构造实现优化。

以下,我们可以用更多的案例来证明这个结论

6.3拷贝优化二:值传参

这里我们分有名对象传参和匿名对象传参。以下逐条解释分析:

  • 第一个构造函数:通过构造函数构造对象testA。
  • 第一个拷贝构造函数:有名对象testA传递参数调用拷贝构造函数,创建临时对象拷贝。
  • 第一个析构函数:临时对象销毁。
  • 第二个构造函数:编译器优化:将构造匿名对象并拷贝构造传递的过程合并为构造函数。
  • 第二给析构函数:匿名对象离开本行声明周期结束。
  • 第三个析构函数:程序运行到结束,testA对象声明周期结束。

我们看看编译器哪里优化了:

  1. 首先:有名对象传参必须经过拷贝构造,同时这个过程不属于连续的拷贝,所以不能优化。
  2. 优化的地方:匿名对象的一个连续的步骤:构造匿名对象+拷贝构造匿名对象优化。

        同理的,将类型转换与函数传参结合起来,同样会被优化,因为这是一个连续操作上的两个拷贝操作,必然优化

6.4拷贝优化三:值返回

Tips:我们先补充一种写代码的方式

test f2(){ test aa; return aa; } int main(){ f2().Print(); return 0; }

        在函数体内创建一个对象,然后将变量值返回,这种方式在C语言中是万万不可以的,但是在C++中,值返回会拷贝构造一个临时对象,这里正是利用了这个临时对象调用了函数print()

我们用类似的方式去测试一下:

         发现,这里也优化了:严格来说这里是省略了aa。为什么省略临时对象不行?因为aa出函数作用域就析构了,无法调用print()。看起来省的是拷贝构造,但是实际上是取消了aa的构造。让构造直接构造临时对象。

原本应该是怎么样编译器优化的结果是什么

对象aa创建->值返回调用拷贝构造函数->创建临时对象->aa销毁->临时对象调用函数打印->临时对象销毁(注意析构发生在print()函数这一行)

省略aa对象的创建和销毁->直接构造临时对象并返回->临时对象调用函数打印->临时对象销毁

6.5高度激进的优化操作一

最厉害的来了(这里我们采用VS2022的debug版本)

A f2(){ A aa(1); ++aa; return aa; }

        接着上面的测试案例,我们看看将aa++后还能不能实现优化。牛逼的来了:编译器会自定语义分析aa++并实现优化。

6.6高度激进的优化操作二

test f2(){ test aa(); return aa; } int main(){ test testC = f2(); }

           如上,该代码本应该执行三步操作,构造对象aa,拷贝构造临时对象传递,拷贝构造初始化给testC,编译器在这里省略了临时对象,将三步和为一步

         如果我们把拷贝构造换成赋值运算符重载,这个时候拷贝构造没有进行任何优化,但是release下任然进行了优化。


         好了,本期内容就到这里,感谢你的阅读,如果对你由帮助,不要忘记点赞三联哦,我是此方,我们下期再见,拜拜~

Read more

C++之基于正倒排索引的Boost搜索引擎项目日志+server代码及详解

C++之基于正倒排索引的Boost搜索引擎项目日志+server代码及详解

首先为了更好的查看自己的项目状况,日志是我们做项目可以说必须要写的一部分。而server部分我们可以理解为写了这么多的类就是为了在这里使用。 1. 日志 __FILE__和__LINE__是 C/C++ 编译器预定义的特殊宏: __FILE__: 它会被编译器自动替换为当前代码所在源文件的路径或文件名(字符串类型)。 在日志函数中,它的作用是记录 “这条日志是从哪个文件输出的”。 例如:如果在 test.cpp 中调用 LOG1 宏,__FILE__ 就会被替换为 "test.cpp"(具体可能包含路径,取决于编译器),最终日志中会显示 [test.cpp : ...]。 __LINE__: 它会被编译器自动替换为当前代码所在的行号(整数类型)。 在日志函数中,它的作用是记录 “这条日志是从文件的哪一行输出的”。 例如:如果 LOG1 宏调用写在 test.cpp 的第 25

By Ne0inhk
C++的核心--继承

C++的核心--继承

目录 前言 一、继承的概念及定义 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 五、继承与友元 六、继承与静态成员 七、复杂的菱形继承及菱形虚拟继承 (一)单继承与多继承 (二)菱形继承 (三)菱形虚拟继承 八、继承的总结和反思 结语 前言 在C++ 编程世界里,继承是一项极为关键的特性,它为代码的复用和层次化设计提供了强大支持。掌握继承机制,对于编写高效、可维护的C++ 代码至关重要。今天,就让我们一起深入探究C++ 中的继承。 一、继承的概念及定义 继承是面向对象程序设计实现代码复用的重要手段。它允许我们在保持原有类特性的基础上进行扩展,产生新的类,即派生类。这体现了面向对象程序设计的层次结构,从简单到复杂逐步构建。 定义格式上,以 class Student : public

By Ne0inhk
C++微服务实战中好友管理子服务的全面解析

C++微服务实战中好友管理子服务的全面解析

【C++ 微服务实战】IM 好友管理子服务全解析:从 Proto 定义到高可用部署 在即时通讯(IM)系统中,好友管理子服务是连接 “用户社交关系” 与 “聊天会话” 的核心枢纽 —— 它既要处理好友申请、关系维护,也要管理单聊 / 群聊会话的创建与成员维护。本文基于实际项目代码(C++/brpc/Protobuf/ODB),从 “接口设计”“数据模型”“核心逻辑”“高可用部署” 四个维度,完整拆解好友管理子服务的实现细节,带你理解如何构建一个解耦、可靠的微服务。 一、服务定位与技术栈 在 IM 微服务架构中,好友管理子服务(Friend Server)的核心职责是 **“管理用户社交关系” 与 “维护聊天会话容器”**,向上对接网关服务接收客户端请求,向下依赖 MySQL/ES 存储数据,

By Ne0inhk
【C++】类和对象(中)

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

一、类的默认成员函数 编译器会自动生成的成员函数称为默认成员函数。一个类,不写的情况下编译器会默认生成以下6个默认成员函数。另外在C++11中,增加了两个默认成员函数,移动构造和移动赋值。默认成员函数从两方面学习: 1. 我们不写时,编译器默认生成的函数行为是啥?满足我们的需求吗? 编译器默认生成的函数不满足我们的需求,那如何自己实现? 二、构造函数 构造函数主要任务是对象实例化时初始化对象。就像每次写栈或队列时需要初始化Stack Init()、Queue Init(),用了构造函数就不需要写这一步。 构造函数的特点:函数名与类名相同:类class Stack,类中的函数Stack()无返回值。也无void对象实例化时系统会自动调用对应的构造函数构造函数可以重载如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧

By Ne0inhk