Skip to content

Cpp面试

benhao edited this page Jun 2, 2025 · 5 revisions

C++编译与内存相关

C++内存管理

C++ 的内存管理是其强大功能和灵活性的核心,但也带来了复杂性和潜在风险(如内存泄漏、野指针)。它需要开发者手动管理动态分配的内存,这与拥有垃圾回收机制的语言(如 Java、Python)有显著区别。

以下是 C++ 内存管理的关键方面:

  1. 内存区域(Memory Layout)

    • 栈(Stack):
      • 功能: 存储函数的局部变量(非 static)、函数参数、返回地址等。
      • 管理: 由编译器自动管理。变量在进入作用域时分配,离开作用域时自动销毁(释放)。分配和释放速度快(指针移动)。
      • 特点: LIFO(后进先出)结构,大小通常有限(操作系统/编译器设置),超出会导致栈溢出。
    • 堆(Heap / Free Store):
      • 功能: 存储程序运行时动态分配的内存。
      • 管理: 必须由程序员手动管理。使用 new / new[]malloc() / calloc() 分配内存。使用 delete / delete[]free() 释放内存。
      • 特点: 空间通常比栈大得多(受系统可用内存限制),分配和释放速度相对栈较慢(涉及更复杂的管理),内存碎片化可能发生。
    • 全局/静态存储区(Global/Static Storage):
      • 功能: 存储全局变量、静态变量(包括类的静态成员变量、函数内的 static 局部变量)、常量字符串字面量。
      • 管理: 在程序启动时分配,在程序结束时销毁。由编译器/运行时环境管理。
      • 特点: 生命周期贯穿整个程序运行期。数据区通常进一步细分为已初始化数据段(.data)和未初始化数据段(.bss)。
    • 常量存储区(Constant Storage):
      • 功能: 存储常量(如 const 全局变量、字符串字面量)。
      • 管理: 程序生命周期内存在,通常不可修改(尝试修改会导致未定义行为)。
    • 代码区(Code / Text Segment):
      • 功能: 存储程序的执行代码(机器指令)。
      • 管理: 程序加载时由操作系统分配,只读。
  2. 动态内存分配(Dynamic Memory Allocation)

    • 核心操作符:newdelete
      • new: 在堆上分配内存。调用对象的构造函数(对于类类型)。
        • T* ptr = new T; // 分配单个对象
        • T* arr = new T[N]; // 分配数组
      • delete: 释放由 new 分配的单个对象的内存。调用对象的析构函数。
        • delete ptr;
      • delete[]: 释放由 new[] 分配的数组的内存。调用数组中每个元素的析构函数。
        • delete[] arr;
    • C 风格函数:malloc, calloc, realloc, free
      • 来自 C 标准库 (<cstdlib>)。在堆上分配原始字节内存。
      • malloc(size_t size): 分配指定字节数的未初始化内存。
      • calloc(size_t num, size_t size): 分配 num 个大小为 size 的元素的数组,并将内存初始化为零。
      • realloc(void* ptr, size_t new_size): 调整之前分配的内存块大小(可能移动)。
      • free(void* ptr): 释放由 malloc, calloc, realloc 分配的内存。
      • 关键区别: new/delete 是 C++ 操作符,理解类型(调用构造/析构函数);malloc/free 是函数,只处理原始字节(不调用构造/析构函数)。混合使用(如 new 分配 free 释放,或 malloc 分配 delete 释放)是严重的未定义行为! 优先使用 new/delete
  3. 常见内存问题

    • 内存泄漏(Memory Leak):
      • 原因: 使用 newmalloc 分配了内存,但忘记使用 deletefree 释放。或者指针指向的内存地址丢失(如指针被覆盖或超出作用域),导致无法释放。
      • 后果: 程序占用的内存持续增长,最终可能导致系统内存耗尽、程序崩溃或性能下降。
    • 野指针(Dangling Pointer):
      • 原因: 指针指向的内存已被释放(delete/free),但指针本身未被置空(nullptr)。后续使用该指针(解引用)访问无效内存。
      • 后果: 未定义行为(Undefined Behavior, UB),常见表现为程序崩溃、数据损坏、难以预测的结果。
    • 重复释放(Double Free):
      • 原因: 对同一块内存多次调用 deletefree
      • 后果: 严重未定义行为,通常会破坏内存管理器的数据结构,导致程序崩溃。
    • 缓冲区溢出(Buffer Overflow):
      • 原因: 向分配的内存区域(尤其是数组)写入超过其容量的数据,覆盖了相邻内存。
      • 后果: 数据损坏、程序崩溃、安全漏洞(常被利用)。
    • 访问无效内存: 解引用 nullptr 或未初始化的指针。
  4. 现代 C++ 的最佳实践与工具

    • 智能指针(Smart Pointers) (C++11 起): 最重要的工具!
      • std::unique_ptr<T>: 拥有对动态分配对象的独占所有权。对象在其析构时自动释放。不可复制(所有权唯一),但可移动。首选用于管理单个对象的独占所有权。
      • std::shared_ptr<T>: 通过引用计数实现共享所有权。当最后一个 shared_ptr 被销毁或重置时,对象被释放。用于需要多个所有者共享同一对象的情况。 注意循环引用问题(可能导致泄漏)。
      • std::weak_ptr<T>: 配合 shared_ptr 使用,不增加引用计数。用于观察共享对象而不延长其生命周期,打破循环引用。
      • 好处: 自动管理内存释放(RAII 的核心体现),极大地减少了手动 new/delete 的需要,有效防止内存泄漏和重复释放。
    • RAII(Resource Acquisition Is Initialization)资源获取即初始化:
      • 核心思想: 将资源(内存、文件句柄、锁等)的获取绑定到对象的构造上,将资源的释放绑定到对象的析构上。
      • 实现: 使用类的构造函数获取资源,在析构函数中释放资源。当对象离开其作用域时(无论是正常结束还是异常抛出),析构函数都会被调用,从而确保资源被正确释放。
      • 体现: 智能指针是 RAII 管理内存的完美范例。标准库中的 std::vector, std::string, std::fstream 等都遵循 RAII 原则管理各自资源。
    • 容器(Containers):
      • 优先使用标准库容器: std::vector, std::string, std::map, std::unordered_map, std::list 等。
      • 好处: 它们内部管理动态内存,自动处理分配、释放、扩容等。遵循 RAII,大大简化了动态数组、字符串等的管理,避免手动 new[]/delete[]
    • 避免裸指针(Raw Pointers)用于所有权:
      • 裸指针应主要用于观察(observing)资源或传递非拥有性引用,不应用于表示所有权。所有权应由智能指针、容器或栈对象明确表示。
    • 使用 std::make_uniquestd::make_shared (C++11/14):
      • 创建智能指针对象的首选方式(比直接 new 更高效、更安全)。
      • auto ptr = std::make_unique<T>(args...);
      • auto sptr = std::make_shared<T>(args...);
    • 工具辅助:
      • 调试器(Debugger): 检查内存、指针值、调用栈。
      • Valgrind (Linux/macOS): 强大的内存调试、泄漏检测、性能分析工具。
      • AddressSanitizer (ASan) / MemorySanitizer (MSan) / LeakSanitizer (LSan): 编译器(如 Clang, GCC)集成的运行时检测工具,用于检测内存错误(溢出、释放后使用、泄漏等),速度较快。
      • 静态分析工具: Clang-Tidy, Cppcheck, PVS-Studio 等,可以在编译时或代码检查阶段发现潜在的内存问题。

