20. 内存管理

一、内存的组织方式

  程序员编写完程序之后,程序要先加载在计算机的内存中,再运行程序。在 C 语言中,不同数据在内存中所存储的位置也不一样。全局变量存储在内存中的静态存储区,非静态的局部变量存储在内存中的动态存储区(栈)。临时使用的数据建立动态内存分配区域,需要的时候开辟,不需要时及时释放(堆)。

  通过内存注释方式可以看出,堆用来存放动态分配内存空间,而栈用来存放局部数据变量、函数的参数以及调用函数与被调函数的联系。

  在内存的全局存储空间中,用于程序动态分配和释放的内存块称为自由存储空间,通常也称之为堆。在 C 程序中,使用 malloc() 函数和 free() 函数来从堆中动态的分配内存和释放内存。

  程序不会像处理堆那样在栈中显示地分配内存。当程序调用函数或声明局部变量时,系统将自动分配内存。

  栈是一个后进先出的压入弹出式的数据结构。在程序运行时,需要每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最晚进栈的对象将被弹出,然后栈指针向上移动一个位置。如果栈指针位于栈顶,则表示栈是空的;如果栈指针指向最下面的数据项的后一个位置,则表示栈为满的。

二、动态内存管理

  在头文件 stdlib.h 中声明了四个有关内存动态分配的函数。

2.1、malloc()函数

  malloc() 函数的原型如下:

void* malloc( size_t size );

  该函数的作用是在内存中动态分配一个 size 大小的内存空间。malloc() 函数会返回一个指针,该指针指向分配的内存空间的第一个地址,如果出现错误,则返回 NULL。

  使用 malloc() 函数分配的内存空间是在堆中,而不是在栈中。因此在使用完这块内存中间之后一定要将其释放掉,释放内存空间使用的是 free() 函数。

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

int main()
{
    int *p;
    int i = 0;

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

    printf("请输入5个整数:\n");
    for(i = 0;i < 5; i++)
    {
        scanf("%d",p+i);
    }

    printf("你输入的数据是:\n");
    for(i = 0; i < 5; i++)
    {
        printf("%d ",*(p+i));
    }

    free(p);

    return 0;
}

2.2、calloc()函数

  calloc() 函数的原型如下:

void* calloc( size_t num, size_t size );

  该函数的作用是在内存中动态分配 num 个长度为 size 的连续内存空间数组,并将该内存中间的字节初始化为 0。calloc() 函数会返回一个指针,该指针指向动态分配的连续内存空间的起始地址。当分配内存空间错误是返回 NULL。

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

int main()
{
    char *str;

    str = (char*)calloc(30,sizeof(char));

    printf("请输入一个字符串:\n");
    gets(str);
    printf("你输入的字符串为:\n");
    printf("%s\n",str);

    free(str);

    return 0;
}

2.3、realloc()函数

  realloc() 函数的原型如下:

void *realloc( void *ptr, size_t new_size );

  该函数的作用是重新分配给定的内存区域。它重新分配 malloc() 函数 或 calloc() 函数 获得的动态空间大小。如果 ptr 的值为为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。如果 ptr 不为空,它先判断当前的指针是否有足够的连续空间,如果有,扩大 ptr 指向的地址,并且将 ptr 的值返回,如果空间不够,先按照 newsize 指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来 ptr 所指内存区域(注意:原来指针是自动释放,不需要使用 free() 函数),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。设定上 new_size 大小是任意的,也就是说既可以比原来的数值大,也可以比原来的数值小。该函数的返回值是一个指向新地址的指针,如果出现错误,则返回 NULL。

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

int main()
{
    char *str;

    str = (char*)malloc(30*sizeof(char));
    str = realloc(str,15*sizeof(char));

    free(str);

    return 0;
}

2.4、free()函数

  free() 函数的原型如下:

void free( void* ptr );

  该函数的作用是释放指针 ptr 指向的内存区,使部分内存区能被其它变量使用。ptr 是最近一次调用 calloc() 函数 或 malloc() 函数时的返回值。free() 函数所用的指针变量可以与 malloc() 函数 或 calloc() 函数不同,但是两个指针必须储存相同的地址。并且,不能释放同一个内存两次。内存块可以在一个函数中创建,在另一个函数中销毁。

三、动态分配内存的基本原则

  1. 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大的内存块的系统开销大;
  2. 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵循谁分配,谁释放原则),否则可能出现内存泄露;
  3. 总是确保释放以分配过的内存。在编写分配内存的代码时,就需要确定在代码的什么地方释放内存;
  4. 在释放内存之前,确保不会无意中覆盖堆上已分配的内存地址,否则程序就会出现内存泄漏。在循环分配内存时,要特别小心。

四、有关内存的函数

  在头文件中 string.h 中,提供了几个有关内存操作的函数。

4.1、内存拷贝函数

  memcpy() 函数 和 memmove() 函数的原型如下:

void * memcpy( void * restrict dest, const void * restrict src, size_t count );
void * memmove( void * dest, const void * src, size_t count );

  memcpy() 函数从 src 的位置开始拷贝 count 字节的数据到目标的内存位置 dest。这个函数在遇到 '\0' 的时候并不会停止。如果 src 和 dest 有任何的重叠,复制的结果都是未定义的;

#include <stdio.h>
#include <string.h>

int main(void)
{
    int src[] = {1,2,3,4,5,6,7,8,9,0};
    int dest[20] = {0};
    int i = 0;

    memcpy(dest, src, 40);

    for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
    {
        printf("%-3d", dest[i]);
    }
    printf("\n");

    return 0;
}

  memmove() 函数从 src 的位置开始拷贝 count 字节的数据到目标的内存位置 dest。这个函数在遇到 '\0' 的时候并不会停止。src 和 dest 内存空间可以重叠。

