c语言—动态内存管理

一.为什么存在动态内存开辟

开辟空间的特点:

  1. 空间开辟大小是固定的

  1. 数组在申明时,必须指定数组长度,她所需要的内存在编译时分配

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,

那数组的编译时开辟空间的方式就不能满足了。

这时候就只能试试动态存开辟了

二.动态内存函数的介绍

1.malloc和free

void*malloc(size_t size);

这个函数向内存申请一块连续的内存空间,并返回这块空间的指针。

  • 如果开辟成功,则返回一个指向这块空间的指针

  • 如果开辟失败,这返回一个NULL,因此malloc函数的返回值一定要做检查

  • 返回的类型是void*,所以malloc函数并不知到开辟空间的类型,具体在使用时使用者自己决定

  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

free函数使用来释放动态开辟的内存的

void free(void*ptr)
  • 如果参数ptr指向的空间不是动态内存开辟的,那么free函数的行为是未定义的

  • 如果ptr是NULL,则free函数什么都不做

注:malloc和free都声明在stdlib.h的文件中

例:

#include<stdio.h>

int main()
{
    int num=0;
    scanf("%d",&num);
    int arr[num]=0;//变长数组
    int*ptr=NULL;
    ptr=(int*)malloc(num*sizeof(int));
    if(NULL!=ptr)//判读ptr是否为空指针
    {
        int i=0;
        for(i=0;i<num;i++)
        {
            *(ptr+i)=i;
        }
    }
    free(ptr);//释放ptr所指向的动态内存
    ptr=NULL;//是否有必要?
    return 0;
}

虽然程序结束后被开辟的内存空间会被释放,但是这样浪费的很多空间,而free能够充分利用内存空间。

最后虽然free(ptr)释放了开辟的动态空间,但是ptr仍然指向那个空间的地址,所以要将ptr设置为空指针来避免空间被不会好意的人通过ptr找到。

2.calloc

void*callor(size_t num,size_t size)
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0

  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
    int* p = calloc(10, sizeof(int));
     
    if(NULL != p)
    {
        //使用空间
    }
    free(p);
    p = NULL;
    return 0;
}

3.realloc

  • realloc函数的出现让动态内存管理更加灵活

  • realloc 函数就可以做到对动态开辟内存大小

的调整。

void*realloc(void*str,size_t size);
  • ptr是要调整的内存地址

  • size是调整之后的内存大小

  • 返回调整之后内存的起始位置

  • 这个函数在调整原内存空间的大小的基础上,还会将原内存的数据移到新的空间里

  • realloc在调整内存空间时有两种情况

情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间

当是情况1的时候,要扩展的内存就在原有的内存之后追加新的空间,原来的数据不发生改变。

当是情况2的时候,扩展的方式就是在堆空间上另找一个适合大小的连续空间使用。这样函数返回的是一个新的内存地址。

例:

int main()
{
    int* ptr = (int*)malloc(100);
    if (NULL == ptr)
    {
        perror(ptr);
    }
    else
    {
        int i = 0;
        for(i=0;i<100;i++)
        {
            *(ptr + i) = i;
        }
    }
    //扩展容量
    ptr = (int*)realioc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
    
   if (NULL == ptr)
    {
        perror(ptr);
    }
    else
    {
        int i = 0;
        for(i=100;i<=1000;i++)
        {
            *(ptr + i) = i;
        }
    free(ptr);
    ptr = NULL;
    return 0;
}

不能像代码1一样操作,因为若(int*)realloc(ptr, 1000)中realloc开辟失败,返回的空指针赋给了ptr,

那么ptr被改掉,ptr指向的空间就找不到了

修改后:

int main()
{
    int* ptr = (int*)malloc(100);
    if (NULL == ptr)
    {
        perror(ptr);
    }
    else
    {
        int i = 0;
        for (i = 0; i < 100; i++)
        {
            *(ptr + i) = i;
        }
    }
    //扩展容量
    int* p = NULL;
    p=realloc(ptr, 1000);

    if (p == NULL)
    {
        perror(p);
    }
    else
    {
        ptr = p;
        int i = 0;
        for (i = 100; i <= 1000; i++)
        {
            *(ptr + i) = i;
        }
    }
    free(ptr);
    ptr = NULL;
    return 0;
}

int a=0;

malloc(sizeof(a))与realloc(NULL,sizeof(a))功能一样

三.常见的动态内存错误

1.对空指针的解引用操作

void test()
{
    int* p = (int*)malloc(INT_MAX);
    *p = 20;//如果p是空指针就会有问题
    free(p);
}

2.对动态开辟空间的越界访问

void test()
{
    int i = 0;
    int* p = (int*)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i;//当i是10的时候越界访问
    }
    free(p);
}

3.对非动态开辟内存使用free释放

void test()
{
    int a = 10;
    int* p = &a;
    free(p);
}

4.使用free释放动态开辟内存的一部分

