「地表最强」C语言(十三)动态内存管理,含柔性数组

环境:CLion2021.3;64位macOS Big Sur


地表最强C语言系列传送门:
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数
「地表最强」C语言(十七)阅读程序

十三、动态内存管理

13.1 为什么存在动态内存分配

举一个简单的例子:张三接触到了数组没多久,想用数组将自己好朋友全部保存起来,于是定义了一个可以存放10个人的数组;过了几天他想试着将全班的人的信息都存放在数组中,于是便重新定义了一个可以存放50个人的数组;又过了几天他想将和他同届的人全部存放在数组中,于是又重新定义了一个可以存放1000人的数组;又过了几天…
就这样不断的折腾,有一天他瘫坐在椅子上心想:要是数组的大小能够动态分配就好了,这样既不会浪费空间,也不会造成空间不足的情况,而且维护起来还很方便。
在这里插入图片描述

13.2 动态内存函数的介绍:malloc、free、calloc、realloc

  1. void* malloc( size_t size )
    malloc函数向内存的堆区申请一块连续可用的空间,并返回指向这块空间的指针:
    (1)若开辟成功,则返回一个指向开辟好空间的指针
    (2)若开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做空指针检查
    (3)返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定,即强制类型转换
    (4)如果参数size为0,malloc的行为是标准为定义的,取决于编译器
	int *p = (int *) malloc(10 * sizeof(int));
    //使用这些空间的时候要检查是否为空指针
    if (NULL == p) {
        perror("main");//若有错误,打印main:xxxxxxx
        return 0;
    }
    //使用
    for (int i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);//*(p+i)
    }
    //	回收空间void free( void* ptr ),和malloc等成对出现
    //  如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
    //  如果参数ptr是NULL指针,则函数什么都不做。
    free(p);//只是将空间还给操作系统,p中仍然存放着那块空间的地址,为了防止非法访问,最好将p置空
    p = NULL;//手动将p置空
  1. void* calloc( size_t num, size_t size )
//	分配10个int大小的空间
    int *p = (int *) calloc(10, sizeof(int));//calloc会对分配的内存初始化,malloc不会;参数不一样,其他和malloc完全一样
    if (NULL == p) {
        perror("main");
        return 1;
    }
    for (int i = 0; i < 10; i++)
        printf("%d ", p[i]);
    free(p);
    p = NULL;
  1. void *realloc( void *ptr, size_t new_size )
    (1)若原空间后边的空间足够新的空间,直接将后边的空间分配给p,返回原地址p;
    (2)若原空间后边的空间不足够开辟新的空间,则会新开辟一块空间,将原空间的数据拷贝到新空间,返回新空间地址,将原空间释放;
    (3)若找不到合适的空间来调整大小,则返回空指针。因此不要直接用原指针接收返回值,防止原地址丢失。
    int *p = (int *) calloc(10, sizeof(int));//先分配10个int的空间
    if (NULL == p) {
        perror("main");
        return 1;
    }
    for (int i = 0; i < 10; i++)
        *(p + i) = 5;
    //若此时需要p指向的空间更大,需要20个int的空间,使用realloc调整空间
    //  若原空间后边的空间足够新的空间,直接将后边的空间分配给p,返回原地址p;
    //  若原空间后边的空间不足够开辟新的空间,则会新开辟一块空间,将原空间的数据拷贝到新空间,返回新空间地址,将原空间释放;
    //  若找不到合适的空间来调整大小,则返回空指针。因此不要用原指针接收返回值
    int *ptr = (int *) realloc(p, 20 * sizeof(int));
    if (NULL != ptr)
        p = ptr;//确定分配成功再赋值给p,方便维护
    free(p);
    p = NULL;

//	realloc(NULL,40)当第一个参数传递空指针时,效果和malloc一样
  1. void free (void* ptr)
    回收申请的空间,一般和上面三个函数成对出现

