Skip to content

C++ 内存管理

从硬件角度来看,计算机中存储的每个值都占用一定的物理内存。C++ 语言将这样的一块内存称为对象 (Object)。需要注意的是,面向对象编程中的对象通常指的是类的实例。

在 C++ 中,对象可以存储一个或多个值。一个对象可能并未存储实际的值,但它在存储适当的值时一定具有相应的大小。

程序可用的内存通常被划分为三个主要部分,用于不同类型的变量:

  • 静态存储区 (Static Memory): 用于存储具有外部链接、内部链接和无链接的静态变量。
  • 栈内存 (Stack Memory): 用于存储自动变量。自动存储期。
  • 堆内存 (Heap Memory): 用于动态内存分配。动态分配存储期。

一个对象存储位置有3中 占内存 堆内存 操作系统内核区 而文件就处于操作系统内核区 栈区 不需要关心回收问题 会自动回收 堆区 我们也有办法做自动回收 毕竟我们可以做引用计数 引用计数归零就回收 操作系统内核区 内核区的东西就真没办法 就只能手动释放

静态存储区

  • 用途: 用于存储全局变量、静态局部变量以及字符串字面量和 const 修饰的全局变量。
  • 生命周期: 静态存储类别所用的内存数量在编译时确定。只要程序还在运行,就可以访问存储在该部分的数据。该类型的变量在程序开始执行时被创建,在程序结束时被销毁。
  • 链接属性与存储期:
    • 对于文件作用域(全局)的变量,static 关键字表明了其链接属性 (Linkage),而不是存储期。
    • static 声明的文件作用域变量具有内部链接,意味着它只能在定义它的源文件中访问。
    • 无论是否使用 static 修饰,文件作用域的变量都具有静态存储期
    • 在函数内部使用 static 声明的局部变量也具有静态存储期,但其作用域仍然是块作用域(仅在函数内部可见),且拥有无链接。它们在程序开始执行时被创建,但在函数第一次被调用时初始化,并在程序结束时销毁。

栈内存

  • 用途: 用于存储函数内部声明的局部变量(自动变量)以及函数参数。
  • 生命周期: 自动存储类型的变量在进入声明它们的代码块时存在,离开该代码块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。
  • 管理方式: 这部分的内存通常作为栈 (Stack) 来处理。这意味着新创建的变量按顺序加入内存(压栈),以相反的顺序销毁(弹栈)。
  • 优点: 分配和释放内存非常快速和高效。
  • 缺点: 栈的大小通常是有限的,不适合存储大型数据结构。

堆内存

  • 用途: 用于在程序运行时动态地分配和释放内存。这允许程序在需要时分配内存,并在不再需要时释放内存,从而更灵活地管理内存。
  • 生命周期: 动态分配的内存在调用 malloc(C 风格)或 new(C++ 风格)等函数时存在,直到显式地调用 free(C 风格)或 delete(C++ 风格)来释放。
  • 管理方式: 这部分内存完全由程序员管理。
  • 灵活性: 内存块可以在一个函数中创建,并在另一个函数中销毁。
  • 速度: 通常使用动态内存会比使用栈内内存慢,因为涉及到查找合适的内存块和维护内存分配信息。

动态内存分配

C 风格

  • malloc: 分配指定大小的内存块,返回一个指向该内存块的 void* 指针。分配的内存不会被初始化。
  • calloc: 也分配指定大小的内存块,但会将分配的内存全部初始化为零。它接受两个参数:要分配的元素个数和每个元素的大小。返回一个指向该内存块的 void* 指针。
  • 与变长数组 (VLA) 的关系: malloccalloc 可以用于在运行时分配大小在编译时未知的数组,这与 C99 引入的变长数组的概念类似。
  • 释放内存: 使用 free() 函数释放由 malloccalloc 分配的内存。
cpp
#include <cstdlib> // 包含 malloc 和 free 的头文件
#include <iostream>

void fun(int n) {
    double *temp = (double*) malloc(n * sizeof(double)); // 分配 n 个 double 大小的内存块

    if (temp == nullptr) {
        std::cerr << "内存分配失败!" << std::endl;
        return;
    }

    // long *newmem = (long *) calloc(100, sizeof(long)); // 分配 100 个 long 大小的内存块并初始化为 0

    // 使用分配的内存...

    free(temp); // 释放之前分配的内存
    // free(newmem);
}

C++ 风格

  • new: 用于在堆上分配内存,并返回一个指向所分配内存的指定类型的指针。它可以用于分配单个对象或对象数组。
  • delete: 用于释放通过 new 分配的单个对象的内存。
  • delete[]: 用于释放通过 new[] 分配的对象数组的内存。

分配单个对象的内存:

普通变量:

cpp
#include <iostream>

// int* pi = new int(); // 不初始化,内存中的值是不确定的
int* pi = new int(100); // 分配一个 int 大小的内存并初始化为 100
std::cout << *pi << std::endl;
delete pi; // 释放内存
pi = nullptr; // 建议将指针置为 nullptr,防止悬空指针

类对象: 使用 new 分配类对象的内存时,会自动调用该类的构造函数 (Constructor)。如果类没有默认构造函数(即没有不带参数的构造函数,或所有构造函数都有参数),则在不提供参数的情况下使用 new 会导致编译错误。

cpp
#include <iostream>
#include <string>

class MyString {
public:
    MyString() : str("") { std::cout << "默认构造函数被调用" << std::endl; }
    MyString(const char* s) : str(s) { std::cout << "带参数的构造函数被调用" << std::endl; }
    ~MyString() { std::cout << "析构函数被调用" << std::endl; }
private:
    std::string str;
};

int main() {
    // MyString* pString1 = new MyString(); // 调用默认构造函数
    MyString* pString2 = new MyString("hello world"); // 调用带参数的构造函数
    // MyString* pString3 = new MyString; // 如果没有默认构造函数,这行会报错

    delete pString2;
    // delete pString1;
    return 0;
}

分配对象数组的内存:

普通变量: 可以使用 () 在分配数组时将所有元素初始化为 0。如果不使用 (),则数组元素不会被默认初始化(其值是不确定的)。

cpp
#include <iostream>

// int* pArr1 = new int[100]; // 不初始化,数组元素的值是不确定的
int* pArr2 = new int[100](); // 将数组的所有元素初始化为 0
std::cout << pArr2[20] << std::endl;
delete[] pArr2; // 使用 delete[] 释放数组内存
pArr2 = nullptr;

类对象: 使用 new[] 分配类对象数组时,会为数组中的每个对象调用该类的默认构造函数。如果类没有默认构造函数,则会发生编译错误。

cpp
#include <iostream>
#include <string>

int main() {
    std::string* pStringArray = new std::string[100](); // 为数组中的每个 string 对象调用默认构造函数(初始化为空字符串)
    std::cout << pStringArray[20] << std::endl;
    delete[] pStringArray;
    pStringArray = nullptr;
    return 0;
}

会报错的示例:

cpp
#include <iostream>

class Test {
public:
    Test(int i) : i(i) { std::cout << "带参数的构造函数被调用,i = " << i << std::endl; }
private:
    int i;
};

int main() {
    Test* pTest1 = new Test[100](); // 错误:Test 类没有默认构造函数,无法对数组元素进行初始化
    Test* pTest2 = new Test[100];  // 错误:Test 类没有默认构造函数,无法对数组元素进行初始化

    // 正确的做法是如果需要动态分配 Test 对象的数组,需要提供参数,或者 Test 类需要有默认构造函数
    Test* pTestArray = new Test[100]{ {1}, {2}, {100} };// 需要提供初始化列表,但当数量很大时不方便
}

或者为 Test 类添加一个默认构造函数:

cpp
    
class Test {
public:
    Test() : i(0) { std::cout << "默认构造函数被调用" << std::endl; }
    Test(int i) : i(i) { std::cout << "带参数的构造函数被调用,i = " << i << std::endl; }
private:
    int i;
};
int main() {
    Test* pTestArray = new Test[100](); // 现在可以正常工作

    delete[] pTest; // 注意:由于上面的 new 操作会报错,这里的 delete 实际上不会执行
    return 0;
}

内存泄漏

如果使用 malloccalloc 分配的内存没有被 free 释放,或者使用 new 分配的内存没有被 deletedelete[] 释放,那么这块内存将无法被程序再次使用,导致内存泄漏。随着时间的推移,大量的内存泄漏会导致程序耗尽所有可用内存并崩溃。

内存泄漏的严重性:

内存泄漏会导致程序的堆内存逐渐被占用,最终可能导致内存耗尽,程序崩溃。

这在长期运行的程序或服务器端应用中尤其危险。

内存泄漏通常在开发和测试阶段难以完全发现,可能在上线运行一段时间后才暴露出来,给问题排查带来很大的困难,并可能造成严重的经济损失。

总结:

内存泄漏是最严重的错误之一。程序不怕报错,就怕一开始运行良好,但随着时间的推移,由于资源耗尽而出现难以预料的错误。

在 C++ 中,正确地管理动态分配的内存(使用 new 分配的内存必须使用 deletedelete[] 释放)至关重要。

推荐使用智能指针(如 std::unique_ptrstd::shared_ptr)来自动管理动态分配的内存,以减少内存泄漏的风险。