八股 8.26

1.网络时延

  • 发送时延:数据包放置到输出链路上所需的时间

    • 发送时延 = 数据包长度 / 链路带宽
  • 传输时延: 信号在物理媒介上传播所需的时间,比如光纤或电缆

    • 传输时延 = 两节点间的距离 / 信号传播速度
  • 处理时延:取决于设备的性能和需要进行的处理复杂度。

2.为什么发送多个小的数据包比一次性发送一个大的数据包开销要大?

  • 头部开销:每个数据包都需要包含一定的头部信息,用于路由和传输控制等目的。无论数据包的大小如何,这些头部信息都是必需的。
  • 处理时延:每个数据包到达路由器或交换机时都需要进行处理,包括检查包头信息、决定转发路径等操作。更多的数据包意味着需要处理更多的头部信息,这增加了设备的处理负担和总体处理时延

  • 发送时延:虽然单个小数据包的发送时延较短,但由于存在多次排队等待的情况(每次发送一个小数据包都可能需要在输出队列中等待),总的发送时延可能会比发送一个大数据包要长。

3.用户态和内核态

用户态:大多数应用程序都在用户态下运行,例如浏览器、文本编辑器等。在用户态下,程序只能访问受限的内存空间,并且不能直接访问硬件资源。

内核态:

  • 管理系统资源:内核态允许操作系统核心代码管理硬件资源(如CPU、内存、I/O设备等)、文件系统以及进程调度等关键功能。
  • 提供服务给用户程序:虽然用户程序在用户态运行,但它们需要通过系统调用进入内核态来请求操作系统提供的服务,比如读写文件、网络通信等。
  • 处理异常和中断:当发生硬件中断或者软件异常时,处理器会切换到内核态来处理这些情况。

4.线程间的通信方式

  • 共享内存 + 同步机制(最基本的方式)

    • 互斥锁(std::mutex)
    • 读写锁(std::shared_muetx)
    • 条件变量(std::condition_variable)
    • 原子操作(<atomic>)
  • 使用queue实现消息传递:std::queue结合互斥锁或条件变量来实现生产者-消费者模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include<mutex>
    #include<iostream>
    #include<thread>
    #incldue<queue>
    using namespace std;
    template<typename T>
    class SafeQueue{
    private:
    queue<T> q;
    mutex mtx;
    condition_variable cv;
    public:
    void enqueue(T t){
    lock_guard<mutex> lock(mtx);
    q.push(t);
    cv.notify_one();
    }
    T dequeue(){
    unique_lock<mutex> lock(m);
    cv.wait(lock,[this](){return !q.empty(); });
    T value=q.front();
    q.pop();
    return value;
    }
    }
  • Future/Promise 模式

  • 事件通知机制:
    • condition_variable
    • std::notify_all()/std::notify_one():唤醒一个或所有等待该条件变量的线程

5.函数指针和指针函数

指针函数:返回值是指针的函数

1
2
3
4
int *getNumber() {
static int num = 10; // 使用static确保num在函数调用之间保持其值
return &num;
}

函数指针:指向函数的指针变量

1
2
3
4
int add(int a, int b) { return a + b; }
int (*funcPtr)(int, int);
funcPtr = add;
int result = funcPtr(2, 3); //通过funcPtr调用函数add

6.Qt信号与槽的实现机制

QT信号与槽的实现机制,是以观察者模式实现的。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <vector>
#include <functional>

template<typename... Args>
class Signal {
public:
using SlotType = std::function<void(Args...)>;

void connect(SlotType slot) {
connections.push_back(slot);
}

void emit(Args... args) {
for (auto& conn : connections) {
conn(std::forward<Args>(args)...);
}
}

private:
std::vector<SlotType> connections;
};

class Observer {
public:
template <typename T>
void update(T a, T b) {
std::cout << "Update called with: " << a << ", " << b << std::endl;
}
};

int main() {
Signal<int, int> signal; // 支持两个int参数的信号
Observer observer;

signal.connect([&observer](int a, int b) {
observer.update(a, b); // 自动推导T=int
});

signal.emit(100, 200);

return 0;
}

7.一个函数如何使其在main函数执行前执行

  • 使用全局对象的构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>

    class Initializer {
    public:
    Initializer() {
    std::cout << "Initializer constructor called before main()" << std::endl;
    // 在这里执行你想做的初始化操作
    }
    };

    // 全局对象,在 main() 前构造
    Initializer init;

    int main() {
    std::cout << "main() is running" << std::endl;
    return 0;
    }
  • 使用函数局部静态变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>

    void initFunction() {
    static bool initialized = [](){
    std::cout << "Initialization code here (before main)" << std::endl;
    return true;
    }();
    }

    // 静态变量在 main 之前初始化
    static bool dummy = (initFunction(), true);

    int main() {
    std::cout << "main() is running" << std::endl;
    return 0;
    }

