# C++核心技术面试复习指南:内存管理、面向对象与STL

0 阅读14分钟

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的相关技术,这也是面试中的常见话题。

希望这篇复习指南能帮助你在面试中取得好成绩!