Skip to content

Latest commit

 

History

History
387 lines (288 loc) · 28.2 KB

File metadata and controls

387 lines (288 loc) · 28.2 KB

1.C++中什么是大端、小端?

大端(Big-endian)和小端(Little-endian)是指计算机中存储多字节数据时字节的排列顺序:大端将高字节存储在低地址处,低字节存储在高地址处;小端则将低字节存储在低地址处,高字节存储在高地址处。

2.重载函数是否能够通过函数返回值的类型不同来区分?

不能。重载函数不能仅仅通过函数返回值类型的不同来区分。C++的函数重载是基于参数列表的不同(包括参数的数量、类型或顺序)来实现的,编译器无法仅根据返回类型来确定。

3.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”?

首先,`extern` 是C/C++语言中用于指定函数和全局变量作用范围的关键字。它告诉编译器,这些声明的函数和变量可以在当前模块或其它模块中使用。

通常在模块的头文件中,对本模块提供给其它模块引用的函数和全局变量使用extern关键字进行声明。extern "C" 是一种连接声明(linkage declaration),被 extern "C" 修饰的变量和函数按照C语言的方式进行编译和连接。作为面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:void foo(int x, int y); 该函数被C编译器编译后在符号库中的名字为 _foo,而C++编译器则会产生像 _foo_int_int 之类的名字。这样的名字包含了函数名、函数参数数量及类型信息,C++通过这种机制实现函数重载。

总结来说,extern "C" 声明的主要目的是解决名字匹配问题,从而实现C++与C的混合编程。

4.进程间如何通信?

管道:允许一个进程和另一个与它有共同祖先的进程之间进行通信。 消息队列:消息的链表,存储在内核中,由进程间发送接收消息。 共享内存:允许多个进程访问同一块内存空间,是最快的IPC方式。 信号量:主要用于解决进程间的同步问题。 套接字:适用于不同机器间的进程通信。 信号:用于通知接收进程某个事件已经发生。

5.在网络编程中涉及并发服务器,使用多进程与多线程的区别?

(1)线程执行开销小,但不利于资源管理和保护;进程则相反,进程可跨越机器迁移。 (2)多进程时每个进程都有自己的内存空间,而多线程间共享内存空间; (3)线程产生的速度快,线程间通信快、切换快; (4)线程的资源利用率比较好; (5)线程使用公共变量或者资源时需要同步机制。

6.map的底层原理是什么?

map的底层原理: map的底层是基于红黑树实现的。 红黑树是一种自平衡二叉搜索树,它能保证基本操作(如查找、插入、删除)的时间复杂度为O(log n),确保树的高度保持在对数级别。 通过键值对自动排序和高效操作维持了其数据结构的稳定性和效率。

7.socket与其他通信方式有什么不同?

Socket 可以实现不同主机间的进程通信,适用于网络中跨操作系统通信。 Socket 通常支持全双工通信,即同一时间可以进行数据的双向传输。 Socket 支持面向连接(如TCP协议)和无连接(如UDP协议)的通信方式。 使用 Socket 进行通信需要创建、配置、使用和关闭套接字,比其他通信方式如管道和信号等有更明确的使用流程。

8.TCP和UDP的区别是什么,TCP什么时候会重传?

连接: TCP是面向连接的协议,进行数据传输前需要建立连接。 UDP是无连接的协议,不需要建立连接就可以直接发送数据。 可靠性: TCP提供可靠的数据传输,确保数据完整性和顺序。 UDP提供不可靠的数据传输,可能出现丢包,不保证数据顺序。 速度: TCP相对较慢,因为它需要确认机制和错误校正。 UDP传输速度更快,没有确认机制,适用于对速度要求高的场合。 数据流: TCP提供字节流服务,通过数据流的方式发送数据。 UDP以数据报文的形式发送信息,发送独立的消息。

