C 中 extern 和 static

来源:互联网 发布:js函数柯里化 编辑:程序博客网 时间:2024/05/21 06:56

C语言中:

extern :extern(外部引用)可以置于变量或者函数前,以标示变量或函数的定义在别的文件中,在一个文件中用到的extern这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。注意,只有其他文件中的全局变量才能被其他文件所extern。

extern int val;

注:

      此处的函数类型可以省略,即extern val;

      因为extern的作用就是告诉编译器这个变量是在其他文件中定义的(是外援)。编译器是相信自己人的,所以在编译的时候要是看到val变量时会认为它是存在,不会报错。只有在链接的时候链接器才会去其它obj文件中寻找val变量的定义(地址),找到则顺利链接,否则报错。因为编译器只需要知道extern所声明变量的名字就可以了,所以extern int val 可以写成 extern val(即省略变量类型)。 

      至于 extern“C”的用法, 一般认为属于 C++的范畴。extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,但在C++中为了兼容面向过程语言(C),扩展了extern功能。


static : 在C中,static(静态的)主要有两个作用: 修饰变量和修饰函数。在C++中,static功能得到了扩展。

第一个作用:修饰变量。变量又分为局部和全局变量,但它们都存在内存的静态区(全局区)。

●静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用 extern声明也没法使用他。准确地说作用域是从定义之处开始,到文件结尾处结束,在定义之处前面的那些代码行也不能使用它。想要使用就得在前面再加 extern *** 或干脆定义在文件顶端。

●静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了。由于被 static修饰的变量总是存在于内存的静态区所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。

例:

第二个作用:修饰函数。函数前加 static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。

关键字 static有着不寻常的历史。起初,在 C 中引入关键字static是为了表示退出一个块后仍然存在的局部变量。随后,static 在 C 中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用 static关键字来表示这第二种含义。

static 与 extern 联系:

      加了static修饰的全局变量或函数,无法在使用extern在其他源文件中使用。

当然,C++里对 static赋予了第三个作用,修饰类的成员变量和成员函数成为静态成员变量和静态成员函数。

 


 

      对于静态成员变量,无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。

      静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。


引用: http://www.vckbase.com/document/viewdoc/?id=1720


参考资料:

变量的声明与定义以及关键字extern的用法

变量的声明与定义:

A of a variable allocates storage for the variable and may also specify an initial value for the variable. There must be one and only one definition of a variable in a program.

A declaration makes known the type and name of the variable to the program. A definition is also a declaration: When we define a variable, we declare its name and type. We can declare a name without defining it by using the extern keyword. A declaration that is not also a definition consists of the object's name and its type preceded by the keyword extern:


