C++ 面试总结

1.Reactor模式
核心思想: Reactor模型的核心思想是将IO事件处理分离出来,使得程序能够通过事件驱动的方式处理IO操作。这种模型的主要目标是提高系统的并发性能和可伸缩性。 工作流程:

  1. 事件注册:程序通过系统调用(如 epoll 、 select 等)向操作系统注册感兴趣的事件(如读 就绪、写就绪等)以及对应的回调函数(事件处理器)。

  2. 事件循环:程序进入一个事件循环(通常是一个无限循环),不断地向操作系统询问是否有已 注册的事件就绪。

  3. 事件分发:当有事件就绪时,操作系统会通知程序,程序会根据事件类型调用对应的事件处理 器进行处理。

  4. 事件处理:事件处理器会执行相应的业务逻辑,可能包括读取数据、写入数据、连接处理等操 作。

  5. 事件驱动:在处理完一个事件后,程序会继续等待下一个事件的到来,整个过程是事件驱动 的。

特点:

事件驱动:所有的IO操作都是由事件触发的,程序不需要主动去轮询IO状态,这样可以减少系 统资源的浪费。 非阻塞IO:在Reactor模型中,通常使用非阻塞IO来处理事件,可以提高系统的并发性能。 事件处理器:Reactor模型通过事件处理器来处理不同类型的事件,使得程序结构清晰,易于 扩展和维护。

适用性广泛:Reactor模型适用于各种类型的IO操作,如网络通信、文件IO等。
单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。但是,这种方案存在 2 个缺点:

第一个缺点,因为只有一个进程,无法充分利用 多核 CPU 的性能;
第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务 处理耗时比较长,那么就造成响应的延迟;

所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。

裸写Socket编程相比使用成熟的协议有以下几个优点:

1灵活性:裸写Socket编程可以更加灵活地控制和定制通信的细节。可以根据具体需求自定义通信 协议、数据格式和消息交互方式,以满足特定的业务需求。这种自定义能力可以在某些特殊场景下提供 更高的性能和效率。

2 效率和性能:使用成熟的协议通常会带来一定的开销,例如协议解析、报文封装等。而裸写 Socket编程可以避免这些额外开销,直接操作底层的Socket接口,从而提高效率和性能。在对性能要求 较高的场景下,裸写Socket编程可能是更好的选择。

3 学习和理解网络原理:通过裸写Socket编程,你可以更深入地学习和理解网络通信的原理和细 节。这对于网络编程的学习和进一步的技术深入是非常有帮助的。裸写Socket编程可以帮助你更好地理 解网络协议栈、网络通信机制和底层数据传输原理。

Preactor 是非阻塞同步网络模式,感知的是就绪可读写事件。Proactor 是异步网络模式, 感知的 是已完成的读写事件。

2.高并发内存池 e.g.超过128page咋办?测试是在空的时候测试的,如果比较满的情况测试,效率?

3.在Linux中,可以使用以下两个命令来查找某一文件和查看指定文件的大小: 1查找某一文件:使用find命令可以在指定目录及其子目录中查找文件。

语法:find [路径] -name [文件名]

示例:find /home/user -name example.txt 2查看指定文件大小:使用ls命令可以查看文件的详细信息,包括文件大小。

语法:ls -l [文件路径]

示例:ls -l /path/to/file.txt 

4.线程池