超时重传:如果发送方在设定的超时时间内没有收到接收方的确认(ACK),它会重传那个数据段。 快速重传:如果发送方收到三个或更多的冗余ACK(即对同一个数据段的连续确认),它会在没有等待超时的情况下立即重传那个被认为丢失的数据段。 接收方提示:接收方可以通过ACK中的SACK选项(选择确认),明确指出哪些数据段已收到,哪些未收到,促使发送方仅重传未被确认接收的数据段。

9.map和unordered_map了解吗?

map: 底层实现是红黑树,一个自平衡的二叉搜索树。 元素根据键值自动排序。 插入、删除和查找操作的时间复杂度为O(log n)。

unordered_map: 底层实现是哈希表。 元素不会自动排序。 平均情况下插入、删除和查找操作的时间复杂度为O(1),最坏情况下为O(n)。

10.hashmap和map的区别,底层数据结构算法是什么?

在 C++ 中,虽然没有直接对应 HashMap 和 Map 的标准库接口,但可通过 unordered_map 和 map 实现类似功能。二者区别在于: 底层实现不同:unordered_map 是哈希表实现,map 是红黑树实现。 元素顺序不同:unordered_map 不保证元素顺序,map 按照键的自然顺序排序。 对空值的处理不同:从 C++11 开始,unordered_map 允许键和值为 null,map 不允许键为空。 底层数据结构算法方面: unordered_map 利用散列函数将键映射到存储桶,采用链地址法解决哈希冲突。 map 以红黑树为底层数据结构,保证元素有序,插入、删除和查找操作的时间复杂度为 O (log N)。

11.介绍一下红黑树

红黑树是一种自平衡二叉搜索树,在插入和删除操作后会通过调整节点颜色与进行旋转来维持平衡。其特点如下:

  • 节点颜色为红或黑。
  • 根节点为黑色。
  • 叶子节点(NULL 节点)皆为黑色。
  • 红色节点的两个子节点必为黑色,不可出现连续红色节点。
  • 从任一节点到其后代叶子节点的简单路径上,黑色节点数量一致,空子树到后代叶子节点的简单路径也如此。

凭借这些规则,红黑树可保持良好的平衡性质,在最坏情况下能实现 O(logN)时间复杂度的搜索、插入和删除操作。

对于插入操作,先将新节点设为红色,若违反红黑树性质 4 则进行旋转和变换修正,最后把根节点设为黑色。删除操作时,若被删除节点有两个非空子节点,需找到其后继节点(右子树最小值或左子树最大值),用后继节点替换原节点后再删除后继节点;若被删除节点只有一个子节点或无子节点则直接删除。删除后若违反性质 4,同样要进行一系列旋转和变换来修正。

12.介绍一下hash

哈希(Hash)是将任意长度的输入数据通过哈希函数

(Hash Function)转换为固定长度的输出值的过程。哈希函数将输入数据映射到一个固定大小的哈希值,通常表示为一串数字或字母。

哈希函数具有以下特点:

输入相同的数据始终会得到相同的哈希值。
不同的输入数据产生不同的哈希值。
哈希值长度固定,无论输入数据多长,输出结果都是相同长度。

在计算机领域,哈希常被用于以下方面:

数据存储和索引:使用哈希表作为底层数据结构,通过键-值对存储和快速查找

数据。 密码学:密码验证、数字签名

、消息摘要等应用需要使用安全的哈希函数。
数据完整性校验:通过比较两个文件或消息的哈希值来判断是否一致。
数据唯一性标识:例如在分布式系统中使用一致性哈希来确定节点位置。

常见的哈希函数包括MD5、SHA-1、SHA-256等,它们能够快速生成具有较低冲突率(即不同输入得到相同输出的概率很小)的哈希值。然而,在安全敏感场景下,通常需要使用更强大的哈希函数来抵抗攻击,如SHA-3或Blake2。

13.介绍一下二叉树

二叉树(Binary Tree)是一种常见的树形数据结构,它由节点(Node)组成,每个节点最多有两个子节点:左子节点和右子节点。

