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) 的关系:
malloc和calloc可以用于在运行时分配大小在编译时未知的数组,这与 C99 引入的变长数组的概念类似。 - 释放内存: 使用
free()函数释放由malloc或calloc分配的内存。
#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[]分配的对象数组的内存。
分配单个对象的内存:
普通变量:
#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 会导致编译错误。
#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。如果不使用 (),则数组元素不会被默认初始化(其值是不确定的)。
#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[] 分配类对象数组时,会为数组中的每个对象调用该类的默认构造函数。如果类没有默认构造函数,则会发生编译错误。
#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;
}会报错的示例:
#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 类添加一个默认构造函数:
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;
}内存泄漏
如果使用 malloc 或 calloc 分配的内存没有被 free 释放,或者使用 new 分配的内存没有被 delete 或 delete[] 释放,那么这块内存将无法被程序再次使用,导致内存泄漏。随着时间的推移,大量的内存泄漏会导致程序耗尽所有可用内存并崩溃。
内存泄漏的严重性:
内存泄漏会导致程序的堆内存逐渐被占用,最终可能导致内存耗尽,程序崩溃。
这在长期运行的程序或服务器端应用中尤其危险。
内存泄漏通常在开发和测试阶段难以完全发现,可能在上线运行一段时间后才暴露出来,给问题排查带来很大的困难,并可能造成严重的经济损失。
总结:
内存泄漏是最严重的错误之一。程序不怕报错,就怕一开始运行良好,但随着时间的推移,由于资源耗尽而出现难以预料的错误。
在 C++ 中,正确地管理动态分配的内存(使用 new 分配的内存必须使用 delete 或 delete[] 释放)至关重要。
推荐使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理动态分配的内存,以减少内存泄漏的风险。
