C/C++ 内存管理、指针与堆栈

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/deletenew[]/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;   // 错误,未定义行为

对于内置类型如 intdouble,因为不涉及析构函数,混用 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 段。
  • 调用全局对象的构造函数。
  • 传递命令行参数 argcargv 给 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 段。
  • 动态申请的内存:使用 newmalloc 等申请的内存存储在堆区。
  • 常量数据:字符串常量等通常存储在 .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

基本实现思路:

  1. 将源地址和目标地址转换为 char* 类型,按字节复制。
  2. 处理空指针或零长度情况。
  3. 由于按字节复制效率较低,实际库实现会按字长(如 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 访问内存的效率。编译器会按照一定规则在成员之间插入填充字节。

对齐规则:

  1. 结构体每个成员的起始地址必须是其自身大小的整数倍。
  2. 结构体整体大小必须是其最大成员大小的整数倍。
  3. 可以使用 #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_ptrstd::shared_ptrstd::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++ 内存泄漏是指使用 newmalloc 等分配的堆内存没有被正确释放,导致这部分内存无法被再次使用。

按照发生场景分类:

  • 堆内存泄漏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_uniquestd::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. 从代码到执行的闭环
    • 第1题「开发流程」是起点:讲清楚代码如何经过预处理→编译→汇编→链接变成可执行文件,是所有程序运行的前提。
    • 第15题「main函数是否是所有动作的起源」是终点:明确main是程序执行的唯一入口,操作系统通过调用main启动程序,收尾“程序怎么跑起来”的问题。
  2. 从范式到语言的对比
    • 第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++语法的语义基石。

二、知识点逻辑链路

遵循「变量存在性→变量类型→变量操作→语义本质→落地案例」的递进逻辑:

  1. 第3题「声明vs定义」:最底层的概念——声明是“告诉编译器变量存在”,不分配内存;定义是“真正创建变量”,分配内存。这是所有变量的前提。
  2. 第11题「常量的两种定义方式」:延伸到特殊变量——常量,区分宏常量(预处理替换)和const常量(有类型有作用域)。
  3. 第8题「赋值与初始化的区别」:讲变量的两种核心操作:初始化是“创建变量时赋初值”(调用构造),赋值是“已有变量覆盖值”(调用operator=),底层机制完全不同。
  4. 第16题「左值与右值引用」:从语义层面升维——左值是“有稳定内存地址、可被赋值”的表达式,右值是“临时值、无地址”的表达式,是理解C++移动语义、赋值规则的基础。
  5. 第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++区别于高级语言的核心——直接操作内存的基础,也是后续内存管理、指针编程、性能优化的前提。

二、知识点逻辑链路

遵循「底层存储原理→复合类型→访问工具→易错场景→类型对比」的认知路径:

  1. 第6题「小数的二进制表示」:建立底层认知——浮点数遵循IEEE 754标准,由符号位、指数位、尾数位组成,理解“浮点数精度丢失”的根源。
  2. 第5题「数组名是什么」:复合类型的起点——数组名本质是指向首元素的地址常量,不可修改,是数组与指针关联的核心。
  3. 第22题「指针与数组的区别」:厘清易混概念——数组是连续内存空间,指针是地址变量,只是数组名可隐式转换为指针。
  4. 第19/28题「野指针、空指针、悬空指针」:指针的易错场景——空指针指向空地址(合法但不可解引用),野指针未初始化(指向随机内存),悬空指针指向已释放内存,是指针bug的重灾区。
  5. 第21题「指针数组vs数组指针」:指针与数组的深度结合——指针数组是“存指针的数组”,数组指针是“指向数组的指针”,考察对类型优先级的理解。
  6. 第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 = &num;         // 指针:存储地址,可改指向,可空
    int& ref = num;        // 引用:变量别名,必须初始化,不可改指向
    ref = 200;
    cout << "num=" << num << ", *p=" << *p << ", ref=" << ref << endl;

    return 0;
}

模块四:函数机制与特殊函数

一、模块核心作用

理解函数的底层执行逻辑,掌握不同函数形态的适用场景,是C/C++模块化编程的核心,也是后续类成员函数、多态、模板的基础。

二、知识点逻辑链路

遵循「函数执行原理→参数传递→函数形态对比→编译期多态」的递进逻辑:

  1. 第10题「函数调用的步骤」:底层原理——函数调用本质是创建栈帧、传参、执行、销毁栈帧、返回结果,理解栈帧是理解递归、局部变量生命周期的关键。
  2. 第14题「值的传递方式」:数据传入函数的三种方式:值传递(拷贝副本)、指针传递(传地址)、引用传递(传别名),决定了函数能否修改实参。
  3. 第9题「宏函数和自定义函数的区别」:两种“函数形态”的本质差异——宏函数是预处理文本替换,无类型检查、高效但易出错;普通函数是编译期生成,有类型检查、有栈帧开销。
  4. 第24题「内联函数」:C++对宏函数的优化——编译期在调用点展开,保留高效性的同时增加类型检查,解决宏函数的安全问题。
  5. 第33题「短小频繁函数的实现」:场景落地题——C语言用宏函数,C++用内联函数,直接对应前面两个知识点的选型。
  6. 第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;
}

模块五:关键字与存储管理

一、模块核心作用

掌握conststatic两大核心关键字的全场景用法,理解变量的存储周期、作用域、链接属性,是理解程序内存布局、跨文件编程的关键。

二、知识点逻辑链路

分为两大主线,最终落地到内存存储原理:

  1. 常量体系主线
    • 第13题「const用途」:基础用法——修饰变量、函数参数、成员函数、指针,核心是“只读”。
    • 第18题「const/constexpr/consteval/constinit区别」:C++常量体系演进——从运行时只读的const,到编译期常量constexpr,到强制编译期执行的consteval,再到保证静态初始化的constinit,不断强化编译期计算能力。
  2. 存储与链接主线
    • 第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种,各司其职,从安全到风险排序:
    1. static_cast:静态安全转换,用于隐式能支持的转换(如int转double、父子类指针向上转换),编译期检查。
    2. const_cast:专门去除/添加const属性,只能操作指针或引用。
    3. reinterpret_cast:底层二进制重解释,极不安全(如int转指针),仅用于底层编程。
    4. 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资源管理(智能指针、锁等)的基础。

二、知识点逻辑链路

两个知识点构成对象生命周期的完整闭环:

  1. 第26题「构造函数的特点」:生命周期起点——对象创建时自动调用,负责初始化成员变量、申请资源。特点:与类同名、无返回值、可重载、支持初始化列表。
  2. 第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;
}

运行输出:
构造调用:张三 诞生
姓名:张三
析构调用:张三 销毁

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值