二叉树的特点如下:

每个节点最多有两个子节点,分别称为左子节点和右子节点。
左子树和右子树也是二叉树,可以为空。
二叉树没有环路(即不存在从某个节点出发经过若干条边回到该节点的路径)。

在二叉树中,通常会定义以下几种特殊类型的二叉树:

完全二叉树

(Complete Binary Tree):除了最后一层外,其他层的所有节点都必须是满的,并且最后一层的所有节点都尽量靠左排列。 满二叉树

(Full Binary Tree):除了叶子节点外,每个内部节点都有两个子节点。
二叉搜索树(Binary Search Tree):对于任意一个节点,其左子树上的值都小于等于该节点的值,右子树上的值都大于等于该节点的值。这种性质使得查找、插入和删除操作非常高效。

二叉树可以用递归或迭代方式进行遍历,常见的遍历方式包括:

前序遍历(Preorder Traversal):先访问根节点,然后按照前序遍历顺序递归地遍历左子树和右子树。
中序遍历(Inorder Traversal):先按照中序遍历顺序递归地遍历左子树,然后访问根节点,最后再递归地遍历右子树。
后序遍历(Postorder Traversal):先按照后序遍历顺序递归地遍历左子树和右子树,最后访问根节点。

二叉树在计算机科学中有广泛应用,例如表示算术表达式、数据库索引结构、图形渲染等。

14.编程实现LRU

LRU(Least Recently Used,最近最少使用)是一种常用的缓存淘汰策略。

下面是一个手撕LRU缓存的示例实现:

#include #include <unordered_map> #include

using namespace std;

class LRUCache { private: int capacity; unordered_map<int, pair<int, list::iterator>> cache; // 存储键值对和对应迭代器 list lruList; // 存储访问顺序

public: LRUCache(int cap) { capacity = cap; }

int get(int key) {
    if (cache.find(key) == cache.end()) {  // 若key不存在于缓存中
        return -1;
    }

    // 将该元素移到链表头部表示最新访问
    lruList.erase(cache[key].second);
    lruList.push_front(key);
    cache[key].second = lruList.begin();

    return cache[key].first;  // 返回对应value值
}

void put(int key, int value) {
    if (cache.find(key) != cache.end()) {  // 若key已存在于缓存中,则更新value,并将其移到链表头部表示最新访问
        lruList.erase(cache[key].second);
        lruList.push_front(key);
        cache[key] = make_pair(value, lruList.begin());
        return;
    }

    if (cache.size() == capacity) {  // 缓存已满,需要淘汰末尾的元素(即最久未使用)
        int lastKey = lruList.back();
        cache.erase(lastKey);
        lruList.pop_back();
    }

    // 插入新元素到链表头部表示最新访问
    lruList.push_front(key);
    cache[key] = make_pair(value, lruList.begin());
}

};

int main() { LRUCache cache(2); // 创建容量为2的LRU缓存

cache.put(1, 1);
cache.put(2, 2);
cout << cache.get(1) << endl;  // 输出:1
cache.put(3, 3); 
cout << cache.get(2) << endl;  // 输出:-1,因为key=2被淘汰了
cache.put(4, 4);
cout << cache.get(1) << endl;  // 输出:-1,因为key=1被淘汰了
cout << cache.get(3) << endl;  // 输出:3
cout << cache.get(4) << endl;  // 输出:4

return 0;

}

15.编程实现memcopy

memcpy是一个用于内存拷贝的标准库函数。下面是一个手撕memcpy函数的简单实现:

#include

void myMemcpy(void* dest, const void* src, size_t size) { char* destPtr = static_cast<char*>(dest); const char* srcPtr = static_cast<const char*>(src);

for (size_t i = 0; i < size; ++i) {
    destPtr[i] = srcPtr[i];
}

}

int main() { int source[] = {1, 2, 3, 4, 5}; int destination[5];

myMemcpy(destination, source, sizeof(source));

for (const auto& element : destination) {
    std::cout << element << " ";
}

return 0;

}

