一、什么是内存的动态分配
全局变量分配在内存中的静态存储区。局部变量(包括形参)分配在内存中的动态存储区,这个存储区是一个称为栈的区域。除此之外,C语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。这些数据是存放在一个特别的自由存储区,称为堆区。可以根据需要向系统申请所需大小的空间。由于未在声明部分定义它们为变量或数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。
也就是说,全局变量分配于静态存储区,局部变量分配于栈,动态内存分配在堆。
二、怎样建立内存的动态分配
对于内存的动态分配是通过系统提供的库函数来实现的,主要有malloc,calloc,realloc,free这四个函数。
1.用malloc函数开辟动态内存
malloc的函数原型为:
void *malloc(unsigned int size);
malloc函数的作用是:
在内存的动态存储区中分配一个长度为size的连续空间。形参size的类型定为无符号整型(不允许为负数)。此函数的值(即“返回值”)是所分配区域的第一个字节的首地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的第一个字节。如:
malloc(100);
//开辟100字节的临时分配域,函数值为其第一个字节的地址。
注意指针的基类型为void,即不指向任何类型的数据,只提供一个地址。如果此函数未能成功执行(例如内存空间不足),则返回空指针(NULL),失败只有一种情况,就是申请的内容太大,超出堆能提供的最大连续空间。
2.用calloc函数开辟动态内存
calloc的函数原型为:
void*calloc(unsigned n,unsigned size);
calloc的函数的作用是:
在内存的动态存储区中分配n个长度为size的连续空间,这个空间一般比较大,足以保存一个数组。
用calloc函数可以为一维数组开辟动态内存存储空间,n为数组元素个数,每个元素长度为size。这就是动态数组。函数的指针指向所分配域的第一个字节,此函数的值(即“返回值”)是所分配区域的第一个字节的首地址;如果不成功,返回空指针(NULL)。如:
p=calloc(50,4);
//开辟50个长度为4个字节的临时分配域,把首地址赋给指针变量p
calloc和malloc最大的区别为:申请完空间之后calloc会将每个元素的值直接置为0。
3.用realloc函数重新分配动态内存
realloc函数的原型:
void *realloc(void*p,unsigned int size);
参数的意义:p:旧内存的地址;size:新申请的内存大小,以字节为单位
realloc函数的作用是:
如果已经通过malloc函数或calloc函数获得了动态内存空间,想改变其大小,可以用realloc函数重新分配。用realloc函数将p所指向的动态内存空间的大小改变为size。p的值不变。函数值成功返回时返回新的动态内存的首地址,如果重分配不成功,返回空指针(NULL)。失败只有一种情况就是申请的内容太大,超出堆能提供的最大连续空间。如:
realloc(p,50);
//将p指向的动态内存空间的大小改变为50字节
用malloc实现realloc扩容:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
int n = 10;
int *p=(int*)malloc(n * sizeof(int));
assert(p != NULL);
int i;
for (i = 0; i < n; i++)
{
p[i] = i;
}
for (i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//1发现p申请的太少,于是重新申请
int*q=(int *)malloc(2* n * sizeof(int));//1申请新内存
//2搬家
for (i = 0; i < 2*n; i++)
{
q[i] =i;
}
for (i = 0; i < 2*n; i++)
{
printf("%d ", q[i]);
}
free(p);//3释放p的内存
p = q;//4接收新内存
q = NULL;
//...后面可以继续使用p
return 0;
}
realloc实现扩容:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
int n = 10;
int* p = (int*)malloc(n * sizeof(int));
assert(p != NULL);
int i;
for (i = 0; i < n; i++)
{
p[i] = i;
}
for (i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//1发现p申请的太少,于是重新申请
p = (int*)realloc(p, 2 * n * sizeof(int));
for (i = 0; i < 2 * n; i++)
{
p[i] = i;
}//2搬家
for (i = 0; i < 2 * n; i++)
{
printf("%d ", p[i]);
}
//...后面可以继续使用p
free(p);
return 0;
}
4.用free函数释放动态存储区
free函数的原型:
void free(viod*p);
free函数的作用是:
释放指针变量p所指向的动态内存空间,使这部分空间能重新被其他变量使用。p应是最近一次调用calloc或malloc函数时得到的函数返回值。free函数无返回值。如:
free(p);
//释放指针变量p所指向的已分配的动态内存空间
free崩溃的原因:
1.p移动了(p++),因为申请p的时候有一个n的大小,如果p移动,找不到头信息
2.找不到尾信息
3.重复释放了一段内存
以上4个函数malloc,calloc,realloc,free的声明都在stdlib.h的头文件中,在用到这些函数时应该用#include<stdlib.h>
指令把stdlib.h头文件包含在程序文件中。
三、void指针类型
C99允许使用基类型为void的指针类型。可以定义一个基类型为void的指针变量(即void*型变量),它不指向任何类型的数据。
注意:不要把“指向void类型”理解为能指向“任何类型”的数据,而应理解为“指向空类型”或“不指向确定的类型”的数据。在将它的值赋给另一指针变量时由系统对它进行类型转换,使之适合于被赋值的变量的类型。
例如:
int main()
{
int a = 3; //定义a为整型变量
int* p1 = &a;//p1指向int型变量
char* p2;//p2指向char型变量
void* p3;//p3为无类型指针变量(基类型为void型)
p3 = (void*)p1;//将p1的值转换为void*类型,然后赋值给p3
p2 = (char*)p3;//将p3的值转换为char*类型,然后赋值给p2
printf("%d", *p1);//合法,输出整型变量a的值
void* p3 = &a; printf("%d", *p3);//错误,p3是无指向的,不能指向a
}
说明:
地址信息应该包含位置信息和基类型的信息。 一定要注意基类型的信息,即存放存放在以此地址标志的存储单元中的数据类型,否则无法实现对数据的存取。所以对于void*
类型的指针,这种指针无指向,在这种无指向的地址所标志的存储单元中是不可以存储任何数据的,也就是说无法通过这种地址对内存存取数据。
什么情况下会用到void*类型的指针:
在调用动态存储分配函数(如malloc、calloc、realloc函数)时会用到void*
类型的指针。用户用这些函数开辟动态存储区时,希望获得此动态存储区的起始地址,以便利用该动态存储区。
C99规定malloc、calloc、realloc函数返回void*
指针,使其“无指向”,这种指针称为“空类型指针”,它不指向任一种具体的数据类型,只提供一个地址。这是C有关地址应用的一种特殊情况。
这种空类型指针在形式上和其他指针一样,遵循C语言对指针的有关规定,它也有基类型,只是它的基类型是void
,可以这样定义:
void*p; //定义p是void*型的指针变量
void*型指针代表“无指向的地址”,这种指针不指向任何类型的数据。不能企图通过它存取数据,在程序中它只是过渡性的,只有转换为有指向的地址,才能存取数据。
四、建立动态内存分配区和使用void指针
【例题】建立动态数组,输入5个学生的成绩,另外用一个函数检查其中有无低于60分的,输出不合格的成绩。
【思路】用malloc开辟一个动态自由区域,用来存放5个学生的成绩,会得到这个动态域的第一个字节的地址,他的基类型是void型。用一个基类型是int的指针变量p来指向动态数组的各元素,并输出它们的值。但必须先把malloc函数返回的void指针转换为整型指针,然后赋给p1。
#include <stdio.h>
#include <stdlib.h> //程序中用了malloc函数,应包含stdlib.h
void check(int* p)//定义check函数,形参是int*指针
{
printf("不合格的成绩为:");
for (int i = 0; i < 5; i++)
{
if (p[i] < 60)
printf("%d ", p[i]);//输出不合格成绩
}
printf("\n");
}
int main()
{
int* p1 = (int*)malloc(5 * sizeof(int)); //p1是int型指针,开辟动态内存,将地址转换为int*型,然后放在p1中
for (int i = 0; i < 5; i++)
{
scanf("%d", &p1[i]);//输入5个学生的成绩
}
check(p1);//调用check函数
free(p1);
return 0;
}
五、断言assert
一种比较保守的编程方式:每一次都要验证动态申请内存的返回值是否为空,这里就需要用到断言assert,assert函数的声明都在assert.h的头文件中,在用到这个函数时应该用#include<assert.h>
指令把assert.h头文件包含在程序文件中。
【例题】输出n个连续自然数中的所有素数
void Getprimer(int n)
{
int* p = (int*)malloc(n * sizeof(int));//堆
assert(p != NULL);
for (int i = 0; i < n; i++)
{
p[i] = 1;
}
p[1] = p[0] = 0;//0和1不是素数
for (int i = 2; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (j % i == 0)
p[j] = 0;
}
}
for (int i = 2; i < n; i++)
{
if (p[i] == 1)
{
printf("%d是素数\n", i);
}
}
free(p);
}
int main()
{
int n;
printf("请输入n的大小:");
scanf("%d", &n);
Getprimer(n);
return 0;
}