C/C++ 内存管理、指针与堆栈:高频考点深度解析(完整版)
1. new 与 malloc 的区别
new 是 C++ 的操作符,执行时会先调用 operator new 分配内存,再调用构造函数完成对象初始化;malloc 是 C 标准库函数,只分配原始内存,不会调用构造函数。
主要区别如下:
- 语言归属不同:
new是 C++ 的一部分,不需要额外头文件;malloc来自<cstdlib>或<stdlib.h>。 - 类型安全:
new返回具体类型的指针;malloc返回void*,需要强制类型转换。 - 失败处理:
new默认抛出std::bad_alloc异常;malloc失败返回NULL。 - 可重载性:
new对应的operator new可以被重载;malloc不能重载。 - 释放方式:
new分配的内存使用delete或delete[]释放;malloc分配的内存使用free释放。 - 数组支持:
new[]会调用多个对象的构造函数,delete[]会调用每个对象的析构函数;malloc只是分配一块连续内存,需要手动计算大小。
实际开发中,C++ 代码优先使用 new/delete 或智能指针,不建议混用 malloc 和 new,否则容易出现未定义行为。
2. new、delete、malloc、free 的关系
new 和 delete 是 C++ 的运算符,封装了内存分配与对象生命周期管理;malloc 和 free 是 C 的库函数,只负责内存分配与释放。
new 的底层通常调用 operator new,而 operator new 的实现可能调用 malloc,但这不是标准强制要求。delete 的底层通常调用 operator delete,也可能调用 free。
核心关系可以概括为:
new= 分配内存 + 调用构造函数delete= 调用析构函数 + 释放内存malloc= 仅分配原始内存free= 仅释放内存
new/delete、new[]/delete[]、malloc/free 三对必须严格配对使用,不能交叉混用。例如,用 malloc 申请的内存不能用 delete 释放,用 new 申请的内存也不能用 free 释放。
3. delete 与 delete[] 的区别
delete 用于释放单个对象,delete[] 用于释放数组对象,二者不可混用。
使用 new[] 分配数组时,编译器通常会在内存头部记录数组元素个数,以便 delete[] 能够正确调用每个对象的析构函数。如果用 delete 释放数组,只会调用第一个对象的析构函数,并错误地释放内存,导致未定义行为,可能引发内存泄漏或程序崩溃。
示例:
class A {
public:
~A() { cout << "destructor" << endl; }
};
A* p1 = new A;
delete p1; // 正确
A* p2 = new A[10];
delete[] p2; // 正确
// delete p2; // 错误,未定义行为
对于内置类型如 int、double,因为不涉及析构函数,混用 delete 和 delete[] 在某些平台上可能不会立即崩溃,但仍然属于未定义行为,必须避免。
4. new operator 与 operator new 的区别
这是两个完全不同的概念。
new operator(new 操作符) 是 C++ 语言内置的运算符,使用时不能重载其整体行为。它的执行步骤是固定的:先调用 operator new 分配内存,再在分配的内存上调用构造函数。
operator new 是一个函数,可以被重载。它的作用只是分配内存,相当于 C 语言中的 malloc。标准库提供了全局的 ::operator new 和类成员版本的 operator new。
示例:
void* raw = operator new(sizeof(A)); // 只分配内存,不构造对象
A* obj = new (raw) A(); // placement new,在已分配内存上构造对象
obj->~A(); // 手动析构
operator delete(raw); // 释放内存
placement new 允许在指定地址构造对象,常用于内存池、嵌入式系统等需要精确控制内存位置的场景。
5. main 函数执行前后还能执行代码吗?
可以。
main 函数执行之前,会完成一系列初始化工作:
- 设置栈指针,准备运行环境。
- 初始化全局变量和静态变量,包括
.data段和.bss段。 - 调用全局对象的构造函数。
- 传递命令行参数
argc、argv给main。 - 可以使用
__attribute__((constructor))(GCC)或在全局对象的构造函数中插入自定义代码。
main 函数执行之后,也可以执行代码:
- 使用
atexit注册函数,程序正常退出时会逆序调用。 - 全局对象的析构函数会在
main之后执行。 - 可以使用
__attribute__((destructor))(GCC)。
示例:
#include <cstdlib>
#include <iostream>
using namespace std;
void before() { cout << "before main" << endl; }
void after() { cout << "after main" << endl; }
__attribute__((constructor)) void init() { before(); }
__attribute__((destructor)) void cleanup() { after(); }
int main() {
atexit(after);
cout << "in main" << endl;
return 0;
}
6. 堆和栈的区别
堆和栈是程序运行时两种最重要的内存区域,区别如下:
| 维度 | 栈 | 堆 |
|---|---|---|
| 分配方式 | 编译器自动分配和释放 | 程序员手动分配和释放 |
| 分配速度 | 很快,只需移动栈指针 | 较慢,需要查找合适内存块 |
| 内存大小 | 通常较小(如 1MB~8MB) | 较大,受限于虚拟内存 |
| 生命周期 | 随函数调用结束自动释放 | 由程序员控制,不释放会泄漏 |
| 碎片问题 | 基本无碎片 | 容易产生内存碎片 |
| 生长方向 | 向低地址生长 | 向高地址生长 |
| 典型用途 | 局部变量、函数参数、返回地址 | 动态申请的内存、大型对象 |
| 线程安全 | 每个线程有独立栈 | 多线程共享堆,需要加锁保护 |
栈溢出通常由递归过深或局部数组过大引起;堆溢出或泄漏则由动态内存管理不当引起。理解两者的差异是排查内存问题的基础。
7. 局部变量、全局变量及动态申请的数据分别存储在哪里?
程序运行时内存布局通常分为代码区、数据区、堆、栈等部分。不同类型变量的存储位置如下:
- 局部变量:存储在栈区。函数调用时分配,函数返回时释放。
- 全局变量:已初始化的全局变量和静态变量存储在
.data段;未初始化的存储在.bss段。 - 动态申请的内存:使用
new、malloc等申请的内存存储在堆区。 - 常量数据:字符串常量等通常存储在
.rodata只读数据段。 - 程序代码:存储在代码区(
.text段)。
示例:
int g_a = 10; // .data
int g_b; // .bss
static int s_c = 20; // .data
const char* p = "hello"; // p 在栈或.data,"hello" 在.rodata
void foo() {
int local = 0; // 栈
int* p = new int(5); // p 在栈,*p 在堆
}
8. 编程实现 memcpy
memcpy 用于将源地址的指定字节数复制到目标地址,要求源和目标内存区域不能重叠。若可能重叠,应使用 memmove。
基本实现思路:
- 将源地址和目标地址转换为
char*类型,按字节复制。 - 处理空指针或零长度情况。
- 由于按字节复制效率较低,实际库实现会按字长(如 8 字节)对齐批量复制。
示例实现:
void* my_memcpy(void* dest, const void* src, size_t n) {
if (dest == nullptr || src == nullptr) return dest;
char* d = static_cast<char*>(dest);
const char* s = static_cast<const char*>(src);
while (n--) {
*d++ = *s++;
}
return dest;
}
更高效的实现可以考虑按 8 字节或 CPU 字长对齐复制,但需要注意未对齐地址的处理。面试中如果要求考虑重叠,应说明 memmove 的实现方式:根据地址高低决定从前向后或从后向前复制。
9. struct 的内存对齐与内存占用计算
结构体内存对齐是为了提高 CPU 访问内存的效率。编译器会按照一定规则在成员之间插入填充字节。
对齐规则:
- 结构体每个成员的起始地址必须是其自身大小的整数倍。
- 结构体整体大小必须是其最大成员大小的整数倍。
- 可以使用
#pragma pack(n)修改默认对齐系数。
示例:
struct S {
char a; // 1 字节
int b; // 4 字节
short c; // 2 字节
};
假设默认对齐系数为 4:
a占 1 字节,后面填充 3 字节。b占 4 字节。c占 2 字节,后面填充 2 字节以满足整体对齐。- 总大小为 12 字节。
合理安排成员顺序可以减少内存占用。例如将相同大小的成员放在一起,通常能降低填充字节。
10. 智能指针的定义与作用
C++11 起标准库提供了三种主要智能指针:std::unique_ptr、std::shared_ptr、std::weak_ptr。
智能指针是封装了原生指针的类模板,核心思想是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)。它在构造时获取资源,在析构时自动释放资源,避免手动管理内存带来的泄漏和悬空指针问题。
主要作用:
- 自动释放动态分配的内存。
- 明确资源所有权,减少悬空指针和重复释放。
- 支持异常安全,即使发生异常也能保证资源被正确释放。
现代 C++ 推荐优先使用智能指针,尽量避免裸指针管理堆内存。
11. 内存泄漏以及解决方法
内存泄漏是指程序动态申请的内存没有被正确释放,导致系统可用内存逐渐减少。长期运行的服务(如模型推理服务)中出现内存泄漏会造成性能下降甚至进程崩溃。
常见原因:
new后没有对应的delete。- 异常导致释放代码未执行。
- 循环引用导致
shared_ptr引用计数无法归零。 - 使用了 C 接口返回的内存但没有调用对应释放函数。
解决方法:
- 使用 RAII 和智能指针管理资源。
- 遵循“谁申请、谁释放”的原则。
- 使用
try-catch-finally或智能指针保证异常安全。 - 借助工具检测,如 Valgrind、AddressSanitizer、Visual Studio 调试器等。
- 对于
shared_ptr循环引用,使用weak_ptr打破循环。
12. 空指针和悬空指针的区别
空指针(null pointer) 是指没有指向任何有效对象的指针,通常用 nullptr 或 NULL 表示。对空指针解引用会导致程序崩溃,但容易排查。
悬空指针(dangling pointer) 是指指向的内存已经释放或失效,但指针本身没有被置空的指针。悬空指针的危害更大,因为它看起来像一个有效指针,解引用时可能产生不可预测的行为。
示例:
int* p = new int(10);
delete p;
// p 此时是悬空指针
p = nullptr; // 好习惯,避免悬空
防止悬空指针的方法:
- 释放内存后立即将指针置为
nullptr。 - 使用智能指针自动管理生命周期。
- 避免返回局部变量的指针或引用。
13. 智能指针的作用与缺点
智能指针通过 RAII 机制自动管理动态内存,主要作用包括:
- 自动释放资源,避免内存泄漏。
- 封装裸指针,降低悬空指针风险。
- 提供明确的所有权语义,改善代码可维护性。
- 支持异常安全。
智能指针的缺点:
std::shared_ptr存在循环引用问题,需要配合std::weak_ptr使用。std::shared_ptr有引用计数开销,包括额外的控制块内存和原子操作性能损耗。- 使用不当仍可能出现问题,例如用同一个裸指针初始化多个
shared_ptr会导致重复释放。 - 不能替代所有裸指针场景,例如非内存资源、数组管理需要自定义删除器。
std::auto_ptr已废弃,不应使用。
14. 应用程序运行时的内存布局
应用程序运行时内存通常划分为代码区和数据区,数据区又可细分为多个部分:
- 代码区(.text):存放程序的可执行指令,只读。
- 已初始化数据区(.data):存放已初始化的全局变量和静态变量。
- 未初始化数据区(.bss):存放未初始化的全局变量和静态变量,程序启动时会被清零。
- 只读数据区(.rodata):存放常量,如字符串常量、常量全局变量等。
- 堆区(heap):动态分配的内存区域,由低地址向高地址生长。
- 栈区(stack):存放局部变量、函数参数、返回地址等,由高地址向低地址生长。
此外,现代程序还可能包含动态链接库映射区、内存映射文件区、内核空间等区域。
15. C++ 内存泄漏详解
C++ 内存泄漏是指使用 new、malloc 等分配的堆内存没有被正确释放,导致这部分内存无法被再次使用。
按照发生场景分类:
- 堆内存泄漏:
new/malloc后没有delete/free。 - 系统资源泄漏:除了内存,还包括文件句柄、网络连接、锁等资源未释放。
- 隐式泄漏:对象生命周期管理复杂,导致引用计数无法归零。
典型示例:
void leak() {
int* p = new int[100];
if (some_error()) {
return; // 未释放 p,造成泄漏
}
delete[] p;
}
预防措施:
- 优先使用
std::unique_ptr和std::shared_ptr。 - 使用工厂函数
std::make_unique、std::make_shared创建智能指针。 - 对于资源句柄,使用 RAII 类封装。
- 定期进行内存泄漏检测和压力测试。
16. C++ 智能指针在模型服务中的选择
在 AIGC 模型服务(如推理服务)中,智能指针的选择需要结合性能、并发和生命周期复杂度综合考虑:
-
优先使用
std::unique_ptr:对于只在一个模块或一个函数中拥有的对象,如单次推理请求的输入输出缓冲区、Session句柄等,unique_ptr零开销且所有权清晰。 -
谨慎使用
std::shared_ptr:模型服务中常见多线程共享的上下文、模型权重缓存等场景适合使用shared_ptr,但要注意引用计数的原子操作开销。高频创建销毁大量shared_ptr可能成为性能瓶颈。 -
使用
std::weak_ptr打破循环引用:例如回调注册、缓存索引等场景,避免shared_ptr相互引用导致内存无法释放。 -
避免
std::auto_ptr:该类型已在 C++17 中移除。 -
大对象和模型权重:对于超大模型权重,通常采用显式内存池或专用分配器管理,智能指针仅作为上层句柄。
-
异步推理流水线:跨线程、跨队列传递对象时,优先使用
shared_ptr或移动语义配合unique_ptr,避免裸指针跨线程传递带来的生命周期问题。
总结:模型服务中智能指针的选择原则是——能用 unique_ptr 就不用 shared_ptr,必须共享时用 shared_ptr,有循环引用风险时引入 weak_ptr,同时关注引用计数和原子操作带来的性能开销。
换言之
下面按7个模块逐一拆解:模块核心作用→知识点逻辑链路→经典代码示例,帮你建立「点→线→面」的完整知识体系,理解每个知识点为什么放在这里、彼此之间如何承接。
模块一:语言概览与程序基础
一、模块核心作用
建立C/C++程序从开发→编译→运行的全局认知,理解两种语言的设计哲学差异,是后续所有语法学习的顶层框架,帮你从“只会写代码”升级到“懂程序本质”。
二、知识点逻辑链路
本模块遵循「程序生命周期 + 语言设计思想」两条并行逻辑:
- 从代码到执行的闭环
- 第1题「开发流程」是起点:讲清楚代码如何经过预处理→编译→汇编→链接变成可执行文件,是所有程序运行的前提。
- 第15题「main函数是否是所有动作的起源」是终点:明确main是程序执行的唯一入口,操作系统通过调用main启动程序,收尾“程序怎么跑起来”的问题。
- 从范式到语言的对比
- 第2题「面向过程vs面向对象」是底层范式:前者以“函数+数据”为核心,后者以“类+封装”为核心,是两种编程思想的本质区别。
- 第12题「C/C++各自特点」是落地延伸:C是面向过程的代表,专注高效、贴近硬件;C++兼容C的同时扩展了面向对象、泛型编程,是多范式语言。
三、经典代码示例
// ========== 面向过程(C风格) ==========
#include <stdio.h>
// 以函数为核心,数据与操作分离
double calcArea(double w, double h) {
return w * h;
}
// ========== 面向对象(C++风格) ==========
#include <iostream>
// 以类为核心,封装数据与操作
class Rectangle {
private:
double width, height; // 数据封装
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() { return width * height; } // 操作封装
};
// main是程序唯一入口,所有执行从这里开始
int main() {
// 面向过程调用
double area1 = calcArea(3.0, 4.0);
printf("面向过程面积:%.2lf\n", area1);
// 面向对象调用
Rectangle rect(3.0, 4.0);
std::cout << "面向对象面积:" << rect.getArea() << std::endl;
return 0;
}
对应知识点:开发流程(两段代码都需经过4步编译链接)、main入口、面向过程vs面向对象、C/C++特性差异。
模块二:基础语法与变量语义
一、模块核心作用
搞懂变量/常量的本质属性与语义规则,区分“声明/定义”“初始化/赋值”“左值/右值”这些极易混淆的基础概念,是理解所有C++语法的语义基石。
二、知识点逻辑链路
遵循「变量存在性→变量类型→变量操作→语义本质→落地案例」的递进逻辑:
- 第3题「声明vs定义」:最底层的概念——声明是“告诉编译器变量存在”,不分配内存;定义是“真正创建变量”,分配内存。这是所有变量的前提。
- 第11题「常量的两种定义方式」:延伸到特殊变量——常量,区分宏常量(预处理替换)和const常量(有类型有作用域)。
- 第8题「赋值与初始化的区别」:讲变量的两种核心操作:初始化是“创建变量时赋初值”(调用构造),赋值是“已有变量覆盖值”(调用operator=),底层机制完全不同。
- 第16题「左值与右值引用」:从语义层面升维——左值是“有稳定内存地址、可被赋值”的表达式,右值是“临时值、无地址”的表达式,是理解C++移动语义、赋值规则的基础。
- 第4题「i++和++i效率对比」:经典落地案例——后置++返回临时右值,前置++返回自身左值,完美体现左值右值、临时对象对效率的影响。
三、经典代码示例
#include <iostream>
using namespace std;
// 3. 声明与定义:声明不分配内存,定义分配内存
extern int g_var; // 声明:仅告知编译器变量存在
int g_var = 10; // 定义:分配内存并初始化
// 11. 常量的两种定义方式
#define MACRO_NUM 100 // 宏常量:预处理文本替换,无类型
const int CONST_NUM = 200; // const常量:有类型,有作用域,编译期检查
int main() {
// 8. 初始化 vs 赋值
int a = 10; // 初始化:创建变量同时赋初值
int b;
b = 20; // 赋值:变量已存在,覆盖原有值
// 16. 左值与右值
int x = 5; // x是左值(有地址),5是右值(临时常量)
// 5 = x; // 报错:右值不能放在赋值号左侧
// 4. i++ vs ++i 左值右值体现
int i = 0;
int val1 = i++; // 后置++:返回原值(临时右值),再自增
int val2 = ++i; // 前置++:先自增,返回自身(左值)
cout << "val1=" << val1 << ", val2=" << val2 << ", i=" << i << endl;
// 输出:val1=0, val2=2, i=2
return 0;
}
模块三:数据类型与底层存储
一、模块核心作用
深入内存层面理解数据的存储与访问方式,是C/C++区别于高级语言的核心——直接操作内存的基础,也是后续内存管理、指针编程、性能优化的前提。
二、知识点逻辑链路
遵循「底层存储原理→复合类型→访问工具→易错场景→类型对比」的认知路径:
- 第6题「小数的二进制表示」:建立底层认知——浮点数遵循IEEE 754标准,由符号位、指数位、尾数位组成,理解“浮点数精度丢失”的根源。
- 第5题「数组名是什么」:复合类型的起点——数组名本质是指向首元素的地址常量,不可修改,是数组与指针关联的核心。
- 第22题「指针与数组的区别」:厘清易混概念——数组是连续内存空间,指针是地址变量,只是数组名可隐式转换为指针。
- 第19/28题「野指针、空指针、悬空指针」:指针的易错场景——空指针指向空地址(合法但不可解引用),野指针未初始化(指向随机内存),悬空指针指向已释放内存,是指针bug的重灾区。
- 第21题「指针数组vs数组指针」:指针与数组的深度结合——指针数组是“存指针的数组”,数组指针是“指向数组的指针”,考察对类型优先级的理解。
- 第20/23题「指针与引用的区别」:两种内存访问方式的对比——指针是地址变量,可空可改指向;引用是变量别名,必须初始化不可改指向,底层汇编层面无本质差异。
三、经典代码示例
#include <iostream>
using namespace std;
int main() {
// 5. 数组名本质:指向首元素的地址常量
int arr[5] = {1,2,3,4,5};
cout << "数组名地址:" << arr << " 首元素地址:" << &arr[0] << endl;
// arr = nullptr; // 报错:数组名是常量,不可修改指向
// 21. 指针数组 vs 数组指针
int* ptr_arr[3]; // 指针数组:数组,每个元素是int*指针
int (*arr_ptr)[5]; // 数组指针:指针,指向长度为5的int数组
arr_ptr = &arr; // 指向整个数组的地址
// 19/28. 空指针、野指针、悬空指针
int* p1 = nullptr; // 空指针:指向空地址,合法
int* p2; // 野指针:未初始化,指向随机内存,危险
int* p3 = new int(10);
delete p3; // p3变为悬空指针:指向已释放内存
p3 = nullptr; // 规范:释放后立即置空
// 20/23. 指针 vs 引用
int num = 100;
int* p = # // 指针:存储地址,可改指向,可空
int& ref = num; // 引用:变量别名,必须初始化,不可改指向
ref = 200;
cout << "num=" << num << ", *p=" << *p << ", ref=" << ref << endl;
return 0;
}
模块四:函数机制与特殊函数
一、模块核心作用
理解函数的底层执行逻辑,掌握不同函数形态的适用场景,是C/C++模块化编程的核心,也是后续类成员函数、多态、模板的基础。
二、知识点逻辑链路
遵循「函数执行原理→参数传递→函数形态对比→编译期多态」的递进逻辑:
- 第10题「函数调用的步骤」:底层原理——函数调用本质是创建栈帧、传参、执行、销毁栈帧、返回结果,理解栈帧是理解递归、局部变量生命周期的关键。
- 第14题「值的传递方式」:数据传入函数的三种方式:值传递(拷贝副本)、指针传递(传地址)、引用传递(传别名),决定了函数能否修改实参。
- 第9题「宏函数和自定义函数的区别」:两种“函数形态”的本质差异——宏函数是预处理文本替换,无类型检查、高效但易出错;普通函数是编译期生成,有类型检查、有栈帧开销。
- 第24题「内联函数」:C++对宏函数的优化——编译期在调用点展开,保留高效性的同时增加类型检查,解决宏函数的安全问题。
- 第33题「短小频繁函数的实现」:场景落地题——C语言用宏函数,C++用内联函数,直接对应前面两个知识点的选型。
- 第25题「函数重载」:C++编译期多态的基础——同名函数通过参数列表区分,底层靠名字修饰(name mangling)实现。
三、经典代码示例
#include <iostream>
using namespace std;
// 9. 宏函数:预处理文本替换,无类型检查
#define ADD_MACRO(a,b) ((a)+(b)) // 必须加括号,避免运算符优先级问题
// 24. 内联函数:编译期展开,有类型检查
inline int addInline(int a, int b) {
return a + b;
}
// 25. 函数重载:同名不同参,底层靠名字修饰区分
void print(int x) { cout << "int类型:" << x << endl; }
void print(double x) { cout << "double类型:" << x << endl; }
// 14. 三种传参方式
void passByValue(int x) { x = 100; } // 值传递:修改副本,不影响实参
void passByPtr(int* x) { *x = 100; } // 指针传递:通过地址修改实参
void passByRef(int& x) { x = 100; } // 引用传递:通过别名修改实参
int main() {
// 宏函数 vs 内联函数
cout << "宏函数结果:" << ADD_MACRO(1+2, 3+4) << endl;
cout << "内联函数结果:" << addInline(1+2, 3+4) << endl;
// 函数重载
print(10);
print(3.14);
// 传参方式对比
int a = 10, b = 10, c = 10;
passByValue(a);
passByPtr(&b);
passByRef(c);
cout << "a=" << a << ", b=" << b << ", c=" << c << endl;
// 输出:a=10, b=100, c=100
return 0;
}
模块五:关键字与存储管理
一、模块核心作用
掌握const、static两大核心关键字的全场景用法,理解变量的存储周期、作用域、链接属性,是理解程序内存布局、跨文件编程的关键。
二、知识点逻辑链路
分为两大主线,最终落地到内存存储原理:
- 常量体系主线
- 第13题「const用途」:基础用法——修饰变量、函数参数、成员函数、指针,核心是“只读”。
- 第18题「const/constexpr/consteval/constinit区别」:C++常量体系演进——从运行时只读的const,到编译期常量constexpr,到强制编译期执行的consteval,再到保证静态初始化的constinit,不断强化编译期计算能力。
- 存储与链接主线
- 第32题「static的三种作用」:分别修饰全局变量(内部链接,仅本文件可见)、局部变量(静态存储,只初始化一次)、函数(内部链接),核心是改变生命周期或链接属性。
- 第29题「static类成员」:面向对象延伸——static成员变量属于类而非对象,所有对象共享;static成员函数无this指针,只能访问static成员。
- 第31题「全局/局部变量的区别与实现」:底层原理——全局变量存在数据段(.data/.bss),生命周期随程序;局部变量存在栈段,生命周期随函数。操作系统和编译器通过内存段划分来区分。
- 第30题「引用全局变量」:落地应用——用
extern声明跨文件全局变量,体现外部链接属性。
三、经典代码示例
#include <iostream>
using namespace std;
// 32. static全局变量:内部链接,仅本文件可见
static int s_global = 10;
// 普通全局变量:外部链接,整个程序可见
int normal_global = 20;
// 13/18. 常量体系
const int const_val = 100; // 运行时常量,只读变量
constexpr int constexpr_val = 200; // 编译期常量,编译期必须能计算
// 29. static类成员
class Demo {
public:
static int s_count; // static数据成员:所有对象共享
int m_id;
Demo(int id) : m_id(id) { s_count++; }
// static成员函数:无this指针,只能访问static成员
static void showCount() {
cout << "对象总数:" << s_count << endl;
}
};
int Demo::s_count = 0; // static成员必须类外初始化
// static局部变量:静态存储,只初始化一次
void testStaticLocal() {
static int s_local = 0;
s_local++;
cout << "s_local = " << s_local << endl;
}
int main() {
// static局部变量测试
testStaticLocal(); // 1
testStaticLocal(); // 2
testStaticLocal(); // 3
// static类成员测试
Demo d1(1), d2(2);
Demo::showCount(); // 输出:对象总数:2
// 30. 引用全局变量:extern声明外部链接
extern int normal_global;
cout << "全局变量值:" << normal_global << endl;
return 0;
}
模块六:C++类型转换体系
一、模块核心作用
掌握C++类型安全的强制转换方式,替代C风格的“一刀切”转换,理解不同转换运算符的适用场景与风险,写出更规范、可维护的代码。
二、知识点逻辑链路
第7题和第17题是同一个知识点的两种问法,核心是C++对C风格强制转换的拆分:
- C风格强制转换
(type)val没有安全检查,任何转换都能通过,风险极高且意图不明确。 - C++将强制转换拆分为4种,各司其职,从安全到风险排序:
static_cast:静态安全转换,用于隐式能支持的转换(如int转double、父子类指针向上转换),编译期检查。const_cast:专门去除/添加const属性,只能操作指针或引用。reinterpret_cast:底层二进制重解释,极不安全(如int转指针),仅用于底层编程。dynamic_cast:多态类型向下转换,运行期检查类型,失败返回nullptr,最安全但有性能开销。
三、经典代码示例
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base类" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived类" << endl; }
};
int main() {
// 1. static_cast:静态安全转换
double d = 3.14;
int i = static_cast<int>(d); // double转int,编译期检查
// 2. const_cast:去除const属性
const int c_val = 100;
int* p = const_cast<int*>(&c_val);
// 注意:修改原const变量属于未定义行为,仅用于非常量原变量的const去除
// 3. reinterpret_cast:底层二进制重解释
int num = 0x61;
char* cp = reinterpret_cast<char*>(&num);
cout << "重解释结果:" << *cp << endl; // 输出字符'a'
// 4. dynamic_cast:多态向下转换,运行期检查
Base* base = new Derived();
Derived* der = dynamic_cast<Derived*>(base);
if (der) der->show(); // 转换成功,输出Derived类
// C风格转换:不推荐,无安全检查,意图不明确
int j = (int)d;
return 0;
}
模块七:类与对象入门
一、模块核心作用
掌握对象生命周期管理,是面向对象封装特性的核心,也是后续继承、多态、RAII资源管理(智能指针、锁等)的基础。
二、知识点逻辑链路
两个知识点构成对象生命周期的完整闭环:
- 第26题「构造函数的特点」:生命周期起点——对象创建时自动调用,负责初始化成员变量、申请资源。特点:与类同名、无返回值、可重载、支持初始化列表。
- 第27题「析构函数的作用」:生命周期终点——对象销毁时自动调用,负责释放资源(堆内存、文件句柄、锁等)。特点:
~类名、无参数无返回值、不能重载、栈对象离开作用域自动调用。
两者一一对应,共同保证“资源申请即初始化,销毁即释放”,是C++ RAII编程范式的核心。
三、经典代码示例
#include <iostream>
#include <cstring>
using namespace std;
class Person {
private:
char* m_name; // 堆内存资源
public:
// 构造函数:对象创建时调用,申请资源并初始化
Person(const char* name) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
cout << "构造调用:" << m_name << " 诞生" << endl;
}
// 析构函数:对象销毁时调用,释放资源
~Person() {
cout << "析构调用:" << m_name << " 销毁" << endl;
delete[] m_name; // 释放堆内存,防止泄漏
}
void showName() {
cout << "姓名:" << m_name << endl;
}
};
int main() {
Person p("张三");
p.showName();
// 函数结束时,栈对象自动调用析构函数
return 0;
}
运行输出:
构造调用:张三 诞生
姓名:张三
析构调用:张三 销毁

希望对你有所帮助,流浪者,浏览者,应用者。

158

被折叠的 条评论
为什么被折叠?



