八股文2
1.基本知识
sizeof
:是C/C++中的操作符,用来计算一个类型/对象的所占用的内存大小(字节数),包括最后的\0
sizeof
和strlen
的区别:strlen
是一个 C 标准库中的函数,用于计算以空字符\0
结尾的字符串的实际长度(不包括结尾的空字符)char array[]
数组作为函数参数时会退化为指针,大小要按指针的计算。这时sizeof()会返回指针的大小;strlen()还是返回字符串实际长度volatile
:是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。当一个变量被声明为volatile
时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。避免因为编译器优化而导致出现不符合预期的结果字节对齐
有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费字节序
是指在多字节数据类型(如整数、浮点数等)中,字节在内存中的存储顺序。主要有两种字节序:- 大端字节序(网络字节序):高位字节存储在低地址处,低位字节存储在高地址处。大端字节序是符合人类阅读习惯的顺序
- 小端字节序:低位字节存储在低地址处,高位字节存储在高地址处。
- 判断系统的字节序:将一个整数num初始化为1,然后将其指针类型
int*
转化为char*
,这样就可以访问该整数的第一个字节。如果系统是小端字节序,那么第一个字节是1;如果系统是大端字节序,那么第一个字节是0。
typedef 是一种类型定义关键字,用于为现有类型创建新的名称。在编译阶段处理的,有严格的类型检查
mutable
是C++中的一个关键字,用于修饰类的成员变量,表示该成员变量即使在一个const
成员函数中也可以被修改- 在C++中,如果一个成员函数被声明为
const
,那么它不能修改类的任何成员变量,除非这个成员变量被声明为mutable
- 在C++中,如果一个成员函数被声明为
2. 宏定义和内联函数的区别与使用场景
- 宏定义和内联函数都是为了减少函数调用开销和提高代码运行效率而引入的机制,但是它们的实现方式和作用机制略有不同。
- 宏定义用于在编译时替换宏定义中的代码,也就是 define 实际上只是做文本的替换。无类型检查、不能进行调试
- 内联函数用于在调用该函数的位置,直接替换函数体代码。有类型检查、可以进行调试
3. extern的作用
- 可以在不同文件间共享变量数据。如果在某个文件中要使用其它文件里定义的全局变量时,可以使用关键字extern来声明
- 使用
extern C
表示以c语言的规则来编译c++程序
4. C++中四种强制类型转换
- static_cast
():其实 static_cast和 C 语言 () 做强制类型转换基本是等价的 - dynamic_cast
():dynamic_cast在C++中主要应用于父子类层次结构中的安全类型转换 - const_cast
():new_type 必须是一个指针、引用或者指向对象类型成员的指针。可以删除一个const变量中const属性 - reinterpret_cast:
5. 类对象的初始化顺序
遵循以下规则顺序
基类初始化顺序:如果当前类继承自一个或多个基类,它们将按照声明顺序进行初始化,但是在有虚继承和一般继承存在的情况下,优先虚继承。(先初始化的类,就先调用它的构造函数)
成员变量初始化顺序:类的成员变量按照它们在类定义中的声明顺序进行初始化
在基类和成员变量初始化完成后,执行类的构造函数。
6. 析构函数可以抛出异常吗
- 析构函数中不应该抛出异常
- 如果一个对象在异常处理过程中被销毁,而它的析构函数又抛出了一个新的异常,此时有两个异常同时存在,就导致程序调用 terminate(),直接崩溃
- 在容器析构时,会逐个调用容器中的对象析构函数,而某个对象析构时抛出异常还会引起后续的对象无法被析构,导致资源泄漏
7. 浅拷贝和深拷贝
浅拷贝:它只是简单地将原对象所有成员变量的值复制给新对象,对于指针成员,两个对象中的指针指向同一块内存空间
- 析构时会出现重复释放内存的问题
深拷贝:它不仅将原对象所有成员变量的值复制给新对象,对于指针成员,它会分配新的内存空间,并将指针成员数据复制到新空间中
8. 多态的实现方式
- C++实现多态的方法主要包括虚函数、纯虚函数和模板函数。其中
虚函数
、纯虚函数
实现的多态叫动态多态
;模板函数
、重载
等实现的叫静态多态
。 - 区分
静态多态
和动态多态
的一个方法就是看决定所调用的具体方法是在编译期还是运行时,运行时就叫动态多态
。
虚函数
和纯虚函数
都可以用来实现多态,但它们适用于不同的场景:虚函数适合于为派生类提供默认行为的情况,而纯虚函数则更适合于定义接口,要求子类必须重写父类纯虚函数,除非也为抽象类
模板函数可以根据传递参数的不同类型,自动生成相应类型的函数代码。模板函数可以用来实现多态。这种是在编译期就能确定下来的叫
静态多态
重载指的是在同一个作用域内定义多个同名但参数列表不同的函数,和模板函数一样,在编译期间就能确定调用具体版本的函数了
实现动态多态必须要满足条件: 1.基类指针或引用指向子类对象 2.子类必须重写父类的虚函数
9. 纯虚函数
- 纯虚函数是一种在基类中声明但没有实现的虚函数。它的作用就是定义了一种接口,这个接口需要由派生类来实现
- 包含纯虚函数的类称为抽象类,抽象类无法实例化,仅仅提供了一些接口
10. 为什么默认的析构函数不是虚函数
- 虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作,比如说生成虚函数表和虚表指针。对于不使用多态的情况下,这些额外工作所带来的开销是没有必要的
11. 函数参数传递常见的方式
- 值传递是将实参的值传递给形参。在这种情况下,函数内对形参的修改不会影响到实参
- 引用传递是将实参的引用传递给形参。在这种情况下,函数内对形参的修改会影响到实参
12. 使用智能指针的注意事项
- 不能使用原始指针(地址)创建多个共享智能指针,这样会产生多个独立的引用计数,从而析构多次
- 解决方法:1.使用
make_shared
;2.使用已有shared_ptr
进行拷贝;
- 解决方法:1.使用
- 不能循环引用,即两个或多个
shared_ptr
互相引用,导致引用计数永远无法降为零,从而无法释放内存- 解决方法:weak_ptr,weak_ptr 是一种不控制对象生命周期的智能指针,它只观察对象,而不增加强引用计数。这可以避免循环引用导致的内存泄漏问题
13. 为什么用 Redis 作为 MySQL 的缓存
- 主要是因为Redis具备
高性能
和高并发
两种特性- Redis 具备高性能,因为其数据主要存储在内存中,而 MySQL 的数据主要存储在磁盘上。当用户首次访问 MySQL 中的数据时,如果数据不在缓冲池中,就需要从磁盘读取,相对比较慢。此时可以将这部分数据缓存到 Redis 中。下次访问相同数据时,应用可以直接从 Redis 读取,由于 Redis 操作的是内存,因此响应速度非常快。
- Redis具备高并发处理能力,由于其基于内存操作和高效的 I/O 多路复用机制,能够承受的请求量远高于 MySQL。因此,在实际应用中,通常会将数据库中的热点数据缓存到 Redis 中,使得用户的部分请求可以直接从缓存中获取数据,而不必每次都访问数据库,从而提升系统性能并降低数据库负载
14. 介绍redis
- Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快。Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
15. Qt中connect()第5个参数的作用
该参数就是 连接类型,它决定了当信号被发射时,槽函数是如何被调用的。特别是在跨线程通信时,它的作用非常重要
- AutoConnection:默认值。根据接收者所在的线程自动选择使用。当接收者和发送者在同一个线程,则使用DirectConnection类型。如果接收者和发送者不在一个线程,则使用QueuedConnection类型
- DirectConnection:这种情况是接收者和发送者在同一个线程,槽函数会在信号发送的时候直接被调用,槽函数是在发送信号的线程中执行的。emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行
- QueuedConnection:当信号被发射时,不会立即调用槽函数,而是将这个调用请求放入接收对象所在线程的 事件队列中,等到该线程的事件循环运行时再执行。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕
- BlockingQueuedConnection:类似于QueuedConnection,但发送方线程会被阻塞,直到槽函数执行完毕。仅在不同线程之间使用时有效,否则会引发死锁。在多线程间需要同步的场合可能需要这个
- UniqueConnection:它不能单独使用,必须与其他连接类型按位或组合使用。当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接
16. QObject的作用
- 支持信号与槽机制
- 支持对象树管理
- 支持元对象系统
- 支持事件处理机制
17. map容器的key如果是一个结构体的话,要怎么设计
如果想要使用一个结构体作为 map 的键(key),需要确保这个结构体支持比较操作,因为 std::map
需要通过某种方式来对键进行排序以维护其内部的红黑树结构。常用的方法如下:
- 重载比较运算符
operator<
1 | struct MyStruct { |
- 提供自定义比较函数
1 | struct MyStruct { |
- 使用c++11引入的
std::tuple
或者std::tie
1 |
|
18. Redis是单线程吗
- Redis单线程指的是
接收客户端请求
->解析请求
->进行数据读写等操作
->发送数据给客户端
这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。 - 但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动一些后台线程的
19. redis是单线程处理请求,效率会不会慢
- 不慢,因为它的设计非常高效,几乎没有任何阻塞操作。
- redis的数据主要存储在内存的,那么Redis 的大部分操作都在内存中完成,读写速度快
- redis的单线程避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,也不需要考虑同步问题(不需要加锁)
20. Redis 如何实现数据不丢失
Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据
Redis共有三种数据持久化的方式:
- AOF 日志:Redis 在执行完一条写数据操作命令(删除和修改)后,就会把该命令以追加的方式写入到一个AOF文件里,然后 Redis 重启时,会读取该文件记录的命令,然后以逐一执行命令的方式来进行数据恢复
- RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘(RDB文件)。因为AOF 文件记录的是命令,而RDB文件记录的是某一个瞬间内存的数据。所以恢复数据时,直接将 RDB 文件读入内存就可以
- 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RDB 的优点
RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以。不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据
21. AOF日志知识
为什么先执行命令,再把数据写入AOF日志呢?
- 好处:避免额外的检查开销(如果该命令有问题,Redis 在使用日志恢复数据时,就可能会出错);不会阻塞当前写操作命令的执行
- 坏处:数据可能会丢失( 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险)
Redis 写入 AOF 日志的过程
- Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区
- 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据(命令)写入内核缓冲区 page cache,等待内核将数据写入硬盘
- 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定,主要有如下3种策略:
- Always:每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘
- Everysec:每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
- No:不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机。每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘
AOF 日志过大,会触发什么机制
- AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会触发AOF 重写机制,来压缩 AOF 文件
重写 AOF 日志的过程是怎样的
- 重写 AOF 过程是由
后台子进程
来完成的 - 好处:子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
- 重写 AOF 过程是由
22. RDB快照知识
Redis 提供了两个命令来生成 RDB 文件,分别是save和bgsave,他们的区别就在于是否在
主线程
里执行- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB 在执行快照的时候,数据能修改吗
- 可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术
23. 为什么会有混合持久化
RDB优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快
所以混合持久化既保证了 Redis 重启速度,又降低数据丢失风险
24. 线程有哪些实现方式?
线程的实现方式取决于操作系统内核是否直接管理线程。主要包括下面三种:
- 用户级线程:线程管理由用户态的线程库完成,OS 内核只知道进程,并不知道线程的存在
- 优点:切换速度快,不需要内核干预
- 缺点:如果一个线程阻塞,整个进程都阻塞。
- 内核级线程:线程由操作系统内核直接支持和调度(由内核提供系统调用创建线程),一个用户级线程映射到一个内核级线程。每个线程在内核中有对应的内核控制块
- 优点:多个线程可在多核 CPU 上同时运行,一个线程 I/O 阻塞,其他线程可继续运行(不会阻塞整个进程)
- 缺点:线程切换需要内核态和用户态之间切换,开销大;内核复杂度高,需管理大量线程
- 混合实现:结合用户级和内核级线程的优点,采用“多对多”映射模型。即多个用户线程映射到少量内核线程上。
- 缺点:需协调用户和内核两级调度
25. 什么是快表
快表(TLB)是一种重要的硬件缓存,用于加速虚拟地址到物理地址的转换过程。
- 现代操作系统普遍采用虚拟内存技术。程序运行时使用的是虚拟地址,而数据实际存储在物理内存中,因此进行访问时就需要通过查找页表来将虚拟地址转换为物理地址。每次访问内存时,CPU都需要先查询页表(通常存储在内存中)来获取物理地址,然后再访问目标内存。这意味着一次内存访问可能需要两次内存查询(查页表 + 访问数据),效率非常低。
- 而快表(TLB)就是为了解决上述效率问题而设计的。TLB 是位于 CPU 内部的一个高速缓存(Cache),专门用来缓存最近使用过的页表项。当 CPU 需要将虚拟地址转换为物理地址时,会优先查询 TLB。如果所需的页表项在 TLB 中找到了(称为 TLB 命中),就可以直接从中获取物理地址,从而避免访问内存中的页表,大大加快地址转换速度。
25. 分页和分段的区别
分页和分段是操作系统中两种不同的内存管理方式。它们的核心区别在于划分的依据和用户可见性
分页是将进程的逻辑地址空间和物理内存都划分为固定大小的块,因此这种划分使得不会出现外部碎片,内存使用率高,但不可避免出现内部碎片的情况(分页的划分是物理导向的,对程序员透明,不可见的)。它们之间的地址转换是通过页表来完成的。
- 过程:当CPU访问一块虚拟地址时,虚拟地址里面有页号和页内偏移量,然后就可以通过页号在页表中去查找对应的物理页号,再加上页内偏移量,就可以再物理内存中算出具体的物理地址了
分段是按照程序的逻辑结构来划分的,比如代码段、数据段、堆、栈等,每个段大小可变。它是逻辑导向的,对用户可见。地址通过段表来进行转换的。存在外部碎片的情况。
- 过程:当CPU访问一块虚拟地址时,虚拟地址里面有段号和段内偏移量,然后就可以通过段号在段表中找到段基地址和段界限,在基于起始地址和段内偏移量就可以算出在物理内存的具体位置了
总的来说,分页更注重物理内存的高效利用,而分段更注重程序的逻辑组织和安全性
26.什么是交换空间
交换空间是操作系统中用于扩展物理内存的一种磁盘空间。当系统的物理内存不足时,操作系统会将一部分暂时不用的内存数据移到磁盘上的一块空间上,从而腾出物理内存给更需要的程序使用。磁盘上的那块空间叫做交换空间,而这一过程被称为交换。
27. 什么是缺页中断
缺页中断是操作系统中虚拟内存管理的核心机制之一。当CPU访问一块虚拟地址时,如果通过页表发现对应的物理页面当前不在物理内存中(例如被交换到磁盘或尚未加载),就会触发缺页中断。操作系统就从磁盘读取该页数据调换到内存,更新页表。
28.硬链接和软链接有什么区别?
硬链接和软链接(又称符号链接)是两种创建文件“别名”的方式,它们都能让你通过不同的路径访问同一个文件,但底层机制和行为有本质区别。
- 硬链接:多个文件名指向同一个inode(索引节点)。所有硬链接和原文件完全等价,没有“原始文件”和“链接文件”之分。只有当所有硬链接都被删除后,文件的数据块才会被真正释放。
- 软链接:一个特殊的文件,点击该文件会跳到指定源文件。它有自己的inode,如果原始文件被删除,软链接就变成悬空链接,访问时会报错。
29.什么是零拷贝
零拷贝是一种优化计算机系统中数据传输效率的技术,其核心目标是:在数据传输过程中,避免 CPU 参与不必要的数据复制操作,尤其是避免在用户空间和内核空间之间多次拷贝数据。
- 核心思想:绕过用户空间,让数据直接在内核空间从源(如磁盘)传输到目标(如网络),避免 CPU 参与数据复制。
30.WebSocket 与 Socket 的区别?
Socket 是操作系统提供的一种 网络通信的编程接口(API),是网络编程的基础。它封装了 TCP/IP、UDP 等底层协议的复杂性,屏蔽网络细节,以方便开发者更好地进行网络编程。
WebSocket 是应用层的通信协议,用来解决 http 不支持持久化连接的问题。WebSocket 协议是运行在 TCP 之上,实际通信时依赖 Socket 来传输数据。可以说,WebSocket 是‘用 Socket 实现的一种高级协议’。”