C++ 高频面试题之一

大纲

基础面试题

C 语言和 C++ 的区别

C 是面向过程语言,注重函数和流程控制,而 C++ 是面向对象语言,是 C 的超集,支持类、继承、多态等特性。C++ 提供了丰富的标准库和更高层的抽象,比如 STL、模板、异常处理等。同时在语法上,C++ 支持命名空间、函数重载、引用、对象构造等,适合开发大型复杂系统,而 C 更适合底层和嵌入式开发。

分类 C 语言 C++
编程范式过程式编程支持过程式 + 面向对象 + 泛型
语言类型中级语言高级语言(兼容 C)
封装性不支持类和对象支持类、对象、继承、多态
函数重载不支持支持
命名空间不支持支持
异常处理不支持支持 try-catch-throw
标准库比较基础(如 stdio.h丰富的 STL(如 vectormap
输入输出printfscanfcincout(可使用 iostream
内存管理mallocfreenewdelete智能指针(现代 C++)
编译方式 C 编译器(如 gccC++ 编译器(如 g++),也支持 C 编译
应用方向嵌入式、底层系统应用软件、大型系统

C++ 中多态的理解

  • 多态的实现效果
    • 多态是在运行期间根据具体对象的类型决定函数调用,同样的调用语句有多种不同的表现形态
  • 多态实现的三个必要条件
    • 有继承、有虚函数(virtual )的重写、有父类的指针(引用)指向子类对象
  • 多态的 C++ 实现
    • 使用 virtual 关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用,而是要根据指针所指向的实际对象类型来判断如何调用
  • 多态的理论基础
    • 动态联编 Vs 静态联编,根据实际的对象类型来判断重写函数的调用
  • 多态的重要意义
    • 多态是设计模式的基础,是框架的基石
  • 实现多态的理论基础
    • 函数指针做函数参数
    • 函数指针一般有两种用法(正、反)
  • 多态原理的探究
    • 与面试官展开讨论

C++ 中多态的实现

C++ 中的多态分为两类:

  • 静态多态(编译期多态):在编译阶段确定调用关系,典型代表是函数重载和模板。
  • 动态多态(运行期多态):通过虚函数机制实现,当基类的指针或者引用指向派生类对象时,调用的是派生类中重写的函数。函数调用关系在运行时根据实际对象类型动态决定,从而实现接口统一、行为多样。

C++ 中继承的作用

C++ 继承的主要作用有两个:

  • 代码复用

    • 在 C++ 中,继承是一种基本特性,它允许派生类继承基类的成员变量和成员函数。
    • 通过这种方式,派生类可以直接使用基类已经实现的功能,这不仅实现了代码复用,还提高了开发效率。
  • 实现多态

    • 通过在基类中定义虚函数(特别是纯虚函数),子类重写后可实现运行时多态,使得基类的指针或者引用可以指向派生类的对象。
    • 这样就可以在运行时根据对象的实际类型来调用对应的方法,使得程序更加灵活,能够应对不同的派生类对象,而无需在编译时确定具体的类型。

C++ 中 this 指针的作用

在 C++ 中,一个类可以创建多个对象,每个对象都有自己的一份成员变量,但它们共享同一套成员函数。为了让成员函数知道是哪个对象在调用它,编译器会隐式地将当前对象的地址(即 this 指针)传入成员函数。比如 t.test() 实际上等价于 test(&t)this 就是指向 t 的指针。

  • 简而言之,this 指针是每个非静态成员函数的隐含参数,指向当前调用该成员函数的对象,主要作用包括:

    • 访问当前对象的成员:在成员函数中,this 指针用于访问调用该函数的对象。
    • 区分成员变量和同名参数:当函数参数与成员变量同名时,使用 this-> 明确指代成员变量。
    • 支持链式调用:通过在成员函数中返回 *this 的引用,实现方法链式调用。
    • 防止自赋值:在重载赋值运算符时,使用 this 指针检查自赋值情况。
  • 特别注意,静态成员函数没有 this 指针,因为它们不属于任何对象实例。

C++ 中重载与重写的区别

  • 函数重载

    • 必须在同一个类中进行
    • 子类无法重载父类的函数,父类同名函数将被子类的覆盖(隐藏)
    • 重载是在编译期间根据参数类型、参数个数和参数顺序决定函数的调用
  • 函数重写

    • 必须发生于父类与子类之间
    • 父类与子类中的函数必须有完全相同的原型
    • 使用 virtual 关键字声明之后,能够产生多态(如果不使用 virtual 关键字声明,那叫重定义)

C++ 中 static 关键字的作用

这道面试题可以从面向对象、ELF 结构、链接过程角度来回答。

  • 从面向对象角度看:

    • static 可以修饰类的成员变量和成员函数,使其属于类本身而非类的实例。
    • static 成员变量在所有对象间共享一份,不随对象创建而复制。
    • static 成员函数没有 this 指针,因此不能访问非 static 成员,只能访问类的静态成员。
  • 从编译链接过程看:

    • static 可以修饰全局变量和函数,使它们的符号绑定从全局(GLOBAL)变为局部(LOCAL),且只能在定义它们的 .cpp 文件内部被访问,起到封装作用。
    • static 修饰的局部变量拥有静态存储周期,分配在 .data.bss 段中,作用域仍限定在函数内,但生命周期贯穿整个程序运行期。由于它们不再分配在栈上,而是保存在数据段中,访问方式也由栈帧偏移改为固定地址或段偏移。

C++ STL 中空间配置器的作用

空间配置器(Allocator)是给容器使用的,它的作用是将内存分配与对象构造分离,以及将对象析构与内存释放分离。

  • 内存分配与对象构造分离:构造对象时,先调用 allocate() 分配原始的未初始化内存,然后调用 construct() 在这块内存上构造对象。
  • 对象析构与内存释放分离:销毁对象时,先调用 destroy() 进行对象析构,再用 deallocate() 释放原始内存。

C++ STL 中 vector 和 list 的区别

  • vector 的底层是动态数组,容量不足时会自动扩容(通常近似两倍扩容),支持高效随机访问,适合访问频繁、尾部插入多的场景。
  • list 的底层是双向链表,不支持随机访问,但任意位置插入和删除效率高,适合频繁修改结构的场景。

C++ STL 中 map 和 multimap 的区别

在 C++ 中,mapmultimap 的主要区别:

  • 相同点:
    • 两者都是基于红黑树实现的有序关联容器,底层自动会对 key 进行排序。
  • 不同点:
    • map 不允许 key 重复,一个 key 对应唯一的映射表,插入相同 key 会失败。
    • multimap 允许 key 重复,适用于一对多的映射关系。

红黑树的介绍

红黑树(Red-Black Tree)是一种自平衡的二叉搜索树(BST),它在插入或删除节点后通过颜色标记和旋转操作,保持树的平衡,从而保证查询、插入和删除的时间复杂度始终是 O(log n)。红黑树满足 5 个特性,可以避免树退化成链表;插入时最多旋转 2 次,删除时最多旋转 3 次,保证了 mapmultimap 等容器在最坏情况下也能稳定地执行查找、插入和删除操作,时间复杂度始终为 O(log n)

C++ 在父类的构造函数中调用虚函数,可否实现多态

子类的虚函数表指针(VPTR)是分步完成初始化的,当执行父类的构造函数时,子类 的 VPTR 指针指向父类的虚函数表,当父类的构造函数执行完毕后,会把子类的 VPTR 指针指向子类的虚函数表。因此,在父类的构造函数中调用虚函数,不能实现多态。

提示

  • 对象在创建的时,由编译器对 VPTR 指针进行初始化。
  • 只有当对象的构造全部完成后,VPTR 指针的指向才能最终确定。

C++ 的 new 和 delete,什么时候用 new [] 申请,需要用 delete [] 释放?

在 C++ 中,newdelete 是成对使用的内存管理操作符:

  • new 对应 deletenew[] 对应 delete[],必须严格匹配,否则可能导致未定义行为或内存泄漏。
  • new 会调用构造函数分配对象,delete 会调用析构函数并释放内存。
  • 如果是自定义类型并提供了构造函数,用 new 创建时一定要用 delete 来释放。
  • 如果是数组申请(例如多个对象,尤其是自定义类型),必须使用 new[] 创建,并使用 delete[] 释放,这样才会调用每个数组元素的析构函数。

C++ 中如何防止内存泄漏

  • 遵循 RAII(资源获取即初始化)原则

    • RAII 是 C++ 中管理资源的核心理念。​通过将资源的分配与对象的生命周期绑定,确保在对象销毁时自动释放资源,避免内存泄漏。
    • ​例如,使用 std::lock_guard 管理互斥锁,或使用 std::ofstream 管理文件句柄。​
  • 避免裸指针和手动内存管理

    • 尽量避免直接使用 newdelete,因为手动管理内存容易导致泄漏。​
    • 如果必须使用裸指针,确保每次分配的内存都有对应的释放操作。
  • 使用智能指针自动管理动态分配的内存

    • std::unique_ptr 实现独占所有权,适用于一个对象只有一个所有者的情况。​
    • std::shared_ptr 实现共享所有权,适用于多个对象共享资源的情况。
    • 在使用 std::shared_ptr 时,循环引用会导致内存无法释放;可以使用 std::weak_ptr 打破循环引用,确保资源正确释放。
  • 利用工具检测内存泄漏和未定义行为

    • Valgrind:运行时内存调试工具,检测内存泄漏、越界访问等问题。​
    • AddressSanitizer(ASan):编译器支持的内存错误检测工具,可以检测越界访问、使用后释放等问题。
  • 使用静态分析工具在编译前分析代码

    • Cppcheck:静态代码分析工具,检查内存泄漏、未初始化变量等问题。

C++ 中如何调用 C 语言函数

C++ 调用在 C 语言中定义的函数时,默认会编译失败,这是因为 C++ 与 C 语言生成函数符号的规则是不同的,从而导致 C++ 在编译时找不到在 C 语言中定义的函数。解决方法是,在 C++ 代码中,使用 extern "C" { } 来包裹函数的声明。反之,如果是 C 语言需要调用在 C++ 中定义的函数,那么在 C++ 的代码中,也需要使用 extern "C" { } 包裹函数的定义。

  • C 语言的源代码
1
2
3
4
5
#include <stdio.h>

int sum(int a, int b) {
return a + b;
}
  • C++ 的源代码
1
2
3
4
5
6
7
8
9
10
11
// 标记大括号里的函数符号是按照 C 语言的规则来生成
extern "C" {
// 函数声明
int sum(int a, int b);
}

int main() {
int result = sum(1, 2);
cout << "result = " << result << endl;
return 0;
}
  • 在多语言的项目开发中,往往会看到下面这样的 C++ 代码,其中的 __cplusplus 是 C++ 编译器内置的宏名。这样写的好处是,无论是在 C++ 还是 C 语言的代码中调用 sum() 函数,编译器都可以正常编译。
1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif
int sum(int a, int b) {
return a + b;
}
#ifdef __cplusplus
}
#endif

C++ 什么时候会出现访问越界

  • 数组访问越界
1
2
int arr[5] = {1, 2, 3, 4, 5};
int x = arr[5]; // 越界访问,合法索引是 0~4
  • 指针操作不当
1
2
3
int arr[3] = {10, 20, 30};
int* p = arr + 3;
int x = *p; // 越界访问,指针 p 指向的是 arr [3],这超出了数组边界
  • 字符串访问越界
1
2
std::string str = "hello";
char c = str[5]; // 合法索引是 0~4,而 str[5] 不会检查边界,可能读取垃圾值;推荐使用 str.at(5),如果访问越界则会抛出异常
  • vector 访问越界
1
2
std::vector<int> vec = {1, 2, 3};
int x = vec[5]; // operator[] 访问越界,不会抛异常(程序可能崩溃);推荐使用 vec.at(5),如果访问越界则会抛出 std::out_of_range 异常
  • 指针删除后访问(悬空指针)
1
2
3
int* p = new int(42);
delete p;
*p = 100; // 已释放内存,访问越界

C++ 中如何避免访问越界

  • 使用 STL 容器代替原始数组(如 vectorstring
  • 使用 at() 函数进行安全访问
  • 写循环时注意索引范围
  • 尽量避免裸指针,使用智能指针
  • 开启编译器警告(如 -Wall -Wextra
  • 使用工具检查,如 Valgrind(运行时内存调试工具)、ASan(内存错误检测工具)

C++ 中类的初始化列表的作用

在 C++ 中,类的初始化列表用于在构造函数体执行前初始化成员变量。它可以提高效率,避免成员先默认构造再赋值,尤其对于 const 成员、引用成员以及没有默认构造函数的成员对象,必须使用初始化列表。初始化列表的执行顺序与成员变量的声明顺序一致,而不是在初始化列表中写的顺序。


  • 初始化列表的概念

    • 初始化列表(Initializer List)是类的构造函数的一种语法,用于在构造函数体执行前直接初始化成员变量,写在构造函数冒号 : 的后面。
  • 初始化列表的作用

    • 提高效率
      • 初始化列表直接调用成员变量的构造函数,比在构造函数体中赋值更高效。
      • 特别是对于 const 成员、引用成员、对象成员,只能用初始化列表初始化。
    • 避免多次构造
      • 如果在构造函数体中使用赋值,成员对象会先被默认构造,再赋值,多了一步。
    • 必须使用初始化列表的情况:
      • 引用成员
      • const 成员
      • 没有默认构造函数的成员对象
  • 初始化列表的陷阱

    • 初始化列表的执行顺序与成员变量的声明顺序一致,不是在初始化列表中写的顺序。
    • 如果顺序不一致,编译器可能会给出警告。
    • 初始化顺序错误在某些情况下可能引发未定义行为。

C++ 中 int * const p 和 const int * p 的区别

  • int * const p 是常量指针,指针地址不能改,指向的数据可以改。
  • const int * p 是指向常量的指针,指向的数据不能改,但指针地址可以改。
声明方式中文名称指针是否可变值是否可变使用说明
int * const p常量指针❌ 不可变✅ 可变指针 p 不能指向别的地址,但可以改地址里的值
const int * p指向常量的指针✅ 可变❌ 不可变指针 p 可以指向别的地址,但不能改所指向的数据
const int * const p指向常量的常量指针❌ 不可变❌ 不可变什么都不能改,彻底只读

指针修饰顺序的口诀

  • 口诀一:左定值,右定向。
  • 口诀二:const 靠近谁,谁就不能变。
  • const int * p —— intconst,不能改值。
  • int * const p —— pconst,不能改指针。

C++ 中 malloc 和 new 的区别

  • new

    • C++ 中的运算符(operator new),用于按类型动态分配内存。
    • 底层同样是调用 malloc() 函数开辟内存,但还会调用类对象的构造函数(如果是类对象)进行初始化。
    • 返回值是对应类型的指针,不需要强制类型转换。
    • 开辟内存失败时,会抛出 std::bad_alloc 异常。
  • malloc

    • C 语言中的标准库函数,用于按字节动态分配内存。
    • 不会初始化分配的内存,也不会调用类对象的构造函数(如果分配的内存是用于类对象)。
    • 返回值是 void *,需要强制类型转换。
    • 开辟内存失败时,返回 nullptr(或 C 中返回 NULL),需要手动检查。
区别点mallocnew
所属语言 C / C++C++ 专用
本质库函数(在 <cstdlib> 中)运算符(可重载)
内存分配方式按字节动态分配内存按类型动态分配内存
返回值返回 void *,需强制类型转换返回对应类型的指针,无需类型转换
是否调用构造函数❌ 不会调用构造函数✅ 会调用构造函数
是否调用析构函数free 不会调用析构函数delete 调用析构函数
失败时的行为返回 nullptr(或 C 中返回 NULL),需手动检查抛出 std::bad_alloc 异常
是否可重载❌ 不可重载✅ 可重载 operator newoperator delete
对象初始化❌ 不支持✅ 自动调用构造函数来初始化对象
释放方式使用 free(ptr) 释放内存使用 delete ptrdelete[] ptr 释放内存

C+ 中 free 和 delete 的区别

  • delete 的概述

    • delete 操作符用于释放通过 new 分配的内存。
    • 在释放内存之前,delete 会先调用类对象的析构函数,以确保资源(如文件句柄、网络连接等)正确释放。
    • 如果分配的是数组,必须使用 delete[],否则可能会导致未定义行为。
  • free 的概述

    • free 是 C 标准库函数,用于释放通过 malloc/calloc/realloc 分配的内存。
    • 它只会释放内存,不会执行任何其他操作,例如对象的析构。
  • deletefree 不能混用

    • 通过 new 分配的内存必须用 delete 释放,不能用 free,否则可能会导致未定义行为。
    • 通过 malloc 分配的内存必须用 free 释放,不能用 delete,否则可能会导致未定义行为。
  • deletefree 的内存布局和管理差异

    • newdelete 是 C++ 的操作符,它们了解对象的类型,并能为复杂类型的构造和析构做出正确的处理。
    • mallocfree 是 C 的函数,它们只分配和释放内存,不了解对象的类型。
特性deletefree
适用语言 C++ 专用 C 和 C++
适用对象动态分配的对象(通过 new 分配)动态分配的内存块(通过 malloc/calloc/realloc 分配)
是否调用析构函数会调用类对象的析构函数只释放内存,不调用类对象的析构函数
分配与释放的匹配要求必须和 new 成对使用必须和 malloc/calloc/realloc 成对使用
数组释放使用 delete[] 释放动态数组没有专门的数组释放功能
底层机制 C++ 的运行时库负责,处理更高级的资源管理 C 的运行时库负责,直接释放内存

C++ 中 map 和 set 的实现原理

  • mapset 是 C++ STL(标准模板库)中的关联式容器,它们的底层都是采用红黑树(Red-Black Tree)实现。
  • set 只存储 key,而 map 存储的是 key-value 键值对。元素在容器中都是自动按 key 排序的,默认使用 < 运算符,也可以自定义比较函数。
  • 如果追求更高性能或无序需求,也可以考虑使用 unordered_map / unordered_set,它们的底层是基于哈希表实现,平均复杂度为 O(1)
容器存储内容底层结构是否有序是否允许重复
set只存储 key 红黑树✅ 按 key 排序❌ 不允许重复(可以改用 multiset
map存储 [key, value]红黑树✅ 按 key 排序❌ key 不可重复(可以改用 multimap

C++ 中 shared_ptr 的引用计数存储在哪里

shared_ptr 的引用计数是和对象是分开存储在堆上的。引用计数并不是存在被管理对象中,而是存储在由 shared_ptr 内部管理的一个独立控制块(Control Block)中。这种设计可以支持多个指针共享对象的同时,又能独立于对象本体跟踪引用和生命周期。

  • 控制块中存储的内容包括:

    • 指向真实对象的指针。
    • use_count:共享引用计数,用于追踪有多少个 shared_ptr 指向这块内存。
    • weak_count:弱引用计数,用于追踪有多少个 weak_ptr 还在引用。
    • 可能还有删除器(deleter),比如自定义内存释放策略。
  • 控制块在什么时候释放

    • use_count 变为 0 时,对象占用的内存会被释放,但控制块还保留(供 weak_ptr 判断是否过期)。
    • use_count = 0weak_count = 0 时,控制块才会被销毁。

C++ 中 STL 容器都有哪些,底层结构是什么

  • 顺序容器
容器名称描述底层结构
vector动态数组容器,支持快速随机访问,尾部插入删除效率高连续线性数组
deque双端队列容器,支持头尾快速插入删除支持动态扩展的二维数组结构(分段连续空间)
list双向链表容器,插入 / 删除效率高,不支持随机访问双向链表
  • 有序关联容器(底层为红黑树)
容器名称描述底层结构
set不可重复集合,自动按 key 排序红黑树
map键值对映射表,key 唯一,自动按 key 排序红黑树
multiset可重复元素集合,自动按 key 排序红黑树
multimap可重复键的映射表,自动按 key 排序红黑树
  • 无序关联容器(底层为哈希表)
容器名称描述底层结构
unordered_set无序集合,key 唯一,查找效率高哈希表(开链法)
unordered_map无序映射表,key 唯一,存储 <key, value> 键值对哈希表(开链法)
unordered_multiset无序集合,元素可重复哈希表(开链法)
unordered_multimap无序映射表,key 可重复哈希表(开链法)
  • 容器适配器
容器名称描述底层结构(默认)
stack后进先出(LIFO)栈结构deque
queue先进先出(FIFO)队列deque
priority_queue优先队列,自动排序堆(默认最大堆,用 vector + make_heap 实现)
  • 近容器
容器名称描述底层结构
array固定大小的数组容器,编译期确定大小静态数组(连续内存)
string字符串容器,支持动态扩展与字符串操作动态字符数组
bitset定长位集容器,适合二进制标志管理位数组(每个位单独存储)

提示

STL 中的哈希表采用开链法(Separate Chaining)解决哈希冲突,桶结构的底层可能是链表或更优化的数据结构(如 C++ 17 后可用链表 + 跳表等)。

C++ STL 中迭代器失效的问题

迭代器失效的定义

当对容器执行某些修改操作(如插入、删除等)后,原有的迭代器、引用或指针变得不再有效,继续使用可能导致未定义行为(如访问野指针、崩溃等),这就是迭代器失效。

不同容器的迭代器失效情况

  • list

    • 插入元素:不会失效
    • 删除元素:指向被删除元素的迭代器失效,其他迭代器仍然有效。
  • vector / deque

    • 插入元素:所有迭代器都可能失效,因为可能会导致整体扩容。
    • 删除元素:被删除元素之后的迭代器都会失效。
  • map / set / multimap / multiset

    • 插入元素:通常不会使迭代器失效。
    • 删除元素:指向被删除元素的迭代器失效,其他迭代器仍然有效。
  • unordered_map / unordered_set

    • 插入元素:所有迭代器都可能失效,因为可能涉及哈希表重排。
    • 删除元素:所有迭代器都可能失效,因为可能涉及哈希表重排。

如何避免迭代器失效

  • 在容器结构修改后,需要及时对迭代器进行更新,如 it = vec.erase(it);
  • 不要在遍历容器时,修改容器结构,或者要注意修改方式。
  • 使用 Stable 容器(如 list)处理频繁插入 / 删除的场景。

C++ STL 中哪些容器是基于红黑树实现

容器名称描述底层结构
set不可重复集合,自动按 key 排序红黑树
map键值对映射表,key 唯一,自动按 key 排序红黑树
multiset可重复元素集合,自动按 key 排序红黑树
multimap可重复键的映射表,自动按 key 排序红黑树

C++ 中 struct 和 class 的区别

  • structclass 最本质的区别是默认访问权限不同:
类型默认成员访问权限默认继承权限
classprivateprivate
structpublicpublic
  • structclass 除了默认访问权限不同,其他功能几乎完全一样:
    • 都可以有构造函数 / 析构函数。
    • 都支持继承、多态。
    • 都支持访问控制(public /protected/private)。
    • 都可以包含成员函数、静态成员、模板等。

特别注意

  • 在标准 C++ 中,structclass 在大小上没有本质区别,空的结构体和空类的大小都至少占 1 个字节,主要是为了满足对象的地址唯一性要求。
  • 在标准 C 语言中,空结构体是不被允许的,是非法的。但像 GCC 这样的编译器出于兼容性和底层优化,会提供非标准扩展,允许存在空结构体,且空结构体的大小默认为 0。这种用法不具备可移植性,不建议在跨平台项目中使用。

C++ STL 中 vector 和数组的区别

  • vector 是 C++ STL 中的动态数组容器,能够自动管理内存、支持动态扩容。
  • 数组是固定大小的内存块,功能有限,需要手动管理内存。
  • 数组的最大缺点是固定大小,并且没有访问越界检查。
  • vector 适合现代 C++ 开发,更安全,更灵活,但比数组有略微的性能开销(需要构造、析构等)

vector 与 数组的深入对比

特性 vector 数组
大小可变✅ 支持动态扩容❌ 固定大小(静态)或需手动重新分配(动态)
内存管理自动管理(构造 / 析构 / 扩容)手动管理
安全性有边界检查(at()无边界检查,容易越界
功能接口提供丰富成员函数(如 push_back()insert()resize()无成员函数
与 STL 配合完美兼容 STL 算法需要手动传长度或转换为迭代器
类型支持支持对象元素(构造 / 析构函数会被调用)只能存储裸数据,管理复杂
复制 / 赋值拷贝构造 & 深拷贝(对象语义)默认浅拷贝(指针语义)
性能稍微多点性能开销(如扩容、拷贝构造)更轻量,性能高,但需谨慎使用

C++ 中编译链接的全过程

C++ 的编译链接过程主要分为 4 个阶段:预处理(Preprocessing)→ 编译(Compilation)→ 汇编(Assembly)→ 链接(Linking),最终生成可执行文件。

  • 预处理:展开头文件内容(#include),处理宏替换(#define),处理条件编译,去除注释。
  • 编译:将预处理后的代码(.i 文件)编译成汇编代码(.s 文件),会进行语法 / 语义分析、生成中间代码。
  • 汇编:将汇编代码(.s 文件)翻译成机器码,生成二进制格式的目标文件(.o),其中包含符号表、段信息等。
  • 链接:将多个目标文件(.o 文件)和库文件合并,解决符号地址引用,生成最终的可执行文件。

C++ 中初始化全局变量和未初始化全局变量的区别

初始化的全局变量存放在 .data 段,未初始化的全局变量存放在 .bss 段,但都属于静态存储区,程序运行前就已经分配好。未初始化的全局变量默认值为 0。

初始化全局变量和未初始化全局变量的深入对比

  • 存储区域

    • .data 段:负责存储已初始化的全局变量。
    • .bbs 段:负责存储未初始化、或者初始化值为 0 的全局变量。
  • 是否初始化

    • 初始化的全局变量:程序启动时由编译器 / 操作系统赋初值。
    • 未初始化的全局变量:会被自动初始化为 0(基本类型)。
  • 程序运行前的表现

    • .data 段(已初始化):编译时已确定,程序启动时拷贝进内存。
    • .bss 段(未初始化):不占用可执行文件的实际存储空间,程序运行时清零。
  • 汇编层面的差异

    • 在编译后的汇编代码中:
      • 初始化的变量在 .data 段中有实际数据。
      • 未初始化的变量在 .bss 段中标记为零初始化。

C++ 中栈和堆的区别

栈适合临时、小型、自动管理的内存;堆适合长期、动态、大型对象的内存,但需要程序员手动管理内存。

栈和堆的深入对比

  • 内存分配方式

    • 栈:由编译器自动分配和释放,速度快。
    • 堆:由程序员使用 new / delete 手动分配和释放,速度相对较慢。
  • 生命周期

    • 栈:变量在离开作用域时自动销毁(例如函数返回)。
    • 堆:变量在程序员手动 delete 后才销毁,或者由智能指针管理。
  • 内存大小限制

    • 栈:空间较小(通常几 MB),适合小型数据。
    • 堆:空间大(受限于系统内存),适合存放大型对象或数组。
  • 使用方式

    • 栈上的变量示例:
      1
      int a = 10;
    • 堆上的变量示例:
      1
      2
      int* p = new int(10);
      delete p;
  • 内存碎片

    • 栈:几乎没有内存碎片,因为是连续分配。
    • 堆:容易产生碎片,特别是在频繁 new / delete 的场景中。
  • 安全性

    • 栈:更安全,系统自动管理,出错概率低。
    • 堆:容易发生内存泄漏、悬空指针等问题,需手动管理,出错风险较高。
  • 性能

    • 栈:性能高,内存分配和释放速度快。
    • 堆:性能稍差,涉及系统调用和管理开销。

提示

在 C++ 11 及以后(现代 C++),推荐使用智能指针(如 unique_ptrshared_ptr)管理堆内存,减少内存泄漏风险。

C++ 中宏和内联的区别

宏是简单的文本替换,不安全;内联函数是真正的函数,有类型检查,更推荐使用。

项目宏(Macro)内联函数(Inline Function)
定义方式使用 #define 预处理指令定义使用 inline 关键字定义
处理阶段预处理器在编译前展开(文本替换)编译器在编译阶段决定是否内联
类型检查没有类型检查,纯粹文本替换有严格的类型检查
调试展开后难以调试,出错时排查困难支持调试,可以单步进入函数内部
语法特点不能访问作用域、命名空间、模板等高级特性可以访问作用域、命名空间、模板等特性
容易出错宏可能因为括号缺失或优先级问题导致错误内联函数遵循 C++ 语法规范,不易出错
适用场景一些简单的常量、短小的代码块(但现代 C++ 用 const /constexpr/inline 替代宏)频繁调用的小函数,希望减少函数调用开销

提示

  • 内联只是给编译器的优化建议,不是强制要求,编译器可以忽略 inline 关键字。
  • 如果函数过于复杂(如有循环、递归、异常处理),即使加了 inline 也可能不会被内联。
  • 过度使用内联会导致程序体积膨胀(代码膨胀问题),影响指令缓存命中率,反而可能拖慢性能。