HeadFirstC笔记_6 数据结构与动态存储:牵线搭桥

保存可变数量的数据
由于数组长度是固定的,为了保存可变数量的数据,需要一个比数组更灵活的东西,即链表。

链表就是一连串的数据
链表是一种抽象数据结构。链表是通用的,可以用来保存很多不同类型的数据,所以被称之为抽象数据结构。
链表保存了一条数据和一个指向另一条数据的链接。

在链表中插入数据
如果想在A和B之间插入C,只要将A中原本指向B的链接改成指向C,然后把C中的链接指向B,就可以插入一个新值了。他的优点就是
插入数据非常快。
相对而言,如果想在数组中插入
一个值,就不得不将插入点后所有数据后移一个位置,效率较低。

如何在C语言中创建链表?
那就是 创建递归结构
链表中的每个结构都需要与下一个结构相连。如果一个结
构包含一个链向同种结构的链接,那么这个结构就被称为 
递归结构。
递归结构含有指向同种结构的指针。如果你有一张航班时间表,上面列出了将要游览的岛屿,就可以用递归结构表示 island ,
下面具体讨论递归结构是怎么工作的:
       
        
        
  1. typedef struct island { // 你必须为这个结构命名。
  2. char *name;
  3. char *opens;
  4. char *closes;
  5. struct island *next; // 你在结构中保存了一个指针,指向下一座岛。
  6. } island;
如何在当前结构中保存链向下一个结构的链接呢?用指针。
只要在结构中保存指针, island 数据就含有下一个我们将游览的 island 的地址。只要我们的代码能访问一个 island ,就能够跳到下一个island 。
注意:递归结构要有名字。
当用typedef命令定义结构时可以跳过为结构起名字这步,但在递归结构中,需要包含一个相同类型的指针, C语言的语法不允许用typedef
别名来声明它,因此必须为结构起一个名字。这就是为什么这里的结构叫struct island。

用C语言创建岛屿……
一旦定义了 island 数据类型,就可以像这样创建第一批island :
       
        
        
  1. island amity = { "Amity", "09:00", "17:00", NULL};
  2. island craggy = { "Craggy", "09:00", "17:00", NULL};
  3. island isla_nublar = { "Isla Nublar", "09:00", "17:00", NULL};
  4. island shutter = { "Shutter", "09:00", "17:00", NULL};
刚开始我们把每个 island 中的 next 字段都设为了 NULL 。在C语言中, NULL 的值实际上为0, NULL专门用来把某个指针设为0。

……把它们链接在一起,构成飞行之旅
一旦你创建好了岛,就可以把它们连接在一起:
        
         
         
  1. amity.next = &craggy;
  2. craggy.next = &isla_nublar;
  3. isla_nublar.next = &shutter;
这样就创建了一次完整的跳岛游

在链表中插入值
通过修改指针的值,就可以插入 island ,就像之前做的那样:
         
          
          
  1. island skull = { "Skull", "09:00", "17:00", NULL};
  2. isla_nublar.next = &skull;
  3. skull.next = &shutter;
短短两行代码,就在链表插入了新值。但如果用数组,为了移动数组元素,你要多写很多代码。

代码示例:
         
          
          
  1. #include <stdio.h>
  2. typedef struct island { // 你必须为这个结构命名。
  3. char *name;
  4. char *opens;
  5. char *closes;
  6. struct island *next; // 你在结构中保存了一个指针,指向下一座岛。
  7. } island;
  8. void display(island *start) {
  9. island *i = start;
  10. for (; i!=NULL ; i = i->next ) { //需要一直循环下去,直到当前island没有next值,在每次循环的最后,跳到下一座岛
  11. printf("Name: %s\n open: %s-%s\n", i->name, i->opens , i->closes );
  12. }
  13. }
  14. int main() {
  15. island amity = { "Amity", "09:00", "17:00", NULL};
  16. island craggy = { "Craggy", "09:00", "17:00", NULL};
  17. island isla_nublar = { "Isla Nublar", "09:00", "17:00", NULL};
  18. island shutter = { "Shutter", "09:00", "17:00", NULL};
  19. amity.next = &craggy;
  20. craggy.next = &isla_nublar;
  21. isla_nublar.next = &shutter;
  22. island skull = { "Skull", "09:00", "17:00", NULL};
  23. isla_nublar.next = &skull;
  24. skull.next = &shutter;
  25. display(&amity);
  26. }
