条件编译和extern "C"的用法总结

      先看下面这个例子:

#ifdef __cplusplus
extern "C"{
#endif

/*the something here*/

#ifdef __cplusplus
}
#endif

        它到底有什么用呢,这样的问题会出现在面试or笔试中,我觉得作为一个学C++的,应该了解一下。下面从下面几个方面来介绍它:

     1、条件编译:#ifdef _cplusplus / #endif _cplusplus 用法

     2extern "C" 用法

  • 2.1extern关键字
  • 2.2"C"
  • 2.3、小结extern "C"

     3CC++互相调用

  • 3.1C++的编译和连接
  • 3.2C的编译和连接
  • 3.3C++中调用C的代码
  • 3.4C中调用C++的代码

      4CC++混合调用特别之处函数指针

      5、几道的题目(问题)

1条件编译:#ifdef _cplusplus/#endif _cplusplus 用法

         在介绍extern "C"之前,我们来看下#ifdef _cplusplus/#endif _cplusplus的作用。很明显#ifdef/#endif#ifndef/#endif用于条件编译,#ifdef _cplusplus/#endif _cplusplus表示如果定义了宏_cplusplus,就执行#ifdef/#endif之间的语句,否则就不执行。

         在这里为什么需要#ifdef _cplusplus/#endif _cplusplus呢?这是C++的条件编译,是四种预编译命令之一(头文件包含,条件编译,宏替换和布局控制)!

         既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。为了防止头文件重复包含,在C++中,经常会使用到条件编译。

        现在假设有如下场景:现有A, B, C三个头文件,它们之间的相互关系如下:文件BC中会直接包含文件A,而文件C中同时会包含文件B,这样通过直接及间接包含,文件C包含了文件A两次, 通常编译的时候就会提醒变量重复定义等问题,这就是有重复包含导致的。C++中通过条件编译解决了这个问题,在文件C中第一次包含文件A时,不会有问题, 而当第二次包含时,条件编译起了作用,组织了重复包含,从而确保了程序的正确性。

        下面是一个C++头文件,该头文件会被多个其他头文件包含,这里使用条件编译解决问题。

#ifndef TOKEN_H
#define TOKEN_H

#include <string>

using namespace std;

enum TokenType{NUMBER,OP,LPAREN,RPAREN,ERROR};

class Token
{
private:
  //save the value and tokentype pair
  string value;
  TokenType tokenType;
public:
  Token();
  //get method for private memeber
  string getValue();
  Tokentype getTokenType();

  //set method for private member
  void setValue(string v);
  void setTokenType(TokenType t);
};

#endif     //TOKEN_H

2extern "C"

        首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

2.1extern关键字

        以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。在C++中用extern关键字来声明变量和函数;对于函数而言,由于函数的声明如“extern int method();”与函数定义“int method(){}”可以很清晰的区分开来,为了简便起见,可以把extern关键字省略,于是有了我们常见的函数声明方式“int method();”,然而对于变量并非如此,变量的定义格式如“int i;”,声明格式为“extern int i;”,如果省略extern关键字,就会造成混乱,故不允许省略。

        如下所示,在file2.cppf()使用的xf()是定义在file1.h中的。extern关键字表明file2.cppx,仅仅是一个变量的声明,其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。但是可以声明多次,且声明必须保证类型一致。注意,在file2.cpp中需包含file1.h

//file1.h
int x=1;

int f()
{
   return x++;
}
//file2.cpp
#include "file1.h"

extern int x;
int f();

void main()
{
  x=f();
}

          经本人测试,上面的程序可以省略掉“extern int x”“int f()”,而只包含"#include "file1.h""。但是反过来不行,也就是:不可以省略掉"#include "file1.h"",而仅仅只包含“extern int x”“int f()”,这样file2.cpp会找不到变量x和函数f()

条件编译和extern "C"的用法总结_第1张图片

                                                                图1  file2.cpp中只包含了#include "file1.h"

         被extern "C"限定的函数或变量是extern类型的回到extern关键字,externC/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块 A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2.2"C"

        典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。

        为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明CC++标准库函数strcpy(),并指定它应该根据C的编译和连接规约来链接:

