runaway(深度递归示例)代码内容:
/* Example of deep recursion */
#include <stdio.h>
#include <stdlib.h>
int recurse(int x) {
int a[1<<15]; /* 4 * 2^15 = 128 KiB */
printf("x = %d. a at %p\n", x, a);
a[0] = (1<<14)-1;
a[a[0]] = x-1;
if (a[a[0]] == 0)
return -1;
return recurse(a[a[0]]) - 1;
}
int main(int argc, char *argv[]) {
int x = 100;
if (argc > 1)
x = atoi(argv[1]);
int v = recurse(x);
printf("x = %d. recurse(x) = %d\n", x, v);
return 0;
}
下面我将分别解释:
/* Example of deep recursion */
/ * 符号和 * / 符号:里面内容为C语言的注释,不会被运行,英文意思为深度递归示例。
#include <stdio.h>
#include <stdlib.h>
#include<stdio.h>是编译预处理命令,即在程序编译之前要处理的内容,<>中以以“.h ”作为结尾的文件称为头文件,如stdio.h,stdlib.h,string.h均为头文件。
stdio 即 “standard input & output"(标准输入输出)在开头加上#include <stdio.h>即可实现c语言中的 输入函数printf、输出函数scanf等函数。
同理,在开头加上#include <stdlib.h>即可实现c语言中的exit、malloc、free等函数。
同样的,在开头加上#include <string.h>即可实现关于字符数组的函数,例如连接字符串函数strcat,复制字符串函数strcpy,比较字符串(区分大小写)函数strcmp等。
类似头文件还有math.h(包括各种常用的三角函数、双曲线函数、指数和对数函数等)等。
int recurse(int x) {
定义一个返回值类型为整型(int )的函数,函数名为recurse,括号里参数x的类型为整型,参数x为形参,主函数输入的实参必须与形参类型匹配,注意实参与形参有各自的存储空间,如不使用指针或return则形参值的改变不会影响实参。
补充:关于c语言基本数据类型,除了int,还有float,double,char,short,long,可参考c语言基本数据类型short、int、long、char、float、double
int a[1<<15]; /* 4 * 2^15 = 128 KiB */
定义一个整型数组变量,变量名为a。数组是由若干类型相同的相关数据项按顺序存储在一起形成的一组同类型有序数据的集合。注意数组下标从0开始,a[1<<15]也就是定义了1<<15长度的数组。<<是左移运算符,在程序运行时可用作乘法。在二进制的计算中,左移n位即乘2的n次方。1<<15即2的15次方,其值为32768,32768字节换算为32KB。其第1至第(1<<15)-1个元素分别存在a[0]到a[(1<<15)-1]中。由于该数组定义为整型,则它有1<<15个元素,每个元素均为整型。由于每个整型元素占4个字节(bytes),故该数组占4 * 2^15 = 128 KiB个空间,如注释所表明的。
数组还有许多扩展知识,可参考:关于数组
printf("x = %d. a at %p\n", x, a);
printf函数是一个标准库函数,它的函数原型在头文件stdio.h中。printf函数调用的一般形式为:printf(“格式控制字符串”, 输出表列)。printf是将一个格式化的字符串输出到屏幕。更多可参考:C语言格式输出函数printf()详解
这里printf函数打印的是“x = 变量x的值. a at 变量a的地址”并换行。
%p是打印地址的,以主机的格式显示指针,即变量的地址。会按十六进制输出数据(加上前缀0x)。
这里出现了一个转义字符,’\n’是一个转义字符,它是换行符,即实现回车换行的功能。其余类似转义字符还有’\t’,它的功能是横向跳到下一制表位置。
关于转义字符:C语言字符型数据(字符)
若运行这行程序,则为显示x和a所在的地址。
a[0] = (1<<14)-1;
a[a[0]] = x-1;
为a的数组做一定的赋值操作,通过阅读下面代码,这些操作可决定停止递归。在c语言中,一个调用自身(不管是直接地还是间接地)的函数被称为是递归的。
这里的操作让a数组的第一个元素赋值为 (1<<14)-1,1<<14即2的14次方,其值为16384,16384字节换算为16KB。同时a[ (1<<14)-1]赋值为x-1。
if (a[a[0]] == 0)
return -1;
if为条件语句。本程序中使用if形式,即
if (条件表达式) 可执行语句
其作用是:若表达式的值为真,则执行可执行语句,否则不执行执行可执行语句。
更多关于if语句可参考:C语言if语句
在此处表明当a[ (1<<14)-1]=0时返回-1,并结束该函数。阅读下面程序可以知道它的意思为退出递归。
return recurse(a[a[0]]) - 1;
}
如果表达式的值为假跳过上一条语句所执行的语句。这个返回表明这个函数是一个递归函数。它将a[ (1<<14)-1]的值作为新的x,即新的参数。这是一个令x不断-1的过程。
int main(int argc, char *argv[]) {
程序的主函数,有main()函数的程序才能运行,即函数必须被main()直接或间接调用才能发挥作用,主函数的返回值类型为int(整型)。其中,main函数可以带参数,这个参数可以认为是main函数的形式参数。C语言规定main函数的参数只能有两个,习惯上这两个参数写为argc和argv。可参考关于main函数参数和int main(int argc,char* argv[])详解
C语言还规定argc(第一个形参)必须是整型变量,argv(第二个形参)必须是指向字符串的指针数组。
argc是命令行总的参数个数,argv[]是argc个参数,其中第0个参数argv[0]指向程序运行的全路径名,以后的参数为命令行后面跟的用户输入的参数。
通过阅读后续代码,可以发现它利用了这些参数间关系。
int x = 100;
定义一个整型变量,变量名为x,并为其赋值为100。
if (argc > 1)
一个条件语句,如果参数个数大于1,即用户除./a.out外输入了另外的参数则执行,否则不执行下面语句。
x = atoi(argv[1]);
atoi函数用来将字符串转换成整数(int),它的头文件为#include <stdlib.h>。atoi函数会扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’\0’)才结束转换,并将结果返回。更多可参考:C语言atoi()函数
在这里则是将argv数组的第二个元素,即用户输入的第一个参数做为函数的参数,并将转换来的整型数赋给x,令x不再为100而为用户输入的第一个参数。
int v = recurse(x);
定义一个整型变量v,并为赋值为 recurse函数的调用结果。
printf("x = %d. recurse(x) = %d\n", x, v);
在屏幕上打印出x的值以及recurse(x)的最终值并换行。
return 0;
}
主函数返回0表程序结束。
以下为该代码在ubantu上运行结果:
不带参数
x = 100. a at 0xbfd3f8b0
x = 99. a at 0xbfd1f890
x = 98. a at 0xbfcff870
x = 97. a at 0xbfcdf850
x = 96. a at 0xbfcbf830
x = 95. a at 0xbfc9f810
x = 94. a at 0xbfc7f7f0
x = 93. a at 0xbfc5f7d0
x = 92. a at 0xbfc3f7b0
x = 91. a at 0xbfc1f790
x = 90. a at 0xbfbff770
x = 89. a at 0xbfbdf750
x = 88. a at 0xbfbbf730
x = 87. a at 0xbfb9f710
x = 86. a at 0xbfb7f6f0
x = 85. a at 0xbfb5f6d0
x = 84. a at 0xbfb3f6b0
x = 83. a at 0xbfb1f690
x = 82. a at 0xbfaff670
x = 81. a at 0xbfadf650
x = 80. a at 0xbfabf630
x = 79. a at 0xbfa9f610
x = 78. a at 0xbfa7f5f0
x = 77. a at 0xbfa5f5d0
x = 76. a at 0xbfa3f5b0
x = 75. a at 0xbfa1f590
x = 74. a at 0xbf9ff570
x = 73. a at 0xbf9df550
x = 72. a at 0xbf9bf530
x = 71. a at 0xbf99f510
x = 70. a at 0xbf97f4f0
x = 69. a at 0xbf95f4d0
x = 68. a at 0xbf93f4b0
x = 67. a at 0xbf91f490
x = 66. a at 0xbf8ff470
x = 65. a at 0xbf8df450
x = 64. a at 0xbf8bf430
x = 63. a at 0xbf89f410
x = 62. a at 0xbf87f3f0
x = 61. a at 0xbf85f3d0
x = 60. a at 0xbf83f3b0
x = 59. a at 0xbf81f390
x = 58. a at 0xbf7ff370
x = 57. a at 0xbf7df350
x = 56. a at 0xbf7bf330
x = 55. a at 0xbf79f310
x = 54. a at 0xbf77f2f0
x = 53. a at 0xbf75f2d0
x = 52. a at 0xbf73f2b0
x = 51. a at 0xbf71f290
x = 50. a at 0xbf6ff270
x = 49. a at 0xbf6df250
x = 48. a at 0xbf6bf230
x = 47. a at 0xbf69f210
x = 46. a at 0xbf67f1f0
x = 45. a at 0xbf65f1d0
x = 44. a at 0xbf63f1b0
x = 43. a at 0xbf61f190
x = 42. a at 0xbf5ff170
x = 41. a at 0xbf5df150
x = 40. a at 0xbf5bf130
x = 39. a at 0xbf59f110
x = 38. a at 0xbf57f0f0
段错误 (核心已转储)
输入参数如 20
x = 20. a at 0xbfb0c910
x = 19. a at 0xbfaec8f0
x = 18. a at 0xbfacc8d0
x = 17. a at 0xbfaac8b0
x = 16. a at 0xbfa8c890
x = 15. a at 0xbfa6c870
x = 14. a at 0xbfa4c850
x = 13. a at 0xbfa2c830
x = 12. a at 0xbfa0c810
x = 11. a at 0xbf9ec7f0
x = 10. a at 0xbf9cc7d0
x = 9. a at 0xbf9ac7b0
x = 8. a at 0xbf98c790
x = 7. a at 0xbf96c770
x = 6. a at 0xbf94c750
x = 5. a at 0xbf92c730
x = 4. a at 0xbf90c710
x = 3. a at 0xbf8ec6f0
x = 2. a at 0xbf8cc6d0
x = 1. a at 0xbf8ac6b0
x = 20. recurse(x) = -20
综上,该程序展示了递归的缺点。该程序输出了x不断变化的值以及对应的存储地址,最后输出x的值以及recurse(x)的最终值。当递归函数内容占用了大量空间,出现了段错误 (核心已转储)的现象。这里之所以会报错是因为堆栈溢出,可以看到当用户不输入参数时,即x从100开始递归时,递减到38便开始报错。而当x为20时,运行递归还不至于溢出,因此能正常运行,x会减到1,并且recurse(x)的值为输入函数的负数。
因此递归的缺点除了时间和空间的消耗比较大外,还有栈溢出的问题,即调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。所以在编程的时候,要考虑到递归的缺陷。
博客参考:《深入理解计算机系统》第三章