C++/Qt自制八股文

简介:

这篇文章是自己在准备面试的时候,也碰到了不少不会的面试题,所以希望能将自己的八股文分享出来,供大家一起学习参考使用,八股文中也有一些自己的理解,如果八股文中有可以不对的或者需要补充的,大家也可以在评论区中留言,我都会进行查看和修改。

以下开始为正文:

重载重写隐藏的区别

  • 重载 就是同名函数,但类型不同或者说参数个数不同参数顺序不同,这个叫重载,静态多态;动态多态就是虚函数

  • 重写 或者说叫覆盖直接把整个函数都重写了,比如说我自己的Qt项目里面就是把鼠标移动这个事件给重写了,让他可以跟随窗口移动

  • 隐藏:子类中定义了与父类中同名的方法或属性,从而"隐藏"了父类中的方法或属性。当在子类中调用同名的方法或属性时,会调用子类中的实现,而非父类中的实现。

区别:隐藏和重载范围不同,隐藏发生在不同类中

参数区别:隐藏和被隐藏的函数参数列表可以相同,也可以不同,但函数名一定要相同;

当参数不同时,无论基类中的函数是否被virtual所修饰,基类都会被隐藏而不是重写。

重载强调函数名称的多态性 ,而重写强调对象之间的多态性。重载是编译时的多态,而重写是运行时的多态。

面向对象三大特征

封装、继承、多态

多态:公交车的例子,学生卡,普通人,老人卡,同样的行为但是产生不一样的结果

多态的实现主要分为静态多态和动态多态,静态多态主要是重载 ,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完 全不同的,覆盖(重写)是动态 态绑定的多态,而重载是静态绑定的多态。

封装:把一个客观的实物抽象成类,每个类对他自己的数据和方法实行protection(共有,私有,保护)

继承:广义的继承有三种方式:实现继承、可视继承、接口继承

怎么理解多态1

多态,就是一个函数多种形态;

比较常见的就是函数重载和函数重写,也就是我们所说的静态多态和动态多态;

下面我分开说说我对动态的理解

函数重载,也叫编译时多态,就是在同一个作用域下面有一种相同的函数名,不同的参数列表的个数或者不同返回值的函数,这叫函数重载

如果没有函数重载,比如说你定义一个加法add函数,那可能就需要定义整型+整型,double + double,double + int等各种类型,

那么这就要为实现同一个函数,写很多个名字,这样做非常不友好;

为什么有重载的概念?他的原理是什么?阐述内涵

我也研究过函数重载的底层实现,反汇编试了一下,就是真正的函数名是跟参数类型相关的,虽然在C++的源码里头,函数名一样

但是在汇编里面,它有一个label,label它是跟参数相关的,但是在C语言里面不会(C 语言编译器不执行 名字改编),这就是C语言不支持汇编的根本原因

另一种就是函数重写,重写是在派生类覆盖基类的同名函数,也叫函数覆盖,要求基类函数必须是虚函数(是为了让C++编译器能够启动动态绑定机制)

我也研究过函数重写的原理,就在C++对象模型里面,编译器在发现基类中有虚函数,会对每个含有虚函数的类,生成一份虚函数表

这个虚函数表,可以理解为一个一维数组,里面包含了各个虚函数的一个入口地址,然后编译器会在每个对象前4个字节或者8个字节

保存一个虚函数表 指向指针,就是我们的虚函数表指针

在构造时,根据对象的类型去初始化虚函数表指针,然后让虚函数表指针指向正确的虚函数表,从而在调用的时候能找到正确的函数

(可不说)在这里我也看过很多动态多态的汇编代码,也去了解了用函数指针去实现,人为修改了虚函数的一个偏移地址的函数调用,就更加验证了自己的猜想

也研究了虚函数表的一个内存分布,发现虚函数表其实本质上就是一个数组,但是不存在于堆区栈区,也不存在于代码区 全局区,而是存在于一个常量区

也叫数据段,就是因为一个类但凡有虚函数,那么这个虚函数表,肯定是全局可见的,并且不能被修改

(可不说)我也反汇编去改了,结果调试器不让改,修改非法

所以是在这个常量区,这就是我对多态的一个研究和理解

