C++从入门到实战(十一)详细讲解C/C++语言中内存分布与C与C++内存管理对比

C++从入门到实战(十一)详细讲解C/C++语言中内存分布与C与C++内存管理对比


前言

  • 在之前的博客系列中,我们深入探讨了C++的第一个重要阶段------类和对象,以及与之相关的诸多核心内容,包括四大构造函数、类的种类、内部类、匿名类、友元等
  • 这些知识点如同坚实的基石,为后续的C++学习构建了稳固的基础,帮助我们逐步建立起对C++面向对象编程的深刻理解。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

  • 接下来,我们将开启新的篇章,深入剖析C/C++语言中的内存分布,并对比C与C++在内存管理方面的差异。

一、C/C++语言中内存分布

1.内核空间

  • 内核空间(Kernel Space):相当于 "管理层办公室",存放着操作系统的核心代码(比如管理 CPU、内存、硬盘的程序)。只有 "管理层"(内核程序)能进去,普通程序不能随便访问,权限很高,负责处理最底层的硬件资源。

2.栈

  • 栈是一种后进先出(LIFO)的数据结构,用于存储局部变量、函数调用信息等。
  • 每当调用一个函数时,系统会在栈上为该函数的局部变量分配内存,函数执行完毕后,这些内存会被自动释放。

想象你去食堂打饭,餐盘是按顺序叠起来的:后放上去的餐盘先被拿走(后进先出)。

  • 栈就是这样一个 "自动清理的临时货架",存放函数运行时的 临时数据(比如局部变量、函数参数、返回地址)。程序会自动分配和释放空间,不需要你手动管理。
cpp 复制代码
// 函数,函数代码存储在代码段
void func() {
    // 局部变量,存储在栈上
    int local_variable = 30;
    std::cout << "Local variable: " << local_variable << std::endl;
}

 // 调用函数,函数调用信息和局部变量会在栈上操作
 int main()
 {
    func();
 }

3.堆

堆是用于动态分配内存的区域,程序可以在运行时从堆中请求和释放内存

想象你去超市存包,储物柜可以按需申请:你觉得需要多大空间,就租多大的柜子,用完自己关门(释放),不关门的话柜子会一直被占着(内存泄漏)

  • 堆就是这样一个 "动态分配的内存区域",程序运行时可以主动申请(如 C 语言的 malloc、Python 的 list 扩容)和释放(free),空间大小不固定,灵活但需要自己管理
c 复制代码
 // 使用 malloc 分配内存
    int *ptr_malloc = (int *)malloc(5 * sizeof(int));
 // 使用 calloc 分配内存
    int *ptr_calloc = (int *)calloc(5, sizeof(int));
 // 使用 realloc 调整内存大小
    ptr_malloc = (int *)realloc(ptr_malloc, 10 * sizeof(int));
 // 释放内存
    free(ptr_malloc);
    free(ptr_calloc);
  • 上面的代码均在堆上执行

4.数据段

  • 数据段用于存储全局变量和静态变量
  • 全局变量在程序的整个生命周期内都存在,并且可以被程序的任何部分访问。
  • 数据段的代码程序运行时一直存在,直到程序结束才释放
cpp 复制代码
// 全局变量,存储在数据段
int global_variable = 10;

// 静态变量,存储在数据段
static int static_variable = 20;

void func() {
    static int static_age = 18;  // 静态局部变量,也存在数据段
}

5.代码段

  • 代码段存放程序的可执行代码(二进制指令) ,比如我们写的 if、for、函数逻辑等,运行时只能被执行,不能被修改(防止程序运行时代码被破坏)

二、例题带练巩固C/C++语言中内存分布的知识

  • 学习完上面的知识,我们用一道典型的企业面试常考例题来巩固一下我们刚刚学到的知识

我们来看下面的一段代码和相关问题

cpp 复制代码
int globalVar = 1;
 static int staticGlobalVar = 1;
 void Test()
 {
 static int staticVar = 1;
 int localVar = 1;
 int num1[10] = { 1, 2, 3, 4 };
 char char2[] = "abcd";
 const char* pChar3 = "abcd";
 int* ptr1 = (int*)malloc(sizeof(int) * 4);
 int* ptr2 = (int*)calloc(4, sizeof(int));
 int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
 free(ptr1);
 free(ptr3);
 }
  • 题目

题目讲解

