【C语言】动态内存管理

发布于:2024-05-15 ⋅ 阅读:(144) ⋅ 点赞:(0)

目录

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

 2、动态内存函数的介绍

2.1、malloc和free

2.2、calloc

 2.3、realloc

 3、常见的动态内存错误

3.1、对NULL指针的解引用操作

3.2、对动态开辟空间的越界访问

 

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

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

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

 3.6、动态开辟内存忘记释放(内存泄漏)

4、动态内存的笔试题

方法1:动态分配内存

方法2:返回静态变量的地址

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

 6.柔性数组

6.1、柔性数组的特点

6.2、柔性数组的使用

6.3、柔性数组的优势


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

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

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

但是上述的开辟空间的方式有两个特点: 1. 空间开辟大小是固定的。 2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。 但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了 



 

 

 2、动态内存函数的介绍

2.1、malloc和free

 

 以下是使用 malloc() 函数的一般步骤:

  1. 包含头文件 <stdlib.h>
  2. 开辟成功:使用 malloc() 函数分配内存空间,并将返回的指针存储在一个指针变量中
  3. 开辟失败:验证内存是否成功分配,即检查返回的指针是否为 NULL。如果是 NULL,则表示内存分配失败,可能是由于内存不足。
  4. 使用分配的内存进行必要的操作。
  5. 最后,在不再需要使用内存时,使用 free() 函数释放内存并将其返回给系统然后还要置为NULL最好

 一些要注意的

  • malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器的处理方式。

 【举例】

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

int main() {
    int *nums;
    int size = 5;

    // 分配内存空间
    nums = (int *) malloc(size * sizeof(int));

    // 检查内存分配是否成功
    if (nums == NULL) {
        printf("内存分配失败\n");
        perror("malloc");
        return -1;
    }

    // 使用分配的内存空间
    for (int i = 0; i < size; i++) {
        nums[i] = i + 1;
    }

    // 打印数组元素
    for (int i = 0; i < size; i++) {
        printf("%d ", nums[i]);
    }
    printf("\n");

    // 释放内存空间
    free(nums);
    nums =NULL;
    return 0;
}

 上述示例首先使用 malloc() 函数分配了一个可以存储 5 个整数的内存空间。然后,将分配的空间用于存储连续的整数,并最后使用 free() 函数释放了该内存空间。


请注意,在使用完 malloc() 分配的内存之后,务必使用 free() 函数将其释放回系统,以避免内存泄漏问题。



 

 

 

  • free函数用来释放动态开辟的内存
  • 包含头文件<stdlib.h>
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。(也就是说不要使用free函数来释放非动态开辟的空间)
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

 【举例】

int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(sizeof(int) * 10);
	if (p == NULL)
	{
		perror("malloc");
		return 1;   //如果为空则不执行下面的代码直接跳出
	}
 
	//使用
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for ( i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
 
	//释放
	free(p);
	p = NULL; //虽然已经free释放了,但是p指针依然指向那个空间,此时p就是野指针了
                //为了防止再使用p访问该空间,将它置成NULL最为合适。
	return 0;
}

2.2、calloc

calloc() 函数用于在运行时从堆区分配内存空间,并将每个字节初始化为零。与 malloc() 函数一样,它也可以用来动态分配内存块。

calloc() 函数的语法如下:

void *calloc(size_t num, size_t size);

其中,num 表示要分配的元素数量,size 表示每个元素的大小。函数的返回值是一个指向分配内存的指针。如果内存分配失败,则函数返回 NULL。

calloc() 函数与 malloc() 的主要区别是 calloc() 在分配内存时会将内存块中的每个字节初始化为零。这使得它适合用于创建数组或需要初始化内存的情况。

 

  • 包含头文件<stdlib.h>
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • malloc和calloc的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

 【举例】

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;   //如果为空则不执行下面的代码直接跳出
	}
 
	//使用
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}


 

 2.3、realloc

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 的调整。

函数原型如下:

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

