分享

已学【C语言探索之旅】 第二部分第八课:动态分配

 昵称29540336 2015-12-10


  内容简介

  1、课程大纲

  2、第二部分第八课: 动态分配

  3、第二部分第九课预告: 实战“悬挂小人”游戏

  课程大纲

  我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案。还会带大家用C语言编写三个游戏。

  C语言编程基础知识

  什么是编程?

  工欲善其事,必先利其器

  你的第一个程序

  变量的世界

  运算那点事

  条件表达式

  循环语句

  实战:第一个C语言小游戏

  函数

  练习题

  习作:完善第一个C语言小游戏

  C语言高级技术

  模块化编程

  进击的指针,C语言王牌

  数组

  字符串

  预处理

  创建你自己的变量类型

  文件读写

  动态分配

  实战:“悬挂小人”游戏

  安全的文本输入

  练习题

  习作:用自己的语言解释指针

  用基于C语言的SDL库开发2D游戏

  安装SDL

  创建窗口和画布

  显示图像

  事件处理

  实战:“超级玛丽推箱子”游戏

  掌握时间的使用

  用SDL_ttf编辑文字

  用FMOD控制声音

  实战:可视化的声音谱线

  练习题

  数据结构

  链表

  堆,栈和队列

  哈希表

  练习题

  第二部分第八课:动态分配

  经历了第二部分的一些难点课程,我们终于来到了这一课,一个听起来有点酷酷的名字: 动态分配。

  “万水千山总是情,分配也由系统定”

  到目前为止,我们创建的变量都是编译器为我们自动构建的,这是简单的方式。其实还有一种更偏手动的创建变量的方式,我们称为“动态分配”(Dynamic Allocation)。

  动态分配的一个主要好处就是可以在内存中“预置”一定空间大小,在编译时还不知道到底会用多少。使用这个技术,我们可以创建大小可变的数组。到目前为止我们所创建的数组都是大小固定不可变的。而学完这一课后我们就会创建所谓“动态数组”了。

  学习这一章需要对指针有一定了解,如果指针的概念你还没掌握好,可以回去复习《指针,C语言的王牌》那一课。

  我们知道当我们创建一个变量时,在内存中要为其分配一定大小的空间。例如:

  int number = 2;

  当程序运行到这一行代码时,会发生几件事情:

  1. 应用程序询问 操作系统(Operating System,简称OS。例如Windows,Linux,Mac OS,等)是否可以使用一小块内存空间

  2. 操作系统回复我们的程序,告诉它可以将这个变量存储在内存中哪个地方(给出分配的内存地址)

  3. 当函数结束后,你的变量会自动从内存中被删除。你的程序对操作系统说:“我已经不需要内存中的这块地址了,谢谢!” 当然,实际上你的程序不可能对操作系统说一声“谢谢”,但是确实是操作系统在掌管一切,包括内存,所以对它还是客气一点比较好。

  可以看到,以上的过程都是自动的。当我们创建一个变量,操作系统就会自动被程序这样调用。

  那么什么是手动的方式呢?说实在的,没人喜欢把事情复杂化,如果自动方式可行,何必要大费周章来使用什么手动方式呢?但是要知道,很多时候我们是不得不使用手动方式。

  这一课中,我们将会:

  1. 探究内存的机制(是的,虽然以前的课研究过,但是还是要继续深入),了解不同变量类型所占用的内存大小

  2. 接着,探究这一课的主题,来学习如何向操作系统动态请求内存。也就是所谓的“动态内存分配”

  3. 最后,通过学习如何创建一个在编译时还不知道其大小(只有在程序运行时才知道)的数组来了解动态内存分配的好处

  变量的大小

  根据我们所要创建的变量的类型不同(char,int,double,等等),变量所占的内存空间大小是不一样的。

  事实上,为了存储一个大小在-128至127之间的数(char类型),只需要占用一个字节(8个二进制位)的内存空间,是很小的。

  然而,一个int类型的变量就要占据4个字节了;一个double类型要占据8个字节。

  问题是:并不总是这样。什么意思呢?

  因为类型所占内存的大小还与操作系统有关系,比如不同的操作系统可能就不一样,或者32位和64位的操作系统的类型大小一般会有区别。也许你的电脑上int类型是8个字节呢。

  这一节中我们的目的是学习如何获知变量所占用的内存大小。

  有一个很简单的方法:使用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。

  是可以的。写一个程序测试一下:

  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 = 18;

  我们用sizeof测试后得知int的大小为4。假设我们的变量age被分配到的内存地址起始是1600,那么我们的内存图示就如下所示:


  我们看到,我们的int型变量age在内存中占用4个字节,起始地址是1600(它的内存地址),一直到1603

  如果我们对一个char型变量同样赋值:

  char number = 18;

  那么,其内存图示是这样的:


  假如是一个int型的数组:

  int age[100];

  用sizeof()测试一下,就可以知道在内存中age数组占用400个字节。4 * 100 = 400

  即使这个数组没有赋初值,但是在内存中仍然占据400个字节的空间。变量一声明,在内存中就为它分配一定大小的内存了。

  那么,如果我们创建一个类型是Coordinate的数组呢?

  Coordinate coordinate[100];

  其大小就是 8 * 100 = 800 个字节了。

  内存的动态分配

  好了,现在我们就进入这一课的关键部分了,重提一次这一课的目的:学会如何手动申请内存空间。

  我们需要引入 stdlib.h 这个标准库头文件,因为接下来要使用的函数是定义在这个库里面。

  这两个函数是什么呢?就是:

  malloc:是Memory Allocation的缩写,也就是英语“内存分配”的意思。询问操作系统能否使用一块内存空间

  free:英语“解放,释放,自由的”的意思,意味着“释放那块内存空间”。告诉操作系统我们不再需要这块已经分配的空间了,这块内存空间会被释放,另一个程序就可以使用这块空间了

  当我们手动分配内存时,须要按照以下三步顺序来:

  1. 调用malloc函数来申请内存空间

  2. 检测malloc函数的返回值,以得知操作系统是否成功为我们的程序分配了这块内存空间

  3. 一旦使用完这块内存,不再需要时,必须用free函数来释放占用的内存,不然可能会造成内存泄漏

  以上三个步骤是不是让我们回忆起关于上一课“文件读写”的内容了?

  这三个步骤和文件指针的操作有点类似,也是先申请内存,检测是否成功,用完释放。

  malloc:申请内存

  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的指针,一个指向“空”的指针,有什么意义呢?malloc函数的作者不会搞错了吧?

  不要担心,人家这样肯定是有理由的。难道我们敢质疑老爷子Dennis Ritchie的智商?来人呐,拖出去...

  事实上,这个函数返回一个指针,指向操作系统分配的内存的首地址。如果操作系统在1600这个地址为你开辟了一块内存的话,那么函数就会返回一个包含1600这个值的指针。

  但是,问题是: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就包含了操作系统分配的那块内存地址的首地址值。假如我们用之前我们的图示来举例,这个值就是1600

  检测指针

  既然上面我们用两行代码使得memoryAllocated这个指针包含了分配到的地址的首地址值,那么我们就可以通过检测memoryAllocated的值来判断申请内存是否成功了:

  1. 如果为NULL,则说明malloc调用没有成功

  2. 否则,就说明成功了

  一般来说分配内存不会失败,但是也有极端情况:

  1. 你的内存(堆内存)已经不够了

  2. 你申请的内存值大的离谱(比如你要申请64GB的内存空间,那我想大多数电脑都是不可能分配成功的)

  希望大家每次用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 指针吗?

  可以测试一下,也可以去查找关于malloc 函数的说明文档。申请0 字节内存,函数并不返回NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为0 的内存!这好比尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这一点一定要小心,因为这时候if(NULL != p)语句校验将不起作用。

  free

  记得上一课我们使用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;

  }

  运行输出:

  您几岁了 ? 27

  您已经 27 岁了

  以上就是我们用动态分配的方式来创建了一个int型变量,使用它,释放它所占用的内存。

  但是,我们也完全可以用以前的方式来实现,如下:

  int main(int argc, char *argv[])

  {

  int myAge = 0; // 分配内存 (自动)

  // 使用这块内存

  printf('您几岁了 ? ');

  scanf('%d', &myAge);

  printf('你已经 %d 岁了\n', myAge);

  return 0;

  } // 释放内存 (在函数结束后自动释放)

  在这个简单使用场景下,两种方式(手动和自动)都是能完成任务的。

  总结说来,创建一个变量(说到底也就是分配一块内存空间)有两种方式:自动和手动。

  自动:我们熟知并且一直用到现在的方式

  手动(动态):这一课我们学习的内容

  你可能会说:“我发现动态分配内存的方式既复杂又没什么用嘛!”

  复杂么?还行吧,确实相对自动的方式要考虑比较多的因素。

  没有用么?绝不!

  因为很多时候我们不得不使用手动的方式来分配内存。接下来我们就来看一下手动方式的必要性。

  动态分配一个数组

  暂时我们只是用手动方式来创建了一个简单的变量。然而,一般说来,我们可不是这样“大材小用”的。如果只是创建一个简单的变量,我们用自动的方式就够了。

  那你会问:“啥时候须要用动态分配啊?”

  问得好。动态分配最常被用来创建在运行时才知道大小的数组,也就是动态数组。

  假设我们要存储一个用户的朋友的年龄列表,按照我们以前的方式(自动方式),我们可以创建一个int型的数组:

  int ageFriends[18];

  很简单对吗?那问题不就解决了?

  但是以上方式有两个缺陷:

  1. 谁告诉你这个用户只有18个朋友呢?可能他有更多朋友呢

  2. 你说:“那好,我就创建一个数组:

  int ageFriends[10000];

  足够储存1万个朋友的年龄。但是问题是:可能我们使用到的只是这个大数组的很小一部分,岂不是浪费内存嘛。

  最恰当的方式,是询问用户他有多少朋友,然后创建对应大小的数组。

  而这样,我们的数组大小就只有在运行时才能知道了。

  Voila,这就是动态分配的优势了:

  1. 可以在运行时才确定申请的内存空间大小

  2. 不多不少刚刚好,要多少就申请多少,不怕不够或过多

  所以借着动态分配,我们就可以询问用户他到底有多少朋友。如果他说有20个,那我们就申请20个int型的空间;如果他说有50个,那就申请50个。经济又环保。

  我们之前说过,C语言中禁止用变量名来作为数组大小,例如不能这样:

  int ageFriends[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,会内存泄露!

  动态分配使得我们可以创建动态数组,就是它的大小在运行时才能确定

  第二部分第九课预告:

  今天的课就到这里,一起加油吧。

  下一次我们学习: 实战“悬挂小人”游戏

  程序员联盟社区

  目前有一个微信群和一个QQ群(微信群120人以上,QQ群290人以上),凡是对编程感兴趣的朋友都可以加,大家可以交流,学习,互动,讨论写的程序的源代码,编程问答等。

  手机上微信里的二维码图片如何“扫描”呢?

  小窍门:

  在微信里长按图片,选择“识别图中二维码”,就可以了

  微信群(程序员联盟),加群请私信我(微信群人数超过100之后,不能通过扫描二维码加入了,只能私信我,谢谢)

  QQ群(程序员联盟),群号是 413981577

  QQ群共享里有很多编程书籍PDF和其他资料。扫描下面二维码加QQ群:


  我们还建立了一个公共的百度云盘,2TB容量,已有很多优秀编程资源,大家也可以上传。链接加群之后会发送。

  《程序员联盟》的微社区,方便大家提问和互动。可以关注一下。

  微社区地址和二维码如下:

  http://m.wsq.qq.com/264152148


  谢谢!


  程序员联盟 微信公众号*您若觉得本文不错,请点击画面右上角《···》按钮“分享到朋友圈”或“发送给朋友”

  *新朋友请关注「程序员联盟」微信搜公众号 ProgrammerLeague

  小编微信号: frogoscar

  小编QQ号: 379641629

  小编邮箱: enmingx@gmail.com

  程序员联盟QQ群:413981577

  程序员联盟微信群:先加我微信

  有朋友反映看手机端的文章太累,其实是可以用浏览器网页来看的

  方法1. 点击画面右上角的《···》按钮,然后选择“复制链接”,再把链接黏贴到你的浏览器里面或用邮件发送给自己,就可以在电脑的浏览器里打开了


  方法2. 头条网www.toutiao.com,搜索我的自媒体“程序员联盟”,内有所有文章,也可以直接进这个链接:http://www.toutiao.com/m3750422747/

  方法3. 我的51CTO博客和CSDN博客链接(所有文章都在上面)

  http://4526621.blog.51cto.com/

  http://blog.csdn.net/frogoscar

  新朋友如何查看所有文章:

  点击“查看公众号”,再点击“查看历史消息”



  “程序员联盟”公众号专为程序员,App设计师,各位喜爱编程和热爱分享的小伙伴们推送各样编程相关知识,优秀软件推荐,业界动态等。搜索 ProgrammerLeague 加关注~

  点击下方“阅读原文”查看 Dennis Ritchie编著的《C程序设计语言》第二版中文版PDF 百度云盘下载 (可以在手机上点开文件直接看)

  ↓↓↓

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多