运行结果:

问: 其他语言,比如Java,有内置链表,C语言有内置数据结构吗?
答: C语言没有内置数据结构,你必须自己创建它们。

问: 如果我想快速地插入数据,就需要链表,但如果我想直接访问元素,就应该用数组。是这样吗?
答: 完全正确。 数据结构没有好与坏之分,只有适合或不适合于它的应用场合之分。

问: 你给出的这个结构含有一个指向其他结构的指针。我能把指针换成一个递归定义的结构吗?
答: NO, C语言需要知道结构在存储器中占的具体大小,如果在结构中递归地复制它自己,那么两条数据就会不一样大。

用堆进行动态存储
目前你用过的大部分存储器都在栈上。栈是存储器用来保
存局部变量的区域。数据保存在局部变量中,一旦离开函
数,变量就会消失。
问题是程序在运行时很难在栈上分配大量空间,所以你需
要堆。堆是程序中用来保存长期使用数据的地方。堆上的
数据不会自动清除,
因此堆是保存数据结构的绝佳场所,
比如我们的链表。可以把在堆上保存数据想象成在储物柜
中寄存物品。

首先,用malloc()获取空间
 想象程序在运行时突然发现有大量数据要保存,程序想申请一个大容量储物柜来保存数据,可以用一个叫 malloc() 的函数来申请。
你告诉malloc() 需要多少存储器,它就会要求操作系统在堆中分配这点空间,然后 malloc() 会返回一个指针,指向堆上新分配的空间。
指针就好比储物柜的钥匙,可以用它来访问存储器,并跟踪这个分配出去的储物柜。

有用有还
使用栈的时候,你无需操心归还存储器,因为
每当你离开函数,系统就会将栈中局部变量清除。
但堆不一样,
一旦申请了堆上的空间,
该空间就再也不能再
分配出去,直到告诉C标准库你不用完了。堆空间有限,
如果不断申请而不释放,
很快就会发生存储器泄漏。

调用free()释放存储器
malloc() 函数分配空间并给出一个指向这块空间的指针。你需要用这个指针访问数据,用完以后,需要用 free() 函数释放存储器,就
像把储物柜的钥匙还给服务员,好让别人能接着用。

malloc()具体使用方式
申请存储器的函数叫 malloc() ,是memory allocation(存储器分配)的意思。 malloc() 接收一个参数:所需要的字节数。
通常你不知道确切的字节数,因此 malloc() 经常与 sizeof 运算符一起使用,sizeof 告知某种数据类型在系统中占了多少字节。这
种数据类型可以是结构,也可以是 int 或 double 这样的基本数据类型。
malloc() 函数为你分配一块存储器,然后返回一个通用指针,即 void* 类型的指针,指针中保存了存储器块的起始地址。
         
          
          
  1. #include <stdlib.h> // 为了使用malloc()和free()函数,需要包含stdlib.h头文件
  2. ...
  3. island *p = malloc(sizeof(island)); // 表示“给我足够的空间来保存island结构”

free()使用方式
free() 需要接收 malloc() 创建的存储器的地址--指针。只要告诉C标准库存储器块从哪里开始,它就能查阅记录,知道要释放多少存储器。
假如想要释放上面那行代码分配的存储器,可以这样做:
          
           
           
  1. free(p); // 表示“释放你分配的存储器,从堆地址 p 开始”
记住:如果在一个地方用malloc()分配了存储器,就应该在后面用free()释放它。

