局部变量和全局变量
局部变量
在我们学习函数之前,我们所理解的变量,只不过是在内存中开辟一个存储数据的位置,并取了个我们好懂的名字而已。因为我们之前写的程序只有一个主函数,因此我们觉得,定义了一个变量,就应该可以随时调用。但是学习了函数之后,我们发现,不同函数之间的变量是不能够相互调用的,这又是为什么呢?
比如:
//Example 01
#include <stdio.h>
int main(void)
{
int i = 100;
printf("Before i = %d\n", i);
for (int i = 0; i <= 10; i++)//再定义一个局部变量i
{
printf("i = %d\n", i);
}
printf("After i = %d\n", i);
return 0;
}
结果如下:
//Consequence 01
Before i = 100
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
After i = 100
我们可以看到,我们在for
函数里和main
函数里都有一个i
变量,但是我们在for
函数里面定义的i
却对外层函数不构成影响。
一般来说,变量名应该是不能够重复的。但是,由于我们定义的位置不一样(在不同的函数中),所以变量名重复又变得合法起来。这就是局部变量的特性:只能在自己的领域里面发挥作用。
像我们刚刚定义的for
一样,C语言允许随处定义变量。也就是说,变量在需要用到的时候再定义。这样也符合我们的思维方式。因为当程序很庞大的时候,没有人愿意翻到顶上去看一个变量的注释。
全局变量
既然有局部变量,那么全局变量也照样不可少。看下面的例子:
//Example 02
#include <stdio.h>
void f1(void);
void f2(void);
void f3(void);
int a = 0;//定义一个全局变量
void f1(void)
{
a++;
}
void f2(void)
{
a++;
}
void f3(void)
{
a++;
}
int main(void)
{
f1();
f2();
f3();
printf("a = %d\n", ++a);
return 0;
}
运行结果如下:
//Consequence 02
a = 4
我们发现,全局变量在每一个函数里面的更改都“有效”,也就是说,全局变量是贯穿整个程序始终的。
有的小伙伴可能会好奇,如果全局变量和局部变量重名了,会发生什么呢?
那我们就来试试:
//Example 03
#include <stdio.h>
void f(void);
int a, b = 100;
void f(void)
{
int b;
a = 50; b = 101;
printf("func, a = %d, b = %d\n", a, b);
}
int main(void)
{
printf("Main, a = %d, b = %d\n", a, b);
f();
printf("Main, a = %d, b = %d\n", a, b);
}
运行结果如下:
//Consequence 03
Main, a = 0, b = 100
func, a = 50, b = 101
Main, a = 50, b = 100
我们发现,名字叫a
的变量只有全局变量,那么被赋值之后就等于在函数f()
中赋的值。但是b
就不一样了。在函数f()
中,也有一个变量叫做b
,那么此时编译器的做法是,在函数f()
里先暂时屏蔽全局变量b
,使用自己的局部变量。等走出了函数f()
,局部变量被释放。
而我们还发现一个问题,我们可以直接输出没有被初始化的a
!这也是全局变量的一个特点,不同于局部变量,全局变量在没有手动初始化的时候,会被系统自动初始化为0
而不是像局部变量那样的很小的一个很奇怪的数。
另外,如果没有需要,尽量不要大量使用全局变量。因为全局变量的内存将会伴随着这个程序,直到程序执行完毕。如果大量使用全局变量的话,可能会造成内存占用过多等问题。这在嵌入式开发这种领域,内存空间寸土寸金,不当使用全局变量会导致资源的浪费。其次,全局变量会造成程序可读性变差。最后,全局变量会使得程序牵连性变强,牵一发而动全身的情况可能会再次出现。
因此全局变量虽然是个好东西,但也要谨慎使用。
作用域和链接属性
在上一节,我们简单地了解了不同的变量,它能够有不同的作用范围,那么这个范围,就是我们所说的作用域。C语言的编译器一共能够确认4种不同的作用域:代码块作用域
文件作用域
原型作用域
函数作用域
代码块作用域
//Example 04
#include <stdio.h>
int main(void)
{
int i = 1;
{
int i = 2;
printf("i = %d\n", i);
}
{
printf("i = %d\n", i);
int i = 3;
printf("i = %d\n", i);
}
printf("i = %d\n", i);
return 0;
}
运行结果如下:
//Consequence 04
i = 2
i = 1
i = 3
i = 1
我们通常管一个大括号里面的语句叫做代码块,那么看这个程序,一个代码块里面的变量只能作用于所在的代码块里,超出了的就无效了。若存在代码块嵌套,那么优先内层代码块的变量。这就是代码块作用域。
当然,有一点需要说说,就是函数的形参也是代码块作用域,只能作用于函数定义代码块里面,即便它没有写在代码块里面。
文件作用域
在任何代码块之外定义的变量,都具有文件作用域。它们的作用域是从变量声明开始,一直到文件尾结束。另外,函数名也是文件作用域,因为函数名本身也在代码块之外。
原型作用域
原型作用域只适合那些在函数原型中声明的参数名。我们知道,在声明一个函数的时候,形参的名字是可以不用写的,只需要把类型写好就行了。但是,其实不妨可以试试写上名字,即便这个名字和正式定义的时候形参的名字不一样,也是没问题的(当然这样做毫无意义),这其实就是原型作用域在起作用。
函数作用域
函数作用域只适用于goto
语句的标签,作用是将goto
语句的标签限定在一个函数的内部,避免出现重名的标签。
链接属性
简单来说,编译器将源代码转换成机器码的时候需要有两个步骤:编译和链接。
所谓编译,就是将我们写的源代码转换为机器码,而链接,就是将相关的库文件添加进来。比如我们在写程序的时候加入的头文件,在最终生成的时候就是要链接进来的。
我们知道,大型程序是由很多源文件构成的,那么在不同的文件中的同名标识符,编译器是入场处理的呢?
在C语言中,链接属性一共有以下3种:
- external(外部的):多个文件中声明的同名标识符表示同一个实体
- internal(内部的):单个文件中声明的同名标识符表示同一个实体
- none(无):声明的同名标识符被当作独立不同的实体。比如,函数的局部变量。
默认情况下,具备文件作用域的标识符拥有external
属性,也就是说,这种标识符允许跨文件访问。使用static
可以使原先拥有external
属性的标识符变为internal
属性。但是static
只能修改具有文件作用域的标识符,并且是不可逆修改。
生存期和存储类型
生存期
上一节我们用空间的角度去解释了不同的变量,但其实,还可以从时间的角度来分析。
C语言的变量通常有两种生存期,静态存储期(static storage duration)和自动存储期(automatic storage duration)。
具有文件作用域的变量具有静态存储期(如全局变量),函数名也有静态存储期。静态存储期的变量在程序执行的期间内将一直占据存储空间。
具有代码块作用域的变量通常具有自动存储期(如局部变量和形参),具有自动存储期的变量将在代码块执行完毕的时候释放内存。
存储类型
变量的存储类型实际上是指存储变量值的内存类型。C语言提供了5种存储类型:auto
register
static
extern
typedef
1. 自动变量
在代码块中声明的变量默认就是自动变量(auto)。
//Example 05
#include <stdio.h>
int main(void)
{
auto int a, b, c;
return 0;
}
但是由于是默认存储类型,所以auto
一般不写也完全没问题。函数中的形参、局部变量以及复合语句中定义的局部变量都具有自动变量。自动变量拥有代码块作用域、自动存储期和空链接属性。
2. 寄存器变量
如果你学过汇编语言,或者对计算机的原理比较了解的话,一定没少听说寄存器这个词。寄存及就集成在CPU内部,因此它和CPU之间的交流几乎可以说没有延迟。
如果你申请了寄存器变量,那么就有可能被存储到寄存器里面去。当然,编译器也有自己的优化方案,它会在程序运行的时候权衡哪些变量更应该被放到寄存器的位置,因此,你的申请只是做一个参考而已。那么那些没有被放入寄存器的变量就会成为自动变量,所以,寄存器变量和自动变量在很多地方是一样的,也拥有代码块作用域、自动存储期和空链接属性。
但是,如果是寄存器变量的话,那么理论上就没法通过**取地址运算符&**来获取地址了,因为我们知道这个是针对内存的。
//Example 06
#include <stdio.h>
int main(void)
{
register int i = 100;
printf("addr of i is %p\n", &i);
return 0;
}
但是VS可能对这种代码有优化,将变量转换成自动变量了:
//Consequence 06 of Visual Studio 2019
addr of i is 00CFFE90
3. 静态局部变量
static
用于描述具有文件作用域的变量或者函数时,表示将其链接属性从external
修改成internal
,它的作用范围就变成了仅当前文件可访问。如果static
用于描述局部变量,那可就不太一样了。
默认情况下,局部变量是自动变量,具有自动存储期。如果使用了static
来声明,那么就可以将局部变量指定为静态局部变量(static)。这使得局部变量具有静态存储期,所以它的生存期和全局变量一样。但是作用域依旧是局部变量,在别的函数中是无法访问这个局部变量的。
4. extern
extern
关键字告诉编译器这个变量或函数在别的地方已经定义过了,先在别的地方找找,不要急着报错。
通常情况下,这个关键词可以不写,但是为了程序更加完善,更加易读。在多人协作的时候,可以避免很多重名的问题。
5. typedef
typedef
与其他四个存储类型的语义不同,typedef
与内存存储无关,用于为数据类型定义一个新名字。
递归
虽然递归隶属于算法的范畴,但是几乎所有的程序语言教程,都会讲到这个知识点。因为,递归是一个非常好的编程思路,有时候一个很难解决的问题,使用递归就可以巧妙地搞定。
什么是递归
递归说白了,就是函数在执行的时候,调用自身的行为。其实递归在生活中有很多实例,比如:
-
汉诺塔游戏
这个游戏要求将中间的柱子的圆盘全部移动到另外一个柱子上,要求每次只能移动一个圆盘,并且较大的圆盘始终在下方。
-
谢尔宾斯基三角形
三角形里边填充三角形,只要空间够大,它可以撑满整个宇宙。
甚至还诞生了一门数学分支:分形几何。专门研究这种递归现象。
说了这么多,那么递归在程序里面该如何实现呢?
//Example 07
#include <stdio.h>
void r(void);
void r(void)
{
printf("Hi!");
r();
}
int main(void)
{
r();
return 0;
}
运行结果如下:
//Consequence 08
...
Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Hi!Segmentation fault
可以看到,满屏幕的Hi!这就是初学者经常会犯的错误,程序无休止地执行下去,直到消耗掉所有的内存。这就像我们讲过的“从前有座山,山里有座庙……”这个故事一样,没有中止的条件,讲多久都讲不完。
修改下代码:
//Example 07 V2
#include <stdio.h>
void r(void);
void r(void)
{
static int count = 5;//设置计数变量
printf("Hi!\n");
if (--count)//设置跳出条件
{
r();
}
}
int main(void)
{
r();
return 0;
}
运行结果如下:
//Consequence 07 V2
Hi!
Hi!
Hi!
Hi!
Hi!
这样,递归这头小猛兽,就这样被我们控制住了。
递归求斐波那契数列
斐波那契数列每一项等于前两项之和,正好符合递归的思路:
#include <stdio.h>
int fibo(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return fibo(n - 1) + fibo(n - 2);
}
}
int main()
{
int n;
printf("请输入斐波那契数列长度:");
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
printf("%d", fibo(i));
if (i < n)
{
printf(", ");
}
}
return 0;
}
程序执行如下:
请输入斐波那契数列长度:10
1, 1, 2, 3, 5, 8, 13, 21, 34, 55
递归求汉诺塔
其实,不论有多少层汉诺塔,都可以使用递归一层一层往下解包。
//Example 08
#include <stdio.h>
void hanoi(int, char, char, char);
void hanoi(int n, char x, char y, char z)
{
if (n == 1)
{
printf("%c --> %c\n", x, z);//剩下底部的圆盘
}
else
{
hanoi(n - 1, x, z, y);//将n-1个圆盘从x移动到y
printf("x --> z\n");
hanoi(n - 1, y, x, z);//将n-1个圆盘从y移动到z
}
}
int main(void)
{
int n;
printf("请输入汉诺塔的层数:");
scanf("%d", &n);
hanoi(n, 'X', 'Y', 'Z');
return 0;
}
运行结果如下:
//Consequence 08
请输入汉诺塔的层数:4
X --> Y
x --> z
Y --> Z
x --> z
Z --> X
x --> z
X --> Y
x --> z
Y --> Z
x --> z
Z --> X
x --> z
X --> Y
x --> z
Y --> Z
分治法
所谓分治法,就是大事化小的思维。递归实际上就是一种分治。层层递归,然后从最简单的问题开始解决。
说到这里,就不得不提到一种排序算法,就是十分经典的——快速排序。
作为20世纪十大算法之一,快速排序的基本思想是:通过一项将待排序数据分割成独立的两部分,其中一部分元素均比另一部分小,然后分别对这两部分继续排序,重复步骤直到完成。
代码如下:
//Example 09
#include <stdio.h>
void qs(int, int, int);
void qs(int array[], int left, int right)
{
int i = left, j = right;
int temp;
int pivot;
//基准点设置为中间元素,当然别的也可以
pivot = array[(left + right) / 2];
while (i <= j)
{
//找到左边大于等于基准点的元素
while (array[i] < pivot)
{
++i;
}
//找到右边小于等于基准点的元素
while (array[j] > pivot)
{
--j;
}
//如果左边下标小于右边,则交换元素
if (i <= j)
{
temp = array[i];
array[i] = array[j];
array[j] = temp;
++i;
--j;
}
}
//递归遍历左子
if (left < j)
{
qs(array, left, j);
}
//递归遍历右子
if (i < right)
{
qs(array, i, right);
}
}
int main(void)
{
int array[] = {135, 156, 120, 102, 130, 62, 410, 158, 173, 113, 124, 184, 131, 214};
int length;
length = sizeof(array) / sizeof(array[0]);
qs(array, 0, length - 1);
printf("排序后结果为:");
for (int i = 0; i < length; i++)
{
printf("%d", array[i]);
if (i < length - 1)
{
printf(", ");
}
}
putchar('\n');
return 0;
}
执行结果如下:
//Consequence 09
排序后结果为:62, 102, 113, 120, 124, 130, 131, 135, 156, 158, 173, 184, 214, 410
学了这么多,也该好好休息下了!下期再见!