嵌入式面试题 - C++总结(一)

嵌入式面试题 - C++总结(一)


部分常用术语解释

  • 字面量:编译时确定的常量,即代码中直接写出的常量值,所以也是常量值的一种表示方式
    • 字面量 = 常量值
  • 成员属性 = 成员变量
  • 成员方法 = 成员函数
  • bool类型:C语言里面是宏,C++里面是一个真正意义上的数据类型
  • 派生类:子类
  • 基类:父类

1.面向过程与面向对象的区别?

  • 面对过程是具体化的,就是一步一步去分析去解决去实现这个问题(自顶向下,逐步细化

    有一道数学题,你现在要一步一步去分析,应该怎么把这一道题给解释出来

  • 面对对象是抽象化的,功能都有了,需要什么功能直接用就可以了(面向对象三大基本特征:封装,继承,多态

    我们先让李华限时回归一下,李华喜欢玩某款二次元手游,但是天天肝觉得想吐了
    于是他写了一个脚本,每天上线,只需要选择对应功能的执行就可以了
    例如,今天脚本执行的功能顺序是【日常任务 - 公会战 - 活动】
    你不需要去管是怎么执行的,你就说今天的游戏肝完了没有


  • 面向过程(POP):函数套函数, 结构套结构面向过程就是面向解决问题的过程进行编程

    • 强联系,耦合度高,关联性强,复用性差,维护性差,扩展性差

    • C语言也可以写面向对象的编程(利用函数指针),但是C中的函数不属于对象

    • C语言本身是面向过程的,但可以通过函数指针结构体等技术来模拟面向对象编程的一些特性
      通过将函数指针作为结构体的一部分,模拟对象的行为,实现某些面向对象的设计模式
      但是C中的函数本身并不属于对象,因此它并不具备完整的面向对象特性,如封装、继承和多态等


  • 面向对象(OOP)

    • 低耦合,复用性高
    • 强调对象、封装、继承和多态,注重模型化和复用
    • 面对对象主要的思想:封装,抽象
      • 抽象类:含有成员函数的类,继承和多态来支撑抽象

2.🍁指针和引用的区别?

  • 1.指针是一个存储地址的变量,引用是变量的别名(跟原来的变量实质上是一个东西)
    • 通过指针,需要先解引用(*)来访问,而通过引用,可以直接访问原始变量
    • 指针:*p 地址中的内容
    • 引用:&p 该变量的地址
  • 2.指针可以为空,引用不能为空的同时必须初始化
  • 3.指针初始化之后可以改变指向,引用初始化之后不能改变指向
  • 4.指针可以有多级指针,引用最多只能有两级
  • 5.指针需要动态分配内存空间,引用不需要动态分配内存空间
  • 6.指针和引用都占8个字节
  • 7.指针间接访问,效率低,引用直接访问,效率高
  • 8.指针做函数形参时,需要考虑是传地址还是传值,
    但是引用不需要考虑,既可以访问地址也可以修改指
  • 9.如果函数返回值是引用可以做左值,如果是指针则不行
    • 当函数返回指针时,虽然可以修改指针指向的内容,但不能像引用那样直接修改返回值本身
    • 而返回引用时,可以直接修改返回的值,因为引用是该变量的别名
  • 10.指针使用sizeof得到的是指针大小,引用`得到的是引用指向变量的大小

3.左值与右值的区别?🍁++i 和 i++ 是左值还是右值?(区分 i++++i)

  • 左值:表示可以持久存在的对象或内存位置

    • 可以 在赋值操作符"="左边的
    • 有内存,可修改,可以被取地址
    • ⭐但凡可以取地址&,那么它就是一个左值
    • 举例:变量,数组元素,指针解引用,字符串字面量(因为它是一块连续的内存,存放在静态数据区)
  • 右值:表示临时的、不可持久的值

    • 可以 在赋值操作符"="右边的
    • 没有内存,不可修改的值
    • 举例:通常字面量(除去字符串字面量),临时对象,表达式结果
  • 将亡值:马上要死了的值(涉及到资源转移)

    • 将亡值用来触发移动构造或移动赋值构造,并进行资源转移,之后调用析构函数(减少不必要的内存复制,提升性能)

    • 将亡值也是右值(无内存,不可修改)

    • 例如:生命周期只有一行的值,或者内存刚构建就要被释放的值

  • // a是左值,1是右值
    int a = 1;
    // b是左值,a是右值
    int b = a;
    

  • i++:给出 i 的值,再自己+1

  • ++i:先自己+1,再给出a

    一个给值,一个给a

  • i++右值,因为后置++操作时编译器会生成一份i值的临时复制,然后递增,返回临时复制内容

    • i++最后的结果是一个临时变量(临时值)
  • ++i左值,因为是直接对i递增后返回的自身

    • ++i最后的结果就是i

4.🍁🍁左值引用与右值引用的区别?🍁🍁

  • 引用:变量别名,声明时必须初始化


  • 1.顾名思义,左值引用就是对左值的引用,右值引用就是对右值的引用,右值引用的目的是为了对资源的转移

  • const左值引用能指向右值,局限是不能修改这个值

  • 可以通过std::move让右值引用指向左值,即左值强转为右值,就可以把左值的资源转移到其他对象当中

  • 声明出来的左值引用和右值引用都是左值

  • int a = 10;
    int& ref = a;    // 左值引用
    int&& rref = 10; // 右值引用
    
    // 引用本身是左值
    std::cout << &ref << std::endl;
    std::cout << &rref << std::endl;
    

  • 2.功能差异

  • 左值引用的作用:避免对象拷贝

    • 函数传参,函数返回值
    • 返回左值引用的作用:
  • 右值引用的作用:实现移动语义,或者实现完美转发

    • 1)移动语义
      • 实现移动语义就是资源转移
      • 对象赋值时,可以避免资源的重新分配
      • 移动构造以及移动拷贝构造
      • std::unique_ptr
    • 2)完美转发
      • 目的:代码保持简洁(维护性)
      • 完美指的不仅能准确转发参数的值,还能保证被转发的参数的左右值属性不变
      • 引用折叠
        • 参数为左值或者左值引用,T&& 将转化为 int&
        • 参数为右值或者右值引用,T&& 将转化为 int&&

参考:【码农Mark】左值引用与右值引用的区别?右值引用的意义?_bilibili


5.万能引用,引用折叠,完美转发

  • 万能引用(T&&auto&&

    • 使用 T&& 声明的引用(需类型推导),通过模板参数推导和引用折叠规则,可绑定到左值或右值
    • 即:能同时接受左值和右值的灵活引用,根据传入值类型自动调整自身类型
  • 类型推导:

    • 如果传入的是左值,T&& 会推导成左值引用(T&)
    • 如果传入的是右值,T&& 会推导成右值引用
  • 用途: 万能引用的灵活性使其适用于完美转发等场景,确保传递参数时类型不会丢失


  • 引用折叠

    • 说直接点就是用于在推导引用类型时折叠多层引用
    • 即:当出现引用的引用时,编译器会按照规则折叠为一个引用
    • 引用折叠的推导规则:
      • 1.左值引用推导结果就是一个左值引用
      • 2.非引用类型或者右值引用类型,推导结果就是右值引用
    • 通过引用折叠规则,T&& 会根据传入值自动调整成合适的引用类型:
    • 左值传递时,T&& 变成 T&
    • 右值传递时,T&& 保持为右值引用

  • 完美转发

    • 通过 std::forward<T> 保留参数的值类别,实现零拷贝的参数传递
    • 目的: 完美转发保证函数的原始参数类型(左值 / 右值)在转发过程中保持一致

6.🍁🍀关键字:const, static ,override,typedef🍀🍁

🍀const

  • const实现原理:

    • 编译器优化:在 C++ 中,const 变量在编译阶段被处理为 常量数据

      • 编译器看到const定义,知道是不可修改的,然后进行优化

      • 把所有const常量出现的地方都替代成原始值(常量值),避免了运行时的访问

      • const int x = 5;
        const int y = x * 2;  // 编译器会将 y 计算为 10,而不是在运行时进行计算
        

  • 作用:

  • 1.修饰常量:用于声明一个常量,表示常量的值不可修改(必须初始化)

    • 在C语言中const修饰的变量是 只读变量

    • 在c++中const修饰变量是 只读常量 ,编译器会自动优化成常量,不可修改,要修改加volatile

    • int x = 10;  // x 是普通变量
      x = 20;  	// x 的值被修改了
      
      const int y = 10;	// y 是常量
      y = 20;  		    // 错误:无法修改常量
      
  • 2.修饰指针:可以限制指针本身或者指针指向的内容不可修改

    • const char *s  VS 	char *(const  s)
      常量指针           	 指针常量
      前者const修饰的是  *s    ,后者const修饰的是s
      所以前者不能修改的是值    ,后者不能修改的是指针朝向
      反之前者可以修改指针朝向  ,后者可以修改数值
      
    • const修饰指针常量(修饰的是一个常量):不能修改指针本身指向的地址 ,但可以修改指针指向的内容(值)

    • const修饰常量指针(修饰的是一个指针):不能修改指针指向的内容(值),但可以修改指针本身指向的地址

  • 3.修饰函数参数:保护函数内部的参数数据,确保函数参数在函数内部不可被修改


  • 4.const修饰成员函数:C++ 中 const 修饰成员函数,表示该函数不会修改调用对象的状态(不会修改成员变量)

  • 5.const修饰常量引用:C++ 中常常使用 const 引用来避免不必要的拷贝,保证数据不被修改

  • 6.const修饰类成员变量const 还可以修饰类的成员变量,使其在对象生命周期内不可变

  • // 1.const修饰成员函数
    class MyClass 
    {
    public:
        void AfterWork() const 
        {  
            // const 成员函数,不会修改成员变量
            cout << "已下班,勿扰" << endl;
        }
    };
    ----------------------------------------------------------------------------------------
    // 2.const修饰常量引用
    void NoOverTime(const int &work) 
    {
        // x 不能被修改,避免了不必要拷贝
    }   
    ----------------------------------------------------------------------------------------
    // 3.const修饰类成员变量
    class MyClass {
    public:
        const int My_purse = 100;  // 你钱包里面的钱(My_purse) 是常量,无法修改
    };
    

  • 总结:const 关键字用于声明常量或不可修改的对象,防止其值在程序执行过程中被改变

🍀static

  • 答法1:

  • 1.修饰局部变量:修饰局部变量变为静态局部变量,延长局部变量的生命周期

    • 静态局部变量:函数结束时不会消失,每次函数调用时,不会重新分配空间,程序结束之后才会释放
  • 2.修饰全局变量:修饰全局变量变为静态全局变量

    • 静态全局变量:仅在当前文件访问,无法在其他文件访问
  • 3.修饰函数:修饰函数变为静态函数

    • 静态函数:仅在当前文件访问,无法在其他文件访问

  • 由此可总结出static的三个主要作用:

    • 1.隐藏修饰对象,一定程度上提高了封装性,避免了外部干扰
    • 2.保持变量内容的持久,因为static变量存放在静态存储区,所以具备持久性
    • 3.默认初始化为0,static变量存放在静态存储区,所以默认值为0

  • 答法2:

  • static(中文翻译:静态的,可以理解为全局统一的)

  • 1.修饰成员变量->静态成员变量

  • 2.修饰成员函数->静态成员函数

    • 成员变量 / 函数:在一个结构体或类中,出现的变量 / 函数

      • static修饰的就是静态成员变量和静态成员函数
      • 与之对应的,没有被static修饰的叫做非静态成员函数(属性)和非静态成员变量(方法)
      • 用我们常用的话来说,也就是属性和方法
    • 静态成员变量和静态成员函数 -> 全局统一的

    • 非静态成员函数(属性)和非静态成员变量(方法) -> 与实例(或者说对象,也就是属性+方法)相关

    • struct Test
      {
          // 静态成员变量和静态成员函数
          static int sta_member;
          static void sta_method();
          // 非静态成员变量和非静态成员函数
          int member;
          void method();
      };
      ---------------------------------------------------
      void Demo()
      {
          Test a, b;
          a.sta_member = 6;
          std::cout << b.sta_member;		// 6
          std::cout << Test::sta_member;	// 6
      }
      ----------------------------------------------------
      void Demo()
      {
          Test a, b;
          a.member = 5;
          b.member = 8;
          std::cout << a.member;		// 5
          std::cout << Test::member;	// error:我不知道你调用的是哪一个结构体的
          						  // Test::member不明确指定是哪个对象的member
      }
      

  • 3.修饰全局变量->静态全局变量

  • 4.修饰普通函数->静态普通函数

    • ⭐static修饰的全局变量和普通函数,仅在当前文件访问,无法在其他文件访问
  • 5.修饰局部变量->静态局部变量

    • ⭐static修饰局部变量变为静态局部变量,延长局部变量的生命周期

    • 静态局部变量的生命周期:第一次初始化时创建,程序结束时释放

    • [!IMPORTANT]

      • 普通局部变量
        • 当函数被调用时,局部变量会在栈上分配内存,函数返回后变量会被销毁,每次函数调用时都会重新初始化
      • 静态局部变量
        • 静态局部变量的内存分配不再是在栈上,而是在程序的静态存储区(通常是数据段)
        • 它的生命周期会持续到程序结束
        • 因此,即使函数多次调用,静态局部变量也不会被销毁,而是保持上次的值
    • void func()
      {
      	int a = 0;		// 非静态局部变量
      	static b = 0;	// 静态局部变量
      	a++;	// 每次调用时,a都会重新初始化为0,然后加1
      	b++;	// b是静态的,因此只会在第一次初始化时赋值为0,之后每次调用都会保持上次的值
      	std::cout << a << std::endl;
      	std::cout << b << std::endl;
      }
      
      void Test()
      {
      	func();		// 1,1
      	func();		// 1,2
      	func();		// 1,3
      }
      

🍀override

  • 一般用于检查派生类重写基类的虚函数是拼写否正确,接口是否一致,确保该函数是虚函数
  • 人话:检测函数有没有写对 + 这个函数是不是虚函数

🍀typedef

  • typedef数据类型重命名(提高移植性、可读性、编码效率)
    • 结构体类型,函数指针(你也不想让你的智能指针长达好几十个字符吧),数组类型,复杂类型等
    • C和C++中的typedef其实作用一模一样
    • 只不过C++中支持的类型更多,例如模板类型

参考:5分钟讲透C++的static(5分钟实在讲不完,超时了很抱歉【手动狗头】)_bilibili
大复制粘贴术,终于可以引用自己的帖子了
参考:嵌入式面试题-C语言总结(一) - Ronronner_Official - 博客园


7.命名空间和内联命名空间的作用以及注意事项

  • 命名空间:命名空间是C++中为了组织代码和解决命名冲突的问题的一种机制

    • 它能够将一组相关的标识符(如函数、变量、类等)放在一个单独的作用域内

    • C语言是使用static关键字解决命名冲突问题

    • 注意事项:

      • 1.命名空间在多个文件中使用:C++ 中的命名空间不仅限于当前文件

      • 2.命名空间成员相同:在同一个命名空间内,成员(如变量、函数等)不能重复定义,否则,编译器会报错

      • 3.命名空间名称相同:不同的源文件中定义相同名称的命名空间,它们会自动合并


  • 内联命名空间:内嵌命名空间的成员默认导入到父命名空间中,使用时 无需指定内嵌命名空间

    • 作用:

      • 1.方便的版本控制:通过内联命名空间,旧版库的功能可以继续使用,而新版功能可以被引入且无需显式指定版本
      • 2.提升兼容性:使用内联命名空间,可以在不修改现有代码的情况下,进行功能的更新或版本控制
    • 注意事项:

    • 1.只能有一个内联命名空间

      • 在同一个命名空间中,inline namespace 只能声明一次
      • 即不能在同一个命名空间内有多个内联命名空间
    • 2.成员自动暴露

      • 内联命名空间中的成员会自动暴露到外部命名空间中,使得使用者可以直接访问,而不需要显式指定内联命名空间
    • 3.常用于版本控制

      • 在库更新时,可以通过内联命名空间方便地管理不同版本的功能,确保新版本功能与旧版本兼容
    • // 理解不了?那我们来说点人话:
      // 内联命名空间实际上就是在一个命名空间里面使用了inline namespace,相当于命名空间套用命名空间
      // 它的作用是在父命名空间中“内嵌”一个子命名空间,使得子命名空间的成员能够直接通过父命名空间访问
      // 但是呢,这个所谓的子空间只能出现一个
      namespace MyLibrary 
      {
          // 内联命名空间
          inline namespace v1 
          {
              void feature() { std::cout << "Version 1" << std::endl; }
          }
      
          namespace v2 
          {
              void feature() { std::cout << "Version 2" << std::endl; }
          }
      }
      
      int main() 
      {
          MyLibrary::feature();      // 自动调用 v1 版本的 feature()
          MyLibrary::v2::feature();  // 显式调用 v2 版本的 feature()
          return 0;
      }
      
      

8.autodecltype

  • auto

    • 1.auto是关键字,不是数据类型,用于获取一个变量的类型
      • 也就是常说的自动类型推导,即:让编译器根据初始化表达式推断变量所属的类型
    • 2.必须有初始值:使用 auto 时,因为编译器通过初始化值推导类型,所以变量必须被初始化
    • 3.运行时推导:auto 通过表达式的值来推导类型

  • decltype

    • 1.也是一个关键字,用于获取一个表达式的类型
    • 2.无需初始化:decltype 可以用于没有初始化的变量或表达式,可以直接获取类型
    • 3.编译阶段推导:decltype 是在编译阶段进行类型推导,允许不需要初始化

9.anyoptional

  • any:C++17引入的一种存储任意类型的容器

    • 允许存储任意类型的对象,而不需要事先知道对象的具体类型(具有类型安全性)

    • 优点:

      • 类型安全:与 C 中的 void* 不同,std::any 具有类型安全性,确保类型转换时不会出错。
      • 动态存储:可以动态地存储和访问不同类型的对象,无需事先定义类型
    • 缺点:

      • 1.分配类型不可控,会产生内存泄露问题

      • 2.保存任意类型,释放很麻烦, 释放难度大(引出optional)

        • 建议使用std::variant或者std::optional更好

        • [!NOTE]

          何时使用 std::anystd::variantstd::optional

          • std::any:适用于你不知道存储的类型,或者类型在运行时才决定的情况
          • std::variant:适用于你知道一组有限类型,并且存储的值属于这组类型中的某一类型
          • std::optional:适用于需要表示值是否存在的情况,通常用于表示一个可能为空的值

  • optional

    • C++17引入的一个模板类
    • std::optional:用于表示一个 可能缺失的值,可以存储一个值或没有值
  • 特点

    • 类型安全,避免了 nullptr 的问题
    • 可以通过 has_value()value()* 来检查和访问值
    • std::nullopt 表示空值
    • value_or() 提供默认值,reset() 清空值
  • 应用场景:函数返回值可能为空,或需要表示缺失数据的场景


10.C++中的结构体的特点,和class类相比有什么区别

  • c 语言中的结构体

    • 1.纯粹的数据容器,只能保存成员属性(成员变量),不能保存成员方法(成员函数)
    • 2.结构体默认成员访问权限是公开的(public)
    • 3.结构体类型通常使用 typedef 来创建类型别名

  • c++中的结构体

    • 1.C++ 结构体默认成员访问控制是公开的,但可以通过 privateprotected 控制访问权限
    • 2.C++ 结构体既可以包含数据成员,也可以在结构体内定义函数
    • 3.C++ 结构体可以像类一样支持继承、虚函数和多态
    • 4.C++ 结构体支持构造函数和析构函数,用于初始化和清理对象
    • 5.C++ 结构体支持运算符重载,可以定义自定义行为
    • 6.C++ 支持 typedef,并且可以使用 using 来创建类型别名
    • 7.结构体名称可以单独作为类型出现

  • C++中的结构体(struct)VS 类(class)的区别

  • 1.默认访问控制权限

    • 结构体 (struct):成员默认是 公开(public)的
      • 定义上,可以
    • 类 (class):成员默认是 私有(private)的
  • 2.访问控制

    • 结构体和类都可以通过 publicprotected 来改变访问控制
  • 3.功能差异

    • 功能没有本质区别,二者都可以拥有成员属性、成员方法、构造函数、析构函数、继承、虚函数、运算符重载等
  • 4.使用习惯

    • 结构体:在 C++ 中,结构体通常用于保存简单的、只包含数据的对象,尽管它可以像类一样包含成员函数
    • 类:类通常用于表示复杂的对象,包含更多的封装性、抽象性和行为
  • 5.内存布局

    • 结构体

    • 结构体对象的大小是由它的成员属性(成员变量)决定的,所有对象的成员变量是独立存储的

      • 成员属性(成员变量):每个结构体(或类)对象都有自己的数据成员,内存中的存储位置是独立的

      • 成员方法(成员函数):不占用每个对象的内存,因为它们是共享的

        • 即:所有对象共享同一份成员方法的代码,它们的地址空间是全局的,不会占用对象的大小
      • 虚函数:结构体有虚函数时,引入虚指针 (vptr) 和虚函数表 (vtable),占用额外内存

      • 内存对齐:根据平台的对齐要求,编译器可能插入填充字节,导致内存大小有所变化

        • 反正面试的时候我不太会说这个,问就是不写一下,那种高度紧张情况下算不出来啊

    • 类对象的内存大小由 成员属性(成员变量)决定,成员方法的大小不影响类对象的内存布局

      • 成员属性:类的每个对象都有独立的内存空间来存储其成员属性(即数据成员),每个对象会为这些属性分配内存

      • 成员方法:类的成员方法是共享的,不占用每个对象的内存

      • 虚函数:如果类包含虚函数,则类对象将增加一个 虚指针(vptr),指向虚函数表(vtable

        • 这个虚指针是每个对象独立的,并会占用额外的内存

        • 因此,包含虚函数的类通常比不包含虚函数的类对象占用更多的内存


    • 总结:

    • 结构体和类的主要区别:默认的访问控制权限(struct 默认公开,class 默认私有),但它们的功能差异不大,二者都支持类似的特性,如成员方法、继承、虚函数等。内存布局上的差异主要体现在虚函数的引入上,虚指针会增加额外的内存开销


11.函数重载的条件?为什么引入函数重载?函数重载的实现原理?

  • 函数重载:不同的函数功能可以使用相同的函数名
    • 作用:解决函数命名冲突的问题,提高代码复用性
    • 函数返回值不同不能构成重载,因为编译器通过函数参数来区分不同的重载,而返回值类型并不是区分函数的依据
    • 重载的规则:
      • 1.参数数量不一样
      • 2.参数类型不一样
      • 3.参数顺序不一样
    • 函数重载实现原理:
      • 编译器根据调用参数的不一致(参数数量,类型和顺序)选择匹配的函数版本
        • 也就是常说的编译时多态
        • C++里面静多态就是函数的重载
        • 静多态:编译器进行函数地址绑定(早绑定)
      • 返回值类型不参与重载决议

12.普通成员与静态成员?

  • 1.定义

    • 普通成员:属于类的对象
    • 静态成员:属于类本身
  • 2.内存分配

    • 普通成员:每个对象创建时分配内存,实例化时存在
    • 静态成员:类加载时分配内存,所有对象共享同一地址
  • 3.生命周期

    • 普通成员:与对象生命周期相同,实例化时(类创建对象时)创建,销毁时释放
    • 静态成员:与程序生命周期相同,类加载时初始化,程序退出时销毁
  • 4.访问权限

    • 普通成员:可以访问对象的普通成员和静态成员
    • 静态成员:只能访问静态成员,不能访问普通成员
  • 5.初始化

    • 普通成员:在类中(构造函数)初始化
    • 静态成员:必须在类外进行初始化
  • 6.共享方式

    • 普通成员:每个对象单独共享

    • 静态成员:全局共享(所有对象共享同一份,类的所有实例共享同一个静态成员)


    • 对象:类的实例

    • 对象是类定义的一个具体实例,拥有类中定义的属性(成员变量)和行为(成员函数)

    • 狂猜你已经忘记了对象的概念,特意拿出来,快说谢谢博主

    • 虽然后面估计也就只有我一个人看这个文章用来复习.......


13.普通函数与静态函数?

  • 1.定义

    • 成员函数:成员函数是类内部定义的函数,它作用于类的对象
    • 静态函数:静态成员函数是类内部定义的函数,但它不依赖于类的实例(对象)
  • 2.访问权限

    • 成员函数:可以访问类的非静态成员(成员变量和其他成员函数)
    • 静态函数:它只能访问类的静态成员(静态成员变量和其他静态成员函数),不能访问非静态成员
  • 3.函数调用

    • 成员函数:必须通过类的对象来调用
    • 静态函数:必须通过类名或者对象来调用
  • 4.举例

    • 成员函数:普通函数,构造函数,析构函数等
    • 静态函数:适用于与类的所有对象共享的功能,例如记录总的对象数量、日志记录等
  • 5.this指针

    • 成员函数:有 this 指针,指向调用该函数的对象,可以访问对象的非静态成员
    • 静态函数:没有 this 指针,因为它与具体的对象无关,不能访问非静态成员

  • 总结

    • 成员函数:依赖于类的对象,可以操作对象的状态,能访问所有成员(静态和非静态)
    • 静态成员函数:不依赖于对象实例,只与类的静态成员相关,能访问静态成员

14.🍁🍀Lambda表达式🍀🍁

  • 语法:

    [ 捕获列表 ] ( 参数列表 ) -> 返回类型 { 函数体 };
    // 捕获列表:指定如何捕获外部作用域的变量,可以 按值捕获/按引用捕获
    // 参数列表:与普通函数的参数列表相同,可以为空
    // 返回类型:指定返回值的类型,若不指定,则自动推断
    // 函数体:Lambda 函数的实现
    
    // 知道你会说不听不听王八念经,这样呢?
    auto func = [ 捕获列表 ] ( 参数列表 ) -> 返回类型 
    { 
        函数体 
    };
    ---------------------------------------------------------------
    // 示例
    // 无参数
    auto func = []() 
    {
        return 1; 
    };
    // 带参数的 Lambda 表达式
    auto add = [](int a, int b) 
    { 
        return a + b; 
    };
    
  • 1.定义

    • Lambda表达式:C++11 引入的一种匿名函数,允许在代码中定义内联的,临时的函数
      • Lambda表达式也可以在函数内部使用,但必须注意,Lambda捕获局部变量的引用后,局部变量生命周期结束
      • 匿名函数:没有名字的函数,通常不需要单独定义一个函数或函数对象,直接在需要的位置定义
  • 2.作用

    • 简化函数定义,提高了代码的可读性,运行效率以及性能
      • Lambda表达式本质是匿名函数,能捕获一定范围的变量,匿名函数在使用的过程中不会产生调用和返回,提高了运行效率
      • 其次,Lambda表达式还提供了一种函数式编程的机制,能够前置在编译期进行处理,提高程序的性能
  • 3.捕获方式

    • 1)按值捕获[=]
    • 2)按引用捕获[&]
    • 3)不捕获:[]
    • 4)捕获this指针[this]
    • 5)捕获变量:[变量名]:(多个变量用逗号分隔)
  • 4.注意事项

    • *引用悬挂:指一个引用(或指针)指向了一个已经被销毁或不再有效的对象

      • Lambda捕获局部变量的引用后,局部变量生命周期结束

      • void createLambda() 
        {
            int x = 10;
            auto lambda = [&x]() 
            {
                std::cout << x << std::endl;  // 捕获x的引用
            };
            
            // x在此处仍然有效
            lambda();  // 正常输出 10
            
            // 这里结束后,x会被销毁
        }
        ------------------------------------------
        // 引用悬挂示例(写代码不行,但是写bug这个我会)
        #include <iostream>
        #include <functional>
        
        std::function<void()> createLambda() 
        {
            int x = 10;
            auto lambda = [&x]() {
                std::cout << x << std::endl;  // 捕获x的引用
            };
            return lambda;  // 返回lambda
        }
        
        int main() 
        {
            auto lambda = createLambda();
            // 此时x已经超出作用域并被销毁
            // 继续使用lambda表达式会出现悬挂引用行为
            lambda();  // 未定义行为,可能会崩溃或输出垃圾值
            return 0;
        }
        

15.仿函数(函数对象)

  • 1.定义

  • 仿函数(函数对象):指一个实现了operator()的类的对象可以像普通函数一样被调用

  • 仿函数与Lambda表达式类似,都允许将函数作为对象传递和执行,但仿函数是通过类的对象来实现的

    • 仿函数是什么:让类的对象像函数一样可以被调用
    • 为什么用仿函数:函数指针可扩展性比较差
    • 仿函数怎么实现的:类重载小括号 ( )
  • #include <iostream>
    
    // 仿函数:让类的对象函数一样可以被调用
    // 原理:构造时传变量 和  类中重载中括号
    class Func
    {
    public:
        // 构造的时候传一个变量C
        Func(int c) : c(c) {}
        // 重载小括号()
        int operator()(int a, int b)
        {
            return a + b + c;
        }
    private:
        int c;
    };
    
    int add(int a, int b) { return a + b; }
    int minus(int a, int b) { return a - b; }
    
    // 使用类的对象
    void func(Func& f, int a, int b)
    {
        std::cout << f(a, b) << std::endl;
    }
    
    int main()
    {
        // func(add, 1, 2);
        // 仿函数:让类的对象函数一样可以被调用
        Func f(3);
        std::cout << f(1, 2) << std::endl;
    
        return 0;
    }
    
  • 2.作用:

    • 仿函数的主要作用是使得对象可以像函数一样被调用
      • 1)函数对象的传递:在需要传递一个函数给其他函数(如STL算法)时,可以使用仿函数
      • 2)状态保持:与普通函数不同,仿函数可以通过类的成员变量保持状态
        • 它能够在多次调用之间保存信息,使得每次调用时可以访问或修改内部状态
        • 例:在仿函数中定义一个计数器,记录函数调用的次数,或者在调用过程中积累结果
      • 3)性能优化:仿函数的调用通常是内联的,避免了函数指针的开销,提升了性能
        • 相比于函数指针,仿函数通常执行更高效,因为其调用不需要间接寻址
  • 3.优点

    • 高效性:仿函数通常会被内联调用,因此其性能优于使用普通函数指针
    • 灵活性:仿函数可以携带状态,可在多个调用之间保留信息,而普通的函数指针没有这个功能
    • 可以隐藏函数类型:仿函数能够封装函数的类型,实现更加灵活和抽象的设计
    • 自定义行为:仿函数能够定义复杂的行为,比如不同的比较方式、过滤条件等
  • 4.缺点:

    • 代码复杂度高,更没有Lambda表达式简洁
  • 5.使用场景

    • STL算法:许多STL算法(如std::sort)都接受一个仿函数作为参数
      • 仿函数通常用于自定义比较、判断或变换操作
    • 自定义行为:仿函数可以保持一些状态,可以通过类的成员变量在多次调用之间保存信息
  • 6.仿函数与Lambda表达式

    • 相同点
      • 都是可调用对象,可以在算法中作为参数传递
      • 都支持自定义行为
    • 不同点
      • 定义:仿函数通过类和operator()实现,而Lambda是匿名的、内联的函数定义
      • 状态持有:仿函数可以通过类成员变量保存状态,而Lambda的状态通过捕获方式指定
      • 简洁性:Lambda语法更简洁,适用于较小的函数或短期使用的函数,而仿函数通常用于需要复杂行为或状态保持的场景

C++的东西是真的多啊,感觉就是一锅大杂烩,什么编程语言的它多多少少都有一些
春节回老家没网,整理了但是一直没有发布,屯了45道题,一口气全部发了

posted @ 2025-02-13 16:56  假设狐狸有信箱  阅读(123)  评论(0)    收藏  举报