--92faeb2b9b760542 x-next-cache-tags: _N_T_/layout,_N_T_/blog/layout,_N_T_/blog/[...slug]/layout,_N_T_/blog/[...slug]/page,_N_T_/blog/c%E8%AF%AD%E8%A8%80/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86 vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch 内存管理 | YBinary
Published on

内存管理

Authors

C 语言的内存管理,分成两部分。

一部分是系统管理的,另一部分是用户手动管理的。

系统管理的内存,主要是函数内部的变量(局部变量)。 这部分变量在函数运行时进入内存,函数运行结束后自动从内存卸载。 这些变量存放的区域称为”栈“(stack)”栈“所在的内存是系统自动管理的

用户手动管理的内存,主要是程序运行的整个过程中都存在的变量(全局变量),这些变量需要用户手动从内存释放。 如果使用后忘记释放,它就一直占用内存,直到程序退出,这种情况称为”内存泄漏“(memory leak)。 这些变量所在的内存称为”堆“(heap)”堆“所在的内存是用户手动管理的

栈内存 vs 堆内存

C/C++ 中有两种主要的内存分配方式:栈内存和堆内存。

  • 栈内存:当你在函数中声明局部变量时,编译器会自动在栈上为这些变量分配内存。这种分配是自动的,且非常高效,因为栈内存的分配和释放是由编译器管理的,分配和释放的时间非常短(通过调整栈指针)。然而,栈内存有以下限制:

    1. 栈的大小是有限的,通常只有几 MB。因此,不能在栈上分配大量内存。
    2. 栈上的变量的生命周期是有限的,它们在函数结束时自动销毁,无法跨函数使用。
  • 堆内存:堆内存是程序运行时的动态内存,程序员可以通过 malloc/free 或 new/delete 来手动管理堆内存。堆内存的优点是可以分配更大的内存块,且内存的生命周期由程序员控制。缺点是堆内存的分配和释放速度比栈慢,且需要程序员手动管理内存,防止内存泄漏。

void指针

前面章节已经说过了,每一块内存都有地址,通过指针变量可以获取指定地址的内存块。 指针变量必须有类型,否则编译器无法知道,如何解读内存块保存的二进制数据。 但是,向系统请求内存的时候,有时不确定会有什么样的数据写入内存,需要先获得内存块,稍后再确定写入的数据类型。

为了满足这种需求,C 语言提供了一种不定类型的指针,叫做 void 指针。 它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。

另一方面,void 指针等同于无类型指针,可以指向任意类型的数据,但是不能解读数据

void 指针与其他所有类型指针之间是互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。

int x = 10;

void* p = &x; // 整数指针转为 void 指针
int* q = p; // void 指针转为整数指针

上面示例演示了,整数指针和 void 指针如何互相转换。&x是一个整数指针,p是 void 指针,赋值时&x的地址会自动解释为 void 类型。 同样的,p再赋值给整数指针q时,p的地址会自动解释为整数指针。

注意,由于不知道 void 指针指向什么类型的值,所以不能用*运算符取出它指向的值。

char a = 'X';
void* p = &a;

printf("%c\n", *p); // 报错

上面示例中,p是一个 void 指针,所以这时无法用*p取出指针指向的值。

restrict 说明符

声明指针变量时,可以使用restrict说明符,告诉编译器,该块内存区域只有当前指针一种访问方式,其他指针不能读写该块内存。这种指针称为“受限指针”(restrict pointer)

int* restrict p;
p = malloc(sizeof(int));

上面示例中,声明指针变量p时,加入了restrict说明符,使得p变成了受限指针。 后面,当p指向malloc()函数返回的一块内存区域,就意味着,该区域只有通过p来访问,不存在其他访问方式。

int* restrict p;
p = malloc(sizeof(int));

int* q = p;
*q = 0; // 未定义行为

上面示例中,另一个指针q与受限指针p指向同一块内存,现在该内存有p和q两种访问方式。这就违反了对编译器的承诺,后面通过*q对该内存区域赋值,会导致未定义行为。

--92faeb2b9b760542--