13.3 常见的动态内存错误

  1. 对空指针的解引用操作,解决方法:对malloc函数的返回值做判空判断
    int* p = (int*)malloc(10000000000000);
    for (int i = 0; i < 10; ++i) {
        *(p+i) = i;
    }
    申请的空间太大,导致申请失败,而申请失败则会返回空指针,即p其实是空指针,后又对其解引用,导致错误
  1. 对动态开辟空间的越界访问
    int *p = (int *) malloc(10 * sizeof(int));
    if (NULL == p)
        return 1;
    //越界访问只开辟了10个int型的空间
    for (int i = 0; i < 40; ++i) {
        *(p + i) = i;//只开辟了10个int型的空间,无法访问到10以后的空间
    }
    free(p);
    p = NULL;
  1. 对非动态开辟内存使用free释放
    int arr[10] = {0};//局部变量,存放在栈区
    int* p = arr;
    free(p);//err,p不是动态开辟的空间,不能用free释放
    p = NULL;
  1. 使用free释放一块动态开辟内存的一部分
    int *p = (int *) malloc(10 * sizeof(int));
    if (NULL == p)
        return 1;
    for (int i = 0; i < 5; ++i) {
        *p++ = i;
    }
    free(p);//此时p已经不指向动态开辟空间的首地址,此时free有两个风险:
    //1.只free掉一部分,这是个操作本身就很离谱       
    //2.动态申请的空间的起始位置无法找到,可能会内存泄漏
    p = NULL;
  1. 对同一块动态内存多次释放
    int* p = (int*)malloc(100);
    free(p);
    free(p);//err,避免方法:第一次释放完后p=NULL;free(NULL)什么事情都不会发生
  1. 动态开辟内存忘记释放(内存泄漏)
    动态开辟的空间只有两种情况才能被销毁:
    (1)主动free
    (2)程序结束,注意是程序而非函数
void test()
{
    int* p  = (int*)malloc(100);
    if(NULL == p)
        return;
    //假装在这里使用了p,但是最后没有释放
}
int main()
{
	test();//出了这个函数,想释放都无法释放,因为局部变量p已经被销毁,再也无法找到那块儿空间
	//这样每次调用test都会泄漏一点儿内存,久而久之内存就会被占满
	return 0;
}

13.4 几个经典的笔试题

void GetMemory(char* p)
{//p是str的一份临时拷贝,p改变不会影响str
    p = (char*)malloc(100);//出函数后行参p销毁,且没有释放空间,导致内存泄漏
}
void Test(void)
{
    char* str = NULL;
    GetMemory(str);//值传递,并没有改变str,str仍是空指针
    strcpy(str,"Hello World");//往空指针里拷贝,失败
    printf(str);
}
int main()
{
	Test();
	return 0;
}
//改正确1
void GetMemory(char* *p)
{
    p = (char*)malloc(100);//出函数后行参p销毁,且没有释放空间,导致内存泄漏
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str);
    strcpy(str,"Hello World");//往空指针里拷贝,失败
    printf(str);
    free(str);
    str = NULL;
}
//改正确2
char* GetMemory(char* p)
{
    p = (char*)malloc(100);//出函数后行参p销毁,且没有释放空间,导致内存泄漏
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory(str);
    strcpy(str,"Hello World");//往空指针里拷贝,失败
    printf(str);
    free(str);
    str = NULL;
}
//返回栈空间地址的问题
char *GetMemory(void) {
    char p[] = "hello world";//数组是在栈上创建的,出了此函数,这块儿空间就还给了操作系统,即使返回地址,也是没有意义的,若访问就是非法访问
    return p;//虽然可以return字符串的首地址,但是出了这个函数后,局部变量p就被销毁了,原空间有可能已经被其他的程序使用,而不是hello world

}
void Test(void) {
    char *str = NULL;
    str = GetMemory();
    printf(str);//非法访问空间
}
int main()
{
	Test();
	return 0;
}
void GetMemory(char** p,int num)
{
    *p = (char*)malloc(num);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str,"Hello World");
    printf(str);
    //使用结束后没有free,导致内存泄漏
}
int main()
{
	Test();
	return 0;
}
void Test(void)
{
    char* str = (char*)malloc(100);
    if(str == NULL)
    	return ;
    strcpy(str,"hello");
    free(str);
    //free不会将str置空,但是已经将空间还给操作系统了,再访问就是非法的
    //考察的是free后一定要置空
    if(str != NULL)
    {
        strcpy(str,"world");
        printf(str);
    }
}

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