一个类的对象去找虚函数,它是如何找到对应的虚函数的?

一个类的对象调用虚函数时,核心是靠 对象里的虚函数指针(vptr) + 类的虚函数表(vtable 完成查找,全程是运行时动态匹配的过程,和普通函数的编译时绑定完全不同。

关键补充(面试常考细节)

  1. 索引固定是多态的基础 :父类和子类的虚函数表,同名字的虚函数索引完全一致 。比如 speak()AnimalCatvtable 中都是索引 0,这样基类指针才能正确找到子类的重写函数。

  2. 普通函数不走这个流程 :非虚函数的地址在编译时就确定了,调用时直接跳转到函数地址,不经过 vptrvtable

  3. 新增虚函数会追加索引 :如果子类新增虚函数(如 Catvirtual void climb()),该函数地址会追加到 vtable 末尾,索引按声明顺序递增。

一句话总结

对象调用虚函数 = 拿自己的 vptr 找所属类的 vtable → 按固定索引取函数地址 → 执行函数,这个过程是运行时完成的,也是动态多态的底层原理。

指针和引用的区别?

**相同点:**都能够间接访问变量、都支持多态、都能在函数传参时避免拷贝、引用是指针的语法糖,在汇编层面引用通过指针实现

不同点:

指针是一个变量 ,引用是一个变量的别名

指针可以不初始化,也可以初始化为空指针(nullptr),引用声明时必须初始化为一个有效的对象,不能初始化为空;

用sizeof看指针的时候是4 ,而引用的话得看被引用对象的大小

指针可以有多级指针 ,引用只有一级

引用可以理解为指针常量;

有const指针,但没有const引用

指针和引用都是用来访问内存中的数据的,它们在以下情况下是相同的:

  1. 传递参数时:指针和引用都可以用于传递函数参数。在这种情况下,它们可以被用来修改传递给函数的参数的值。

  2. 访问数组元素时:指针和引用都可以用来访问数组元素。

  3. 访问对象成员时:指向对象的指针和对象的引用都可以用来访问对象的成员。

但是,指针和引用之间还有一些重要的区别。例如,指针可以被重新赋值为指向另一个对象,而引用必须在创建时初始化,并且不能被改变指向其他对象。此外,指针可以为空(NULL),而引用始终指向某个对象。

vector和list的区别?

vector和list是C++ STL中两种不同的数据结构,它们之间的区别主要有以下几个方面:

  1. 存储方式:vector是基于连续内存空间实现的动态数组,而list是基于双向链表实现的。

  2. 访问效率:vector支持随机访问,可以通过下标直接访问元素,因此访问效率比较高;而list只支持顺序访问,无法通过下标直接访问元素,需要遍历列表才能访问指定元素,因此访问效率比较低。

  3. 内存分配:vector在创建时需要预先分配一段连续的内存空间,如果需要增加容量,则需要重新分配更大的连续内存空间并将原有数据复制到新的内存中;而list则可以动态地分配和释放内存,插入或删除元素时,只需要调整节点的指针即可,不需要移动其他元素。

  4. 插入和删除操作:由于vector底层实现是数组,因此在中间位置插入或删除元素时,需要将后续元素依次移动,因此效率较低;而list通过修改节点的指针来完成插入和删除操作,因此效率较高。

  5. 迭代器失效:由于vector底层实现是数组,所以在对vector执行添加、删除操作时,可能会导致迭代器失效;而list的节点不会被移动,因此在对list进行添加、删除操作时,迭代器仍然有效。

总体来说,当需要快速随机访问元素时,应该使用vector;当需要频繁的插入和删除操作,并且不需要随机访问元素时,应该使用list。

总结:vector是连续存储的动态数组,支持随机访问,增删尾部元素快、中间慢;list是双向链表,不支持随机访问,增删任意位置元素快。

vector适合频繁随机访问、尾部增删的场景;list适合频繁在任意位置增删的场景。

vector的扩容机制是什么?

vector扩容时会申请新的更大内存(通常是原容量的 1.5~2 倍),将原数据拷贝到新内存,释放旧内存。

MVSC 是 1.5倍,GCC用的2倍

class和struct的区别?

在C++中,class和struct做类型定义是只有两点区别:

默认继承权限不同,class继承默认是private继承 ,而struct默认是public继承

class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数 C++保留struct关键字,原因

保证与C语言的向下兼容性,C++必须提供一个struct

C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制

对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

extern C的作用是什么?在代码中加了这个和不加这个有什么区别?

extern "C"的本质是让 C++ 编译器按 C 语言的规则处理代码,解决跨语言编译 / 链接的兼容性问题

extern "C"的核心是让 C++ 编译器按 C 语言的名字修饰 / 调用约定处理代码,解决 C/C++ 混合编程的链接问题;

不加extern "C":C++ 函数会被名字修饰,无法被 C 代码调用,链接时会报 "未定义引用";

extern "C":函数按 C 规则生成符号名,兼容 C 语言的调用,是 C/C++ 混合编程的必备手段。

定义一个static成员函数有什么作用

定义一个 static 成员函数,可以使该函数独立于类的任何对象,也就是说,它可以直接通过类名调用,而不需要依赖于任何特定的类实例。

这种方式非常有用,因为 static 成员函数可以在不创建类实例的情况下执行某些操作,比如:

  1. 计算、转换或验证输入数据;

  2. 操作静态成员变量或全局变量;

  3. 实现单例模式;

  4. 提供实用工具函数等;

从性能和设计角度考虑,使用 static 成员函数可以避免创建对象时的开销,同时保持代码的可读性和可维护性。

用最通俗的话讲,static 核心就干两件事:

一是 "锁范围"------ 让变量 / 函数只能在指定范围内用,不被外面随便访问;二是 "固寿命"------ 让变量不随函数 / 对象销毁而消失,全程都在

static变量存在哪个区

在 C++ 中,static 变量分配在静态存储区 中,也称为全局数据区或者 .data 段,而函数内部创建普通变量上分配内存。

静态存储区是一块固定的内存区域,在程序启动时被分配,直到程序退出时才被释放。所有 static 变量都在该区域中分配了固定的内存空间,并且它们在整个程序的生命周期内都存在。因此,一旦 static 变量被初始化,它们的值可以在程序的不同函数调用之间保持不变。

需要注意的是,静态存储区中的变量默认情况下会进行零初始化,也就是将变量的初始值设置为 0。如果 static 变量在定义时显式指定了初值,则其初值将覆盖默认的零初始化。

析构函数为什么要定义为虚函数

C++ 中,析构函数是用于释放对象占用的资源、清理数据成员并销毁对象自身的特殊成员函数。当一个类中定义了虚函数时,会生成一个虚函数表(Virtual Table) ,其中记录了该类所有虚函数的地址。在程序运行时,每个对象都有一个指向虚函数表的指针(Vptr)来实现动态绑定。

如果一个类中存在虚函数,且该类的对象被删除时,需要先调用对象的析构函数,然后再从虚函数表中查找并调用其基类的析构函数,以确保释放对象所占用的所有资源和内存空间。如果析构函数不是虚函数,则只会调用当前类 的析构函数,而不会调用基类 的析构函数,导致析构不完全 ,可能会出现内存泄漏等问题。

因此,在设计继承关系时,如果基类含有虚函数,那么可以将其析构函数定义为虚函数,以便在对派生类进行析构时能够正确地调用基类的析构函数,释放所有分配的内存并维护整个对象的正确状态。

总之,将析构函数定义为虚函数是一种良好的编程习惯,也是 C++ 中正确使用继承机制的必要条件,可以避免因析构不完全导致的各种错误,并确保程序的正确性和健壮性。

在 C++ 中,如果一个类含有虚函数,那么这个类的对象在被销毁时,需要调用其析构函数以释放资源和内存空间。当涉及到继承关系时,如果子类对象被销毁时只调用了自己的析构函数而没有调用父类的析构函数,那么可能会导致父类中资源没有被正确地释放,出现内存泄漏等问题。

为了避免这种情况的发生,需要将基类的析构函数定义为虚函数。这样,在通过基类指针或引用删除指向派生类对象的指针或引用时,会先调用派生类的析构函数,再调用基类的析构函数,保证所有的资源都能够被正确地释放。

因此,如果你的类涉及到继承关系并且其中包含有虚函数,那么就应该将析构函数定义为虚函数,以确保程序不会出现因析构顺序错误而导致的各种问题。

当基类指针 / 引用指向子类对象时,若基类析构函数不是虚函数,只会调用基类析构函数,子类析构函数不会执行,导致子类持有的资源内存泄漏;将基类析构函数定义为虚函数后,会触发多态调用,先执行子类析构,再执行基类析构,保证所有资源被正确释放。

如何避免"野指针"

野指针是指指向未知、无效或已释放的内存地址的指针,它可能会导致程序崩溃或其他未定义行为。为了避免野指针的出现,可以采取以下几种方式:

  1. 初始化指针:确保指针在使用之前被初始化为一个有效的地址或空指针(nullptr),而不是随机的地址值。

  2. 避免悬挂指针:当一个指针指向一个已释放的内存块时,该指针就成为了悬挂指针。为了避免悬挂指针的出现,应该尽量避免手动管理内存,而是使用智能指针等自动管理内存的工具。

  3. 使用 const 和引用:在函数中参数使用 const 或者引用传递参数,可以避免因拷贝造成的野指针问题。

  4. 查找指针错误:在程序运行过程中,如果发现程序出现了奇怪的行为或异常,可以利用调试工具查找指针错误。例如,使用 Valgrind 工具检测内存泄漏和非法访问等问题。

  5. 确认指针生命周期:确保每个指针都有正确的生命周期,并且在不需要时及时释放该指针所占的内存空间。

  6. 使用 nullptr :C++11 引入了一个新的关键字 nullptr,用于表示空指针。与 NULL 相比,nullptr 更加类型安全可以避免因隐式转换引起的潜在风险

综上所述,在编写 C++ 程序时,需要注意指针的使用,避免出现野指针等常见错误。如果遇到了问题,可以通过代码审查、测试和调试等手段找出并解决这些问题。

深拷贝和浅拷贝的区别

深拷贝和浅拷贝是两种常见的对象拷贝方式,它们之间的区别在于拷贝时是否复制对象所包含的动态分配内存。

浅拷贝只是简单地将一个对象的数据成员的值复制到另一个对象中,这样得到的两个对象共享同一块动态分配内存。如果其中一个对象释放了内存,那么另一个对象也就不能再使用该内存了。因此,浅拷贝容易导致悬垂指针等内存问题。

深拷贝则会将对象的所有数据成员都复制一份 ,并为新对象分配一块独立的动态分配内存。这使得得到的两个对象互不干扰,并且不会出现由于释放内存而导致的悬垂指针等问题。

可以使用以下两种方式来实现深拷贝:

  1. 重载复制构造函数或赋值运算符,手动完成对象数据成员的复制操作。

  2. 利用智能指针等自动内存管理工具来完成数据成员的拷贝操作。

需要注意的是,深拷贝可能会涉及到对象内部的对象以及相关联的动态分配内存的复制,因此理解对象的拷贝语义非常重要。

C和C++的类型安全?

C和C++在类型安全方面有一些区别。

C语言是弱类型的,比如可以将一个整型指针赋值给一个字符型指针,因为它们都是指针类型。这种类型转换可能会导致程序出现运行时错误,因此C语言的类型安全性相对较低。但是C语言提供了一些类型转换函数(如atoi、atof等)来减少这种问题的发生。

C++相对于C来说具有更好的类型安全性,它引入了严格的类型检查和类型转换规则,如const修饰符、函数重载、引用、模板等特性。这些特性大大改善了类型安全性,避免了由于类型转换不当而引起的错误。例如,在C++中,不允许将一个整型指针赋值给一个字符型指针,除非进行显式的强制类型转换。

此外,C++还提供了异常处理机制,使得程序可以在运行时捕获并处理错误,提高了程序的健壮性。

总之,C++相对于C来说具有更好的类型安全性,这使得编写程序变得更加健壮和可靠。但是开发人员仍然需要注意类型转换和类型检查等问题,以避免类型相关的错误。

声明和定义的区别

  • 声明:告诉编译器,这个东西存在、叫什么、是什么类型。不给它分配内存。

  • 定义:给这个东西分配内存、给具体实现 / 值 。真正把它 "造出来"。

超级好记口诀

  • 声明:我在这,我是谁。

  • 定义:我实体,我实现。

最核心的 3 条区别

  1. 内存

    • 声明:不分配内存

    • 定义:分配内存

  2. 次数

    • 声明:可以多次

    • 定义:只能一次(多重定义会链接报错)

  3. 作用

    • 声明:告诉编译器 "有这个东西"

    • 定义:真正创建 / 实现这个东西

多线程的优缺点

优点主要有两点:

一是提升效率,把图像检测、硬件控制拆分到多线程并行,充分利用 CPU 多核,提升晶圆检测的吞吐量;

二是保证实时性,主线程负责设备状态监控和用户交互,不被耗时的图像运算阻塞,能即时响应硬件异常(如相机断连)。

缺点核心是线程安全和复杂度:首先代码复杂度大幅提升,需要处理互斥锁、条件变量等同步机制;其次易出现死锁、数据竞争等问题,可能导致载台移动错误、设备卡死;最后线程过多会增加 CPU 切换开销,反而降低效率,所以我们通常按 CPU 核心数控制线程数量,同时统一锁的获取顺序、加超时机制来避坑。

23种设计模式和应用场景?

设计模式是针对特定问题的通用解决方案,而不是一种具体的算法或代码实现。在软件开发中,常用的设计模式包括23种,它们分别是:

  1. 工厂方法模式:用于创建对象的接口,让子类决定实例化哪一个类。

  2. 抽象工厂模式:提供一个接口,用于创建相关或相互依赖对象的家族,而无需指定具体类。

  3. 单例模式:确保一个类只有一个实例,并提供一个全局访问点。(任务管理器、计数器、回收站)

  4. 建造者模式:将一个复杂对象的构建与表示分离,使同样的构建过程可以创建不同的表示。

  5. 原型模式:通过复制现有的实例来创建新的实例。

  6. 适配器模式:将一个类的接口转换成客户希望的另外一个接口。

  7. 桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

  8. 装饰器模式:动态地给一个对象添加一些额外的职责。

  9. 组合模式:将对象组合成树形结构以表示"部分-整体"的层次结构。

  10. 外观模式:为子系统中的一组接口提供一个一致的界面。

  11. 享元模式:运用共享技术来有效地支持大量细粒度对象的复用。

  12. 观察者模式:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。

  13. 中介者模式:用一个中介对象来封装一系列的对象交互。

  14. 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。

  15. 职责链模式:将请求的发送者和接收者解耦,使得多个对象都有机会处理这个请求。

  16. 策略 模式:定义一系列的算法,把他们封装起来,并且使他们可以相互替换

  17. 模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中实现。

  18. 迭代器模式:提供一种方法来访问聚合对象,而不需要暴露这个对象的内部表示。

  19. 访问者模式:表示一个作用于某对象结构中的各元素的操作。

  20. 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

  21. 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

  22. 状态模式:允许对象在其内部状态改变时改变它的行为。

  23. 合成模式:将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

这些设计模式可以应用于各种软件开发场景,例如构建高效、易于维护的系统、优化程序的性能、提升代码可读性、降低修改成本等。开发人员可以根据项目需求,选择适合的设计模式来提高代码质量和开发效率。

什么是简单工厂模式?

简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以 理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体 实现,只要告诉它你的需求即可。

简单工厂 / 工厂方法:只能创建 「单个产品」(比如只创建一个 PLC 控制器,或只创建一个 Modbus 解析器);

什么是抽象工厂模式?

把咖啡抽象出来,今天想喝酒了,直接新建喝酒即可

抽象工厂模式:专门创建 「一整套、成系列、配套使用」的产品族 ,并且保证这一族的产品绝对兼容、不会配错

什么是观察者模式?

也叫发布、订阅模式,观察者模式是一种行为型设计模式,它定义了对象之间的一种一对多的依赖关系,使得当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

什么是装饰器模式

装饰器模式是一种结构型设计模式,它允许我们在不改变对象接口的前提下,动态地给一个对象添加额外的功能。

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式, 装饰模式可以动态扩展一个实现类的功能。 装饰器模式的关键:装饰器中使用了被装饰的对象。

什么是模板方法模式

模板方法模式是指定义一个模板结构,将具体内容延迟到子类去实现。

优点: 1. 提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子 类中; 2. 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的 行为,实现了反向控制并且符合开闭原则。

什么是代理模式

代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

优点: 1. 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度; 2. 可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。

缺点: 1. 由于使用了代理模式,因此程序的性能没有直接调用性能高; 2. 使用代理模式提高了代码的复杂度。

策略模式

策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。

优点:遵循了开闭原则,扩展性良好。

缺点:随着策略的增加,对外暴露越来越多。

C++11:

C++11有哪些新特性?

智能指针、右值引用、移动语义、auto关键字、nullptr关键字, Lambda 表达式(匿名函数),noexcept 异常说明符,constexpr 编译期常量

C++14 有哪些新特性?

泛型 Lambda(Generic Lambdas):Lambda 表达式的参数可以用 auto 声明,使其成为泛型函数,自动推导参数类型,无需显式指定。

返回值类型推导(Return Type Deduction):函数的返回值可以用 auto 声明,编译器会根据函数体的返回语句自动推导返回类型,无需显式指定。

变量模板:允许定义模板化的变量,支持针对不同类型生成不同的变量实例。

Lambda 捕获增强:允许在捕获列表中直接初始化变量,将外部变量转换为 Lambda 内部的成员,支持移动语义。

C++17/20的一些新特性有什么?

C++17:

结构化绑定(直接解构结构体 / 数组 / STL 容器的成员,无需手动提取,大幅简化多返回值 / 多字段数据的读取)

if/switch 初始化语句(将变量的作用域限制在 if/switch 块内,避免全局 / 外层无用变量,代码更安全、简洁。)

std::optional (可选值,类型安全地表示 "值存在 / 不存在",替代NULL/ 裸指针 / 魔法值(如 - 1),解决 "返回值是否有效" 的歧义问题。)

std::variant(类型安全的联合体,替代 C 风格union,支持非 POD 类型(如std::string),类型安全,可存储 "多种类型但同一时刻仅一种" 的数据。)

std::filesystem(跨平台文件系统,提供跨平台的文件 / 路径操作接口,替代平台特有 API(如 Windows 的FindFirstFile、Linux 的stat`));

C++20:

协程(Coroutines,实现轻量级异步编程,无需手动管理回调 / 线程,适合 "耗时但非 CPU 密集" 的操作(如传感器数据读取、网络通信)。)

范围(Ranges,增强 STL 算法,支持 "管道式" 操作(|),简化数据处理流程,代码更直观(感知数据筛选 / 转换高频)。)

std::jthread(自动管理的线程,替代std::thread,自动join(析构时自动等待线程结束),避免线程泄漏(车载系统稳定性核心)。)

什么是右值引用,跟左值又有什么区别?

在C++中,左值指的是可以取地址的表达式(即具有标识符的变量、对象或表达式),而右值指的是不可以取地址的表达式(如临时对象或字面量)。右值引用是一种用于引用右值的特殊类型的引用类型。

与左值引用相比,右值引用更加灵活,可以更好地支持移动语义、完美转发等特性,能够提高程序的效率和性能。

左值和右值的概念:

左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象; 右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;

区别: 左值能寻址,右值不能; 左值能赋值,右值不能; 左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);

C++移动语义

C++移动语义是一种新的语言特性,它允许在函数参数和返回值中进行资源所有权的转移,从而提高了代码的效率和可读性。传统上,在C++中,当一个对象被传递给一个函数时,会进行复制构造函数的调用,这会导致对象的拷贝和额外的内存分配,从而影响程序的性能。移动语义通过使用右值引用和移动构造函数来避免这种情况。通过移动构造函数,可以将一个临时对象的资源所有权从原对象转移到另一个对象,从而减少了对象拷贝和内存分配的开销。

移动语义的本质是:对于一个「不再被使用的对象」,把它手里持有的堆内存、文件句柄、网络连接等「资源」,直接「过户 / 转移」给新对象,原对象放弃资源所有权,变成一个「空壳对象」(有效但不可用)

移动语义和拷贝的区别

拷贝:是 "复制一份新资源",原对象和新对象各自拥有独立的资源(堆内存 / 硬件句柄等),互不影响;

移动:是 "资源所有权转移",把原对象的资源直接 "过户" 给新对象,原对象放弃资源所有权(变为空 / 无效状态),全程无内存拷贝,效率极高。

记住一句话:"拷贝是复制,移动是剪切;拷贝慢而安全,移动快但原对象作废"。 拷贝类似打印机,而移动,只直接把纸拿过去

智能指针:

shared、weak、unique(前三者最常用)、 auto(98的方案,目前已经被c++11弃用)

传统指针 两个指向共同地址的指针,其中以后析构后,另一个没有值null的时候,A析构后,B就会报错;但是使用传统指针时需要手动管理内存,如申请/释放内存空间,避免内存泄漏等问题。 shared内部引用了计数器,增加的时候计数器+1,释放的时候减一,delete后不会立刻delete,而是变成0的时候才释放。

智能指针是为了尽可能的减少内存泄漏的情况。

智能指针相比传统指针更加安全和方便,可以有效地减少内存泄漏等错误。

  • 内存泄漏:申请的内存忘记释放,导致内存泄漏
  • 内存溢出:你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

  • 内存越界:向系统申请了一块内存,而在使用内存时,超出了申请的范围

智能指针也会内存泄漏,可以用两个指针互相指向对方,会一直计数,然后导致内存泄漏,所以这个时候我们需要用weak来,weak不会增加计数。

智能指针的增减是否是线程安全的?

智能指针的 "增减" 本质是指std::shared_ptr的引用计数增减(std::unique_ptr无引用计数,不存在该问题),其中shared_ptr的引用计数增减是线程安全的,但存在关键的安全边界(对象访问、同一shared_ptr实例修改并非线程安全)。

智能指针本身的基本操作(创建、拷贝、析构)是线程安全的,但智能指针指向的对象的成员变量/方法访问,默认不是线程安全的(和普通指针一样,需手动加锁保护)。

三大非线程安全场景(高频考点)

  • 多个线程修改同一个shared_ptr实例(重置、移动、赋值);

  • 多个线程通过shared_ptr读写托管对象

  • weak_ptr``lock()后,读写托管对象(lock()本身安全,对象访问不安全);

  • unique_ptr :仅支持单线程独占,多线程操作同一个unique_ptr不安全,适合车载单线程管理的独占资源;

    shared_ptr :仅 "引用计数增减" 线程安全,多线程操作同一个shared_ptr对象、读写指向的资源均不安全;

    weak_ptr :继承shared_ptr的线程安全特性,lock()操作安全,但资源访问仍需加锁;

    车载核心原则:智能指针解决 "内存泄漏",互斥锁 / 原子操作解决 "资源竞争",二者缺一不可。

关于unique_ptr的一切

std::unique_ptr 是 C++11 标准引入的独占式智能指针 ,隶属于 <memory> 头文件,核心作用是自动管理动态分配的内存(或其他资源),避免手动调用 new/delete 导致的内存泄漏、野指针、重复释放等问题。

同一时间内,仅有一个 std::unique_ptr 实例可以拥有对某个动态对象的所有权 ,不允许其他 unique_ptr 共享该对象。这是它与 std::shared_ptr(共享所有权)最核心的区别。

  • 编译器会禁用 unique_ptr 的拷贝构造函数和拷贝赋值运算符,防止所有权被意外拷贝。

  • 若需转移所有权,必须通过 ** 移动语义(std::move)** 实现,转移后原 unique_ptr 会变为空(不再指向任何对象)。

    unique_ptr 超出其作用域(如函数执行完毕、局部变量销毁),或被手动重置(reset())时,会自动调用其内部持有的删除器(默认是 delete/delete[]),释放所管理的对象 / 资源,无需手动干预。

unique_ptr是独占式智能指针,同一时间仅有一个unique_ptr持有对象所有权,不支持拷贝(仅支持移动),因此根本没有 "引用计数" 这个概念,自然不存在 "引用计数增减" 的线程安全问题。

unique_ptr是独占的,那它是如何实现独占的?

本质是unique_ptr通过禁用拷贝语义、仅保留移动语义 ,强制同一时刻只有一个unique_ptr指向某块资源,从而实现独占性。这也是它相比shared_ptr更轻量(无引用计数)、更高效的关键

unique_ptr实现独占的核心是:禁用拷贝构造 / 赋值(delete),仅开放移动构造 / 赋值,从语法上杜绝多指针共享资源;

移动语义保证 "所有权转移" 而非复制,转移后原指针置空,仍维持独占性;

析构时自动释放资源,且因独占性,不会出现双重释放(相比裸指针更安全);

车载场景价值:适合管理独占性硬件 / 算法资源,兼顾轻量(无引用计数)和安全(RAII),是车载开发中独占资源管理的首选。

weak指针的实现原理

weak_ptr的底层实现离不开两个引用计数(这是理解它的关键):

  1. 强引用计数(use_count) :记录当前持有资源的shared_ptr数量,计数为0时,自动释放托管的堆资源;

  2. 弱引用计数(weak_count) :记录当前观察该资源的weak_ptr数量(包括未过期的weak_ptr),计数为0时,自动释放 "引用计数控制块"。

std::weak_ptr的底层实现基于std::shared_ptr的双引用计数机制,核心是关联shared_ptr创建的堆上控制块,自身不持有资源所有权。

  1. 控制块包含强引用计数(use_count)和弱引用计数(weak_count),weak_ptr创建时仅将weak_count1,不修改use_count,析构时将weak_count1

  2. 通过expired()检查use_count是否为0,判断资源是否存活;通过lock()方法,在资源存活时创建新的shared_ptruse_count1),安全访问资源;

  3. 它的核心价值是解决shared_ptr的循环引用问题,同时适合车端 / 工业场景的缓存管理,避免野指针和内存泄漏。

shared_ptr的引用计数是怎么实现的?

引用计数的本质是为动态分配的资源维护一个 "计数器",记录当前有多少个shared_ptr指向该资源,当计数归 0 时自动释放资源

shared_ptr的引用计数存储在独立的控制块 中,控制块与资源指针分离,多个shared_ptr共享同一个控制块;

引用计数的增减是原子操作 (线程安全),拷贝shared_ptr加计数,析构 / 覆盖减计数,计数归 0 释放资源;

控制块还包含弱引用计数,仅当强 / 弱引用计数都归 0 时,才销毁控制块;

关键坑点:引用计数线程安全 ≠ 资源访问线程安全,车载场景中共享资源仍需加锁;

车载优化建议:优先用std::make_shared(减少内存分配),结合std::mutex保护资源读写。

智能指针的循环引用是什么?

循环引用是指两个(或多个)对象通过shared_ptr互相引用对方,导致它们的引用计数永远无法降到 0,最终触发内存泄漏。

循环引用的本质是:两个对象的shared_ptr互相持有对方,导致各自的引用计数至少为 1,永远无法被释放

相关推荐
冰暮流星1 小时前
javascript如何实现删除数组里面的重复元素
开发语言·前端·javascript
程序员小假1 小时前
为什么要有 time _wait 状态,服务端这个状态过多是什么原因?
java·后端
「QT(C++)开发工程师」2 小时前
C++11 新特性 正则表达式、随机数库、元组
c++·正则表达式
yuweiade2 小时前
【Spring】Spring MVC案例
java·spring·mvc
free-elcmacom3 小时前
C++ 默认参数详解:用法、规则与避坑指南
开发语言·c++
码云数智-大飞3 小时前
分布式事务解决方案全景指南:2PC、TCC、SAGA 与 Seata 实战
开发语言
娇娇yyyyyy3 小时前
QT编程(10): QLineEdit
开发语言·qt
Albert Edison3 小时前
【ProtoBuf 语法详解】Any 类型
服务器·开发语言·c++·protobuf
喵叔哟3 小时前
5. 【Blazor全栈开发实战指南】--Blazor组件基础
开发语言·javascript·ecmascript