小红书C++引擎架构一面-面经总结

1.c++ 多态,如何实现的,虚表、虚表指针存储位置?

1.静态:函数重载+模板(允许函数和类以通用方式实现。编译器根据传递的类型生成具体的函数版本。)(泛型编程 不用指定具体类型 可以自动生成具体类型)

2.动态:继承和虚函数

多态是:允许同一接口通过不同类型的对象进行不同的行为。通过虚函数和继承体系来实现,就是动态多态。

*以下为重点*
1.声明虚函数:
    在基类中声明虚函数。
    派生类中可以重写这些虚函数。
1.生成虚表:
    编译器为每个包含虚函数的类生成一个虚表。
    虚表中存储了该类中所有虚函数的地址。
3.初始化虚表指针:编译器在构造对象时,自动初始化对象的虚表指针,使其指向该类的虚表。
4.调用虚函数:通过基类的指针或引用来调用虚函数时,实际上通过虚表指针访问虚表,找到对应的函数地址,然后调用该函数。

虚表:虚函数表:每个类都有一个。存着所有虚函数的地址,编译的时候生成,用虚表指针访问。在只读数据段中。

虚表指针:每个对象都有一个隐式的虚表指针,指向这个对象所属类的虚表。虚表指针在对象的最前面,具体位置还得看编译器。

2.explicit 关键字?

C++ 中,explicit 关键字用于防止隐式类型转换,通常应用于单参数的构造函数和转换运算符。

3.unique_ptr、shared_ptr、weak_ptr的原理,有没有线程安全问题,weak_ptr的解决了什么问题?可以用裸指针吗?会有什么问题?

三个指针先看别的面经

线程安全问题:unique并不安全 多线程同时访问一个unique不安全
shared是安全的 安全的增加或者减少引用计数
weak是安全的 本身不直接访问对象 转换成shared

裸指针就是普通的指针,可以是可以,但是建议智能指针。

裸指针的问题:内存泄漏(内存释放得自己管)
指针悬挂
双重删除
资源管理繁琐。

4.介绍B树和B+树?

B树:自平衡的多路搜索树,保持数据有序。
多路搜索-平衡-节点结构复杂-空间利用率低

B+树:就是变种 可以顺序访问和范围查询了
文件系统 数据库索引

B树用于高效查找、插入、删除
B+树用于顺序访问 范围查询 InnoDB

5.介绍unordered_map、map,区别,应用场景?

map他是基于红黑树实现的 自平衡 二叉是搜索树 有序 插入删除查找都是logn
开销还比较大

而另一个就是无序的容器 是哈希表 根据哈希函数的输出确定存储位置
理想状态的时候是o1 哈希在大量数据的时候是节省空间的

map就是用于实现(优先队列 键值顺序迭代 范围查询 某个区间的 需要顺序的)
unordered_map是查找特定值的时候,统计频率的时候 超大数据量的时候。

6.c++ 11 以来有哪些新特性,标准库增加了什么新功能?

这个是大重点:总结:

auto范围shared 
lambda强制nullptr 
unordered和多线程

语法糖auto关键字-自动推导

范围的for循环

shared_ptr和unique_ptr

lambda表达式

nullptr代替NULL

强制类型转换

右值引用和移动语义

新的std::unordered_map、std::unordered_set

提供了原生的多线程支持

标准库新增:
1.正则表达式的支持
2.原子操作 atomic
3.精确的时间库
4.文件系统操作

6.写一个右值引用的场景?

首先:右值引用是什么?(移动而不是复制 提高效率 减少消耗)
右值引用允许我们“移动”资源而不是复制,从而提高程序的性能,
特别是在处理大型数据结构时。通过右值引用,可以避免不必要的复制操作,减少内存消耗,提高效率。

// 移动构造函数
    MyString(MyString&& other) noexcept {
        data = other.data;
        other.data = nullptr; // 将 other 的资源置为空
    }

