C++ 基础八股文(基础面试题)
目录
二、动态内存管理:new / delete 与 malloc / free
五、const / volatile / constexpr
一、C++ 程序的内存模型与生命周期
1.1 C++ 程序的内存布局
1)内存分布示意图
下面是一张标准的内存分布示意图:


一个 典型 C++ 程序运行时,在进程虚拟地址空间中通常分为:
代码区(Text / Code)常量区(ROData)静态存储区(Data / BSS)堆区(Heap)栈区(Stack)
⚠️ 注意:这是逻辑划分,不是物理内存
如图所示,一个 C++ 程序在运行时通常分布成以下几个部分:
2)代码区(Text / Code)
- 存放内容:
- 函数机器指令
- 成员函数实现
- 特点:
- 只读
- 多进程可共享(同一程序多进程)
- 生命周期:
- 程序加载 → 程序结束
void foo() { } // foo 的指令在代码区 3)静态存储区( Data / BSS )
静态存储区分为两部分:
- Initialized Data(已初始化区):存储程序中所有已初始化的全局变量和静态变量。
- BSS(未初始化区):存储声明但未显式初始化的全局变量和静态变量,默认初始化为 0。
| 区域 | 存放内容 |
|---|---|
| Data | 已初始化的全局 / static |
| BSS | 未初始化或初始化为 0 的 |
int g1 = 10; // Data static int g2 = 0; // BSS int g3; // BSS 两者的区别是:
Data:可执行文件中占空间BSS:只记录“大小”,不存实际数据(节省磁盘)
⚠️ 纠偏(非常重要)
1. 全局变量 不在堆上,而在 静态存储区
2. 所有static变量(不论写在函数里还是文件里),都不在栈,也不在堆,而在“静态存储区”(Data / BSS / ROData)。作用域 ≠ 存储位置。
4)堆区 / 自由存储区( Heap )
- 分配方式:
new / malloc - 生命周期:程序员控制
- 特点:
- 空间大
- 分配慢
- 容易泄漏 / 碎片化
由程序员或运行时库动态分配在程序运行时向操作系统请求空间生命周期由程序员控制(必须释放)
int* p = new int(10); // 堆 delete p; 堆和栈的内存增长方向通常是相反的:栈从高地址向低地址增长,堆从低地址向高地址增长。
堆 = 按需申请、按需释放(无固定顺序)。堆是“自由存储区/动态内存池”:new/malloc申请一块,delete/free在你指定的时候释放释放顺序不固定:你可以先释放后申请的,也可以先释放先申请的,完全取决于代码逻辑分配通常较栈慢:需要管理空闲块、可能产生碎片、可能涉及锁(多线程)等空间通常较栈大:受进程虚拟内存/系统限制
5)栈区( Stack )
- 由编译器自动管理添加/释放
- 存放内容:
- 局部变量
- 函数参数
- 返回地址
- 特点:
- 自动分配 / 自动释放
- 极快
- 空间有限
void foo() { int x = 10; // 栈 } 栈区是后进先出(LIFO)结构,分配非常快,但空间有限。是 LIFO(后进先出):因为函数调用/返回、局部变量进出栈天然就是“最后进的最先退”分配/释放快:通常就是移动栈指针空间相对有限:受线程栈大小限制(不同系统/配置不同)
6)常量区(ROData)
- 存放:
- 字符串字面量
- const 全局常量
- 只读
const int c = 10; printf("hello"); // "hello" 在 ROData 1.2 栈 vs 堆(深度对比 )
1)对比图
下面是一张对比图,更清晰地展示两者异同:

