CC++八股文之内存

C/C++ 八股文之内存

Author:Once Day Date:2026年1月13日

漫漫长路,才刚刚开始...

全系列文章请查看专栏: C语言_Once-Day的博客-CSDN博客

参考文档:

文章目录

      • [C/C++ 八股文之内存](#C/C++ 八股文之内存)
        • [1. 内存本质](#1. 内存本质)
        • [2. 内存分区](#2. 内存分区)
        • [3. 指针与引用的区别](#3. 指针与引用的区别)
        • [4. 指针传递、值传递、引用传递](#4. 指针传递、值传递、引用传递)
        • [5. 资源获取即初始化](#5. 资源获取即初始化)
        • [6. malloc-free 内存分配原理](#6. malloc-free 内存分配原理)
        • [7. malloc和new的区别](#7. malloc和new的区别)
        • [8. 内存泄漏/野指针/空悬指针](#8. 内存泄漏/野指针/空悬指针)
1. 内存本质

内存是计算机中的存储空间,本质上是一个连续的字节数组 。程序运行时,所有的指令、数据和状态都存储在内存中。从这个角度看,编程的本质就是操控内存中的数据:读取、修改、传递这些数据,最终完成特定的计算任务。

内存中的每个字节都有一个唯一的地址(address),这个过程称为内存编址。地址本质上是一个非负整数,用于标识某个字节在内存中的位置。

在32位系统中,地址用32位表示,理论上可以访问 2^32 = 4GB 的内存空间;

64位系统使用64位地址,理论寻址空间达到 2^64 字节(实际受硬件和操作系统限制)。

地址空间 (address space)是程序可以使用的所有有效地址的集合。现代操作系统为每个进程提供虚拟地址空间,这是一个抽象层,让每个进程都认为自己独占整个内存。

典型的进程地址空间布局(以Linux为例):

  • 代码段(text):存储可执行指令。
  • 数据段(data/bss):存储全局变量、静态变量。
  • 堆(heap):动态分配的内存,由低地址向高地址增长。
  • 栈(stack):存储局部变量、函数调用信息,由高地址向低地址增长。

变量是对一块内存区域的抽象命名。编译器为每个变量分配特定大小的内存空间,变量名在编译后会被替换为对应的内存地址。

变量的三个关键属性:

  1. 地址: 变量在内存中的位置。
  2. 大小: 变量占用的字节数。
  3. 类型: 决定如何解释这些字节。

当一个多字节数据(如int、long)存储到内存时,其各字节的存储顺序称为字节序。主要有两种:

  • 大端序(Big-Endian):高位字节存储在低地址。
  • 小端序(Little-Endian):低位字节存储在低地址。
c 复制代码
int num = 0x12345678;

// 在小端系统(如x86)中,内存布局:
// 地址:  0x1000  0x1001  0x1002  0x1003
// 内容:   0x78    0x56    0x34    0x12

// 在大端系统中,内存布局:
// 地址:  0x1000  0x1001  0x1002  0x1003
// 内容:   0x12    0x34    0x56    0x78
2. 内存分区

程序在运行时,操作系统会将进程的虚拟地址空间划分为多个逻辑区域,每个区域有不同的用途和特性。理解内存分区对于编写高效、安全的C/C++程序至关重要。典型的内存分区包括:代码区、全局/静态存储区、栈区、堆区和常量区

(1)代码区存储程序的可执行指令,即编译后的机器码:

  • 只读属性:防止程序意外修改自身指令。
  • 可共享:多个进程可以共享同一份代码(如动态链接库)。
  • 固定大小:在程序编译时确定,运行时不变。

(2)全局/静态存储区 (Data Segment),存储全局变量和静态变量:

  • 初始化数据段 (.data),存储已初始化的全局变量和静态变量。
  • 未初始化数据段 (.bss),存储未初始化或初始化为0的全局变量和静态变量。BSS段在可执行文件中不占用空间,只记录大小,程序加载时由操作系统自动清零。

(3)栈区 (Stack),用于存储函数调用时的局部变量、函数参数、返回地址等信息:

  • 自动管理:变量离开作用域时自动释放,无需手动管理。

  • 有限大小 :通常为几MB(Linux默认8MB),可通过ulimit -s查看。

  • 生长方向:通常从高地址向低地址生长。

  • 速度快:仅需移动栈指针即可分配/释放内存。

(4)堆区用于程序运行时的动态内存分配,由程序员通过malloc/calloc/realloc(C)或new(C++)显式分配:

  • 手动管理:必须显式释放,否则造成内存泄漏。

  • 灵活大小:可以在运行时决定分配多少内存。

  • 生长方向:通常从低地址向高地址生长。

  • 速度较慢:涉及复杂的内存管理算法。

  • 碎片化:频繁分配释放可能导致内存碎片。

(5)常量区存储程序中的常量数据,主要包括字符串字面量和const修饰的全局常量:

  • 只读:试图修改会导致段错误。

  • 可共享:相同的字符串字面量可能共享同一份存储。

  • 生命周期:整个程序运行期间。

典型的32位Linux进程内存布局(从低地址到高地址):

c 复制代码
高地址
+------------------+
|   内核空间       |  (不可访问)
+------------------+
|   栈区 (↓)      |  (向下生长)
|                  |
|   ↓              |
|                  |
|   ...            |
|                  |
|   ↑              |
|                  |
|   堆区 (↑)      |  (向上生长)
+------------------+
|   .bss段        |  (未初始化数据)
+------------------+
|   .data段       |  (已初始化数据)
+------------------+
|   常量区         |  (只读数据)
+------------------+
|   代码区         |  (只读代码)
+------------------+
低地址
3. 指针与引用的区别

指针和引用在 C++ 中都用于间接访问变量,但它们有一些区别。

  • 指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。
  • 指针(除指针常量)可以被重新赋值,指向不同的变量;引用在初始化后不能更改,始终指向同一个变量。
  • 指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量,不能为 nullptr。
  • 使用指针需要对其进行解引用以获取或修改其指向的变量的值;引用可以直接使用,无需解引用。

从底层实现看,引用通常被C++编译器当做const指针来操作,但在语法层面,引用更像是原变量的透明别名。

特性 指针 引用
本质 存储地址的变量 变量的别名
可否重新赋值 可以(非const指针) 不可以
可否为空 可以(nullptr 不可以
初始化要求 可以不初始化 必须初始化
使用语法 需要*解引用 直接使用
自身占用内存 是(有地址) 否(语义上)
底层实现 直接表示 通常为const指针
4. 指针传递、值传递、引用传递

值传递(Pass by Value)是将实参的副本传递给形参。函数接收的是实参值的一个拷贝,二者在内存中占据不同的存储空间:

  • 形参是实参的独立副本。
  • 函数内对形参的修改不影响实参。
  • 适用于基本数据类型和小型对象。

引用传递(Pass by Reference)是将实参的引用(别名)传递给形参。引用本质上是实参的另一个名字,与实参共享同一块内存地址:

  • 形参是实参的别名,二者指向同一内存地址。

  • 函数内对形参的修改直接作用于实参。

  • C++特有机制(C语言不支持引用)。

  • 语法简洁,无需解引用操作。

指针传递(Pass by Pointer)实际上是值传递的特殊形式,传递的值是实参的地址。虽然指针本身是按值传递的,但通过解引用可以访问和修改实参:

  • 形参接收实参的地址值(指针的副本)。
  • 通过解引用可以修改实参指向的数据。
  • C/C++都支持。
  • 需要显式的取地址(&)和解引用(*)操作。

三种参数传递方式的对比:

传递方式 能否修改实参 语法复杂度 适用场景
值传递 简单 小型数据、不需修改实参
引用传递 简单 需修改实参、大型对象
指针传递 是* 中等 C语言、需要空指针判断
5. 资源获取即初始化

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种关键的资源管理技术,其核心思想是:将资源的生命周期与对象的生命周期绑定

  • 在对象构造时获取资源。

  • 在对象析构时释放资源。

  • 利用C++的自动对象生命周期管理机制,确保资源安全释放。

RAII适用于所有需要显式管理的有限资源:

  • 内存资源:堆内存分配。
  • 文件资源:文件句柄、文件流。
  • 网络资源:套接字连接。
  • 同步资源:互斥锁、信号量。
  • 系统资源:线程、数据库连接、磁盘空间。

下面是使用 RAII 技术来管理文件句柄的代码示例:

c++ 复制代码
class FileHandler {
private:
    FILE* file;
    
public:
    // 构造函数:获取资源
    FileHandler(const char* filename, const char* mode) {
        file = fopen(filename, mode);
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    // 析构函数:释放资源
    ~FileHandler() {
        if (file) {
            fclose(file);
        }
    }
    
    // 禁止拷贝(避免双重释放)
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
    
    FILE* get() { return file; }
};

// 使用示例
void processFile() {
    FileHandler fh("data.txt", "r");  // 自动获取资源
    // 使用文件...
    // 无需手动关闭,离开作用域时自动释放
}

RAII确保资源按照获取的相反顺序释放。

RAII的关键优势在于自动处理异常情况,即使发生异常也能正确释放资源(异常栈回溯时C++会自动销毁当前作用域内的所有局部对象)。

6. malloc-free 内存分配原理

mallocfree 是 C 语言中用于动态内存分配和释放内存的两个函数。它们是 C 语言标准库的一部分,用于在程序运行期间请求和释放堆内存。

malloc 根据申请内存的大小采用不同的分配策略:

  • 小块内存分配(< 128KB,典型阈值为 MMAP_THRESHOLD),使用 brk()/sbrk() 系统调用扩展进程的堆区(data segment),通过移动程序间断点(program break)来增加堆的大小,分配的内存来自连续的堆空间。
  • 大块内存分配(≥ 128KB),使用 mmap() 系统调用创建匿名内存映射,直接从虚拟地址空间映射独立的内存区域,每次分配独立的内存段,不影响堆区。

malloc 维护一个内存池(arena),通过空闲链表(free list)管理已分配和空闲的内存块:

  • 搜索空闲链表,寻找合适大小的空闲块(首次适配、最佳适配等策略)。
  • 如果找到足够大的块,分割并返回。
  • 如果没有合适的块,通过 brk()mmap() 向系统申请新内存。

为什么不全部使用 mmap?

  • 系统调用开销,每次 mmap 都是系统调用,涉及用户态到内核态的切换(约 100-1000 CPU 周期)、页表的创建和管理、VMA(虚拟内存区域)结构的维护,对于频繁的小内存分配,开销不可接受。

  • 虚拟地址空间碎片,每次 mmap 创建独立的 VMA,大量小块分配会导致虚拟地址空间碎片化,页表项数量激增,内核 VMA 管理结构占用过多内存。

  • 最小分配单位限制,mmap 以页为单位分配(通常 4KB),小于一页的请求也会占用整页,浪费严重。

为什么不全部使用 brk?

  • 大块内存释放困难,brk 分配的内存位于堆的连续区域,只能通过降低 program break 来释放。如果堆顶部的内存未释放,下方的空闲内存无法归还系统。
  • 内存归还效率低,通过 brk 降低堆顶需要满足严格条件,实际中很多内存长期无法归还,导致进程虚拟内存占用持续增长。
  • 大块内存的独立管理需求,大块内存通常生命周期独立,使用 mmap 可以释放时立即归还系统(通过 munmap),避免影响堆区的碎片状态,实现更精确的内存控制。

free 如何确定释放空间大小?

malloc 在返回给用户的指针之前存储元数据,记录块的大小信息。

7. malloc和new的区别

malloc/free 是 C 语言标准库函数,定义在 <stdlib.h> 中,纯粹的内存分配工具,不感知对象类型,函数调用开销,可被替换实现。

new/delete 是 C++ 语言内置运算符(operator),面向对象的内存管理机制,编译器内建支持,可重载但不可替换其核心语义。

malloc从堆(heap)分配,堆是操作系统层面的概念,通过 brk()/mmap() 等系统调用管理。

new从自由存储区(free store)分配,自由存储区是 C++ 的抽象概念,默认实现可能使用堆,但不强制要求,可通过重载 operator new 改变分配策略(如内存池、栈分配等)。

malloc仅分配原始内存,需要手动计算大小,返回 void *,需要强制转换类型。

new分配内存 + 构造对象,编译器自动计算大小,返回强类型指针(类型安全)。

malloc在分配内存失败时,返回 NULL,需要手动判断进行处理。

new在分配内存失败时,抛出 std::bad_alloc 异常,无需进行 nullptr 判断。

在 C++ 中应优先使用 new/delete(或更好的智能指针),它们提供了类型安全、自动对象管理和异常安全等关键特性。仅在与 C 代码交互或特定优化场景下才考虑 malloc

特性 malloc new
类型 C 函数 C++ 运算符
内存来源 堆(heap) 自由存储区(free store)
返回类型 void* 对象类型指针
大小指定 手动计算 编译器自动
构造函数 不调用 自动调用
析构函数 不调用 delete 时调用
失败处理 返回 NULL 抛出 bad_alloc
可重载 否(仅可替换实现) 是(支持多种重载)
数组释放 free(p) delete[] p
类型安全 弱(需转换) 强(编译期检查)
8. 内存泄漏/野指针/空悬指针

内存泄漏(Memory Leak),是指程序动态分配的内存在不再需要时未被释放,导致该内存无法被重新使用。随着程序运行,可用内存逐渐减少,最终可能耗尽系统资源。

野指针,是未初始化的指针,其值是随机的,指向未知的内存地址。可能在声明时未初始化,或者指针变量本身存储在栈上的随机数据。

空悬指针,是指向已释放内存的指针。该内存地址曾经有效,但现在指向的区域可能被回收、重新分配或包含无效数据。

C++ 标准(C++11 §5.3.5/2)明确规定:删除空指针是合法且无操作的

四种指针问题综合对比:

类型 指针状态 产生原因 典型症状 检测难度
野指针 未初始化 声明未赋值 随机崩溃或数据损坏 中(可能偶然指向有效内存)
空悬指针 指向已释放内存 释放后未置空 延迟崩溃或数据损坏 高(内存可能暂时有效)
空指针 nullptr/NULL 显式赋值 解引用时立即崩溃 低(行为确定)
内存泄漏 内存未释放 忘记 delete 内存耗尽 高(需工具检测)
相关推荐
2301_765715142 小时前
C语言轮子制造
c语言·开发语言·制造
量子炒饭大师2 小时前
【C++入门】Cyber骇客的同名异梦——【C++重载函数】(与C的函数差异)
c语言·开发语言·c++·函数重载
charlie1145141912 小时前
现代嵌入式C++教程:if constexpr——把编译期分支写得像写注释 —— 工程味实战指南
开发语言·c++·笔记·学习·嵌入式·现代c++
LIZhang20162 小时前
c++ 转化句柄,解决多线程安全释放问题
开发语言·c++
youqingyike2 小时前
Qt 中 QWidget 调用setLayout 后不显示
开发语言·c++·qt
_OP_CHEN3 小时前
【从零开始的Qt开发指南】(二十二)Qt 音视频开发宝典:从音频播放到视频播放器的实战全攻略
开发语言·c++·qt·音视频·前端开发·客户端开发·gui开发
oioihoii3 小时前
从C++到C#的转型完全指南
开发语言·c++·c#
学嵌入式的小杨同学3 小时前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
数据结构·c++·算法·unity·游戏引擎·代理模式
小欣加油3 小时前
leetcode 174 地下城游戏
c++·算法·leetcode·职场和发展·动态规划