C语言动态内存管理详解

目录

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. 经典练习

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

6.柔性数组

6.1定义

6.2特点

6.3 优点


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

  • 因为内存空间相对比较少,如果全部是不能动态申请和释放的话对于大的程序,内存会因为被占用完而崩溃,此时就要发生内存泄露。

  • 很多时候我们需要分配的空间大小是不确定的,直到程序运行时才能知道。比如,数组的长度是根据每个用户输入的需求决定的,系统就应该动态的给该数组分配长度,该段代码运行结束后,系统调用free()函数释放分配的内存,然后接着运行剩下的程序。动态分配内存可以根据需要去申请内存,用完后就还回去,让需要的程序用。

2.动态内存函数的介绍

2.1 malloc和free 

C语言动态内存管理详解_第1张图片

功能:分配一个内存大小为size字节的内存块,返回指向该块开头的指针。如果开辟成功,则返回一个指向开辟好空间的指针。 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。

注意:新分配的内存块的内容不会初始化。如果 size 为零malloc的行为是标准是未定义的,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被取消引用。

 char * buffer;
 printf ("How long do you want the string? ");
 scanf ("%d", &i);
 buffer = (char*) malloc (i+1);

C语言动态内存管理详解_第2张图片

功能:先前由调用 malloc、calloc 或重新分配分配的内存块被解除分配,归还操作系统,使其再次可用于进一步分配。如果 ptr 是空指针,则该函数不执行任何操作。

注意:如果 ptr 不指向使用上述函数分配的内存块首地址,则会导致未定义的行为,一般会导致程序奔溃。此函数不会更改 ptr 本身的值,因此它仍然指向相同的(现在无效的)位置。意味着ptr为野指针。

#include
#include//malloc free要包含的头文件
int main()
{
	int num = 0;
	scanf("%d", &num);
	int* p = (int*)malloc(sizeof(4) * num);
	if (p == NULL)//先判断再使用
	{
		perror("malloc");
		return;
	}
	else
	{
		for (int i = 0; i < num; i++)
		{
			p[i] = i;
		}
		for (int i = 0; i < num; i++)
		{
			printf("%d ", p[i]);
		}
	}
	free(p);
	p = NULL;//避免野指针被使用
	return 0;
}

2.2 calloc

C语言动态内存管理详解_第3张图片

功能:为 num 个元素数组分配一个内存块,每个元素的大小为长为size字节,并将其所有位初始化为零。如果 size 为零malloc的行为是标准是未定义的,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被取消引用。

注意:与函数 malloc 的区别只在于 calloc 会把申请的空间的每个字节初始化为全0。

2.3 realloc

realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。

C语言动态内存管理详解_第4张图片

功能:更改 ptr 所指向的内存块的大小,size为新要求的字节空间大小,如果能开辟成功就会返回一个指向新大小空间的起始地址且原来大小的那块空间里的数据还在且相对返回值的位置和原来一样。如果内存分配不成功,会返回null

注意:如果 ptr 是空指针,则该函数的行为等价于 malloc,分配一个新的大小字节块,并返回指向其开头的指针。开辟成功时的地址可能会是原来的地址也可能不是,如图:

 C语言动态内存管理详解_第5张图片

#include 
#include
int main()
{
    int* ptr = (int*)malloc(100);
    if (ptr != NULL)
    {
        //相关操作
    }
    else
    {
        exit(EXIT_FAILURE);
    }
    //扩展容量
    //error
    ptr = (int*)realloc(ptr, 1000);//error 如果申请失败返回NULL,不但没有增加成功,还丢失原有数据

    //正确:
    int* p = NULL;
    p = realloc(ptr, 1000);
    if (p != NULL)
    {
        ptr = p;
        //相关操作
        free(ptr);
    }
    else
    {
        //相关操作
    }
    
    return 0;
}

3.常见的动态内存错误

3.1对NULL指针的解引用操作

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题,所以使用前要判断
 free(p);
}

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

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


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