代码示例:
           
            
            
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. typedef struct island { // 你必须为这个结构命名。
  4. char *name;
  5. char *opens;
  6. char *closes;
  7. struct island *next; // 你在结构中保存了一个指针,指向下一座岛。
  8. } island;
  9. void display(island *start) {
  10. island *i = start;
  11. for (; i!=NULL ; i = i->next ) {
  12. printf("Name: %s\n open: %s-%s\n", i->name, i->opens , i->closes );
  13. }
  14. }
  15. island* create(char *name) {
  16. island *i = malloc(sizeof(island));
  17. i->name = name;
  18. i->opens = "09:00";
  19. i->closes = "17:00";
  20. i->next = NULL;
  21. return i;
  22. }
  23. int main() {
  24. char name[80];
  25. fgets(name, 80, stdin);
  26. island *p_island0 = create(name);
  27. fgets(name, 80, stdin);
  28. island *p_island1 = create(name);
  29. p_island0->next = p_island1;
  30. display(p_island0);
  31. }
运行结果:

 你发现第一个island的name也被改成了第二个的name!为什么?
当代码记录岛名时,并没有接收一份完整的 name 字符串,而只是记录了 name 字符串在存储器中的地址。程序要求用户输入每座岛的名字,
但两次它都使用本地的字符数组name 来保存岛名,也就是说两座岛共享同一个 name 字符串,一旦局部变量 name 更新为第二座岛的名字,
第一座岛的名字也就自然改变了。
怎么解决这个问题?

用strdup()函数来复制字符串
在C语言中,经常需要复制字符串。你可以调用 malloc() 函数在堆上创建一些空间,然后手动把字符串的所有字符复制到堆上。
但是C标准库已经封装好了现成的,在string.h头文件中有一个叫 strdup() 的函数。假设你有一个指向你想复制的字符串常量的指针:
             
              
              
  1. char *s = "MONA LISA";
strdup() 函数可以把字符串复制到堆上,并返回一个指针:
             
              
              
  1. char *copy = strdup(s);
strdup()函数内部会计算出字符串的长度,然后调用malloc()函数在上分配相应的空间,所以千万记得要用 free() 函数释放空间!

用strdup()修复代码
              
               
               
  1. ...
  2. island* create(char *name) {
  3. island *i = malloc(sizeof(island));
  4. i->name = strdup(name); // 这里改用strdup将字符串复制到堆上
  5. i->opens = "09:00";
  6. i->closes = "17:00";
  7. i->next = NULL;
  8. return i;
  9. }
  10. int main() {
  11. char name[80];
  12. fgets(name, 80, stdin);
  13. island *p_island0 = create(name);
  14. fgets(name, 80, stdin);
  15. island *p_island1 = create(name);
  16. p_island0->next = p_island1;
  17. display(p_island0);
  18. // 记得释放
  19. free(p_island1->name);
  20. free(p_island1);
  21.    free(p_island0->name);
  22. free(p_island0);
  23. }
运行结果:
 
问: 如果island结构用数组保存岛名,而不是字符指针,还需要用strdup()吗?
答: 不需要,如果用数组,每个island结构都会保存自己的副本,不需要你自己创建。

问: 那为什么要在数据结构中使用字符指针而不是字符数组呢?
答: 字符指针不会限制字符串的大小。如果用字符数组,需要提前决定字符串的长度。

写个程序从标准输入读取一列岛名,然后将它们连成链表
              
               
               
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. typedef struct island {
  5. char *name;
  6. char *opens;
  7. char *close;
  8. struct island *next; // 表示链表的下一个island
  9. } island;
  10. // 在堆内存中开辟一个island变量
  11. island* create(char* name) {
  12. island* i = malloc(sizeof(island));
  13. i->name = strdup(name);
  14. i->opens = "09:00";
  15. i->close = "17:00";
  16. i->next = NULL;
  17. return i;
  18. }
  19. //释放岛链
  20. void release(island *start) {
  21. island *i = start;
  22. island *next = NULL;
  23. for (; i != NULL; i = next) {
  24. next = i->next;
  25. free(i->name);
  26. free(i);
  27. }
  28. }
  29. // 打印岛链
  30. void printIslandLink(island* start) {
  31. island* i;
  32. for( i = start; i !=NULL; i = i->next) {
  33. printf("Name: %s: open: %s-%s\n", i->name, i->opens , i->close);
  34. }
  35. }
  36. void displayIslandLinkFromKeybord() {
  37. island *start = NULL;
  38. island *i = NULL;
  39. island *next = NULL;
  40. char name[80];
  41. for (; fgets(name, 80, stdin) != NULL; i = next) {
  42. next = create(name);
  43. if (start == NULL)
  44. start = next;
  45. if (i != NULL)
  46. i->next = next;
  47. }
  48. printIslandLink(start);
  49. release(start);
  50. }
  51. int main() {
  52. displayIslandLinkFromKeybord();
  53. }