13.5 柔性数组

C99中,允许结构体中的最后一个元素是未知大小的数组,这就叫做柔性数组成员。
柔性数组成员前必须至少有一个其他成员;
sizeof计算带有柔性数组的结构体的大小不包括柔性数组的大小;
包含柔性数组成员的结构用malloc函数进行动态内存分配,并且分配的内存大小应该大于结构体的大小,以适应柔性数组的预期大小。

struct S {
    int n;
//    int arr[];//大小未知,是柔性数组,写法1
    int arr[0];//大小未知,是柔性数组,写法2,即方括号内的数字为0或空则是柔性数组
};
int main()
{
    struct S s = {0};//实际上不应该这样创建对象,而应该动态内存分配,这里只是为了展示一下大小的计算。
    printf("%d ", sizeof(s));//4,说明计算带有柔性数组的结构体大小,计算的结果不包括柔性数组
	return 0;
}

柔性数组的使用:

int main()
{
    struct S* ps = (struct S*)malloc(sizeof(struct S) + 10*sizeof(int));
    //动态分配内存时,柔性数组所需空间要额外计算,原因上边已经说明,这里假设暂时需要10个int的空间
    if(ps == NULL)
    	return 1;
    //使用
    ps->n = 10;
    for(int i = 0; i < 10; i++)
    	*(ps->arr[i]) = i;
    //假设发现数组大小不够用,需要20个int的空间,于是扩容
    struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if(tmp != NULL)
    	ps = tmp;
    //假装在这里使用了扩容后的ps
    //使用后释放
    free(ps);
    ps = NULL;
	return 0;
}

其实柔性数组就是可以动态的改变结构体中数组的大小,我掐指一算发现,即使不使用柔性数组,也可以实现相同的功能,只要将结构体内的数组单独动态开辟内存就可以了;而动态内存开辟的空间是放在堆区的,那么对于这个结构体的其他变量,想要也放在堆区的话(根据空间局部性原理,这样可以提高效率,后面总结会说明),结构体也需要动态开辟内存,于是一个想法产生了:

struct S2 {
    int n;
    int *arr;
};
int main()
{
	struct S2* ps2 = (struct S2*)malloc(sizeof(struct S2));
	if(ps2 == NULL)
		return 1;
	ps2->n = 10;
	ps2->arr = (int*)malloc(10 * sizeof(int));
	for(int i = 0; i < 10; ++i)
		ps2->arr[i] = i;
	//发现数组不够用,想扩容到20个int的空间
	int* tmp = (int*)realloc(ps2->arr,10 * sizeof(int))
	if(tmp != NULL)
		ps2->arr = tmp;
	//假装使用
	//使用完毕,释放
	free(ps2->arr);//注意先释放数组,若先释放结构体,数组这块儿空间将找不到,导致内存泄漏
	ps2->arr = NULL;
	free(ps2);
	ps2 = NULL;
	return 0;
}

对比发现使用柔性数组的好处:

  1. 内存释放方便:可以看到,不使用柔性数组的时候,结构体的数组成员变量也是需要动态分配的,这样一来就造成了使用结构体需要进行两次内存分配,也就意味着结束时需要调用两次free,这就容易发生错误
  2. 有利于提高访问速度:实际上,单次动态内存分配分配的是连续的空间,但是多次动态内存分配分配的并不是连续的空间,不知道在哪里分配的,因此多次分配就容易造成内存碎片的产生,这样内存利用率也降低了

内存池:为了提高内存利用率,操作系统为当前程序分配的一块儿内存,此程序就使用这块内存
局部性原理:
(1)空间局部性:当使用一块儿内存的时候,接下来80%的可能会使用此块内存周边的内存,因此采用柔性数组时,整个结构体内的成员的地址都是较为连续的,访问效率也就提高了,而不使用柔性数组,多次动态开辟的空间在内存上不是连续的,影响效率
(2)时间局部性

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值