用#include可以包含其他头文件中变量、函数的声明,为什么还要extern关键字?

如果我想引用一个全局变量或函数a,我只要直接在源文件中包含#include<xxx.h> (xxx.h包含了a的声明)不就可以了么,为什么还要用extern呢??

这个问题一直也是似是而非的困扰着我许久,经过实践和查找资料,有如下总结:

一、头文件

首先说下头文件,其实头文件对计算机而言没什么作用,她只是在预编译时在#include的地方展开一下,没别的意义了,其实头文件主要是给别人看的。

我做过一个实验,将头文件的后缀改成xxx.txt,然后在引用该头文件的地方用

   #include"xxx.txt"

编译,链接都很顺利的过去了,由此可知,头文件仅仅为阅读代码作用,没其他的作用了!

不管是C还是C++,你把你的函数,变量或者结构体,类啥的放在你的.c或者.cpp文件里。然后编译成lib,dll,obj,.o等等,然后别人用的 时候,最基本的gcc hisfile.cpp yourfile.o|obj|dll|lib 等等。
        但对于我们程序员而言,他们怎么知道你的lib,dll...里面到底有什么东西?要看你的头文件。你的头文件就是对用户的说明。函数,参数,各种各样的接口的说明。
        那既然是说明,那么头文件里面放的自然就是关于函数,变量,类的“声明”(对函数来说,也叫函数原型)了。记着,是“声明”,不是“定义”。

声明和定义的区别。

    变量的声明有两种情况:

    1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。

    2、另一种是不需要建立存储空间的。 例如:extern int a 其中变量a是在别的文件中定义的。

    声明是向编译器介绍名字--标识符。它告诉编译器“这个函数或变量在某处可找到,它的模样象什么”。

    而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。无论定义的是函数还是变量,编译器都要为它们在定义点分配存储空间。 对于变量,编译器确定变量的大小,然后在内存中开辟空间来保存其数据,对于函数,编译器会生成代码,这些代码最终也要占用一定的内存。

    总之就是:把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”。

    基本类型变量的声明和定义(初始化)是同时产生的;而对于对象来说,声明和定义是分开的。

    例如:类A

    如果A a;就是一个声明,告诉编译器a是A类的一个对象变量,但是不进行初始化;

    如果以后a=new A();这就是初始化,分配了空间。

    (我们声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分配存储空间。)

    用static来声明一个变量的作用有二:

    (1)对于局部变量用static声明,则是为该变量分配的空间在整个程序的执行期内都始终存在。

    (2)外部变量用static来声明,则该变量的作用只限于本文件模块。

    补充:

    什么是定义?什么是声明?它们之间的区别是什么?

    所谓定义就是(编译器)创建一个对象,为这个对象分配一块内存,并给它取上一个名字,这个名字就是就是我们经常所说的变量名或对象名。

    声明有2重含义:

    (1) 告诉编译器,这个名字已经匹配到一块内存上,下面的代码用到变量或者对象是在别的地方定义的。声明可以出现多次。

    (2) 告诉编译器,这个名字已经被预定了,别的地方再也不能用它来作为变量名或对象名。

    定义和声明的最重要区别就是:

    定义创建对象并为这个对象分配了内存,声明没有分配内存。

所以,最好不要傻嘻嘻的在头文件里定义什么东西。比如全局变量:
  /*xx头文件*/
  #ifndef _XX_头文件.H
  #define _XX_头文件.H
  int A;
  #endif

那么,很糟糕的是,这里的int A是个全局变量的定义,所以如果这个头文件被多次引用的话,你的A会被重复定义,显然语法上错了。只不过有了这个#ifndef的条件编译,所以能保证你的头文件只被引用一次,不过也许还是不会出岔子,但若多个c文件包含这个头文件时还是会出错的,因为宏名有效范围仅限于本c源文件(指宏所在的文件和include该文件的文件),所以在这多个c文件编译时是不会出错的,但在链接时就会报错,说你多处定义了同一个变量,

  Linking...
  incl2.obj : error LNK2005: "int glb" (?glb@@3HA) already defined in incl1.obj
  Debug/incl.exe : fatal error LNK1169: one or more multiply defined symbols found

  注意!!!