void test()
{
    int* p = (int*)malloc(10 * sizeof(int));
    p++;
    free(p);//p不再指向动态内存的起始位置
}

5.对同一块内存多次释放

void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
}

6.动态内存开辟忘记释放(内存泄漏)

void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while(1);
}

注:忘记释放不再使用的动态开辟的空间会造成内存泄漏

四.几个经典的笔试题

题目1.

void GetMemory(char *p)
{
    p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

int main()
{
    Test();
    return 0;
}

请问运行Test 函数会有什么样的结果?

结果:

  1. 运行代码会出现兵溃的现象

  1. 程序存在内存泄漏

原因是str以值传递的形式给p

p是GetMemory函数的形参,只能在函数内部有效

等GetMemory函数返回返回之后,动态开辟内存尚未释放

并且无法找到,所以会造成内存泄漏

改进:

法一:

可以传str的指针给GetMemory函数,*p就是str

在使用完str后释放它指向的内存空间并将str置为NULL

void GetMemory(char **p)
{
    *p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);
    free(str);
    str=NULL;
}

int main()
{
    Test();
    return 0;
}
法二:

将GetMemory的返回类型改为char*,返回指向开辟空间的地址

在使用完str后释放它指向的内存空间并将str置为NULL

char* GetMemory(char *p)
{
    p = (char *)malloc(100);
    return p;
}
void Test(void)
{
    char *str = NULL;
    str=GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);
    free(str);
    str=NULL;
}

int main()
{
    Test();
    return 0;
}

题目2.

char *GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}

int main()
{
    Test();
    return 0;
}

请问运行Test 函数会有什么样的结果?

结果:打印的是随机值

题目3.

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

int main()
{
    Test();
    return 0;
}

请问运行Test 函数会有什么样的结果?

结果:

没有释放动态开辟的空间,导致内存泄漏。

题目4.

void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

int main()
{
    Test();
    return 0;
}

结果:

free函数释放ptr后没有将ptr置成空指针,造成内存非法访问。

五.C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结

束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是

分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返

回地址等。

2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分

配方式类似于链表。

3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

六.柔性数组

可能你们没有听过柔性数组,但是它真的存在。

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

注:柔性数组是在结构体中体现的

struct S1
{
    int n;
    int arr[];//大小未知的
};

struct S2
{
    int n;
    int arr[0];//大小未知的-柔性数组成员-数组大小是可以调整的
};

1.柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。

  • sizeof 返回的这种结构大小不包括柔性数组的内存。

  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大

小,以适应柔性数组的预期大小。

struct S
{
    int n;
    int arr[0];
};

int main()
{
    printf("%d",sizeof(struct S));//输出为4
                                  //所以sizeof 返回的这种结构大小不包括柔性数组的内存。
}

2.柔性数组的使用

struct S
{
    int n;
    int arr[0];
};

int main()
{
    struct S*ps=(struct S*)malloc(sizeof(struct S)+5*sizeof(int));
                                  //sizeof(struct S)是给结构体除了柔性数组的所有成员开辟的
                                  //5*sizeof(int)是给柔性数组开辟的
    ps->n=100;
    int i=0;
    for(i=0;i<5;i++)
    {
        ps->arr[i]=i;//0 1 2 3 4
    }
    struct S*ptr=realloc(ps,44);
    if(ptr!=NULL)
    {
        ps=ptr;
    }
    for(i=5;i<10;i++)
    {
        ps->arr[i]=i;
    }
    for(i=0;i<10;i++)
    {    
        pritnf("%d ",ps->arr[i]);
    }
    free(ps);
    ps=NULL;
    return 0;
}
//这是不使用柔性数组的方法
struct S
{
    int n;
    int*arr;
};

int main()
{
    struct S*ps=(struct S*)malloc(sizeof(struct S));
    ps->arr=(int*)malloc(5*sizeof(int);
    ps->n=100;
    int i=0;
    for(i=0;i<5;i++)
    {
        ps->arr[i]=i;//0 1 2 3 4
    }
    int *ptr=realloc(ps->arr,10*sizeof(int));
    if(ptr!=NULL)
    {
        ps->arr=ptr;
    }
    for(i=5;i<10;i++)
    {
        ps->arr[i]=i;
    }
    for(i=0;i<10;i++)
    {    
        pritnf("%d ",ps->arr[i]);
    }
    free(ps->arr);
    ps->arr=NULL;
    free(ps);
    PS=NULL;
    return 0;
}

3,柔性数组的优势

上述两种代码都可以完成同样的功能,但是 代码1 的实现有两个好处:

好处一方便内存释放

代码一只用一次free函数把所有的动态内存全部释放,不容易出错。

好处二:内存利用率更高

代码一的开辟的空间是连续的,减少了内存碎片,内存利用率更高。

好处三这有利于访问速度

代码一的开辟的空间是连续的,局部性原理

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言中常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过中序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言中都有相应的实现方式,可以应用于各种不同的场景。C语言中的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存中是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值