嵌入式面试题 - 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&&
- 参数为左值或者左值引用,
- 1)移动语义
参考:【码农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.auto,decltype
-
auto- 1.
auto是关键字,不是数据类型,用于获取一个变量的类型- 也就是常说的自动类型推导,即:让编译器根据初始化表达式推断变量所属的类型
- 2.必须有初始值:使用
auto时,因为编译器通过初始化值推导类型,所以变量必须被初始化 - 3.运行时推导:
auto通过表达式的值来推导类型
- 1.
-
decltype- 1.也是一个关键字,用于获取一个表达式的类型
- 2.无需初始化:
decltype可以用于没有初始化的变量或表达式,可以直接获取类型 - 3.编译阶段推导:
decltype是在编译阶段进行类型推导,允许不需要初始化
9.any与optional
-
any:C++17引入的一种存储任意类型的容器
-
允许存储任意类型的对象,而不需要事先知道对象的具体类型(具有类型安全性)
-
优点:
- 类型安全:与 C 中的
void*不同,std::any具有类型安全性,确保类型转换时不会出错。 - 动态存储:可以动态地存储和访问不同类型的对象,无需事先定义类型
- 类型安全:与 C 中的
-
缺点:
-
1.分配类型不可控,会产生内存泄露问题
-
2.保存任意类型,释放很麻烦, 释放难度大(引出optional)
-
建议使用
std::variant或者std::optional更好 -
[!NOTE]
何时使用
std::any、std::variant或std::optionalstd::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++ 结构体默认成员访问控制是公开的,但可以通过
private和protected控制访问权限 - 2.C++ 结构体既可以包含数据成员,也可以在结构体内定义函数
- 3.C++ 结构体可以像类一样支持继承、虚函数和多态
- 4.C++ 结构体支持构造函数和析构函数,用于初始化和清理对象
- 5.C++ 结构体支持运算符重载,可以定义自定义行为
- 6.C++ 支持
typedef,并且可以使用using来创建类型别名 - 7.结构体名称可以单独作为类型出现
- 1.C++ 结构体默认成员访问控制是公开的,但可以通过
-
C++中的结构体(
struct)VS 类(class)的区别 -
1.默认访问控制权限
- 结构体 (
struct):成员默认是 公开(public)的- 定义上,可以
- 类 (
class):成员默认是 私有(private)的
- 结构体 (
-
2.访问控制
- 结构体和类都可以通过
public或protected来改变访问控制
- 结构体和类都可以通过
-
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捕获局部变量的引用后,局部变量生命周期结束
- 匿名函数:没有名字的函数,通常不需要单独定义一个函数或函数对象,直接在需要的位置定义
- Lambda表达式:C++11 引入的一种匿名函数,允许在代码中定义内联的,临时的函数
-
2.作用
- 简化函数定义,提高了代码的可读性,运行效率以及性能
- Lambda表达式本质是匿名函数,能捕获一定范围的变量,匿名函数在使用的过程中不会产生调用和返回,提高了运行效率
- 其次,Lambda表达式还提供了一种函数式编程的机制,能够前置在编译期进行处理,提高程序的性能
- 简化函数定义,提高了代码的可读性,运行效率以及性能
-
3.捕获方式
- 1)按值捕获
[=] - 2)按引用捕获
[&] - 3)不捕获:
[] - 4)捕获
this指针:[this] - 5)捕获变量:
[变量名]:(多个变量用逗号分隔)
- 1)按值捕获
-
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)都接受一个仿函数作为参数- 仿函数通常用于自定义比较、判断或变换操作
- 自定义行为:仿函数可以保持一些状态,可以通过类的成员变量在多次调用之间保存信息
- STL算法:许多STL算法(如
-
6.仿函数与Lambda表达式
- 相同点:
- 都是可调用对象,可以在算法中作为参数传递
- 都支持自定义行为
- 不同点:
- 定义:仿函数通过类和
operator()实现,而Lambda是匿名的、内联的函数定义 - 状态持有:仿函数可以通过类成员变量保存状态,而Lambda的状态通过捕获方式指定
- 简洁性:Lambda语法更简洁,适用于较小的函数或短期使用的函数,而仿函数通常用于需要复杂行为或状态保持的场景
- 定义:仿函数通过类和
- 相同点:
C++的东西是真的多啊,感觉就是一锅大杂烩,什么编程语言的它多多少少都有一些
春节回老家没网,整理了但是一直没有发布,屯了45道题,一口气全部发了

浙公网安备 33010602011771号