一、回顾链接,代码块、本文件作用域

我们在第一章详细的讲过链接,这里因为课程的需求,我们需要再回顾下

一个真正的C工程一定是多文件的(多.c、多.h),这些文件被编译为.o后,
需要被链接为一个完整的可执行文件,链接的工作由链接器来完成

链接时主要做两件事:
(1)符号解析

  • 1)对全局符号进行符号统一
  • 2)将符号的引用 与 符号的定义关联起来

(2)地址重定位


回顾 代码块作用域

形参和局部变量的作用域就是代码块作用域,对于形参和局部变量来说,不允许出现同名符号,
所以不存在需要统一同名符号的情况

而且代码块作用域只局限在代码块内,与其它文件没有任何关系,所以与链接无关


回顾 本文件作用域

在单个.c中,全局变量和函数的作用域就是本文件作用域,由于允许对全局变量和函数进行声明,
所以在单个.c中存在同名符号的问题,编译时需要进行同名符号的统一,统一规则就是强弱符号的统一规则

由于本文件作用域只与当前文件有关,与其它文件无关,因此也与链接无关。


二、 链接域(跨文件作用域) 与 extern关键字

为什么需要跨文件作用域?

对于全局变量和函数来说,有时不仅仅只希望在本文件可以被使用,还希望在其它的文件中也能被使用,
此时作用域就必须跨越到其它文件中,这就所谓的涉及跨文件作用域

跨文件作用域涉及到多个文件,由于多文件最后要被链接到一起,
与链接有关,所以我们也将跨文件作用域称为链接域


如何实现跨文件的作用域?只要满足两个条件即可:

  • 1)将定义标记为extern
    extern表示定义的符号是一个全局符号,由于是全局符号,因此对于其它文件来说这个符号是可见的。
  • 2)在其它文件中进行声明,声明也需要标记为extern
    extern表示声明的符号也是一个全局符号,对于其它文件也是可见的。

正式因为extern将符号标记为了全局可见,在链接阶段才能对全局符号进行“符号统一”

例子

a.c                             b.c

extern int a;                   int a = 100; //全局符号,extern可以省略
extern int fun();
int main(void)                  int fun()
{                               {
    printf("helloworld\n");

}                               }

extern可以省略,省略后默认就是extern的,与auto有点像


对于几乎所有的编译器来说,都认可在定义时将extern省略,
但是对于声明来说,有些编译器允许省略extern,但是有些就不允许,
我们目前使用的gcc就允许声明时省略extern

不过为了保证不出错,经常的做法是,定义时省略extern,但是声明时必须保留extern

由于全局符号的定义和声明是同名的,所以在链接阶段需要按照强弱符号的统一规则,
对全局符号进行统一,声明作为弱符号最后会消失,虽然消失了,但是它却将“作用域跨”拓展到了其它文件中

从这里可以看出,想要实现跨文件作用域的话,必须使用声明这个弱符号来拓展作用域

不过有一点需要注意,我们说全局变量和全局符号时,这两个全局的意思不相同。

  • 全局变量的“全局”:指的是文件
  • 全局符号的“全局”:指的是整个C工程项目

三、全局符号的重名问题 与 static 关键字

1. 全局符号的重名问题

extern 所修饰的符号是所有文件都可见的全局符号

如果在不同文件中存在同名强符号的话,全局符号符号统一时就会报错,
但是大家要知道一旦C工程变得复杂之后,在不同的文件中,误定义同名的函数和全局变量的情况是无法避免的

为了避免同名全局强符号的错误,我们应该尽量使用static关键字来避免这个问题

2. static 修饰函数和全局变量时的作用

将符号标记为本地符号

1)什么是本地符号?

所谓本地符号,就是符号只在本文件内可见,其它文件不可见,链接阶段进行全局符号统一时,
所有static修饰的本地符号在全局是不可见的,所以不参与链接阶段的符号统一,因此就算同名了也不会报错

2)本地符号的作用域

static将符号变为本地符号,说白了就是关闭符号的链接域,或者说关闭符号的跨文件作用域,
符号此时只剩下“本文件作用域”

为了最大化的防止重名问题,建议凡事只在本文件起作用,而其它文件根本用不到的函数和全局变量,
统统使用static修饰,让符号在全局不可见,防止全局强符号的同名冲突。

