c-c++:extern的些许感悟

c-c++:extern的些许感悟

最近需要使用extern这个关键字,网上查了一些资料,现在汇总了一部分,并加入了部分个人感悟,如有错误,敬请指出!

extern可置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。

extern使用小例子:

/*****************************************************************************************************
版本1:
main.c
#include <stdio.h>
#include <stdlib.h>
#include "test.h"

extern int arr[];
int main(int argc, char *argv[]) {
    printf("%d\n",my_add(17,2));
    int i ;
    for( i = 0 ; i < 5 ; i++ ){
        printf("result is : %d\n",arr[i]);
    }

    return 0;
}

test.c
#include "test.h"

int my_add(int a, int b){
    return a+b;
}

int arr[5]={88,77,66,55,22};

test.h
#ifndef _TEST_H
#define _TEST_H

int my_add(int a, int b);

#endif
*****************************************************************************************************/

/*****************************************************************************************************
版本2:
main.c
#include <stdio.h>
#include <stdlib.h>
#include "test.h"

int main(int argc, char *argv[]) {
    extern int arr[];
    printf("%d\n",my_add(17,2));
    int i ;
    for( i = 0 ; i < 5 ; i++ ){
        printf("result is : %d\n",arr[i]);
    }

    return 0;
}
test.c以及test.h和版本1中完全相同。
*****************************************************************************************************/
/*****************************************************************************************************
版本3:
main.c
#include <stdio.h>
#include <stdlib.h>
#include "test.h"

int main(int argc, char *argv[]) {

    printf("%d\n",my_add(17,2));
    int i ;
    for( i = 0 ; i < 5 ; i++ ){
        printf("result is : %d\n",arr[i]);
    }

    return 0;
}

test.h
#ifndef _TEST_H
#define _TEST_H

int my_add(int a, int b);

extern int arr[];
#endif

test.c与版本1中完全相同,未发生改变。
*****************************************************************************************************/
结论:

1.
对比 版本1 以及 版本2可得出结论:

extern int arr[];放在main.c中的哪里都可以,只要在main.c中访问arr数组的时候(比如说例子中的输出就是访问的一种形式),
计算机能到对应的内存单元取数据即可。类似对于函数的定义,我们知道,对于一个函数,可以把它的函数原型放在适当的位置,
这样就可以被程序中其他函数访问得到,,这里的arr类似新定义函数的函数原型,规则是类似的(函数原型放哪里?回去看看书就知道啦!)。

2.
对比 版本1 版本2 版本3 可得出结论:

#include原理确实是文本的一种复制,把extern int arr[];这个声明放在头文件中还是很合适的,
其实质就是由于在main.c中#include “test.h”,所以会把test.h的文本内容拿到main.c中,然后进行一系列操作。
实际上 版本3 相当于 版本1 ,因为本质就是把 extern int arr[]; 放到了整个main函数的外部。

注意:
main.c不能修改成以下形式:

#include <stdio.h>
#include <stdlib.h>
#include "test.h"

int main(int argc, char *argv[]) {
    extern int *arr;
    printf("%d\n",my_add(17,2));
    int i ;
    for( i = 0 ; i < 5 ; i++ ){
        printf("result is : %d\n",arr[i]);
    }

    return 0;
}

也就是把声明 extern int arr[]; 改成了 extern int *arr;是不可以的。

原因在于,指向类型T的指针并不等价于类型T的数组。

extern char *arr声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。

应该将声明改为extern char arr[ ]。

这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。

extern用在变量声明中常常有这样一个作用:你要在.c文件中引用另一个文件中的一个全局的变量,那就应该放在.h中用extern来声明这个全局变量。

编译连接:

现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是互相不透明的。

也就是说,在编译时,全局变量的可见域限制在文件内部。

下面举一个简单的例子:

创建一个工程,里面含有A.cpp和B.cpp两个简单的C++源文件:

//A.cpp
int i;
int main(){
    return 0;
}
//B.cpp
int i;

这两个文件极为简单,在A.cpp中我们定义了一个全局变量i,在B中我们也定义了一个全局变量i。

我们对A和B分别编译,都可以正常通过编译,但是进行链接的时候,就会报出Error。

原因是,在编译阶段,各个文件中定义的全局变量相互是不透明的,编译A时觉察不到B中也定义了i,同样,编译B时觉察不到A中也定义了i。

