动态内存管理(2)

回顾:

1.为什么要有动态内存管理?

2.动态内存管理是在堆区上进行内存空间的开辟,开辟到是一块连续的空间

  • malloc -- 不初始化
  • realloc -- 初始化为0
  • realloc -- 调整内存空间的大小,如果第一个参数是NULL,那么它也具有malloc函数的功能

这里补充一下

int main()

{

        int * p = ( int* ) realloc ( NULL,40 );

        //等价于malloc ( 40 ) ;

        return 0 ;

}

  • free -- 释放动态内存开辟的内存空间的

1.常见的动态内存错误

1.1对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

正确做法:

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

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

局部变量进作用域范围创建,出作用域范围销毁

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

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

对于free函数的正确使用:

//正常p的释放是指向动态内存开辟空间的起始地址的
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	return 0;
}

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

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

1.6动态开辟内存忘记释放(内存泄露)

//正常的使用,开辟完,在函数的结尾要释放掉这块空间
void get_memory()
{
	int* p = (int*)malloc(40);
	//使用...
	free(p);
	p = NULL;
}
//函数会返回动态开辟空间的地址,记得在使用之后释放
int* get_memory()
{
	int* p = (int*)malloc(40);
	//...
	return p;
	//此时p开辟的空间没有释放掉
}
int main()
{
	int* ptr = get_memory();
	//释放
	free(ptr);
	ptr = NULL;//现在申请的40个字节空间回收了
	//如果没有释放这申请的40个字节的空间就造成了内存泄露
	return 0;
}

2.几个经典的笔试题

2.1 题目1:

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;
}
  • 现在str内部放NULL,GetMemory函数调用将str传过取,p是指针变量得到str的内容,p得到NULL,然后为p开辟100个字节的空间,但是这些操作对p而言的,对str没有什么影响(p是一个变量,str是另外一个变量),调用完之后,到strcpy函数这里,此时ptr还是空指针,所以不能将Hello world放到str中(核心问题)
  • 调用GetMemory函数的时候,str的传参为值传递,p是str临时拷贝,所以GetMemory函数内部将动态开辟空间的地址存放在p中的时候,不会影响str,所以GetMemory函数返回之后,str中依然是NULL指针。strcpy函数就会调用失败,原因是对NULL解引用操作,程序会崩溃
  • 存在内存泄露,没有释放p所开辟的空间(并且p在Test函数中并不能释放,因为p出了作用域的范围就被销毁,所以并不能在Test函数中释放)
  • 还没有对malloc函数的返回值进行合理的判断

上面代码的修改

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

int main()
{
	Test(); 
	return 0;
}
//此时就能打印出Hello world了

2.2 题目2:

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

并不能打印出hello world,p是局部的数组,p出了作用域就被销毁了,p这块空间的内容换给了操作系统

返回栈空间地址的问题,数组是局部变量在栈中开辟,而栈上的空间出了作用域就被销毁

GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给str,但是数组的内存出了GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str,str就是野指针

注意下面这段代码是没有问题的

 a这块空间在销毁之前会找一个寄存器把a的内容放进去,a出了作用域范围被销毁,然后将寄存器中的内容放到ret中

 此时这样写又变成了栈空间地址的问题,但是此时我们还能打印出10,这是为什么?

因为此时没有其他变量使用这块空间,所以这块空间的内容放的还是10,当有其他变量时,就会将这块空间覆盖掉,这时就不能打印出10

2.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);
}
int main()
{
	Test();
	return 0;
}

这里能打印出hello,但是存在内存泄露的问题,在Test函数内部free(str),并将str=NULL;

2.4 题目4:

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

这段代码打印出world,虽然打印结果,但本质还是错误的;这段代码存在非法访问,给str开辟了100个字节的空间然后又释放掉,此时str没有置为NULL,还记得这段已经被释放空间的起始地址,str确实不等于NULL(这里str时野指针),接下的操作(strcpy)就是非法访问

修改:

1.在开辟好空间之后判断是否开辟成功,if(str==NULL)return 1;

2.在free函数后面,将str置为空(str=NULL;)

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

 局部变量和函数参数是放到栈区上的,动态内存管理放到堆区,静态变量和全局变量放到静态区中(数据段)

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

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

实际上普通的局部变量是在栈区分配空间的,栈区的特点是创建的变量出了作用域就销毁,但是被static修饰的变量存放在数据段(静态区),数据段的特点是创建的变量直到程序结束才销毁,所以生命周期变长

4.柔性数组

在C99中,结构中的最后一个元素允许是未知的大小的数组,这就叫做柔性数组成员

eg:

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

4.1柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof返回的这种结构大小不包括柔性数组的大小
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小--->在创建结构体变量的时候,这个变量中是没有这个柔性数组的,只有n和s(下面这张图片的代码),所以包含柔性数组时,整个结构体(不仅仅给n和s开辟空间,还要给柔性数组开辟空间)应该使用malloc函数来开辟空间

4.2柔性数组的使用

struct S
{
	int n;
	float s;
	int arr[];//[柔性]数组成员
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
	//既为n、s开辟空间,也为arr开辟了空间
	if (ps == NULL)
	{
		return 1;
	}
	//使用
	ps->n = 100;
	ps->s = 5.5f;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		ps->arr[i] = i;
	}
	printf("%d %lf\n", ps->n, ps->s);
	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//调整,使用realloc函数进行调整
	struct S*ptr=(struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 10);
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps = ptr;
	}

	//释放
	free(ps);
	ps = NULL;

	return 0;
}

4.3柔性数组的优势

struct S
{
	int n;
	float s;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;
	ps->n = 100;
	ps->s = 5.5f;
	int* ptr = (int*)malloc(4 * sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	//使用
	printf("%d %lf\n", ps->n, ps->s);
	int i = 0; 
	for (i = 0; i < 4; i++)
	{
		scanf("%d",&(ps->arr[i]));
		printf("%d ", ps->arr[i]);
	}
	//调整
	//调整的时候只需要调整arr
	realloc(ps->arr, 10 * sizeof(int));
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

上面是不使用柔性数组的方法,这种方法使用两次malloc,很有可能这次malloc开辟的空间不是一块连续的空间,有可能是分开的两段空间,造成内存碎片(内存和内存之间存在缝隙,这个内存碎片空间也不好使用),malloc次数越多,出现内存碎片的可能性越大,内存的利用率下降,并且需要两次释放,如果忘记释放就会造成内存泄露,所以还是使用柔性数组更好一些

柔性数组的优点:

  • 第一个好处是:方便内存释放
    • 如果我们的代码是在一个给别人用的函数中那个,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉
  • 第二个好处是:这样有利于访问速度
    • 连续的内存有益于提高访问速度,也有益于减少内存碎片。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值