C语言extern的概念(声明和定义的区别)
在java语言中,没有注意,C语言还专门有个关键词 extern来标示声明,在这记录一下:
extern adj. 外面的;外来的;对外的 外部变量的意思
最简单的说法:
声明就是没有分配值空间
定义就是分配了值空间
这样说貌似也没错,但一些场景有点说不清,比如下面
extern int i; int i; extern int d = 3, f = 5; // d 和 f 的声明与初始化 int d = 3, f = 5; // 定义并初始化 d 和 f
这两种情况:要么都没有赋值,要么都赋值。那么这样还有什么意义,有什么区别。
具体来说就是:
extern int i; //声明,不是定义 int i; //声明,也是定义 区别就是定义包括了声明,声明只是声明。 意思就是变量使用前都需要声明然后定义, 但写法可以一步到位(声明就定义), 也可分开写(不仅可以在同一个文件不同地方,还可以声明和定义分别在不同文件)。 声明的使用有两种情况: 1、一种是声明需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。 2、另一种只声明不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a, 其中变量 a 可以在别的文件中定义的。 除非有extern关键字,否则都是变量的定义。 int i;这种写法和java一样都是, 首先编译器初始化(分配空间值为0),再有赋值语句,就修改内存空间的值。
extern int d = 3, f = 5; // d 和 f 的声明与初始化 int d = 3, f = 5; // 定义并初始化 d 和 f 个人理解这两种的区别: extern int d = 3, f = 5; 分配空间的直接赋值。 int d = 3, f = 5;分配空间先指定一个默认值再赋值。
补充:初始化的意思有两种情况
1,定义时指定值,第一次主动赋值。 2,定义不指定值,编译器默认赋值
一个声明和赋值不在同一个文件的例子:
addtwonum.c 文件代码:
#include <stdio.h> /*外部变量声明*/ extern int x ; extern int y ; int addtwonum() { return x+y; }
test.c 文件代码:
#include <stdio.h> /*定义两个全局变量*/ int x=1; int y=2; int addtwonum(); int main(void) { int result; result = addtwonum(); printf("result 为: %d\n",result); return 0; }
执行:
$ gcc addtwonum.c test.c -o main $ ./main result 为: 3
注意点:C语言的方法定义不同java语言(略微有点java抽象方法的影子,这里先声明方法体再用抽象方法(个人的一种非正规表达方式))
/*外部变量声明*/ extern int x ; extern int y ; int addtwonum() { return x+y; } /*定义两个全局变量*/ int x=1; int y=2; int addtwonum(); //别的文件有方法体,这里竟然可以这样写。而且不用传参数就赋值!!!(从java角度看,挺别扭的!) int main(void) { int result; result = addtwonum(); printf("result 为: %d\n",result); return 0; }
补充:声明和定义的故事
声明和定义 源码执行经过编译器这座桥梁。 用文件去编写程序,一个大型程序会被组织成多个文件,这就给编译带来了难题。 这些文件最终是要被翻译成程序的,可是它们的数量却是变化的。 较小的程序可能有2个文件,较大的程序可能有几百万个文件, 代码是组织在多个文件中的,编译器为了解决这个问题, 提出了声明和定义这两个概念声明 Declarations 定义 Definitions 一个变量或函数在内存中只能存在一份,所以在代码中它只能在一个地方被定义,这就是定义。 而这个变量或函数可能被多个文件使用, 使用的时候需要知道它的类型, 可是它却只能有一个定义,怎么解决这个矛盾呢?-- 用声明。 如果程序不是存放在多个文件中,那么根本就不需要声明,直接定义对象就够了。 如果程序虽然放在多个文件中,可是它们能相互间自由引用(考,那和一个文件有什么分别),那么也不需要声明。 可是,你知道这些假设都是不可能的,因为人类是用一个一个的文件去表达的。 看待一个文件的时候,某个事物在它的上下文中意义更明确并与众不同。 编译器把它定义为作用域,并用在了函数中。造成了编译器必须去这么设计,必须有声明和定义这种语法。
赋值和初始化的故事:
赋值和初始化现在的编译器已经“聪明”到超出你的想象。 即使去看几十年前的老 c 编译器,它的聪明程度也会令你惊叹。 初始化就是这样的一个“聪明”的行为。 可是偏偏初始化使用了和赋值一样的语法,形如 int foo = 123;结果,导致了这个编译时行为有点耍“小聪明”的味道。 如果我告诉你,在c语言本来的设计中,初始化和赋值是两种截然不同的语法。你就会恍然大悟了。 int foo 123; /* 初始化,只能用在全局变量 */ int foo = 123; /* 赋值,只能用在局部变量 */ 这两种语法出现的场景、作用的对象和含义都不相同,很好区分。 初始化完全是编译器的行为,赋值则是运行时的行为。 标准 C 后来统一了初始化这个概念,全局变量的初始化和自动变量的默认值赋值都叫初始化。 这确实更“高级”了,但是其实这两个初始化差别却存在,全局变量的初始化值只能是常量。所以这也是 C 的一个遗憾。
故事会:
一、未声明 1.c: int main() { a = 1; } $ cc 1.c1.c:2:5: error: use of undeclared identifier 'a' a = 1; ^1 error generated. undeclared:declare 是【对外宣告】, undeclared形容词 --【没有对外宣告过的】, 叫【未声明】 identifier: identify是【身份证】,也叫ID。 *ier是什么人,identifier就是【有身份的人】, 叫【标识符】 合起来是, a 标识符没有声明,不知道它是个什么东西。这就叫未声明 undeclared 二、未定义 1.c: extern int a; int main() { a = 1; } $ cc 1.c/tmp/ccxhuV7j.o: In function `main':1.c:(.text+0x6): undefined reference to `a'collect2: error: ld returned 1 exit status undefinedundefined: 【没有定义过的】意思 reference:【介绍信】的意思 叫【引用】 to 'a': 对于 a 合起来是,对于 a 的【介绍信】,是【没有定义过的】 这句话是说,a 是个名称,它引用的内存实体是不存在的。 这就叫未定义 undefined 三、不能赋值 1.c: int main() { main = 123; } $ cc 1.c1.c:2:8: error: non-object type 'int ()' is not assignable main = 123; ~~~~ ^1 error generated. assignable这句话是说non-object type: 非对象类型 'int ()': 返回值为int的函数类型 is not assignable: 不可以被赋值合起来就是,非对象类型的函数类型不可以被赋值main 是返回值为int的函数类型, 它为什么不能被赋值呢? 要从对象说起,对象是一块可以操作的内存块。 言外之意,内存中还存在不能被操作的内存? 是的, 内存有向量区 禁止入内文本区 禁止乱涂乱画数据区 自己的可以随便玩,不是自己的禁止拍照IO区 只开放给专家学者对象! 本例中,main 是一块文本区的内存,不是可操作的内存,所以不能被赋值。 这就叫不能赋值 not assignable 四、不能初始化 1.c: int a = "foo"; int main() {} $ cc -w 1.c1.c:1:9: error: initializer element is not computable at load time int a = "foo"; initializerelement:是常量"foo",是个字符串地址 is not computable:不是算数at load time: 在程序运行的时候合起来就是,字符串地址在运行时不能被计算。 a 这个位置是 int ,字符串就是一个地址,也是 int 。 因此,从原理上来说上面程序没有问题。 事实上,在老 c 语言中,上述程序正常。 但是,后来语法变了。为什么? 为了更加规范和安全,这种行为被禁止了。 编译器给出的理由是,初始化的元素在运行时是算不了的。 其实,这是一个善意的谎言,指针当然可以计算。 但是为了规范有人阻止了你,阻止你的人正是初始化。 初始化是编译器这个大程序的一个子程序。 程序分编译时,运行时。 目前看来,编译时越来越庞大,越来越聪明。 各种思想方法论被发明出来,典型如c++。 事实上,编译器初始化的一些小聪明展现了他的可怜父母心,巴不得把所有后事都料理完,脏活累活全都不让孩子干。 比如下面的代码 1.c: char *s = "bar"; int days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; long hour = 60*60*1000; int main() {} 编译器为"bar"分配字符串存储,把days变成数组,hour算成600000,而不是在运行时再算。 这就是初始化器 initializer,一个编译逻辑