1.基本知识
  • sizeof :是C/C++中的操作符,用来计算一个类型/对象的所占用的内存大小(字节数),包括最后的\0

  • sizeofstrlen 的区别:strlen是一个 C 标准库中的函数,用于计算以空字符 \0 结尾的字符串的实际长度(不包括结尾的空字符)

  • char array[]数组作为函数参数时会退化为指针,大小要按指针的计算。这时sizeof()会返回指针的大小;strlen()还是返回字符串实际长度

  • volatile:是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。当一个变量被声明为volatile时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。避免因为编译器优化而导致出现不符合预期的结果

  • 字节对齐有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费

  • 字节序是指在多字节数据类型(如整数、浮点数等)中,字节在内存中的存储顺序。主要有两种字节序:

    • 大端字节序(网络字节序):高位字节存储在低地址处,低位字节存储在高地址处。大端字节序是符合人类阅读习惯的顺序
    • 小端字节序:低位字节存储在低地址处,高位字节存储在高地址处。
    • 判断系统的字节序:将一个整数num初始化为1,然后将其指针类型int*转化为char*,这样就可以访问该整数的第一个字节。如果系统是小端字节序,那么第一个字节是1;如果系统是大端字节序,那么第一个字节是0。
  • typedef 是一种类型定义关键字,用于为现有类型创建新的名称。在编译阶段处理的,有严格的类型检查

  • mutable是C++中的一个关键字,用于修饰类的成员变量,表示该成员变量即使在一个const成员函数中也可以被修改

    • 在C++中,如果一个成员函数被声明为const,那么它不能修改类的任何成员变量,除非这个成员变量被声明为mutable
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 进行拷贝;
  • 不能循环引用,即两个或多个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
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MyStruct {
int field1;
double field2;
// 其他成员...

// 重载 < 运算符
bool operator<(const MyStruct& other) const {
if (field1 != other.field1)
return field1 < other.field1;
return field2 < other.field2;
}
};

std::map<MyStruct, int> myMap;
  • 提供自定义比较函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct MyStruct {
int field1;
double field2;
// 其他成员...
};

// 自定义比较函数对象
struct MyStructCompare {
bool operator()(const MyStruct& lhs, const MyStruct& rhs) const {
if (lhs.field1 != rhs.field1)
return lhs.field1 < rhs.field1;
return lhs.field2 < rhs.field2;
}
};

std::map<MyStruct, int, MyStructCompare> myMap;
  • 使用c++11引入的 std::tuple 或者 std::tie
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <tuple>

struct MyStruct {
int field1;
double field2;
// 其他成员...
};

bool operator<(const MyStruct& lhs, const MyStruct& rhs) {
return std::tie(lhs.field1, lhs.field2) < std::tie(rhs.field1, rhs.field2);
}

std::map<MyStruct, int> myMap;
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 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
22. RDB快照知识
  • Redis 提供了两个命令来生成 RDB 文件,分别是save和bgsave,他们的区别就在于是否在主线程里执行

    • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
    • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
  • RDB 在执行快照的时候,数据能修改吗

    • 可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术
23. 为什么会有混合持久化
  • RDB优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。

  • AOF 优点是丢失数据少,但是数据恢复不快

  • 所以混合持久化既保证了 Redis 重启速度,又降低数据丢失风险