C++中的extern “C” {}

1、 C++ 项目中的extern “C” {}

// 在用C++的项目源码中,经常会不可避免的会看到下面的代码:

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

它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试 or 笔试中。

下面我就从以下几个方面来介绍它:
1、#ifdef _cplusplus/ #endif _cplusplus 及发散 
2、extern "C"
  2.1、extern关键字
  2.2、"C"
  2.3、小结extern "C"
3、C和C++互相调用
  3.1、C++的编译和连接
  3.2、C的编译和连接
  3.3、C++中调用C的代码
  3.4、C中调用C++的代码
4、C和C++混合调用特别之处函数指针

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 语言中不支持 extern "C" 声明,
如果你明白 extern "C" 的作用就知道在 C 中也没有必要这样做,这就是条件编译的作用!
在 .c 文件中包含了 extern "C" 时会出现编译时错误。
既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。
还记得腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源 web 服务器 : Mongoose 的头文件 mongoose.h 中的一段代码):

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/*...................  * do something here  *................... */
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* MONGOOSE_HEADER_INCLUDED */
然后叫你说明上面宏#ifndef/#endif的作用?
为了解释一个问题,我们先来看两个事实:
这个头文件 mongoose.h 可能在项目中被多个源文件包含(#include "mongoose.h"),而对于一个大型项目来说,
这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中mongoose.h可能会被 #include 两次
(如,a.h 头文件包含了 mongoose.h,而在 b.c文件中 #include a.h和 mongoose.h)----这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立。

为了解决这个问题,上面代码中的
#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
/*……………………………*/
#endif /* MONGOOSE_HEADER_INCLUDED */
就起作用了。
如果定义了 MONGOOSE_HEADER_INCLUDED,#ifndef/#endif 之间的内容就被忽略掉。
因此,编译时第一次看到 mongoose.h 头文件,它的内容会被读取且给定 MONGOOSE_HEADER_INCLUDED 一个值。
之后再次看到 mongoose.h 头文件时,MONGOOSE_HEADER_INCLUDED 就已经定义了,mongoose.h 的内容就不会再次被读取了。

2、extern “C”

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

2.1、extern关键字

在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。
首先来一个例子:
//file1.c:     int x=1;     int f(){do something here}
//file2.c:     extern int x;     int f();     void g(){x=f();}
在 file2.c 中 g() 使用的 x 和 f() 是定义在 file1.c 中的。
extern 关键字表明 file2.c 中 x,仅仅是一个变量的声明,其并不是在定义变量 x,并未为x分配内存空间。
变量 x 在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
但是可以声明多次,且声明必须保证类型一致,如:
//file1.c:     int x=1;     int b=1;     extern c;
//file2.c:     int x;// x equals to default of int type 0     int f();     extern double b;     extern int c; 
在这段代码中存在着这样的三个错误:
x被定义了两次
b两次被声明为不同的类型
c被声明了两次,但却没有定义

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

2.2、”C”

典型的,一个 C++ 程序包含其它语言编写的部分代码。
类似的,C++ 编写的代码片段可能被使用在其它语言编写的代码中。
不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。
例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。
为了使它们遵守统一规则,可以使用 extern 指定一个编译和连接规约。
例如,声明 C 和 C++ 标准库函数 strcpy(),并指定它应该根据C的编译和连接规约来链接:
extern "C" char* strcpy(char*,const char*);
注意它与下面的声明的不同之处:
extern char* strcpy(char*,const char*);

下面的这个声明仅表示在连接的时候调用strcpy()。
extern "C" 指令非常有用,因为 C 和 C++ 的近亲关系。
注意:
   extern "C" 指令中的 C,表示的一种编译和连接规约,而不是一种语言。

C 表示符合 C 语言的编译和连接规约的任何语言,如 Fortran、assembler 等。
还有要说明的是,extern "C" 指令仅指定编译和连接规约,但不影响语义。

例如在函数声明中,指定了 extern "C",仍然要遵守 C++ 的类型检测、参数转换规则。
再看下面的一个例子,为了声明一个变量而不是定义一个变量,你必须在声明时指定 extern 关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。
如果你有很多语言要加上 extern "C",你可以将它们放到extern "C"{ }中。

2.3、小结extern “C”

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

3、C和C++互相调用

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

3.1、C++的编译和连接

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

void print(int i);
void print(char c);
void print(float f);
void print(char* s);
编译为:
_print_int
_print_char
_print_float
_pirnt_string
这样的函数名,来唯一标识每个函数。
注:
    不同的编译器实现可能不一样,但是都是利用这种机制。
所以当连接是调用 print(3)时,它会去查找 _print_int(3) 这样的函数。
下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

3.2、C的编译和连接

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

3.3、C++中调用C的代码

假设一个 C 的头文件 cHeader.h 中包含一个函数 print(int i),为了在C++中能够调用它,
必须要加上 extern 关键字(原因在 extern 关键字那节已经介绍)。
它的代码如下:
#ifndef C_HEADER
#define C_HEADER
    extern void print(int i);
#endif
C_HEADER 相对应的实现文件为 cHeader.c 的代码为:
#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

现在 C++ 的代码文件 C++.cpp 中引用 C 中的 print(int i) 函数:
extern "C"{
    #include "cHeader.h"
}
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

3.4、C 中调用 C++ 的代码

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

#ifndef CPP_HEADER
#define CPP_HEADER
    extern "C" void print(int i);
#endif //CPP_HEADER
相应的实现文件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);
    return 0;
}
注意在 C 的代码文件中直接 #include "cppHeader.h" 头文件,编译出错。
而且如果不加 extern int print(int i) 编译也会出错。