在很多编码规则都有这样一条,变量与函数的声明放在头文件中,它们相应的定义放在源文件中。
可是很多人尤其是初学者对声明和定义分辨不清,觉得声明即是定义,定义即是声明。造成这种混淆并不奇怪,因为变量在大多数情况下,声明的同时就定义了。
e.g.
//main1.c
int var; //声明的同时也对其定义了
int main(int argc, char **argv[])
{
 ...
}
而如下是纯粹的声明,没有包含定义
e.g.
//main2.c
extern int val; //仅仅声明这是个外部变量,extern的具体作用在后面讲。
extern int val1 = 1; // 声明并定义外部变量
int main(int argc, char **argv[])
{
 ...
}
而函数的声明与定义也是类似的。而且要更容易区分。
e.g.
//main3.c
void func() //即是声明也是定义
{
 ...
}
int main(int argc, char **argv[])
{
 ...
 func(); //可以编译链接
 ...
}
仅仅对函数声明的话,就不需要写函数体,只要声明函数类型以及函数形参的信息就可以了。
e.g.
//main4.c
void func(char); //仅仅是声明
int main(int argc, char **argv[])
{
 ...
 func(); //编译可以顺利通过,如果后面没有函数定义的话链接将会失败
 ...
}
void func(char *str) //函数定义,如果此文件中没有这部分定义的话,main中可以编译,但不能链接
{
 ...
}
之所以函数的定义与声明比较好区分是因为函数必须得有函数体编译器才能给它分配空间,而变量仅仅需要要个名字和它的类型编译器就
可以分配空间给它。
所以可以这样说,声明只是告诉编译器声明的变量和函数是存在的,但并没有真正分配空间给它,所以当后面的代码
用到前面声明的变量或函数时,编译器在编译的时候不会报错,但是在链接的时候会报错,因为链接的时候编译器会去
寻找这些变量和函数的内存地址,因为只声明了但没定义,链接器当然找不到它们了,所以就报错了。而对它们进行定义
了的话编译器就会给它们分配空间,它们就有自己的地址了,这时就能链接了。
所以定义是要分配空间的,所以定义只能有一次。而声明不分配空间,你可以声明多次。
extern关键字
我今天写这边短文就是因为有人问我extern的用法,他们知道怎么样extern,但是对extern原理不清楚,知其然不知其所以然。
上面main2.c中的extern int val; 它的作用就是告诉编译器这个变量是在其他文件中定义的(是外援),要是在本文件中
看到它的名字千万别奇怪。编译器是相信自己人的,所以在编译的时候要是看到val变量时会认为它是存在,不会报错。只有在
链接的时候链接器才会去其它obj文件中寻找val变量的定义(地址),找到则顺利链接,否则报错。因为编译器只需要知道extern所
声明变量的名字就可以了,所以extern int val 可以写成 extern val(即省略变量类型)。
e.g.
//main2.c
extern int val; //声明val是个外部变量,也可以写成extern val;
int main(int argc, char **argv[])
{
 val = 10;
 ...
}
//another.c
int val;
把这两个文件一起编译链接是没有问题的。
并不是所有的变量都能用extern声明,只有全局变量并且没有被static 声明的变量才能声明为extern.
所以如果你不想自己源文件中全局的变量被其他文件引用,你就可以给变量加上static声明
e.g.
extern val; //链接的时候会报错,因为val被声明为static.
int main(int argc, char **argv[])
{
 val = 10;
 ...
}
//another.c
static int val;
这也是static全局变量和非static全局变量的唯一的一个区别。  

 

 


 


 extern "C":

C++中extern "C"含义深层探索

1.引言

  C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

  2.从标准头文件说起

  某企业曾经给出如下的一道面试题:

  面试题
  为什么标准头文件都有类似以下的结构?

#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */


  分析
  显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。

  那么

#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif


  的作用又是什么呢?我们将在下文一一道来。


  3.深层揭密extern "C"

  extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

  被extern "C"限定的函数或变量是extern类型的;

  extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:

  extern int a;


  仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

  通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

  与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

  被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;

  未加extern “C”声明时的编译方式

  首先看看C++中对类似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++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

  未加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这样的符号!

  加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及其它语言的混合编程。
  明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。

 

4.extern "C"的惯用法

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

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


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

  笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#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"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}


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

  (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
  笔者编写的C引用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;
}


  如果深入理解了第3节中所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。


#pragma once 与 #ifndef 避免头文件被重复引用
2009-11-29 21:38

 

为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。
    方式一:
    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 声明、定义语句
    #endif


    方式二:

   #pragma once
    ... ... // 声明、定义语句

    #ifndef的方式受C/C++语言标准支持。它不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。
    当然,缺点就是如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,编译器却硬说找不到声明的状况——这种情况有时非常让人抓狂。
    由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。

    #pragma once一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。
    其好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。大型项目的编译速度也因此提高了一些。
    对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。

    #pragma once方式产生于#ifndef之后,因此很多人可能甚至没有听说过。目前看来#ifndef更受到推崇。因为#ifndef受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式却不受一些较老版本的编译器支持,一些支持了的编译器又打算去掉它,所以它的兼容性可能不够好。一般而言,当程序员听到这样的话,都会选择#ifndef方式,为了努力使得自己的代码“存活”时间更久,通常宁愿降低一些编译性能,这是程序员的个性,当然这是题外话啦。

    还看到一种用法是把两者放在一起的:

    #pragma once
    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 声明、定义语句
    #endif

    看起来似乎是想兼有两者的优点。不过只要使用了#ifndef就会有宏名冲突的危险,也无法避免不支持#pragma once的编译器报错,所以混用两种方法似乎不能带来更多的好处,倒是会让一些不熟悉的人感到困惑。

    选择哪种方式,应该在了解两种方式的情况下,视具体情况而定。只要有一个合理的约定来避开缺点,我认为哪种方式都是可以接受的。而这个已经不是标准或者编译器的责任了,应当由程序员自己或者小范围内的开发规范来搞定。

    btw:我看到GNU的一些讨论似乎是打算在GCC 3.4(及其以后?)的版本取消对#pragma once的支持。不过事实上,我手上的GCC 3.4.2和GCC 4.1.1仍然支持#pragma once,甚至没有deprecation warning,倒是GCC2.95会对#pragma once提出warning。
    VC6及其以后版本亦提供对#pragma once方式的支持,这一特性应该基本稳定下来了。


