先上最重要的结论和解释:
1)预编译后头文件在所有包含它的源文件里面展开,
2)任何两个文件(全局区)不能有重名的定义
以预编译以后为准进行推断,头文件里面的定义会在各个包含它的源文件里面展开,不管在哪个文件编译器都会把它们放全局区,无法区分,除非有static修饰文件作用域(c语言很严格,任何重名定义都不被允许,两个不同类型的变量但名字相同不被允许,两个函数返回类型或参数不同但名字相同不被允许(所谓的不支持重载),一个是函数一个是变量但名字相同也不被允许)
本文用一个头文件,两个源文件来做实验。
在后面截图中,中间那个文件是头文件,上下是两个源文件。
-
c语言所有执行的动作只能放在函数里面,函数以外只能放声明、定义之类的。原因要结合编译过程理解,全局区数据在编译时就分配好了,执行动作都是在函数里面进行的。比如下面这个例子,我在源文件的函数外面进行赋值操作,编译器还以为我是在声明语句漏了类型。
2.若在头文件里面只对变量声明,且只在一个源文件里面进行定义,则可以编译执行。比如我把上面的definition.c里面的错误执行,改成带类型的定义,编译器认为a变量在头文件里面只是一个声明,而此处才是正式的定义,编译执行正常。
3.如果此时我在另一个源文件里面也对a做定义,编译就会认为重复定义了a变量(即头文件里声明,多个源文件进行了定义,被判定重复定义)
4.这时即使我把头文件里面的a变量声明去掉,也会一样提示重复定义
这说明根子在于:在c语言的所有源文件里面,不能有重名变量(若加static可以限制文件作用域,避开此问题)。头文件在预编译后就不存在了,真正编译的都是源文件,头文件导致的重复定义问题要结合编译过程来解。
即使两个源文件对变量a定义的类型不一样也不行:
5.把源文件里面的定义都去掉掉,只在头文件里面定义,也会提示多重定义。实际上这里和第4点分别在两个源文件里面有定义同名的变量是一样的,因为预编译阶段头文件在源文件里面展开了
6.于是就有规范:一般如果需要开放全局变量给其他文件用,使用extern修饰,以免发生多重定义,实际上一般不建议开放全局变量,一般都是提供函数接口
7.其他人的做法
我看了一下同事的库头文件,里面是直接定义了一些变量,前面加了static修饰,在库源文件里面有使用它们。我用此方法试了一下在固件源文件引用它们,发现不会发生multiple definition错误。但是这种做法是不对的,在预编译展开后,其实际是在不同的源文件里生成了自己文件作用域里面的变量,也就是我引用的和他库里面使用的此时压根就不是一个变量了。
8.对于函数呢?也是一样,如下,我把函数定义放头文件里了,两个源文件都没有做定义,也编译提示重复定义
根子在于:
1)预编译后头文件在所有包含它的源文件里面展开,
2)只要是参与编译的源文件,任何两个文件里面不能有重名的定义
经过预编译后,头文件里面的定义会在各个包含它的源文件里面展开,不管在哪个文件,编译器都会把它们放全局区,无法区分,除非有static修饰文件作用域(等一等,如果一个是变量一个是函数行不行呢?看下面第9点)
9.c语言的严格程度让我感到震撼,不光是类型不同的同名变量不允许存在,连一个是变量一个是函数只要名字相同都是不合法的