2)栈(Stack)
- 由系统 / 编译器自动管理
- 作用域退出自动回收
- 访问速度快
- 内存连续性高
- 空间较小
3)堆(Heap)
- 由程序员手动管理(new/delete 或 malloc/free)
- 生命周期不与作用域绑定
- 空间大但申请/释放成本高
- 容易产生内存碎片和泄漏
4)栈 vs 堆 对比表
| 属性 | 栈 Stack | 堆 Heap |
|---|---|---|
| 管理者 | 编译器/运行时 | 程序员/运行时 |
| 生命周期 | 随作用域自动收 | 程序员控制 |
| 分配/释放 | 快 | 慢 |
| 内存连续性 | 通常连续 | 不保证 |
| 常见用途 | 函数局部变量 | 动态分配对象 |
| 内存风险 | 栈溢出 | 内存泄漏 / 碎片 |
1.3 生命周期与作用域
1)局部变量
定义在函数内部,栈上分配,函数返回自动释放。
void foo() { int x = 1; // 栈上,进入作用域创建,离开自动销毁 } 2)静态 / 全局变量
数据段分配,程序开始即存在,程序结束才销毁
static int s = 10; int g = 20; 3)堆上对象
必须手动管理,否则会泄漏。
int* p = new int(30); delete p; // 需手动 1.4 程序运行时内存增长方向
在大多数操作系统和编译器 ABI 下:
栈向低地址增长堆向高地址增长
这意味着当程序开辟大量栈空间时,可能栈与堆“相向生长”,导致 stack overflow 或 heap 分配失败。
1.5 面试追问:为什么堆和栈有不同的管理方式?
Q1:为什么栈分配这么快,堆较慢?
👉 栈只需移动一个指针(Stack Pointer)。无需查找空闲内存,也无需复杂算法。
堆需要查空闲链表 / 分配算法
Q2:为什么堆需要手动释放?
👉 堆不与作用域绑定,C++ 不提供 GC(垃圾回收)。因此需要显式释放,否则程序长期运行会泄漏空间。
二、动态内存管理:new / delete 与 malloc / free
请先理解上节内存模型,否则本节你可能无法看懂 new/delete 和 malloc/free 的区别。
2.1 C++ 如何申请内存
int* p1 = new int; int* p2 = (int*)malloc(sizeof(int)); 2.2 new vs malloc(高频 + 纠偏)
1)本质不同
new:运算符malloc:函数
2)构造 / 析构(核心)
class A { public: A(){ cout<<"ctor\n"; } ~A(){ cout<<"dtor\n"; } }; A* p = new A; // ctor delete p; // dtor A* q = (A*)malloc(sizeof(A)); // ❌ 不调用 ctor free(q); // ❌ 不调用 dtor 3)失败行为(⚠️ ZEEKLOG 常错)
int* p = new int; // 失败抛异常 int* q = new(std::nothrow)int;// 失败返回 nullptr 4)初始化差异
new int; // 未初始化 new int(); // 值初始化(0) 2.3 为什么 C++ 不能只用 malloc/free
- 无法构造 / 析构对象
- 无法与 RAII / OOP 结合
- 不支持重载
三、作用域、生命周期与 static / extern
3.1 static定义静态变量
1)作用:
控制变量的存储方式和作用域(生命周期和作用范围)
2)static修饰局部变量:
对于一般局部变量而已,一般存放在栈区,且局部变量的生命周期在所包含的语句块结束后而结束
通过static修饰后的局部变量,则该局部变量存放在静态区,生命周期会一直延续到整个程序执行结束后而结束,但其作用域未发生改变,作用的还是其所在语句块中。
3)static修饰全局变量:
对于全局变量,一般存放在全局区,能够被整个程序所访问到,同时也能被同一个工程中的其他源文件所访问到(但是需添加extern声明),
通过static修饰过后,则该全局变量存放在静态区,且也改变了该变量的作用域,通过static修饰后,该全局变量只能在本文件中被访问到,而其他文件访问不到。
4)static修饰函数:
与修饰全局变量一样,改变了其作用域。
5)static修饰类:
如果类中的某个成员函数被static所修饰,那么该成员函数不属于该类的任何一个对象,他属于整个类,所有对象共享同一个函数,同时该函数也只能访问类中的静态变量,而不能访问其他成员变量和成员函数。
如果static修饰类中的成员变量,那么该变量就归所有的对象,存储空间中也只有一个副本,所有对象共享同一份数据,可以通过类或对象直接进行调用。
3.2 局部变量
- 栈上
- 作用域随函数结束销毁
3.3 静态局部变量(高频)
void counter() { static int cnt = 0; cnt++; } - 生命周期:程序全程
- 作用域:函数内
- 只初始化一次
3.4 全局变量 vs static 全局变量
int g = 10; static int sg = 20;
| 类型 | 可见性 |
|---|---|
| 全局变量 | 可跨程序 |
| static 全局 | 当前文件 |
3.5 extern 的作用
- 跨文件声明变量/函数
extern "C":解决 C/C++ 混编名改编问题
extern int g; 四、指针、引用与参数传递(重灾区)
4.1 参数传递三种方式
- 值传递
- 指针传递(地址的值)
- 引用传递(别名语义)
4.2 指针 vs 引用
| 维度 | 指针 | 引用 |
|---|---|---|
| 是否为空 | 可以 | 不可以 |
| 是否可改指向 | 可以 | 不可以 |
| 使用 | *p | 直接用 |
面试标准回答
必须有对象 → 引用
可能为空 / 需改指向 → 指针
4.3 指针 vs 数组(陷阱)
int arr[10]; int* p = arr; sizeof(arr); // 40 sizeof(p); // 8 4.4 野指针 & 悬挂指针
- 野指针:未初始化
- 悬挂指针:释放后未置空
- 无论是野指针还是悬挂指针,所指向的都是一块无效内存的指针,访问无效内存,将导致程序编译出错。
- 避免:对于野指针的避免,就是在定义指针后且在使用之前对指针进行初始化,或用智能指针。对于悬挂指针的避免,就是在内存释放后,即使的吧指针置空或调用智能指针。
五、const / volatile / constexpr
5.1 const定义常量
1)const修饰基本数据类型:
使得这些变量变为常量,其值不可更改。
2)const修饰指针或引用:
const * 则const修饰的是变量,*const则const修饰的是指针。
3)const作用到函数:
const作用到函数中时,对其函数作用部分进行常量化,当用作函数参数时,其参数被定义为接收常量,函数体内则无法改变其参数,这样就可以保护原对象的变量不会被函数所改变(一般作用参数的是指针或引用)。
4)const在类中的用法:
const修饰成员变量,该变量只在该对象中是常量,其值不可以改变,但不同的对象可以申请不同的常量值,所以不能在类中初始化const修饰的成员变量,因为类的对象在没有创建时,编译器不知道const修饰的成员变量的值是什么,const修饰的成员变量的初始化只能在构造函数中初始化。const修饰成员函数,主要是为了防止成员函数修改其对象的内容(就是成员函数不可以更改成员变量),若成员函数想修改某个变量,则可以将该变量用mutable进行修饰。
5)const成员函数和static不可同时使用
因为static修饰的成员函数不包含this指针,且不能实例化,而const成员函数需要具体到某一个函数。
6)const修饰类:
定义常量对象,常量对象只能调用常量函数,不可以调用其他函数。
5.2 const 修饰指针(顶层 / 底层)
const int* p; // 底层 const int* const p; // 顶层 const - 顶层 const:修饰变量自身(“我不可变”);拷贝时常被忽略。
- 底层 const:修饰经由指针/引用访问到的对象(“对方不可变”)。
| 写法 | 顶层/底层 | 含义 |
|---|---|---|
int* const p | 顶层 | p 的指向不可变(常量指针) |
const int* p 或 int const* p | 底层 | 经 p 访问的 int 值 不可改(内容常量) |
const int* const p | 顶 & 底 | 指向和所指都不可改 |
const int& r = x | 底层 | 通过引用只读(可绑定右值) |
5.3 const vs volatile
| const | volatile | |
|---|---|---|
| 编译期 | 是 | 否 |
| 防优化 | 否 | 是 |
const:程序语义不可改volatile:禁止编译器优化- volatile ≠ 线程安全
5.4 constexpr(现代 C++)
- 编译期常量
- C++14 起 constexpr 函数体可多语句
constexpr⇒ 一定是const
六、编译过程、宏、typedef、inline
6.1 编译流程
预处理 → 编译 → 汇编 → 链接6.2 #define vs const
| #define | const | |
|---|---|---|
| 类型 | 无 | 有 |
| 安全 | 低 | 高 |
6.3 typedef vs #define
- typedef 有类型系统
- 宏只是文本替换
6.4 inline vs #define
- inline 可被拒绝
- 有类型检查
七、面向对象(构造、析构、多态)
7.1 构造 / 析构
- 构造:初始化
- 析构:资源释放
- 多态基类析构函数必须 virtual
7.2 Rule of 3 / 5 / 0
- 有资源 → Rule of 5
- 用智能指针 → Rule of 0
7.3 构造 / 析构顺序
- 构造:基类 → 成员 → 本类
- 析构:反向
八、C++中重载、重写、重定义的区别
8.1 重载(Overload,静态多态):
1)介绍:
👉 同一作用域内
👉 函数名相同,参数列表不同
👉 返回值 不能 作为区分依据
重载指的是函数重载或运算符重载,指同一访问区内,被声明的几个参数列表不同的同名函数,C没有函数重载,C++能实现重载,主要是C++中对函数名的修饰和C不一样,C++对函数名的修饰,会把函数的参数类型加到函数名中,从而使得在程序中函数名一样,但在访问区中函数名不一样,返回值类型不能作为函数重载的依据。(属于静态多态)
2)示例:函数重载
#include <iostream> using namespace std; void foo(int x) { cout << "foo(int)" << endl; } void foo(double x) { cout << "foo(double)" << endl; } int main() { foo(10); // 调用 foo(int) foo(3.14); // 调用 foo(double) } ✔️ 编译期就能确定调用哪个函数
✔️ 属于 静态多态
3)错误示例(不能靠返回值重载)
int foo(int x); double foo(int x); // ❌ 编译错误 8.2 重写(Override,动态多态)
1)介绍:
👉 发生在继承关系中
👉 必须是 virtual 函数
👉 函数签名完全一致
👉 通过 基类指针 / 引用调用
主要指派生类中重新定义父类中的出函数体外其他都完全相同的虚函数,重写的一定是虚函数,在子类中重写函数,其访问权限可以随便由程序员自己定义。(属于动态多态)
2)示例:虚函数重写
#include <iostream> using namespace std; class Base { public: // virtual 表示:这是虚函数 -> 允许派生类重写,并在运行期进行动态绑定 virtual void func() { cout << "Base::func" << endl; } // ⚠️ 实战建议:基类通常要有虚析构,否则 delete Base* 指向派生对象时可能出问题 virtual ~Base() = default; }; class Derived : public Base { public: /* * func() override: * - override 关键字不是必须,但强烈推荐 * - 编译器会检查:你是否真的“重写了一个基类虚函数” * 如果你把参数写错、const 写错、返回值不匹配等,编译器会直接报错 */ void func() override { cout << "Derived::func" << endl; } ~Derived() override = default; }; int main() { /* * 这里 new Derived() 产生的是“派生类对象”,在堆上 * 但我们用 Base*(基类指针)去接它 —— 这叫“向上转型” * 向上转型是安全的:Derived is-a Base */ Base* p = new Derived(); /* * 重点来了: * p 的静态类型(编译期看到的类型)是 Base* * p 指向的动态类型(运行期真实对象类型)是 Derived * * 因为 func 是 virtual: * 调用 p->func() 时会走“动态绑定”,运行期查虚函数表 vtable * 最终调用的是 Derived::func() */ p->func(); // 输出:Derived::func /* * 清理内存: * 因为 Base 的析构函数是 virtual,所以 delete p 时会正确调用: * Derived::~Derived() -> Base::~Base() * 不会发生资源泄漏或未定义行为 */ delete p; return 0; }✔️ 运行期通过虚函数表决定
✔️ 属于 动态多态
8.3 重定义 / 隐藏(Name Hiding)
1)介绍:
👉 子类定义了和父类同名的函数
👉 不要求 virtual
👉 参数列表可以不同
👉 父类同名函数被 整体隐藏
重定义,指在派生类中,重新定义和父类名字相同的非virtual函数,其参数列表和返回值都可以不同。则父类中的同名函数被子类所隐藏,如果想要调用父类中的同名函数,则需要加上父类的作用域。
2)示例:函数隐藏(重定义)
#include <iostream> using namespace std; class Base { public: void func(int x) { cout << "Base::func(int)" << endl; } }; class Derived : public Base { public: void func(double x) { cout << "Derived::func(double)" << endl; } }; int main() { Derived d; d.func(3.14); // 调用 Derived::func(double) // d.func(10); // ❌ 编译错误,被隐藏了 d.Base::func(10); // ✅ 显式调用父类版本 }