但是到了链接阶段,要将各个文件的内容“合为一体”,因此,如果某些文件中定义的全局变量名相同的话,在这个时候就会出现错误(重复定义)。

因此,各个文件中定义的全局变量名不可相同。

在链接阶段,各个文件的内容(实际是编译产生的obj文件)是被合并到一起的,因而,定义于某文件内的全局变量,在链接完成后,它的可见范围被扩大到了整个程序。

这样一来,按道理说,一个文件中定义的全局变量,可以在整个程序的任何地方被使用,举例说,如果A文件中定义了某全局变量,那么B文件中应可以使用该变量。

修改我们的程序,加以验证:

//A.cpp
int i;
int main(){
    i = 100;    //在A.cpp中访问B.cpp中的全局变量
return 0;
}
//B.cpp
int i;

结果是编译错误。

其实出现这个错误是意料之中的,因为文件中定义的全局变量的可见性扩展到整个程序是在链接完成之后,而在编译阶段,他们的可见性仍局限于各自的文件。

编译器的目光不够长远,编译器没有能够意识到,某个变量符号虽然不是本文件定义的,但是它可能是在其它的文件中定义的。

虽然编译器不够有远见,但是我们可以给它提示,帮助它来解决上面出现的问题。这就是extern的作用了。

extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

//A.cpp
int i;
extern int I;
int main(){
    i = 100;    //在A.cpp中访问B.cpp中的全局变量
return 0;
}
//B.cpp
int i;

编译链接成功!

extern有两个作用:

1.
当它与”C”一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,

可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的”脾气”了(不同的编译器采用的方法不一样),为什么这么做呢,因为C++支持函数的重载,在这里不去过多的论述这个问题。

2.
当extern不与”C”在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块活其他模块中使用,记住它是一个声明不是定义!

也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

单方面修改extern 函数原型:

Q:当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?

A: 目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。

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

A: C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况。

此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。

下面是一个标准的写法:
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif  /* __cplusplus */ 
 …
 …
        //.h文件结束的地方
#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif  /* __cplusplus */

Q: 常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?

A: 如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:

extern int f(); 和int f();

当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。

关于这样做的原因和利弊可见下面的这个例子:

“用extern修饰的全局变量”:

test1.h
#ifndef TEST1H
#define TEST1H
extern char g_str[];        // 声明全局变量g_str
void fun1();
#endif

test1.cpp
#include "test1.h"
char g_str[] = "123456";    // 定义全局变量g_str
void fun1() { cout << g_str << endl; }

test2.cpp(test2模块也想使用g_str,只需要在原文件中引用就可以了)
#include "test1.h"
void fun2()    { cout << g_str << endl;    }

以上test1和test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面找到”123456”这个字符串,但是你却不能在test2.obj里面找到,

这是因为g_str是整个工程的全局变量,在内存中只存在一份,test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!

注意:只在头文件中做声明,这个是比较好的行为。

关于extern和static:

extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量。

static 表示静态的变量,分配内存的时候, 存储在静态区,不存储在栈上面。

static 作用范围是内部连接的关系, 和extern有点相反.它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern 引用,而static 不可以,只允许对象本身用它。

具体差别:

  1. static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量
  2. static修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了
  3. static修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它

对于上述3.的示例:

test1.h:
#ifndef TEST1H
#define TEST1H
static char g_str[] = "123456"; 
void fun1();
#endif

test1.cpp:
#include "test1.h"
void fun1()  {   cout << g_str << endl;  }
test2.cpp
#include "test1.h"
void fun2()  {   cout << g_str << endl;  }

以上两个编译单元可以连接成功, 当你打开test1.obj时,你可以在它里面找到字符串”123456”,同时你也可以在test2.obj中找到它们。

它们之所以可以连接成功而没有报重复定义的错误是因为虽然它们有相同的内容,但是存储的物理地址并不一样,就像是两个不同变量赋了相同的值一样,而这两个变量分别作用于它们各自的编译单元。

注意:一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染。

extern和const:

C++中const修饰的全局常量据有跟static相同的特性,即它们只能作用于本编译模块中,但是const可以与extern连用来声明该常量可以作用于其他编译模块中, 如extern const char g_str[];

然后在原文件中别忘了定义: const char g_str[] = “123456”;

所以当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!

注意,const char* g_str = “123456” 与 const char g_str[] =”123465”是不同的, 前面那个const 修饰的是char *而不是g_str,它的g_str并不是常量,

