extern使用说明

1 基本解释
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
另外,extern也可用来进行链接指定。

2 问题:extern 变量
在一个源文件里定义了一个数组:char a[6];
在另外一个文件里用下列语句进行了声明:extern char *a;
请问,这样可以吗?
答案与分析:
1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。
2)、例子分析如下,如果a[] = "abcd",则外部变量a=0x61626364 (abcd的ASCII码值),*a显然没有意义
显然a指向的空间(0x61626364)没有意义,易出现非法内存访问。
3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。
4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。

4 问题:extern 函数2
当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?
答案与分析:
目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。
宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。

5 问题:extern “C”
在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?

答案与分析:
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
下面是一个标准的写法:
//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif /* __cplusplus */


//.h文件结束的地方
#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif /* __cplusplus */

3 问题:extern 函数1
常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
答案与分析:
如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:
extern int f(); 和int f();
当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。

-----------------------------------------------------------

extern数组与extern指针

数组名代表了存放该数组的那块内存,它是这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。数组名跟枚举常量一样,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址。这就是数组名属于符号常量的意义所在。由于数组名是一种符号常量,它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不是左值,那么,数组名永远都不会是指针!

对于这段话我是这么理解的:数组名在经过编译之后将变成一个数值,这个数值就是该数组的首地址。由于数组名是一个地址,那么把它赋给一个指针变量也就不足为奇了。

例如有定义

char a[14];

char * p;

char * q;

void foo(char * pt)

{

};

考虑以下几种赋值:

p=a;//合法,将一个地址赋给一个指针变量;

q=p;//合法,将一个指针变量的值赋给另一个指针变量;

a=p;//非法,a是数组名即地址,不是一个变量,不可被赋值(也就是上文中说的"数组名是右值")

再看这几种调用:

foo(a);//将一个地址作为参数传入函数,函数中用一个指针变量接收这个地址值

foo(p);//将一个指针变量的值传入函数(也是一个地址),函数中用一个指针变量接收这个地址

可以看出许多时候数组名和指针可以等同地看待,而c也把它们看作是兼容的类型对待,这就是为什么我那个错误的声明不被编译器在语法检查的时候“喀嚓”的原因。

关于extern的作用,许多地方都有说明,例如可以在c++里进行c格式函数的声明,可以声明一个变量或函数是外部变量或外部函数;我们这里要讨论的是外部变量的声明。被extern修饰的全局变量不被分配空间,而是在连接的时候到别的文件中通过查找索引定位该全局变量的地址。

有了这些基础后,我们现在正式开始研究extern 数组和extern 指针的问题:

首先在一个.c文件中有如下定义:

char a[]={1,2,3,4};

分析:这是一个数组变量的定义,编译器将为这个数组分配4字节的空间,并且建立一个索引,把这个数组名、数组类型和它被分配的空间首地址对应起来。它被编译之后生成一个中间文件

然后我们在另一个.c文件中分别以不同的形式进行声明:

(1) extern char a[];

分析:这是一个外部变量的声明,它声明了一个名为a的字符数组,编译器看到这个声明就知道不必为这个变量分配空间,这个.c文件中所有对数组a的引用都化为一个不包含类型的标号,具体地址的定位留给连接器完成。编译完成之后也得到一个中间文件,连接器遍历这个文件,发现有未经定位的标号,于是它搜索其他中间文件,试图寻找到一个匹配的空间地址,在此例中无疑连接器将成功地寻找到这个地址并将此中间文件中所有的这个标号替换为连接器所寻找到的地址,最终生成的可执行文件中,所有曾经的标号都应当已经被替换为地址。这是一个正常工作过程,连接出来的可执行文件至少在对于该数组的引用部分将工作得很好。

(2) extern char * a;

分析:这是一个外部变量的声明,它声明了一个名为a的字符指针,编译器看到这个声明就知道不必为这个指针变量分配空间,这个.c文件中所有对指针a的引用都化为一个不包含类型的标号,具体地址的定位留给连接器完成。编译完成之后仍然得到一个中间文件,连接器遍历这个文件,发现有未经定位的标号,于是它搜索其他中间文件,试图寻找到一个匹配的空间地址,经过一番搜索,找到了一个分配过空间的名为a的地方(也就是我们先定义的那个字符数组),连接器并不知道它们的类型,仅仅是发现它们的名字一样,就认为应该把extern声明的标号连接到数组a的首地址上,因此连接器把指针a对应的标号替换为数组a的首地址。这里问题就出现了:由于在这个文件中声明的a是一个指针变量而不是数组,连接器的行为实际上是把指针a自身的地址定位到了另一个.c文件中定义的数组首地址之上,而不是我们所希望的把数组的首地址赋予指针a(这很容易理解:指针变量也需要占用空间,如果说把数组的首地址赋给了指针a,那么指针a本身在哪里存放呢?)。这就是症结所在了。所以此例中指针a的内容实际上变成了数组a首地址开始的4字节表示的地址(如果在16位机上,就是2字节)。本例中指针a的初值将会是0x0a090807(little endian)(4321),显然不是我们的期望值,所以运行会出错也就理所应当了。

?

几点细节:我们发现,使用extern修饰的变量在连接的时候只找寻同名的标号,不检查类型,例如如果我们定义的甚至不是一个变量而是一个全局的函数,比如去掉定义

char a[]={....};

代之以

void a(){};

连接器居然也会连接通过。

实例如下:

比如在a.c文件中有这样一段代码

int g_i[] = {1, 2, 3, 4};

extern void testdotp();

void main(void)

{

int i = 0;

int num = 0;

num = sizeof(g_i) / sizeof(int);

printf("in main: g_i =%d", i, g_i);

for (i = 0; i < num; i++)

{

printf("g_i[%d] = %d ", i, g_i[i]);

}

printf("/n");

testdotp();

}

而在b.c中的代码如下:

extern int *g_i;

void testdotp()

{

printf("*(&g_i + 2) = %d/n", *(&g_i + 2));

printf("&g_i = %d/n", &g_i);

printf("&g_i + 1= %d/n", &g_i + 1);

printf("g_i = %d/n", g_i);

printf("g_i + 1 = %d/n", g_i + 1);

}

运行结果为

in main: g_i =134518852 g_i[0] = 1 g_i[1] = 2 g_i[2] = 3 g_i[3] = 4 /n
in b.c:
*(&g_i + 2) = 3
&g_i = 134518852
&g_i + 1= 134518856
g_i = 1
g_i + 1 = 5

分析如下:

因为b.c文件中g_i变量的地址是a.c文件中g_i数组的首地址,故g_i的值为g_i[0]的值,&g_i的值为g_i地址的首地址。

而*(&g_i + 2)的值:&g_i的值为g_i数组的首地址,(&g_i + 2)就为数组g_i第3个元素的地址,*(&g_i + 2)就为第2个元素的值,即3。

&g_i + 1:由于&g_i的值为g_i数组首地址,由于在32位机上运行,故&g_i + 1在&g_i基础上加上4个字节

g_i + 1:由于g_i是一个指针变量,g_i变量内存放的是地址,又因为g_i的值为1,而g_i + 1就为1 + 4的单元的内存空间(32位机上),故g_i + 1为5。

 

posted on 2012-05-30 15:45  york_hust  阅读(754)  评论(0编辑  收藏  举报