template<class T>
class ThreadPool
{
private:
    bool IsEmpty()
    {
        return _task_queue.size() == 0;
    }
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void UnLockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

void Wait() {

        pthread_cond_wait(&_cond, &_mutex);
    }
    void WakeUp()
    {
        pthread_cond_signal(&_cond);
    }
public:
    ThreadPool(int num = NUM)
        : _thread_num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

//线程池中线程的执行例程
static void* Routine(void* arg) {

pthread_detach(pthread_self()); ThreadPool* self = (ThreadPool*)arg; //不断从任务队列获取任务进行处理
while (true){

主线程就负责不断向任务队列当中Push任务就行了,此后线程池当中的线程会从任务队列当中获取 到这些任务并进行处理。

  线程池中的任务队列是会被多个执行流同时访问的临界资源,因此我们需要引入互斥锁对任务队列
进行保护。线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程
池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程
应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

5.线程有什么东西是不共享的?寄存器和线程栈。

线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,为什么系统要让进程崩溃呢,这主要 是因为在进程中,各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导 致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一 系列严重的后果,于是干脆让整个进程崩溃。kill 执行的是系统调用,将控制权转移给了内核(操作系 统),由内核来给指定的进程发送信号让其崩溃。

            self->LockQueue();
            while (self->IsEmpty()){
                self->Wait();
            }
            T task;
            self->Pop(task);
            self->UnLockQueue();

task.Run(); //处理任务 }

    }
    void ThreadPoolInit()
    {
        pthread_t tid;
        for (int i = 0; i < _thread_num; i++){

pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针 }

}

//往任务队列塞任务(主线程调用) void Push(const T& task)
{

        LockQueue();
        _task_queue.push(task);
        UnLockQueue();
        WakeUp();

}

//从任务队列获取任务(线程池中的线程调用) void Pop(T& task)
{

        task = _task_queue.front();
        _task_queue.pop();
    }

private:
std::queue<T> _task_queue; //任务队列 int _thread_num; //线程池中线程的数量 pthread_mutex_t _mutex; pthread_cond_t _cond;

};

  操作系统根据情况执行相应的信号处理程序(函数),一般执行完信号处理程序逻辑后会让进程退
出;如果进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序,但如果注册
了,则会执行自己的信号处理函数。

区别:
1. 定义:

进程(Process):是程序在执行过程中的一个实例,是操作系统资源分配的基本单位, 每个进程有独立的内存空间。 线程(Thread):是进程中的一个执行单元,是CPU调度和分派的基本单位,同一进程 内的多个线程共享相同的内存空间。

2. 资源分配: 进程独享资源:每个进程有独立的内存空间、文件描述符、进程控制块等系统资源。

线程共享资源:同一进程内的多个线程共享相同的内存空间和文件描述符等资源。 3. 创建和销毁:

       进程创建和销毁较为复杂:创建新进程需要分配独立的内存空间、建立进程控制块等,销
       毁进程需要释放资源并通知操作系统。
       线程创建和销毁较为简单:在同一进程内部创建新线程只需分配线程控制块即可,销毁线
       程也比较轻量级。

4. 调度和切换: 进程调度和切换开销大:进程切换涉及到切换内存空间、文件描述符表等,开销较大。

线程调度和切换开销小:线程切换只需切换线程控制块和栈即可,开销较小。 5. 通信方式:

进程间通信复杂:需要使用进程间通信(IPC)机制,如管道、信号量、消息队列等。 线程间通信简单:由于线程共享相同的内存空间,可以直接通过共享内存、全局变量等方 式进行通信。

6.为什么能进行进程通信,本质原因?

Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连管道、socket 等,也都 是统一交给文件系统管理的。Linux 文件系统会为每个文件分配两个数据结构:索引节点(node)来记 录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置。 索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用 磁盘空间。目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联 关系。多个目录项关联起来,就会形成目录结构。一个目录文件的内容:该目录下所以文件的目录项。

  从用户角度来看文件的话,我们要怎么使用文件?过程:

首先用 open 系统调用打开文件, open 的参数中包含文件的路径名和文件名。
使用 write 写数据,其中 write 使用 open 所返回的文件描述符,Linux进程默认情况下会 有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。并不使用文件名 作为参数。
使用完文件后,要用 close 系统调用关闭文件,避免资源的泄露。

