extern是C语言中的一个关键字,一般用在变量名前或函数名前,作用是用来说明“此变量/函数是在别处定义的,要在此处引用”,extern这个关键字大部分读者应该是在变量的存储类型这一类的内容中
遇到的,下面先分析C语言不同的存储类型
在C语言中变量和函数有数据类型和存储类型两个属性,因此变量定义的一般形式为:存储类型 数据类型 变量名表;
C语言提供了一下几种不同的存储类型:
(1) 自动变量(auto)
(2) 静态变量(static)
(3) 外部变量(extern)
(4) 寄存器变量(register)
(上面的auto、static、extern、register都是C语言的关键字),这里只分析extern关键字的使用
外部变量(全局变量)extern----全局静态存储区
标准定义格式:extern 类型名 变量名;
如果在所有函数之外定义的变量没有指定其存储类别,那么它就是一个外部变量,它的作用域是从它的定义点到本文件的末尾(在单个源文件中的确是这样,如果有多个源文件,全局变量的作用范围不是从变量定义处到该文件结尾,而是在其他文件中也有效),但是如果要在定义点之前或者其他文件中使用它,那么就需要使用关键字extern对其进行声明(注意不是定义,编译器并不为其分配内存)
Tips:
定义:表示创建变量或分配存储单元
声明:说明变量的性质,但并不分配存储单元
extern int i; //是声明,不是定义,没有分配内存
int i; //是定义
如果在声明的时候给变量赋值,那么就和去掉extern直接定义变量赋值是等价的
extern int a = 10;//尽量不要写这种定义方式
int a = 10;//上述两条语句等价
(注意上面的不同语句对声明和定义的区分,对源文件中的局部变量来说是成立的(也就是.c文件),而对于源文件中的全局变量(外部变量)int a和在头文件中的int a就不能用上面的语句来解释声明和定义的区别)
补充:定义和声明的一个小坑,对于int a;来说,在源文件中,如果是全局变量的话就是声明,如果是局部变量的话就是定义
全局变量:
文章前面提到过一句话:如果在所有函数之外定义的变量没有指定其存储类别,那么它就是一个外部变量,意思就是这里的int a;等价于extern int a;相当于声明,声明是可以多次的
局部变量:
文章中只提到了extern(外部变量),这里补充下auto(自动变量),由于auto(自动变量)极为常用,所以C语言把它设计成缺省的存储类型,即auto 可以省略不写,在main函数内部的变量int a也是局部变量,
相当于auto int a;相当于定义,定义只能一次
谨记:声明可以多次,定义只能一次
外部变量保存在静态存储区内,在程序运行期间分配固定的存储单元,其生存期是整个程序的运行期,没有显式初始化的外部变量由编译程序自动初始化为0(extern说明符来扩展全局变量的作用域,
可以将全局变量的作用域扩展到其他文件,但不能限制全局变量的作用域)
extern作用于函数名和变量名时的区别:
读者应该会发现,函数声明时并没有使用 extern 关键字,这是因为,函数的定义有函数体,函数的声明没有函数体,编译器很容易区分定义和声明,所以对于函数声明来说,有没有extern 都是一样的
但是作用于变量名时extern关键字就不是可有可无的了,全局变量在外部使用声明时,extern关键词是必须的,如果变量无extern修饰且没有显式的初始化,就成为了变量的定义,因此此时必须加extern(全局变量在不指定初值时会自动初始化为0)
多文件编程
C语言代码是由上到下依次执行的,不管是变量还是函数,原则上都要先定义再使用,否则就会报错。但在实际开发中,经常会在函数或变量定义之前就使用它们,这个时候就需要提前声明(extern)
头文件中包含的都是函数声明,而不是函数定义
最好不要在头文件中定义变量,例如全局变量
这里的int a是个全局变量的定义,所以如果这个头文件被多次引用的话,a会被重复定义,这显然是不允许的,下面举一个简单多文件编程的例子
从上面的四张图可以看出,一共用了3个文件,两个c文件(源文件)一个h文件(头文件),我在my.c中写了一个函数名为Max的函数,在头文件中声明了这个Max函数,在有main的这个test1.c这个
源文件中用#include "my.h"包含了这个头文件,然后在函数中就可以直接使用Max函数了(注意头文件中只是声明,没有定义,而且不建议在头文件中定义函数,会引起很多不必要的麻烦)
在《高质量C/C++编程指南》一书中,对此也有说明:
【建议1-2-1】头文件中只存放“声明”,而不存放“定义”。
在C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义分开,不论该函数体有多么小。
【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现现象 extern int value 这类声明。
问题一:如果在my.c这个文件中定义了一个全局变量,想要在main函数中使用,该怎么做呢?
方法一:在含有main函数的源文件test1.c中加extern声明
运行结果:
方法二:在头文件中my.h中对my.c中的全局变量进行声明,再在test1.c中include头文件my.h
运行结果:
防止C语言头文件被重复包含
头文件包含命令 #include 的效果与直接复制粘贴头文件内容的效果是一样的,预处理器实际上也是这样做的,它会读取头文件的内容,然后输出到 #include 命令所在的位置,头文件包含是一个
递归(循环)的过程,如果被包含的头文件中还包含了其他的头文件,预处理器会继续将它们也包含进来;这个过程会一直持续下去,直到不再包含任何头文件,这与递归的过程颇为相似,
递归包含会导致一个问题,就是重复引入同一个头文件,重复引入同一头文件有什么问题呢,当你在头文件中定义变量或者函数时(注意是定义不是声明,多次声明是没有问题的)多次引入头文件就会报“变量被多次定义”的错误
下面来还原这种错误:
上面一共使用了3个头文件k1.h、k2.h、my.h在k1.h和k2.h中都包含了my.h,而且在my.h中有一个全局变量int k = 10,在test1.c中包含了上述三个头文件
编译结果
解决方法:
方法一
使用条件编译#ifndef(如果读者对条件编译不了解,可以先看一下这一篇随笔预处理命令使用详解----#if、#endif、#undef、#ifdef、#else、#elif)
解释一下上面的条件编译语句,如果_MY_H这个宏名没有被定义,那么定义_MY_H这个宏名,并且定义全局变量int k = 10,这样做就避免了头文件的重复包含引起的,变量或函数的重复定义问题
我们可以在stdio.h这个“标准输入输出头文件”中看到类似的用法
格式就像下面这个样子:
#ifndef _INC_STDIO
#define _INC_STDIO
/* 头文件内容 */
#endif
这种宏保护方案使得程序员可以“任性”地引入当前模块需要的所有头文件,不用操心这些头文件中是否包含了其他的头文件
但也不是没有缺点
#ifndef的方式依赖于宏名不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含
缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况
方法二
使用#pragma once
#pramgma once是微软编译器独有的,也是后来才有的,所以知道的人并不是很多,用的人也不是很多,因为他不支持跨平台。如果你想写跨平台的代码,最好使用条件编译,如果想使用#pragma once,
只需在头文件开头加上#pragma once即可
#pragma带来的好处是:你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题,坏处也有,#pragma once是由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件,如果某个头文件有多份拷贝,这个方法就不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正
总结:
1.#ifndef 由语言支持所以移植性好
2.#pragma 可以避免名字冲突