【Cpp】C和C++混合编程

10 篇文章 0 订阅

一、混合编程杂绪

1.1 为什么需要混合编程

  1. Cpp是从C演变过来的,C有很多优秀成熟项目和库,没必要在C++中重写,C++程序可以直接调用调用
  2. 庞大项目划分后一部分适合用C(底层),一部分适合用C++(中间层、上层)

1.2 混合编程的支撑

  1. 编译型程序的编译过程:源文件->目标(库)文件->可执行程序->镜像文件
  2. 任何编程语言执行时都必须是可执行程序,所以都必须先被编译成目标文件
  3. 混合编程的“混合”操作发生在链接这一步

1.3 C++和C混合编程出现的问题

C++和C都是编译型语言,互相混合相对容易。但是:C++支持函数名重载,而C不支持,因此编译器生成目标文件时,函数名在目标文件中的临时内部名称规则不同,导致链接时符号对不上,如下所示:

创建测试文件:

    touch cadd.c cppadd.cpp add.h Makefile

源文件和头文件的内容如下,其中cadd.cappadd.c的内容都是一致的,均是库函数中声明的函数的定义
clipboard_20200215044304.png
我们分别把.c源文件和.cpp源文件做成静态链接库然后把库进行反汇编

dec:
	objdump -d libcadd.a > libcadd.i
	objdump -d libcppadd.a > libcppadd.i

clipboard_20200215051018.png
对比得到的反汇编文件,可以发现同一个函数分别在gccg++下编译后生成的二进制代码其实是一样的。而临时内部名称是不同的:在gcc中,函数名保持不变;而g++中,还在函数名后增加了参数类型(add -> addii),如下图所示:
clipboard_20200215045941.png
这时如果用一个.c文件和g++编译制作的静态库进行gcc编译链接或用一个.cpp文件和gcc编译制作的库进行g++编译链接都会链接错误:

#include "add.h"
int main(void)
{
    int x = 1, y = 1;
    add(x, y);
    return 0;
}

clipboard_20200215053956.png
这就是因为主函数#include "add.h"中的add(预编译时直接替换相关内容)与静态库中的add用了不同的编译环境,导致标文件中的临时内部名称不同所致无法正确链接!

二、C和C++混合编程的解决方案

通过前面的研究我们可以知道C和C++本质上是可以混合编程的,但是生成的中间符号名称不同,所以链接器在进行链接时会出问题,那如何解决呢?

2.1 先谈谈__cplusplus

__cplusplus是C++中预定义的一个标准宏,为长整型,其值为cpp标准的年月;在C中是未进行定义的,可以用来检测编译环境

#include <stdio.h>

int main(void)
{
    #ifdef __cplusplus
    printf("G++ %ld.\n", __cplusplus);
    #else
    printf("GCC.\n");
    #endif

    return 0;
}

clipboard_20200215064211.png

2.2 extern "C"{}

使用extern "C"{}可以使{}中的内容用C的标准来编译,我们将1.3中的add.h中的内容改为:

#ifndef __ADD_H__
#define __ADD_H__

extern "C"
{

int add(int a, int b);

}

#endif

然后和cppadd.cpp一起制作成静态库然后反编译,我们可以发现虽然用的g++进行编译,反编译得到的文件中add的临时名称还是add,而不是_Z3addii
clipboard_20200215065948.png

2.3 解决方案

通过2.2我们可以知道在C++的头文件中只要把函数的声明放在extern "C"{}的大括号范围之内,就可以让g++在编译这个函数时生成中间符号名时按照C的规则而不是按照C++的规则,所以这样的函数就可以和C的库进行共同链接。但是,在C语言中,extern "C"{}是未被定义的
clipboard_20200215070522.png
所以我们将__cplusplus和extern “C”{}结合使用,通常有以下两种情况:

三、两种常见情况

3.1 同一个项目中C是库,C++是源码,C++调用C

这是最常见的情况,我们只需要在C的头文件中加上extern "C"的声明,在C++中直接包含头文件调用。后继直接用g++编译链接即可,如下所示(mian.cpp和C库均来自1.3):
clipboard_20200215073447.png
add.h中的内容更改如下:

#ifndef __ADD_H__
#define __ADD_H__

#ifdef __cplusplus
extern "C"
{
#endif

int add(int a, int b);

#ifdef __cplusplus
}
#endif

#endif

return.sh中内容如下:

#!/bin/sh

g++ main.c -lcadd -L. -o main.elf
./main.elf
echo $?

根据结果我们可以看到程序进行了成功的编译、链接、执行:
clipboard_20200215073854.png
如果我们同时有C和Cpp的源码,我们也是只需要在C的.h库函数中加入__cplusplusextern "C"{},对于Cpp的.hpp文件不做任何改动,最后统一进行g++编译即可

3.2 同一个项目中C++是库,C是源码,C调用C++