cpp 复制代码
int globalVar = 1;
 static int staticGlobalVar = 1;
 void Test()
 {
 static int staticVar = 1;
 int localVar = 1;
 int num1[10] = { 1, 2, 3, 4 };
 char char2[] = "abcd";
 const char* pChar3 = "abcd";
 int* ptr1 = (int*)malloc(sizeof(int) * 4);
 int* ptr2 = (int*)calloc(4, sizeof(int));
 int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
 free(ptr1);
 free(ptr3);
 }
  • 结合我们刚刚讲的知识和例子和知识

  • globalVar:全局变量,存放在 数据段(静态区)数据段用于存放全局变量和静态变量。

  • staticGlobalVar:静态全局变量,也在 数据段(静态区)。静态全局变量本质还是全局变量,只是作用域受限。

  • staticVar:静态局部变量,存于 数据段(静态区)。静态局部变量生命周期长,存储在数据段

  • localVar:局部变量,存于 栈 中。栈用于存放局部变量,由系统自动分配释放。

  • num1:局部数组,也存于 栈 中,数组作为局部变量,空间在栈中分配。

  • char2:字符数组(局部),存于 栈。数组本身是局部变量,元素空间在栈中。

  • *char2:char2 是栈中的数组,*char2 访问栈中数组元素,存于 栈。

  • pChar3:指针变量(局部),存于 栈。指针本身作为局部变量,空间在栈。

  • *pChar3:pChar3 指向字符串常量 "abcd",字符串常量存于 代码段(常量区)。代码段存放常量

  • ptr1:指针变量(局部),存于 栈。指针本身是局部变量,在栈中。

  • *ptr1:ptr1 指向 malloc 分配的内存,malloc 分配的内存来自 堆。堆用于动态内存分配。

题目答案

  • 结合我们刚刚的知识。想必大家已经搞定了刚刚的例题,下面我们将例题的答案放出来

三、C语言动态内存分配(知识回顾)

  • 在正式开始C++的new与delete之前,我们需要回顾并补充一些知识C语言的知识,以便我们后续的理解

3.1 为什么需要动态内存分配

在许多场景下,我们在编写代码时可能并不清楚程序运行时到底需要多少内存。

  • 例如,你要存储用户输入的一串字符,可用户输入的字符数量并不固定。
  • 这种情况下,动态内存分配就能派上用场,它可以让程序在运行时根据实际需求来分配内存。

C语言提供了几个用于动态内存分配的函数,这些函数都在 <stdlib.h> 头文件中。

3.2 malloc 函数

malloc 函数用于分配指定大小的内存块,其原型如下:

c 复制代码
void* malloc(size_t size);

size 表示要分配的内存字节数,若分配成功,函数会返回一个指向该内存块起始位置的指针;若分配失败,则返回 NULL

下面是一个简单的示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 分配一个能存储 5 个整数的内存块
    int *ptr = (int *)malloc(5 * sizeof(int));

    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 给分配的内存块赋值
    for (int i = 0; i < 5; i++) {
        ptr[i] = i;
    }

    // 打印内存块中的值
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放分配的内存
    free(ptr);

    return 0;
}

3.3 calloc 函数

calloc 函数和 malloc 类似,不过它会把分配的内存初始化为 0。其原型如下:

c 复制代码
void* calloc(size_t num, size_t size);

num 代表要分配的元素数量,size 表示每个元素的大小。

以下是使用 calloc 的示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 分配一个能存储 5 个整数的内存块,并初始化为 0
    int *ptr = (int *)calloc(5, sizeof(int));

    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 打印内存块中的值
    for (int i = 0; i < 5; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放分配的内存
    free(ptr);

    return 0;
}

3.4 realloc 函数

realloc 函数用于调整已分配内存块的大小。其原型如下:

c 复制代码
void* realloc(void* ptr, size_t size);

ptr 是指向已分配内存块的指针,size 是新的内存块大小。若分配成功,函数会返回一个指向新内存块起始位置的指针;若失败,则返回 NULL

下面是使用 realloc 的示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 分配一个能存储 5 个整数的内存块
    int *ptr = (int *)malloc(5 * sizeof(int));

    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 给分配的内存块赋值
    for (int i = 0; i < 5; i++) {
        ptr[i] = i;
    }

    // 调整内存块大小,使其能存储 10 个整数
    ptr = (int *)realloc(ptr, 10 * sizeof(int));

    if (ptr == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }

    // 给新分配的内存块赋值
    for (int i = 5; i < 10; i++) {
        ptr[i] = i;
    }

    // 打印内存块中的值
    for (int i = 0; i < 10; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放分配的内存
    free(ptr);

    return 0;
}

3.5 free 函数

free 函数用于释放之前动态分配的内存。其原型如下:

c 复制代码
void free(void* ptr);

ptr 是指向要释放的内存块的指针。一旦内存被释放,就不能再使用该指针访问这块内存了。

3.6 C语言动态内存分配的缺点

  • 在我们刚刚的代码中,我们发现
  • 在 C 语言中,使用malloc、calloc和realloc函数进行动态内存分配时,返回的是void*类型的指针。
  • 这就要求程序员手动将其转换为所需的指针类型,若转换出错,就可能在运行时引发难以调试的错误
  • 而且,C 语言主要是面向过程的语言,动态分配的内存只是简单的字节块,无法自动调用对象的构造函数和析构函数。
  • 在处理复杂的数据类型(如类对象)时,就需要手动管理对象的初始化和清理工作,容易出错
  • C 语言没有内置的异常处理机制,在内存分配失败时,通常只能通过返回NULL指针来表示错误,程序员需要手动检查返回值并进行相应处理,代码会变得繁琐