8.子类和父类的构造函数与析构函数的执行顺序

构造函数:先父类,再子类

析构函数:先子类,再父类

9.vector在clear后的内存变化

  • vector.clear()移除 vector 中的所有元素

    • size() 变为 0
    • 不会释放底层内存(capacity() 不变)
  • 如何真正释放内存?

    • 交换空 vector:

      1
      2
      3
      std::vector<int>(vec).swap(vec);
      // 或者更明确地写成:
      vec = std::vector<int>();
    • shrink_to_fit()

      1
      2
      vec.clear();
      vec.shrink_to_fit();

10.fork的原理

  • 复制进程:当程序执行到 fork() 调用时,操作系统会复制调用 fork()当前进程的所有信息来创建一个新的进程。
  • 返回值fork() 在父进程中返回子进程的进程ID(PID),在子进程中返回 0。
  • 共享与独立
    • 父进程和子进程拥有独立的地址空间,这意味着它们各自的数据段、堆和栈是分开的。
    • 文件描述符默认情况下是在父子进程间共享的,即如果父进程打开了一些文件,在没有特殊处理的情况下,子进程也会有这些文件的访问权限,可以读写相同的文件位置。

11.bss段和data段存放的是什么?

  • BSS: 存放程序中未初始化的全局变量和静态变量
  • 代码段: 存储程序的实际机器码,即编译后的二进制指令。这个段通常被多个进程共享,以节省内存。
  • 初始化数据段: 包含全局变量和静态变量,这些变量在程序开始执行之前已经被赋予了初始值。

12.unique_ptr如何转移所有权?

1
2
3
4
#include<memory>

std::unique_ptr<int> ptr1=std::make_unique<int> (10);
std::unique_ptr<int> ptr2=std::move(ptr1);//转移所有权,ptr1现在为空

13.什么是虚拟内存?为什么需要虚拟内存?

虚拟内存:虚拟内存在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间,它不是真实存在的,而是通过映射与实际地址空间对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存 RAM 更大的地址空间,每个程序都可以认为它拥有足够的内存来运行。

使用虚拟内存的主要原因: 隔离性+可扩展

  • 内存扩展: 物理内存(RAM)是有限的,但虚拟内存可以通过使用硬盘空间模拟额外内存来“扩展”内存容量。
  • 内存隔离: 每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存。

    • 安全性(防止恶意或错误访问其他程序的数据)
    • 系统稳定性(一个程序崩溃不会直接影响其他程序)
  • 物理内存管理:

    • 程序不需要一次性全部加载到内存中
    • 操作系统只加载当前需要的部分(称为“页”)
    • 不常用的页面可以被交换到磁盘上(即“换出”),腾出空间给其他更需要的程序使用
  • 页面交换: 当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。

    注:虽然这会导致性能下降(硬盘比内存慢得多),但它能防止程序因内存不足而崩溃。

  • 内存映射文件: 虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。

    • 操作系统可以把磁盘上的文件“映射”到虚拟内存中
    • 程序可以直接像访问内存一样读写文件内容,而不必调用传统的文件I/O函数
    • 这种方式效率更高,常用于大文件处理、共享库等场景

14.多线程顺序执行控制问题

问题描述:有三个线程分别调用 first()second()third() 方法,要求不管这三个线程的启动顺序如何,它们的执行顺序必须是:first() -> second() -> third()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<iostream>
#include<vector>
#include<mutex>
#include<condition_variable>
using namespace std;
class Solution {
private:
mutex mtx;
condition_variable cv;
int turn;
public:
Solution() :turn(1) {};
void first() {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]() {
return turn == 1;
});
cout << "first" << endl;
++turn;
cv.notify_all();
}
void second() {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]() {
return turn == 2;
});
cout << "second" << endl;
++turn;
cv.notify_all();
}
void third() {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]() {
return turn == 3;
});
cout << "third" << endl;
++turn;
cv.notify_all();
}
};

int main() {
Solution solution;

// 输入 nums,表示线程启动顺序
vector<int> nums = { 2, 1, 3 }; // 示例输入

vector<thread> threads;

for (int num : nums) {
switch (num) {
case 1:
threads.emplace_back([&solution]() {
solution.first();
});
break;
case 2:
threads.emplace_back([&solution]() {
solution.second();
});
break;
case 3:
threads.emplace_back([&solution]() {
solution.third();
});
break;
default:
cout << "Invalid method number: " << num << endl;
}
}
// 等待所有线程完成
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
return 0;
}