搜索

C语言教程

编程基础

通俗地理解什么是编程语言 C 语言究竟是一门怎样的语言? C 语言和 C++ 到底有什么关系? 学了 C 语言到底能做什么,能从事什么工作? 二进制、八进制和十六进制 不同进制之间的转换 数据在内存中的存储 载入内存,让程序运行起来 ASCII 编码,将英文存储到计算机 GB2312 编码和 GBK 编码,将中文存储到计算机 Unicode 字符集,将全世界的文字存储到计算机 程序员的薪水和发展方向大全 不要这样学习 C 语言,这是一个坑! 明白了这点才能学好编程,否则参加什么培训班都没用

C 语言初探

第一个 C 语言程序 选择正确的输入法,严格区分中英文 什么是源文件? 什么是编译和链接? 主流 C 语言编译器有哪些? 什么是 IDE(集成开发环境)? 什么是工程/项目? 哪款 C 语言编译器(IDE)适合初学者? 如何在手机上编写 C 语言代码? C 语言的三套标准:C89、C99 和 C11 C 语言为什么有那么多编译器? 程序安装是怎么回事? 制作安装包,让用户安装程序 C 语言程序的错误和警告 分析第一个 C 语言程序 C 语言代码中的空白符 彩色版的 C 语言,让文字更漂亮 一个真正带界面的 C 语言程序

变量和数据类型

大话 C 语言变量和数据类型 在屏幕上输出各种类型的数据 C 语言中的整数(short,int,long) 二进制数、八进制数和十六进制数 C 语言中的正负数及其输出 整数在内存中是如何存储的,为什么它堪称天才般的设计 整数的取值范围以及数值溢出 C 语言中的小数(float,double) 小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计(长篇神文) 在 C 语言中使用英文字符 在 C 语言中使用中文字符 C 语言到底使用什么编码?谁说 C 语言使用 ASCII 码,真是荒谬! C 语言转义字符 C 语言中的几个重要概念 C 语言加减乘除运算 C 语言自增(++) 和自减(--) 变量的定义位置以及初始值 运算符的优先级和结合性 C 语言数据类型转换

C 语言输入输出

数据输出大汇总以及轻量进阶 在屏幕的任意位置输出字符,开发小游戏的第一步 使用 scanf 读取从键盘输入的数据 从键盘输入字符和字符串 进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症 结合缓冲区谈 scanf 函数,那些奇怪的行为其实都有章可循 清空(刷新)缓冲区,从根本上消除那些奇怪的行为 scanf 的高级用法,原来 scanf 还有这么多新技能 C 语言模拟密码输入(显示星号 ) 非阻塞式键盘监听,用户不输入数据程序也能继续执行

循环结构和选择结构

else 语句 C 语言关系运算符 C 语言逻辑运算符 case 语句 C 语言条件运算符 C 语言 while 循环 C 语言 for 循环 C 语言跳出循环 C 语言循环嵌套 对选择结构和循环结构的总结 谈编程思维的培养,初学者如何实现自我突破(非常重要) 写一个内存泄露的例子,让计算机内存爆满

C 语言数组

什么是数组 C 语言二维数组 判断数组中是否包含某个元素 C 语言字符数组和字符串 字符串的输入和输出 C 语言字符串处理函数 C 语言数组是静态的,不能插入或删除元素 C 语言数组的越界和溢出 C 语言变长数组:使用变量指明数组的长度 对数组元素进行排序 对 C 语言数组的总结

C 语言函数

什么是函数? C 语言函数定义 C 语言函数的形参和实参 C 语言函数的返回值 C 语言函数的调用(从中发现程序运行的秘密) 函数声明以及函数原型 全局变量和局部变量 C 语言变量的作用域 C 语言块级变量 C 语言递归函数(带实例演示) 中间递归函数(比较复杂的一种递归) 多层递归函数(最烧脑的一种递归) 递归函数的致命缺陷:巨大的时间开销和内存开销(附带优化方案) 忽略语法细节,从整体上理解函数

预处理命令

什么是预处理命令? #include 的用法 C 语言宏定义 C 语言带参数的宏定义 带参宏定义和函数的区别 宏参数的字符串化和宏参数的连接 C 语言中几个预定义宏 C 语言条件编译 #error 命令,阻止程序编译 C 语言预处理命令总结

