动态内存管理(2)

4.经典笔试题

4.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 函数会有什么样的结果?

第一个问题:传参的是指针,然后GetMemory函数里面使用malloc开辟了100个字节的空间,并且将这块空间的起始地址赋给了p,p是一个临时变量,出了这个函数就不在了,找不到这块空间了,所以str还是NULL,所以对NULL进行解引用操作了

第二个问题:这块空间在使用完之后没有进行free释放,就会出现内存泄漏的问题。

修改:将str的地址作为参数,用二级指针接收,解引用找到str的地址,这样就能为str开辟空间。在使用完之后用free释放再置为空指针就行了。

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

4.2 题目2:

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

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

p是一个临时变量,出了函数会销毁,返回p的地址给str,地址是返回了,但是里面的内容是不存在了,所以str在打印的时候就是一个野指针了。这就是一个返回栈空间地址的问题。局部变量都是在栈上开辟空间的,出了函数就销毁了,如果返回地址的话,就会变成野指针,这块空间已经不属于p了。

4.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 函数会有什么样的结果?

这代码其实没有很大的问题,唯一的问题就是没有free释放,存在内存泄漏的问题。

修改:在printf之后进行free释放,然后将str置为NULL。

4.4 题目4:

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

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

首先是为str开辟100个字节的空间,然后拷贝字符串“hello”,然后进行释放。free只是把这块空间还给了操作系统,但是指针变量还是这个地址,所以if依然为真,但是这个str是野指针了,使用strcpy就出现错误,非法访问内存。


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

局部变量和形式参数在栈区开辟空间,malloc,realloc,calloc是在堆区开辟空间,数据段也就是静态区,,代码段的数据是不能被修改的,否则代码会出现错误,所以常量就放在这块区域。

动态内存管理(2)_第1张图片

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

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

有了这些文字,我们就能更加理解static关键字修饰局部变量的例子了。

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


6. 柔性数组

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

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

有些编译器会报错无法编译可以改成:

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

 6.1 柔性数组的特点:

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

 例如:

按照正常来说c占1个字节,i占4个字节,然后按照结构体内存对齐要浪费掉3个字节,就相当于以及占用了8个字节,所以这个结构体的大小至少是8.那么答案是多少呢?

struct S
{
	char c;//1
	//浪费3个字节
	int i;//4
	int arr[];
};
int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

答案就是8,这就是柔性数组的特点,sizeof 返回的这种结构大小不包括柔性数组的内存

在拥有柔性数组这个成员的结构体在开辟内存空间的时候要额外开辟空间,用以适应柔性数组的大小。比如下面这个代码使用malloc开辟空间就在后面加了20个字节,这20个字节就是为柔性数组arr预留的,通过arr访问时就是访问后面这20个字节。当使用realloc增加空间时,就是在这20个字节后面增加,也就是给arr增加的空间。但是柔性数组必须在结构体中,且使用malloc开辟空间才能有效果。

struct S
{
	char c;//1
	//浪费3个字节
	int i;//4
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
	if (ps = NULL)
	{
		perror("malloc");
		return 1;
	}
	printf("%d\n", sizeof(struct S));
	return 0;
}

 6.2 柔性数组的使用

20个字节就是5个整型,所以我们可以放进去5个整型,使用完这块空间之后就free释放。

struct S
{
	char c;//1
	int i;//4
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->c = 'w';
	ps->i = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

如果觉得空间不够,就使用realloc扩容,第一个参数ps是需要增容的空间的起始地址,后面加上40,相当于扩容20个字节,如果增容成功,则将ptr赋给ps。

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->c = 'w';
	ps->i = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	struct S* ptr=(struct S*)realloc(ps,sizeof(struct S) + 40);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	else 
	{
		prrror("realloc");
		return 1;
	}
	free(ps);
	ps = NULL;
	return 0;
}

 6.3 柔性数组的优势

柔性数组当然是可以使用其它办法代替的,比如我们使用一个指针来指向一块空间,然后使用malloc为这块空间开辟内存也可以。需要注意的是使用了两次malloc开辟空间,但是怎么free呢?必须要先free掉data指向的那块空间,因为如果先释放掉结构体的空间,这块空间包括data的空间,就找不到data的空间了,所以就得先释放掉data这块空间。

struct S
{
	char c;//1
	int i;//4
	int* data;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->c = 'w';
	ps->i = 100;
	ps->data = (int*)malloc(20);
	if (ps->data == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		ps->data[i] = i;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", ps->data[i]);
	}
	int* ptr=(int*)realloc(ps->data,40);
	if (ptr != NULL)
	{
		ps->data = ptr;
	}
	else 
	{
		prrror("realloc");
		return 1;
	}
	free(ps->data);
	ps->data = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

虽然这两种方法都能实现一样的效果,但是有各自的优缺点。

像柔性数组就有两个好处:

第一个好处是:方便内存释放

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

第二个好处是:这样有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)


今天的分享到这里就结束啦!谢谢老铁们的阅读,让我们下期再见。

你可能感兴趣的:(C语言,算法,开发语言,c语言,学习,1024程序员节)