二、extern

这个关键字真的比较可恶,在定义变量的时候,这个extern居然可以被省略(定义时,默认均省略);在声明变量的时候,这个extern必须添加在变量前,所以有时会让你搞不清楚到底是声明还是定义。或者说,变量前有extern不一定就是声明,而变量前无extern就只能是定义。注:定义要为变量分配内存空间;而声明不需要为变量分配内存空间。

下面分变量和函数两类来说:

(1)变量

  尤其是对于变量来说。
  extern int a;//声明一个全局变量a
  int a; //定义一个全局变量a

  extern int a =0 ;//定义一个全局变量a 并给初值。
  int a =0;//定义一个全局变量a,并给初值,

第四个 等于 第 三个,都是定义一个可以被外部使用的全局变量,并给初值。
       糊涂了吧,他们看上去可真像。但是定义只能出现在一处。也就是说,不管是int a;还是extern int a=0;还是int a=0;都只能出现一次,而那个extern int a可以出现很多次。

当你要引用一个全局变量的时候,你就必须要声明,extern int a; 这时候extern不能省略,因为省略了,就变成int a;这是一个定义,不是声明。注:extern int a; 中类型int可省略,即extern a; 但其他类型则不能省略。

(2)函数
       函数,对于函数也一样,也是定义和声明,定义的时候用extern,说明这个函数是可以被外部引用的,声明的时候用extern说明这是一个声明。 但由于函数的定义和声明是有区别的,定义函数要有函数体,声明函数没有函数体(还有以分号结尾),所以函数定义和声明时都可以将extern省略掉,反正其他文件也是知道这个函数是在其他地方定义的,所以不加extern也行。两者如此不同,所以省略了extern也不会有问题。
   比如:
  /*某cpp文件*/
  int fun(void)
  {
        return 0;
  }

  很好,我们定义了一个全局函数
  /*另一cpp文件*/
  int fun(void);
  我们对它做了个声明,然后后面就可以用了
  加不加extern都一样
  我们也可以把对fun的声明 放在一个头文件里,最后变成这样
  /*fun.h*/
  int fun(void);   //函数声明,所以省略了extern,完整些是extern int fun(void);
  /*对应的fun.cpp文件*/
  int fun(void)
  {
       return 0;
  }//一个完整的全局函数定义,因为有函数体,extern同样被省略了。
       然后,一个客户,一个要使用你的fun的客户,把这个头文件包含进去,ok,一个全局的声明。没有问题。
但是,对应的,如果是这个客户要使用全局变量,那么要extern 某某变量;不然就成了定义了。

总结:

对变量而 言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的变量,方法有2种:(1)在A文件中必须用extern声明在B文件中定义 的变量(当然是全局变量);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的变量声明,也即在这个头文件中必须用extern声明 该变量,否则,该变量又被定义一次。

对函数而 言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的函数,方法有2种:(1)在A文件中用extern声明在B文件中定义的函 数(其实,也可省略extern,只需在A文件中出现B文件定义函数原型即可);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的 函数原型,在头文件中函数可以不用加extern。

******************************************************************************************************************************************************

对上述总结换一种说法:

(a)对于一个文件中调用另一个文件的全局变量,因为全局变量一般定义在原文件.c中,我们不能用#include包含源文件而只能包含头文件,所以常用的方法是用extern  int a来声明外部变量。   另外一种方法是可以是在a.c文件中定义了全局变量int global_num ,可以在对应的a.h头文件中写extern int global_num ,这样其他源文件可以通过include a.h来声明她是外部变量就可以了。

(b)还有变量和函数的不同举例

int fun(); 和 extern int fun(); 都是声明(定义要有实现体)。  用extern int  fun()只是更明确指明是声明而已。

而 int a;   是定义

extern int a; 是声明。

(3)此外,extern修饰符可用于C++程序中调用c函数的规范问题。

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

C++语言在编译的时候为了解决的多态问题,会将名和参数联合起来生成一个中间的名称,而c语言则不会,因此会造成链接时找不到对应的情况,此时C就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间名。