#include <stdio.h>
#include <string.h>

int main(void)
{
    int src[] = {1,2,3,4,5,6,7,8,9,0};
    int i = 0;

    memmove(src + 3, src, 12);

    for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
    {
        printf("%-3d", src[i]);
    }
    printf("\n");

    return 0;
}

  模拟实现 memcpy() 函数:

#include <stdio.h>
#include <assert.h>

void * my_memcpy(void * dest, void * src, size_t num);

int main(void)
{
    int src[] = {1,2,3,4,5,6,7,8,9,0};
    int dest[20] = {0};
    int i = 0;

    my_memcpy(dest, src, 40);

    for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
    {
        printf("%-3d", dest[i]);
    }
    printf("\n");

    return 0;
}

void * my_memcpy(void * dest, void * src, size_t num)
{
    void * temp = dest;

    assert(src);                        // 断言,判断指针不能为空
    assert(dest);                       // 断言,判断指针不能为空

    while(num--)
    {
        *(char *)dest = *(char *)src;
        dest = (char *)dest + 1;
        src = (char *)src + 1;
    }

    return temp;
}

  模拟实现 memmove() 函数:

#include <stdio.h>
#include <assert.h>

void * my_memmove(void * dest, const void * src, size_t num);

int main(void)
{
    int src[] = {1,2,3,4,5,6,7,8,9,0};
    int i = 0;

    my_memmove(src + 3, src, 12);

    for(i = 0; i < sizeof(src)/sizeof(src[0]); i++)
    {
        printf("%-3d", src[i]);
    }
    printf("\n");

    return 0;
}

void * my_memmove(void * dest, const void * src, size_t num)
{
    void * temp = dest;

    assert(src);                        // 断言,判断指针不能为空
    assert(dest);                       // 断言,判断指针不能为空

    if(dest < src)                      // 从后向前拷贝
    {
        while(num--)
        {
            *(char *)dest = *(char *)src;
            dest = (char *)dest + 1;
            src = (char *)src + 1;
        }
    }
    else                                // 从前向后拷贝
    {
        while(num--)
        {
            *((char *)dest + num) = *((char *) src + num);
        }
    }

    return temp;
}

4.2、内存比较函数

  memcmp() 函数的原型如下:

int memcmp( const void * lhs, const void * rhs, size_t count );

  memcmp() 函数比较 lhs 指向的内存空间的数据与 rhs 指向的内存空间的数据前 count 字节数据是否相等。如果 lhs 指向的内存空间的前 count 字节的数据小,则返回负数;如果相等,则返回 0;如果大于,则返回整数;

#include <stdio.h>
#include <string.h>

int main(void)
{
    int array1[] = {1,2,3,4,5};
    int array2[] = {1,2,3};
    int result = memcmp(array1,array2,12);

    printf("%d\n", result);

    return 0;
}

4.3、内存设置函数

  memset() 函数的原型如下:

void * memset( void * dest, int ch, size_t count );

  memset() 函数将 dest 指向的内存空间的 count 字节的数据设置为 ch;

#include <stdio.h>
#include <string.h>

int main(void)
{
    char array[] = "hello world!";
    memset(array+6, 'x', 5);

    printf("%s\n", array);

    return 0;
}

五、创建的动态内存错误

5.1、对NULL指针的解引用操作

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

int main(void)
{
    int *p = (int *)malloc(INT_MAX / 4);
    if(p == NULL)
    {
        printf("开辟内存失败!\n");
        return 1;
    }

    // 如果p的值是NULL,就会有问题
    *p = 30;

    free(p);
    p = NULL;
  
    return 0;
}

5.2、对动态开辟空间的越界访问

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

int main(void)
{
    int i = 0;
    int *p = (int *)malloc(sizeof(int) * 10);
    if(p == NULL)
    {
        printf("开辟内存失败!\n");
        return 1;
    }

    // 对动态开辟空间的越界访问
    for(i = 0; i <= 10; i++)
    {
        p[i] = i;
    }

    free(p);
    p = NULL;
  
    return 0;
}

5.3、对非动态开辟内存使用free()函数

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

int main(void)
{
    int i = 0;
    int *p = &i;

    // 对非动态开辟内存使用free()函数
    free(p);
    p = NULL;
  
    return 0;
}

5.4、使用free()函数释放一块动态开辟内存的一部分

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

int main(void)
{
    int i = 0;
    int *p = (int *)malloc(sizeof(int) * 10);
    if(p == NULL)
    {
        printf("开辟内存失败!\n");
        return 1;
    }

    for(i = 0; i < 5; i++)
    {
        p[i] = i;
        p++;
    }

    // 使用free()函数释放一块动态开辟内存的一部分
    free(p);
    p = NULL;
  
    return 0;
}

5.5、对同一块动态内存多次释放

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

int main(void)
{
    int i = 0;
    int *p = (int *)malloc(sizeof(int) * 10);
    if(p == NULL)
    {
        printf("开辟内存失败!\n");
        return 1;
    }

    free(p);
    // 对同一块动态内存多次释放
    free(p);
    p = NULL;
  
    return 0;
}

5.6、动态开辟内存忘记释放

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

void test(void);

int main(void)
{
    test();
  
    return 0;
}

void test(void)
{
    int i;
    int *p = (int *)malloc(sizeof(int) * 10);
    if(p == NULL)
    {
        printf("开辟内存失败!\n");
        return;
    }

    scanf("%d",&i);
    if(i == 5)
    {
        return ;
    }

    free(p);
    p = NULL;
}
posted @ 2023-03-23 17:47  星光樱梦  阅读(52)  评论(0编辑  收藏  举报