C动态内存分配

目录

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

2. 动态内存函数的介绍

2.1. malloc

1. malloc的介绍

2. malloc的简单使用 

 3. malloc的细节

2.2. free

1. free的介绍

2. free的简单使用 

3. free的细节

2.3. calloc

1. calloc的介绍

2. calloc的使用 

2.4. realloc

1. realloc的介绍

2. realloc调整内存空间的是存在两种情况的

1. 缩容

2. 扩容

1. 原地扩容

2. 异地扩容

3. 常见的动态内存错误

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

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

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

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

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

6. 动态开辟内存忘记释放(内存泄漏)

4. 几个经典的笔试题

1. 第一题

解决方案一:址传递

解决方案二:返回值

2. 第二题

解决方案一:把p定义为静态变量

解决方案二:把p定义为堆区的变量

3. 第三题

4. 第四题

5.  C/C++的内存区域

6. 柔性数组

6.1. 柔性数组的概念

6.2. 柔性数组的特点

6.3. 柔性数组的优势


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

动态内存分配是一种在程序运行时动态地分配和释放内存的方法。它的存在有以下几个原因:

1. 灵活性:在程序运行时,各种数据的数量和大小都无法预知,动态内存分配可以让程序根据需要动态地在堆中分配内存,而不必提前预留固定的内存空间,从而提高代码的灵活性和适应性。

2. 内存利用率:动态内存分配可以让程序动态地调整内存分配的大小,避免了因为提前预留过多的内存空间而造成内存浪费的情况。

3. 支持数据结构:许多动态数据结构,如链表、树等,需要不断分配和释放内存,动态内存分配可以满足这些数据结构的需求。

然而,动态内存分配也存在一些问题:必须手动管理内存,如果没有适当释放分配的内存,可能会出现内存泄漏和程序崩溃等问题。另外,动态内存分配的开销通常比静态内存分配更大,所以需要谨慎处理内存分配和释放。

例如:当我们需要从存储一些信息数据的时候,如果此时我们不知道长度,或者这个数据量是有动态变化的,那么我们以前定义成静态的就不满足需求了,因为我们要知道,静态申请空间其实是在编译时就已经决定了的。程序运行起来,无法更改,此时只有动态申请空间,因为动态申请空间是在进程运行时决定的。而C或者C++都为我们提供了动态申请资源的能力,而在这里要介绍的就是C为我们提供的这种能力。

2. 动态内存函数的介绍

C语言为我们提供了4个函数,其中三个函数是用来申请动态资源的,分别是malloc、calloc、realloc;另一个也就是free是专门用来释放其申请的动态资源的。注意:动态申请的资源,必须有我们手动且正确的释放。

2.1. malloc

1. malloc的介绍

// 函数原型
// return: 返回申请成功后的空间的首地址,类型为void*
// size: 代表你要申请多少字节的空间
// 包含在 
void* malloc( size_t size );

C动态内存分配_第1张图片

malloc的细节:

  1. 如果开辟成功,则返回一个指向由该函数分配的内存块的起始地址。
  2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  3. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,一般在使用时需要使用者强转为自己所需要的指针变量类型。
  4. 如果size0,malloc的返回值取决于特定库的实现(它可能是一个空指针,也可能不是一个空指针),但是返回的指针不可以被解引用,即如果size是0,其行为是C标准未定义的。
  5.  开辟好的空间的内容是没有初始化的,保持不确定的值

2. malloc的简单使用 

那么如何使用呢?

void Test1(void)
{
	/*
	*  这段代码是什么意思呢?
	*  malloc函数会去堆区申请4 * 10,即40个字节
	*  并将这段空间的起始地址返回给p1,由于
	*  其返回值类型是void*,故p1的类型为void*
	*/
	void* p1 = malloc(sizeof(int)* 10); 

	/*
	*  但由于void*的指针不能进行解引用
	*  因此实际中,我们会强转为我们所需要的类型
	*  如下:
	*  这段代码是将申请的这段动态资源的起始地址经由强转赋值给p2
	*  此时对p2进行解引用,就可以得到这段资源的4个字节
	*  再经由指针 + 整数, 我们就可以访问这段我们申请的动态空间
	*/
	int* p2 = (int*)malloc(sizeof(int)* 10);

	for (size_t i = 0; i < 10; ++i)
	{
		//p2的类型是int*,p2+1会跳过4个字节,以此类推
		*(p2 + i) = i;
	}
	for (size_t i = 0; i < 10; ++i)
	{
		printf("%d ", *(p2 + i));
	}
}