  从系统内部这个过程分成三步:

从目录文件数据块中的所有目录项找到和这个文件名path相匹配的目录项(保存目录的格式改成哈 希表,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过 名称取哈希),从而找对对应的inode号;

通过inode号码,获取inode信息;

根据inode信息,找到文件数据所在的block,读出数据。 本质上,文件描述符就是指针数组的下标。用户程序不能直接访问内核中的文件描述符表,而只能使

用文件描述符表的索引 (即0、1、2、3这些数字。file* fd_array是files_struct中的一个成员变量。

page5image38696560

内核为所有打开文件维护一张文件表项,每个文件表项包含内容可以由结构体看出:

struct file {

mode_t f_mode;//表示文件是否可读或可写,FMODE_READ或FMODE_WRITE dev_ t f_rdev ;// 用于/dev/tty
off_t f_ops;//当前文件位移
unsigned short f_flags;//文件标志,O_RDONLY,O_NONBLOCK和O_SYNC unsigned short f_count;//打开的文件数目

unsigned short f_reada;
struct inode *f_inode;//指向inode的结构指针 struct file_operations *f_op;//文件索引指针

}

page5image38698640

7.操作系统中的缺页中断是什么?

缺页中断(Page Fault)是操作系统中的一种异常情况,发生在程序访问虚拟内存中的某个页面 (Page)时,但该页面并未加载到物理内存中。这种情况通常发生在使用虚拟内存管理的系统中,即将 虚拟地址空间映射到物理地址空间的过程中。当发生缺页中断时,操作系统会执行以下步骤:

  1. 中断处理:CPU收到缺页中断信号后,会暂停当前正在执行的指令,切换到内核态。

  2. 异常处理:操作系统内核会根据异常的原因进行处理。在缺页中断的情况下,操作系统会根据

    缺页地址查找页表(Page Table),判断该页面是否已经加载到物理内存中。

  3. 页表查找:如果页表中记录了该页面的物理地址,则表示该页面已经在物理内存中,操作系统 会更新页表中的相关信息(如访问位、修改位等),并重新执行导致缺页中断的指令,以完成

    对该页面的访问操作。

  4. 页面调入:如果页表中未记录该页面的物理地址,表示该页面尚未加载到物理内存中,需要进

    行页面调入(Page In)操作。操作系统会选择一个物理页面用于存储该虚拟页面的内容,并 将该页面从磁盘读取到物理内存中。在页面调入完成后,操作系统会更新页表中的相关信息, 并重新执行导致缺页中断的指令。

  5. 继续执行:当页面调入完成后,操作系统会重新执行导致缺页中断的指令,以完成对该页面的 访问操作。

8.排序

page6image38715600

归并排序采用分治法(Divide and Conquer)策略。它的基本思想是将待排序的序列分成两部分, 分别对这两部分进行排序,然后将两个已排序的部分合并成一个有序序列。

  下面是归并排序的详细步骤:
  1. 分解(Divide):将待排序的序列分成两个长度相等(或相差最多 1)的子序列,找到序列的 中间位置。

  2. 解决(Conquer):递归地对左右两个子序列进行归并排序,直到子序列的长度为 1 或 0。

  3. 合并(Merge):将两个已排序的子序列合并成一个有序序列。合并过程需要额外的空间来存 储临时序列。

page7image38730576

快速排序:

快速排序(Quick Sort)的时间复杂度为 O(n log n),待排数据有序时,时间复杂度为O(n^2)其中 n 为待排序序列的长度。原理如下:

  1. 选择基准元素:从待排序序列中选择一个元素作为基准(pivot)。通常选择第一个元素、最 后一个元素或者中间元素作为基准。

  2. 分区(Partition):将序列中的其他元素按照与基准的比较结果分为两部分,一部分小于基 准,一部分大于基准。在分区过程中,使用双指针法或者三指针法进行操作,将小于基准的元 素放在基准的左边,大于基准的元素放在基准的右边。

  3. 递归排序:对基准元素左右两部分分别进行递归排序,直到每个部分只有一个元素或为空。

  4. 合并:合并左右两部分已排序的子序列,得到最终的排序结果

三指针的快速排序最坏情况下不会退化为O(N*N),而且时间复杂度为O(N).

page8image38713568

9.mplace_back 与 push_back 有啥区别?

1. push_back(const T& value) :将一个类型为 T 的元素 value 添加到 std::vector 的末 尾。如果 value 是一个临时对象或者右值引用,会调用移动构造函数将其添加到容器中;如 果 value 是一个左值引用,会调用拷贝构造函数将其添加到容器中。

2. emplace_back(Args&&... args) :直接在 std::vector 的末尾构造一个类型为 T 的元 素,使用 args 作为构造函数的参数。与 push_back 不同的是, emplace_back 不需要创建 临时对象或者进行拷贝或移动操作,它直接在容器的末尾构造元素,因此效率更高。

emplace_back 相比 push_back 更高效,因为它避免了拷贝或移动操作,直接在容器的末尾构造 元素。在使用时,可以根据具体情况选择合适的函数来添加新元素,如果需要添加已有对象,可以使用

push_back ;如果需要直接在容器中构造新对象,可以使用 emplace_back 。

10.了解哪些设计模式?

单例模式(Singleton Pattern):用于确保一个类只有一个实例,并提供全局访问点。在项目中, 单例模式常用于管理共享资源,如配置信息、日志记录器等。需要注意的是:

线程安全性:如果单例对象在多线程环境下使用,需要考虑其线程安全性。可以通过加锁(悲 观锁或乐观锁)、双重检查锁(Double-Checked Locking)等方式来保证线程安全。 对象生命周期管理:单例对象的生命周期通常与整个应用程序的生命周期相同,需要注意在合 适的时机释放资源,避免内存泄漏。

全局状态管理:单例对象是全局唯一的,因此对全局状态的管理需要谨慎,避免出现意外的状
态修改导致程序错误。
class LazySingleton {
public:

// 获取单例实例的静态成员函数
static LazySingleton& getInstance() {

// 使用静态局部变量来延迟初始化 static LazySingleton instance; return instance;

}

// 其他成员函数和数据成员 private:

LazySingleton() { // 构造函数的实现

}

// 禁用拷贝构造函数和赋值操作符
LazySingleton(const LazySingleton&) = delete; LazySingleton& operator=(const LazySingleton&) = delete;

};
class EagerSingleton {
public:

// 获取单例实例的静态成员函数
static EagerSingleton& getInstance() {

        return instance;
    }

// 其他成员函数和数据成员 private:

EagerSingleton() { // 构造函数的实现

}

// 禁用拷贝构造函数和赋值操作符
EagerSingleton(const EagerSingleton&) = delete; EagerSingleton& operator=(const EagerSingleton&) = delete;

// 在类加载时创建单例对象

    static EagerSingleton instance;
};

// 在类外部初始化静态成员变量
EagerSingleton EagerSingleton::instance;

工厂模式(Factory Pattern):用于创建对象的模式,通过将对象的创建逻辑封装在工厂类中,客 户端代码可以通过工厂类来创建对象,而不必直接实例化对象。工厂模式在项目中常用于解耦对象的创 建和使用。

  例如,一个图形库可以使用工厂模式来创建不同类型的图形对象。

// 基类
class Shape { public:

virtual void draw() = 0;

11.互斥锁(Mutex)和自旋锁

互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败会从用户态陷入到内核态,让内核帮我们切换线程,虽然简 化了使用锁的难度,但是会有两次线程上下文切换的成本:

当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行; 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时 间,把 CPU 切换给该线程运行。

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操 作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

  一般加锁的过程,包含两个步骤:
    第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
    第二步,将锁设置为当前线程持有;

};

// 具体的图形类
class Circle : public Shape { public:

    void draw() override {
        std::cout << "Draw a circle." << std::endl;

} };

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Draw a rectangle." << std::endl;

} };

