动态内存函数

目录

前言:

动态内存函数:

malloc函数:

free函数:

calloc函数: 

realloc函数: 

realloc函数如何开辟内存? 

1.开辟失败

2.开辟成功

 free的使用:

动态内存函数使用注意事项:

1.不能越界访问

2.没有从最开始的空间释放内存 

 3.多次释放同一块内存

4.对非动态开辟的内存free 

5.对NULL解引用 

6.内存泄漏

练习:

习题一:

习题二: 

习题三:

习题四:

结束语


前言:

       当我们每次使用数组时,有一弊端,就是固定大小的,不能改变其空间,于是乎我们会想,能不能有一种东西能方便我们随意得控制大小(链表可以,但这里先不做讨论)。于是就衍生出了内存函数。

       在了解内存函数前,我们先来看一段代码:

//C语言是可以创建变长数组 - C99中增加了
struct S
{
    char name[20];
    int age;
};
int main()
{
    int n = 0;
    scanf("%d", &n);
    struct S arr[n];//能够存放50个struct S 类型的数据
    //这样写vs不支持,数组中只能放常量
    //在C99中可以使用,不是所有编译器都支持,C99标准用的不够普遍
    //30 - 浪费
    //60 - 不够
    return 0;
}

       出于可移植性考虑的原因,有些编译器支持C99,有些不支持(但是刷题网站都支持), 变长数组我们一般不使用,所以我们无法创建完数组以后再指定其大小,因为大小已经固定了,此时就会用到动态内存函数。

动态内存函数:

       我们知道C语言中有很多函数,但是大家不要将内存函数和动态内存函数搞混了(内存函数可以看我这一篇文章内存函数(超详细)-CSDN博客)。我们使用内存会占用空间,内存大致分为三个区域,栈区、堆区和静态区。我们可以向空间申请动态内存,动态内存在栈区占据。动态内存函数_第1张图片

       因为我们平时使用函数创建的大部分变量都是放在栈区,我们也知道函数的创建会进行压栈(详细请看这篇文章函数的栈帧-CSDN博客) ,但是动态内存函数开辟的空间是放在堆区的,所以它是不是就不会像函数栈帧一样?出了函数就会自动销毁呢?

       答案是肯定的,堆区开辟的内存是不会随着函数一样,使用完就销毁。此时我们就要研究其性质了。

malloc函数:

       我们先来看malloc函数的原码定义:动态内存函数_第2张图片

       参数是大小,大小是指针指向空间的大小,单位是字节,返回的的却是void*,这是小伙伴们就有疑问?void*这不是空类型指针么?有毛用?这里我先埋个伏笔。它的具体作用就是开辟空间,是指针指向一片区域,并返回一个开辟内存的首地址(指针),类型为void*.

int main()
{
	int* p = malloc(40);
	//此时向堆区申请40个字节

	return 0;
}

动态内存函数_第3张图片

       我们知道有强制转换类型操作符,使用动态内存函数的人一定知道使用的是什么类型的指针,所以我们直接将其强制类型转换为想要的类型即可。

int main()
{
	//int* p = (int*)malloc(0);//未定义行为,会报错
	int* p = (int*)malloc(40);

	return 0;
}

        这些申请的内存不会被初始化,此时我们就来观察一下:

int main()
{
	//int* p = (int*)malloc(0);//未定义行为,会报错
	int* p = (int*)malloc(40);
	
    //打印申请的每一个空间内容
	for (int i = 0; i < 10; i++)
	{
		printf("%x ", p[i]);
	}

	return 0;
}

动态内存函数_第4张图片

       奇了怪了,为什么 内容是cdcd……,此时就可以看我的这一篇文章(函数的栈帧-CSDN博客)。所以我们申请完以后空间,就需要将其初始化。

       上述代码中,我们不能传入大小为0的数,这样是没有意义的,所以C语言标准未定义。但是堆区是万能的吗?我们想开辟多少就开辟多少吗?

       因为内存大小是固定的,不可能像开辟多少空间就开辟多少空间,所以malloc函数有时开辟的空间过大就会返回空指针,比如我们向堆区开辟4000000000000个字节,此时就会失败,并将p赋为空指针。

