1. 内存管理
1.1 内存分区
C++程序运行时的内存空间分为五个区域:
- 代码区:存放函数体的二进制代码,由操作系统管理
- 全局/静态区:存放全局变量和静态变量,程序结束后由操作系统释放
- 常量区:存放常量字符串,程序结束后由操作系统释放
- 堆区:由程序员分配和释放,若不释放,程序结束后由操作系统回收
- 栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等
1.2 智能指针
智能指针是C++11引入的内存管理工具,用于自动管理动态内存,避免内存泄漏。
unique_ptr
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // 所有权转移
// p1现在为空,不能再使用
- 特点:独占所有权,不能复制,只能移动
- 优点:开销最小,无需引用计数
- 适用场景:明确 ownership 的场景
shared_ptr
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = p1; // 引用计数加1
// 当所有shared_ptr离开作用域,内存自动释放
- 特点:共享所有权,通过引用计数管理
- 优点:适合共享资源的场景
- 缺点:有引用计数开销,可能导致循环引用
weak_ptr
std::shared_ptr<int> p1(new int(10));
std::weak_ptr<int> wp = p1; // 不增加引用计数
if (auto sp = wp.lock()) { // 检查对象是否存在
// 使用sp
}
- 特点:不增加引用计数,用于解决循环引用问题
- 优点:避免循环引用,观察共享对象
1.3 RAII原则
RAII(Resource Acquisition Is Initialization)是C++的核心资源管理原则:
- 思想:资源在构造函数中获取,在析构函数中释放
- 优点:确保资源正确释放,即使发生异常
- 应用:智能指针、锁管理等
2. this指针
2.1 基本概念
- 定义:this是一个指向当前对象的指针,隐含在每个非静态成员函数中
- 类型:const T* const(在const成员函数中)或 T* const(在非const成员函数中)
- 用途:区分成员变量和局部变量,返回当前对象的引用
2.2 应用场景
class Person {
private:
std::string name;
public:
Person& setName(const std::string& name) {
this->name = name; // 区分成员变量和参数
return *this; // 返回当前对象的引用,支持链式调用
}
};
2.3 常见问题
- this指针是否为空:在成员函数内部,this指针永远不为空,因为只有通过对象调用成员函数
- this指针的存储:通常存储在寄存器中,而非内存
- const成员函数中的this:指向const对象,不能修改成员变量
3. 面向对象编程
3.1 三大特性
封装
- 概念:将数据和操作数据的方法绑定在一起,对外部隐藏实现细节
- 实现:使用访问修饰符(public、protected、private)
- 优点:提高代码安全性,简化接口
继承
- 概念:派生类继承基类的属性和方法
- 实现:使用冒号语法
class Derived : public Base - 优点:代码复用,层次化设计
- 注意:避免多继承带来的歧义问题
多态
- 概念:同一接口,不同实现
- 实现:虚函数和动态绑定
- 优点:提高代码灵活性和可扩展性
3.2 虚函数与纯虚函数
class Shape {
public:
virtual void draw() { // 虚函数
std::cout << "Drawing a shape" << std::endl;
}
virtual void area() = 0; // 纯虚函数,Shape成为抽象类
virtual ~Shape() {} // 虚析构函数,确保正确释放派生类资源
};
class Circle : public Shape {
public:
void draw() override { // 重写虚函数
std::cout << "Drawing a circle" << std::endl;
}
void area() override {
// 实现面积计算
}
};
3.3 构造函数与析构函数
- 构造函数:初始化对象,不能是虚函数
- 析构函数:清理资源,基类析构函数应设为虚函数
- 拷贝构造函数:创建新对象时复制现有对象
- 移动构造函数:将资源从一个对象转移到另一个对象
4. std::move与移动语义
4.1 基本概念
- 移动语义:将资源从一个对象转移到另一个对象,避免不必要的复制
- std::move:将左值转换为右值引用,提示编译器可以移动资源
4.2 实现原理
// std::move的简化实现
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
- 注意:std::move本身不移动任何东西,只是转换类型
- 作用:告诉编译器,这个对象的资源可以被移动
4.3 应用场景
std::string s1 = "Hello";
std::string s2 = std::move(s1); // 移动s1的资源到s2
// s1现在为空,但仍然有效(可以被赋值或销毁)
4.4 移动构造函数与移动赋值运算符
class MyString {
private:
char* data;
public:
// 移动构造函数
MyString(MyString&& other) noexcept : data(other.data) {
other.data = nullptr; // 避免析构函数重复释放
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
5. STL容器实现原理
5.1 序列容器
vector
- 实现:动态数组,连续内存
- 特点:随机访问O(1),尾部插入/删除O(1),中间插入/删除O(n)
- 内存管理:当容量不足时,重新分配更大的内存并复制元素
list
- 实现:双向链表
- 特点:任意位置插入/删除O(1),随机访问O(n)
- 内存管理:每个节点独立分配内存
deque
- 实现:分段连续内存,使用中央控制结构管理
- 特点:两端插入/删除O(1),随机访问O(1)
- 内存管理:按需分配段,段大小固定
5.2 关联容器
set/multiset
- 实现:红黑树
- 特点:自动排序,查找O(log n),插入/删除O(log n)
- 存储:set存储唯一键,multiset允许重复键
map/multimap
- 实现:红黑树
- 特点:键值对存储,自动按键排序,查找O(log n)
- 存储:map存储唯一键,multimap允许重复键
5.3 无序容器
unordered_set/unordered_map
- 实现:哈希表
- 特点:平均查找/插入/删除O(1),最坏情况O(n)
- 存储:基于哈希函数和桶
5.4 容器适配器
stack
- 实现:基于deque(默认)
- 特点:后进先出(LIFO)
- 操作:push、pop、top
queue
- 实现:基于deque(默认)
- 特点:先进先出(FIFO)
- 操作:push、pop、front、back
priority_queue
- 实现:基于vector和堆算法
- 特点:最大堆,顶部元素始终是最大的
- 操作:push、pop、top
6. 函数编译到链接的过程
6.1 预处理(Preprocessing)
- 输入:源代码文件(.cpp)
- 输出:预处理后的代码文件(.i)
- 操作:
- 处理预处理器指令(#include、#define、#ifdef等)
- 展开头文件
- 替换宏定义
- 删除注释
6.2 编译(Compilation)
- 输入:预处理后的代码文件(.i)
- 输出:汇编代码文件(.s)
- 操作:
- 词法分析:将源代码分解为词法单元
- 语法分析:构建抽象语法树
- 语义分析:检查类型匹配、作用域等
- 优化:进行代码优化
- 生成汇编代码
6.3 汇编(Assembly)
- 输入:汇编代码文件(.s)
- 输出:目标文件(.o/.obj)
- 操作:
- 将汇编代码转换为机器码
- 生成符号表
- 记录未解析的符号
6.4 链接(Linking)
- 输入:目标文件(.o/.obj)和库文件
- 输出:可执行文件或库文件
- 操作:
- 解析符号引用
- 合并段
- 重定位符号地址
- 生成可执行文件
6.5 示例
source.cpp → [预处理] → source.i → [编译] → source.s → [汇编] → source.o → [链接] → executable
7. 常见面试问题
7.1 内存管理
-
问题:智能指针的实现原理?
-
答案:unique_ptr通过独占所有权实现,shared_ptr通过引用计数实现,weak_ptr用于解决循环引用。
-
问题:内存泄漏的原因和解决方案?
-
答案:原因包括未释放动态内存、循环引用等;解决方案包括使用智能指针、RAII原则、内存泄漏检测工具。
7.2 面向对象
-
问题:虚函数的实现原理?
-
答案:通过虚函数表和虚指针实现。每个含有虚函数的类有一个虚函数表,每个对象有一个虚指针指向该表。
-
问题:如何实现抽象类?
-
答案:包含至少一个纯虚函数的类就是抽象类,不能实例化。
7.3 std::move
-
问题:std::move和std::forward的区别?
-
答案:std::move将左值转换为右值引用,std::forward保持值类别(左值/右值)。
-
问题:移动语义的优点?
-
答案:避免不必要的复制,提高性能,特别是对于大对象。
7.4 容器
-
问题:vector和list的区别?
-
答案:vector是连续内存,随机访问快,插入删除慢;list是链表,插入删除快,随机访问慢。
-
问题:map和unordered_map的区别?
-
答案:map基于红黑树,有序,查找O(log n);unordered_map基于哈希表,无序,平均查找O(1)。
7.5 编译链接
-
问题:编译和链接的区别?
-
答案:编译将源代码转换为目标文件,链接将目标文件和库文件组合成可执行文件。
-
问题:什么是符号解析?
-
答案:链接器将目标文件中未解析的符号(如函数调用)与定义该符号的目标文件或库文件进行匹配的过程。
8. 代码优化建议
8.1 内存优化
- 使用智能指针管理动态内存
- 避免频繁的内存分配和释放
- 使用内存池减少内存碎片
8.2 性能优化
- 合理选择容器,根据操作特点选择合适的容器
- 使用移动语义减少复制开销
- 避免不必要的对象创建和销毁
8.3 代码质量
- 遵循RAII原则
- 使用const和引用传递避免不必要的复制
- 编写清晰的类层次结构,合理使用继承和多态
9. C++新特性
9.1 C++11
- auto关键字:自动类型推导
- lambda表达式:匿名函数
- 右值引用:移动语义的基础
- 智能指针:shared_ptr、unique_ptr、weak_ptr
- ** nullptr**:空指针常量
- 范围for循环:简化循环代码
- 初始化列表:统一的初始化方式
- 类型别名:using关键字
- 委托构造函数:构造函数调用其他构造函数
- override和final:明确虚函数重写
9.2 C++14
- 泛型lambda:auto作为lambda参数
- 返回类型推导:函数返回类型自动推导
- 二进制字面量:0b前缀表示二进制数
- 数字分隔符:使用'分隔数字,提高可读性
- 变量模板:模板变量
- [[deprecated]]属性:标记已弃用的实体
9.3 C++17
- 结构化绑定:同时声明多个变量并赋值
- if constexpr:编译时条件判断
- inline变量:内联变量
- 折叠表达式:简化可变参数模板的使用
- std::optional:表示可能不存在的值
- std::variant:类型安全的联合
- std::any:可以存储任意类型的值
- 文件系统库:std::filesystem
9.4 C++20
- 概念(Concepts):约束模板参数
- 协程(Coroutines):异步编程支持
- 范围库(Ranges):更灵活的范围操作
- 模块(Modules):替代头文件的新机制
- 三路比较运算符:<=>运算符
- consteval:编译时计算
- constinit:在全局作用域初始化常量
10. 模板元编程
10.1 基本概念
- 模板:泛型编程的基础
- 模板特化:为特定类型提供特殊实现
- 偏特化:为部分类型参数提供特殊实现
- SFINAE:替换失败不是错误
- traits:类型特性提取
10.2 常用技巧
- 类型萃取:使用std::is_same、std::is_integral等
- 编译时计算:使用constexpr和模板递归
- 标签分发:基于类型特性选择不同实现
- 类型列表:使用模板递归处理类型列表
10.3 示例
// 编译时计算阶乘
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N-1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
// 使用
constexpr int fact5 = Factorial<5>::value; // 120
11. 异常处理
11.1 基本概念
- 异常:程序运行时的错误情况
- try块:可能抛出异常的代码
- catch块:捕获并处理异常
- throw表达式:抛出异常
11.2 最佳实践
- 异常安全:确保异常发生时资源不泄漏
- 异常规范:使用noexcept标记不抛出异常的函数
- 异常层次:合理设计异常类层次
- 避免在析构函数中抛出异常:可能导致程序终止
11.3 示例
try {
// 可能抛出异常的代码
if (something_wrong) {
throw std::runtime_error("Something went wrong");
}
} catch (const std::exception& e) {
// 处理异常
std::cerr << "Exception caught: " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他异常
std::cerr << "Unknown exception caught" << std::endl;
}
12. 线程与并发
12.1 基本概念
- 线程:程序执行的基本单位
- 互斥量:保护共享资源
- 条件变量:线程间同步
- 原子操作:无锁编程
- ** futures和promises**:异步任务
12.2 常用工具
- std::thread:线程类
- std::mutex:互斥量
- std::lock_guard:RAII风格的锁管理
- std::unique_lock:更灵活的锁管理
- std::condition_variable:条件变量
- std::atomic:原子类型
- std::future:获取异步任务结果
- std::async:异步执行函数
12.3 示例
// 简单的线程创建
std::thread t([]() {
std::cout << "Hello from thread!" << std::endl;
});
t.join(); // 等待线程完成
// 互斥量保护共享资源
std::mutex mtx;
int shared_data = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++shared_data;
}
13. ROS2相关C++知识
13.1 ROS2节点
- 节点创建:使用rclcpp::Node
- 生命周期管理:使用rclcpp_lifecycle::LifecycleNode
- 回调组:管理回调函数的执行
13.2 通信机制
- 话题:rclcpp::Publisher和rclcpp::Subscription
- 服务:rclcpp::Service和rclcpp::Client
- 动作:rclcpp_action::Server和rclcpp_action::Client
- 参数:rclcpp::Parameter
13.3 QoS策略
- 可靠性:Reliable或Best Effort
- 持续性:Transient Local或Volatile
- 历史记录:Keep Last或Keep All
- ** deadline**:消息发送的最大时间间隔
- ** lifespan**:消息的最大生命周期
13.4 示例
// 创建节点
auto node = rclcpp::Node::make_shared("my_node");
// 创建发布者
auto publisher = node->create_publisher<std_msgs::msg::String>("topic", 10);
// 创建订阅者
auto subscription = node->create_subscription<std_msgs::msg::String>(
"topic", 10, [](const std_msgs::msg::String::SharedPtr msg) {
std::cout << "Received: " << msg->data << std::endl;
});
// 运行节点
rclcpp::spin(node);
14. 更多面试问题
14.1 C++基础
-
问题:const关键字的作用?
-
答案:修饰变量表示不可修改,修饰指针有三种情况,修饰成员函数表示不修改成员变量。
-
问题:引用和指针的区别?
-
答案:引用必须初始化且不能改变指向,指针可以为空且可以改变指向;引用是别名,指针是地址。
-
问题:内联函数的优缺点?
-
答案:优点是减少函数调用开销,缺点是可能增加代码大小。
14.2 内存管理
-
问题:new和malloc的区别?
-
答案:new调用构造函数,malloc不调用;new返回对应类型指针,malloc返回void*;new失败抛出异常,malloc失败返回NULL。
-
问题:什么是内存对齐?
-
答案:内存对齐是指变量在内存中的地址是其大小的整数倍,提高访问效率。
14.3 面向对象
-
问题:什么是虚函数表?
-
答案:虚函数表是存储虚函数地址的表,每个含有虚函数的类有一个虚函数表,对象通过虚指针指向它。
-
问题:菱形继承问题及解决方案?
-
答案:菱形继承会导致基类成员被重复继承,解决方案是使用虚继承。
14.4 模板
-
问题:模板特化和偏特化的区别?
-
答案:特化是为特定类型提供完全不同的实现,偏特化是为部分类型参数提供特殊实现。
-
问题:什么是SFINAE?
-
答案:Substitution Failure Is Not An Error,当模板替换失败时,编译器会尝试其他重载,而不是报错。
14.5 并发
-
问题:死锁的原因和解决方案?
-
答案:死锁原因是循环等待资源;解决方案包括避免循环等待、使用超时机制、按顺序加锁等。
-
问题:原子操作和互斥量的区别?
-
答案:原子操作是无锁的,由硬件支持;互斥量是有锁的,由操作系统支持。
14.6 ROS2相关
-
问题:ROS2和ROS1的主要区别?
-
答案:ROS2基于DDS,去中心化;支持实时性;跨平台;安全性更好。
-
问题:如何优化ROS2节点的性能?
-
答案:合理配置QoS策略;使用共享内存;优化消息大小;使用多线程执行器。
15. 总结
C++的核心技术是面试中的重点,包括内存管理、面向对象编程、移动语义、STL容器、编译链接过程、C++新特性、模板元编程、异常处理、线程并发以及ROS2相关知识等。理解这些概念的原理和应用,不仅可以帮助你通过面试,还能提高代码质量和性能。
在复习过程中,建议结合实际代码练习,加深对这些概念的理解。同时,关注C++标准的最新发展,以及ROS2的相关技术,这也是面试中的常见话题。
希望这篇复习指南能帮助你在面试中取得好成绩!