云计算百科
云计算领域专业知识百科平台

动态内存管理(下)

一、动态内存经典笔试题分析

1.1 题目1:

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

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

        程序会发生崩溃。原因是:str 初始为 NULL,调用 GetMemory(str) 时,由于参数是值传递,函数内部虽然执行了 malloc(100),但并没有改变外部的 str,返回后 str 仍然是 NULL。随后执行 strcpy(str, "hello world"); 时,相当于向空指针写入数据,属于非法内存访问,程序会直接崩溃或异常终止。

1.2 题目2:

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

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

        输出乱码。原因在于:GetMemory 函数中定义的 char p[] = "hello world"; 是一个局部数组,存储在函数栈帧里。当函数返回时,这块栈空间就失效了,但函数却把它的地址返回给了外部。于是 str 得到的是一块已经被释放(无效)的内存地址,称为野指针 / 悬空指针。随后执行 printf(str); 就是在访问无效内存.

1.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);
}

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

        输出 "hello"。函数中,str 初始为 NULL,通过调用 GetMemory(&str, 100) 在堆上分配了 100 字节内存,并把地址回传给 str。随后,strcpy(str, "hello") 将字符串写入这块内存,再通过 printf("%s", str) 打印出来。

1.4 题目4:

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

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

        会输出world。这段代码的解析如下:函数 Test 首先用 malloc(100) 为指针 str 在堆上分配了 100 字节内存,然后用 strcpy(str, "hello"); 将 "hello" 写入该内存。接着调用 free(str); 释放内存,但指针 str 本身仍然保存着原来的地址,这时它成为 悬空指针。之后,代码判断 if (str != NULL),条件成立,因为指针本身非空,但它指向的内存已经被释放。随后执行 strcpy(str, "world"); 和 printf(str)。

二、柔性数组

        也许你从来没有听说过柔性数组这个概念,但是它确实是存在的。 C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。 例如:

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

2.1柔性数组的特点

         结构中的柔性数组成员前面必须至少一个其他成员。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

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

}type_a;

int main()
{
    printf("%d\\n", sizeof(type_a));//输出的是4
    return 0;
}

2.2柔性数组的使用

代码1:

#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
//业务处理

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

        这样柔性数组成员a,相当于获得了100个整型元素的连续空间。这首先,定义了结构体 type_a,其中 i 是普通整型成员,a[] 是灵活数组成员,没有固定长度。main 函数中,调用 malloc(sizeof(type_a) + 100 * sizeof(int)) 为结构体本身加上 100 个整数的数组空间在堆上分配内存,并将返回的地址赋给指针 p。接着检查 malloc 是否成功,如果失败直接返回。然后通过 p->i = 100; 给普通成员赋值,再用循环将 0~99 的整数依次写入灵活数组 p->a[i]。最后通过 printf 打印结构体成员 i 以及数组首尾元素,验证数据正确性。

2.3柔性数组的优势

        上述的type_a 结构也可以设计为下面的结构,也能完成同样的效果。

代码2:

#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int* p_a;
}type_a;
int main()
{
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int*)malloc(p->i * sizeof(int));
//业务处理

for (int i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
//释放空间

free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}

      首先定义结构体 type_a,包含一个整数 i 和一个整型指针 p_a,用于指向动态分配的数组。在 main 中,先用 malloc(sizeof(type_a)) 为结构体本身在堆上分配内存,并将返回的地址赋给指针 p。然后设置 p->i = 100;,表示数组长度为 100,再通过 p->p_a = malloc(p->i * sizeof(int)) 为指针成员 p_a 分配 100 个整数的内存。

        接着通过循环 for (int i = 0; i < 100; i++) p->p_a[i] = i; 将 0~99 的值写入数组,实现业务处理。使用完数组和结构体后,先 free(p->p_a) 释放数组内存,并将指针置 NULL 避免悬空指针,再 free(p) 释放结构体本身,同时将 p 置 NULL,保证安全。

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

1.方便内存释放 如果我们的代码是在一个给别⼈用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

2.这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。

三、总结C/C++中程序内存区域划分

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

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

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

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

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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 动态内存管理(下)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!