如果我们只有C++库而源码是C,我们最后只能用gcc编译链接,这带来了很多麻烦:

  1. g++gcc的编译时符号差异
  2. c++支持很多c并不支持的特性,如函数重载

而C++继承了C的所有特性,也就是说C编译时无法直接像C++一样使用特定的语句实现编译Cpp时按照C的规则。但是,我们可以用cpp写一层封装层,加上extern "C",用g++编译成静态库,这个时候我们在C源码中就可以直接调用我们的静态库,用gcc编译链接!

我们还是使用1.3中制作的静态库,我们检查发现此时libcppadd.aadd的临时符号是_Z3addii
clipboard_20200215080137.png
我们制作封装层,创建文件

touch cppaddwrapper.cpp cppaddwrapper.hpp

其中,cppaddwrapper.hpp中的内容如下:

#ifndef __CPP_ADDWRAPPER_HPP__
#define __CPP_ADDWRAPPER_HPP__

#ifdef __cplusplus
extern "C"
{
#endif

int addwrapper(int a, int b);

#ifdef __cplusplus
}
#endif

#endif

cppaddwrapper.cpp中的内容如下

#include "add.hpp"
#include "cppaddwrapper.hpp"

int addwrapper(int a, int b)
{
    add(a, b);
}

我们只需要在这个函数中对add进行调用即可,我们将封装层用g++ar制作成静态封装库,再进行反编译,结果如下:
clipboard_20200215084644.png
因为在addwrapper函数的声明中加了extern "C",故临时符号没有改变。由于用的是g++编译,而该头文件包含中的add.hppadd函数没有extern "C"声明,所以调用时使用的add的临时名称还是会变成_Z3addii,和静态库中的临时名称相匹配,所以在反汇编代码callq 1d <addwrapper+0x1d>中发生了跳转。第一个参数1d就是add函数的地址入栈,尖括号里面的是偏移地址,因为我们使用的是静态库,无需重定位,所以偏移地址也是0x1d

这个时候,我们就可以在我们自己的c文件中直接调用静态封装库中的函数,和封装库一起用gcc进行链接了!如下建立测试文件夹,将头文件和静态库移入文件夹中:
clipboard_20200215085510.png

main.c中的测试代码如下:

#include "cppaddwrapper.hpp"

int main(void)
{
    addwarpper(1, 1);
    return 0;
}

shell脚本中的内容如下:需要注意的是由于封装层的静态库是使用了C++库的,所以在链接的时候也需要用-lxx指定静态库

#!/bin/sh

gcc main.c -lcppaddwrapper -lcppadd -L. -o main.elf
./main.elf
echo $?

根据main函数的返回值我们可以知道程序成功地进行了编译链接执行
clipboard_20200215085827.png
在只有C++库而源码是C的情况下,使用封装层的技巧就是封装层使是用g++编译,但是只在封装头文件函数声明中加了extern "C",而调用的C++静态库函数的#include未加,因此调用的函数还是按C++的规则解析,制作库时可以实现连接。在C源码中调用封装库,巧妙避免了直接使用C++库由于临时符号不同导致的链接错误

  • 15
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ImageJ是一款基于Java语言开发的图像处理软件,而C++是一种编译型语言。在将它们混合编程时,需要使用Java Native Interface (JNI)工具来实现Java和C++之间的通信。 以下是一个简单的例子,展示了如何使用JNI在ImageJ中调用C++代码: 1. 创建C++代码文件 创建一个名为“example.cpp”的C++文件,并在其中添加以下代码: ``` #include <iostream> extern "C" { JNIEXPORT void JNICALL Java_example_callNative(JNIEnv *env, jobject obj) { std::cout << "Hello from C++!" << std::endl; } } ``` 此代码导出了一个名为“Java_example_callNative”的函数,它将在ImageJ中被调用。该函数会在控制台上输出一条消息。 2. 生成动态链接库 使用C++编译器将C++代码编译为动态链接库。在Linux系统中,可以使用以下命令: ``` g++ -shared -fPIC -o libexample.so example.cpp ``` 在Windows系统中,可以使用以下命令: ``` g++ -shared -o example.dll example.cpp ``` 这将生成一个名为“libexample.so”或“example.dll”的动态链接库。 3. 在ImageJ中调用C++代码 在ImageJ中创建一个Java类,并使用JNI调用C++代码。在该类中添加以下代码: ``` public class Example { static { System.loadLibrary("example"); } public native void callNative(); public static void main(String[] args) { Example ex = new Example(); ex.callNative(); } } ``` 此代码加载名为“example”的动态链接库,并声明了一个名为“callNative”的本地方法。该方法将调用在C++代码中定义的函数。 4. 运行程序 编译并运行Java程序。在控制台上应该会输出一条消息:“Hello from C++!”。 以上是一个简单的例子,展示了如何使用JNI在ImageJ中调用C++代码。实际上,JNI可以实现更复杂的Java和C++之间的交互。如果您需要更多的帮助,请参考JNI文档。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值