假设有一个函数 createString,它创建并返回一个 MyString 对象:

MyString createString() {
    return MyString("Hello, World!");
}

当调用这个函数时,返回的 MyString 对象是一个临时对象,即右值。如果使用移动构造函数,可以显著提高性能:

int main() {
    MyString str1 = createString(); // 使用移动构造函数
    str1.print();

    MyString str2 = "Another String";
    str2 = createString(); // 使用移动赋值运算符
    str2.print();

    return 0;
}

str1 的初始化使用了移动构造函数,直接将临时对象的资源转移给了 str1,避免了不必要的内存分配和复制。

str2 的赋值使用了移动赋值运算符,同样避免了不必要的资源复制。

7.cpp 变成可执行文件的过程,链接的过程在做什么事,可执行文件里各部分都有什么?

预处理(sharp define宏的替换 扩展名.i)
编译(高级的变成汇编 .s)
汇编(汇编弄成机器码 .obj)
链接(多个目标文件和库文件组合成exe)

可执行文件exe有什么?
文本段:机器码
数据段:放数据
堆:动态分配内存
栈:存储局部变量和函数参数
重定位表:调整地址
符号表:存函数名 变量名

8.进程空间,栈会保存什么?

进程空间,是操作系统为每个运行中的进程分配的虚拟内存空间,旨在确保每个进程在运行时都感觉自己独占了整个计算机的内存资源。

栈其实就是为了保存:局部变量 函数参数 返回地址 寄存器值 零食存储区
栈是后进先出.

9.介绍一下知道的内存管理?

1.内存分区:
栈(Stack):由编译器自动管理,存放局部变量和函数调用的上下文。
堆(Heap):程序员手动管理,用于动态内存分配,需要显式分配和释放。
全局/静态存储区(Global/Static Storage):存放全局变量和静态变量,程序运行期间持续存在。

2.动态内存分配:
new和delete操作符:用于动态分配和释放对象。
new 分配内存并调用构造函数,delete 释放内存并调用析构函数。
new[] 和 delete[] 用于数组的动态内存管理。
手动管理内存需要精确匹配 new 和 delete,以及 new[] 和 delete[],以避免内存泄漏。

3.智能指针

4.资源获取即初始化(RAII)

5.内存池:
预先分配大块内存作为内存池,用于提高内存分配和释放的效率,减少内存碎片。

10.new 的底层原理是什么,底层操作系统如何将空间分配给用户进程的,new有哪些用法?

new分为两块:

一块是分配内存 有可能operator new 有可能malloc:
operator new 是C++中提供的一个全局函数,用于分配指定大小的内存块。如果分配成功,operator new 返回指向这块内存的指针;如果失败,则抛出 std::bad_alloc 异常。

一块是构造函数调用 这就是初始化。

空间如何给用户进程?
这就得看虚拟内存系统,如果物理内存不够用,不活跃的页面给他置换到磁盘,这个是对用户进程透明,维护页表,跟踪虚拟地址和物理地址的映射。

new的用法:
new一个对象 为对象分配内存
new一个对象数组

11.怎么调试-gdb, 介绍你知道的gdb命令?

得先启动吧:

gdb ./your_program

在GDB中启动程序的执行,可以使用 run 命令。

run

打个断点用break:

单步执行用next
进入函数用step
继续执行用continue

查看变量print
查看内存x
查看调用栈backtrace
结束调试quit

12.介绍一下你知道的linux指令?

cd
ls
pwd
mv
cat
top
chown chgrp chmod
head tail
mkdir rmdir

13.文件的软连接和硬链接?

软连接就是个目标文件的引用:软连接是一个独立的文件,有自己的inode号。

ln -s source_file_or_directory target_link

所有硬链接共享相同的文件数据:删除源文件不会影响其他硬链接,只有当所有硬链接都被删除时,文件数据才会被真正删除。

