当前位置:网站首页>C-动态内存管理

C-动态内存管理

2022-08-11 05:30:00 CHAKMING1

1. 为什么存在动态内存分配

int val = 10;
int arr[10] = {0}; 

这是我们已知的内存开辟的方式,第一条语句是在栈空间开辟一个整型大小的空间,将val的值放到内存里面。第二条语句是在栈空间开辟10个连续的整型大小的空间,分别对这些空间初始化为0.

上述开辟空间的方式有三个特点:

1.空间开辟的大小是固定的,无法改变。

2.数组在创建时,必须指定数组的长度,它所需的内存在编译时分配。

3.这些空间都是开辟在栈区的,函数结束时,会自动回收,不需要手动释放。

那么,我们可以理解为上述的开辟方式是静态开辟的,而且需要提前知道要分配的空间大小,那么,遇到不符合上述的要求时,这些方法就没办法满足我们了,就只能通过动态内存分配实现。

2. 动态内存函数介绍

2.1 malloc和free

c语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向这块空间的指针。
  • 如果开辟失败,则返回一个NULL指针,通常我们会对开辟失败做相应处理。
  • 返回值的类型时void*,因为malloc是可以开辟任何类型的空间,我们在具体使用的时候需要根据我们的需求进行强制类型转换,因为void*是无法进行解引用的。
  • size_t 是一个无符号类型
  • size 是需要开辟空间的字节大小,如果size为0,这种标准是为定义的,取决于编译器。尽量避免这些未定义的行为。

C语言专门提供了一个free函数,是用来对动态内存进行释放和回收,函数如下:

void free (void* ptr);

这个函数的功能是用来释放动态开辟的内存。

  • ptr参数是一个void*类型的指针,可以接受任何的指针类型。
  • 如果ptr指向的空间不是动态开辟的,这种在c语言标准是未定义的,尽量避免。
  • 如果ptr是NULL指针,那么这个函数什么都不做。
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *ptr = NULL;
    prt = (int*)malloc(sizeof(int)*10); // 在内存开辟了十个整型空间的大小
    if(ptr == NULL){
        printf("开辟失败\n");
        return 0;    
    }
    int i = 0;
    for(i = 0;i<10;i++)
        *(ptr+i) = 0; // 等价于ptr[i] = 0;
    free(ptr); // 释放ptr指向这块空间
    ptr = NULL; // 这是防止ptr释放变成野指针,所以置为NULL
}

 2.2 calloc

c语言还提供了一个函数calloc进行动态内存开辟,函数如下:

void* calloc (size_t num, size_t size);
  • 函数的功能是开辟num个大小为size字节的空间,并且把每个空间的每个字节初始化为0
  • 该函数与malloc函数的区别在于开辟的空间的值自动初始化为0。

举个例子:

#include <stdio.h>      /* printf, scanf, NULL */
#include <stdlib.h>     /* calloc, exit, free */

int main ()
{
  int i,n;
  int * pData;
  printf ("请输入要创建几个int型的空间:");
  scanf ("%d",&i);
  pData = (int*) calloc (i,sizeof(int)); // 开辟i个int型的空间,全部为0
  if (pData == NULL) 
    return 0;
  free (pData);
  return 0;
}

2.3 realloc

C语言提供了一个函数realloc,可以对动态申请的内存进行灵活的调整。函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr是要调整的内存地址
  • size是调整后的新大小
  • 返回值为调整之后内存的起始位置
  • 这个函数调整原内存空间大小的基础上,会将原来内存的数据移到新空间上。
  • realloc函数调整会存在两种情况
    • 情况1:原有空间之后有足够大的空间,这时内存会直接在原有空间后面直接追加空间,原来空间的数据不发生变化。
    • 情况2:原有空间之后没有足够大的空间,这时会重新在堆空间(动态分配的都在堆空间开辟)找到一个合适大小的位置,将原有空间的数据移动到这个新空间上,再返回一个新的内存地址。

举个例子:

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