注意: 如果一个函数在同一文件中不只被声明一次则链接指示符可以出现在每个声明中,也可以只出现在函数的第一次声明中,在这种情况下第二个及以后的声明都接受第一个声明中链接指示符指定的链接规则例如
// ---- myMath.h ----
extern "C" double calc( double );
// ---- myMath.C ----
// 在Math.h 中的calc() 的声明
#include "myMath.h"
// 定义了extern "C" calc() 函数
// calc() 可以从C 程序中被调用
double calc( double dparm ) { // ...

三、extern和头文件的联系
        这种联系也解决了最初提出的2个问题:

  (a)用#include可以包含其他头文件中变量、函数的声明,为什么还要extern关键字?

(b)如果我想引用一个全局变量或函数a,我只要直接在源文件中包含#include<xxx.h> (xxx.h包含了a的声明)不就可以了么,为什么还要用extern呢??

答 案:如果一个文件(假设文件名A)要大量引用另一个文件(假设文件名B)中定义的变量或函数,则使用头文件效率更高,程序结构也更规范。其他文件(例如文 件名C、D等)要引用文件名B中定义的变量或函数,则只需用#include包含文件B对应的头文件(当然,这个头文件只有对变量或函数的声明,绝不能有 定义)即可。

********************************************************************************************************************************************

那是一个被遗忘的年代,那时,编译器只认识.c(或.cpp)文件,而不知道.h是何物的年代。
       那时的人们写了很多的.c(或.cpp)文件,渐渐地,人们发现在很多.c(或.cpp)文件中的声明变量或函数原型是相同的,但他们却不得不一个字一个 字地重复地将这些内容敲入每个.c(或.cpp)文件。但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(或.cpp)文件,并修改其中的 声明,啊~,简直是世界末日降临!
       终于,有人(或许是一些人)再不能忍受这样的折磨,他(们)将重复的部分提取出来,放在一个新文件里,然后在需要的.c(或.cpp)文件中敲 入#include   XXXX这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了---世界还是那么美好!
       因为这个新文件,经常被放在.c(或.cpp)文件的头部,所以就给它起名叫做“头文件”,扩展名是.h.
       从此,编译器(其实是其中预处理器)就知道世上除了.c(或.cpp)文件,还有个.h的文件,以及一个叫做#include命令。

四、头文件内容

如果想在一个C文件里面引用另外一个C文件里面的变量,怎!么!办?

我们的做法是将变量在H文件中声明为ertern,然后在其他文件中导入这个H文件。

这里需要注意的是,如果导入了H文件,那就不能声明同名的变量了。

另外,H文件中的声明变量必须是在别的文件里面已经声明过的。这里特别强调变!量!

如上图。

左边是H文件,里面声明的一个ppp变量。在右边的文件里面引入H文件。但是!这个ppp变量是不能直接用的。

warning: data definition has no type or storage class [enabled by default]
 ppp = 1;
 ^

这个ppp实际上还是未定义的变量。所以H文件出现不带extern的变量声明是没有意义的。

但是宏定义和typedef都是可以在H文件里面的。如果在H文件里面已经typedef的,在导入H的C文件里面就可以直接用了,而且在次typedef就是重复。

所以这里总结一下H文件里面有什么。

1 #define

2 typedef

3 extern 变量

4 函数声明

转自:http://www.cnblogs.com/tshua/p/5741009.html

   http://lpy999.blog.163.com/blog/static/117372061201182051413310/

     http://blog.csdn.net/feitianxuxue/article/details/7204116

     http://blog.csdn.net/qc_liu/article/details/42236631

定义与声明、头文件与extern总结的更多相关文章

  1. C++ 中的模板类声明头文件和实现文件分离后,如何能实现正常编译?

    C++ 中的模板类声明头文件和实现文件分离后,如何能实现正常编译? 这个feature叫做Export Template,即外名模板,它的作用在于使得模板代码可依照C/C++语言习惯,将模板声明和实现 ...

  2. 定义与声明、头文件与extern总结(转)

    本文转自: http://lpy999.blog.163.com/blog/static/117372061201182051413310/ http://blog.csdn.net/feitianx ...

  3. C++中定义NULL的头文件

    NULL不是C语言基本类型,其定义在stddef.h文件中,作为最基本的语言依赖宏存在.但是随着C/C++的发展,很多文件只要涉及了系统或者标准操作都会将NULL作为标准宏声明或者包含.所以几乎包含任 ...

  4. gsoap 学习 1-自己定义接口生成头文件

    接口头文件的格式在向导中没有看到明确的说明性的内容,但通过看开发包中示例程序中头文件定义和通过wsdl生成的头文件的内容,可以发现,头文件中都会出现以下几行信息 //gsoap ns service ...

  5. class类型重定义,防止头文件重复加载

    今天调用自己写的一个类,出现了class类型重定义问题,上网查了相关资料,发现是头文件重复include引起的问题. 防止头文件重复加载: 系统那些头文件,无论怎么include都没事,因为一般都用了 ...

  6. 详解keil采用C语言模块化编程时全局变量、结构体的定义、声明以及头文件包含的处理方法

    一.关于全局变量的定义.声明.引用: (只要是在.h文件中定义的变量,然后在main.c中包含该.h文件,那么定义的变量就可以在main函数中作为全局变量使用) 方法1: 在某个c文件里定义全局变量后 ...

  7. keil采用C语言模块化编程时全局变量、结构体的定义、声明以及头文件包含的处理方法

    以前写单片机程序时总是把所用函数和变量都写在一个c文件里,后来遇到大点的项目,程序动则几千行,这种方式无疑会带来N多麻烦,相信大家都有所体验吧! 后来学会了在keil里进行模块化编程,即只把功能相同或 ...

  8. extern 用法,全局变量与头文件(重复定义)

    转自 https://www.cnblogs.com/chengmin/archive/2011/09/26/2192008.html 当你要引用一个全局变量的时候,你就要声明,extern int  ...

  9. C++ 多文件编译简述:头文件、链接性、声明与定义

    目录 Commen Sense 头文件 链接性 static 与链接性控制 extern 与外部链接性 Reference Commen Sense C++ 在编译时对每个翻译单元(Translati ...

随机推荐

  1. Android开源之BaseRecyclerViewAdapterHelper(持续更新!)

    官方地址:http://www.recyclerview.org/ 文档 v1.9.8 English 中文 v2.0.0 English 中文 Extension library PinnedSec ...

  2. 解决 slf4j + log4j 在云服务上打印乱码

    由于云服务器的环境是纯英文的 虽然在eclipse中可以打印日志显示中文,但是实用putty的时候查看却是乱码,下载日志也同样是乱码 那么只要设置utf-8即可

  3. 关于generate用法的总结

    Abtract generate语句允许细化时间(Elaboration-time)的选取或者某些语句的重复.这些语句可以包括模块实例引用的语句.连续赋值语句.always语句.initial语句和门 ...

  4. java 分布式锁总结

    第一步,自身的业务场景: 在我日常做的项目中,目前涉及了以下这些业务场景: 场景一: 比如分配任务场景.在这个场景中,由于是公司的业务后台系统,主要是用于审核人员的审核工作,并发量并不是很高,而且任务 ...

  5. mysql中delimiter

    delimiter xx 其实就是告诉MySQL解释器,该段命令已经结束了,mysql是否可以执行了.默认请况下delimiter是分号';'.

  6. https://jzh.12333sh.gov.cn/jzh/

    https://jzh.12333sh.gov.cn/jzh/ https://superuser.com/questions/171917/force-a-program-to-run-withou ...

  7. Dynamic Control Flow in ML

    https://arxiv.org/abs/1805.01772 https://www.leiphone.com/news/201702/cb7cPOtzFj1pgRpk.html

  8. SVN Cleanup failed to process the following paths错误的解决

    在使用TortoiseSVN工具执行Cleanup操作时经常出现Cleanup failed to process the following paths的错误,具体如下图: 网上搜索了一下,找到了解 ...

  9. hadoop的核心思想【转】

    [转自]:http://www.superwu.cn/2014/01/10/963/ 1.1.1. hadoop的核心思想 Hadoop包括两大核心,分布式存储系统和分布式计算系统. 1.1.1.1. ...

  10. 字符驱动程序之——poll机制

    关于这个韦老师给了一个简单的参考文档: poll机制分析 韦东山 2009.12.10 所有的系统调用,基本都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数.比如系统调用open.r ...