ln source_file target_link

14.介绍一下Go的Goroutine, 和线程的区别?

一种轻量级的线程,由Go语言的运行时(runtime)管理。
轻量级:初始只占用2KB
高效调度:Go Runtime管理 而非os
高并发:
简单的模型:使用go关键字就可以启动

与线程:
更轻量
高效的调度:用runtime 而不是os
还可以动态调整栈

goroutine适合IO密集
线程适合计算密集

15.IO多路复用的原理,应用场景?

允许一个线程同时监听多个输入流(例如网络套接字、文件描述符等),并在有数据可读或可写时进行相应的处理。这种技术通过将多个IO通道注册到一个事件管理器中,然后通过阻塞方式等待事件的发生。一旦有事件发生(如有数据可读或可写),线程就会被唤醒,然后可以针对具体的事件进行处理。

IO多路复用的核心原理是利用操作系统提供的IO多路复用技术,如select、poll或epoll等,来监控多个文件描述符的状态变化。

Select:最大文件描述符数限制(通常是1024)
Poll:解决了select的最大文件描述符数限制问题。
epoll:Linux特有的IO多路复用接口,具有更好的性能和更高的扩展性。epoll通过维护一个文件描述符表来跟踪关注的事件,只有当事件发生时才会通知应用程序。这使得epoll在处理大量并发连接时表现出色。

高性能Web服务器
数据库服务器
实时通信系统
游戏服务器
物联网(IoT)平台

16.在linux c++ 写一个服务器应该怎么写?各个模块应该怎么设计

Main Reactor
职责:负责监听主服务器socket(listenFd),接受新的客户端连接。
实现:通常在一个主事件循环(MainEventLoop)中运行。
技术:使用epoll的ET(边缘触发)模式来监听文件描述符上的事件。

  1. Sub Reactors(子反应器)

    职责:处理已接受的连接上发生的事件(如读、写事件)。
    实现:每个子反应器可能关联一个EventLoop,负责处理分配给它的特定客户端连接。
    扩展性:可以根据负载动态创建更多的子反应器。

  2. Memory Pool(内存池)

    职责:管理内存分配,优化内存使用,减少内存碎片。
    实现:预分配一定数量的内存块,用于存储会话数据、请求、响应等。

  3. ThreadPool(线程池)

    职责:执行业务逻辑处理,如HTTP、FTP请求处理。
    实现:使用轮询(Round Robin)算法来分配客户端请求给线程池中的工作线程。
    优点:提高并发处理能力,减少线程创建和销毁的开销。

  4. Timer Manager(定时器管理器)

    职责:处理定时任务,如超时检测、心跳检测等。
    实现:可以集成到事件循环中,使用时间轮或最小堆等数据结构来管理定时事件。

  5. HTTP和FTP模块

    职责:实现具体的应用层协议处理。
    实现:处理客户端的HTTP或FTP请求,并生成相应的响应。

  6. Epoll机制

    职责:提供高效的I/O多路复用功能。
    实现:使用EPOLLIN来检测读事件,EPOLLOUT来检测写事件。

  7. 长连接/短连接的处理

    职责:根据应用需求处理长连接或短连接。
    实现:长连接可以维持会话状态,短连接则每次请求后关闭。

服务器设计步骤:

初始化:设置主反应器,初始化epoll实例,设置内存池。
监听:在主反应器中监听服务器端口,接受新的连接。
分发:将新的连接分配给子反应器处理。
事件处理:在子反应器的事件循环中处理读、写事件。
业务逻辑:使用线程池来执行具体的业务逻辑处理。
定时任务:通过定时器管理器处理超时和定时任务。
响应:生成响应报文,并通过子反应器发送给客户端。
清理:关闭连接,释放资源。

上一篇:Scroll 生态首个 meme 项目 $Baggor,我们可以有哪些期待?


下一篇:通信工程学习:什么是VPN虚拟私人网络