extern "C" char* strcpy(char*,const char*);

        注意它与下面的声明的不同之处:

extern char* strcpy(char*,const char*);

        上面的这个声明仅表示在连接的时候调用strcpy()

         extern "C"指令非常有用,因为CC++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortranassembler等。

        还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。

        综上所述,为了声明一个变量而不是定义一个变量,你必须在声明时指定extern关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。如果你有很多语言要加上extern "C",你可以将它们放到extern "C"{ }中。

2.3、小结extern "C"

        通过上面两节的分析,我们知道extern "C"的真实目的是实现CC++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在C++的代码中就可以调用C的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言

        下面再给出一个具体的实例进行分析:

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

(3)加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中函数声明了fooextern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然
     所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。):实现C++C及其它语言的混合编程。

3CC++互相调用

       我们既然知道extern "C"是实现的类CC++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白CC++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。

3.1C++的编译和连接

        C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

1    voidprint(inti);
2    voidprint(charc);
3    voidprint(floatf);
4    voidprint(char* s);

编译为:
1    _print_int
2    _print_char
3    _print_float
4    _pirnt_string

        这样的函数名,来唯一标识每个函数,这就是C++中的名字修饰,也有很多优势,可以确保类型安全连接。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找 _print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(一种接口多种实现),如果硬要认为重载是多态,它顶多是编译时多态

        C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

3.2C的编译和连接

        C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的 print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。

3.3C++中调用C的代码    

        假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:

//cHeader.h
#ifndef C_HEADER
#define C_HEADER
	 
extern void print(int i);
#endif

       相对应的实现文件为cHeader.c的代码为:

//cHeader.c
#include <stdio.h>
#include "cHeader.h"
voidprint(int i)
{
  printf("cHeader %d\n",i);
}

      现在C++的代码文件C++.cpp中引用C中的print(int i)函数:

//C++.cpp
extern "C"{
    #include "cHeader.h"
}
	 
int main(int argc,char** argv)
{
    print(3);
    return0;
}
       linux执行上述文件的命令为:

       首先执行gcc -c cHeader.c,会产生cHeader.o

       然后执行g++ -o C++ C++.cpp cHeader.o 

       执行程序输出:


         如果把“extern "C"”删除,会出现无法解析的外部符号的错误,可见其作用。

        在C++.cpp文件中也可以不用包含函数声明的文件,即“extern "C"{#include"cHeader.h"}”,而直接改用extern "C" void print(int i)的形式。那C++.cpp是如何找到C中的print函数,并调用的呢?

        那是因为我们首先通过gcc -c cHeader.c 这个命令,产生了一个cHeader.c的目标文件。然后我们通过执行g++ -o C++ C++.cpp cHeader.o这个命令,该命令中指明了要链接的目标文件:cHeader.o,所以C++.cpp中只需要说明哪些函数(比如该题中的void print(int i))需要以C的形式调用,然后去其目标文件(这里为cHeader.o)中找该函数即可。

        注:“.o文件为目标文件,类似于windows下的obj文件。

       下图给出了直接以函数的形式包含的形式:

条件编译和extern "C"的用法总结_第2张图片

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

extern "C"
{
   #include "cExample.h"
}
       而在C语言的头文件(.h文件)中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
       如果C++调用一个C语言编写的动态链接库(window的动态链接库为.DLL格式,linux的动态链接库为.so格式)时,当包括.DLL(.so)的头文件或声明接口函数时,应加extern "C" {}

3.4C中调用C++的代码

       现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:

//cppHeader.h
#ifndef CPP_HEADER
#define CPP_HEADER
	 
extern "C" void print(int i);

#endif
       相应的实现文件 cppHeader.cpp 文件中代码如下:

//cppHeader.cpp
#include "cppHeader.h"	 
#include <iostream>
using namespace std;

void print(int i)
{
    cout << "cppHeader " << i << endl;
}
       在 C 的代码文件 c.c 中调用 print 函数:
extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return0;
}
       注意在 C 的代码文件中直接 #include "cppHeader.h" 头文件,编译出错 , 原因为:当以头文件的形式包含 cppHeader.h 时,会将 cppHeader.h 的内容展开,而 cppHeader.h 中包含 “extern "C"” 关键字,而 c 语言中没有这种用法,所以会出现编译错误。而且如果不加 extern void print(int i) 编译也会出错。

     下面给出linux下的执行那个过程,程序如下图所示:

条件编译和extern "C"的用法总结_第3张图片

       编译、链接过程如下:

1)首先执行命令:g++ cppHeader.cpp -fpic -shared -g -o cppHeader.so

该命令是将cppHeader.cpp编译成动态连接库,其中编译参数的解释如下:

-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件

-fpic:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的,所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。

-g:为调试

2)然后再执行命令:gcc c.c cppHeader.so -o cmain

该命令是编译c.c文件,同时链接cppHeader.so文件,然后产生cmain的可执行文件。

(3)最后执行命令:./cmain 来执行该可执行程序

总结:在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

4CC++混合调用特别之处函数指针

        当我们CC++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一种语言定义的函数。如果CC++共享同一种编译和链接、函数调用机制,这样做是可以的。然而,这样的通用机制,我们必须小心地确保函数以期望的方式调用。

        而且当指定一个函数指针的编译和链接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

typedef int (*FT)(const void*,const void*);//style of C++

extern "C"{
    typedef int (*CFT)(const void*,const void*);//style of C
    void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}
 	 
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C

extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);//style of C

int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C
 	 
void f(char* v,int sz)
{
  qsort(v,sz,1,&compare);//error,as qsort is style of C,but compare is style of C++
  qsort(v,sz,1,&ccomp);//ok

  isort(v,sz,1,&compare);//ok
  isort(v,sz,1,&ccopm);//error,as isort is style of C++,but ccomp is style of C
}
       注意: typedef int (*FT) (const void* ,const void*) ,表示定义了一个函数指针的别名 FT ,这种函数指针指向的函数有这样的特征:返回值为 int 型、有两个参数,参数类型可以为任意类型的指针(因为为 void* )。最典型的函数指针的别名的例子是,信号处理函数 signal ,它的定义如下:

       最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:

       typedef  void(*HANDLER)(int);

       HANDLER signal(int,HANDLER);

      上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:
      void  (*signal(int,void(*)(int) ))(int)
      比较之后可以明显的体会到typedef的好处。

5、几道的题目(问题)

问题1extern 变量
  在一个源文件里定义了一个数组:char a[6];
  在另外一个文件里用下列语句进行了声明:extern char* a;
  请问,这样可以吗?
答案与分析:
  1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]
  2)、例子分析如下,如果a[] = "abcd",则外部变量a=0x61626364 (abcdASCII码值)*a显然没有意义,显然a指向的空间(0x61626364)没有意义,易出现非法内存访问。
  3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。
  4)extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
问题2extern 函数1
  常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
答案与分析:
  如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:  extern int f();和int f();
  当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。
问题3extern 函数2
  当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?
答案与分析:
  目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。
  宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
问题4extern “C”
  在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
答案与分析:
  C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
  下面是一个标准的写法:

//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
  #endif 
  #endif 
 …
 …
 //.h文件结束的地方
 #ifdef __cplusplus
 #if __cplusplus
}
#endif
#endif 

问题5:某企业曾经给出如下的一道面试题:为什么标准头文件都有类似以下的结构?

#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
 #endif 
 //some code
 #ifdef __cplusplus
}
#endif
#endif 

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

#ifdef __cplusplus
extern "C" {
  #endif
  //some code 
  #ifdef __cplusplus
}
#endif

的作用又是什么呢?

        #ifdef __cplusplus的作用是如果程序是C++,则启用extern "C"extern "C" 的作用,包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。第一重含义:该函数可能在别的源文件中定义,链接时可能需要去其他的模板寻找其定义;第二重含义:以C语言的方式编译该函数。

你可能感兴趣的:(条件编译和extern "C"的用法总结)