总结:

C++ 内存管理的核心在于理解栈、堆等内存区域的差异,以及手动管理堆内存(通过 new/deletemalloc/free)带来的责任和风险。现代 C++ 通过 智能指针(unique_ptr, shared_ptr, weak_ptr)、遵循 RAII 原则的类、以及标准库容器,极大地简化了内存管理,显著提高了代码的安全性和健壮性。最佳实践是:尽可能使用栈内存和 RAII 对象(容器、智能指针),将显式的 new/delete 限制在绝对必要的最小范围内,并利用工具检测潜在问题。 牢记“谁分配,谁释放”的原则,并用语言特性(智能指针)或设计模式(RAII)来强制执行这一原则。

堆与栈

C++ 内存管理:堆与栈详解

在 C++ 中,堆(Heap)栈(Stack) 是两种核心的内存分配区域,它们在生命周期、管理方式、性能和使用场景上有显著区别。


1. 栈(Stack)

  • 特点

    • 自动管理:由编译器自动分配和释放(函数结束时自动弹出)。
    • 速度快:仅需移动栈指针(硬件优化)。
    • 大小固定:通常较小(默认 1-8 MB,依赖系统和编译器)。
    • 局部性:存储函数参数、局部变量、返回地址等。
    • 连续内存:内存地址连续分配(高效缓存利用)。
  • 示例

    void foo() {
        int a = 10;          // a 在栈上
        char buffer[256];    // buffer 在栈上
    } // 函数结束,a 和 buffer 自动释放
  • 限制

    • 大对象(如巨型数组)可能导致栈溢出
    • 生命周期仅限于作用域内(无法跨函数传递所有权)。

