Bootstrap

动态内存管理

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

2.malloc和free

3.calloc和realloc

4.常见的动态内存的错误

5.动态内存经典笔试题分析

6.柔性数组

7.总结C/C++程序内存区域划分

1.为什么要有动态内存分配

我们掌握的内存开辟方式有:

int val=20;//在栈区上开辟四个空间
char arr[10]={0};//在栈区上开辟10个字节的连续空间

以上的开辟空间方式有俩个特点:

1.开辟空间大小是固定的。

2.在VS环境中,数组的声明必须指定数组的长度,数组空间大小确定了就不能改变,

但是不是任何时候都能精准把握需要开辟多少空间,所有C语言引入了动态内存开辟,让程序员自己申请和释放空间,有灵活性。

2.malloc和free

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

void* malloc(size_t size);//需要一个无符号整型的参数

这个函数会向内存申请一块连续可用的空间,并返回一个指针(类型是void*,指向开辟的空间)。

1.如果开辟失败,会返回一个NULL指针,因此malloc的返回值需要检查。

2.返回值是void*类型的,malloc并不知道开辟空间的类型,具体类型需要使用者自己定义(用到强制转换)。

3.如果参数size为0,malloc的行为是标准未定义的(错误行为),取决于编译器。

free

C语言提供了另外一个函数free,专门用来做动态内存释放和回收。

void free(void* ptr);

1.参数需要一个地址(指向动态开辟的空间,如果不是会报错)。

2.如果ptr指向NULL指针,则函数无事发生。

malloc和free都声明在stdlib.h头文件中。

例子:

图中可知数组大小必须一开始确定好(一个确定的数)。

开辟4个整型大小的空间(int* ptr指向),但是这些都是未初始化的(随机值),开辟完后要用free函数释放掉开辟的空间,在把ptr指向的地方置NULL,不然会造成野指针(指向一块没有权限的地方)。

3.calloc和realloc

calloc

C语言还提供了一个函数叫calloc,这个函数也用来动态内存分配。

void* calloc(size_t num,size_t size);

1.函数的功能是为num个大小为size的元素开辟空间,并把空间的每个字节初始化为0.

2.与malloc的区别在于会不会把每个字节初始话以及参数不同。

如果想对申请的内存空间的内容要求初始化,可以使用calloc来实现。

3.realloc

1.realloc函数的出现是动态内存管理更加灵活。

2.又是开辟太少或者太多便可以使用realloc函数来解决。

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

1.ptr是要进行调整的内存地址(用malloc或者realloc开辟的空间会返回一个地址)

2.size是调整后的大小

3.返回值为内存起始位置

4.realloc在调整内存空间是存在俩种情况:

        情况1:原有的空间后面有足够的空间开辟(连续存放的下)

        情况2:原因的空间后面没有足够大的空间连续存放

对应情况1:在原有内存空间后面直接追加空间,返回的地址不变。

对应情况2:原有空间后面不够,则在堆区空间上找到另外一个合适大小的连续空间来使用(动态内存开辟是在堆区上开辟的)。则返回值就是一个新的内存地址。(之前的会自动被释放掉)

写程序的时候不知道原有空间后面也没有足够的空间,所以使用realloc函数就要注意一些。(申请失败就会返回NULL)

这个是不安全的使用方法:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = NULL;
	if (p != NULL)
	{
		//
	}
	else
	{
		return 1;
	}
	p = (int*)realloc(p, 100);//如果扩展失败了怎么办?
	return 0;
}

如果扩展失败会返回NULL空指针,不仅没多,而却原本的也没有了(指针不在指向之前开辟的空间),竹篮打水一场空。

下面是安全的realloc使用方法:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = NULL;
	if (p != NULL)
	{
		//
	}
	else
	{
		return 1;
	}
	int* pp = NULL;
	pp = (int*)realloc(p, 100);
	if (pp != NULL)
	{
		p = pp;//把开辟好的空间返回给p(指针赋值就是把指向的地方告诉被赋值的指针,让它也可以指向这个地方)
	}
	free(p);
	p = NULL;
    pp=NULL;
	return 0;
}

先让pp也指向开辟的空间,让pp去检验是否为空,不为NULL,则说明了扩展成功了,再把pp指向的地址赋给p。

另外realloc也可以当成malloc使用:

realloc(NULL,20)=malloc(20);

4.常见的动态内存错误

1.对NULL指针的解引用操作

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(INT_MAX / 4);//【C/C++】中常量 INT_MAX 和 INT_MIN 分别表示最大、最小整数 。 INT_MAX , INT_MIN 数值大小因为int占4字节32位,
	//根据二进制编码的规则,INT_MAX = 2^31-1,INT_MIN= -2^31。C/C++中,所有超过该限值的数,都会出现溢出,出现 warning,但是并不会出现error。
	// 如果想表示的整数超过了该限值,可以使用长整型 long long 占8字节64位。
	*p = 20;
	free(p);
	return 0;
}

