八股 8.26
八股 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
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 | int *getNumber() { |
函数指针:指向函数的指针变量
1 | int add(int a, int b) { return a + b; } |
6.Qt信号与槽的实现机制
QT信号与槽的实现机制,是以观察者模式实现的。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。
1 |
|
7.一个函数如何使其在main函数执行前执行
使用全局对象的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
3std::vector<int>(vec).swap(vec);
// 或者更明确地写成:
vec = std::vector<int>();shrink_to_fit()
:1
2vec.clear();
vec.shrink_to_fit();
10.fork的原理
- 复制进程:当程序执行到
fork()
调用时,操作系统会复制调用fork()
的当前进程的所有信息来创建一个新的进程。 - 返回值:
fork()
在父进程中返回子进程的进程ID(PID),在子进程中返回 0。 - 共享与独立:
- 父进程和子进程拥有独立的地址空间,这意味着它们各自的数据段、堆和栈是分开的。
- 文件描述符默认情况下是在父子进程间共享的,即如果父进程打开了一些文件,在没有特殊处理的情况下,子进程也会有这些文件的访问权限,可以读写相同的文件位置。
11.bss段和data段存放的是什么?
- BSS: 存放程序中未初始化的全局变量和静态变量
- 代码段: 存储程序的实际机器码,即编译后的二进制指令。这个段通常被多个进程共享,以节省内存。
- 初始化数据段: 包含全局变量和静态变量,这些变量在程序开始执行之前已经被赋予了初始值。
12.unique_ptr
如何转移所有权?
1 |
|
13.什么是虚拟内存?为什么需要虚拟内存?
虚拟内存:虚拟内存在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间,它不是真实存在的,而是通过映射与实际地址空间对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存 RAM 更大的地址空间,每个程序都可以认为它拥有足够的内存来运行。
使用虚拟内存的主要原因: 隔离性+可扩展
- 内存扩展: 物理内存(RAM)是有限的,但虚拟内存可以通过使用硬盘空间模拟额外内存来“扩展”内存容量。
内存隔离: 每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存。
- 安全性(防止恶意或错误访问其他程序的数据)
- 系统稳定性(一个程序崩溃不会直接影响其他程序)
物理内存管理:
- 程序不需要一次性全部加载到内存中
- 操作系统只加载当前需要的部分(称为“页”)
- 不常用的页面可以被交换到磁盘上(即“换出”),腾出空间给其他更需要的程序使用
页面交换: 当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。
注:虽然这会导致性能下降(硬盘比内存慢得多),但它能防止程序因内存不足而崩溃。
内存映射文件: 虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。
- 操作系统可以把磁盘上的文件“映射”到虚拟内存中
- 程序可以直接像访问内存一样读写文件内容,而不必调用传统的文件I/O函数
- 这种方式效率更高,常用于大文件处理、共享库等场景
14.多线程顺序执行控制问题
问题描述:有三个线程分别调用 first()
、second()
和 third()
方法,要求不管这三个线程的启动顺序如何,它们的执行顺序必须是:first() -> second() -> third()
。
1 |
|