轻松高效拿捏C语言22内存管理函数还不赶紧搞定

程序员Feri一名12年+的程序员,做过开发带过团队创过业,擅长Java、鸿蒙、嵌入式、人工智能等开发,专注于程序员成长的那点儿事,希望在成长的路上有你相伴!君志所向,一往无前!


1 内存管理相关函数

1.1 C程序的内存分配

C程序中,不同数据在内存中分配说明:

  1. 全局变量和静态局部变量——内存中的静态存储区/全局区

  2. 非静态的局部变量——内存中的动态存储区:stack 栈

  3. 临时使用的数据——建立动态内存分配区域,需要时随时开辟,不需要时及时释放——heap 堆

  4. 根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,只能通过指针来引用)

1.2 void 指针(无类型指针)

  • 每一块内存都有地址,通过指针变量可以获取指定地址的内存块。

  • 指针变量必须有类型,否则编译器无法知道如何解读内存块保存的二进制数据。但是,向系统请求内存的时候,有时不确定会有什么样的数据写入内存,需要先获得内存块,稍后再确定写入的数据类型。

综上,为了满足这种需求,C 语言提供了一种不定类型的指针,叫做 void 指针。

它只有内存块的地址信息没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。

此外,由于void 指针等同于无类型指针(typeless pointer),可以指向任意类型的数据,但是不能解读数据

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

int x = 10;
void *p = &x; // 整数指针转为 void 指针
int *q = p; // void 指针转为整数指针
char a = 'X';
void* p = &a;
printf("%c\n", *p); // 报错

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

void 指针的重要之处在于,很多内存相关函数的返回值就是 void 指针。

1.3 内存动态分配函数

头文件 <stdlib.h>声明了四个关于内存动态分配的函数。所谓动态分配内存,就是按需分配,申请才能获得。

malloc()

函数原型:

void *malloc(unsigned int size);  //size的类型为无符号整型

作用:在内存的动态存储区(堆区)中分配一个长度为size连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。

由于返回的指针的基类型为 void,应通过显式类型转换后才能存入其他基类型的指针变量,否则会有警告。如果分配不成功,返回空指针(NULL)。

举例1:

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

举例2:动态申请数组空间

int *p;
p = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
  p[i] = i * 5;

得到一个元素类型为int型,长度为n的数组。取元素方式与之前相同,如获取第2个元素:p[1]。

举例3:

struct node *p;
p = (struct node *) malloc(sizeof(struct node));  //(struct node*)为强制类型转换
typedef struct BTNode{
 int data;
 struct BTNode *lchild;
 struct BTNode *rchild;
}BTNode;

//声明二叉树结点方式1
BTNode bt1;
//声明二叉树结点方式2:需熟练掌握
BTNode *bt2;
bt2 = (BTNode*)malloc(sizeof(BTNode));

方式2中的BT是指针型变量,还可以指向其它节点。而方式1中的BT则不行。此外,调用结构体成员时,

//针对于方式1:结构体变量取成员,用"."
int x = bt1.data;

//针对于方式2:指向结构体的指针取成员,用"->"
int x = bt2->data;
int x = (*bt2).data;//以前的写法

关于返回值为NULL:

malloc() 分配内存有可能分配失败,这时返回常量 NULL。Null 的值为0,是一个无法读写的内存地址,可以理解成一个不指向任何地方的指针。

它在包括 stdlib.h 等多个头文件里面都有定义,所以只要可以使用 malloc() ,就可以使用 NULL 。

由于存在分配失败的可能,所以最好在使用 malloc() 之后检查一下,是否分配成功。

int* p = malloc(sizeof(int));
if (p == NULL) { // 内存分配失败
  
}
// 或
if (p != NULL) {
  //...
}

上面示例中,通过判断返回的指针 p 是否为 NULL ,确定 malloc() 是否分配成功。

calloc()

函数原型:

void *calloc(unsigned int n,unsigned int size);

作用:在内存的**动态存储区(堆区)**中分配n个,单位长度为size的连续空间,这个空间一般比较大,总共占用n*size 个字节。并将该空间的首地址作为函数的返回值。如果函数没有成功执行,返回NULL。

calloc()函数适合为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。

举例:

int *p;
p = (int *)calloc(10,sizeof(int)); //开辟空间的同时,其内容初始化为零

//等同于
int* p;
p = (int *)malloc(10 * sizeof(int));
memset(p, 0, sizeof(int) * 10);

上面示例中, calloc() 相当于 malloc() + memset() 。

realloc()

函数原型:

void *realloc(void* p, unsigned int size)

作用:重新分配malloc()或calloc()函数获得的动态空间大小,即调整大小的内存空间。将先前开辟的内存块的指针p指向的动态空间大小改变为size,单位字节。

返回值是一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。分配失败返回NULL。

  • realloc() 优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。

  • 如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用 memset() )。

举例1:

int* b;
b = (int *)malloc(sizeof(int) * 10);
b = (int *)realloc(b, sizeof(int) * 2000);

指针 b 原来指向10个成员的整数数组,使用 realloc() 调整为2000个成员的数组。

