零基础学C语言——作用域
这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。
在数学中,变量x一般都有其值域,也就是x都可以有哪些值,或者说在什么数值范围内x是有效的。同理,C语言中,每个变量或者函数都有其作用域,也就是在什么范围内这个变量或函数有效(可被编译器找到)。
对于变量,根据作用域的不同被分为两大类——局部变量和全局变量。
局部变量
所谓的局部变量是指在函数体中定义的变量,这些变量的作用域就是函数内部,即函数返回后,这些变量将被销毁。这类变量我们一般称作自动变量。
自动变量的定义形式我们在变量一文中介绍过:
数据类型 变量名;
或
数据类型 变量名 = 初始值;
还有一种自动变量——函数参数,函数参数也是仅在函数内有效的。
此外,C语言还提供了一种作用域在函数内,但函数返回后不会销毁的变量——静态变量。
静态变量是指在函数体中,用如下形式定义的变量:
static 数据类型 变量名;
或
static 数据类型 变量名 = 初始值;
静态变量与自动变量的区别是:每次函数调用时,函数内的自动变量所占用的内存都会被重新分配,其值也会按照语句重新赋值。而静态变量不同,如果使用定义同时初始化形式定义静态变量,则静态变量仅会被初始化一次。并且每次函数调用时静态变量的值都是沿用上一次调用改变后的值,而不会被重置。
举个例子:
#include <stdio.h>
void foo(void)
{
static int a = 1;
++a;
printf("%d\n", a);
}
int main(void)
{
foo();
foo();
return 0;
}
这个例子的输出是:
2
3
原因是,第一次进入foo时,静态变量a被初始化为1,然后自加变为2,所以printf打印的结果是2(第一行)。随后函数返回main,之后再次调用foo函数。这次foo中a不再被重新赋值为1,而是依旧保持上次被修改后的结果,即2。然后再自加变为3,最后打印其值3(第二行)。
下面看一个初学者常犯的错误:
int *return_array(void)
{
int array[2] = {1, 2};
return array;
}
int main(void)
{
int *ret = return_array();
return 0;
}
这是一个典型的错误用法。我们说过,函数内的自动变量的作用域仅限于函数内,当函数返回时,自动变量会被销毁。因此,main中ret指向的数组,其内容将是不可预知的内容,访问其内容可能会导致程序崩溃。
想要正常返回一个数组,利用静态变量是一种解决方案。除此之外,还有一种动态分配内存的方案,将在后续内存管理相关的文章中说明。
全局变量
全局变量是指变量定义于任何函数体之外,且作用域是整个程序范围内的变量。这类变量又分为两类——普通全局变量和静态全局变量。
普通全局变量定义形式如下:
数据类型 变量名;
或
数据类型 变量名 = 初始值;
而静态全局变量的定义形式为:
static 数据类型 变量名;
或
static 数据类型 变量名 = 初始值;
与局部变量中自动变量和静态变量的定义一样,但是含义完全不同的。
静态全局变量与普通全局变量的不同在于作用域范围。普通全局变量是作用于整个程序范围内的,而静态全局变量的作用域则是当前的源文件。
举例:
/*a.c*/
int a = 10;
static int b = 100;
int main(void)
{
foo();
}
/*b.c*/
#include <stdio.h>
void foo(void)
{
printf("a:%d\n", a);
printf("b:%d\n", b);//这句是无法通过编译的
}
如果按照上面代码创建两个源文件并编译,是无法生成可执行程序的,且会报错。
原因有二:
1.正如我注释所写,b是a.c中的静态全局变量,作用域仅在a.c,因此b.c无法访问。
2.全局变量a虽然不是静态全局变量,但在b.c中缺少声明,因此无法使用。
下面我们重写b.c,修正这两个问题:
/* b.c */
#include <stdio.h>
extern int a;
void foo(void)
{
printf("a:%d\n", a);
}
这里,去掉了b的打印,同时增加了全局变量a的声明。
注意,这个全局变量的声明使用了extern关键字。extern关键字用于告知编译器,用其声明的变量或者函数是全局作用域的,需要从可执行程序涉及到的全部源文件中寻找。
同名覆盖
不知是否有读者想过,如果全局变量和局部变量同名,那么函数内的变量的值会是什么呢?
看一个例子:
#include <stdio.h>
int a = 1;
int main(void)
{
int a = 2;
printf(“%d\n”, a);
return 0;
}
这段代码的执行结果是:2。
这里存在同名覆盖原则:同名的局部变量会覆盖同名的全局变量。
函数作用域
函数的作用域与全局变量的作用域相同,毕竟在C语言中函数内部无法再定义函数。
提供给外部其他源文件使用的函数的声明形式如下:
extern 返回值类型 函数名(参数列表...);
给本文件内使用的函数的声明形式如下:
static 返回值类型 函数名(参数列表...);
并且,函数对编译器的可见性也取决于函数声明的位置,例如:
int main(void)
{
foo();
return 0;
}
static void foo(void);
void foo(void)
{
}
如此声明foo函数,编译器依旧会报错,因为foo函数的定义对main不可见。如果将foo函数的static声明提前到main函数前(即本例中放在第一行),则可正常编译。
块作用域
前面关于语句的文章中并未提及一种特殊的语句——块语句。
这种语句是以大括号({})扩起的,其大括号内部可以是单条语句,也可以是多条语句。
{
...//一条或多条语句
}
这并非是说C语言中看到大括号就是块语句。函数的大括号并不属于块语句,其余则皆为块语句,包括if-else、for、while等结构中涉及大括号的部分。
我们先来看一个例子:
#include <stdio.h>
int main(void)
{
int a = 1;
{
int a = 2;
printf("In block a:%d\n", a);
}
printf("Out of block a:%d\n", a);
return 0;
}
运行结果为:
In block a:2
Out of block a:1
这个例子告诉我们两个事实:
- 块内同名变量将覆盖外层同名变量
- 块内定义的变量在块外无法访问,即块结构内的自动变量会随块结构完结而销毁。
头文件与源文件
之前的文章中,所涉及到的例子都是放在.c文件中的。然而C语言中并不只有.c文件。
在C语言中有两种文件——头文件和源文件。
源文件就是我们所说的文件名后缀以.c结尾的文件,其中的代码一般都是各类函数的定义。
头文件是文件名以.h结尾的文件。这类文件中一般记录一些结构定义、函数声明、变量声明、类型定义等。关于结构体和类型定义我们后续文章会有专门说明。
什么情况下需要头文件呢?我们来看个例子:
/*b.c*/
extern void foo(void);
void bar(void)
{
foo();
}
/*c.c*/
void foo(void)
{
}
可以看到,a.c和b.c都用到了c.c中的foo函数,因此它们都需要声明foo函数。如果这时我对foo函数的返回类型做了修改,那么我需要到声明foo的其他源文件中修改其声明。如果我有20个源文件中都用到了foo呢?那么此时的修改会不会引起混乱呢?因此,头文件就派上了用场。
我们看下修改后的代码:
/*a.c*/
#include "c.h"
extern void bar(void);
int main(void)
{
foo();
bar();
return 0;
}
/*b.c*/
#include "c.h"
void bar(void)
{
foo();
}
/*c.c*/
void foo(void)
{
}
/*c.h*/
extern void foo(void);
如此,我们将foo的extern声明仅写一份放在c.h头文件中。
然后利用预编译的include指令,将c.h的内容引入到需要使用foo函数的a.c和b.c文件中。关于include的更详细介绍,将在预编译宏文章中给出。目前只需要知道,在编译时,include会将其后紧跟的文件名所指定的文件中的内容原封不动展开(可看作复制)进使用该include指令的源文件中,且展开点就是include指令所在位置。
喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。
感谢阅读!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!