2. 堆(Heap)

  • 特点

    • 手动管理:需显式分配(new/malloc)和释放(delete/free)。
    • 速度慢:需搜索可用内存块(可能触发系统调用)。
    • 空间大:仅受系统可用内存限制(GB 级别)。
    • 全局性:可在任何地方访问(通过指针)。
    • 非连续内存:动态分配(可能产生内存碎片)。
  • 示例

    void bar() {
        int* p = new int(42);   // 堆上分配 int
        std::vector<int>* vec = new std::vector<int>(1000); // 堆上分配对象
        // ... 使用 p 和 vec ...
        delete p;               // 手动释放
        delete vec;
    }
  • 风险

    • 内存泄漏:忘记 delete
    • 悬空指针:访问已释放内存。
    • 碎片化:频繁分配/释放导致内存利用率降低。

3. 关键对比

特性 栈(Stack) 堆(Heap)
管理方式 编译器自动管理 程序员手动管理(new/delete
速度 极快(移动栈指针) 较慢(搜索可用内存)
大小限制 小(默认 MB 级) 大(仅限系统内存)
生命周期 作用域内(函数结束即释放) 直到显式释放
内存布局 连续地址 碎片化(非连续)
使用场景 局部变量、小型数据 大对象、动态数据结构、跨函数共享
错误风险 栈溢出 内存泄漏、悬空指针、碎片化

4. 代码示例分析

#include <iostream>
#include <vector>

void stackExample() {
    int x = 5;                  // 栈分配
    std::vector<int> arr(100);   // arr 对象在栈上,内部数据在堆上
} // x 和 arr 自动释放(arr 的析构函数释放内部堆内存)

void heapExample() {
    int* y = new int(10);        // 堆分配
    std::vector<int>* vec = new std::vector<int>(1000); // 对象本身在堆上
    // ... 使用 y 和 vec ...
    delete y;                    // 必须手动释放!
    delete vec;                  // 否则内存泄漏
}

int main() {
    stackExample();  // 安全:无内存泄漏
    heapExample();   // 风险:若忘记 delete 则泄漏
    return 0;
}

5. 最佳实践

  1. 优先使用栈

    • 小型对象和局部变量。
    • 避免栈溢出(避免巨型栈分配)。
  2. 智能指针管理堆

    #include <memory>
    void safeHeap() {
        auto ptr = std::make_unique<int>(30); // 自动释放
        auto arr = std::make_shared<std::vector<int>>(100); // 引用计数
    } // 无需 delete
  3. 避免裸指针:用 std::vector 替代动态数组,用 std::string 替代 char*

  4. 栈溢出防范

    • 递归时控制深度。
    • 大对象改用堆(如 std::vector 存储大量数据)。

6. 常见问题

  • Q:newmalloc 的区别?
    new 调用构造函数,malloc 仅分配内存;delete 调用析构函数,free 仅释放内存。

  • Q:如何检测内存泄漏?
    使用工具:Valgrind(Linux)、AddressSanitizer(GCC/Clang)、Visual Studio 诊断工具。

  • Q:对象成员存储在堆还是栈?
    对象本身的位置决定其成员位置:

    class MyClass {
        int a;          // 若 MyClass 在栈上,则 a 在栈上
        int* b;         // b(指针)在栈上,但 *b 指向堆内存
    };

总结

  • :自动、快速、安全,适合小型临时数据。
  • :灵活、大容量,需谨慎管理,适合动态生命周期数据。
  • 现代 C++ 准则:用智能指针和容器(如 std::vector)减少裸堆操作!

变量定义与生存周期

  • 技巧:区分栈(自动销毁)、堆(手动管理)、静态区(程序生命周期)变量的生存周期。

  • 常考点:static局部变量(只初始化一次,函数调用间保持值)。

内存对齐

  • 技巧:记住对齐规则(如#pragma pack),解释为什么需要对齐(性能优化)。

  • 示例:struct大小计算时注意成员排列顺序。

智能指针简介与使用

  • 核心:掌握unique_ptr(独占所有权)、shared_ptr(引用计数)、weak_ptr(解决循环引用)。

  • 必答点:make_shared vs 直接new(更高效,单次内存分配)。

智能指针的创建

  • 技巧:使用make_unique和make_shared创建智能指针,避免裸指针。
  • 常考点:智能指针的构造和析构(自动管理内存)。
  • 常考点:智能指针的生命周期和作用域(自动释放)。

编译与链接

  • 技巧:分阶段解释(预处理→编译→汇编→链接),.h和.cpp的作用。

  • 常考题:undefined reference错误原因(链接阶段找不到定义)。

大端与小端

  • 速记:大端——高位字节在前(网络序);小端——低位字节在前(x86)。

  • 面试题:如何用代码判断系统字节序(联合体或指针强转)。

内存泄漏

  • 关键点:new/delete成对使用,优先用智能指针。

  • 检测工具:Valgrind、ASan(AddressSanitizer)。

内存泄露检测与预防

STL内存优化

STL 的内存管理采用了二级内存配置器机制:

第一级配置器

  • 直接以 malloc()free()realloc() 等 C 函数执行实际的内存分配、释放和重新分配。
  • 主要用于分配大于 128 字节的空间。
  • 如果分配失败,会调用指定的处理函数尝试释放部分内存,若仍失败则抛出异常。
  • 本质上是对 mallocfree 的简单封装:
    • allocate 内调用 mallocdeallocate 内调用 free
    • oom_malloc 用于处理 malloc 失败的情况。

第二级配置器

  • 直接调用 malloc/free 存在以下问题:
    1. 内存分配/释放效率低。
    2. 大量小内存块分配时容易产生内存碎片。
    3. 小块分配还需额外空间存储块信息,导致额外内存负担。
  • 解决方案
    • 对于小于 128 字节的分配,采用内存池管理。
    • 维护一个自由链表数组(16 个链表),每个链表管理一种大小的内存块(8~128 字节,步长 8 字节)。
    • 分配时直接从对应链表取节点,效率高。

自由链表数组

  • 本质是一个指针数组,每个元素指向一个链表的起始节点。
  • 每个链表的节点(obj)为实际内存块。
  • 相同链表的块大小相同,不同链表的块大小不同。

内存分配流程

  1. allocate 判断分配大小:

    • 大于 128 字节:直接用第一级配置器。
    • 小于等于 128 字节
      • 选择对应链表,取第一个节点。
      • 若链表为空,调用 refill 填充链表(默认取 20 个数据块)。
  2. refill 填充链表:

    • 调用 chunk_alloc 从内存池分配一大块内存(默认 20 个节点大小)。
    • 若内存池不足 20 个节点,则返回实际可分配的节点数。
    • 将大块内存分割为若干小块,串成链表。
  3. 内存池管理chunk_alloc):

    • 判断内存池是否足够:
      • 足够 20 个节点:直接返回。
      • 不足 20 个但足够 1 个节点:返回实际可分配数量。
      • 连 1 个节点都不够
        • 将剩余内存分配给其他合适链表。
        • 调用 malloc 分配所需两倍大小。
        • malloc 失败,尝试从其他链表回收可用内存块,递归调用 chunk_alloc
        • 若仍失败,只能调用第一级配置器。

include ""和<>的区别

  • 技巧:""先查当前目录,再查系统路径;<>直接查系统路径。

C++ 语言对比

C 和 C++ 的对比

  • 核心差异:C++支持OOP、模板、异常、RAII。

  • 必答:C++的struct可包含函数,默认访问权限为public。

Java 和 C++ 的对比

Python 和 C++ 的对比

Go 和 C++ 的对比

Rust 和 C++ 的对比

C++ 11, C++ 14, C++ 17 新特性

高频考点:

  • auto/decltype(类型推导)

  • 移动语义(std::move)、右值引用

  • lambda表达式

  • nullptr(替代NULL)

  • 智能指针(shared_ptr等)

C++ 关键字与关键库函数

sizeof 和 strlen 的区别

sizeof vs strlen:sizeof算容量(含\0),strlen算长度(不含\0)。

lambda 表达式的应用

explicit 的作用

C 和 C++ static 中的作用

const 作用及用法

define 和 const 的区别

const vs #define:const有类型检查、作用域;#define文本替换。

define 和 typedef 的区别

用宏实现比较大小,以及两个数中的最小值

inline 作用及使用方法

inline 函数工作原理

宏定义(define)和内联函数(inline)的区别

new 的作用

new 和 malloc 的区别

new vs malloc:new调构造函数,malloc仅分配内存。

delete 与 free 的区别

C 和 C++ struct 的区别

struct 和 union 的区别

class 和 struct 的异同

volatile 的作用与使用场景

返回函数中静态变量的地址会发生什么

extern C 的作用

sizeof(1==1) 在 C 和 C++ 中的结果

memmove 函数的底层原理

strcpy 函数的缺陷

auto 类型推导的原理

C++ 面向对象

面向对象及其三大特性

重载、重写、隐藏的区别

多态及其实现方法

对象创建限制在堆或栈

C++ 模板编程

虚函数和纯虚函数详解

虚函数和纯虚函数的区别

核心机制:虚函数表(vtable),动态绑定(运行时决定调用)。

纯虚函数:定义接口(=0),含纯虚函数的类是抽象类。

虚函数的实现机制

单继承和多继承的虚函数表结构

如何禁止构造函数的使用

什么是类的默认构造函数

构造函数、析构函数是否可以定义成虚函数

如何避免拷贝

如何减少构造函数开销

多重继承的常见问题及避免方法

空类字节数及对应生成的成员函数

类的大小

为什么拷贝构造函数必须声明为引用

C++ 类对象的初始化顺序

如何禁止一个类被实例化

成员初始化列表效率高的原因

实例化一个对象需要哪几个阶段

友元函数的作用及使用场景

静态绑定和动态绑定的实现

深拷贝和浅拷贝的区别

编译时多态和运行时多态的区别

不允许修改类的成员变量的函数实现方法

如何让类不能被继承

C++ 语言特性相关

左值和右值:区别、引用及转化

std::move() 函数的实现原理

指针及其大小、用法

野指针和悬空指针详解

C++ 11 nullptr 比 NULL 的优势比较

指针和引用的区别

指针可重指向,引用是别名(必须初始化)。

常量指针(const int*) vs 指针常量(int* const)。

常量指针和指针常量的区别

函数指针的定义

强制类型转换的类型

结构体相等的判断方式及 memcmp 函数的使用

参数传递中:值传递、引用传递、指针传递的区别

模板及其实现

函数模板和类模板的区别

什么是可变参数模板

什么是模板特化

switch 的 case 里为何不建议定义变量

迭代器的作用

泛型编程如何实现

什么是类型萃取

C++ I/O 与进程同步

C++ I/O 操作

线程同步与异步

C++ 互斥信号量

C++ 条件变量

设计模式

常见设计模式

单例模式及其实现

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (!instance) instance = new Singleton();
        return instance;
    }
};

工厂模式及其实现

封装对象创建逻辑,解耦调用代码与具体类。

观察者模式及其实现

Clone this wiki locally