C++ 基础八股文(基础面试题)

C++ 基础八股文(基础面试题)

目录

一、C++ 程序的内存模型与生命周期

1.1 C++ 程序的内存布局

1)内存分布示意图

2)代码区(Text / Code)

3)静态存储区( Data / BSS )

 4)堆区 / 自由存储区( Heap )

5)栈区( Stack )

6)常量区(ROData)

1.2 栈 vs 堆(深度对比 )

1)对比图

2)栈(Stack)

3)堆(Heap)

4)栈 vs 堆 对比表

1.3 生命周期与作用域

1)局部变量

2)静态 / 全局变量

3)堆上对象

1.4 程序运行时内存增长方向

1.5 面试追问:为什么堆和栈有不同的管理方式?

Q1:为什么栈分配这么快,堆较慢?

Q2:为什么堆需要手动释放?

二、动态内存管理:new / delete 与 malloc / free

2.1 C++ 如何申请内存

2.2 new vs malloc(高频 + 纠偏)

1)本质不同

2)构造 / 析构(核心)

3)失败行为(⚠️ ZEEKLOG 常错)

4)初始化差异

2.3 为什么 C++ 不能只用 malloc/free

三、作用域、生命周期与 static / extern

3.1 static定义静态变量

1)作用:

2)static修饰局部变量:

3)static修饰全局变量:

4)static修饰函数:

5)static修饰类:

3.2 局部变量

3.3 静态局部变量(高频)

3.4 全局变量 vs static 全局变量

3.5 extern 的作用

四、指针、引用与参数传递(重灾区)

4.1 参数传递三种方式

4.2 指针 vs 引用

4.3 指针 vs 数组(陷阱)

4.4 野指针 & 悬挂指针

五、const / volatile / constexpr

5.1 const定义常量

1)const修饰基本数据类型:

2)const修饰指针或引用:

3)const作用到函数:

4)const在类中的用法:

5)const成员函数和static不可同时使用

6)const修饰类:

5.2 const 修饰指针(顶层 / 底层)

5.3 const vs volatile

5.4 constexpr(现代 C++)

六、编译过程、宏、typedef、inline

6.1 编译流程

6.2 #define vs const

6.3 typedef vs #define

6.4 inline vs #define

七、面向对象(构造、析构、多态)

7.1 构造 / 析构

7.2 Rule of 3 / 5 / 0

7.3 构造 / 析构顺序

八、C++中重载和重写、重定义的区别

8.1 重载(Overload,静态多态):

1)介绍:

2)示例:函数重载

3)错误示例(不能靠返回值重载)

8.2 重写(Override,动态多态)

1)介绍:

2)示例:虚函数重写

8.3 重定义 / 隐藏(Name Hiding)

1)介绍:

2)示例:函数隐藏(重定义)


一、C++ 程序的内存模型与生命周期

1.1 C++ 程序的内存布局

1)内存分布示意图

下面是一张标准的内存分布示意图:

一个 典型 C++ 程序运行时,在进程虚拟地址空间中通常分为:

代码区(Text / Code)常量区(ROData)静态存储区(Data / BSS)堆区(Heap)栈区(Stack)
⚠️ 注意:这是逻辑划分,不是物理内存

如图所示,一个 C++ 程序在运行时通常分布成以下几个部分:

2)代码区(Text / Code)

  • 存放内容:
    • 函数机器指令
    • 成员函数实现
  • 特点:
    • 只读
    • 多进程可共享(同一程序多进程)
  • 生命周期:
    • 程序加载 → 程序结束
void foo() { } // foo 的指令在代码区 

3)静态存储区( Data / BSS )

静态存储区分为两部分:

  1. Initialized Data(已初始化区):存储程序中所有已初始化的全局变量和静态变量。
  2. 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 参数传递三种方式

  1. 值传递
  2. 指针传递(地址的值)
  3. 引用传递(别名语义)

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* pint const* p底层经 p 访问的 int 值 不可改(内容常量)
const int* const p顶 & 底指向和所指都不可改
const int& r = x底层通过引用只读(可绑定右值)

5.3 const vs volatile

constvolatile
编译期
防优化
  • const:程序语义不可改
  • volatile:禁止编译器优化
  • volatile ≠ 线程安全

5.4 constexpr(现代 C++)

  • 编译期常量
  • C++14 起 constexpr 函数体可多语句
  • constexpr ⇒ 一定是 const

六、编译过程、宏、typedef、inline