指针

1 分钟彻底理解指针的概念 指针变量的定义和使用 C 语言指针变量的运算 数组指针(指向数组的指针) 字符串指针(指向字符串的指针) C 语言数组灵活多变的访问形式 指针变量作为函数参数 C 语言指针作为函数返回值 二级指针(指向指针的指针) 空指针 NULL 以及 void 指针 数组和指针绝不等价,数组是另外一种类型 数组到底在什么时候会转换为指针 指针数组(数组每个元素都是指针) 一道题目玩转指针数组和二级指针 二维数组指针(指向二维数组的指针) 函数指针(指向函数的指针) 只需一招,彻底攻克 C 语言指针,再复杂的指针都不怕 main() 函数的高级用法:接收用户输入的数据 对 C 语言指针的总结

结构体

C 语言结构体 C 语言结构体数组 C 语言结构体指针 C 语言枚举类型 C 语言共用体 大端小端以及判别方式 C 语言位域 C 语言位运算 使用位运算对数据或文件内容进行加密

重要知识点补充

typedef 的用法 const 的用法 C 语言随机数

文件操作

C 语言中的文件是什么? C 语言打开文件 文本文件和二进制文件到底有什么区别? 以字符形式读写文件 以字符串的形式读写文件 以数据块的形式读写文件 格式化读写文件 随机读写文件 C 语言实现文件复制功能 FILE 结构体以及缓冲区深入探讨 获取文件大小(长度) 插入、删除、更改文件内容

C 语言调试

调试的概念以及调试器的选择 设置断点,开始调试 查看和修改变量的值 单步调试(逐语句调试和逐过程调试) 即时窗口的使用 查看、修改运行时的内存 有条件断点的设置 assert 断言函数 调试信息的输出 VS 调试的总结以及技巧

递归函数的致命缺陷:巨大的时间开销和内存开销(附带优化方案)

递归函数是一种强有力的技巧,用来解决某些问题很顺手,比如前面提到的求阶乘、求菲波那契数;但是和其他技巧一样,递归函数也是有缺陷的,而且这种缺陷是致命性的。

递归函数的空间开销

在程序占用的整个内存中,有一块内存区域叫做栈(Stack),它是专门用来给函数分配内存的,每次调用函数,都会将相关数据压入栈中,包括局部变量、局部数组、形参、寄存器、冗余数据等。

栈是针对线程来说的,每个线程都拥有一个栈,如果一个程序包含了多个线程,那么它就拥有多个栈。目前我们编写的程序都是单线程的,所以不必考虑多线程的情况。

对每个线程来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。

栈内存的大小和编译器有关,编译器会为栈内存指定一个最大值,在 VC/VS 下,默认是 1M,在 C-Free 下,默认是 2M,在 Linux GCC 下,默认是 8M。当然,我们也可以通过参数来修改栈内存的大小。

发生函数调用时会将相关数据压入栈中,函数调用结束会释放这一部分内存,对于一般的函数来说,这不会有任何问题,但是对于递归函数,这会导致严重的问题!

递归函数内部嵌套了对自身的调用,除非等到最内层的函数调用结束,否则外层的所有函数都不会调用结束。通俗地讲,外层函数被卡住了,它要等待所有的内层函数调用完成后,它自己才能调用完成。

每一层的递归调用都会在栈上分配一块内存,有多少层递归调用就分配多少块相似的内存,所有内存加起来的总和是相当恐怖的,很容易超过栈内存的大小限制,这个时候就会导致程序崩溃。

例如,一个递归函数需要递归 10000 次,每次需要 1KB 的内存,那么最终就需要 10MB 的内存。

为了演示由于栈溢出而导致程序崩溃的情形,下面我们用递归的方式来求 1+2+3+ ...... + (n-1) + n 的值:
#include <stdio.h>

long sum(int n) {
    //为了增加每次函数调用的内存,额外增加了一个无用的数组,它占用 1KB 的内存
    int arr[250];

    if (n <= 1) {
        return n;
    } else {
        return  n + sum(n-1);
    }
}

