zl程序教程

您现在的位置是:首页 >  其他

当前栏目

C语言进阶——动态内存管理(下)

2023-03-14 23:00:03 时间


🌲六、动态内存开辟笔试题

 下面是几道比较经典的动态内存开辟笔试题,看完这些题后我们对动态内存的理解能提升一个层次!


 题目出自经典书籍《高质量C/C++编程》


🌱第一题

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


//第一题

//第一题
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;
}

 第一题中的主要错误是对空指针的解引用,出现空指针的原因:给 GetMemory 函数传值,然后再进行动态内存开辟,无法对实参 str 造成影响,相当于此时的 str 仍然是一个空指针,对空指针解引用是非法的。当然此题还有其他错误,下面来看看详解:


1.传值调用,即使成功开辟空间,也不会对实参 str 造成影响


2.没有对函数 GetMemory 中的开辟情况进行判断


3.对空指针 str 的解引用(strcpy 会对其进行解引用)


4.没有对开辟的内存进行释放(显然此时只能在 GetMemory 中释放)


🪴纠正方案


 将上面的错误逐个解决就好了,下面来看看纠正后的代码:


//第一题
void GetMemory(char** p)
{
    *p = (char*)malloc(100);
    if (*p == NULL)
        exit(-1);//申请失败就直接结束程序
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str);//传址调用
    strcpy(str, "hello world");//合法解引用
    printf(str);//这种打印方法是合法的
 
    free(str);//释放
    str = NULL;//置空
}
int main()
{
    Test();
    return 0;
}

56eda41f69dd4d3ca23fbd9891bd1eb6.png

🌱第二题

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

//第二题
char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    Test();
    return 0;
}

 第二题中的主要错误是使用已经回收的空间,GetMemory 中的 p 作为数组首元素地址,同时也是一个位于函数中的局部变量,生命周期仅仅在函数内部 ,函数结束后的指针 p 指向空间已被回收。此时即使得到了 p 指向空间的地址,也无法打印出之前的值。换句话说,此时的指针 str 指向空间是一块全是随机值的空间,强制打印会得到一串乱码。


🪴纠正方案


 将数据存放在静态区中,这样在函数 Test 中也能使用了。


 至于为什么不直接在堆上申请,使用完后释放?原因很简单,如果想把数据存储在堆区上,需要挨个存入,之后才能正常释放,就拿字符串 "hello world" 来说,需要一个字母一个字母的存,如果直接让指针 p 指向字符串常量 "hello world" 的话,也能达到打印的效果。但释放就不行了,因为 p 此时指向的是只读数据区(非堆区)


//第二题
char* GetMemory(void)
{
    static char p[] = "hello world";//存放在静态区中
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    Test();
    return 0;
}

🌱第三题

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

//第三题
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;
}

 第三题中的主要错误是没有对已开辟的空间进行释放,这样会造成内存泄漏;其次就是没有对开辟的空间进行判断。短期来看这段代码并没有大问题,但如果此段代码日夜不停的运行,不断申请空间,却又不释放,长此以往内存就泄漏了,是个比较严重的问题。


🪴纠正方案


 在申请空间后进行判断,使用完内存后记得释放就行了。

//第三题
void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
    if (*p == NULL)
        exit(-1);//申请失败
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
    free(str);//释放
    str = NULL;//置空
}
int main()
{
    Test();
    return 0;
}


🌱第四题

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

//第四题
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;
}


 第四题主要问题是将 str 释放后,仍然对其进行操作(野指针),这会导致难以预料的后果;另一个小问题就是没有对开辟的空间进行判断。当然 free 语句把 ptr 指向空间释放后,其中的内容会变成随机值,实际上 str != NULL 这条语句是不会起作用的。


🪴纠正方案


 将释放后置空、申请后判断的语句加上就行了