它被看做是一个定义了的全局变量(可以被其他编译单元使用), 所以如果你想让char*g_str遵守const的全局常量的规则,最好这么定义const char* const g_str=”123456”。

extern用在变量声明中常常有这样一个作用,你在.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在.h中并用extern来声明。

如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有区别:

extern int f(); 和int f();

如果定义函数的c/cpp文件在对应的头文件中声明了定义的函数,那么在其他c/cpp文件中要使用这些函数,只需要包含这个头文件即可。

如果你不想包含头文件,那么在c/cpp中声明该函数。一般来说,声明定义在本文件的函数不用“extern”,声明定义在其他文件中的函数用“extern”,这样在本文件中调用别的文件定义的函数就不用包含头文件。

注意下面这个例子:

main.c
#include <stdio.h>
#include <stdlib.h>

void my_print();
int main(int argc, char *argv[]) {
    my_print();
    return 0;
}

fun.c
#include <stdio.h>
void my_print(){
    printf("this is my_print function !\n");
}

程序可以正常运行,想想问什么呢?原因在于,main.c中编译后的.obj文件还不需要指定函数实体,当在链接的时候,才会与函数实体关联。

实际上在连接的时候,在main.c中的my_print()的声明恰好和链接过来的fun.obj中内容对应的,应该说void my_print();这个声明作用域范围由原本的main.c(main.obj)扩大到了fun.c(fun.obj)中。

关于extern “C”的用法解析:

C++保留了一部分过程式语言的特点,因而它可以定义不属于任何类的全局变量和函数。

但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。

加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;

而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern “C”来强制编译器不要修改你的函数名。

#ifndef __INCvxWorksh   /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus          //__cplusplus是cpp中自定义的一个宏
extern "C" {                //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif
/**** some declaration or so *****/  
#ifdef __cplusplus
}
#endif
#endif                              /* __INCvxWorksh */

上面曾经提到说,extern的一个作用是提示编译器按照C语言方式编译和链接的,这里再次提及下:

作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。
例如,假设某个函数的原型为:

void foo( int x, int y );

该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。

例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以”.”来区分。

而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

举例说明:

(1) 未加extern “C”声明时的连接方式

假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
//在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);

实际上,在连接阶段,链接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

(2) 加extern “C”声明后的编译和链接方式

加extern “C”声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

<1>A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

<2>链接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern “C”类型,而模块B中包含的是extern int foo(int x, int y),则模块B找不到模块A中的函数;反之亦然。

extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程。

通常extern “C” 的使用场合:C++代码调用C语言代码、在C++的头文件中使用

在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

extern "C"
{
#include "cExample.h"
}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern “C”声明,在.c文件中包含了extern “C”时会出现编译语法错误。

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);     //注:写成extern "C" int add(int , int ); 也可以
#endif

/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
 return x + y;
}

// c++实现文件,调用add:cppFile.cpp
extern "C"
{
 #include "cExample.h"        //注:此处不妥,如果这样编译通不过,换成 extern "C" int add(int , int ); 可以通过
}

int main(int argc, char* argv[])
{
 add(2,3);
 return 0;
}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern “C”{}。

//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
 return x + y;
}

/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
 add( 2, 3 );
 return 0;
}

重点注意:

如果全局变量定义在了头文件:

#ifndef _XX_头文件.H
#define _XX_头文件.H
int A;
#endif

如果这个头文件被多次引用的话,你的A会被重复定义

显然语法上错了。

只不过有了这个#ifndef的条件编译,所以能保证你的头文件只被引用一次,不过也许还是会岔子(想想什么意思!!!很重要!!!),

但若多个c文件包含这个头文件时还是会出错的,因为宏名有效范围仅限于本c源文件,所以在这多个c文件编译时是不会出错的,

但在链接时就会报错,说你多处定义了同一个变量,

暂时的小结:

对变量而言,如果你想在本源文件中使用另一个源文件的变量,就需要在使用前用extern声明该变量,或者在头文件中用extern声明该变量;

对函数而言,如果你想在本源文件中使用另一个源文件的函数,就需要在使用前用声明该变量,声明函数加不加extern都没关系,所以在头文件中函数可以不用加extern。

0.
extern修饰变量的声明。

举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。

这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。

这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。

还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。

对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。

1.
extern修饰函数声明。

从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。

如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。

就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。

使用extern和包含头文件来引用函数有什么区别呢?

extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这大概是KISS原则的一种体现吧!

这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

2.
此外,extern修饰符可用于指示C或者C++函数的调用规范。

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。

这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。

主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

其他需要注意的:
extern数组 和 extern 指针

数组名代表了存放该数组的那块内存,它是这块内存的首地址。

这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。

数组名跟枚举常量一样,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。

注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址。这就是数组名属于符号常量的意义所在。

由于数组名是一种符号常量,它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不是左值,那么,数组名永远都不会是指针!

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

例如有定义

char a[14];

char * p;

char * q;

void foo(char * pt)

{

};

考虑以下几种赋值:

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

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

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

再看这几种调用:

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

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

关于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。

上面这个很经典的例子是别人Blog中的,我下面说一下我个人的认识:

1.
首先我们区分一下,a.c中的g_i称为“数组g_i”,b.c中的g_i称为“指针g_i”。数组g_i是一个什么样的东西呢?

他其实就是一个符号,学过编译原理的同学知道,数组g_i是会被变成一个具体的值得,比如一段文本“g_i[20]”就变成了“0x0a090807[20]”。

2.
指针g_i,它本身只一个指针变量,这个变量是用来存放一个地址,指向内存中的某个区域,而且,在计算机中,这个指针变量必然需要一个空间去存储吧?

于是,指针g_i是一个指针变量,这个变量有自己的地址,这个变量也有自己的值。

3.
现在需要注意一点,在b.c中,有这么一句话, extern int * g_i;这个是什么?这是一个声明!这句话执行完后,有指针g_i这个变量吗?

没有!有的只是编译器知道,这是一个符号,具体被替换成什么?不知道!要去别的文件中寻找。

也就是说,指针变量在这个时候没有确切的内存空间,只有这么一个符号。


  1. “连接器的行为实际上是把指针a自身的地址定位到了另一个.c文件中定义的数组首地址之上”的意思是什么?

就是把这个extern int * g_i这个符号用a.c中的“0x0a090807”(假设)来替代,于是在b.c中所有g_i都被替换成了“0x0a090807”(文本级别)。

5.
那现在可以知道了,在b.c中,实际上指针g_i和数组g_i一样,都代表内存中数组的首地址。

这个时候,在b.c中,虽然没有定义g_i这个指针变量(之前仅仅是声明),但是这个指针变量“有其空间”,它里面有内容(实际上是数组g_i的内容),“可以指向”一个具体的内存单元。

6.
假设数组g_i被替换成134518852(文本级别),那么“指针g_i”这个变量的地址,也就是134518852。

虽然没有定义(开辟新内存)“指针g_i”这个变量,但是编译器什么的不知道呀,他认为,“指针g_i”就是一个变量的声明,具体真的有没有,我不关心。于是可以进行 &g_i。

值是多少呢?134518852。

7.
因为你是用%d来输出的,原文是这样“printf(“g_i = %d/n”, g_i);”注意这里没有取地址,那也就是按照一个整型来去输出,输出是“1”也就在意料之中了。

(想一想,虽然程序中输出了“g_i=1”,但是实际上真的g_i=1吗?

平时我们输出是这么进行的:

int n = 5;

printf(“%d\n”,n);

现在是这个样子的:printf(“g_i = %d/n”, g_i);

类比:

int n = 5;

printf(“%d\n”,n);

上面的这句话的意思是,输出从&n这个地址开始的四个字节的长度,应该是几呢,很明显是5。

如果把g_i和这个n类比,你能得到什么结论呢?

资料来源:

  1. http://baike.baidu.com/
  2. http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html
  3. http://www.jianshu.com/p/5d2eeeb93590
  4. http://blog.csdn.net/liu1028701143/article/details/7359381
  5. http://blog.csdn.net/hxg130435477/article/details/4012686
  6. http://www.cnblogs.com/growup/archive/2011/10/19/2217354.html
  7. http://blog.csdn.net/hxg130435477/article/details/4012686#reply
  8. http://songpengfei.iteye.com/blog/1100239
  9. http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html
  10. http://www.jb51.net/article/62351.htm
  11. http://baike.baidu.com/link?url=eYUHpseW-Ob2WmMTG43U-oR2nf4Hq2yaDrn0DJe7mYDUPhVA4Da5fKrrCsjkSykTirEJw-Q_8pPBYVhZaJluya
  12. http://baike.baidu.com/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值