在这个示例中,myMemcpy函数接受三个参数:目标指针 dest、源指针 src 和要拷贝的字节数 size。通过将指针转换为 char* 类型,我们可以按字节进行拷贝。然后,使用一个循环来逐字节地将源数据复制到目标内存中。

注意,在实际开发中,建议使用标准库提供的 memcpy 函数或其他相关的安全替代品,以确保更高的效率和正确性。上述手撕版本只是为了演示原理,并不考虑边界情况和优化处理。

16.在AI行业中,C/C++编程中的动态库和静态库的含义是什么?两者之间什么差异?

好的,这个问题考察的是C/C++工程实践的基础知识,在AI算法岗面试中出现,是因为算法工程师不仅需要设计模型,还需要理解模型部署和集成的底层机制。动态库和静态库是模型、算子、预处理/后处理代码集成到最终应用程序的关键方式。下面我将详细解释,并用实际案例和三大领域的应用场景来说明。

核心概念解析

  1. 静态库 (Static Library, 通常 .a in Linux/macOS, .lib in Windows)

    • 含义:在编译链接阶段,库中的代码会被完整地复制到最终生成的可执行文件(.exe 或 无后缀的二进制文件)中。
    • 关键特性
      • 自包含:生成的可执行文件包含了所有需要的库代码,运行时不依赖外部的库文件。
      • 文件大小:可执行文件体积较大,因为它包含了库的完整拷贝。
      • 内存占用:如果多个程序使用了同一个静态库,每个程序的内存中都会有一份该库代码的副本,内存利用率较低
      • 更新/部署修改库需要重新编译链接整个程序才能生效。部署时只需发布单个可执行文件。
      • 链接时机:编译时链接 (gcc -lstaticlib)。
  2. 动态库 (Dynamic Library / Shared Library, 通常 .so in Linux, .dylib in macOS, .dll in Windows)

    • 含义:在编译链接阶段,编译器/链接器只在可执行文件中记录它需要哪些动态库(符号引用)。在程序运行时,操作系统的动态链接器(ld.sodyld)才会将所需的动态库加载到内存中,并将程序中的符号引用绑定到库中实际的函数/变量地址上。
    • 关键特性
      • 共享性:同一个动态库在内存中通常只有一份副本,可以被多个运行中的程序共享使用,提高内存利用率。
      • 文件大小:可执行文件体积较小,因为它只包含对库的引用。
      • 内存占用内存利用率高(共享代码段)。
      • 更新/部署更新库文件后,只需替换旧的 .so/.dll 文件,所有依赖它的程序在下次启动时自动使用新版本(注意ABI兼容性)。部署时需要同时发布可执行文件和它依赖的所有动态库。
      • 链接时机:运行时链接 (gcc -lsharedlib, 程序运行时由系统加载)。

核心差异总结表

特性 静态库 (.a, .lib) 动态库 (.so, .dll, .dylib)
链接时机 编译时 运行时
代码位置 嵌入到最终可执行文件中 独立的库文件
可执行文件大小 较大 (包含库代码) 较小 (仅含引用)
内存占用 较高 (每个程序独立拷贝) 较低 (多个程序共享内存中同一份代码)
更新库 需重新编译链接整个程序 替换库文件即可 (需注意ABI兼容性)
部署复杂度 简单 (单文件) 较复杂 (需同时部署程序+所有依赖的动态库)
运行时依赖 必须有对应版本的库文件存在
加载速度 启动快 (代码已在可执行文件中) 启动稍慢 (需加载和链接库)
插件化支持 困难 天然支持 (dlopen/LoadLibrary)

