C/C++内存详解

2024-09-04 08:42:22 浏览数 (1)

C/C 内存模型

让我们先来看看这段代码:

代码语言:javascript复制
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);
}

你知道上面代码中定义的变量分别存储在内存中的哪些部分吗?

说明一下:

  1. 栈又叫做堆栈,用来存储非静态局部变量、函数参数和返回值等等,栈是向下增长的。
  2. 内存映射段是高效的IO映射的方式,用来装载一个共享的动态内存库,用户可调用接口创建共享内存,用于进程间通信。
  3. 堆用于存储运行时动态内存分配,堆是向上增长的。我们使用malloc动态内存申请的空间在堆上。包括我们一会儿讲到的new也是如此。
  4. 数据段又叫做静态区,用于存储全局变量静态数据
  5. 代码段又叫做常量区,用来存储可执行的代码只读常量

C语言动态内存管理

mallocrealloccallocfree 是C语言中用于动态内存管理的标准库函数,它们定义在<stdlib.h>头文件中。这些函数允许程序在运行时根据需要分配和释放内存,而不是在编译时静态地分配内存。这对于处理未知大小的数据或需要动态增长的数据结构(如链表、树等)特别有用。

malloc

malloc(Memory Allocation)函数用于动态分配一块指定大小的内存区域。其原型为:

代码语言:javascript复制
void* malloc(size_t size);
  • size 参数指定了要分配的字节数。
  • 如果分配成功,返回指向分配的内存区域的指针;如果分配失败,则返回 NULL

使用 malloc 分配的内存区域是未初始化的,其内容是未定义的。

realloc

realloc(Re-Allocation)函数用于重新调整之前通过 malloccallocrealloc 分配的内存区域的大小。其原型为:

代码语言:javascript复制
void* realloc(void* ptr, size_t size);
  • ptr 是指向要调整大小的内存区域的指针。如果 ptrNULL,则 realloc 的行为类似于 malloc,分配一块新的内存区域。
  • size 是新的大小。
  • 如果分配成功,返回指向新内存区域的指针(可能与原指针相同,也可能不同)。如果失败,则返回 NULL,但原内存区域不会被释放。
calloc

calloc(Contiguous Allocation)函数也用于动态分配内存,但它还会将分配的内存区域初始化为零。其原型为:

代码语言:javascript复制
void* calloc(size_t num, size_t size);
  • num 指定了要分配的元素数量。
  • size 指定了每个元素的大小(以字节为单位)。
  • calloc 分配的内存总大小是 num * size
  • 分配的内存区域会被初始化为零。
  • 如果分配成功,返回指向分配的内存区域的指针;如果失败,则返回 NULL
free

free 函数用于释放之前通过 malloccallocrealloc 分配的内存区域。其原型为:

代码语言:javascript复制
void free(void* ptr);
  • ptr 是指向要释放的内存区域的指针。
  • 一旦内存被释放,ptr 指针就成为悬垂指针(dangling pointer),不应再被使用。
  • 尝试访问已释放的内存区域是未定义行为,可能导致程序崩溃或数据损坏。

总的来说,mallocrealloccallocfree 提供了在C语言中进行动态内存管理的核心功能,允许程序在运行时灵活地管理内存资源。

面试题:malloc、realloc和calloc有什么区别? malloc:动态申请空间,但不对空间进行初始化 realloc:对申请过的内存空间进行扩容处理。 calloc:申请空间的同时进行初始化处理,calloc=malloc memset。

C 动态内存申请

C语言的动态内存申请函数对于C 依旧可以使用。但也引入了新的动态内存申请方式:new、delete。

注意:malloc、realloc和calloc属于函数,但是new和delete属于操作符

new 操作符

new 操作符用于在堆(heap)上动态分配内存,并调用对象的构造函数(如果有的话)。其基本语法有两种形式:

为单个对象分配内存

代码语言:javascript复制
TypeName* pointer = new TypeName(initializer);

这里,TypeName 是要创建的对象类型,initializer 是传递给对象构造函数的参数(如果构造函数需要的话;如果构造函数没有参数或对象类型是基本数据类型,则可以省略)。pointer 是一个指向新创建对象的指针。