C动态内存分配_第2张图片

 3. malloc的细节

我们说过,malloc是存在失败的可能的,因此我们需要检查malloc的返回值。

void Test2(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	// 如果返回值为NULL,说明申请动态资源失败
	if (ptr == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	// 如果返回值!NULL,说明成功申请动态资源
	else
	{
		//... 
	}
}

 我们也说过,malloc申请的空间的内容是没有被初始化的,其保持随机值。例如:

C动态内存分配_第3张图片

我们还说过,如果malloc动态申请0个字节,其返回值可能为NULL,也可能不为NULL,这是由特定库决定的,标准认为这是一种未定义行为。

可以看到,在vs2013的这环境下,返回值不为NULL,但是我们仍然认为这种行为是非法的,因此我们应该杜绝这种非标准行为。

而上面的这些代码都是存在潜在问题的,我们申请的动态资源并没有释放,如果此时这个进程退出了,那么会由OS自动回收这份资源;如果是长期运行的进程或者变成了僵尸进程就会导致内存泄露的问题。

而为了解决内存泄露:我们需要对我们开辟的动态资源,进行手动释放,而C为我们提供了一个函数free,它就可以完成对动态申请资源的释放。

2.2. free

1. free的介绍

// 函数原型
// memblock 就是你要释放的空间的起始位置
// 包含在 头文件中
void free( void *memblock );

C动态内存分配_第4张图片

free的细节:

  1. free函数可以将先前由malloc、calloc、realloc函数分配的内存块释放,使这段空间可以再次被有效的申请
  2. 如果free函数释放的指针不是指向由上面这些函数分配的内存块,它会导致未定义行为。
  3. 如果free函数释放的是一个NULL,那么这个函数啥也不做。

注意: free函数不会改变这个指针自身,因此它仍然指向这个相同的(现在是无效的)位置。也就是说此时这个地址指向的是一段已经被释放的空间(无效空间),此时这个指针就是典型的"野指针"。

2. free的简单使用 

那么如何释放我们申请的动态资源呢?

void Test4(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	if (ptr == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	else
	{
		//...
	}
	// 释放我们申请的动态资源
	free(ptr); 
    // 防止ptr成为野指针
    ptr = NULL; 
}

3. free的细节

第一点:free函数必须要求被释放的空间是由malloc、calloc、realloc函数动态分配的,如果释放的是非动态分配的,其行为标准是未定义的。例如:

void Test5(void)
{
	int i = 10;
	int* p = &i;
	free(p);  // 非法行为,释放的资源不是动态开辟的
}

C动态内存分配_第5张图片

可以看到,在vs下,这种未定义行为会导致进程终止。  

第二点:我们也说过,动态资源经由free释放后,可以再次被malloc等函数重新申请这段资源,例如:

C动态内存分配_第6张图片

可以看到,ptr1申请的这段资源,经由释放后,此时这段动态资源又被ptr2申请成功。

第三点:我们也说过,如果free释放一个空指针,那么这个函数啥也不做。例如:

void Test7(void)
{
	int* ptr = NULL;
	free(NULL);  // the function does nothing
	free(ptr);   // the function does nothing
}

第四点: 我们还说过,free函数的参数是一段动态资源的起始地址,释放后,此时这个参数依旧指向这段已经被释放的资源(无效空间),故一般情况下我们需要将这个参数置为NULL。

释放前:

释放后:

C动态内存分配_第7张图片

因此我们为了避免野指针的产生,故将这个ptr置为NULL(尽量保持这个好习惯,释放动态资源后,就将其置为NULL)

C动态内存分配_第8张图片

第五点: 当使用free时,我们一定要保证free的参数是我们申请动态资源的起始位置,如果不是起始地址,进程会挂掉。例如:

void Test9(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	assert(ptr);
	// 此时的ptr就不再指向这段动态资源的起始位置
	ptr++;
	// 那么此时会发生什么呢?
	free(ptr);
    ptr = NULL;
}

C动态内存分配_第9张图片

2.3. calloc

1. calloc的介绍

// 函数原型
// num代表元素个数
// size代表元素大小
void* calloc( size_t num, size_t size );

C动态内存分配_第10张图片

calloc的细节:

  1. calloc会去分配num个元素的动态内存块,每个元素都是size个字节,并且将这段动态资源的每一个bit位初始化为0
  2. calloc带来的有效的结果就是分配了一块(num * size)个字节的零初始化内存块
  3. 如果size等于0,那么calloc的返回值是由特定库决定的(可能为NULL,也可能不为NULL),但是这个返回值不可以被解引用。

C动态内存分配_第11张图片

返回值:

  1. 如果calloc成功,那么这个函数返回一个指向由该函数分配的内存块的起始地址。 
  2. 这个返回值类型总是void*,可以将这个指针类型转化为所需类型的指针类型以便于可以支持解引用。
  3. 如果calloc分配所请求的内存块失败,那么会返回一个NULL指针。

2. calloc的使用 

calloc如何使用呢?

void Test10(void)
{
	// calloc第一个参数代表 元素个数
	// 第二个参数代表 元素大小
	int* ptr = (int*)calloc(10, sizeof(int));
	if (ptr == NULL)
	{
		perror("calloc");
		exit(-1);
	}
	else
	{
		//...
	}

	free(ptr);
	ptr = NULL;
}

calloc和malloc大致相同,其细节处理也与malloc差异不大,只有一点是calloc独有的。即它申请的动态资源会被初始化为0,例如:

C动态内存分配_第12张图片

其余的处理,与malloc几乎一致。 

2.4. realloc

1. realloc的介绍

// 函数原型
// memblock: 你要调整的动态资源的起始地址
// size: 调整后的大小,即新的动态资源有size个字节
// 包含在 
void *realloc( void *memblock, size_t size );

C动态内存分配_第13张图片

realloc的细节:

  1. realloc这个函数会更改ptr所指向动态资源的大小,更改后,ptr所指向动态资源的大小为size字节
  2. realloc可能将内存块移动到一个新的位置,并且该函数会返回新位置的地址。
  3. 无论内存块是否被移动到新位置,原内存块中的内容都会被保留,但只保留到新旧内存块大小中较小的那个大小为止。也就是说,如果新分配的内存块小于原内存块的大小,那么原内存块中超过新内存块大小的部分会被截断丢弃。如果新分配的大小比原内存块的大小大,那么新分配的部分的值是不确定的(indeterminate),也就是说,重新分配后新增加的内存部分的内容是未定义的,一般是随机值
  4. 如果此时ptr是一个空指针,那么此时realloc的行为类似malloc,会分配一个大小为size字节的内存块并返回这段空间的起始地址。

C90(C++98):

如果size等于0,那么之前粉喷的那段内存块,也就是ptr所指向的动态资源将会被释放,就好像调用了free一样(即free(ptr)),并且返回一个空指针。

C99/C11(C++11)

如果size等于0,那么返回值依赖于特定库实现:它可能是一个空指针或者是一些其他不能被解引用的位置。

如果realloc函数分配所请求的内存块失败了,那么返回一个空指针,并且这个ptr参数所指向的内存块没有被释放(它仍然是有效的,并且它的内容没有被改变)。

2. realloc调整内存空间的是存在两种情况的

1. 缩容

如果新分配的内存块的大小小于原内存块的大小,那么原内存块中超过新内存块大小的部分会被截断丢弃。例如:

void Test11(void)
{
	int* ptr1 = (int*)malloc(sizeof(int)* 10);
	assert(ptr1);
	memset(ptr1, 0x11, 40);  // 将每个字节的内容置为0x11
	int* ptr2 = (int*)realloc(ptr1,20);  //缩容
	free(ptr2);
	ptr2 = NULL;
}

缩容前:

C动态内存分配_第14张图片

缩容后: 

C动态内存分配_第15张图片

C动态内存分配_第16张图片

并且我们可以发现,此时的ptr1和ptr2是指向同一段空间的,即缩容会在原地缩容。 

2. 扩容
1. 原地扩容

一般情况下,如果当前的动态资源后面有足够的空间可以满足扩容后的空间,那么则支持原地扩容。例如:

C动态内存分配_第17张图片

void Test12(void)
{
	int* ptr1 = (int*)malloc(sizeof(int)* 10);
	assert(ptr1);
	memset(ptr1, 0x22, 40);
	int* ptr2 = (int*)realloc(ptr1,80); // 扩二倍 
	assert(ptr2);
}

C动态内存分配_第18张图片

可以看到此时的ptr1和ptr2指向同一段空间,只不过此时这段空间的大小是原空间的二倍。

2. 异地扩容

如果当前的动态资源后面没有足够的空间支持扩容后的内存块,那么编译器就会进行异地扩容,异地扩容会先去堆区找一个可以存放扩容后的内存块,并将原有内存块中的数据拷贝过来,并释放掉旧的内存空间,最后返回新的内存块的起始地址。例如:

C动态内存分配_第19张图片

void Test13(void)
{
	int* ptr1 = (int*)malloc(sizeof(int)* 10);
	assert(ptr1);
	int* ptr2 = (int*)realloc(ptr1, 10000); // 新空间的大小为10000字节
	assert(ptr2);
}

扩容前:

C动态内存分配_第20张图片

扩容后:

此时就是一个异地扩容。

3. 常见的动态内存错误

1. NULL指针的解引用操作

void Test14(void)
{
	int* ptr = (int*)malloc(1024u * 1024u * 1024 * 2u);
	*ptr = 10;  
	free(ptr);
    ptr = NULL;
}

上面的代码是存在问题的,因为此时我们没有检查ptr是否合法,因为一般情况下,malloc是无法一次性申请2GB的内存的,因此这里malloc会返回NULL,对NULL指针解引用,是一种非法行为。

C动态内存分配_第21张图片

如何解决呢? 我们需要对malloc的返回值进行判断。  例如:

void Test14(void)
{
    int* ptr = (int*)malloc(1024u * 1024u * 1024 * 2u);
	if (ptr == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	else
	{
		*ptr = 10;
		free(ptr);
        ptr = NULL;
	}
}

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

void Test15(void)
{
	int* p = (int*)malloc(sizeof(int)* 5);
	assert(p);
	for (size_t i = 0; i <= 5; ++i)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
}

C动态内存分配_第22张图片

上面的代码也会存在问题,当 i 等于 5时,此时p会跳过20个字节,如果此时访问p+5指向的空间就是非法访问。

C动态内存分配_第23张图片

我们要正确处理边界问题,防止出现上面这种问题

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

void Test16(void)
{
	int i = 10; 
	int* ptr = &i;
	free(ptr);
	ptr = NULL;
}

上面对一段非动态开辟的资源进行释放,其行为属于标准未定义行为,在vs下,进程会崩溃掉。我们要牢记free的规则,它是释放动态开辟的资源的。不可以做标准之外的行为。

C动态内存分配_第24张图片

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

void Test17(void)
{
	int* ptr = (int*)malloc(sizeof(int)*10);
	assert(ptr);
	++ptr;
	free(ptr);
	ptr = NULL;
}

注意:free释放动态资源时,free中的参数必须是这段动态资源的起始位置。如果不是会导致进程崩溃。例如:上面的++ptr后,此时的ptr就不在是这段动态资源的起始位置,故释放的时候导致进程崩溃。

C动态内存分配_第25张图片

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

void Test18(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	assert(ptr);
	free(ptr);
	free(ptr);
}

动态资源只可以被free释放一次。上面会导致进程崩溃。原因是因为,当这段动态资源被释放后,此时的ptr依旧指向这段资源(但是这段空间已经不属于你了),你又去释放这段资源,其行为是非法行为。

C动态内存分配_第26张图片

要防止多次释放的问题,其实很简单,我们只需要在释放动态资源后,将其置为NULL,因为free(NULL)不会做任何事,也就不会导致进程崩溃了。例如: 

void Test18(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	assert(ptr);
	free(ptr);
	ptr = NULL;
	free(ptr);
	ptr = NULL;
}

此时多次释放也没有啥问题了。但虽然如此,我们还是尽量保证只释放一次即可。 

6. 动态开辟内存忘记释放(内存泄漏)

void Test19(void)
{
	int* ptr = (int*)malloc(sizeof(int)* 10);
	if (ptr == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	else
	{
		//...
	}
    //忘记释放动态资源,导致内存泄漏
}

如果这个进程正常退出,那么由系统回收资源。
如果这个进程是长期运行的或者成为了僵尸进程,那么其后果很严重,故内存泄漏我们要严阵以待,小心处理。

4. 几个经典的笔试题

1. 第一题

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

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

上面的代码首先是存在问题的。那么问题是什么呢?分析如下:

str是一个char*的指针变量,其初值为NULL,将str传递给了GetMemory,但注意,此时这种传递方式是一种值传递(指针变量也是一种变量)。即将str自身的值传递给了GetMemory中的形参p,即p是str的拷贝,而我们知道,形参的改变是不影响是实参的。也就是说,当GetMemory函数内部将动态空间申请完后,此时p的确指向了一段动态资源,但是p的改变不会影响str,也就是说当GetMemory结束时,str的值依旧为空,而strcpy要求参数不可以为NULL,故进程崩溃。不仅如此,由于p的作用域仅限于GetMemory这个函数内部,出了函数作用域,其p就会被销毁,此时就无法释放它申请的动态资源,故导致了内存泄漏

GetMemory之前:

GetMemory后:

C动态内存分配_第27张图片

那么如今解决呢?

解决方案一:址传递

// char*的地址的类型自然是char**,故在这里用char**接收
void Solution_GetMemory1(char** p)
{
	*p = (char *)malloc(100);
}

void Test(void)
{
	char *str = NULL;
	// 址传递,传递str这个指针变量的地址
	Solution_GetMemory1(&str);
	strcpy(str, "hello world");
	printf(str);
	// 防止内存泄漏,释放Solution_GetMemory1申请的动态资源
	free(str);
}

解决方案二:返回值

// 传值返回,将申请的动态资源的地址返回
char* Solution_GetMemory2()
{
	return (char*)malloc(100);
}

void Test(void)
{
	char *str = NULL;
	// 利用传值返回,接收开辟的动态资源
	str = Solution_GetMemory2();
	strcpy(str, "hello world");
	printf(str);
	// 防止内存泄漏,释放Solution_GetMemory2申请的动态资源
	free(str);
}

2. 第二题

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

首先,上面的代码是危险且错误的。为什么?

GetMemory本意是将函数内部的一个字符数组的地址返回给Test中的str,但是这会导致非法访问。原因是因为p是GetMemory函数中的一个局部字符数组的起始地址。其生命周期和作用域仅限于GetMemory这个函数内部,出了函数栈帧,p所在的这块空间就会被销毁。即你将一块已经被系统回收的空间返回给了str,此时的str就指向了这段被回收的空间,也就是说这里的str就是一个野指针。printf去访问这段空间的数据,就导致了非法访问,此时printf打印的内容就是随机的。

而上面这类问题我们称之为返回栈空间变量的地址的问题。

那么上面这个问题如何解决呢?起始问题的关键就在于p这个变量是一个局部的栈区的变量。所以问题很简单,我们只需要将这个p存于静态区或者堆区,延长其生命周期,此时这个问题就迎刃而解了。

解决方案一:把p定义为静态变量

char* GetMemory(void)
{
	// 将其定义为静态字符数组,延长其生命周期
	static char p[] = "hello world";
	return p;
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

解决方案二:把p定义为堆区的变量

char* GetMemory(void)
{
	// 将p定义为动态资源,延长其生命周期
	char* p = (char*)malloc(15);
	p = "hello world";
	return p;
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
	// 防止内存泄漏
	free(str);
}

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

上面代码的问题就在于资源泄露,没有释放动态开辟的资源。

void GetMemory(char **p, int num)
{
	*p = (char *)malloc(num);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
    // 防止内存泄漏,释放动态资源
    free(str);
}

4. 第四题

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

上面的代码也是存在问题的。

申请了一段动态资源并将这段空间的首地址返回给了str,strcpy会将"hello"拷贝给str,此时str的前6个字符分别是 'h' 'e' 'l' 'l' 'o' '\0',但是free会将str的这段动态申请的资源给释放掉。但是我们之前说过,free只会释放掉str指向的资源,并不会将str本身置为NULL,因此此时str依旧指向这段空间,即此时的str就是一个野指针,它的值不为NULL,但指向的是一段非法空间。此时这个if语句不起任何作用,当再次调用strcpy的时候,strcpy函数会向str指向的空间写入内容,这就是一个非法访问。

如何更改呢?当free后,就将str置为NULL,杜绝野指针的产生。

void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);
	// 防止str成为野指针
	str = NULL;
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

5.  C/C++的内存区域

C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

6. 柔性数组

6.1. 柔性数组的概念

柔性数组(Flexible Array)是一种在C语言中用于定义结构体中的可变长度数组的技术。

在传统的C语言中,数组的长度需要在定义时就确定,并且不能改变。然而,柔性数组允许我们定义结构体中最后一个成员为长度未知的数组。这使得我们可以根据实际需要动态分配结构体及其柔性数组的内存空间。

注意:柔性数组是在C99标准引入的。

例如:

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

// 有些编译器可能会报错,那么可以改成:

struct flexible_array2
{
	int i;
	int arr[];  // 柔性数组成员
};

6.2. 柔性数组的特点

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

首先我们讨论一下,为什么要求结构体中的柔性数组成员前面必须至少一个其他成员。

这是因为柔性数组的长度在编译阶段是不确定的,只有在运行时才能确定。为了能够正确地分配结构体和柔性数组所需的内存空间,需要确保在柔性数组之前至少有一个其他成员。

这样做的主要原因是确保结构体的大小能够正确计算。在C语言中,结构体的大小是按照其成员的顺序和对齐方式来计算的。由于柔性数组的长度不确定,编译器无法准确地计算结构体的总大小。

综上所述,要求结构体中柔性数组成员之前至少有一个其他成员,是为了确保结构体的大小能够正确计算,从而能够正确地分配内存空间给结构体及其柔性数组使用。

struct flexible_array3
{
	char ch;
	int arr[]; 
};

其次,我们在讨论第二点,为什么说 sizeof 返回的这种结构大小不包括柔性数组的内存

struct flexible_array3
{
	char ch;
	int arr[]; 
};

void Test20(void)
{
	struct flexible_array3 fa;
	printf("%d\n", sizeof(fa));
}

C动态内存分配_第28张图片

诶,有人看到就蒙了,你不是说sizeof 会忽略其柔性数组的大小吗?这里怎么是4呢?原因是因为结构体的内存对齐。虽然此时不会计算这个柔性数组的大小,但此时编译器认为这个结构体的最大对齐数是4,1不是4的整数倍,故在这里会进行内存对齐,因此我们看到的大小就是4。如何印证?

struct flexible_array4
{
	char ch;
	char arr[];
};

void Test21(void)
{
	struct flexible_array4 fa;
	printf("%d\n", sizeof(fa));
}

此时我们判断结果是1,因为此时不计算柔性数组的大小,并且只有一个char类型的成员变量,此时这个结构体的最大对齐数是1,故这个类型的大小就是1。

C动态内存分配_第29张图片

回归到正题,由于柔性数组并不是在编译阶段决定其大小的,而是在运行时决定。而sizeof这个操作符是在编译阶段计算其操作数(可以是类型、表达式或变量)的大小,返回一个常量。

在执行sizeof操作时,并不会对操作数进行求值或执行任何运行时的计算。编译器根据操作数的类型来确定其所需的存储空间大小,并在编译阶段为该操作数分配内存。

sizeof操作符的结果在编译时就是已知的,因此可以在编译期间使用该结果值进行其他的编译时优化或计算。

需要注意的是,sizeof 操作符返回的是一个以字节为单位的值,表示操作数所占用的存储空间大小。对于数组,sizeof 操作符返回整个数组的大小。对于指针,sizeof 操作符返回指针本身的大小,而不是指针所指向的对象的大小。

总结起来,sizeof 操作符是在编译时计算的,用于确定静态类型的大小而不是在运行时根据实际数据进行计算

为什么说包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

前面我们已经说过了,柔性数组成员的大小是在运行时决定的,也就是说在编译阶段其大小无法确定。因此当我们用malloc开辟空间时,所开辟的空间必须要大于用sizeof计算结构体的大小的值,也就是说用多出来的那份空间在运行时分配给柔性数组成员。 

例如:

typedef struct flexible_array5
{
	char ch;
	int arr[];
}type1;

void Test22(void)
{
	// 实际上 sizeof type1这段动态空间是分配给ch这个成员变量
	// 而这 20个字节 才是分配给这个柔性数组的
	type1* ptr = (type1*)malloc(sizeof(type1)+20);
    assert(ptr);
	ptr->ch = 'x';
	for (size_t i = 0; i < 5; ++i)
	{
		ptr->arr[i] = i;
	}
    free(ptr);
    ptr = NULL;
}

故这就是为什么说包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

但是这个成员被称之为柔性数组成员,如何体现出它的柔性呢?那么realloc就粉墨登场了:

typedef struct flexible_array5
{
	char ch;
	int arr[];
}type1;

void Test23(void)
{
	// 实际上 sizeof type1这段动态空间是分配给ch这个成员变量
	// 而这 20个字节 才是分配给这个柔性数组的
	type1* ptr = (type1*)malloc(sizeof(type1)+20);
    assert(ptr);
	ptr->ch = 'x';
	for (size_t i = 0; i < 5; ++i)
	{
		ptr->arr[i] = i;
	}

	// 假设柔性数组需要40个字节呢?
	// 此时对原动态内存块进行realloc
	// 根据要求,柔性数组需要40个字节再加上ch这个成员变量的大小(也就是sizeof 结构体类型的大小)
	type1* tmp = (type1*)realloc(ptr, sizeof(type1)+40);
    assert(tmp);
	ptr = tmp;
	for (size_t i = 5; i < 10; ++i)
	{
		ptr->arr[i] = i;
	}
	free(ptr);
	ptr = NULL;
}

6.3. 柔性数组的优势

那么柔性数组有什么优势呢?

首先,我们看看上面的代码,即如果你想让一个结构里面的成员是可大可小的,那么也可以不用柔性数组啊,例如下面的代码:

typedef struct type
{
	char ch;
	int* arr;  // 我也可以不用柔性数组
}type2;

void Test24(void)
{

	type2* ptr = (type1*)malloc(sizeof(type1));
	assert(ptr);
	ptr->arr = (int*)malloc(sizeof(int)* 5);
	assert(ptr->arr);

	// 业务逻辑...

	// 扩容逻辑
	int* tmp = (int*)malloc(sizeof(int)* 10);
	assert(tmp);
	ptr->arr = tmp;

	//  虽然也可以达到柔性数组的功能
	//  但是此时是不是要先释放ptr->arr,在释放ptr
	//  如果有人直接先释放了ptr,是不是就导致ptr->arr的这段动态资源无人释放,
	//  即导致了内存泄漏
	free(ptr->arr);
	ptr->arr = NULL;
	free(ptr);
	ptr = NULL;
}
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free(此时就导致了内存泄漏),而你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉,而柔性数组便能符合需求。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片,柔性数组中的元素存储在连续的内存空间中,这有利于CPU缓存的利用。相比于链表等非连续存储的数据结构,柔性数组具有更好的数据局部性,可以提高访问效率。

你可能感兴趣的:(c语言)