6.1 编译流程

预处理 → 编译 → 汇编 → 链接

6.2 #define vs const

#defineconst
类型
安全

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); // ✅ 显式调用父类版本 } 

Read more

C++未声明的标识符问题详解

C++未声明的标识符问题详解 1. 问题概述 未声明的标识符(undeclared identifier)是C++开发中最常见的编译错误之一。编译器在遇到标识符(变量、函数、类、类型等)时,需要在当前作用域或可见作用域中找到其声明。 2. 常见场景和原因 2.1 变量未声明 intmain(){ x =5;// 错误:'x'未声明return0;}// 正确做法intmain(){int x =5;// 先声明再使用return0;} 2.2 函数未声明 intmain(){myFunction();// 错误:'myFunction'未声明return0;}voidmyFunction(){// 定义在调用之后// ...} 2.3 类型未声明 intmain(

By Ne0inhk
【C++】告别“类型转换”踩坑,从基础到四种核心强制转换方式

【C++】告别“类型转换”踩坑,从基础到四种核心强制转换方式

各位大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页:落羽的落羽 文章目录 * 一、回顾C语言的类型转换 * 二、C++中的类型转换 * 1. 内置类型转为自定义类型 * 2. 自定义类型转为内置类型 * 3. 自定义类型之间的转换 * 4. 类型安全与C++的四种强制类型转换方式 * 4.1 什么是类型安全? * 4.2 static_cast * 4.3 reinterpret_cast * 4.4 const_cast * 4.5 dynamic_cast 一、回顾C语言的类型转换 C语言的类型转换主要是隐式类型转换和强制类型转换: * 隐式类型转换,是编译器在特定情况下自动进行的类型转换,通常发生在不同类型的表达式运算中。主要是整型之间、整型与浮点型之间、

By Ne0inhk
【C++开源库使用】调用开源库STB中的stbi_load_from_memory加载图片文件,进行灰化处理,然后调用stbi_write_png或stbi_write_jpg将灰化图片保存到文件中

【C++开源库使用】调用开源库STB中的stbi_load_from_memory加载图片文件,进行灰化处理,然后调用stbi_write_png或stbi_write_jpg将灰化图片保存到文件中

目录 1、图片灰化的实现思路 2、开源STB库下载 3、将图片文件的内容读到buffer中 4、将buffer中存放的图片文件数据传入到stbi_load_from_memory接口中,然后对返回的图片颜色值进行灰化处理 5、调用stbi_write_png或stbi_write_jpg接口将灰化后的图片数据保存成图片文件 6、图片灰化的完整代码        前一篇文章我们讲到了使用libcurl库发http/https请求去下载用户头像文件(文章链接:https://blog.ZEEKLOG.net/chenlycly/article/details/149175549),本篇文章则是同个SDK项目的后续需求中涉及到的功能。第三方厂商要求,对于不在线的人员,要显示灰化的头像。经研究决定使用开源STB库辅助实现图片灰化,调用STB开源库中的stbi_load_from_memory、stbi_write_png或stbi_write_jpg等接口。本文详细讲述一下实现过程,以供大家借鉴或参考。

By Ne0inhk
【C++深学日志】C++“类”的完全指南--从基础到实践(一)

【C++深学日志】C++“类”的完全指南--从基础到实践(一)

假想一下,你是一个顶级汽车设计师,你的任务不是亲自拧紧每一个螺丝,而是要设计出一幅“汽车蓝图”,你在图纸上设计了一辆汽车所需的一切:车轮、车灯、V8发动机、方向盘等,你手上这份设计好的蓝图就相当于我们今天要讲的C++中的“类”,它规定了汽车的属性(例如:离合器)和方法(功能:换挡),它本身并不是一辆真正的汽车,只是你的一份设计规划,后续你交付给工厂,工厂按照你的设计蓝图,生产出了一辆汽车,这就是实例化,后续工厂有根据你的蓝图设计了一条流水线,每一辆从流水线上生产下来的车辆,都是里这个蓝图(类)的一个对象,他们都有蓝图定义的属性和功能。在C++中类就充当着蓝图的作用,它定义了对象拥有哪些属性,那么就和我一起来揭开这份“蓝图”的面纱吧。 1.类 1.1.类的定义 类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现的分离式编程技术,类的接口包括用户所能执行的操作,类的实现则是包括类的数据成员、负责接口实现的函数以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了他的视线细节,也就是说,

By Ne0inhk