彻底摘明白 C++ 的动态内存分配原理
【摘要】 理解 C++的动态内存分配原理,以及掌握如何避免动态内存分配导致的内存泄漏?是在开发中非常关键的知识,这篇文章希望可以帮助到你,关注威哥爱编程,全栈开发就你行。
大家好,我是 V 哥。在C++中,动态内存分配允许程序在运行时请求和释放内存,这对于处理不确定大小的数据或者在程序执行过程中动态调整数据结构非常有用。C++主要通过new
和delete
运算符(用于对象)以及malloc
、calloc
、realloc
和free
函数(继承自C语言)来实现动态内存分配,下面详细介绍它们的原理。先赞再看后评论,腰缠万贯财进门
。
1. new
和delete
运算符
原理概述
new
运算符用于在堆上分配内存并调用对象的构造函数进行初始化,delete
运算符用于释放new
分配的内存并调用对象的析构函数。
详细步骤
new
运算符
- 内存分配:
new
运算符首先调用operator new
函数来分配所需大小的内存块。operator new
是一个全局函数,它通常会调用操作系统提供的内存分配函数(如malloc
)来获取内存。 - 构造函数调用:如果分配内存成功,
new
运算符会调用对象的构造函数对分配的内存进行初始化。
示例代码:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
// 使用new分配内存并调用构造函数
MyClass* obj = new MyClass();
// 使用delete释放内存并调用析构函数
delete obj;
return 0;
}
delete
运算符
- 析构函数调用:
delete
运算符首先调用对象的析构函数,用于清理对象占用的资源(如关闭文件、释放动态分配的子对象等)。 - 内存释放:调用
operator delete
函数释放之前分配的内存。operator delete
通常会调用操作系统提供的内存释放函数(如free
)。
数组的动态分配
使用new[]
和delete[]
可以动态分配和释放数组。new[]
会为数组中的每个元素调用构造函数,delete[]
会为数组中的每个元素调用析构函数。
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructor called" << std::endl;
}
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
// 使用new[]分配数组内存并调用构造函数
MyClass* arr = new MyClass[3];
// 使用delete[]释放数组内存并调用析构函数
delete[] arr;
return 0;
}
2. malloc
、calloc
、realloc
和free
函数
原理概述
这些函数是C语言标准库提供的动态内存分配函数,C++为了兼容C语言也支持这些函数。它们不涉及对象的构造和析构,只是简单地分配和释放内存。
详细介绍
malloc
函数
malloc
函数用于分配指定大小的内存块,返回一个指向该内存块的指针。如果分配失败,返回NULL
。
#include <iostream>
#include <cstdlib>
int main() {
// 分配10个int类型的内存空间
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr != NULL) {
// 使用内存
for (int i = 0; i < 10; ++i) {
ptr[i] = i;
}
// 释放内存
free(ptr);
}
return 0;
}
calloc
函数
calloc
函数用于分配指定数量和大小的内存块,并将所有字节初始化为0。
#include <iostream>
#include <cstdlib>
int main() {
// 分配10个int类型的内存空间并初始化为0
int* ptr = (int*)calloc(10, sizeof(int));
if (ptr != NULL) {
// 释放内存
free(ptr);
}
return 0;
}
realloc
函数
realloc
函数用于重新分配已经分配的内存块的大小。如果新的大小比原来的大,可能会在堆上移动内存块;如果新的大小比原来的小,会截断内存块。
#include <iostream>
#include <cstdlib>
int main() {
// 分配10个int类型的内存空间
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr != NULL) {
// 重新分配为20个int类型的内存空间
int* newPtr = (int*)realloc(ptr, 20 * sizeof(int));
if (newPtr != NULL) {
ptr = newPtr;
// 释放内存
free(ptr);
}
}
return 0;
}
free
函数
free
函数用于释放malloc
、calloc
或realloc
分配的内存。
#include <iostream>
#include <cstdlib>
int main() {
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr != NULL) {
// 释放内存
free(ptr);
}
return 0;
}
内存管理的注意事项
- 内存泄漏:如果使用
new
或malloc
分配的内存没有使用delete
或free
释放,会导致内存泄漏,程序运行时会逐渐耗尽可用内存。 - 悬空指针:释放内存后,指向该内存的指针成为悬空指针,继续使用悬空指针会导致未定义行为。
- 不匹配使用:
new
必须与delete
配对使用,new[]
必须与delete[]
配对使用,malloc
、calloc
、realloc
必须与free
配对使用。
如何避免动态内存分配导致的内存泄漏?
在C++中,动态内存分配如果管理不当很容易导致内存泄漏,即程序中已分配的内存不再被使用,但却没有被释放,随着程序的运行,可用内存会逐渐减少。以下是一些避免动态内存分配导致内存泄漏的方法:
1. 遵循配对原则
在使用动态内存分配时,要确保new
与delete
、new[]
与delete[]
、malloc
/calloc
/realloc
与free
正确配对使用。
示例代码:
#include <iostream>
#include <cstdlib>
int main() {
// 使用new分配单个对象
int* singlePtr = new int(10);
// 使用delete释放单个对象
delete singlePtr;
// 使用new[]分配数组
int* arrayPtr = new int[5];
// 使用delete[]释放数组
delete[] arrayPtr;
// 使用malloc分配内存
char* charPtr = (char*)malloc(10 * sizeof(char));
// 使用free释放内存
free(charPtr);
return 0;
}
2. 使用智能指针
智能指针是C++标准库提供的一种类模板,它可以自动管理动态分配的内存,当智能指针的生命周期结束时,会自动释放所指向的内存。
2.1 std::unique_ptr
std::unique_ptr
是一种独占式智能指针,同一时间只能有一个std::unique_ptr
指向同一个对象,当它离开作用域时,会自动释放所指向的内存。
示例代码:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
int main() {
// 创建std::unique_ptr
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
// 不需要手动释放内存,ptr离开作用域时会自动释放
return 0;
}
2.2 std::shared_ptr
std::shared_ptr
是一种共享式智能指针,多个std::shared_ptr
可以指向同一个对象,使用引用计数来管理对象的生命周期,当引用计数为0时,会自动释放所指向的内存。
示例代码:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
int main() {
// 创建std::shared_ptr
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数加1
// 当ptr1和ptr2都离开作用域时,引用计数变为0,自动释放内存
return 0;
}
3. 异常安全
在使用动态内存分配时,要确保在发生异常的情况下也能正确释放内存。可以使用try-catch
块来捕获异常,并在catch
块中释放内存。但使用智能指针可以更方便地实现异常安全。
示例代码(使用智能指针实现异常安全):
#include <iostream>
#include <memory>
#include <stdexcept>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
void func() {
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
// 可能会抛出异常的代码
if (true) {
throw std::runtime_error("An error occurred");
}
// 不需要手动释放内存,ptr离开作用域时会自动释放
}
int main() {
try {
func();
} catch (const std::exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
4. 封装动态内存管理
将动态内存管理封装在类中,通过类的构造函数分配内存,析构函数释放内存,遵循RAII(资源获取即初始化)原则。
示例代码:
#include <iostream>
class MemoryWrapper {
private:
int* data;
public:
MemoryWrapper() {
data = new int[10];
std::cout << "Memory allocated" << std::endl;
}
~MemoryWrapper() {
delete[] data;
std::cout << "Memory freed" << std::endl;
}
};
int main() {
MemoryWrapper wrapper;
// 当wrapper离开作用域时,析构函数会自动释放内存
return 0;
}
5. 代码审查和静态分析工具
定期进行代码审查,检查动态内存分配和释放的代码是否正确。同时,可以使用静态分析工具(如Clang Static Analyzer、Cppcheck等)来帮助检测潜在的内存泄漏问题。
最后
理解 C++的动态内存分配原理,以及掌握如何避免动态内存分配导致的内存泄漏?是在开发中非常关键的知识,这篇文章希望可以帮助到你,关注威哥爱编程,全栈开发就你行。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)