realloc 函数就可以做到对动态开辟内存大小

参数说明

  • ptr:指向之前分配的内存块的指针(要调整的内存地址),如果这是 NULL,则 realloc 的行为类似于 malloc,分配一个新的内存块。
  • size:新的内存块的大小,以字节为单位。(注意是调整之后的大小)

 【注意】

  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  1. 内存内容: 如果新的内存块比原来的大,那么在增加的内存部分中的内容是不确定的。如果新的内存块比原来的小,超出的部分会被自动丢弃。
  2. 指针更新: 如果 realloc 返回一个新的指针(即,内存块被移动到了新的位置),那么你必须更新所有指向原内存块的指针。也就是下面对应的情况二
  3. 内存泄漏: 如果 realloc 返回 NULL 而旧的内存块指针 ptr 仍然有效,那么你需要手动释放旧的内存块以避免内存泄漏。(申请空间失败了返回NULL)

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

情况1:原有空间之后有足够大的空间情况2:原有空间之后没有足够大的空间

 

 情况1

情况2

 

后面的空间被占用了,后续的空间不够拓展的,就重新找一块空间

1.将旧的空间中的数据拷贝到新的空间

2.释放掉旧的空间

3.realloc函数返回新的空间的地址

 

注意:realloc也可能开辟空间失败,失败是返回NULL。因此不能直接将realloc开辟的空间直接赋值给原指针p,因为这样做会导致当realloc开辟失败时p直接被置成NULL了,那么就意味着不但realloc没有调整大小反而把p原有的内容丢失了。所以此处需要用一个tmp先接收返回值,当判断了 返回值不为NULL时再将tmp赋值给p。

也就是前面说的内存泄漏: 如果 realloc 返回 NULL 而旧的内存块指针 ptr 仍然有效,那么你需要手动释放旧的内存块以避免内存泄漏。(申请空间失败了返回NULL)

 

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;   //如果为空则不执行下面的代码直接跳出
	}
	//使用
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//空间不够,realloc调整为20个int
	int* tmp = (int*)realloc(p, 20 * sizeof(int));
	if (tmp != NULL)
	{
		p = tmp;
	}
    //使用
 
 
	//释放
	free(p);
	p == NULL;
	return 0;
}

 特殊情况

realloc的第一个参数为NULL,拿它的功能等价于malloc



 

 

 3、常见的动态内存错误

3.1、对NULL指针的解引用操作

int main()
{
	int* p = (int*)malloc(INT_MAX / 4);
	//不做返回值判断,就可能使用NULL指针
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
    return 0;
}

解决方案是要对返回的指针进行判断才行。不然就有可能会对空指针解引用了 

这将导致未定义行为,很可能导致程序崩溃。

为了防止这种情况,程序员应该在解引用指针之前检查它是否为 NULL

3.2、对动态开辟空间的越界访问

 

int main()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		return 1;
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
	return 0;
}

 检查了返回的指针是不是空指针。这里是越界访问的问题在大多数情况下,这种越界访问会导致程序崩溃或产生其他错误。

 

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

int main()
{
	int a = 10;
	int* p = &a;
	free(p);//错误操作
	return 0;
}

这将导致未定义行为。未定义行为意味着程序可能会崩溃、行为异常,或者在某些情况下似乎正常工作,但在其他情况下失败。这种行为取决于操作系统、编译器和程序的其他部分。

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

int main()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
	return 0;
}

 指针移动了,也释放了但是释放的只是开辟的空间的一部分。

只释放一部分会报错。尽量避免让p自己移动位置,如果非要移动,应该再定义一个指针,让新定义的指针动。

 

 

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

int main()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
	return 0;
}

 为了避免这样的情况要free之后把指针设置为空指针。这样就算是重复了free也不会起作用如果参数 ptr 是NULL指针,则函数什么事都不做。

 

 3.6、动态开辟内存忘记释放(内存泄漏)