int main()
{
	//int* p = (int*)malloc(0);//未定义行为,会报错
	int* p = (int*)malloc(4000000000000);
    
    if (p == NULL)
	{
		printf("hahah");
	}
	return 0;
}

动态内存函数_第5张图片

       此时我们就可以通过判断,来防止开辟空间失败的情况。其实这一步是很有必要的,因为开辟失败空间会赋值为空指针,不会报错,所以为了防止野指针的情况,我们必须判断。

       为了更好的看出具体报错内容,我们可以使用strerror报错函数(具体可以看这篇文章字符串函数(超详细)-CSDN博客),为了方便使用,我们直接使用perror函数(相当于printf+strerror函数)来观察哪里出先了错误。

int main()
{
	int* p = (int*)malloc(404000000000000);
	if (p == NULL)
	{
		//使用perror函数打印错误信息
		perror("malloc");
		return 1;
	}
	
	return 0;
}

动态内存函数_第6张图片

         我们之前提到,函数一般使用是在栈区上是用内存的,而动态内存是在堆区上创建的,不会随着函数的使用完成就销毁,所以就有了free函数。

free函数:

       当我们向堆区动态内存申请空间指向完成以后,我们要释放使用堆区静态内存,就要使用free函数(只能释放动态内存),要传入指向动态内存的指针(指针指向动态内存的首个地址),之后释放这个指针指向的空间内存。动态内存函数_第7张图片

       但是free函数不会将该指针置为空指针,此指针指向的内存地址不变,我们要手动将此指针置为NULL(空指针)。

int main()
{
	//int* p = (int*)malloc(0);//未定义行为,会报错
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		//使用perror函数打印错误信息
		perror("malloc");
		return 1;
	}
	//释放空间
	free(p);

    //必须手动置空
    p = NULL;
	
	return 0;
}

calloc函数: 

       这个函数和malloc函数几乎没有什么实质性区别,我们来看原码定义:动态内存函数_第8张图片

       上面的描述很清楚,和malloc函数一样,也是在堆取开辟的内存,并返回开辟内存的首地址(指针)。但是要传入两个参数,第一是分配类型的个数,第二是分配类型的大小。和malloc的的区别是它会将开辟的空间初始化为0.

int main()
{
    //malloc(10*sizeof(int))
    int *p=(int*)calloc(10, sizeof(int));//堆区开辟
    //calloc传入创建的类型数量,之后传入每个类型的大小
    //calloc会初始空间的变量,都是0
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i));
        }//打印出calloc函数初始化空间的值
    }
    //释放空间
    //free函数是用来释放动态开辟的空间的
    free(p);
    p = NULL;
    return 0;
}

动态内存函数_第9张图片

问题来了,记得我们在开始提出的问题吗?我们使用动态内存函数就是为了合理使用空间,造成不必要的浪费,可此时申请的空间大小还固定的?而且申请了还要释放并且手动置空,还不如使用数组来的方便。哎,别着急,文章这才二分之一呢,接下来要出场的是——realloc 函数!

realloc函数: 

       这个函数就可以用来调节使用的空间了,但是也有弊端,我们先来看其具体定义:动态内存函数_第10张图片

       realloc函数可以调整动态内存的大小,当使用完malloc或calloc函数后,申请的内存都是固定的,我们可以通过realloc函数进行调整。

int main()
{
    //realloc可以调整动态内存的大小
    char* p1 = (char*)malloc(sizeof(char)* 2);

	//赋值
	int i = 0;
	for (i = 0; i < 2; i++)
	{
		p1[i] = 'a';
	}

	//扩容(将原来的总大小2字节扩容为4字节
	char* p2 = (char*)realloc(p1, sizeof(char) * 4);
    //此时申请总大小为4字节
	
	//判断扩容是否成功
	if (p2 == NULL)
	{
		perror("realloc");
	}

	//开辟赋值
	p1 = p2;
	for (i = 2; i < 4; i++)
	{
		p1[i] = 'b';
	}
	//打印
	for (i = 0; i < 4; i++)
	{
		printf("%c", p1[i]);
	}

    //释放
    free(p1);
    p1 = NULL;
    return 0;
} 