执行命令:islandlink < msg1.txt
 
运行结果:

 有了动态分配存储器,就能在运行时创建需要的存储器。使用malloc()与free(),可以访问动态堆存储器。

问: 我需要在程序结束前释放
所有数据吗?
答: 不必,操作系统会在程序结
束时清除所有存储器。不过,你还是
应该显式释放你创建的每样东西,这
是一种好的习惯。

数据结构很有用,但要小心使用!
当用C语言创建这些数据结构时需要非常小心,如果没有记 录好保存的数据,就很可能把不用的数据留在堆上。时间一
久,它们就开始消耗机器上的存储器,程序也可能因为存储 器错误而崩溃。所以,你必须学会如何追查代码中的存储器
泄漏,并学会如何修复它们……

使用valgrind查找内存泄漏!
                  
                   
                   
  1. /*
  2. * find_suspect.c
  3. * Created on: 2016年11月23日
  4. * Author: Administrator
  5. */
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. typedef struct node {
  10. char *question;
  11. struct node *no;
  12. struct node *yes;
  13. } node;
  14. int yes_no(char *question) {
  15. char answer[3];
  16. printf("%s? (y/n): ", question);
  17. fgets(answer, 3, stdin);
  18. return answer[0] == 'y';
  19. }
  20. node* create(char *question) {
  21. node *n = malloc(sizeof(node));
  22. n->question = strdup(question);
  23. n->no = NULL;
  24. n->yes = NULL;
  25. return n;
  26. }
  27. void release(node *n) {
  28. if (n) {
  29. if (n->no)
  30. release(n->no);
  31. if (n->yes)
  32. release(n->yes);
  33. if (n->question)
  34. free(n->question);
  35. free(n);
  36. }
  37. }
  38. int main() {
  39. char question[80];
  40. char suspect[20];
  41. node *start_node = create("Does suspect have a mustache");
  42. start_node->no = create("Loretta Barnsworth");
  43. start_node->yes = create("Vinny the Spoon");
  44. node *current;
  45. do {
  46. current = start_node;
  47. while (1) {
  48. if (yes_no(current->question)) {
  49. if (current->yes) {
  50. current = current->yes;
  51. } else {
  52. printf("SUSPECT IDENTIFIED\n");
  53. break;
  54. }
  55. } else if (current->no) {
  56. current = current->no;
  57. } else {
  58. /* Make the yes-node the new suspect name */
  59. printf("Who's the suspect?");
  60. fgets(suspect, 20, stdin);
  61. node *yes_node = create(suspect);
  62. current->yes = yes_node;
  63. /* Make the no-node a copy of this question */
  64. node *no_node = create(current->question);
  65. current->no = no_node;
  66. /* Then replace this question with the new question */
  67. printf(
  68. "Give me a question that is TRUE for %s but not for %s? ",
  69. suspect, current->question);
  70. fgets(question, 80, stdin);
  71. // 这里current -> question已经指向了堆上的某个question,因此在分配新的question之前要先释放它
  72. free(current->question); // 这句很容易漏掉!
  73. current->question = strdup(question);
  74. break;
  75. }
  76. }
  77. } while (yes_no("Run again"));
  78. release(start_node);
  79. return 0;
  80. }
运行结果:

  有人在实验室中连续用了这个系统几个小时,他注意到,尽管程 序看起来运行正确,但却多用了一倍存储器。