4、C和C++混合调用特别之处函数指针

当我们 C 和 C++ 混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一种语言定义的函数。
如果 C 和 C++ 共享同一种编译和连接、函数调用机制,这样做是可以的。
然而,这样的通用机制,通常不会假定它存在,因此我们必须小心地确保函数以期望的方式调用。
而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

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
//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
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)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}
注意:
    typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,
这种函数指针指向的函数有这样的特征:
    返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。

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

工程中 C 文件调用 C++ 函数(https://www.cnblogs.com/cfzhang/p/25bbffa718da778a213c94ebd6971528.html)

1、描述

C调用C++链接库:
 1.编写C++代码,编写函数的时候,需要加入对C的接口,也就是extern “c"
 2.由于C不能直接用"class.function”的形式调用函数,所以C++中需要为C写一个接口函数。例如本来要调用student类的talk函数,就另外写一个cfun(),专门建一个student类,并调用talk函数。而cfun()要有extern声明
 3.我在练习中就使用在C++头文件中加extern ”c”的方法。而C文件要只需要加入对cpp.h的引用
 4.详细见如下代码:
  student是一个类,里边有talk函数,就输出一句话而已
  cpp.cpp与cpp.h是两个C++代码,包含对C的接口
  最后用C代码:helloC.c来测试结果

代码

1、student.cpp

#include <iostream>
using namespace std;
#include "student.h"
void student::talk()
{
    cout<<"I am Kenko"<<endl;
}

2、student.h

#ifndef _STUDENT_
#define _STUDENT_
class student {
    public:
        void talk();
};
#endif

3、cpp.h

#ifndef _CPP_
#define _CPP_
    #include "student.h"

#ifdef __cplusplus
    extern "C" {
#endif
    void helloCplusplus();
#ifdef __cplusplus
}
#endif
#endif

4、cpp.cpp

#include <iostream>
using namespace std;
#include "cpp.h"
student stu;
void helloCplusplus() {
    cout << "hello C++" << endl;
    stu.talk();
}

void helloCplusplus2() {
    cout << "hello C++" << endl;
}

5、helloC.c

#include <stdio.h>
#include "cpp.h"
int main() {
    helloCplusplus();
    return 0;
}

3、怎样编译运行

linux 编译:
  g++ -fPIC -shared -g -o libcccall.so cpp.cpp student.cpp
  g++ -g helloC.c ./libccall.so -o main

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注