海康威视C++软件开发 一面总结
1. 虚函数的实现原理是什么?虚函数表存储在哪里?
答案:
- 虚函数通过虚函数表(vtable)和虚函数指针(vptr)实现
- 每个包含虚函数的类都有一个虚函数表,存储虚函数的地址
- 每个对象内部有一个vptr指针,指向该类的虚函数表
- vptr通常位于对象内存布局的开头
- 虚函数表存储在只读数据段(.rodata)
- 调用虚函数时:通过对象的vptr找到vtable,再通过偏移量找到具体函数地址
示例:
class Base {
virtual void func() { }
};
// 对象内存布局:[vptr][成员变量...]
2. 智能指针的实现原理?shared_ptr是线程安全的吗?(保留八股文)
答案:
- unique_ptr:独占所有权,内部只保存裸指针,析构时delete
- shared_ptr:引用计数,内部有控制块(control block)存储引用计数和删除器
- weak_ptr:不增加引用计数,通过lock()转换为shared_ptr使用
shared_ptr的线程安全性:
- 引用计数的增减是原子操作,线程安全
- 但指向的对象本身不是线程安全的
- 多个线程同时修改同一个shared_ptr对象也不安全
- 结论:引用计数安全,对象访问和指针修改需要额外同步
3. 说说你对RAII的理解,在项目中如何应用?
答案:RAII(Resource Acquisition Is Initialization)资源获取即初始化:
- 核心思想:资源的生命周期与对象生命周期绑定
- 构造函数获取资源,析构函数释放资源
- 利用C++的栈展开机制自动管理资源
典型应用:
- 智能指针管理内存
- lock_guard/unique_lock管理互斥锁
- fstream管理文件句柄
- 自定义资源管理类(如数据库连接、网络socket)
示例:
class FileHandler {
FILE* fp;
public:
FileHandler(const char* name) { fp = fopen(name, "r"); }
~FileHandler() { if(fp) fclose(fp); }
};
4. 什么是内存对齐?为什么需要内存对齐?如何控制对齐方式?
答案:内存对齐:数据在内存中的起始地址必须是某个值的整数倍
原因:
- CPU访问对齐的数据效率更高,一次读取即可
- 某些CPU架构访问未对齐数据会崩溃
- 提高缓存命中率
对齐规则:
- 结构体成员按其类型大小对齐
- 结构体总大小是最大成员对齐值的整数倍
- 可用
#pragma pack(n)或alignas控制
示例:
struct A {
char c; // 1字节
int i; // 4字节对齐,前面填充3字节
short s; // 2字节
}; // 总大小12字节(需要填充到4的倍数)
// 使用alignas
struct alignas(16) B {
int x;
}; // 强制16字节对齐
5. 左值、右值、左值引用、右值引用分别是什么?移动语义的作用?
答案:
- 左值(lvalue):有名字、可取地址的表达式,如变量
- 右值(rvalue):临时对象、字面量,不可取地址
- 左值引用:
T&,只能绑定左值 - 右值引用:
T&&,可以绑定右值,用于移动语义
移动语义:
- 避免深拷贝,转移资源所有权
- 通过移动构造函数和移动赋值运算符实现
- 使用
std::move将左值转换为右值引用
示例:
class String {
char* data;
public:
// 移动构造
String(String&& s) noexcept : data(s.data) {
s.data = nullptr; // 转移所有权
}
};
String s1("hello");
String s2 = std::move(s1); // 移动而非拷贝
6. 进程和线程的区别?多线程通信方式有哪些?
答案:区别:
- 进程是资源分配的基本单位,线程是调度的基本单位
- 进程有独立地址空间,线程共享进程地址空间
- 进程切换开销大,线程切换开销小
- 进程间通信复杂,线程间通信简单
多线程通信方式:
- 共享内存:全局变量、堆内存(需要同步)
- 互斥锁(mutex):保护临界区
- 条件变量(condition_variable):线程间同步
- 信号量(semaphore):控制访问数量
- 原子操作(atomic):无锁编程
- 消息队列:生产者-消费者模型
7. 说说C++的四种类型转换,各自的使用场景?(保留八股文)
答案:
- static_cast:编译期类型转换,用于基本类型转换、向上转型(子类到父类)
- dynamic_cast:运行时类型检查,用于向下转型(父类到子类),失败返回nullptr
- const_cast:去除const属性,用于修改const对象(需谨慎)
- reinterpret_cast:重新解释内存,用于指针类型转换(如int到char)
示例:
// static_cast double d = 3.14; int i = static_cast<int>(d); // dynamic_cast Base* b = new Derived(); Derived* d = dynamic_cast<Derived*>(b); // const_cast const int* cp = &i; int* p = const_cast<int*>(cp); // reinterpret_cast int* ip = &i; char* cp = reinterpret_cast<char*>(ip);
8. 什么是死锁?如何避免死锁?
答案:死锁:多个线程互相等待对方持有的资源,导致永久阻塞
死锁的四个必要条件:
- 互斥:资源不能共享
- 持有并等待:持有资源的同时等待其他资源
- 不可剥夺:资源不能被强制释放
- 循环等待:存在资源等待环路
避免死锁的方法:
- 按固定顺序获取锁
- 使用
std::lock同时获取多个锁 - 使用超时机制(try_lock_for)
- 使用层次锁(hierarchical mutex)
- 避免嵌套锁
- 使用RAII管理锁(lock_guard)
示例:
// 错误:可能死锁
void thread1() {
lock(mutex1);
lock(mutex2);
}
void thread2() {
lock(mutex2);
lock(mutex1);
}
// 正确:固定顺序
void thread1() {
std::lock(mutex1, mutex2); // 同时获取
lock_g
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
C++八股文全集 文章被收录于专栏
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。