// 图形工厂
class ShapeFactory { public:

    static Shape* createShape(const std::string& type) {
        if (type == "Circle") {
            return new Circle();
        }
        else if (type == "Rectangle") {
            return new Rectangle();

}

        return nullptr;
    }

};

// 在应用程序中使用工厂创建图形对象 int main() {

    Shape* shape1 = ShapeFactory::createShape("Circle");
    shape1->draw();
    Shape* shape2 = ShapeFactory::createShape("Rectangle");
    shape2->draw();
    delete shape1;
    delete shape2;
    return 0;

}

CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可 分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。

12.LRU在多线程时存在什么问题?如何解决?用锁来解决,如何提高效率?举个例子:os里的 pagecache页面换入换出,多线程下如何提高效率?

存在的问题:

  1. 线程不安全:LRUCache类的成员函数get和put在多线程环境下可能会导致数据竞争和不一致 性,因为多个线程可能同时访问和修改LRUCache对象的内部状态。

  2. 数据竞争:多个线程同时访问和修改LRUCache对象的cache head、tail_等成员变量,可能 导致数据竞争和未定义行为。

解决办法:

  1. 在LRUCache类中添加一个互斥锁成员变量,用于保护LRUCache对象的访问和修改。

  2. 在LRUCache类的成员函数中,使用互斥锁对LRUCache对象的访问和修改进行加锁和解锁操

    作,确保在任意时刻只有一个线程可以访问和修改LRUCache对象。

  3. 在对LRUCache对象进行访问和修改的地方使用互斥锁进行保护,包括对cache head、tail_

        等成员变量的访问和修改。
    