因为malloc开辟的空间都是未初始化的(既有可能是NULL),但*这个解引号是不能解引用空指针。

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

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		exit(-1);//结束程序作用
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}
	free(p);//不free(pp)是因为pp和p指向的是同一个地方,
//把p释放了就不用多写一个了,只需要把pp置成NULL,避免野指针出现
	p = NULL;
    pp=NULL;
	return 0;
}

只开辟了10个整型大小的空间,但是在for循环里面访问了第11个空间。

3.对非动态开辟内存使用free释放

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

5.对同一块内存空间多次释放

6.动态内存空间忘记释放(内存泄漏)

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = NULL;
	p = (int*)malloc(4 * sizeof(int));
	
	return 0;
}

这个虽然不会报错,但是要保持一个好的习惯(开辟空间就要记得去释放)。

开辟空间未以正确释放,导致无法回收之前分配出去的内存,则无法被使用,导致内存浪费。

程序一直工作时,若开辟的空间不释放会越积越多直到挂掉,重启程序。

malloc/calloc/realloc申请的内存,如果不使用时可以用free,也可以让程序结束,操作系统会回收掉开辟的空间。

这种状况可能为造成为释放,但if条件成立,不会之执行下面的过程。

void test()
{
    int* p = (int*)malloc(100);
    //使用
    int n = 3;
    if (n > 0)
        return;

    free(p);
    p = NULL;
}

int main()
{
    test();
    
    while (1);
}

下面也是容易疏忽的地方,在调用函数中申请空间,要在函数中free,不能在main函数释放,因为int* p是在栈区上申请的变量(局部变量),指向开辟的空间,但出了test()函数就会被销毁掉,从而找不到指向动态开辟的空间。 

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

int main()
{
    test();

    while (1);
}

5.动态内存经典笔试题分析

题目1:

从整体来看没有free函数的影子,没有释放开辟空间,其次在调用GetMemory函数时传的时值,不是地址,这样是给形参p开辟一块空间,跟str没关系,str指向的还是NULL,而后面运用函数strcpy时,会有解引用过程,但str是空指针是不能被解引用的,所以程序会挂掉,不会打印任何东西。

只有通过传地址过去,用二级指针接受&str,这样在解引时是指针刚好对应malloc返回一个地址,是str指向开辟的空间,最后在释放str并置空。

第二种改法:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include<string.h>

char* GetMemory()
{
	char* p = (char*)malloc(100);
	return p;
}

void test()
{
	char* str = GetMemory();
	strcpy(str, "hello world");
	printf("%s",str);
	free(str);
	str = NULL;

}

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

题目二:

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

这个问题是什么呢?

首先就是调用函数部分,局部变量数组p返回是会被销毁的。指向空间被销毁了,未被覆盖的情况还是能打印出来,若被覆盖则不能打印。

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

str通过调用函数指向一块开辟的空间,但并没有用free函数释放空间。

题目4:

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

需要注意一点,free(str),但指针str还是指向开辟的空间(为置空的情况下),只是指向的空间是没有权限的(非法访问),所以if条件还是能执行(只是free掉了str但str还是能指向开辟的空间,但非法访问无权限了),然而在无权限的地方是不能操作的(strcpy函数是无权限执行的,错误的), 所以这个问题是为把释放的指针置空,这是主要问题。

6.柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

例如:

typedef struct st_typr
{
    int i;
    int a[];//柔性数组成员
}type_a;//重命名

柔性数组的特点:

1.结构体中的柔性数组成员前面必须至少有一个其他成员。

2.sizeof返回的这种结构大小不包括柔性数组的内存。(所以前面至少有一个成员,不然结构体的大小就为0,不合理)

3.包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性的预期大小。

例子:

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

typedef struct rx
{
	int i;
	int a[];

}rx;

int main()
{
	int i = 0;
	rx* p = (rx*)malloc(sizeof(rx) + 100 * sizeof(int));//第一个是结构体的大小,后一个是柔性数组的要开辟的大小
	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", p->a[i]);
	}
	free(p);
	p = NULL;
	return 0;
}


 

柔性数组a,开辟了100个整型元素的连续空间。

第二种方式:

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

typedef struct rx
{
	int i;
	int* a;

}rx;

int main()
{
	int i = 0;
	rx* p = (rx*)malloc(sizeof(rx));//第一个是结构体的大小,后一个是柔性数组的要开辟的大小
	p->i = 100;
	p->a = (int*)malloc(p->i * sizeof(int));//因为a的类型是int* 可以接受malloc开辟空间返回的地址
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;//a[i]=*(a+i)
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", p->a[i]);
	}
	free(p->a);//要先freep->a因为先free(p)会找不到p->a
	free(p);
	p = NULL;
	return 0;
}

上述代码都可以完成相同的功能,但是方法1实现有俩个好处:

第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤
⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能
指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返
回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你
跑不了要⽤做偏移量的加法来寻址)

7.总结C/C++程序内存区域划分

上面图片能清晰的了解变量属于哪一个地方。 

;