首页 /  技术专区  /  C 宽屏模式 >

链接域与extern、static关键字

1、 链接域 与 extern、static关键字

1.1 回顾链接

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

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

链接时主要做两件事:

(1)符号解析

        1)对全局符号进行符号统一

        2)将符号的引用 与 符号的定义关联起来

(2)地址重定位


本小节的要讲“链接域”其实就与链接时的“全局符号统一”有关。

1.2 链接域 ———— 跨文件作用域

1.2.1 回顾代码块作用域

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

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

1.2.2 回顾 本文件作用域

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

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

1.3 跨文件作用域 与 extern关键字

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

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

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

(1)如何实现跨文件的作用域

        只要满足两个条件即可。

        1)将定义标记为extern

        extern表示定义的符号是一个全局符号,由于是全局符号,因此对于其它文件来说这个符号是可见的。

        2)在其它文件中进行声明,声明也需要标记为extern

        extern表示声明的符号也是一个全局符号,对于其它文件也是可见的。

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

(2)例子

a.c                           

extern int a;                   
extern int fun();

int main(void)                 
{                               
                        
}

b.c


int a = 100; //全局符号,extern可以省略

int fun()
{
    printf("helloworld\n");
}

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

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

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

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

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

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

        · 全局变量的“全局”:指的是文件

        · 全局符号的“全局”:指的是整个C工程项目

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

(1)全局符号的重名问题

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

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

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

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

        将符号标记为本地符号。

        1)什么是本地符号?

        我们在之前详细介绍过,这里再简单回顾下。

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

        2)本地符号的作用域

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

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

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


1.5 总结一下extern 和 static关键字

本章我们介绍了不少的关键字,其中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.6 声明的作用

1.6.1 变量的声明

拓宽变量的作用域。

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

1.6.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()
{
}




头像
0/200
图片验证码