C++ 高频面试题之一
大纲
基础面试题
C 语言和 C++ 的区别
C 是面向过程语言,注重函数和流程控制,而 C++ 是面向对象语言,是 C 的超集,支持类、继承、多态等特性。C++ 提供了丰富的标准库和更高层的抽象,比如 STL、模板、异常处理等。同时在语法上,C++ 支持命名空间、函数重载、引用、对象构造等,适合开发大型复杂系统,而 C 更适合底层和嵌入式开发。
分类 | C 语言 | C++ |
---|---|---|
编程范式 | 过程式编程 | 支持过程式 + 面向对象 + 泛型 |
语言类型 | 中级语言 | 高级语言(兼容 C) |
封装性 | 不支持类和对象 | 支持类、对象、继承、多态 |
函数重载 | 不支持 | 支持 |
命名空间 | 不支持 | 支持 |
异常处理 | 不支持 | 支持 try-catch-throw |
标准库 | 比较基础(如 stdio.h ) | 丰富的 STL(如 vector 、map ) |
输入输出 | printf 、scanf | cin 、cout (可使用 iostream ) |
内存管理 | malloc 、free | new 、delete 、智能指针(现代 C++) |
编译方式 | C 编译器(如 gcc ) | C++ 编译器(如 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++ 中,map
和 multimap
的主要区别:
- 相同点:
- 两者都是基于红黑树实现的有序关联容器,底层自动会对 key 进行排序。
- 不同点:
map
不允许 key 重复,一个 key 对应唯一的映射表,插入相同 key 会失败。multimap
允许 key 重复,适用于一对多的映射关系。
红黑树的介绍
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树(BST),它在插入或删除节点后通过颜色标记和旋转操作,保持树的平衡,从而保证查询、插入和删除的时间复杂度始终是 O(log n)
。红黑树满足 5 个特性,可以避免树退化成链表;插入时最多旋转 2 次,删除时最多旋转 3 次,保证了 map
、multimap
等容器在最坏情况下也能稳定地执行查找、插入和删除操作,时间复杂度始终为 O(log n)
。
C++ 在父类的构造函数中调用虚函数,可否实现多态
子类的虚函数表指针(VPTR
)是分步完成初始化的,当执行父类的构造函数时,子类 的 VPTR
指针指向父类的虚函数表,当父类的构造函数执行完毕后,会把子类的 VPTR
指针指向子类的虚函数表。因此,在父类的构造函数中调用虚函数,不能实现多态。
提示
- 对象在创建的时,由编译器对
VPTR
指针进行初始化。 - 只有当对象的构造全部完成后,
VPTR
指针的指向才能最终确定。
C++ 的 new 和 delete,什么时候用 new [] 申请,需要用 delete [] 释放?
在 C++ 中,new
和 delete
是成对使用的内存管理操作符:
new
对应delete
,new[]
对应delete[]
,必须严格匹配,否则可能导致未定义行为或内存泄漏。new
会调用构造函数分配对象,delete
会调用析构函数并释放内存。- 如果是自定义类型并提供了构造函数,用
new
创建时一定要用delete
来释放。 - 如果是数组申请(例如多个对象,尤其是自定义类型),必须使用
new[]
创建,并使用delete[]
释放,这样才会调用每个数组元素的析构函数。
C++ 中如何防止内存泄漏
遵循 RAII(资源获取即初始化)原则
- RAII 是 C++ 中管理资源的核心理念。通过将资源的分配与对象的生命周期绑定,确保在对象销毁时自动释放资源,避免内存泄漏。
- 例如,使用
std::lock_guard
管理互斥锁,或使用std::ofstream
管理文件句柄。
避免裸指针和手动内存管理
- 尽量避免直接使用
new
和delete
,因为手动管理内存容易导致泄漏。 - 如果必须使用裸指针,确保每次分配的内存都有对应的释放操作。
- 尽量避免直接使用
使用智能指针自动管理动态分配的内存
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 |
|
- C++ 的源代码
1 | // 标记大括号里的函数符号是按照 C 语言的规则来生成 |
- 在多语言的项目开发中,往往会看到下面这样的 C++ 代码,其中的
__cplusplus
是 C++ 编译器内置的宏名。这样写的好处是,无论是在 C++ 还是 C 语言的代码中调用sum()
函数,编译器都可以正常编译。
1 |
|
C++ 什么时候会出现访问越界
- 数组访问越界
1 | int arr[5] = {1, 2, 3, 4, 5}; |
- 指针操作不当
1 | int arr[3] = {10, 20, 30}; |
- 字符串访问越界
1 | std::string str = "hello"; |
vector
访问越界
1 | std::vector<int> vec = {1, 2, 3}; |
- 指针删除后访问(悬空指针)
1 | int* p = new int(42); |
C++ 中如何避免访问越界
- 使用 STL 容器代替原始数组(如
vector
、string
) - 使用
at()
函数进行安全访问 - 写循环时注意索引范围
- 尽量避免裸指针,使用智能指针
- 开启编译器警告(如
-Wall -Wextra
) - 使用工具检查,如 Valgrind(运行时内存调试工具)、ASan(内存错误检测工具)
C++ 中类的初始化列表的作用
在 C++ 中,类的初始化列表用于在构造函数体执行前初始化成员变量。它可以提高效率,避免成员先默认构造再赋值,尤其对于 const
成员、引用成员以及没有默认构造函数的成员对象,必须使用初始化列表。初始化列表的执行顺序与成员变量的声明顺序一致,而不是在初始化列表中写的顺序。
初始化列表的概念
- 初始化列表(Initializer List)是类的构造函数的一种语法,用于在构造函数体执行前直接初始化成员变量,写在构造函数冒号
:
的后面。
- 初始化列表(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
——int
是const
,不能改值。int * const p
——p
是const
,不能改指针。
C++ 中 malloc 和 new 的区别
new
- C++ 中的运算符(
operator new
),用于按类型动态分配内存。 - 底层同样是调用
malloc()
函数开辟内存,但还会调用类对象的构造函数(如果是类对象)进行初始化。 - 返回值是对应类型的指针,不需要强制类型转换。
- 开辟内存失败时,会抛出
std::bad_alloc
异常。
- C++ 中的运算符(
malloc
- C 语言中的标准库函数,用于按字节动态分配内存。
- 不会初始化分配的内存,也不会调用类对象的构造函数(如果分配的内存是用于类对象)。
- 返回值是
void *
,需要强制类型转换。 - 开辟内存失败时,返回
nullptr
(或 C 中返回NULL
),需要手动检查。
区别点 | malloc | new |
---|---|---|
所属语言 | C / C++ | C++ 专用 |
本质 | 库函数(在 <cstdlib> 中) | 运算符(可重载) |
内存分配方式 | 按字节动态分配内存 | 按类型动态分配内存 |
返回值 | 返回 void * ,需强制类型转换 | 返回对应类型的指针,无需类型转换 |
是否调用构造函数 | ❌ 不会调用构造函数 | ✅ 会调用构造函数 |
是否调用析构函数 | ❌ free 不会调用析构函数 | ✅ delete 调用析构函数 |
失败时的行为 | 返回 nullptr (或 C 中返回 NULL ),需手动检查 | 抛出 std::bad_alloc 异常 |
是否可重载 | ❌ 不可重载 | ✅ 可重载 operator new 、operator delete |
对象初始化 | ❌ 不支持 | ✅ 自动调用构造函数来初始化对象 |
释放方式 | 使用 free(ptr) 释放内存 | 使用 delete ptr 或 delete[] ptr 释放内存 |
C+ 中 free 和 delete 的区别
delete
的概述delete
操作符用于释放通过new
分配的内存。- 在释放内存之前,
delete
会先调用类对象的析构函数,以确保资源(如文件句柄、网络连接等)正确释放。 - 如果分配的是数组,必须使用
delete[]
,否则可能会导致未定义行为。
free
的概述free
是 C 标准库函数,用于释放通过malloc/calloc/realloc
分配的内存。- 它只会释放内存,不会执行任何其他操作,例如对象的析构。
delete
与free
不能混用- 通过
new
分配的内存必须用delete
释放,不能用free
,否则可能会导致未定义行为。 - 通过
malloc
分配的内存必须用free
释放,不能用delete
,否则可能会导致未定义行为。
- 通过
delete
与free
的内存布局和管理差异new
和delete
是 C++ 的操作符,它们了解对象的类型,并能为复杂类型的构造和析构做出正确的处理。malloc
和free
是 C 的函数,它们只分配和释放内存,不了解对象的类型。
特性 | delete | free |
---|---|---|
适用语言 | C++ 专用 | C 和 C++ |
适用对象 | 动态分配的对象(通过 new 分配) | 动态分配的内存块(通过 malloc/calloc/realloc 分配) |
是否调用析构函数 | 会调用类对象的析构函数 | 只释放内存,不调用类对象的析构函数 |
分配与释放的匹配要求 | 必须和 new 成对使用 | 必须和 malloc/calloc/realloc 成对使用 |
数组释放 | 使用 delete[] 释放动态数组 | 没有专门的数组释放功能 |
底层机制 | C++ 的运行时库负责,处理更高级的资源管理 | C 的运行时库负责,直接释放内存 |
C++ 中 map 和 set 的实现原理
map
和set
是 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 = 0
且weak_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 的区别
struct
和class
最本质的区别是默认访问权限不同:
类型 | 默认成员访问权限 | 默认继承权限 |
---|---|---|
class | private | private |
struct | public | public |
struct
和class
除了默认访问权限不同,其他功能几乎完全一样:- 都可以有构造函数 / 析构函数。
- 都支持继承、多态。
- 都支持访问控制(public /protected/private)。
- 都可以包含成员函数、静态成员、模板等。
特别注意
- 在标准 C++ 中,
struct
和class
在大小上没有本质区别,空的结构体和空类的大小都至少占 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
2int* p = new int(10);
delete p;
- 栈上的变量示例:
内存碎片
- 栈:几乎没有内存碎片,因为是连续分配。
- 堆:容易产生碎片,特别是在频繁
new
/delete
的场景中。
安全性
- 栈:更安全,系统自动管理,出错概率低。
- 堆:容易发生内存泄漏、悬空指针等问题,需手动管理,出错风险较高。
性能
- 栈:性能高,内存分配和释放速度快。
- 堆:性能稍差,涉及系统调用和管理开销。
提示
在 C++ 11 及以后(现代 C++),推荐使用智能指针(如 unique_ptr
、shared_ptr
)管理堆内存,减少内存泄漏风险。
C++ 中宏和内联的区别
宏是简单的文本替换,不安全;内联函数是真正的函数,有类型检查,更推荐使用。
项目 | 宏(Macro) | 内联函数(Inline Function) |
---|---|---|
定义方式 | 使用 #define 预处理指令定义 | 使用 inline 关键字定义 |
处理阶段 | 预处理器在编译前展开(文本替换) | 编译器在编译阶段决定是否内联 |
类型检查 | 没有类型检查,纯粹文本替换 | 有严格的类型检查 |
调试 | 展开后难以调试,出错时排查困难 | 支持调试,可以单步进入函数内部 |
语法特点 | 不能访问作用域、命名空间、模板等高级特性 | 可以访问作用域、命名空间、模板等特性 |
容易出错 | 宏可能因为括号缺失或优先级问题导致错误 | 内联函数遵循 C++ 语法规范,不易出错 |
适用场景 | 一些简单的常量、短小的代码块(但现代 C++ 用 const /constexpr/inline 替代宏) | 频繁调用的小函数,希望减少函数调用开销 |
提示
- 内联只是给编译器的优化建议,不是强制要求,编译器可以忽略
inline
关键字。 - 如果函数过于复杂(如有循环、递归、异常处理),即使加了
inline
也可能不会被内联。 - 过度使用内联会导致程序体积膨胀(代码膨胀问题),影响指令缓存命中率,反而可能拖慢性能。