通俗易懂的实际案例:AI推理引擎集成

  • 场景:你开发了一个图像分类的AI模型(比如ResNet50),并编写了C++的推理代码(包含模型加载、预处理、推理、后处理)。现在需要将这个功能集成到两个不同的应用程序中:

    1. 一个轻量级的命令行工具:用于快速测试单张图片分类。
    2. 一个大型的图形界面软件:包含图像编辑、管理、分析等多种功能,你的分类功能只是其中一部分。
  • 解决方案与对比

    • 方案A (全用静态库):

      1. 将你的推理代码编译成一个静态库 libresnet.a
      2. 编译命令行工具时,链接 libresnet.a,生成独立的 classify_tool。部署只需这一个文件。
      3. 编译图形界面软件时,也链接 libresnet.a。最终生成一个很大的 image_editor_app 文件。部署也只需这一个文件。
      • 优点:两个应用都独立部署,互不影响。
      • 缺点
        • 图形界面软件变得非常庞大,因为它包含了完整的ResNet50推理代码。
        • 如果你修复了推理库 libresnet.a 中的一个bug,你需要重新编译并重新发布两个应用程序,用户需要重新下载安装整个大软件包。命令行工具也需要重新编译发布。
        • 如果系统同时运行 classify_toolimage_editor_app,内存中会存在两份完全相同的ResNet50推理代码,浪费内存。
    • 方案B (全用动态库):

      1. 将你的推理代码编译成一个动态库 libresnet.so (Linux) 或 resnet.dll (Windows)。
      2. 编译命令行工具 classify_tool 时,链接 libresnet.so (告诉它运行时需要这个库)。
      3. 编译图形界面软件 image_editor_app 时,也链接 libresnet.so
      4. 部署:
        • 发布 classify_tool + libresnet.so
        • 发布 image_editor_app + libresnet.so
      • 优点
        • 两个应用程序的可执行文件都变小了。
        • 如果同时运行 classify_toolimage_editor_app,操作系统通常只会将 libresnet.so 加载一次到内存,供两个程序共享使用,节省内存。
        • 如果你修复了 libresnet.so 中的一个bug,你只需重新编译发布 libresnet.so。用户只需要替换这个动态库文件,下次启动 classify_toolimage_editor_app 时,它们就会自动使用修复后的新版本!无需重新编译或重新安装整个大型应用
      • 缺点
        • 部署时需要确保 libresnet.so 文件放在程序能找到的地方(系统库路径、程序同目录或LD_LIBRARY_PATH指定)。
        • 如果用户不小心删除了 libresnet.so 或者版本不匹配,两个程序都将无法运行(运行时错误,提示找不到库或符号)。
        • 启动程序时,操作系统需要花一点时间加载和链接动态库。
    • 方案C (混合使用 - 最常见):

      • 命令行工具 classify_tool 追求极致的独立性和简单部署,使用静态链接 libresnet.a,生成一个独立的、无依赖的可执行文件。
      • 大型图形界面软件 image_editor_app 追求模块化、可更新性和内存共享,使用动态链接 libresnet.so