int main()
{
    int *ptr = (int*)malloc(100);

    if(ptr == NULL)
        return 0;

    // 拓展容量
    // 代码1
    ptr = (int*)realloc(ptr,1000); 
    
    // 代码2
    int *ptr_new = NULL;
    ptr_new = (int*)realloc(ptr,1000);
    if(ptr_new != NULL)
    {
        ptr = p;
    }

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

我们分析一下上述两种代码:

代码1:这样进行realloc函数看似好像没什么问题,给ptr指针指向的位置拓展1000个字节的大小,再用ptr指向这块新区域的起始位置,实际上也是这样的,但是如果申请失败呢?

我们知道申请失败的内存会返回一个NULL指针,那么这里假设我们申请失败,那么代码会变成ptr = NULL;这样子会造成内存丢失,原来的内存是没有被释放的,这就是内存泄漏的问题。

代码2:这段才是正确的处理,我们重新创建一个指针指向这块区域,即使申请失败也不会改变原来的指针。

3. 常见的动态内存错误

3.1 对NULL指针进行解引用

我们来看一段代码:

void test()
{
    int *p = (int*)malloc(INT_MAX);
    *p = 20;
    free(p);
}

我们先来分析一下上述代码的问题,首先我们在内存开辟一个INT_MAX大小的区域,INT_MAX是整型范围内的最大值,系统肯定不会真的给这么大一块的区域给我们,这时候会造成申请失败,返回一个NULL指针,那么我们没有对申请失败的情况进行判断,会造成直接对p进行解引用。正确如下:

void test()
{
    int *p = (int*)malloc(INT_MAX);
    if(p == NULL)
        return 0;
    *p = 20;
    free(p);
}

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

代码如下:

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

我们在对内存空间进行操作的时候应该与数组一样,避免越界去访问里面的元素。

3.3 对非动态开辟的内存使用free函数

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

3.4 使用free释放一块动态开辟内存的一部分

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

这段代码问题出在p++上面,在进行p++后,p指向起始位置后一个int大小的位置,这会造成我们在free这个p指针的时候,有一小块区域没有进行释放,造成出错。那么解决这种情况的办法如下:

void test()
{
    int *p = (int *)malloc(100);
    int *p1 = p;
    p++;
    free(p++);
}

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

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

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

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

虽然,我们动态开辟的内存,在系统结束后自动释放,但是上面这种代码,如果一直循环下去,那么这个程序不会结束,也就不会释放,这个时候就会造成内存泄漏的问题。

4. 经典笔试题目

4.1 题目1

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

这段代码会有什么结果。

可能你会以为是ptrintf函数出现了问题,但其实没错,printf函数本身就可以直接打印一个字符串,不一定需要

printf("%s",str);

其实这段代码问题出在GetMemory函数上,我们在传参数的时候通常有两种方式,一种是传值,一种是传地址,那么传值的时候,函数会创建临时变量,看下面代码:

void fun(int x)
{
    x = 10;
}
int main()
{
    int a = 5;
    fun(a);
    printf("%d\n", a);
}

这是我们在调试过程中,发现了x和a两个的地址根本不是同一个,所以这个x只是a的一份临时拷贝,那么我们修改x,也不会对a的值有任何影响。我们再回到原来的面试题,我们传进去str,用一个p指针接收 ,这里传的只是一个值,并不是一个地址,那么我们只是对str的临时拷贝p进行分配内存,那么在这个函数退出的时候,p就会自动销毁掉。

那么,如何解决这种情况呢?

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();
}

只需要传str的地址进去,这里传的是一个一级指针的地址,需要用一个二级指针接收,*p访问的是这个二级指针存放的地址,再进行内存开辟。

4.2 题目2

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

这段代码能否顺利打印结果呢?

答案是不能的,这段代码不是一个动态分配的问题,他问题出在返回p这个指针的时候,返回的是一个什么东西。

我们看到GetMemory函数的内部,内部创建了一个字符数组,存放了一个字符串,之后将这个字符数组首元素的地址返回给str,那么在这个函数结束的时候,创建的p会自动释放了,这个时候返回的一个地址也确确实实是p原本的地址,但是这一块内存已经没有原来的数据了。我们之后进行打印,也会是打印一些奇奇怪怪的值,而不是我们想要的结果。

4.3 题目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);
}

这段代码看似没有任何问题,也是通过用传地址的方式动态开辟内存,但是有一个很多新手都会犯的错误,就是忘记释放,这段代码在使用完这个内存,并没有进行释放。

4.4 题目4

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

这段代码的问题比较明显,那么就是对已经被free函数释放的指针继续使用。

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

我们刚刚提到了栈空间、堆空间,那么这些是什么意思呢?有什么关联?

其实,在我们的内存中有分为好几块区域:

  1. 栈区(Stack) : 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
    回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
    配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
     

6. 柔性数组

柔性数组(flexible array),这是c99 新增的一个东西,允许结构中最后一个元素是未知大小的数组。

struct st
{
    int i;
    int a[]; // 柔性数组成员
}; 

6.1 柔性数组的特点:

  • 结构中柔性数组成员前面至少包含一个其他成员
  • sizeof返回的这种结构大小不包括柔性数组的大小
  • 包含柔性数组成员的结构用malloc函数进行内存的动态分配,而且分配的内存应大于结构的大小。
struct st
{
    int i;
    int a[]; // 柔性数组成员
}; 

sizeof(struct st); // 结果为4

6.2 柔性数组的使用

typedef struct st
{
    int i;
    int a[]; // 柔性数组成员
}st; 

int main(){
    //代码1
    int i = 0;

    st *p = (st*)malloc(sizeof(st)+100*sizeof(int));

    p->i = 100;
    for(i=0; i<100; i++)
    {
        p->a[i] = i;
    }

    free(p);
}

你可以理解对这个结构进行malloc相当于对它的柔性数组成员进行malloc,上面这段代码就得到了100个整型元素的连续空间。

原网站

版权声明
本文为[CHAKMING1]所创,转载请带上原文链接,感谢
https://blog.csdn.net/CHAKMING1/article/details/124191817