链接指示符extern C
如果程序员希望调用其他程序设计语言尤其是C 写的函数那么调用函数时必须
告诉编译器使用不同的要求例如当这样的函数被调用时函数名或参数排列的顺序可能
不同无论是C++函数调用它还是用其他语言写的函数调用它
程序员用链接指示符linkage directive 告诉编译器该函数是用其他的程序设计语言
编写的链接指示符有两种形式既可以是单一语句single statement 形式也可以是复
合语句compound statement 形式
// 单一语句形式的链接指示符
extern "C" void exit(int);
// 复合语句形式的链接指示符
extern "C" {
int printf( const char* ... );
int scanf( const char* ... );
}
// 复合语句形式的链接指示符
extern "C" {
#include <cmath>
}
链接指示符的第一种形式由关键字extern 后跟一个字符串常量以及一个普通的函数
声明构成虽然函数是用另外一种语言编写的但调用它仍然需要类型检查例如编译器
会检查传递给函数exit()的实参的类型是否是int 或者能够隐式地转换成int 型
多个函数声明可以用花括号包含在链接指示符复合语句中这是链接指示符的第二种形
式花招号被用作分割符表示链接指示符应用在哪些声明上在其他意义上该花括号被忽
略所以在花括号中声明的函数名对外是可见的就好像函数是在复合语句外声明的一样
例如在前面的例子中复合语句extern "C"表示函数printf()和scanf()是在C 语言中写的
函数因此这个声明的意义就如同printf()和scanf()是在extern "C"复合语句外面声明的
一样
当复合语句链接指示符的括号中含有#include 时在头文件中的函数声明都被假定是用
链接指示符的程序设计语言所写的在前面的例子中在头文件<cmath>中声明的函数都是C
函数
链接指示符不能出现在函数体中下列代码段将会导致编译错误
int main()
{
// 错误: 链接指示符不能出现在函数内
extern "C" double sqrt( double );
305 第七章函数
double getValue(); //ok
double result = sqrt ( getValue() );
//...
return 0;
}
如果把链接指示符移到函数体外程序编译将无错误
extern "C" double sqrt( double );
int main()
{
double getValue(); //ok
double result = sqrt ( getValue() );
//...
return 0;
}
但是把链接指示符放在头文件中更合适在那里函数声明描述了函数的接口所属
如果我们希望C++函数能够为C 程序所用又该怎么办呢我们也可以使用extern "C"
链接指示符来使C++函数为C 程序可用例如
// 函数calc() 可以被C 程序调用
extern "C" double calc( double dparm ) { /* ... */ }
如果一个函数在同一文件中不只被声明一次则链接指示符可以出现在每个声明中它
也可以只出现在函数的第一次声明中在这种情况下第二个及以后的声明都接受第一个声
明中链接指示符指定的链接规则例如
// ---- myMath.h ----
extern "C" double calc( double );
// ---- myMath.C ----
// 在Math.h 中的calc() 的声明
#include "myMath.h"
// 定义了extern "C" calc() 函数
// calc() 可以从C 程序中被调用
double calc( double dparm ) { // ...
在本节中我们只看到为C 语言提供的链接指示extern "C" extern "C"是惟一被
保证由所有C++实现都支持的每个编译器实现都可以为其环境下常用的语言提供其他链接
指示例如extern "Ada"可以用来声明是用Ada 语言写的函数extern "FORTRAN"用来
声明是用FORTRAN 语言写的函数等等因为其他的链接指示随着具体实现的不同而不同
所以建议读者查看编译器的用户指南以获得其他链接指示符的进一步信息

原创粉丝点击