小鹏汽车 AI Infra 一面
Q: C++中const的作用范围有哪些?
const是C++中表达”不可修改”语义的关键字,在不同上下文中有不同含义和编译期行为:
1. 修饰变量(顶层const):
1 | const int x = 42; // x的值不可修改,必须初始化 |
编译器可能将const整型放入符号表而非分配内存(类似#define但有类型安全)。
2. 修饰指针(底层const vs 顶层const):
1 | const int* p1; // 指向常量的指针:*p1不可修改,p1可修改(底层const) |
助记法:从右向左读——const int* = “pointer to const int”。
3. 修饰函数参数:
1 | void process(const std::string& s); // 承诺不修改s,且避免拷贝开销 |
const引用传参(const T&)是最常用的参数传递方式:无拷贝开销 + 可接受右值 + 承诺不修改。
4. 修饰成员函数(const限定):
1 | class Widget { |
这是C++接口设计中表达”查询操作”vs”修改操作”的核心手段。
5. 修饰返回值:
1 | const std::string& getName() const; // 返回const引用,防止外部通过返回值修改内部状态 |
6. constexpr(编译期常量):
1 | constexpr int square(int x) { return x * x; } |
C++11引入constexpr,C++14/17逐步放宽约束,C++20支持constexpr容器和动态分配。
const与线程安全:C++标准库将const成员函数视为线程安全的(多个线程可以同时调用const方法),这是C++11的重要约定。
Q: 动态链接库的依赖顺序问题?
动态链接库的加载顺序是C/C++项目中常见的”隐蔽bug”来源,需要理解链接器和加载器的工作机制:
链接时(ld linker)的符号解析规则:
- 从左到右扫描命令行中的库文件。
- 遇到未定义符号时,在后续库中查找定义。
- 被依赖的库必须放在依赖者的右边(后面)。
1
2
3
4# 正确:main.o依赖libA,libA依赖libB
gcc main.o -lA -lB
# 错误:libB在前面,找不到libA中的符号
gcc main.o -lB -lA # undefined reference错误 - 循环依赖解决:
-Wl,--start-group -lA -lB --end-group(强制多次扫描)。
运行时(ld.so dynamic linker)的搜索顺序:
RPATH(编译时嵌入ELF中,-Wl,-rpath,/path)LD_LIBRARY_PATH环境变量RUNPATH(优先级低于LD_LIBRARY_PATH)/etc/ld.so.cache(ldconfig生成的缓存)- 默认路径:
/lib、/usr/lib、/lib64、/usr/lib64
常见问题与调试工具:
ldd binary:查看所有动态库依赖及实际解析路径。nm -D libfoo.so:查看动态符号表。LD_DEBUG=libs ./binary:打印加载过程详细信息。ldconfig:更新共享库缓存。chrpath/patchelf:修改已编译binary的RPATH。
常见陷阱:
- 符号冲突(symbol interposition):先加载的库的同名符号会覆盖后加载的。
- ABI兼容性:库升级后如果ABI变了(如类增加了成员变量),需要重新编译使用者。
- dlopen的RTLD_GLOBAL vs RTLD_LOCAL:控制符号是否对后续dlopen可见。
Q: C++四种Cast的区别与底层含义?
C++提供四种显式类型转换运算符,取代C风格的(Type)expr强转,每种有明确的语义和安全级别:
1. static_cast —— 编译期已知转换
1 | double d = 3.14; |
- 底层:编译时进行类型检查和可能的代码生成(如浮点到整数的截断指令)。
- 适用:相关类型间的已知安全转换。不安全的向下转换可能导致UB(如base_ptr实际不指向Derived)。
2. dynamic_cast —— 运行时安全转换
1 | Base* b = getObject(); |
- 底层:依赖RTTI(RunTime Type Information),查询虚函数表中的类型信息。沿继承链查找是否存在目标类型。
- 开销:需要遍历类型信息(最坏O(继承深度)),约比static_cast慢10-100倍。
- 限制:基类必须有虚函数(否则无RTTI信息)。
3. const_cast —— 添加/移除const
1 | const char* cs = "hello"; |
- 底层:不生成任何代码,纯编译器层面的类型标记修改。
- 唯一合法场景:当你确定原始数据不是const的,但API返回了const指针(如C接口兼容)。
4. reinterpret_cast —— 按位重新解释
1 | int* ip = ...; |
- 底层:不生成任何代码,仅改变编译器对该地址中数据的类型解释。
- 最危险:几乎不做任何检查,误用会导致UB、对齐问题、alias violation。
- 合法使用:序列化(按字节读写对象)、底层系统编程、与C库交互。
安全级别从高到低:dynamic_cast > static_cast > const_cast > reinterpret_cast。
Q: 给出CUDA Kernel代码,识别其功能?
分析CUDA Kernel的系统化方法:
Step 1:看Kernel签名 —— 确定输入输出类型和维度
1 | __global__ void mystery_kernel(float* input, float* output, int N, int C, int H, int W) |
Step 2:看线程索引计算 —— 确定并行维度和数据映射
1 | int tid = blockIdx.x * blockDim.x + threadIdx.x; // 一维线性索引 → 逐元素操作 |
Step 3:看内存访问模式 —— 确定数据流向
- 只有全局内存读 → element-wise或reduce
- 使用共享内存 → 需要线程协作(GEMM/转置/规约)
- 有原子操作 → 多线程竞争写(直方图/reduce)
Step 4:看核心计算逻辑 —— 确定算法功能
常见kernel模式识别:
| 模式 | 特征 |
|---|---|
| 逐元素操作 | 线性tid索引,每线程处理一个元素 |
| 矩阵乘法 | 共享内存分块,双重循环累加 |
| 规约(Reduce) | 树形折半累加,__syncthreads()间隔减半 |
| 转置 | 共享内存+padding,写入时行列交换 |
| 卷积 | 多重循环遍历kernel窗口 |
| Softmax | 先求max(reduce),再exp和sum(reduce),最后归一化 |
实战技巧:关注 __syncthreads() 的位置(划分计算阶段)、shared memory的使用pattern(数据复用方式)、边界条件检查(if tid < N)。
Q: 手撕:计算平面上两个任意角度矩形的重叠面积?
(编程题)