​
void test()
{
 int a = 10;
 int *p = &a;
 free(p);//程序崩溃
 
}

​


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

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置,程序崩溃
}


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

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放,程序崩溃
}

如果及时将p置空会发现,free(NULL)将什么操作都不进行。


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

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

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还是NULL 

C语言动态内存管理详解_第6张图片

2.

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,造成内存泄露

 3.

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

//返回栈空间临时变量的地址,函数介绍后p地址使用权还给了操作系统了,p相当于野指针,而且此时p为局部变量已经被自动销毁了。

注:销毁局部变量的具体操作是将局部变量名p指向另外的区域,这样我们编写程序时如果想要通过使用a读取那个地址空间存储的值,会报错,销毁相当于重置指针,将其指向不可访问处,并没有对原地址存储的值做什么,所以,如果我们记录了局部变量的地址,就可以再次对其进行读写,但是是非法不安全的。
打印里面的内容大部分情况打印时里面的内容已经改变,因为函数栈帧的存在。退出函数后,里面的内容会被更改。

C语言动态内存管理详解_第7张图片

4.

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

//free后没有及时置空,造成野指针被使用,造成程序崩溃

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

 

C语言动态内存管理详解_第8张图片

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

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

3. 数据段(静态区)(static)存放全局变量、静态数据。特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。 

6.柔性数组

6.1定义

柔性数组在C99标准中的定义是:

在至少两个成员的结构体中,最后一个成员其类型若是不完整类型的数组类型,则该成员称为柔性数组。

struct s { int n; char str[]; };

注意,str后面的中括号只能为空,数组类型不局限于char。

然而GCC编译器在C99发布之前就支持“柔性数组”了,但是形式不一样:

​
struct s { int n; char str[0]; };

所以,对于不同的编译器实现柔性数组的形式不一样。当时由于gcc柔性数组的语法扩展实用并且受欢迎,C99标准将其作为一个特殊情况并得到了支持。

6.2特点

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

2.sizeof 返回包含柔性数组的结构大小不计算柔性数组的内存。因为标准规定sizeof的操作数不可以是不完整类型或者函数类型或者位字段类型(由于C没有真正的“位字段类型”),而柔性数组作为不完整类型,对这样的结构体求大小不会包括柔性数组的大小。柔性数组只是把名字放在结构体内,没有实质上的定义,这样方便通过结构体使用.或->语法来操作它。

typedef struct stu
{
 int i;
 int a[0];//柔性数组成员
}S;
printf("%d\n", sizeof(S));//输出的是4

3.包含柔性数组成员的结构体用malloc ()函数进行内存的动态分配,并且分配的内存应该根据柔性数组的大小而定。

柔性数组使用:

#include
#include
struct stu
{
	char c;
	int a;
	int arr[];
};

int main()
{
	int num = 0;
	scanf("%d", &num);


	struct stu* p = (struct stu*)malloc(sizeof(struct stu) + num*4);
	if (p == NULL)
		perror("malloc:");
	else
	{
		for (int i = 0; i < num; i++)
		{
			p->arr[i] = i;
		}
		for (int i = 0; i < num; i++)
		{
			printf("%d ", p->arr[i]);
		}
	}
	
	free(p);
	p = NULL;
	return 0;
}

6.3 优点

在柔性数组出现之前,相关处理是这样的:

#include
#include
struct s
{
	int a;
	char c;
	int* p;
};
int main()
{
	int num = 0;
	scanf("%d", &num);
	struct s* ps = (struct s*)malloc(sizeof(struct s));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		ps->p = (int*)malloc(4 * num);
		if (ps == NULL)
		{
			perror("p_malloc");
			return 1;
		}
		else
		{
			for (int i = 0; i < num; i++)
			{
				ps->p[i] = i;
			}
			for (int i = 0; i < num; i++)
			{
				printf("%d ", ps->p[i]);
			}
		}
	}
	free(ps->p);
	free(ps);
	ps->p = NULL;
	ps = NULL;
}

对比可以发现实现柔性数组的好处:

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

二.这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。

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