做者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文:www.jianshu.com/p/bbce8f04f…程序员
《C语言探索之旅》全系列编程
上一课是 C语言探索之旅 | 第二部分第七课:文件读写 。数组
经历了第二部分的一些难点课程,咱们终于来到了这一课,一个听起来有点酷酷的名字:动态分配。bash
“万水千山老是情,分配也由系统定”。微信
到目前为止,咱们建立的变量都是系统的编译器为咱们自动构建的,这是简单的方式。函数
其实还有一种更偏手动的建立变量的方式,咱们称为“动态分配”(Dynamic Allocation)。dynamic 表示“动态的”,allocation 表示“分配”。学习
动态分配的一个主要好处就是能够在内存中“预置”必定空间大小,在编译时还不知道到底会用多少。测试
使用这个技术,咱们能够建立大小可变的数组。到目前为止咱们所建立的数组都是大小固定不可变的。而学完这一课后咱们就会建立所谓“动态数组”了。ui
学习这一章须要对指针有必定了解,若是指针的概念你还没掌握好,能够回去复习 C语言探索之旅 | 第二部分第二课:进击的指针,C语言的王牌! 那一课。spa
咱们知道当咱们建立一个变量时,在内存中要为其分配必定大小的空间。例如:
int number = 2;
复制代码
当程序运行到这一行代码时,会发生几件事情:
应用程序询问操做系统(Operating System,简称 OS。例如Windows,Linux,macOS,Android,iOS,等)是否可使用一小块内存空间。
操做系统回复咱们的程序,告诉它能够将这个变量存储在内存中哪一个地方(给出分配的内存地址)。
当函数结束后,你的变量会自动从内存中被删除。你的程序对操做系统说:“我已经不须要内存中的这块地址了,谢谢!” (固然,实际上你的程序不可能对操做系统说一声“谢谢”,可是确实是操做系统在掌管一切,包括内存,因此对它仍是客气一点比较好...)。
能够看到,以上的过程都是自动的。当咱们建立一个变量,操做系统就会自动被程序这样调用。
那么什么是手动的方式呢?说实在的,没人喜欢把事情复杂化,若是自动方式可行,何须要大费周章来使用什么手动方式呢?可是要知道,不少时候咱们是不得不使用手动方式。
这一课中,咱们将会:
探究内存的机制(是的,虽然之前的课研究过,可是仍是要继续深刻),了解不一样变量类型所占用的内存大小。
接着,探究这一课的主题,来学习如何向操做系统动态请求内存。也就是所谓的“动态内存分配”。
最后,经过学习如何建立一个在编译时还不知道其大小(只有在程序运行时才知道)的数组来了解动态内存分配的好处。
准备好了吗?Let's Go !
根据咱们所要建立的变量的类型(char,int,double,等等),其所占的内存空间大小是不同的。
事实上,为了存储一个大小在 -128 至 127 之间的数(char 类型),只须要占用一个字节(8 个二进制位)的内存空间,是很小的。
然而,一个 int 类型的变量就要占据 4 个字节了;一个 double 类型要占据 8 个字节。
问题是:并不老是这样。
什么意思呢?
由于类型所占内存的大小还与操做系统有关系。不一样的操做系统可能就不同,32 位和 64 位的操做系统的类型大小通常会有区别。
这一节中咱们的目的是学习如何获知变量所占用的内存大小。
有一个很简单的方法:使用 sizeof()
。
虽然看着有点像函数,但其实 sizeof 不是一个函数,而是一个 C语言的关键字,也算是一个运算符吧。
咱们只须要在 sizeof 的括号里填入想要检测的变量类型,sizeof 就会返回所占用的字节数了。
例如,咱们要检测 int 类型的大小,就能够这样写:
sizeof(int)
复制代码
在编译时,sizeof(int)
就会被替换为 int 类型所占用的字节数了。
在个人电脑上,sizeof(int)
是 4,也就是说 int 类型在个人电脑的内存中占据 4 个字节。在你的电脑上,也许是 4,但也多是其余的值。
咱们用一个例子来测试一下吧:
// octet 是英语“字节”的意思,和 byte 相似
printf("char : %d octets\n", sizeof(char));
printf("int : %d octets\n", sizeof(int));
printf("long : %d octets\n", sizeof(long));
printf("double : %d octets\n", sizeof(double));
复制代码
在个人电脑(64 位)运行,输出:
char : 1 octets
int : 4 octets
long : 8 octets
double : 8 octets
复制代码
咱们并无测试全部已知的变量类型,你也能够课后本身去测试一下其余的类型,例如:short,float。
曾几什么时候,当电脑的内存很小的年代,有这么多不一样大小的变量类型可供选择是一件很好的事,由于咱们能够选“够用的最小的”那种变量类型,以节约内存。
如今,电脑的内存通常都很大,“有钱任性”么。因此咱们在编程时也不必太“拘谨”。不过在嵌入式领域,内存大小通常是有限的,咱们就得斟酌着使用变量类型了。
既然 sizeof 这么好用,咱们可不能够用它来显示咱们自定义的变量类型的大小呢?例如 struct,enum,union。
是能够的。写一个程序测试一下:
#include <stdio.h>
typedef struct Coordinate
{
int x;
int y;
} Coordinate;
int main(int argc, char *argv[])
{
printf("Coordinate 结构体的大小是 : %d 个字节\n", sizeof(Coordinate));
return 0;
}
复制代码
运行输出:
Coordinate 结构体的大小是 : 8 个字节
复制代码
以前,咱们在绘制内存图示时,仍是比较不精准的。如今,咱们知道了每一个变量所占用的大小,咱们的内存图示就能够变得更加精准了。
假如我定义一个 int 类型的变量:
int age = 17;
复制代码
咱们用 sizeof 测试后得知 int 的大小为 4。假设咱们的变量 age 被分配到的内存地址起始是 1700,那么咱们的内存图示就以下所示:
咱们看到,咱们的 int 型变量 age 在内存中占用 4 个字节,起始地址是 1700(它的内存地址),一直到 1703。
若是咱们对一个 char 型变量(大小是一个字节)一样赋值:
char number = 17;
复制代码
那么,其内存图示是这样的:
假如是一个 int 型的数组:
int age[100];
复制代码
用 sizeof() 测试一下,就能够知道在内存中 age 数组占用 400 个字节。4 * 100 = 400。
即便这个数组没有赋初值,可是在内存中仍然占据 400 个字节的空间。变量一声明,在内存中就为它分配必定大小的内存了。
那么,若是咱们建立一个类型是 Coordinate 的数组呢?
Coordinate coordinate[100];
复制代码
其大小就是 8 * 100 = 800 个字节了。
好了,如今咱们就进入这一课的关键部分了,重提一次这一课的目的:学会如何手动申请内存空间。
咱们须要引入 stdlib.h 这个标准库头文件,由于接下来要使用的函数是定义在这个库里面。
这两个函数是什么呢?就是:
malloc:是 Memory Allocation 的缩写,表示“内存分配”。询问操做系统可否预支一块内存空间来使用。
free:表示“解放,释放,自由的”。意味着“释放那块内存空间”。告诉操做系统咱们再也不须要这块已经分配的空间了,这块内存空间会被释放,另外一个程序就可使用这块空间了。
当咱们手动分配内存时,需要按照如下三步顺序来:
调用 malloc 函数来申请内存空间。
检测 malloc 函数的返回值,以得知操做系统是否成功为咱们的程序分配了这块内存空间。
一旦使用完这块内存,再也不须要时,必须用 free 函数来释放占用的内存,否则可能会形成内存泄漏。
以上三个步骤是否是让咱们回忆起关于上一课“文件读写”的内容了?
这三个步骤和文件指针的操做有点相似,也是先申请内存,检测是否成功,用完释放。
malloc 分配的内存是在堆上,通常的局部变量(自动分配的)大可能是在栈上。
关于堆和栈的区别,还有内存的其余区域,如静态区等,你们能够本身延伸阅读。
以前“字符串”那一课里已经给出过一张图表了。再来回顾一下吧:
名称 | 内容 |
---|---|
代码段 | 可执行代码、字符串常量 |
数据段 | 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据 |
BSS段 | 未初始化全局变量,未初始化全局静态变量 |
栈 | 局部变量、函数参数 |
堆 | 动态内存分配 |
给出 malloc 函数的原型,你会发现有点滑稽:
void* malloc(size_t numOctetsToAllocate);
复制代码
能够看到,malloc 函数有一个参数 numOctetsToAllocate,就是须要申请的内存空间大小(用字节数表示),这里的 size_t(以前的课程有提到过)其实和 int 是相似的,就是一个 define 宏定义,实际上不少时候就是 int。
对于咱们目前的演示程序,能够将 sizeof(int) 置于 malloc 的括号中,表示要申请 int 类型的大小的空间。
真正引发咱们兴趣的是 malloc 函数的返回值:
void*
复制代码
若是你还记得咱们在函数那章所说的,void 表示“空”,咱们用 void 来表示函数没有返回值。
因此说,这里咱们的函数 malloc 会返回一个指向 void 的指针,一个指向“空”(void 表示“虚无,空”)的指针,有什么意义呢?malloc 函数的做者不会搞错了吧?
不要担忧,这么作确定是有理由的。
难道有人敢质疑老爷子 Dennis Ritchie(C语言的做者)的智商? 来人呐,拖出去... 罚写 100 个 C语言小游戏。
事实上,这个函数返回一个指针,指向操做系统分配的内存的首地址。
若是操做系统在 1700 这个地址为你开辟了一块内存的话,那么函数就会返回一个包含 1700 这个值的指针。
可是,问题是:malloc 函数并不知道你要建立的变量是什么类型的。
实际上,你只给它传递了一个参数: 在内存中你须要申请的字节数。
若是你申请 4 个字节,那么有多是 int 类型,也有多是 long 类型。
正由于 malloc 不知道本身应该返回什么变量类型(它也无所谓,只要分配了一块内存就能够了),因此它会返回 void*
这个类型。这是一个能够表示任意指针类型的指针。
void*
与其余类型的指针之间能够经过强制转换来相互转换。例如:
int *i = (int *)p; // p 是一个 void* 类型的指针
void *v = (void *)c; // c 是一个 char* 类型的指针
复制代码
若是我实际来用 malloc 函数分配一个 int 型指针:
int *memoryAllocated = NULL; // 建立一个 int 型指针
memoryAllocated = malloc(sizeof(int)); // malloc 函数将分配的地址赋值给咱们的指针 memoryAllocated
复制代码
通过上面的两行代码,咱们的 int 型指针 memoryAllocated 就包含了操做系统分配的那块内存地址的首地址值。
假如咱们用以前咱们的图示来举例,这个值就是 1700。
既然上面咱们用两行代码使得 memoryAllocated 这个指针包含了分配到的地址的首地址值,那么咱们就能够经过检测 memoryAllocated 的值来判断申请内存是否成功了:
若是为 NULL,则说明 malloc 调用没有成功。
不然,就说明成功了。
通常来讲内存分配不会失败,可是也有极端状况:
你的内存(堆内存)已经不够了。
你申请的内存值大得离谱(好比你申请 64 GB 的内存空间,那我想大多数电脑都是不可能分配成功的)。
但愿你们每次用 malloc 函数时都要作指针的检测,万一真的出现返回值为 NULL 的状况,那咱们须要当即中止程序,由于没有足够的内存,也不可能进行下面的操做了。
为了中断程序的运行,咱们来使用一个新的函数:
exit()
复制代码
exit 函数定义在 stdlib.h 中,调用此函数会使程序当即中止。
这个函数也只有一个参数,就是返回值,这和 return 函数的参数是同样原理的。实例:
int main(int argc, char *argv[])
{
int *memoryAllocated = NULL;
memoryAllocated = malloc(sizeof(int));
if (memoryAllocated == NULL) // 若是分配内存失败
{
exit(0); // 当即中止程序
}
// 若是指针不为 NULL,那么能够继续进行接下来的操做
return 0;
}
复制代码
能够测试一下,也能够去查找关于 malloc 函数的说明文档。
申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。 可是你却没法使用这块大小为 0 的内存!
这就比如尺子上的某个刻度,刻度自己并无长度,只有某两个刻度一块儿才能量出长度。
对于这一点必定要当心,由于这时候 if(NULL != p)
语句校验将不起做用。
记得上一课咱们使用 fclose 函数来关闭一个文件指针,也就是释放占用的内存。
free 函数的原理和 fclose 是相似的,咱们用它来释放一块咱们再也不须要的内存。原型:
void free(void* pointer);
复制代码
free 函数只有一个目的:释放 pointer 指针所指向的那块内存。
实例程序:
int main(int argc, char *argv[])
{
int* memoryAllocated = NULL;
memoryAllocated = malloc(sizeof(int));
if (memoryAllocated == NULL) // 若是分配内存失败
{
exit(0); // 当即中止程序
}
// 此处添加使用这块内存的代码
free(memoryAllocated); // 咱们再也不须要这块内存了,释放之
return 0;
}
复制代码
综合上面的三个步骤,咱们来写一个完整的例子:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int* memoryAllocated = NULL;
memoryAllocated = malloc(sizeof(int)); // 分配内存
if (memoryAllocated == NULL) // 检测是否分配成功
{
exit(0); // 不成功,结束程序
}
// 使用这块内存
printf("您几岁了 ? ");
scanf("%d", memoryAllocated);
printf("您已经 %d 岁了\n", *memoryAllocated);
free(memoryAllocated); // 释放这块内存
return 0;
}
复制代码
运行输出:
您几岁了 ? 32
您已经 32 岁了
复制代码
以上就是咱们用动态分配的方式来建立了一个 int 型变量,使用它,释放它所占用的内存。
可是,咱们也彻底能够用之前的方式来实现,以下:
int main(int argc, char *argv[])
{
int myAge = 0; // 分配内存 (自动)
// 使用这块内存
printf("您几岁了 ? ");
scanf("%d", &myAge);
printf("你已经 %d 岁了\n", myAge);
return 0;
} // 释放内存 (在函数结束后自动释放)
复制代码
在这个简单使用场景下,两种方式(手动和自动)都是能完成任务的。
总结说来,建立一个变量(说到底也就是分配一块内存空间)有两种方式:自动和手动。
自动:咱们熟知而且一直使用到如今的方式。
手动(动态):这一课咱们学习的内容。
你可能会说:“我发现动态分配内存的方式既复杂又没什么用嘛!”
复杂么?还行吧,确实相对自动的方式要考虑比较多的因素。
没有用么?毫不!
由于不少时候咱们不得不使用手动的方式来分配内存。
接下来咱们就来看一下手动方式的必要性。
暂时咱们只是用手动方式来建立了一个简单的变量。
然而,通常说来,咱们的动态分配可不是这样“大材小用”的。
若是只是建立一个简单的变量,咱们用自动的方式就够了。
那你会问:“啥时候需要用动态分配啊?”
问得好。动态分配最常被用来建立在运行时才知道大小的变量,例如动态数组。
假设咱们要存储一个用户的朋友的年龄列表,按照咱们之前的方式(自动方式),咱们能够建立一个 int 型的数组:
int ageFriends[18];
复制代码
很简单对吗?那问题不就解决了?
可是以上方式有两个缺陷:
你怎么知道这个用户只有 18 个朋友呢?可能他有更多朋友呢。
你说:“那好,我就建立一个数组:
int ageFriends[10000];
复制代码
足够储存 1 万个朋友的年龄。”
可是问题是:可能咱们使用到的只是这个大数组的很小一部分,岂不是浪费内存嘛。
最恰当的方式是询问用户他有多少朋友,而后建立对应大小的数组。
而这样,咱们的数组大小就只有在运行时才能知道了。
Voila,这就是动态分配的优点了:
能够在运行时才肯定申请的内存空间大小。
很少很多刚恰好,要多少就申请多少,不怕不够或过多。
因此借着动态分配,咱们就能够在运行时询问用户他到底有多少朋友。
若是他说有 20 个,那咱们就申请 20 个 int 型的空间;若是他说有 50 个,那就申请 50 个。经济又环保。
咱们以前说过,C语言中禁止用变量名来做为数组大小,例如不能这样:
int ageFriends[numFriends]; // numFriends 是一个变量
复制代码
尽管有的 C编译器可能容许这样的声明,可是咱们不推荐。
咱们来看看用动态分配的方式如何实现这个程序:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int numFriends = 0, i = 0;
int *ageFriends= NULL; // 这个指针用来指示朋友年龄的数组
// 询问用户有多少个朋友
printf("请问您有多少朋友 ? ");
scanf("%d", &numFriends);
if (numFriends > 0) // 至少得有一个朋友吧,否则也太惨了 :P
{
ageFriends = malloc(numFriends * sizeof(int)); // 为数组分配内存
if (ageFriends== NULL) // 检测分配是否成功
{
exit(0); // 分配不成功,退出程序
}
// 逐个询问朋友年龄
for (i = 0 ; i < numFriends; i++) {
printf("第%d位朋友的年龄是 ? ", i + 1);
scanf("%d", &ageFriends[i]);
}
// 逐个输出朋友的年龄
printf("\n\n您的朋友的年龄以下 :\n");
for (i = 0 ; i < numFriends; i++) {
printf("%d 岁\n", ageFriends[i]);
}
// 释放 malloc 分配的内存空间,由于咱们再也不须要了
free(ageFriends);
}
return 0;
}
复制代码
运行输出:
请问您有多少朋友 ? 7
第1位朋友的年龄是 ? 25
第2位朋友的年龄是 ? 21
第3位朋友的年龄是 ? 27
第4位朋友的年龄是 ? 18
第5位朋友的年龄是 ? 14
第6位朋友的年龄是 ? 32
第7位朋友的年龄是 ? 30
您的朋友的年龄以下 :
25岁
21岁
27岁
18岁
14岁
32岁
30岁
复制代码
固然了,这个程序比较简单,但我向你保证之后的课程会使用动态分配来作更有趣的事。
不一样类型的变量在内存中所占的大小不尽相同。
借助 sizeof 这个关键字(也是运算符)能够知道一个类型所占的字节数。
动态分配就是在内存中手动地预留一块空间给一个变量或者数组。
动态分配的经常使用函数是 malloc(固然,还有 calloc,realloc,能够查阅使用方法,和 malloc 是相似的),可是在不须要这块内存以后,千万不要忘了使用 free 函数来释放。并且,malloc 和 free 要一一对应,不能一个 malloc 对应两个 free,会出错;或者两个 malloc 对应一个 free,会内存泄露!
动态分配使得咱们能够建立动态数组,就是它的大小在运行时才能肯定。
今天的课就到这里,一块儿加油吧!
下一课: C语言探索之旅 | 第二部分第九课: 实战"悬挂小人"游戏
我是 谢恩铭,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」