void test()
{
	int* p = (int*)malloc(40);
	if (NULL != p)
	{
		*p = 20;
	}
}
 
int main()
{
	test();
	while (1)//这里表示程序还在一直运行,不会结束,例如服务器
	{
		;
	}
	return 0;
}

空间拿走了也没用,离开test函数之后谁都没办法使用这些被动态开辟的空间了。这样内存就被浪费了。

 

4、动态内存的笔试题

【题目1】

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

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

【解析】

  1. 在函数 GetMemory 中,参数 p 被传递进来并被修改为指向动态分配的内存空间。然而,由于 C 语言中的参数传递是按值传递的,所以在 GetMemory 函数内部对 p 的修改不会影响到外部的 str 指针。

  2. 在函数 Test 中,str 是一个空指针(NULL),没有指向有效的内存空间。然后,将 str 作为参数传递给 GetMemory 函数。

  3. 在 GetMemory 函数内部,使用 malloc(100) 动态分配了 100 字节的内存空间,并将其赋值给局部变量 p。但这个赋值只在 GetMemory 函数内部起作用,不会对 Test 函数中的 str 产生任何影响。(这里还没有释放空间,内存泄漏)

  4. 接着,在 Test 函数中使用了 strcpy(str, "hello world"),试图将字符串 "hello world" 复制到 str 所指向的内存空间。由于 str 是空指针,它没有合法的内存空间可以存储这个字符串,会导致未定义的行为。这可能会导致程序崩溃或产生其他异常结果。

 

【修改】

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

 

改为传地址调用,GetMemory函数的参数是二级指针

别忘记了free然后置为空指针

补充一点这里的printf函数的使用是没有问题的,

printf("hehe\n")

这个hehe字符串的表达式的结果是首字符h的地址。

同理这样的写法也没有问题

char* p ="hehe\n;
printf(p);



 

 

【题目2】 

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

 

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

 【解析】

打印出乱码。返回的是局部变量的地址

主要原因在于 GetMemory 函数返回局部变量的地址。在C语言中,当一个函数返回局部变量的地址时,这通常会导致未定义行为。具体来说,GetMemory 函数返回了 p 数组的地址,但是当函数执行完毕后,p 数组所占用的内存会被释放,因此返回的地址指向的是一个已经被释放的内存区域。(返回栈空间问题)

在 Test 函数中,str 被初始化为 NULL,然后调用 GetMemory() 并将返回的地址赋给 str。由于 GetMemory 返回的地址指向已经被释放的内存,当 printf(str) 尝试访问这个地址时,可能会导致程序崩溃或产生未定义行为。(str这时已经是野指针)

出去函数这个p就被销毁了,返回了它的地址

 

【修改】

两种方法:动态分配内存和返回静态变量的地址

方法1:动态分配内存

#include <stdlib.h>

char* GetMemory(void)
{
    char* p = (char*)malloc(11 * sizeof(char)); // 分配足够的空间包括字符串结束符
    if (p != NULL) {
        strcpy(p, "hello world");
    }
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    if (str != NULL) {
        printf(str);
        free(str); // 释放动态分配的内存
    } else {
        printf("Memory allocation failed\n");
    }
}

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

GetMemory 函数动态分配内存并返回指针。如果分配成功,它将 “hello world” 复制到新分配的内存中。在 Test 函数中,我们检查 str 是否为 NULL,如果是,则说明内存分配失败。

 

方法2:返回静态变量的地址

#include <stdio.h>

static char static_str[] = "hello world";

char* GetMemory(void)
{
    return static_str;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}

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

采用static来修饰:

GetMemory 函数返回一个静态变量的地址。静态变量在程序运行期间一直存在,因此返回它的地址是安全的。

【补充】

 函数返回局部变量是通过寄存器来完成的

 test函数调用完之后,本来属于它的空间就不属于它了比如可能被别人使用了。【函数栈帧的创建和销毁】

 



 

【题目3】

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

 

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。