因此C++ 引入了new和delete运算符来进行动态内存分配和释放,它们能很好地解决 C 语言动态内存分配的部分问题

四、C++动态内存分配

1. new 和 delete

C++ 引入了new和delete运算符来进行动态内存分配和释放,它们能很好地解决 C 语言动态内存分配的部分问题

  • new 运算符:用于动态分配内存,同时会自动调用对象的构造函数。
  • delete 运算符:用于释放动态分配的内存,同时会自动调用对象的析构函数
cpp 复制代码
#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "构造函数被调用" << std::endl;
    }
    ~MyClass() {
        std::cout << "析构函数被调用" << std::endl;
    }
};

int main() {
    // 使用new分配内存并创建对象
    MyClass *obj = new MyClass();

    // 使用对象
    // ...

    // 使用delete释放内存
    delete obj;

    return 0;
}

2. new[] 和 delete[] 运算符

  • 若要动态分配数组,可使用new[]和delete[]运算符
cpp 复制代码
#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "构造函数被调用" << std::endl;
    }
    ~MyClass() {
        std::cout << "析构函数被调用" << std::endl;
    }
    
    void printMessage() {
        std::cout << "这是 MyClass 类对象的消息。" << std::endl;
    }
};

int main() {
    // 使用new[]分配数组内存
    MyClass* arr = new MyClass[3];

    // 使用数组,遍历数组并调用成员函数
    for (int i = 0; i < 3; ++i) {
        arr[i].printMessage();
    }

    // 使用delete[]释放数组内存
    delete[] arr;

    return 0;
}eturn 0;
}

五、C与C++内存管理对比

5.1 C语言内存管理

C语言主要通过标准库函数来进行内存管理,核心函数有 malloccallocreallocfree。下面是这些函数的详细介绍:

  • malloc :用来分配指定大小的内存块,返回的是 void* 类型指针,需要手动进行类型转换。分配的内存内容是未初始化的。
  • calloc :功能和 malloc 类似,不过它会把分配的内存初始化为 0。
  • realloc:用于调整已经分配的内存块大小,可以扩大或缩小。
  • free:释放之前动态分配的内存,释放后该内存可被系统重新使用。

5.2 C++内存管理

C++ 除了可以使用 C 语言的内存管理函数,还引入了 newdelete 运算符来进行动态内存管理。

  • new :用于动态分配内存,会自动调用对象的构造函数。对于单个对象使用 new,对于数组使用 new[]
  • delete :用于释放 new 分配的内存,会自动调用对象的析构函数。对应 new 使用 delete,对应 new[] 使用 delete[]

5.3 C 与 C++ 内存管理对比表格

对比项 C 语言 C++
内存分配函数/运算符 malloccallocrealloc newnew[]
内存释放函数/运算符 free deletedelete[]
类型安全性 返回 void* 指针,需要手动类型转换,类型安全性低 直接返回正确类型的指针,无需手动转换,类型安全性高
对象构造和析构 不支持自动调用构造和析构函数,需要手动管理 自动调用构造和析构函数,简化对象生命周期管理
异常处理 内存分配失败返回 NULL,需手动检查 内存分配失败抛出 std::bad_alloc 异常,可使用 try-catch 处理
代码风格 面向过程,使用函数进行内存管理 面向对象,使用运算符进行内存管理,与类和对象结合紧密
  • C 语言的内存管理方式更偏向于底层和过程化,需要程序员手动处理很多细节,容易出错。
  • 而 C++ 的内存管理方式在类型安全性、对象生命周期管理和异常处理方面有很大改进,更适合开发大型、复杂的面向对象程序。
  • 不过,在一些性能敏感或者需要与 C 代码兼容的场景中,C 语言的内存管理方式仍然很有用。

以上就是这篇博客的全部内容,下一篇我们将继续探索C++中new和delete中更多精彩内容。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |

相关推荐
OG one.Z5 分钟前
文件读取操作
c++·学习·文件读取
fengchengwu201218 分钟前
归并排序算法
数据结构·算法·排序算法
彷徨而立23 分钟前
【C++】频繁分配和释放会产生内存碎片
开发语言·c++
Bt年41 分钟前
第十六届蓝桥杯 C/C++ B组 题解
c语言·c++·蓝桥杯
jiunian_cn2 小时前
【c++】【STL】list详解
数据结构·c++·windows·list·visual studio
虾球xz2 小时前
游戏引擎学习第250天:# 清理DEBUG GUID
c++·学习·游戏引擎
我命由我123453 小时前
STM32 开发 - stm32f10x.h 头文件(内存映射、寄存器结构体与宏、寄存器位定义、实现点灯案例)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
xyd陈宇阳3 小时前
嵌入式开发高频面试题全解析:从基础编程到内存操作核心知识点实战
c语言·数据结构·stm32·算法·面试
淋过很多场雨4 小时前
现代c++获取linux所有的网络接口名称
java·linux·c++
刘 大 望4 小时前
Java写数据结构:队列
java·数据结构·intellij-idea