13.谈一下在浏览器输入一段URL后获取资源的全过程?

  1. URL 解析:浏览器首先会对输入的 URL 进行解析,提取出其中的协议、主机名、端口号、路 径等信息。

  2. DNS 解析:如果输入的 URL 中包含了主机名,浏览器会向 DNS 服务器发送 DNS 请求,将主 机名解析为 IP 地址。

  3. 建立 TCP 连接:浏览器根据 URL 中的协议确定使用的传输协议,通常是 HTTP 或 HTTPS。浏 览器会向服务器发起 TCP 连接请求,建立与服务器的连接。

  4. 发起 HTTP 请求:一旦 TCP 连接建立成功,浏览器就会构建一个 HTTP 请求报文,包括请求的 方法(GET、POST 等)、请求头、请求体等信息,然后将请求报文发送给服务器。

  5. 服务器处理请求:服务器收到浏览器发送的 HTTP 请求后,根据请求的内容进行相应的处理, 可能是查询数据库、读取文件等操作。

  6. 服务器返回响应:服务器处理完请求后,会生成一个 HTTP 响应报文,包括状态码、响应头、 响应体等信息,然后将响应报文发送给浏览器。

  7. 浏览器渲染页面:浏览器收到服务器返回的响应后,会根据响应的内容进行页面的渲染,包括 解析 HTML、加载 CSS 和 JavaScript、渲染页面等操作。

  8. 关闭 TCP 连接:页面渲染完成后,浏览器会关闭与服务器的 TCP 连接.

14.进程占用的内存比较多,该怎么调试是什么情况?

当一个进程占用的内存比较多时,可能会遇到内存泄漏(memory leak)或者内存使用不当等问 题。

  1. 使用内存分析工具:可以使用专门的内存分析工具来检测内存泄漏和内存使用情况。例如, Valgrind 是一个常用的开源工具,它可以检测内存泄漏和内存错误,并提供详细的报告。

  2. 监控系统资源:使用系统监控工具(如 top、htop、ps 等)来监视进程的内存使用情况。通

        过观察进程的内存占用情况和系统资源的使用情况,可以初步判断进程是否存在内存泄漏或者
    

    内存使用异常的情况。

  3. 检查代码中的内存分配和释放:仔细检查代码中的内存分配和释放操作,确保每次分配内存后

        都能正确释放,避免内存泄漏。特别注意在循环中分配内存而未释放的情况,以及在异常情况
    

    下未释放内存的情况。

  4. 分析堆栈和内存转储:在发生内存泄漏或内存使用异常时,可以通过分析进程的堆栈信息和内

    存转储(core dump)来定位问题。堆栈信息可以帮助你找到内存分配和释放的位置,而内存

        转储可以提供进程崩溃时的内存状态,有助于定位问题的根本原因。
    
  5. 使用代码审查和静态分析工具:进行代码审查可以帮助发现潜在的内存泄漏和内存使用问题。

另外,一些静态分析工具可以帮助检测代码中可能存在的内存问题,提前发现潜在的风险。

15、定位到一个进程的内存比较异常,该如何进一步查找为什么内存会异常?

  1. 使用内存分析工具进行检测:使用专门的内存分析工具(如Valgrind、AddressSanitizer等) 对进程进行内存分析,以检测是否存在内存泄漏或者内存错误。这些工具可以提供详细的内存 使用情况报告,帮助定位问题的具体原因。

  2. 检查内存分配和释放的逻辑:仔细检查代码中的内存分配和释放逻辑,确保每次分配的内存都 能够正确释放。特别关注循环中的内存分配和释放,以及异常情况下的内存处理逻辑。

  3. 分析内存使用模式:观察进程的内存使用模式,包括内存的分配和释放情况、内存占用的变化 趋势等。通过分析内存使用模式,可以发现是否存在内存泄漏或者内存占用异常的情况。

  4. 检查第三方库和系统调用:如果进程使用了第三方库或者系统调用,需要检查这些部分是否存 在内存使用不当的情况。有些第三方库可能存在内存泄漏或者内存占用过高的问题,需要特别 注意。

  5. 分析核心转储(core dump):如果进程发生了崩溃,可以分析核心转储文件以获取进程崩溃 时的内存状态。核心转储文件包含了进程崩溃时的内存快照,可以帮助定位问题的根本原因。

  6. 使用日志和调试信息:在代码中添加日志和调试信息,记录内存分配和释放的情况,以及内存 使用过程中的关键参数和状态。这些信息可以帮助你更好地理解进程的内存使用情况,并定位

    问题。

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

**K

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值