所以我们把你请来。源代码深处藏着一段代码,它在堆上分配存 储器,但从不释放。

valgrind 的工具原理
valgrind 通过伪造 malloc() 可以监控分配在堆上的数据。当 程序想分配堆存储器时, valgrind将 会拦截你对 malloc() 和
free() 的调用,然后运行自己的 malloc() 和 free() 。 valgrind 的 malloc() 会记录调用它的是哪段代码和分配了哪段存储器。程
序结束时, valgrind 会汇报堆上有哪些数据,并告诉你这些数据 是由哪段代码创建的。
(目前只有linux板的:http://valgrind.org/downloads/current.html#current

准备好代码:添加调试信息
在使用 valgrind 运行代码前,你不需要做任何修改,甚至不需
要重新编译代码。但为了发挥 valgrind 的最大威力,应当在可
执行文件中包含调试信息。调试信息是编译时打包到可执行文件
中的附加数据,比如某段代码在源文件中的行号。只要有调试信
息, valgrind 就能提供更多有助于发现存储器泄漏的信息。
为了此, 你需要加上 -g 开关,并重新编
译源代码:
                  
                   
                   
  1. 执行命令 gcc -g ./find_suspect.c -o ./find_suspect
  2. // -g开关告诉编译器要记录要编译代码的行号。

使用valgrind,收集泄漏证据
在命令行 启动 valgrind ,加上 --leak-check=full 选项,并把你想运 行的程序传给 valgrind :
                   
                    
                    
  1. linux下执行命令:valgrind --leak-check=full ./find_suspect

find_suspect 程序退出时,堆上什么都没有。再次运行程序, 教程序辨认一个叫Hayden Fantucci的新嫌疑犯,看看会发 生什么。

这次valgrind发现了存储器泄漏
程序结束时,似乎有19字节的信息留在了堆上, valgrind 告诉你以下几件事:
1.分配了19字节的存储器,但没有释放。
2.看起来我们分配了11次存储器,但只释放了10次。
3.你能从这几行中看出什么吗?
4.为什么是19字节?你能推测出什么吗? 

推敲证据
1. 定位
运行了两次代码,第一次没有任何问题。只有当输入一个新嫌
疑犯的名字时,存储器才会泄漏。这条线索十分重要,因为它
说明泄漏不可能发生在第一次运行的代码中。
2. valgrind提供的线索
当用 valgrind 运行代码并添加一名嫌疑犯时,程序分配了11
次存储器,但只释放了10次,这说明什么?
valgrind 告诉你程序结束时有19个字节的数据留在了堆上。
看一下源代码,哪条数据像是有19字节?
最后,下面这段 valgrind 的输出告诉你什么?


结论:
1. 有几条数据留在了堆上?
    有一条数据。
2. 哪条数据留在了堆上?
    字符串“Loretta Barnsworth”,18个字符外加一个字符串终结符。
3. 哪一行或哪几行代码导致了泄漏?
create()函数本身不会导致泄漏,因为第一遍运行的时候没有发生,
所以一定是下面这行strdup()出了问题:
                   
                    
                    
  1. current->question = strdup(question);
4. 如何修复泄漏?
current -> question已经指向了堆上的某个question,因此在分配新的question之前要先释放它:
                   
                    
                    
  1. ...
  2. free(current->question);
  3. current->question = strdup(question);
  4. ...
最终审判
既然修改了代码,再用 valgrind 运行一次:

泄漏已修复!

问: valgrind说泄漏的存储器是在第46行创建的,但我们却修改了另一行代码,为什么?
答: 虽然数据“Loretta...”是由 第46行代码放到堆上的,但泄漏却发 生在变量(current->question) 重新赋值的那一刻,因为当时变量指
向的“Loretta...”还没有释放。创建 数据不会发生泄漏,只有当程序失去 了所有对数据的引用才会导致泄漏。

问: valgrind这个名字的由来是什么?
答: valgrind是英灵殿 ① 入口的名 字,而valgrind(程序)为你打开了 一扇通向计算机堆的大门。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53640696