C中使用static来解决全局强符号的命名冲突,其实是非黑即白的解决方式,为了能够更加精细化的解决
命名冲突问题,从C扩展得到C++时,C++引入了命名空间这一概念,当然这个就是属于C++的内容


四、总结一下 extern 和 static 关键字

1. static

1)修饰局部变量

与存储类有关,表示局部变量的存储类为静态数据段。

2)修饰全局变量

与存储类无关,因为全局变量的存储类本来就是固定的静态数据段。
static修饰全局变量,表示符号为本地符号,关闭链接域(跨文件作用域),让其在全局不可见

3)修饰函数

与修饰全局变量是一样的,将符号变为本地符号,关闭链接域,让其全局不可见


2. extern

1)修饰函数、全局变量的定义和声明时

表示符号是全局符号,将链接域(跨文件作用域)被打开,让其全局可见

2)将函数体外的全局变量和函数,声明到函数内部
a.c

int main(void)
{
    extern int a;
    extern int fun();

    a = a+1;
    fun();
}

int a;
int fun()
{

}

此时fun函数也可以在其它的.c中,此时涉及到的就跨文件作用域


五、声明的作用

1. 变量的声明

拓宽变量的作用域

如果没有通过声明来拓宽变量作用域的话,在第二阶段编译时,编译器就会提示你所使用的某个符号找不到,
有了声明后,其实就是告诉编译器,你所使用的这符号是由定义,不要报错


2. 函数的声明

(1)拓宽函数的作用域
(2)进行形参、返回值的类型检查

1)如果函数的定义位置在调用位置之前时

此时函数定义本身就是一个函数声明,无需额外的声明

编译阶段进行函数的类型检查时,直接通过函数定义来进行类型检查

2)如果函数定义的位置不在调用位置之前

如果不进行声明的话,编译时是不会进行类型检查的,所以我们必须进行声明,
声明后再进行编译时,就会通过声明来进行函数的类型检查

3)类型检查有什么用

类型检查其实很有用,进行类型检查时如果发现类型有问题的话,编译时打印提示信息,
这样可以帮助我们更好的排查函数错误

例子:

int main(void)
{
    int a = 10;

    fun(100); // 需要指针参数,这里传错,编译就会报错

    return 0;
}

int fun(int *p)
{

}

1)如果没有声明

只报函数没有声明的警告

尽管实参和形参类型明显不匹配,但是编译器并没有提示“类型有问题”,因为没有做类型检查

2)如果加上声明

编译时就会通过声明进行类型的检查,然后报实参和形参类型不匹配的警告,
这个信息可以帮助我们排查函数的类型错误


3. 不进行函数声明,编译可以通过吗?

如果函数定义本来就在函数调用位置的前面,定义本身就是声明,编译肯定能过!

但是如果函数定义不在调用位置的前面,而且还没有给额外的声明,编译还能过吗?

这要看编译器的严格程度,有可能编译通过,但是有些严格编译器编译时不能通过

不过不管人家编译器严不严格,按照正规操作,我们必须要进行声明,
如果不进行声明的话,编译时不会进行函数的类型检查,由于没有做类型检查,
就算程序能够编译通过,而且还能运行,但是很有可能会出现因传参和返回值类型不对而导致的错误


4. 调用库函数为什么需要进行声明

比如在程序中调用printf时,必须在.c中包含stdio.h头文件,因为这里面有printf函数的声明

对库函数进行声明,函数声明的目的是一样的。

  • 一是为了拓展作用域
  • 二是为了进行参数检查

与我们前面所举例子的唯一不同是,库函数时别人帮我们写好

如果调用的函数不在本文件中,而在其它的文件中,比如

  • 在我自己写的其它.c
  • 在库文件中

除了要对函数进行声明外,还必须链接函数所在的文件,函数声明的只用于“作用域的拓展”和类型检查,
第四阶段链接时,必须要链接函数所在的文件

只有在链接了函数所在文件后,在链阶段进行声明和定义的全局符号统一时,
你才能在链接文件中找到全局函数的定义,不然链接时会报该函数没有定义的错误

a.c

extern fun();

int main()
{
    fun();
}

b.c

int fun()
{

}