高性能计算面经
- C++八股文
- 真景一面凉经
- 自我介绍,介绍一下你做过的加速的模块(叠噪,噪声跟原图有什么关系?)
- OpenGL和OpenCL有什么区别?
- **1. 核心用途**
- **2. 编程模型**
- **3. 硬件抽象**
- **4. API设计**
- **5. 典型应用场景**
- **6. 互操作性**
- **总结表**
- **选择依据**
- 为什么要用OpenCL?
- OpenGL和OpenCL同时走会有GPU抢占问题,怎么解决?为什么要内存拷贝?
- OpenCL为什么对功耗有优化?
- 你们是怎么做优化的(性能拆解和分析)?
- 性能本地跑很快,部署到手机很慢怎么办?跟别的算法一起跑,性能有影响怎么办(并行,调度)?
- OpenCL的img和buf有什么区别?
- **1. 数据结构和存储方式**
- **2. 访问方式**
- **3. 性能特点**
- **4. 典型应用场景**
- **5. 创建方式**
- **6. 内核代码中的访问**
- **7. 关键区别总结**
- **何时选择?**
- **互操作示例**
- 说一下内存对齐,为什么要做字节对齐
- 你们做加速有多少人?怎么划分的?哪些模块是你独立完整实现的?
- Neon是如何做的?
- 稳定性和效果问题是怎么排查的?
- 如果只能靠压测且低概率出现的问题,怎么解决?
- 为什么想离职?期望是多少?
- 反问:部门做服务端AI加速,有4-5个人
- 影石一面
- 什么是8bit量化?量化是怎么实现的?
- **1. 什么是8bit量化?**
- **2. 量化的核心步骤**
- **具体实现方法**
- **3. 量化的类型**
- **4. 实现示例**
- **5. 量化的优势与挑战**
- **总结**
- 量化之后出现精度损失怎么办?
- 1. **校准过程优化**
- 2. **量化策略调整**
- 3. **模型结构调整**
- 4. **后处理与微调**
- 5. **工具与位宽选择**
- 6. **高级技术融合**
- 7. **验证与监控**
- 实施示例(PyTorch QAT):
- 关键点总结:
- C和C++有什么区别?
- 1. **编程范式**
- 2. **核心特性**
- 3. **内存管理**
- 4. **标准库**
- 5. **兼容性**
- 6. **应用场景**
- 7. **语法细节差异**
- 8. **性能**
- 总结
- C++如何调用C函数?反过来呢?
- **1. C++调用C函数**
- **关键步骤**:使用 `extern "C"` 声明C函数,避免C++的名称修饰。
- **示例**:
- **2. C调用C++函数**
- **关键步骤**:在C++代码中,用 `extern "C"` 导出C兼容的函数接口,并在C中声明。
- **示例**:
- **注意事项**
- **总结**
- C++中如何new和delete一个数组?delete没有加[]会怎么样?
- **1. 如何正确分配和释放数组**
- **分配数组**
- **释放数组**
- **2. 错误使用 `delete` 而非 `delete[]` 的后果**
- **(1) 内存泄漏或资源泄漏**
- **(2) 堆内存损坏**
- **(3) 行为不可预测**
- **3. 对基本类型数组的影响**
- **4. 总结与最佳实践**
- **建议**:
- **示例验证**
- C++的多态是怎么实现的?
- **1. 虚函数表(vtable)**
- **2. 虚函数指针(vptr)**
- **3. 动态绑定过程**
- **4. 关键特性与注意事项**
- **5. 总结**
- 类中有虚函数,析构函数应该注意什么?什么情况下会触发虚函数?
- **一、虚函数与析构函数的注意事项**
- **1. 虚析构函数必须存在**
- **2. 构造函数和析构函数中避免调用虚函数**
- **二、虚函数的触发条件**
- **1. 通过指针或引用调用虚函数**
- **2. 虚函数未被隐藏或覆盖**
- **3. 虚函数未被`final`禁止重写**
- **三、总结**
- **关键点**
- **示例验证**
- 指针和引用的区别?
- **1. 定义与本质**
- **示例代码**
- **2. 操作与功能**
- **示例代码**
- **3. 参数传递与语义**
- **示例代码**
- **4. 内存管理**
- **示例代码**
- **5. 底层实现**
- **总结:选择指针还是引用?**
- **核心原则**
- 什么是深拷贝和浅拷贝?
- **1. 浅拷贝(Shallow Copy)**
- **示例问题**
- **2. 深拷贝(Deep Copy)**
- **正确使用**
- **3. 关键区别总结**
- **4. 何时需要深拷贝?**
- **5. 其他语言的实现**
- **6. 总结**
- C++中opencv的mat是怎么深拷贝和浅拷贝的?
- 说一下快排,二分查找
- 快速排序(Quick Sort)
- 二分查找(Binary Search)
- 对比总结
- char a = -1,在计算机内存中是怎么存储的?
- **1. 符号性决定存储形式**
- **2. 存储过程**
- **情况1:有符号char(signed char)**
- **情况2:无符号char(unsigned char)**
- **3. 验证代码**
- **4. 总结**
- **5. 扩展:补码的优势**
- C++实现sharedPtr,手写C++的string类
- 对cache的理解,如何提高缓存命中率?
- 数据结构中顺序存储和链式存储的优缺点
- 大疆车载面经
- 自我介绍+介绍简历项目
- 围绕项目问涉及的知识点
- 问了很多DSP的内容(项目相关)
- 回顾你做的项目,你觉得有哪些地方可以改进?有哪些挑战?难忘的经历
- 说说你对大疆车载的了解,反问
- 优化手段有哪些
- 模型推理加速是怎么整的
- 量化部署过程
- gpu加速有什么好处,原理是什么
- cuda相关
- 多线程如何使用,痛点是什么
- 如何做同步,同步的手段有哪些?区别是什么?分别用在什么场景
- 内存优化有哪些手段?dma相关的
- cache cpu 内存之间的联系
- 高性能面试问题
C++八股文
真景一面凉经
自我介绍,介绍一下你做过的加速的模块(叠噪,噪声跟原图有什么关系?)
OpenGL和OpenCL有什么区别?
OpenGL(Open Graphics Library)和OpenCL(Open Computing Language)是两种不同的开放标准,由Khronos Group维护,但它们的应用场景和设计目标有显著区别。以下是两者的主要区别:
1. 核心用途
-
OpenGL
专注于图形渲染,用于在GPU上高效渲染2D/3D图形。它提供了一套API,允许开发者利用硬件加速绘制复杂的图形场景(如游戏、CAD、可视化等)。 -
OpenCL
专注于通用并行计算,允许开发者利用GPU、CPU或其他计算设备(如FPGA)进行高性能计算。它适用于科学计算、物理模拟、图像处理、机器学习等需要大规模并行计算的场景。
2. 编程模型
-
OpenGL
- 基于图形管线(Graphics Pipeline),包含顶点处理、光栅化、片段处理等固定或可编程阶段。
- 使用着色器语言(GLSL)编写顶点着色器、片段着色器等程序。
- 以图形对象(如纹理、缓冲区、帧缓存)为中心,操作围绕绘制图形展开。
-
OpenCL
- 基于数据并行或任务并行模型,强调将计算任务分解为多个并行执行的工作项(Work Items)。
- 使用C语言扩展(OpenCL C)编写内核(Kernel)函数,直接在计算设备上运行。
- 以数据缓冲区和计算任务为中心,操作围绕数据处理展开。
3. 硬件抽象
-
OpenGL
- 主要面向GPU的图形功能(如光栅化、纹理映射、深度测试)。
- 设计目标是最大化图形渲染性能,对图形硬件的细节进行抽象。
-
OpenCL
- 面向异构计算(支持CPU、GPU、DSP等多种设备)。
- 提供对硬件资源的更底层控制(如内存模型、工作组划分),适合优化计算密集型任务。
4. API设计
-
OpenGL
- 状态机(State Machine)驱动,通过设置全局状态(如颜色、纹理)控制渲染流程。
- 强调图形管线的配置(如绑定着色器、设置顶点数据)。
-
OpenCL
- 面向任务的API,需要显式管理计算设备、上下文、命令队列等。
- 更接近通用编程模型,支持异步任务提交和内存传输。
5. 典型应用场景
-
OpenGL
- 游戏开发、3D建模软件(如Blender)、虚拟现实(VR)、图形界面渲染。
- 例如:通过OpenGL绘制游戏中的角色和场景。
-
OpenCL
- 科学计算(如流体模拟)、图像处理(如滤镜加速)、密码学、深度学习推理加速。
- 例如:用OpenCL加速Photoshop中的图像处理算法。
6. 互操作性
- 两者可以结合使用:
- OpenCL处理计算任务(如物理模拟、图像生成),结果通过OpenGL渲染。
- 通过共享内存对象(如OpenCL缓冲区与OpenGL纹理互操作),减少数据传输开销。
总结表
特性 | OpenGL | OpenCL |
---|---|---|
目标 | 图形渲染 | 通用并行计算 |
硬件 | 主要GPU图形单元 | 支持多设备(GPU/CPU等) |
编程模型 | 图形管线 + 着色器 | 数据/任务并行 + 内核函数 |
主要API | 状态机驱动(glBind*) | 显式任务管理(clEnqueue*) |
适用领域 | 游戏、可视化、UI | 科学计算、图像处理、AI |
选择依据
- 需要渲染图形(如3D模型、特效) → OpenGL 或现代替代品(Vulkan、DirectX)。
- 需要加速数值计算(如矩阵运算、模拟) → OpenCL 或类似框架(CUDA、SYCL)。
- 混合场景 → 结合两者(如用OpenCL生成数据,OpenGL渲染)。
为什么要用OpenCL?
OpenGL和OpenCL同时走会有GPU抢占问题,怎么解决?为什么要内存拷贝?
OpenCL为什么对功耗有优化?
你们是怎么做优化的(性能拆解和分析)?
性能本地跑很快,部署到手机很慢怎么办?跟别的算法一起跑,性能有影响怎么办(并行,调度)?
OpenCL的img和buf有什么区别?
在OpenCL中,image
对象(图像内存对象)和buffer
对象(缓冲区内存对象)是两种不同的内存类型,设计目标和适用场景有明显区别。以下是它们的核心差异:
1. 数据结构和存储方式
特性 | Buffer (cl_mem ) | Image (cl_mem ) |
---|---|---|
数据结构 | 一维线性内存,存储任意数据 | 多维结构化数据(2D/3D),类似纹理 |
数据格式 | 无格式要求(原始字节流) | 需明确指定像素格式(如CL_RGBA , CL_FLOAT 等) |
存储优化 | 普通全局内存 | 可能存储在GPU的纹理内存中(硬件缓存优化) |
2. 访问方式
特性 | Buffer | Image |
---|---|---|
读写接口 | 直接通过指针(__global float* ) | 必须通过采样器(Sampler)和坐标(如read_imagef ) |
坐标访问 | 线性索引(buffer[offset] ) | 多维坐标(如(x, y, z) ) |
自动滤波 | 不支持 | 支持(如双线性插值、边界处理) |
数据类型转换 | 无自动转换 | 自动转换为规范化值(如[0,1] ) |
3. 性能特点
特性 | Buffer | Image |
---|---|---|
缓存效率 | 依赖访问模式(适合随机访问) | 利用空间局部性(适合相邻像素访问) |
硬件加速 | 无特殊优化 | 可能使用纹理硬件单元(高速缓存、滤波) |
内存带宽 | 普通全局内存带宽 | 可能更高(纹理内存的合并访问优化) |
4. 典型应用场景
场景 | Buffer | Image |
---|---|---|
适合任务 | 通用数据(数组、结构体等) | 图像/纹理处理(滤波、插值等) |
示例 | 矩阵运算、物理模拟、数值计算 | 图像卷积、光线追踪采样、体积渲染 |
5. 创建方式
// Buffer的创建(一维数据)
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE, size_in_bytes, NULL, &err
);// Image的创建(需指定格式和维度)
cl_image_format format;
format.image_channel_order = CL_RGBA; // 通道顺序
format.image_channel_data_type = CL_FLOAT; // 数据类型cl_mem image = clCreateImage2D(context, CL_MEM_READ_ONLY, &format, width, height, 0, // row_pitch(0表示连续存储)NULL, &err
);
6. 内核代码中的访问
// Buffer的访问(直接指针操作)
__kernel void buffer_kernel(__global float* buffer) {int idx = get_global_id(0);buffer[idx] = ...;
}// Image的访问(需采样器和坐标)
__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP | CLK_FILTER_NEAREST;__kernel void image_kernel(__read_only image2d_t input, __write_only image2d_t output) {int2 coord = (int2)(get_global_id(0), get_global_id(1));float4 pixel = read_imagef(input, sampler, coord); // 读取像素write_imagef(output, coord, pixel * 2.0f); // 写入像素
}
7. 关键区别总结
维度 | Buffer | Image |
---|---|---|
数据布局 | 一维线性 | 多维结构化 |
硬件支持 | 普通内存 | 纹理内存+硬件滤波 |
适用场景 | 通用计算 | 图像/空间数据 |
访问灵活性 | 高(直接指针) | 低(需采样器+坐标) |
性能优化 | 依赖算法 | 空间局部性+缓存优化 |
何时选择?
-
用Buffer:
需要处理任意结构的原始数据(如数值数组、自定义结构体)或需要直接内存操作(如原子操作)。 -
用Image:
处理图像/纹理数据,且需要利用硬件加速的滤波(如双线性插值)、规范化坐标(如[0,1]
范围)或空间局部性优化(如卷积核计算)。
互操作示例
两者可以结合使用:
- 用Buffer存储中间计算结果,再转换为Image进行渲染。
- 从Image读取数据后,用Buffer进行非图像类计算(如统计像素值)。
说一下内存对齐,为什么要做字节对齐
你们做加速有多少人?怎么划分的?哪些模块是你独立完整实现的?
Neon是如何做的?
稳定性和效果问题是怎么排查的?
如果只能靠压测且低概率出现的问题,怎么解决?
为什么想离职?期望是多少?
反问:部门做服务端AI加速,有4-5个人
影石一面
网上的参考:
什么是8bit量化?量化是怎么实现的?
8bit量化是一种通过降低数据精度来减少存储和计算开销的技术,常用于优化深度学习模型。以下是详细的解释和实现步骤:
1. 什么是8bit量化?
- 核心概念:将高精度(如32位浮点数)数据映射到8位整数(范围通常为0-255或无符号整数,或-128-127有符号整数),以减少内存占用和加速计算。
- 应用场景:主要用于模型推理阶段的优化,适用于移动端、嵌入式设备等资源受限环境。
2. 量化的核心步骤
量化通过以下公式实现:
q = round ( x S + Z ) 和 x ′ = S ⋅ ( q − Z ) q = \text{round}\left(\frac{x}{S} + Z\right) \quad \text{和} \quad x' = S \cdot (q - Z) q=round(Sx+Z)和x′=S⋅(q−Z)
其中:
- (x):原始浮点数值。
- (q):量化后的8位整数。
- (S)(缩放因子):浮点范围与整数范围的比值。
- (Z)(零点):将浮点零点对齐到整数的偏移量。
具体实现方法
-
确定浮点范围:
- 非对称量化:统计张量中的最小值( x min x_{\text{min}} xmin)和最大值( x max x_{\text{max}} xmax)。
- 对称量化:取绝对值最大的值( x max = max ( ∣ x min ∣ , ∣ x max ∣ ) x_{\text{max}} = \max(|x_{\text{min}}|, |x_{\text{max}}|) xmax=max(∣xmin∣,∣xmax∣)),范围变为 [ − x max , x max ] [-x_{\text{max}}, x_{\text{max}}] [−xmax,xmax]。
-
计算缩放因子((S)):
- 非对称量化:
S = x max − x min 2 8 − 1 ( 如用无符号8位整数 ) S = \frac{x_{\text{max}} - x_{\text{min}}}{2^8 - 1} \quad (\text{如用无符号8位整数}) S=28−1xmax−xmin(如用无符号8位整数) - 对称量化:
S = x max 2 7 − 1 ( 如用有符号8位整数,范围-128 127 ) S = \frac{x_{\text{max}}}{2^{7} - 1} \quad (\text{如用有符号8位整数,范围-128~127}) S=27−1xmax(如用有符号8位整数,范围-128 127)
- 非对称量化:
-
计算零点((Z))(仅非对称量化需要):
Z = round ( 0 − x min S ) Z = \text{round}\left(0 - \frac{x_{\text{min}}}{S}\right) Z=round(0−Sxmin)- 用于对齐浮点数0与整数零点(例如,处理负数)。
-
量化与反量化:
- 量化:将浮点数转换为整数:
q = clip ( round ( x S + Z ) , 0 , 255 ) q = \text{clip}\left(\text{round}\left(\frac{x}{S} + Z\right), 0, 255\right) q=clip(round(Sx+Z),0,255) - 反量化:恢复近似浮点值:
x ′ = S ⋅ ( q − Z ) x' = S \cdot (q - Z) x′=S⋅(q−Z)
- 量化:将浮点数转换为整数:
3. 量化的类型
- 训练后量化(Post-Training Quantization, PTQ):
- 直接对训练好的模型进行校准(统计各层输入/输出的范围),无需重新训练。
- 速度快但可能损失精度。
- 量化感知训练(Quantization-Aware Training, QAT):
- 在训练时模拟量化过程,让模型适应低精度误差。
- 精度更高但需要额外训练时间。
4. 实现示例
以非对称量化为例:
- 输入浮点范围:[-3.0, 5.0]
- 计算缩放因子:
S = 5.0 − ( − 3.0 ) 255 ≈ 0.03137 S = \frac{5.0 - (-3.0)}{255} \approx 0.03137 S=2555.0−(−3.0)≈0.03137 - 计算零点:
Z = round ( 0 − − 3.0 0.03137 ) ≈ 96 Z = \text{round}\left(0 - \frac{-3.0}{0.03137}\right) \approx 96 Z=round(0−0.03137−3.0)≈96 - 量化值:
q = round ( − 3.0 0.03137 + 96 ) = 0 ( 下限 ) q = \text{round}\left(\frac{-3.0}{0.03137} + 96\right) = 0 \quad (\text{下限}) q=round(0.03137−3.0+96)=0(下限)
q = round ( 5.0 0.03137 + 96 ) = 255 ( 上限 ) q = \text{round}\left(\frac{5.0}{0.03137} + 96\right) = 255 \quad (\text{上限}) q=round(0.031375.0+96)=255(上限)
5. 量化的优势与挑战
- 优势:
- 模型大小减少约75%(32位→8位)。
- 内存带宽需求降低,推理速度提升(支持整数运算硬件)。
- 挑战:
- 精度损失需通过校准或QAT缓解。
- 某些层(如残差连接)对量化敏感,需特殊处理。
总结
8bit量化通过缩放因子和零点将浮点数映射到8位整数,显著压缩模型并加速推理。其实现需权衡精度与效率,常用方法包括训练后量化和量化感知训练。实际应用中,TensorFlow Lite、PyTorch等框架已提供自动化工具简化流程。
量化之后出现精度损失怎么办?
模型量化后出现精度损失是常见问题,但可通过多种方法缓解。以下是分步解决方案:
1. 校准过程优化
- 使用代表性数据:确保校准数据集覆盖真实场景的数据分布,避免偏差。
- 动态范围调整:采用百分位数校准(如99.99%)而非最大值,避免异常值影响范围估计。
- 逐通道量化(Per-Channel):对卷积层按通道单独量化,减少权重分布差异带来的误差。
2. 量化策略调整
- 混合精度量化:对敏感层(如首尾层、注意力机制)保留FP16,其他层量化到INT8。
- 对称与非对称量化:尝试激活层用非对称量化(处理ReLU后的非负值),权重用对称量化。
- 量化感知训练(QAT):在训练中模拟量化噪声,微调模型参数以适应低精度。
3. 模型结构调整
- 插入归一化层:在激活函数前添加BatchNorm或LayerNorm,稳定激活值分布。
- 避免敏感操作:减少通道间差异大的结构(如深度可分离卷积),或用量化友好型结构(如MobileNet)。
4. 后处理与微调
- 量化后微调:使用少量数据对量化模型微调,调整参数补偿量化误差。
- 逐层误差分析:对比量化前后各层输出,针对高误差层进行优化(如提升其量化位宽)。
5. 工具与位宽选择
- 更换量化工具:尝试TensorRT、TFLite等不同框架,利用其优化特性。
- 调整位宽:对关键层尝试INT16,或在支持的情况下使用混合位宽(如8/4位混合)。
6. 高级技术融合
- 知识蒸馏:用原模型指导量化模型训练,提升小模型精度。
- 自适应量化:动态调整不同输入的量化参数,平衡精度与速度。
7. 验证与监控
- 分阶段量化:逐步量化模型部分层,监控精度变化,定位敏感模块。
- 鲁棒性测试:在边缘案例(Corner Cases)上验证量化模型表现,针对性增强。
实施示例(PyTorch QAT):
import torch
from torch.quantization import QuantStub, DeQuantStub, prepare_qat, convertclass QATModel(torch.nn.Module):def __init__(self, original_model):super().__init__()self.quant = QuantStub()self.model = original_modelself.dequant = DeQuantStub()def forward(self, x):x = self.quant(x)x = self.model(x)x = self.dequant(x)return x# 加载预训练模型
model = ... # 原始模型
qat_model = QATModel(model)# 配置量化参数
qat_model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')# 准备并训练
prepare_qat(qat_model, inplace=True)
# 微调训练循环(模拟量化噪声)
train(qat_model, train_loader, epochs=5)# 转换为量化模型
quantized_model = convert(qat_model.eval())
关键点总结:
- 校准数据质量和量化策略是影响精度的核心因素。
- QAT通常比后训练量化(PTQ)效果更好,但需训练资源。
- 模型本身对量化的友好性(如结构设计)决定上限,必要时需结构调整。
通过上述方法逐步调试,通常可将精度损失控制在1%以内,甚至无损量化。若仍不达标,需权衡性能与精度,考虑硬件支持的极限位宽或异构计算方案。
C和C++有什么区别?
C和C++是两种广泛使用的编程语言,虽然C++最初是作为C的扩展开发的,但两者在设计和用途上有显著区别。以下是它们的主要差异:
1. 编程范式
- C:过程式编程语言,强调函数和步骤的分解,通过函数模块化解决问题。
- C++:多范式语言,支持面向对象编程(类、继承、多态)、泛型编程(模板)、过程式编程,甚至函数式编程。
2. 核心特性
-
C:
- 无类和对象的概念。
- 不支持函数重载(同名函数)和运算符重载。
- 没有引用(仅用指针),无异常处理机制。
- 结构体(
struct
)不能包含函数。
-
C++:
- 面向对象特性:类(
class
)、继承、多态(虚函数)、封装。 - 函数重载:允许同名函数根据参数不同实现不同功能。
- 运算符重载:例如自定义
+
操作符的行为。 - 引用类型:通过
int& ref = a
直接操作变量。 - 模板:支持泛型编程(如
vector<T>
)。 - 异常处理:
try/catch/throw
机制。 - 命名空间:避免命名冲突(
namespace
)。
- 面向对象特性:类(
3. 内存管理
- C:手动管理内存,仅通过
malloc
/calloc
和free
操作堆内存。 - C++:引入
new
和delete
运算符,支持构造/析构函数自动管理资源(如RAII技术)。
4. 标准库
- C:标准库较小,提供基础功能(如
stdio.h
,stdlib.h
, 字符串处理等)。 - C++:包含C的大部分库,并扩展了面向对象和泛型的库:
- STL(标准模板库):容器(
vector
,map
)、算法(sort
,find
)、迭代器。 - 输入输出流:
cout
/cin
代替printf
/scanf
。
- STL(标准模板库):容器(
5. 兼容性
- C++兼容C:大多数C代码可在C++中编译(需注意少数语法差异,如强制类型转换)。
- C不兼容C++:C无法直接使用C++的类、引用等特性。
6. 应用场景
- C:适合底层开发(操作系统、嵌入式系统、驱动程序),追求极致性能和硬件控制。
- C++:适合大型软件、游戏引擎、GUI应用、高频交易等,兼顾性能与抽象需求。
7. 语法细节差异
特性 | C | C++ |
---|---|---|
空指针 | NULL | nullptr (更安全) |
默认返回值 | 函数不写返回值默认int | 必须显式声明返回值类型 |
结构体声明 | 定义变量需加struct 关键字 | 可直接使用类型名(如MyStruct s; ) |
常量定义 | const 变量可能不是真正的常量 | const 为真正常量,支持常量表达式 |
8. 性能
- 两者性能接近,但C++的某些高级特性(如虚函数、RTTI)可能带来微小开销,可通过谨慎编码规避。
- C更贴近硬件,适合对性能极度敏感的场景。
总结
- C:简单、高效,适合底层和资源受限环境。
- C++:功能丰富,适合复杂项目,通过抽象提高代码可维护性,同时保持高性能。
选择时需根据项目需求:若需要面向对象或高级抽象,选C++;若追求极简和直接硬件控制,选C。许多开发者会同时掌握两者以应对不同场景。
C++如何调用C函数?反过来呢?
在C++和C之间相互调用函数时,由于两种语言的编译机制和符号命名规则不同,需要特别注意名称修饰(Name Mangling)和链接规范(Linkage Specification)。以下是具体实现方法:
1. C++调用C函数
关键步骤:使用 extern "C"
声明C函数,避免C++的名称修饰。
示例:
-
C代码(
example.c
):#include <stdio.h>void c_function() {printf("This is a C function.\n"); }
-
C头文件(
example.h
):声明时添加extern "C"
(仅在C++中生效):#ifdef __cplusplus extern "C" { // 告诉C++编译器:以下函数按C的规则编译和链接 #endifvoid c_function();#ifdef __cplusplus } #endif
-
C++代码(
main.cpp
):#include "example.h"int main() {c_function(); // 正确调用C函数return 0; }
-
编译命令:
gcc -c example.c -o example.o # 编译C代码 g++ main.cpp example.o -o main # 编译C++并链接C的目标文件
2. C调用C++函数
关键步骤:在C++代码中,用 extern "C"
导出C兼容的函数接口,并在C中声明。
示例:
-
C++代码(
cpp_code.cpp
):#include <iostream>// 导出C兼容的函数(禁止名称修饰) extern "C" void cpp_function() {std::cout << "This is a C++ function called from C.\n"; }
-
C头文件(
cpp_header.h
):#ifdef __cplusplus extern "C" { // C++需要extern "C",C编译器会忽略这段代码 #endifvoid cpp_function(); // 在C中声明为普通函数#ifdef __cplusplus } #endif
-
C代码(
main.c
):#include "cpp_header.h"int main() {cpp_function(); // 调用C++函数return 0; }
-
编译命令:
g++ -c cpp_code.cpp -o cpp_code.o # 编译C++代码 gcc main.c cpp_code.o -o main -lstdc++ # 编译C并链接C++的目标文件(需C++标准库)
注意事项
-
类型兼容性:
- 被调用的函数参数和返回值必须是C兼容的类型(如基本类型、指针、
struct
等)。 - 避免传递C++特有类型(如类、模板、引用等)。
- 被调用的函数参数和返回值必须是C兼容的类型(如基本类型、指针、
-
全局函数限制:
- C无法直接调用C++的类成员函数或操作对象,需通过封装全局函数间接实现:
extern "C" void wrapper_for_cpp_class_method() {MyClass obj;obj.method(); }
- C无法直接调用C++的类成员函数或操作对象,需通过封装全局函数间接实现:
-
名称修饰问题:
- C++函数若未用
extern "C"
,其符号名会被编译器修饰(如_Z12func_namev
),导致C代码无法找到该函数。
- C++函数若未用
-
编译器和链接器:
- 确保C和C++代码分别用对应编译器编译(如
gcc
和g++
)。 - 链接时可能需要指定C++标准库(如
-lstdc++
)。
- 确保C和C++代码分别用对应编译器编译(如
总结
场景 | 关键方法 | 示例 |
---|---|---|
C++调用C函数 | 在C头文件中用 extern "C" 包裹声明 | extern "C" { void c_func(); } |
C调用C++函数 | 在C++代码中用 extern "C" 导出函数 | extern "C" void cpp_func() { ... } |
通过合理使用 extern "C"
和类型兼容性约束,可以无缝实现C与C++的互操作。
C++中如何new和delete一个数组?delete没有加[]会怎么样?
在C++中,动态分配和释放数组需要严格遵循new[]
和delete[]
的配对使用规则。以下是具体说明及错误使用的后果:
1. 如何正确分配和释放数组
分配数组
使用 new[]
运算符为数组动态分配内存,并初始化元素(若为类类型,调用构造函数):
// 分配一个包含10个int的数组
int* intArray = new int[10]; // 分配一个包含5个对象的数组(假设MyClass有默认构造函数)
MyClass* objArray = new MyClass[5];
释放数组
必须使用 delete[]
释放数组内存(若为类类型,调用每个元素的析构函数):
delete[] intArray; // 正确释放int数组
delete[] objArray; // 正确释放对象数组
2. 错误使用 delete
而非 delete[]
的后果
如果误用 delete
释放数组,会导致未定义行为(Undefined Behavior, UB),具体表现可能包括:
(1) 内存泄漏或资源泄漏
- 类类型数组:
delete
只会调用第一个元素的析构函数,其余元素的析构函数不会被调用。
若类持有动态资源(如堆内存、文件句柄),这些资源将泄漏。class ResourceHolder { public:ResourceHolder() { data = new int[100]; } // 分配资源~ResourceHolder() { delete[] data; } // 应在析构函数释放资源 private:int* data; };ResourceHolder* array = new ResourceHolder[3]; delete array; // 错误!只有第一个元素调用析构函数,剩余2个资源泄漏!
(2) 堆内存损坏
new[]
和delete[]
的实现通常会在分配的内存块头部记录数组长度。
若用delete
释放数组,内存管理器可能无法正确识别分配块的大小,导致堆结构破坏,引发程序崩溃或难以调试的错误。
(3) 行为不可预测
- 未定义行为的具体表现取决于编译器和运行时环境,可能包括:
- 程序直接崩溃。
- 内存泄漏但程序看似正常运行。
- 后续内存操作出现诡异错误。
3. 对基本类型数组的影响
即使数组元素是基本类型(如int
、float
),仍需使用 delete[]
:
int* arr = new int[10];
delete arr; // 错误!应为 delete[] arr;
- 虽然基本类型没有析构函数,但
new[]
可能仍会记录数组长度信息。
使用delete
会导致内存管理器错误解析分配块,可能引发堆损坏。
4. 总结与最佳实践
操作 | 正确用法 | 错误用法 | 后果 |
---|---|---|---|
分配数组 | new T[n] | - | - |
释放数组 | delete[] ptr | delete ptr | 未定义行为(内存泄漏、崩溃等) |
分配单个对象 | new T | - | - |
释放单个对象 | delete ptr | delete[] ptr | 未定义行为(堆损坏) |
建议:
- 避免手动管理数组:优先使用
std::vector
或std::unique_ptr<T[]>
等容器/智能指针。// 使用智能指针自动管理数组 std::unique_ptr<int[]> safeArray(new int[10]);
- 严格配对:若必须手动管理,确保
new[]
和delete[]
严格成对出现。
示例验证
#include <iostream>class Test {
public:Test() { std::cout << "Constructor\n"; }~Test() { std::cout << "Destructor\n"; }
};int main() {Test* arr = new Test[3];delete[] arr; // 正确:输出3次"Destructor"// delete arr; // 错误:仅输出1次"Destructor",其余泄漏return 0;
}
输出(正确使用 delete[]
):
Constructor
Constructor
Constructor
Destructor
Destructor
Destructor
输出(错误使用 delete
):
Constructor
Constructor
Constructor
Destructor // 只有第一个元素被销毁
遵循规则可避免资源管理错误,确保程序健壮性。
C++的多态是怎么实现的?
C++的多态性主要通过虚函数(Virtual Functions)和虚函数表(vtable)机制来实现,其核心在于动态绑定(Dynamic Binding),允许在运行时确定调用的具体函数。以下是详细的实现机制:
1. 虚函数表(vtable)
- 定义:每个包含虚函数的类(或从包含虚函数的类派生的类)都有一个虚函数表。这是一个隐式的静态数组,存储该类所有虚函数的地址。
- 结构:
- 表中条目按虚函数的声明顺序排列。
- 若子类重写虚函数,表中对应条目替换为子类函数的地址;未重写则保留父类函数地址。
- 多重继承:若类继承多个含虚函数的基类,会为每个基类维护一个独立的虚函数表。
2. 虚函数指针(vptr)
- 隐藏成员:每个对象实例内部包含一个指向其所属类虚函数表的指针(
vptr
),通常位于对象内存布局的起始位置。 - 初始化:
- 在对象构造时,由构造函数隐式初始化
vptr
,使其指向当前类的虚函数表。 - 构造顺序:基类→派生类,故
vptr
在构造过程中可能被多次修改,最终指向实际类型的虚函数表。
- 在对象构造时,由构造函数隐式初始化
- 析构过程:析构时反向调整
vptr
,确保销毁阶段仍能正确调用当前类的虚函数。
3. 动态绑定过程
- 调用步骤:
- 通过对象指针/引用找到
vptr
。 - 通过
vptr
定位到虚函数表。 - 根据函数在表中的偏移量获取实际函数地址。
- 执行该地址对应的函数。
- 通过对象指针/引用找到
- 示例:
class Animal { public:virtual void speak() { cout << "Animal sound" << endl; } };class Dog : public Animal { public:void speak() override { cout << "Woof!" << endl; } };Animal* animal = new Dog(); animal->speak(); // 输出 "Woof!"
animal->speak()
运行时,通过Dog
对象的vptr
找到Dog
的虚函数表,调用Dog::speak()
。
4. 关键特性与注意事项
- 虚析构函数:若通过基类指针删除派生类对象,基类析构函数必须为虚函数,以确保正确调用派生类析构函数,避免资源泄漏。
- 性能开销:
- 空间:每个对象需存储
vptr
(通常4/8字节)。 - 时间:虚函数调用需两次间接寻址(访问
vptr
→查表→跳转),略慢于非虚函数。
- 空间:每个对象需存储
- 覆盖控制:
override
(C++11):显式标记重写,编译器检查签名一致性。final
:禁止进一步重写或继承。
5. 总结
C++多态通过虚函数表和虚函数指针实现动态绑定:
- 编译时:生成虚函数表,记录虚函数地址。
- 运行时:通过对象的
vptr
查找虚函数表,确定实际调用的函数。
这种机制在保持高效性的同时,提供了灵活的运行时多态支持,是C++面向对象设计的核心基石之一。
类中有虚函数,析构函数应该注意什么?什么情况下会触发虚函数?
在C++中,当类包含虚函数时,析构函数的设计和虚函数的触发条件是保证多态行为正确性的关键。以下是详细解答:
一、虚函数与析构函数的注意事项
1. 虚析构函数必须存在
-
问题背景:
若通过基类指针删除一个派生类对象,而基类析构函数未声明为虚函数,会导致派生类的析构函数未被调用,引发资源泄漏(如内存、文件句柄未释放)。 -
正确做法:
class Base { public:virtual ~Base() {} // 虚析构函数virtual void foo() = 0; };class Derived : public Base { public:~Derived() { /* 释放派生类资源 */ }void foo() override { /* 实现 */ } };Base* obj = new Derived(); delete obj; // 正确调用 Derived::~Derived() 和 Base::~Base()
- 基类析构函数必须为虚函数,确保通过基类指针删除对象时,派生类析构逻辑被执行。
2. 构造函数和析构函数中避免调用虚函数
-
问题:
在构造函数和析构函数中调用虚函数时,动态绑定失效,实际调用的是当前类的版本(而非派生类重写的版本)。- 构造函数执行时,派生类尚未初始化,虚函数表(vtable)指向当前类的虚函数。
- 析构函数执行时,派生类已部分销毁,虚函数表可能已恢复为基类版本。
-
示例:
class Base { public:Base() { callFoo(); } // 危险:调用 Base::foo()virtual void foo() { cout << "Base::foo" << endl; }void callFoo() { foo(); } };class Derived : public Base { public:void foo() override { cout << "Derived::foo" << endl; } };Derived d; // 输出 "Base::foo",而非 "Derived::foo"
二、虚函数的触发条件
虚函数的动态绑定(多态)仅在以下场景生效:
1. 通过指针或引用调用虚函数
- 动态绑定触发条件:
Base* ptr = new Derived(); ptr->foo(); // 调用 Derived::foo()Base& ref = *ptr; ref.foo(); // 调用 Derived::foo()
- 若直接通过对象实例调用虚函数,静态绑定生效,无多态行为:
Derived d; d.foo(); // 调用 Derived::foo() Base b = d; b.foo(); // 调用 Base::foo()(对象切片,无多态)
- 若直接通过对象实例调用虚函数,静态绑定生效,无多态行为:
2. 虚函数未被隐藏或覆盖
- 函数签名必须一致:
派生类的虚函数需与基类虚函数名称、参数、返回类型完全一致(协变返回类型除外)。- 使用
override
关键字(C++11)可让编译器检查重写是否合法:class Derived : public Base { public:void foo() override { /* ... */ } // 显式标记重写 };
- 使用
3. 虚函数未被final
禁止重写
- 若基类虚函数被声明为
final
,派生类无法重写:class Base { public:virtual void foo() final {} // 禁止派生类重写 };
三、总结
关键点
-
虚析构函数:
- 基类的析构函数必须为虚函数,确保多态删除对象时资源正确释放。
- 若类可能被继承,且需通过基类指针操作对象,析构函数应声明为虚函数。
-
虚函数触发条件:
- 通过基类指针或引用调用虚函数时触发动态绑定。
- 构造函数和析构函数中调用虚函数可能导致非预期行为。
-
设计建议:
- 若类含虚函数,析构函数应始终为虚函数(除非明确禁止继承)。
- 使用
override
和final
增强代码可读性和安全性。
示例验证
#include <iostream>
using namespace std;class Animal {
public:virtual ~Animal() { cout << "~Animal()" << endl; }virtual void speak() { cout << "Animal sound" << endl; }
};class Dog : public Animal {
public:~Dog() { cout << "~Dog()" << endl; }void speak() override { cout << "Woof!" << endl; }
};int main() {Animal* animal = new Dog();animal->speak(); // 输出 "Woof!"delete animal; // 正确调用 ~Dog() 和 ~Animal()return 0;
}
输出:
Woof!
~Dog()
~Animal()
通过合理使用虚析构函数和虚函数,可以确保C++多态行为的正确性和资源管理的安全性。
指针和引用的区别?
在C++中,指针(Pointer)和引用(Reference)都是用于间接访问数据的工具,但它们在语义、用法和底层实现上存在显著差异。以下是它们的核心区别及适用场景:
1. 定义与本质
特性 | 指针 | 引用 |
---|---|---|
本质 | 存储变量地址的变量(独立实体) | 变量的别名(与原变量共享内存地址) |
初始化要求 | 可以不初始化(但建议初始化为nullptr ) | 必须初始化,且绑定后不可更改指向对象 |
空值(Null) | 可以指向空地址(nullptr ) | 不能为空,必须绑定有效对象 |
示例代码
int a = 10;// 指针
int* p = &a; // 合法
int* p2; // 合法(但未初始化,危险!)
p2 = nullptr; // 合法// 引用
int& r = a; // 合法
int& r2; // 编译错误!引用必须初始化
2. 操作与功能
特性 | 指针 | 引用 |
---|---|---|
重新赋值 | 可修改指向其他对象 | 一旦绑定,无法更改(始终指向初始对象) |
内存操作 | 支持指针算术(如p++ 、p += 1 ) | 不支持算术运算(仅是别名) |
多级间接访问 | 支持多级指针(如int** pp ) | 不支持多级引用(但可通过typedef 间接实现) |
解引用方式 | 需显式使用* (如*p = 20 ) | 直接使用原变量名(如r = 20 ) |
示例代码
int arr[3] = {1, 2, 3};// 指针算术
int* p = arr;
p++; // 指向arr[1]
*p = 20; // arr[1] = 20// 引用无法重新绑定
int x = 5, y = 10;
int& ref = x;
ref = y; // 实际是x = y(ref仍绑定x,而非y)
3. 参数传递与语义
场景 | 指针 | 引用 |
---|---|---|
函数参数 | 显式传递地址,需检查空值 | 隐式传递别名,语法更简洁,无需空值检查 |
函数重载 | void func(int*) 和void func(int&) 视为不同函数 | |
返回值 | 可以返回指针(如动态分配的对象) | 可以返回引用(如避免对象拷贝) |
示例代码
// 指针参数
void swap_ptr(int* a, int* b) {if (a && b) { // 需检查空指针int tmp = *a;*a = *b;*b = tmp;}
}// 引用参数(更安全)
void swap_ref(int& a, int& b) {int tmp = a;a = b;b = tmp;
}
4. 内存管理
特性 | 指针 | 引用 |
---|---|---|
动态内存 | 可指向堆内存(需手动new /delete ) | 通常绑定栈内存,生命周期由作用域管理 |
资源所有权 | 可能涉及资源所有权转移(需谨慎管理) | 不拥有资源,仅作为别名 |
示例代码
// 指针管理动态内存
int* p = new int(10);
delete p; // 必须手动释放// 引用绑定栈对象
int x = 10;
int& r = x; // 无需释放
5. 底层实现
- 指针:直接对应内存地址,操作透明(如
mov
指令操作地址值)。 - 引用:通常由编译器实现为“自动解引用的指针”,但在语法层面隐藏了地址操作。
总结:选择指针还是引用?
场景 | 推荐选择 | 原因 |
---|---|---|
可选参数(允许空值) | 指针(如nullptr 表示无数据) | 引用无法表示空值 |
函数内需重新绑定对象 | 指针 | 引用一旦绑定不可修改 |
避免拷贝大对象 | 引用 | 传递别名而非副本,高效 |
实现多态 | 指针或引用均可(虚函数通过引用/指针触发多态) | |
资源管理(如动态内存) | 指针(配合RAII或智能指针) | 引用不拥有资源 |
核心原则
- 优先使用引用:在参数传递、返回值优化等场景中,引用更安全、简洁。
- 必须使用指针:当需要处理动态内存、可选参数或多级间接访问时。
什么是深拷贝和浅拷贝?
在编程中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是两种不同的对象拷贝方式,主要区别在于对资源所有权和内存管理的处理方式。以下是它们的核心区别及实际应用场景:
1. 浅拷贝(Shallow Copy)
-
定义:
仅复制对象的成员值(包括指针的值),而不复制指针指向的实际数据。- 拷贝后的对象与原对象的指针成员指向同一块内存。
-
特点:
- 速度快(仅复制指针地址,不涉及内存分配和数据复制)。
- 潜在风险:多个对象共享同一资源,可能导致悬空指针、双重释放等问题。
-
C++中的默认行为:
class ShallowExample {int* data; public:// 默认拷贝构造函数和赋值运算符执行浅拷贝ShallowExample(const ShallowExample& other) : data(other.data) {} };
示例问题
ShallowExample obj1;
ShallowExample obj2 = obj1; // 浅拷贝
// obj1和obj2的data指向同一内存
delete obj1.data; // 释放内存
obj2.data[0] = 10; // 危险!obj2.data已是悬空指针
2. 深拷贝(Deep Copy)
-
定义:
不仅复制对象的成员值,还会为指针成员分配新内存,并复制原指针指向的所有数据。- 拷贝后的对象与原对象完全独立,资源互不影响。
-
特点:
- 安全性高(资源独立,避免共享冲突)。
- 速度较慢(需要分配内存并复制数据)。
-
实现方式:
class DeepExample {int* data;size_t size; public:// 自定义深拷贝构造函数DeepExample(const DeepExample& other) {size = other.size;data = new int[size]; // 分配新内存memcpy(data, other.data, size * sizeof(int)); // 复制数据}// 深拷贝赋值运算符DeepExample& operator=(const DeepExample& other) {if (this != &other) { // 防止自赋值delete[] data; // 释放旧资源size = other.size;data = new int[size]; // 分配新内存memcpy(data, other.data, size * sizeof(int));}return *this;}~DeepExample() { delete[] data; } };
正确使用
DeepExample obj1;
DeepExample obj2 = obj1; // 深拷贝:obj2.data是独立内存块
delete obj1.data; // 不影响obj2.data
obj2.data[0] = 10; // 安全
3. 关键区别总结
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
资源所有权 | 共享资源(多个对象指向同一内存) | 独立资源(每个对象拥有自己的内存副本) |
内存操作 | 不分配新内存,仅复制指针值 | 分配新内存并复制数据 |
性能 | 快 | 慢(取决于数据大小) |
安全性 | 低(需手动管理共享资源) | 高(资源隔离,避免冲突) |
适用场景 | 只读共享数据、性能敏感且无资源所有权的场景 | 需独立管理资源的场景(如动态数组、字符串等) |
4. 何时需要深拷贝?
- 对象管理资源时:
若类包含指针成员,且该指针指向动态分配的内存(如数组、文件句柄、网络连接等),必须实现深拷贝。 - 遵循“三法则”:
在C++中,如果类需要自定义析构函数,通常也需要自定义拷贝构造函数和拷贝赋值运算符(即深拷贝逻辑)。
5. 其他语言的实现
- Java/Python:
- 默认的赋值和拷贝(如
clone()
或copy.copy()
)通常是浅拷贝。 - 深拷贝需手动实现(如Java实现
Cloneable
接口,Python使用copy.deepcopy()
)。
- 默认的赋值和拷贝(如
- JavaScript:
- 对象展开符(
{...obj}
)或Object.assign()
是浅拷贝。 - 深拷贝需递归复制或使用
JSON.parse(JSON.stringify(obj))
(局限性:无法处理函数、循环引用)。
- 对象展开符(
6. 总结
- 浅拷贝:适合轻量级、无需资源隔离的场景,但需谨慎管理共享资源。
- 深拷贝:确保资源独立性,是管理动态内存或独占资源的必要手段。
- 在C++中:深拷贝需手动实现;在高级语言中(如Python),可依赖内置方法,但仍需理解底层逻辑以避免陷阱。
C++中opencv的mat是怎么深拷贝和浅拷贝的?
说一下快排,二分查找
快速排序(Quick Sort)
算法思想:
快速排序采用分治策略,通过选择一个基准元素将数组分成两部分,使左边元素均小于基准,右边元素均大于基准,然后递归地对子数组排序。
步骤详解:
-
选择基准(Pivot):
通常可选第一个元素、最后一个元素、中间元素或随机元素作为基准。优化方法如三数取中法(选首、中、尾的中位数)可减少最坏情况概率。 -
分区(Partition):
重新排列数组,使小于基准的元素位于左侧,大于基准的位于右侧。最终基准元素的位置即为分区点。- 双指针法:
i = 左边界 - 1 pivot = 右边界元素 for j from 左边界 to 右边界-1:if arr[j] <= pivot:i += 1swap arr[i] 和 arr[j] swap arr[i+1] 和 arr[右边界] return i+1
- 双指针法:
-
递归排序:
对分区后的左右子数组重复上述步骤,直到子数组长度为1。
时间复杂度:
- 平均:(O(n \log n))
- 最坏(已排序数组+固定基准):(O(n^2))
- 优化后(随机基准):接近平均情况
代码示例:
#include <iostream>
#include <vector>
using namespace std;// 分区函数:将数组分为左右两部分,返回基准元素的正确索引
int partition(vector<int>& arr, int low, int high) {int pivot = arr[high]; // 选择最后一个元素作为基准int i = low - 1; // i标记比基准小的元素的右边界// 遍历数组,将小于等于基准的元素交换到左侧for (int j = low; j < high; j++) {if (arr[j] <= pivot) {i++;swap(arr[i], arr[j]);}}// 将基准元素放到正确的位置(i+1)swap(arr[i + 1], arr[high]);return i + 1;
}// 快速排序递归函数
void quickSort(vector<int>& arr, int low, int high) {if (low < high) {int pi = partition(arr, low, high); // 获取基准位置quickSort(arr, low, pi - 1); // 递归排序左半部分quickSort(arr, pi + 1, high); // 递归排序右半部分}
}// 测试快速排序
void testQuickSort() {vector<int> arr = {10, 7, 8, 9, 1, 5};cout << "原始数组: ";for (int num : arr) cout << num << " ";cout << endl;quickSort(arr, 0, arr.size() - 1);cout << "排序后数组: ";for (int num : arr) cout << num << " ";cout << endl;
}
二分查找(Binary Search)
算法思想:
在有序数组中,通过不断将搜索区间对半分,快速定位目标值的位置。
步骤详解:
-
初始化边界:
左边界left = 0
,右边界right = len(arr) - 1
。 -
循环查找:
- 计算中间索引
mid = left + (right - left) // 2
(避免整数溢出)。 - 若
arr[mid] == target
,返回mid
。 - 若
arr[mid] < target
,调整左边界left = mid + 1
。 - 否则调整右边界
right = mid - 1
。
- 计算中间索引
-
终止条件:
当left > right
时,表示目标不存在,返回 -1。
时间复杂度:
- (O(\log n)),每次将搜索范围缩小一半。
代码示例:
#include <iostream>
#include <vector>
using namespace std;// 二分查找函数(返回目标索引,未找到返回-1)
int binarySearch(const vector<int>& arr, int target) {int left = 0;int right = arr.size() - 1;while (left <= right) {int mid = left + (right - left) / 2; // 避免整数溢出if (arr[mid] == target) {return mid;} else if (arr[mid] < target) {left = mid + 1; // 目标在右半部分} else {right = mid - 1; // 目标在左半部分}}return -1;
}// 测试二分查找
void testBinarySearch() {vector<int> arr = {1, 3, 5, 7, 9};int target = 5;cout << "有序数组: ";for (int num : arr) cout << num << " ";cout << endl;int index = binarySearch(arr, target);if (index != -1) {cout << "目标 " << target << " 的索引为: " << index << endl;} else {cout << "目标 " << target << " 未找到" << endl;}
}
对比总结
算法 | 应用场景 | 时间复杂度 | 核心操作 | 关键点 |
---|---|---|---|---|
快速排序 | 大规模数据排序 | 平均 (O(n \log n)) | 分区与递归 | 基准选择、原地排序 |
二分查找 | 有序数据查询 | (O(\log n)) | 折半比较 | 边界处理、有序前提 |
注意事项:
- 快速排序需注意基准选择优化以避免最坏情况。
- 二分查找必须确保输入数组有序,且正确处理中间值计算防止溢出。
char a = -1,在计算机内存中是怎么存储的?
在C/C++中,char a = -1;
在内存中的存储方式如下:
1. 符号性决定存储形式
-
char
的符号性:
char
的默认符号性由编译器决定:- 有符号(signed char):范围是
-128
到127
(8位补码)。 - 无符号(unsigned char):范围是
0
到255
(8位纯二进制)。
大多数编译器(如GCC/x86)默认
char
为 有符号,但某些平台(如ARM)可能默认为无符号。
需通过编译选项或显式声明(signed char
/unsigned char
)明确符号性。 - 有符号(signed char):范围是
2. 存储过程
情况1:有符号char(signed char)
- 补码表示:
-1
的补码形式为11111111
(二进制),即0xFF
(十六进制)。- 补码规则:绝对值取反后加1。
1的二进制:00000001 取反:11111110 加1:11111111 → 即-1的补码
- 补码规则:绝对值取反后加1。
- 内存存储:
a
的内存占用1字节(8位),直接写入0xFF
。
情况2:无符号char(unsigned char)
- 溢出处理:
若编译器默认char
为无符号,-1
会被隐式转换为 模运算结果:-1 mod 256 = 255 → 二进制 11111111 (0xFF)
- 内存存储:
同样存入0xFF
,但程序逻辑上将其视为255
。
3. 验证代码
#include <stdio.h>int main() {char a = -1;unsigned char *p = (unsigned char*)&a;printf("内存值(十六进制): 0x%02X\n", *p); // 输出 0xFFreturn 0;
}
无论 char
的默认符号性如何,输出均为 0xFF
,证明内存中存储的是 11111111
。
4. 总结
关键点 | 说明 |
---|---|
内存存储值 | 固定为 11111111 (二进制)或 0xFF (十六进制) |
符号性影响 | 有符号char解释为 -1 ,无符号char解释为 255 |
编译器差异 | 依赖默认符号性,建议显式声明 signed char 或 unsigned char 避免歧义 |
补码机制 | 负数的存储通过补码实现,保证运算一致性 |
5. 扩展:补码的优势
- 统一加减法:补码允许使用同一套电路处理加减法,无需区分符号位。
- 零的唯一性:补码中
0
仅有00000000
一种表示,无+0
和-0
歧义。 - 范围对称性:8位有符号整数范围为
-128
到127
,充分利用所有位组合。
通过上述分析,char a = -1;
的内存存储始终为 0xFF
,其符号性由编译器决定值的解释方式。
C++实现sharedPtr,手写C++的string类
对cache的理解,如何提高缓存命中率?
数据结构中顺序存储和链式存储的优缺点
大疆车载面经
网上的参考:
自我介绍+介绍简历项目
围绕项目问涉及的知识点
问了很多DSP的内容(项目相关)
回顾你做的项目,你觉得有哪些地方可以改进?有哪些挑战?难忘的经历
说说你对大疆车载的了解,反问
优化手段有哪些
模型推理加速是怎么整的
量化部署过程
gpu加速有什么好处,原理是什么
cuda相关
多线程如何使用,痛点是什么
如何做同步,同步的手段有哪些?区别是什么?分别用在什么场景
内存优化有哪些手段?dma相关的
cache cpu 内存之间的联系
高性能面试问题
C++
-
static的作用,修饰成员变量,成员函数。static全局变量和普通变量的区别。
-
怎么只在堆上创建构造函数
-
右值引用
-
锁
-
lambda表达式
-
move forward
-
编译的过程 动态库和静态库的区别
-
new 和 malloc的区别, new的底层实现
-
拷贝构造函数是传值还是引用,为什么要传引用。
-
线程安全的单例模式
-
智能指针,shared_ptr是不是线程安全的
-
vector的扩容机制?为什么要2倍扩容
-
C++内存模型
-
为什么构造函数不能是虚函数,为什么析构函数是虚函数
-
线程间共享内存,什么时候用到条件变量,什么时候用到锁 ,有什么区别
-
死锁条件,如何避免,lock_gard unquie_lock 区别
-
C++怎么调用c语言封装的函数
-
多态实现,原理,虚函数表存的位置,注意别忘了模板多态,模板的偏特化
-
进程和线程的区别
-
大端小端 怎么判断,两种方法
高性能相关
-
opencl的运行流程
-
GPU架构
-
GPU全局内存和局部内存区别, 怎么更好利用局部内存
-
Cache原理
-
如何提高cache命中率
-
通常优化的思路
-
写opencl为什么要减少分支,掩码
-
opencl 写kernel的主要参数有哪些
-
计算密集型和访存密集型的区别
-
可分离卷积在GPU上为什么慢,为什么是访存密集型
-
算子融合 conv+BN 融合的公式,为什么可以融合
-
推理框架中卷积的实现有哪些
-
时间局部性和空间局部性
-
产生bank confict的原因和解决方法
-
TVM
-
opencl 实现矩阵乘法, 向量求和