为对象数组分配内存

代码语言:javascript复制
TypeName* array = new TypeName[arraySize];

这里,TypeName 是数组元素的类型,arraySize 是数组中元素的数量。array 是一个指向数组第一个元素的指针。注意,对于数组,不会调用构造函数来初始化每个元素(除非元素类型是类类型且该类提供了默认构造函数),而是进行默认初始化(对于类类型,调用默认构造函数;对于内置类型,不进行初始化)。

delete 操作符

delete 操作符用于释放之前通过 new 分配的内存,并调用对象的析构函数(如果有的话)。其语法也有两种形式,对应于 new 的两种用法:

释放单个对象

代码语言:javascript复制
delete pointer;

这里,pointer 是指向之前通过 new 分配的内存的指针。使用 delete 后,pointer 变成了悬垂指针,不应再被使用。

释放对象数组

代码语言:javascript复制
delete[] array;

这里,array 是指向之前通过 new[] 分配的内存的指针。注意,对于数组,必须使用 delete[] 而不是 delete 来释放内存,以确保为每个元素调用析构函数(如果元素类型是类类型的话)。

注意事项
  • 使用 new 分配的内存必须使用 delete(或 delete[])来释放,以避免内存泄漏。
  • 释放内存后,指针变成悬垂指针,不应再被解引用或用于其他内存操作。
  • 对于类类型的对象,new 会自动调用构造函数,delete 会自动调用析构函数。这是 new/deletemalloc/free 的一个重要区别。
  • 如果 new 表达式失败(例如,由于内存不足),它会抛出 std::bad_alloc 异常(在 <new> 头文件中定义)。因此,在使用 new 时,可能需要考虑异常处理。
  • 对于基本数据类型(如 intfloat 等),newdelete 主要用于分配和释放内存,不会调用任何特殊的构造函数或析构函数。然而,对于类类型,它们的行为更加复杂,因为它们涉及到对象的构造和析构。
用法示例
代码语言:javascript复制
#include<iostream>
using namespace std;
int main()
{
	int* p1 = new int(10);//申请一个空间
	int* p2 = new int[10];//申请一个数值

	delete p1;//释放一个空间
	delete []p2;//释放一个数组
}

思考一下:既然已经有了malloc等函数,为什么还要设计出new这些操作符呢?new相对于malloc有哪些优势呢?

  • 当申请的空间类型为内置类型时,malloc和new的功能相同。
  • 如果内存申请失败,malloc会返回0,而new则会选择抛异常
  • 当申请的类型为自定义类型时,malloc和new的功能就有些差别了。接下来我们就介绍一下二者之间的差别。

让我们先来看看这段代码:

代码语言:javascript复制
class A
{
public:
	A()
	{
		_a = 1;
		cout << "A()" << endl;
	}
public:
	int _a;
		
};
int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	cout << "--------------------------------------" << endl;
	A* a2 = new A;

}

运行一下,我们会发现:

说明一下: 对于自定义类型的对象,例如类对象,new对象的同时会调用构造函数对对象进行构造,delete对象的同时会调用析构函数对对象进行析构。

operator new和operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

operator new和operator delete在用法上和malloc和free完全一样,都会在堆上申请空间

用法如下:

代码语言:javascript复制
int* p = (int*)operator new(sizeof(int) * 10);//申请10个int类型大小的空间
operator delete (p);//对申请的空间进行释放

其在用法上等价于:

代码语言:javascript复制
int* q = (int*)malloc(sizeof(int) * 10);
free(p);

尽管operator new和malloc在用法和作用上非常相似。但是仍然有不同之处?

不同之处有如下:

处理错误的方式不同,让我们看看如下的代码:

总结一下: 在申请失败的情况下,malloc返回0,operator new抛异常。


malloc VS operator new VS new

  • operator new=malloc 抛异常
  • new=operator 初始化

内存泄露

什么是内存泄露?内存泄露有什么⚠️?

  • 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
  • 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

内存泄露的分类

C/C 程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免内存泄露

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。 总结一下:

内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具。

0 人点赞