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

Model Context Protocol (MCP) Python SDK 权威指南

Model Context Protocol (MCP) Python SDK 权威指南

1. 简介与核心概念 Model Context Protocol (MCP) 是一个开放标准,旨在标准化 AI 模型(如 Claude, GPT)与外部数据源(IDE, 数据库, 生产工具)之间的交互。 MCP Python SDK 是该标准的官方 Python 实现,它屏蔽了底层的 JSON-RPC 通信细节,让开发者能够专注于业务逻辑。 github: https://github.com/modelcontextprotocol/python-sdk 核心架构图解 Host (Claude Desktop/IDE) ↔ MCP Client ↔ Transport (Stdio/SSE) ↔ MCP Server ↔ Data Source \text{Host

By Ne0inhk

PyCharm 完全指南:Python 开发者的首选集成开发环境

目录 引言 一、PyCharm 概述与核心价值 二、里程碑式更新:统一版本与许可模式 三、核心功能深度剖析 1. 智能代码辅助 2. 高效的导航与搜索 3. 无缝的 Web 开发支持(Pro 版) 4. 内置工具与集成 四、新版本亮点:PyCharm 2025.x 五、如何开始:安装与第一个项目 1. 安装与环境准备 2. 创建并运行你的第一个项目 六、结语 引言 在 Python 开发的世界里,选择一款顺手的代码编辑器往往能事半功倍。而提到 Python 集成开发环境(IDE),PyCharm 无疑是一个绕不开的名字。这款由 JetBrains 公司打造的

By Ne0inhk
Anaconda安装(2024最新版)

Anaconda安装(2024最新版)

安装新的anaconda需要卸载干净上一个版本的anaconda,不然可能会在新版本安装过程或者后续使用过程中出错,完全卸载干净anaconda的方法,可以参考我的博客! 第一步:下载anaconda安装包         官网:Anaconda | The Operating System for AI (不过官网是外网,这里推荐国内清华大学的镜像源,对于国内的网络友好,下载速度更快!) 清华镜像网:Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source MirrorIndex of /anaconda/archive/ | 清华大学开源软件镜像站,致力于为国内和校内用户提供高质量的开源软件镜像、Linux 镜像源服务,帮助用户更方便地获取开源软件。本镜像站由清华大学 TUNA 协会负责运行维护。https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/         这里有许多版本,博主这些选择下载最新版本的2024.06-1版本

By Ne0inhk

Python 异步爬虫实战:FindQC 商品数据爬取系统完整教程

本文详细介绍如何使用 Python 异步编程技术构建一个高性能的商品数据爬虫系统,包括 API 调用、数据库存储、消息队列集成等核心功能。 📋 目录 * 一、项目概述 * 二、技术栈 * 三、项目架构 * 四、核心功能 * 五、环境配置 * 六、代码详解 * 七、使用示例 * 八、性能优化 * 九、常见问题 * 十、总结 一、项目概述 1.1 项目简介 service_spider 是一个基于 Python 异步编程的商品数据爬虫服务,主要功能包括: * ✅ 目录遍历:自动遍历所有需要爬取的目录 * ✅ 分页处理:智能分页获取商品列表,直到最后一页 * ✅ 商品详情获取:获取商品基本信息、图集(QC图、视频等)

By Ne0inhk