//第四题
void Test(void)
{
    char* str = (char*)malloc(100);
    if (str == NULL)
        return 1;//申请失败
 
    strcpy(str, "hello");
    free(str);//释放
    str = NULL;//置空
 
    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
int main()
{
    Test();
    return 0;
}


🌲七、C/C++中的内存区域划分

 我们都知道,C++ 是 C语言 的超集,因此二者在内存区域划分基本一致。主函数、局部变量、返回地址等占用空间小的数据是存放在栈区上的;而占用空间大或程序员指定存放的数据是存放在堆区上的;全局变量、静态数据等则是存放在静态区(数据段)中。


a0b5d96d2d2b4b3ba88dc79828ea2798.png


1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

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

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

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


🌲八、柔性数组


🌱声明

 柔性数组(flexible array),这是个出现在C99标准中的新特性,其表现形式为数组作为结构体中最后一个成员,且数组前面至少有一个其他成员。


c20bc9d216b241c1a0a2f05449e09bd8.png

//柔性数组的形式
struct Test
{
    int a;
    char b;
    int arr[];//这就是柔性数组
};
int main()
{
    printf("包含柔性数组结构体的大小:%d
", sizeof(struct Test));
    return 0;
}

599c5907909f461ab7b484df225f0bbf.png

 可以看到,在计算包含柔性数组结构体的大小时,并未包含此数组的大小,说明此结构体中的最后一个成员(柔性数组)的大小是可控的,而要让大小可控,就需要用到我们前面介绍的动态内存管理函数,这也正是柔性数组柔的原因。


🌱使用

那么柔性数组该怎么使用呢?一起来看看下面这个例子吧


 此时结构体中的柔性数组获得了100个整型大小的空间,可以随意使用,如果觉得不够用了,还可以通过 realloc 再次扩容

//柔性数组的使用
struct Test
{
    int i;
    int arr[];//柔性数组
}T1;
int main()
{
    struct Test* p = (struct Test*)malloc(sizeof(struct Test) + sizeof(int) * 100);//获取100个整型大小的空间
    if (p == NULL)
        return 1;
    T1.i = 100;
    int i = 0;
    for (i = 0; i < 100; i++)
    {
        T1.arr[i] = i;
        printf("%d ", T1.arr[i]);
    }
    free(p);//释放
    p = NULL;//置空
    return 0;
}

1f8ec8fedb8c4a54aaa6eacab5ffd6e9.png


🌱注意

注意


1.柔性数组前至少要有一个其他成员

2.sizeof 计算结构体大小时,并不会包含柔性数组的大小

3.在对柔性数组进行空间分配时,一定要包含结构体本来的大小

4.柔性数组是C99中的新特征,部分编译器可能不支持

🌱模拟实现柔性数组

 既然我们拥有众多动态内存管理神器,能否直接通过对一个指针指向空间的再次申请来模拟实现柔性数组呢?答案是可以的,不过会有些麻烦:


//模拟实现柔性数组
struct Test
{
    int i;
    int* p;
}T1;
int main()
{
    struct Test* ptr = (struct Test*)malloc(sizeof(T1));//先在堆上开辟空间
    if (ptr == NULL)
        return 1;
    T1.p = (int*)malloc(sizeof(T1) + sizeof(int) * 100);
    if (T1.p == NULL)
        return 1;
    T1.i = 100;
    int i = 0;
    for (i = 0; i < 100; i++)
    {
        T1.p[i] = i;
        printf("%d ", T1.p[i]);
    }
    //需要释放两次
    free(T1.p);
    T1.p = NULL;
    free(ptr);
    ptr = NULL;
    return 0;
}

 光是动态内存申请和内存释放就需要操作两次,而且还有很多隐藏问题,而这些问题在柔性数组中可以得到避免


🌱柔性数组的优势

 既然柔性数组是作为一个C语言的新特征而出现的,那么其设计者在设计语法的时候肯定考虑到了上面的问题,于是才会出现这么个新特征。


优势


1.不易于产生内存碎片,有益于提高访问速度

2.方便内存释放(只需要释放一次)  

🌳总结

 以上就是关于C语言中动态内存管理的全部内容了,我们从 malloc 开始,到柔性数组结束,学习了多种动态内存开辟的方式,还了解C/C++中的内存区域划分。这样我们以后在编写程序的时候,就可以不用把数据全都存放在栈区了,可以往堆区中存,毕竟那儿空间大;还可以通过函数灵活使用堆区中的空间,我想这正是C语言灵活强大的原因之一吧。能力越大,责任越大,我们在每次使用完开辟出的空间后,都要对其进行释放,不要引发内存泄漏这样的严重问题。总而言之,我们可以去尝试使用动态内存管理函数了!

9e86efae0ca446138bd87bcd10125582.jpg

 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正