在AI三大领域中的应用

  1. AIGC (生成式AI - 如Stable Diffusion, LLM 部署)

    • 应用场景
      • 模型插件化:图像编辑软件(如Photoshop插件)或聊天应用中集成文生图、文生文功能。模型本身和推理引擎通常被打包成动态库 (*.so/*.dll)。
      • 模型热更新:AIGC模型迭代速度极快。使用动态库,开发者可以在不更新主程序的情况下,仅替换模型推理库或模型文件本身,用户重启应用即可体验新模型效果。
      • 多后端支持:同一个AIGC应用可能需要支持CPU、不同厂商GPU(NVIDIA/AMD/Intel)。可以将针对不同硬件优化的推理引擎(ONNX Runtime, TensorRT, OpenVINO, DirectML)编译成不同的动态库(如 sd_cpu.so, sd_cuda.so, sd_metal.so)。主程序在运行时根据检测到的硬件动态加载 (dlopen/LoadLibrary) 最合适的库。
    • 典型选择动态库是绝对主流,因其支持插件化、热更新和灵活的后端切换。静态库可能用于构建非常独立的命令行工具(如独立的SD图像生成器)。
  2. 传统深度学习 (CV/NLP模型部署 - 如ResNet, BERT)

    • 应用场景
      • 嵌入式设备部署:在资源受限的边缘设备(摄像头、工控机)上,追求极致的启动速度和确定性,避免动态链接的开销和依赖问题。常将核心模型推理代码静态链接进最终应用程序,甚至和操作系统一起编译成固件。减少文件数量和依赖,启动更快。
      • 服务器端推理服务:大型推理服务(如基于TensorFlow Serving, Triton)本身是一个大程序,它加载的模型插件通常实现为动态库。服务本身不需要重启,就能加载新的模型(新动态库)或更新现有模型库。
      • 算法SDK分发:AI公司向客户提供人脸识别、OCR等功能的SDK。为了便于客户集成和更新(尤其是修复bug或安全漏洞),SDK的核心逻辑通常封装为动态库。客户应用程序动态链接该库。更新SDK时,客户只需替换动态库文件并重启应用(或服务)。
    • 典型选择混合使用。嵌入式/边缘端倾向静态库或深度优化的单一可执行文件;服务器端SDK分发、插件模型加载普遍使用动态库。
  3. 自动驾驶 (Perception, Fusion, Planning 模块)

    • 应用场景
      • 模块化与安全隔离:自动驾驶系统极其复杂,包含感知(摄像头/雷达/Lidar)、融合、预测、规划、控制等多个模块。这些模块通常由不同团队开发。使用动态库可以将每个模块封装成独立的组件,定义清晰的接口(API)。主框架负责加载这些库并在模块间调度数据。一个模块崩溃(如除零错误导致SIGFPE)不一定导致整个系统崩溃(取决于框架设计),便于容错和调试。
      • OTA更新:汽车通过OTA更新软件是刚需。如果某个感知算法(如新的障碍物检测模型)需要更新,只需替换对应的动态库文件 (perception_camera.so),无需更新整个庞大的自动驾驶软件栈,节省带宽和时间,降低更新风险。
      • 硬件抽象层:自动驾驶芯片平台多样(NVIDIA Orin, Qualcomm Ride, Horizon Journey, Tesla FSD)。硬件驱动和底层加速库(如针对特定NPU的卷积优化)通常被封装为动态库。上层感知规划等业务逻辑通过HAL动态库接口访问硬件,业务逻辑代码无需为不同平台重新编译
      • 实时性与确定性要求:某些对启动时间或确定性要求极高的核心模块(如最底层的安全监控、看门狗),可能会采用静态链接,确保其绝对独立和快速启动。
    • 典型选择动态库是架构基石。强大的模块化、独立更新、硬件抽象能力是其核心优势。静态库仅用于少数对启动或鲁棒性有极端要求的底层组件。

面试回答建议

  1. 清晰定义:开门见山说明静态库是编译时嵌入代码,生成独立大文件;动态库是运行时加载共享代码,文件小但需依赖。
  2. 核心差异对比:重点强调链接时机、代码位置、文件大小、内存共享、更新方式、运行时依赖这几点差异。用表格对比更佳。
  3. 案例生动:使用“AI推理引擎集成到命令行工具VS大型图形软件”的例子,具体说明静态链接和动态链接在不同场景下的优劣和选择逻辑。突出动态库在大型应用中的热更新和内存共享优势。
  4. 领域应用点睛
    • AIGC:强调动态库支撑插件化、模型热更新、多后端运行时选择,适应快速迭代。
    • 传统深度学习:点明嵌入式/边缘端用静态库求快求稳,服务器端SDK/服务用动态库求灵活易更新
    • 自动驾驶:突出动态库是实现复杂系统模块化、OTA增量更新、硬件抽象层的关键技术,提升系统可维护性和安全性。
  5. 总结升华:说明理解两者差异对于AI工程师设计可部署、可维护、高效的AI系统至关重要,尤其是在模型集成、SDK开发和大型系统架构层面。