int main() {
    printf("从1加到1000的值为 %ld\n", sum(1000));
    return 0;
}
在 Visul Studio 下运行该程序,稍等片刻后就看到程序崩溃了,如下图所示:


这是因为,每次递归调用都需要超过 1KB 的内存(仅仅数组就占用了 1KB 内存),而要得到最终的结果需要 1000 次递归调用,这样一来,所有内存的总和就超过了 1MB。

上面我们说过,Visual Studio 默认的栈内存只有 1MB,超过这个界限程序就无法运行了,只能让它崩溃。使用其它的编译器也许程序不会崩溃,读者可以亲自尝试。

递归函数的时间开销

每次调用函数都会在栈上分配内存,函数调用结束后再释放这一部分内存,内存的分配和释放都是需要时间的。

每次调用函数还会多次修改寄存器的值,函数调用结束后还需要找到上层函数的位置再继续执行,这也是需要时间的。

所有的这些时间加在一起是非常恐怖的。

下面我们以「求斐波那契数」为例来演示双层递归的时间开销。
#include <stdio.h>
#include <time.h>

// 递归计算斐波那契数
long fib(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}

int main() {
    int a;
    clock_t time_start, time_end;

    printf("Input a number: ");
    scanf("%d", &a);
    time_start = clock();
    printf("Fib(%d) = %ld\n", a, fib(a));
    time_end = clock();
    printf("run time: %lfs\n", (double)(time_end - time_start)/ CLOCKS_PER_SEC );

    return 0;
}
运行结果:
Input a number: 42↲
Fib(42) = 267914296
run time: 13.137000s

可以看到,为了求 42 的斐波那契数程序竟然运行了 13 秒,简直让人发指。

使用迭代来替换递归函数

既然递归函数的解决方案存在巨大的内存开销和时间开销,那么我们如何进行优化呢?优化个毛,这是函数实现原理层面的缺陷,无法优化。

其实,大部分能用递归解决的问题也能用迭代来解决。所谓迭代,就是循环。

许多问题是以递归的形式进行解释的,这只是因为它比非递归形式更为清晰。但是,这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性可能稍差一些。

与递归函数相比,迭代不但没有额外的内存开销,也没有额外的时间开销。

下面我们分别用递归和迭代的方案来求斐波那契数,看看它们究竟孰快孰慢。
#include <stdio.h>
#include <time.h>

//递归计算斐波那契数
long fib_recursion(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib_recursion(n - 1) + fib_recursion(n - 2);
    }
}

//迭代计算斐波那契数
long fib_iteration(int n){
    long result;
    long previous_result;
    long next_older_result;
    result = previous_result = 1;
    while (n > 2) {
        n -= 1;
        next_older_result = previous_result;
        previous_result = result;
        result = previous_result + next_older_result;
    }
    return result;
}

int main() {
    int a;
    clock_t time_start_recursion, time_end_recursion;
    clock_t time_start_iteration, time_end_iteration;

    printf("Input a number: ");
    scanf("%d", &a);

    //递归的时间
    time_start_recursion = clock();
    printf("Fib_recursion(%d) = %ld\n", a, fib_recursion(a));
    time_end_recursion = clock();
    printf("run time with recursion: %lfs\n", (double)(time_end_recursion - time_start_recursion)/ CLOCKS_PER_SEC );

    //迭代的时间
    time_start_iteration = clock();
    printf("Fib_iteration(%d) = %ld\n", a, fib_iteration(a));
    time_end_iteration = clock();
    printf("run time with iteration: %lfs\n", (double)(time_end_iteration - time_start_iteration) / CLOCKS_PER_SEC);

    return 0;
}
运行结果:
Input a number: 42↲
Fib_recursion(42) = 267914296
run time with recursion: 13.173000s
Fib_iteration(42) = 267914296
run time with iteration: 0.000000s

你看,递归用了 13 秒,迭代几乎瞬间完成(接近 0 秒),迭代比递归快成千上万倍,这个差异是巨大的。

总结

函数调用本来就存在内存开销和时间开销,递归一次这种开销就增加一倍,如果有成千上万次的递归,那么所有开销的总和就是巨大的。这是递归的致命缺陷,无法优化。所以建议大家尽量少用递归,能用迭代就用迭代吧。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注