此处GetMemory的参数为传址调用,*p就等于str,因此对*p进行动态内存开辟就等于对str动态内存开辟,所以可以正常打印处hello。

但是malloc动态开辟的空间并没有用free释放,存在内存泄漏的风险

【修改】 

在printf之后加上

free(str);
str = NULL;



【题目4】 

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

 

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

malloc开辟的空间释放了,但是str保存的还是这100字节的空间的地址【str为野指针】,所以if语句判断为真

strcpy函数通过str找到原来开辟的100字节的空间然后放进去一个world【非法访问了】

 

 free和NULL要配套使用,释放完空间之后立即将指针置空,可以避开很多错误。

 




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

1.20,49

 

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

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

 

 6.柔性数组

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

  • 结构体中
  • 最后一个成员
  • 未知大小的数组【柔性数组】
struct MyStruct {
    int length;
    int data[];
};

 【注意】

  1. 柔性数组只能作为结构体的最后一个成员,因为它的大小是由结构体中其他成员的大小和对齐方式决定的。
  2. 柔性数组不能使用sizeof操作符来获取其大小,因为它是可变大小的。
  3. 在使用柔性数组之前,需要为结构体分配足够的空间,包括柔性数组需要存储的元素个数。
  4. 使用柔性数组时应注意边界检查,确保不会越界访问。

6.1、柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
 typedef struct st_type
 {
 int i;
 int a[0];//柔性数组成员
}type_a;
 printf("%d\n", sizeof(type_a));//输出的是4

 sizeof 返回的这种结构大小不包括柔性数组的内存。所以结果是4

6.2、柔性数组的使用

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

 【举例】 

struct S
{
	int i;
	int a[];//柔性数组成员
};
 
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20); //4+20
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
    free(ps);
	ps = NULL;
	return 0;
}

 

 当开辟的空间不够,使用realloc调整大小时也需要加上其他成员的大小:

 

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20); //4+20
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
 
	//调整大小 20->40
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40);//4+40
	if (ptr != NULL)
	{
		ps = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
 
	free(ps);
	ps = NULL;
	return 0;
}

6.3、柔性数组的优势

【代码1】指针动态分配内存

typedef struct st_type
 {
 int i;


int *p_a;
 }type_a;
 type_a *p = (type_a *)malloc(sizeof(type_a));
 p->i = 100;
 p->p_a = (int *)malloc(p->i*sizeof(int));

 //业务处理
for(i=0; i<100; i++)
 {
 p->p_a[i] = i;
 }

 //释放空间
free(p->p_a);
 p->p_a = NULL;
 free(p);
 p = NULL;

需要先释放malloc给*p_a开辟的空间,然后置为NULL,然后释放malloc给p开辟的空间,置为NULL。

【代码2】柔性数组

​
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20); //4+20
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
 
	//调整大小 20->40
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40);//4+40
	if (ptr != NULL)
	{
		ps = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
 
	free(ps);
	ps = NULL;
	return 0;
}

​

 

  1. 【代码1】中需要先给结构体动态开辟一块空间,然后再对结构体中的a指针再动态开辟一块空间,这里就使用了两次malloc来动态开辟,增加了代码量
  2. 而使用了柔性数组的【代码2】只需要对结构体整体使用malloc动态开辟一次适合的大小即可。
  3. 【代码1】中由于malloc开辟了两次空间,因此也需要使用两次free释放空间,并且释放顺序还不能错,必须先释放成员a指向的空间,再释放结构体空间。而【代码2】只需要释放一次。

 

因此使用柔性数组实现有两个好处:

 

方便操作:只需要一次malloc和free就可以把所有内存分配好与释放掉。


减少内存碎片和提高访问速度:如果在内存中频繁开辟空间,内存和内存之间就很容易留下一些缝隙,而这些缝隙又称之为内存碎片,内存碎片越多内存利用率就越低。并且连续的内存有益于提高访问速度。


网站公告

今日签到

点亮在社区的每一天
去签到