动态内存函数_第11张图片

       上面的程序只是为了搞好的解释realloc的使用,因为字符串要有结束的标志‘\0’,所以要多申请一个字节放置‘\0’,所以我们给出一下改进代码:

int main()
{
    //realloc可以调整动态内存的大小
    char* p1 = (char*)malloc(sizeof(char)* 2);

	//赋值
	int i = 0;
	for (i = 0; i < 2; i++)
	{
		p1[i] = 'a';
	}

	//扩容(将原来的总大小2字节扩容为4字节
	//char* p2 = (char*)realloc(p1, sizeof(char) * 4);
	char* p2 = (char*)realloc(p1, sizeof(char) * 5);
	//多申请一个字节

    //此时申请总大小为5字节
	
	//判断扩容是否成功
	if (p2 == NULL)
	{
		perror("realloc");
	}

	//开辟赋值
	p1 = p2;
	for (i = 2; i < 4; i++)
	{
		p1[i] = 'b';
	}
	//将最后一个字节赋值为'\0'
	p1[i] = '\0';
	printf("%s\n", p1);//打印

    //释放
    free(p1);
    p1 = NULL;
    return 0;
}  

动态内存函数_第12张图片

       通过以上内容,我们不难发现,realloc申请的空间是总大小,而不是添加的大小。

       realloc因为也会返回NULL(就是申请的空间过大),所以我们也要进行判断。所以是不是realloc也可以当做malloc和calloc函数一样使用呢?答案是肯定的,我们来看以下代码:

int main()
{
	int* p = (int*)realloc(NULL, sizeof(int) * 5);
	//直接传入NULL充当malloc的使用
	if (p == NULL)
	{
		perror("realloc");
	}

	//赋值
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", p[i]);//打印
	}

	//释放
	free(p);
	p = NULL;//置空
	return 0;
}

动态内存函数_第13张图片

那么,realloc函数到底是如何开辟内存呢?为什么不直接就赋值给原来的指针呢?为什么还要创建一个临时指针变量来接受呢?接下来,为你解开谜团。 

realloc函数如何开辟内存? 

1.开辟失败

       就是开辟空间过大或者不够用,就会返回NULL。

2.开辟成功

       因为内存分布有它自己的规则,比如原来申请20字节动态内存,要调整为40个字节,没有影响到下一块内存,则返回的地址没有改变;但要调整到40000字节,就会影响到下一块内存,这时realloc函数就会在堆区中开辟一块新的内存区域,返回的地址就是新开辟的内存区的首个字节内存的地址。动态内存函数_第14张图片

       我们来举一个改变原来指针指向的例子(就是申请字节过大):

int main() 
{
	int* p = malloc(NULL, sizeof(int) * 5);

	//扩容为40000个字节
	int* ptr = (int*)realloc(p, sizeof(int) * 40000);
	if (ptr == NULL)
	{
		perror("realloc");
	}
	
	free(p);
	p = NULL;//置空
	return 0;
}

动态内存函数_第15张图片

       此时就发现指向的位置发生了改变。

       那么之前的问题就迎刃而解了, 定义一个临时指针就是为了防止realloc开辟的内存失败,为了防止这种情况我们定义了一个临时指针。

这时,还是会有小伙伴有问题:为什么临时指针指向的空间没有释放?临时指针的值没有置空?好问题,接下来给你答案。

动态内存函数_第16张图片动态内存函数_第17张图片

       所以,我们此时应该就能理解,只需要释放p即可。至于置空,free没有将传入指针指向改变的功能,所以要手动置空。但是却没有置空ptr,实际上,ptr的指向也确实没有改变,会形成野指针,但是毕竟是临时指针,我们其实也就是用这一次。

 free的使用:

       因为由malloc/calloc/realloc申请的空间,如果不主动释放,出了作用域是不会销毁的,和函数的栈帧不一样。有两种释放方式:

  1. free主动释放
  2. 直到程序结束,才由操作系统回收。

       所以一定要释放空间。

动态内存函数使用注意事项:

1.不能越界访问

       在使用动态内存时,首先注意不能越界访问。

int main() 
{
	//对动态内存的越界访问
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		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;//置空
	return 0;
}

动态内存函数_第18张图片

