传送门:C语言-第三章:变量进阶与函数
目录
第一节:static 关键字
在讲 static 关键字之前,必须先讲一讲什么是作用域。
1-1.作用域
假设这里有一个局部变量 a:
int main()
{
if(1)
{
int a = 10; // 局部变量a只能从这里开始使用
} // 到这里结束
return 0;
}
我们想使用它只能在上图所示的区域使用,这个区域就叫做 a 的作用域。
有人会说:这不是 a 的生命周期吗?
实际上就是这样,在一般情况下,对于定义变量的文件来说,它的 生命周期 等于 作用域
1-1-1.全局变量的作用域
前面也说到了 作用域 是对于定义它的文件来说等于 生命周期,实际上 全局变量 的 作用域 还包括了其他同目录文件,想使用其他同目录文件定义的 全局变量,需要 extern 关键字,以下是一个示范:
首先在同目录下创建两个文件:
然后两个文件分别写入下列代码:
// test.c int global = 100;
// main.c #include <stdio.h> extern global; // 用 extern 声明这个变量来自外部 int main() { printf("另一个文件中的全局变量 global的值是:%d", global); return 0; }
程序的执行结果:
说明 main.c 找到了来自 test.c 的全局变量 global
上述 extern 的作用就是帮助文件 main.c 找到了 global 的定义
1-1-2.函数的作用域
函数的作用域和全局变量的作用域相同,都可以在其他同目录文件中使用,但是它不需要任何关键字的辅助,以下是一个示范:
首先在同目录下创建两个文件:
然后两个文件分别写入下列代码:
// test.c int Add(int x, int y) { int sum = x + y; return sum; }
// main.c #include <stdio.h> int main() { int sum = Add(4,5); printf("sum = %d\n",sum); return 0; }
程序的执行结果:
说明 main.c 找到了来自 test.c 的函数 Add 的具体实现
不需要 extern 辅助是因为程序在执行之前先生成了一个函数表,里面放入了函数的实现位置,main.c 查阅这个表就找到了 Add 函数的位置
1-1-3.局部变量的作用域
局部变量的作用域就等于它的生命周期。
1-2.static 的作用
1-2-1.限制全局变量和函数的访问权限
我们上面讲了 作用域,它和 static 关键字的有什么联系呢?
如果说 全局变量 和 函数 是活泼好动的孩子,喜欢跑到其他文件里,那么 static 关键字就是管教它们的母亲,可以让它们无法被其他文件访问。
下列是全局变量的测试用例:
还是上述的两个文件,但是在 global 定义时加上 static 关键字:
// test.c static int global = 100;
// main.c #include <stdio.h> extern global; // 用 extern 声明这个变量来自外部 int main() { printf("另一个文件中的全局变量 global的值是:%d", global); return 0; }
执行会报错:
这次 extern 就没找到 global 的定义了
函数也是同理:
// test.c static int Add(int x, int y) { int sum = x + y; return x + y; }
// main.c #include <stdio.h> int main() { int sum = Add(4,5); printf("sum = %d\n",sum); return 0; }
执行也会报错:
这次 main.c 就没在函数表中找到 Add 函数了。
1-2-2.延长局部变量的生命周期
用 static 修饰的局部变量可以获得和全局变量一样的生命周期,即从定义开始,到程序结束时结束。注意:static 只增加了 生命周期,但是 作用域没有改变,不能使用的地方还是不能使用。
不仅如此,被 static 修饰的局部变量也只能被定义和初始化一次。
那么上述两个效果有什么作用呢,请跟我看以下案例:
请自定义一个函数,第一次调用这个函数时返回1,以后每次调用这个函数时返回值都比前一次大1。
我们很容易想到用 全局变量 来实现:
#include <stdio.h>
int g = 1;
int fun()
{
return g++;
}
int main()
{
int ret_1 = fun();
int ret_2 = fun();
int ret_3 = fun();
int ret_4 = fun();
printf("%d %d %d %d\n", ret_1, ret_2, ret_3, ret_4);
return 0;
}
但是这样做的话如果,其他文件要调用这个函数时还需要 extern g,使用 static 关键字就可以解决这个问题:
#include <stdio.h>
int fun()
{
static int i = 1;
return i++;
}
int main()
{
int ret_1 = fun();
int ret_2 = fun();
int ret_3 = fun();
int ret_4 = fun();
printf("%d %d %d %d\n", ret_1, ret_2, ret_3, ret_4);
return 0;
}
当第一次调用 fun 函数时,变量 i 会被定义并初始化,每次以后再次调用 fun 函数时,static 检测到变量 i 已经存在了,就会自动跳过“static int i = 1;” 这条语句。
第二节:函数递归与链式访问
2-1.函数的链式访问
函数的链式访问就是把一个函数的返回值作为另一个函数的参数使用。先看看我们之前的使用方法:
#include <stdio.h>
int Add(int x,int y)
{
int sum = x + y;
return sum;
}
int main()
{
int result = Add(4, 5);
printf("result = %d\n", result);
return 0;
}
值传递过程为:Add -> result ->printf
实际上可以化简为:Add -> printf
#include <stdio.h>
int Add(int x,int y)
{
int sum = x + y;
return sum;
}
int main()
{
printf("result = %d\n", Add(4, 5));
return 0;
}
上图中 Add 将先于 printf执行,然后返回一个值,这个返回值直接作为 printf 的参数使用。而且Add函数也可以用其他的函数的返回值作为参数,如此套娃下去,函数总是会从最里面先开始执行,就像链条一样一环扣一环,所以叫做 链式访问。
2-2.函数递归
函数递归就是一个函数自己调用自己,它的核心思想是大事化小,即把大型问题转化成一层一层原理相似的小问题。
2-2-1.理解递归
递归的思考方式与平时的不同,请看以下案例:
你面前有一个罐子,里面装了很多的糖果,你需要拿出50颗糖果放到自己的口袋里,但是你每次只能拿1颗糖果
在你拿到需要50颗糖果之前,你先得拿到49颗糖果;
在你拿到需要49颗糖果之前,你先得拿到48颗糖果;
在你拿到需要48颗糖果之前,你先得拿到47颗糖果;
..........
在你拿到需要2颗糖果之前,你先得拿到1颗糖果,你现在就拿了;
------------------------我是分割线--------------------------
现在50颗糖果的问题暂时被转化成了1颗糖果,也是你现在就可以完成的,于是:
你现在拿了1颗糖果,可以拿第2颗糖果了;
你现在拿了2颗糖果,可以拿第3颗糖果了;
你现在拿了3颗糖果,可以拿第4颗糖果了;
..........
你现在拿了49颗糖果,可以拿第50颗糖果了;
你现在拿了50颗糖果,问题完成了!
在分割线以上就是“递归”中“递”的过程,即把一个大问题分割成一个一个的子问题,并且每一个子问题都是类似的
在分割线以下就是“递归”中“归”的过程,即从最初的子问题开始一步一步向上完成,并且每一步的动作也是类似的
ps:上面的“递”、“归”是我个人方便理解而分开谈的
现在我们来用递归的思想写一份代码完成上面的案例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Recursion(int n) // 递归函数
{
// 分割成子问题,“递”的过程
if (n > 1)
{
printf("在你拿到需要 %2d 颗糖果之前,你先得拿到 %2d 颗糖果\n",n,n-1);
Recursion(n - 1); // 自己调用自己,只要没有开始“归”,所有函数都不会执行下面的代码
}
// 子问题无法再分了,“归”开始
printf("你现在拿了 %2d 颗糖果,可以拿第 %2d 颗糖果了\n",n,n+1);
}
int main()
{
int n;
printf("请输入需要拿的糖果数量:");
scanf("%d", &n); // n为需要拿的糖果数量
Recursion(n);
return 0;
}
这就是一个非常简单的递归,可以看到,“递”与“归”之间有一个临界,达到这个临界就是从“递”到“归”的开始,所以每次递归必须逼近这个临界否则递归会永无止境
内存中有一块有限的空间叫做栈区,而函数在调用时会占用栈区,如果递归真的无限的进行下去,栈区会被递归函数占满,造成 栈溢出问题:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Recursion(int n) // 递归函数
{
// 故意让问题被无限分割
if (1)
{
printf("在你拿到需要 %2d 颗糖果之前,你先得拿到 %2d 颗糖果\n",n,n-1);
Recursion(n - 1); // 自己调用自己,只要没有开始“归”,所有函数都不会执行下面的代码
}
}
int main()
{
int n;
printf("请输入需要拿的糖果数量:");
scanf("%d", &n); // n为需要拿的糖果数量
Recursion(n);
return 0;
}
Stack overflow 就是栈溢出的英文。
2-2-2.青蛙跳台阶问题
青蛙跳台阶是递归的经典问题,它的题目如下:
现在有n个台阶,一只青蛙一次可以跳 1 阶可以跳 2 阶,青蛙从最下面跳到最上面的方式一共有多少种?
还是用 糖果问题 思考方式,但是有一些不一样。
糖果问题每一次的子问题分割只有一种情况:n -> n-1
而这个问题每一次的子问题分割有两种情况:n -> n-1 或 n -> n-2
糖果问题的递归函数每次只调用一次自己,而青蛙跳台阶问题只需要函数调用两次自己即可:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
long Recursion(int n) // 递归函数
{
if (n > 2)
{
return Recursion(n - 1) + Recursion(n - 2);
}
else if(n == 1) // 1阶只有一种途径
{
return 1;
}
else if (n == 2)// 2阶有两种途径
{
return 2;
}
}
int main()
{
int n;
printf("请输入台阶数量:");
scanf("%d", &n); // n台阶数量
printf("%d 阶台阶有 %ld 种途径\n", n, Recursion(n));
return 0;
}
n个台阶就会调用2的n次方次函数,这是一种指数爆炸,在n等于40时我的电脑得出结果就已经有明显的延迟了。
递归对人是一个比较抽象的概念,因为它的思考深度太深了,所以需要多多感悟,感兴趣还可以去网上搜索“汉诺塔递归”和“斐波那契数列”,它们都是递归的经典题目。
下期预告:
下一章将进入第四章:操作符的学习,主要内容如下:
算数操作符、位移操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、三目操作符和逗号表达式
传送门C语言-第四章:操作符