零基础学C语言——作用域

发布时间:2023年12月25日

这是一个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

这个例子告诉我们两个事实:

  1. 块内同名变量将覆盖外层同名变量
  2. 块内定义的变量在块外无法访问,即块结构内的自动变量会随块结构完结而销毁。

头文件与源文件

之前的文章中,所涉及到的例子都是放在.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指令所在位置。



喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。
感谢阅读!
文章来源:https://blog.csdn.net/weixin_40960130/article/details/135191974
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。