2.没有从最开始的空间释放内存 

       我们使指向堆区的指针改变了指向之后释放内存,会报错。

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 0;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        p++;
    }//越界访问
    free(p);
    p = NULL;//错误示范,指针变量自增
    return 0;
}

动态内存函数_第19张图片

动态内存函数_第20张图片

 3.多次释放同一块内存

       我们已经释放了动态开辟的空间,之后又释放了一次。

 int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 0;
    }
    free(p);
    //p = NULL;
    //除非手动把p指针赋值为空指针
    free(p);//对同一块动态内存多次释放是不行的
    return 0;
}

动态内存函数_第21张图片

4.对非动态开辟的内存free 

       free只能由于动态函数,只能释放堆区空间,不能释放栈区空间。

int main()
{
	//对非动态开辟内存使用free释放
	int a = 10;
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}

	p = &a;//p指向的空间不再是堆区的空间
	free(p);
	p = NULL;

	return 0;
}

动态内存函数_第22张图片

5.对NULL解引用 

       如果我们不做判断就赋值,此时也可能会出现问题。

int main()
{
	//对NULL指针的解引用操作
	int* p = (int*)malloc(100);
	*p = 20;//p有可能是空指针

	return 0;
}

       使用前一定要检测有效性。 

6.内存泄漏

       就是开辟内存以后没有释放内存,就叫做内存泄露,因为只有程序运行完以后,操作系统才会回收,所以一般就存在于运行中的程序。

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 0;
    }
    //没释放空间 free(p)
    //内存没有释放

    //手动置空没有用
    p = NULL;
    return 0;
}

        此时观察不到结果,因为观察不到结果,就很难发现,此时就是一件很危险的事,所以我们使用动态内存一定在程序运行后记得使用free释放内存。

练习:

习题一:

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

       来告诉我你们的答案,是不是hello world?答案是没有任何输出。此时我们画图来分析以上代码:动态内存函数_第23张图片

       开辟空间后,就会更新临时指针p的指向。但是执行完以后临时指针p销毁,而且没有释放空间,导致内存泄露。但是str的指向没有改变,所以还是空,此时还是拷贝不了。动态内存函数_第24张图片

       此时有两种解决方案:

方案1:

       我们传入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);
}
int main()
{
    Test();
    return 0;
}

动态内存函数_第25张图片

 方案2:

       我们使函数返回开辟空间的地址。

char* GetMemory(char* p)
{
    p = (char*)malloc(100);
    return p;
}
void Test(void)
{
    char* str = NULL;
    str=GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    free(str);
}
int main()
{
    Test();
    return 0;
}

动态内存函数_第26张图片

       细心的同学会发现(图片中没有free,大家记得free一下),我并没有将str手动置空,这是因为函数是用完以后,函数里面的变量都已经销毁了,我们也并不会使用到了,所以没有必要多此一举。

习题二: 

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

动态内存函数_第27张图片        函数中创建的数据出函数后就销毁了,所以即使str指向p原来的内存,可是里面的数据已经销毁了,打印不出来。

习题三:

void test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str);//因为free不会把str置成空指针,所以str指向没有改变
    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
int main()
{
    test();
    return 0;
}

       因为free不会将指针置空,所以str不为空,以至于打印world。动态内存函数_第28张图片        打印world是非法访问,所以不能这样写,即使能运行想要的结果。

习题四:

int* test()
{
    int a=10;//栈区
    return &a;
}
int main()
{
    int *p=test();
    *p = 20;
    printf("%d\n", *p);
    return 0;
}

       即使返回了地址,但是返回的是地址,内存是会被销毁的,所以*p即使成功,也会非法访问。 动态内存函数_第29张图片

       此时解决办法就是将a的生命周期延长,用static修饰。

int* test()
{
    static int a = 10;//静态区
    //int a=10;//栈区
    return &a;
}
int main()
{
    int *p=test();
    *p = 20;
    printf("%d\n", *p);
    return 0;
}

       此时a的内存就没有被销毁, 我们对其解引用就是正常访问。

结束语

       此时也会有人发现动态内存函数的不足之处,就是无法将开辟的空间缩小,只能扩大,此时我们就需要一种神奇的知识了:数据结构。其中的链表是可以随意调整的。细心看完这篇文章,相信你收获颇丰。

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