举例2:动态栈入栈时,判断是否需要扩容

int push(SqStack &S, ElemType e) {
    if (S.top - S.bottom >= S.stacksize) {               //栈满,追加存储空间
        S.bottom = (ElemType *) realloc(S.bottom,(STACKINCREMENT + S.stacksize) * sizeof(ElemType));
        if (!S.bottom) //if(S.bottom == NULL)
            return FALSE;       //空间分配失败
        S.top = S.bottom + S.stacksize;
        S.stacksize += STACKINCREMENT;
    }
    *S.top = e;
    S.top++;     // 栈顶指针加1
    return TRUE;
}
free()

函数原型:

void free(void *p);

函数无返回值。p是最近一次调用malloc()或calloc()函数时的返回值。

作用:释放指针变量p所指向的内存空间,使这部分内存能重新被其它变量使用。否则这个内存块会一直占用到程序运行结束。

举例:

int *p;
p=(int *)malloc(sizeof(int));
    
//...各种操作...
    
free(p); //千万不要忘了使用free()释放内存!

注意:

1、指针 p 必须是经过动态分配函数 malloc 成功后返回的首地址。

2、分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。

3、如果忘记调用free()函数,同时p所在的函数调用结束后p指针已经消失了,导致无法访问未回收的内存块,构成内存泄漏。

1.4 举例

举例:动态创建数组,输入5个学生的成绩,另外一个函数检测成绩低于60 分的,输出不合格的成绩。

#include <stdio.h>
#include <stdlib.h>

#define N 5

void check(int *ptr) {
    printf("\n不及格的成绩有: ");
    for (int i = 0; i < 5; i++) {
        if (ptr[i] < 60) {
            printf(" %d ", ptr[i]);
        }
    }
}

int main() {
    int *p;
    //动态创建数组
    p = (int *) malloc(N * sizeof(int));

    printf("请输入%d个成绩:\n",N);
    for (int i = 0; i < N; i++) {
        scanf("%d", p + i);
    }
    //检查不及格的学生
    check(p);
    
    free(p); //销毁 堆区 p 指向的空间

    return 0;
}

图示:

1.5 动态分配内存的基本原则

1)避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大

2)仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它,否则可能出现内存泄漏。

这里需要遵守原则:谁分配,谁释放。

3)总是确保释放以分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存。

1.6 常见的内存错误及其对策

1)内存分配未成功,却使用了它

新手常犯这种错误,因为他们没有意识到内存分配会不成功。

常用解决办法是,在使用内存之前检查指针是否为NULL。比如,如果指针p是函数的参数,那么在函数的入口处应该用if(p==NULL)if(p!=NULL)进行防错处理。

2)内存分配虽然成功,但是尚未初始化就引用它

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误。

int * p = NULL;
p = (int*)malloc(sizeof(int));
if (p == NULL){/*...*/}
/*初始化为0*/
memset(p, 0, sizeof(int));

题外话,无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

3)内存分配成功并且已经初始化,但操作时提示内存越界

在使用数组时经常发生下标“+1”或者“-1”的操作,特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

数组访问越界在运行时,它的表现是不定的,有时什么事也没有,程序一直运行(当然,某些错误结果已造成);有时,则是程序一下子崩溃。

4)忘记了释放内存,造成内存泄漏

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

动态内存的申请与释放必须配对,程序中malloc()free()的使用次数一定要相同,否则肯定有错误。

5)未正确的释放内存,造成内存泄漏

程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存。此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

#include <stdio.h>
#include <stdlib.h>

void getMemory(int *p) {
    p = (int *) malloc(sizeof(int));  // 在这里修改的是局部指针 p,不会影响 main 函数中的原始指针 ptr
    //....
}

int main() {
    int *ptr = NULL;
    getMemory(ptr);  // 将 ptr 的值传递给 getMemory,但是在函数内部修改的是 p,而不是 ptr
    printf("ptr = %d\n", *ptr);  // 这里的 *ptr 是未定义行为,因为 ptr 没有指向有效的内存
    free(ptr);  // 这里试图释放未分配的内存,会导致问题
}

在本例中,getMemory()中的p申请了新的内存,只是把 p所指的内存地址改变了,但是ptr丝毫未变。getMemory()中的p也始终没有进行内存的释放。事实上,因为没有用free释放内存,每执行一次getMemory()就会泄漏一块内存。

6)释放了内存却继续使用它

函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

long *p;

void addr() {
    long k;
    k = 0;
    p = &k;
}
void port() {
    long i, j;
    j = 0;
    for (i = 0; i < 10; i++) {
        (*p)--;
        j++;
    }
}


int main() {
    addr();
    port();
}


由于addr函数中的变量k在函数返回后就已经不存在了,但是在全局变量p中却保存了它的地址。在下一个函数port中,试图通过全局指针p访问一个不存在的变量,进而出错。

在计算机系统,特别是嵌入式系统中,内存资源是非常有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何有效地管理内存资源

好啦,就到这里啦,内存函数,特别重要,希望每一个人都要重视!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值