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,一个编译逻辑

 

posted @ 2021-05-08 10:48  假程序猿  阅读(2995)  评论(0编辑  收藏  举报