• C与C++混编


    介绍

    了解一下C与C++如何合作,gcc和g++编译出来的东西有什么区别。C++为了支持重载等特性,编译出来的符号和C是不一样的。

    每个公司都会有一些古老的库,几乎每个程序都在使用它,它可能是C写的,或者是C++写的,通常情况下,我们能做的就是调用里面的函数,而不能修改这个库,因为很多程序都在用它,你改了它就得测所有的程序,得不偿失。

    工具使用

    objdump是个好工具,可以用于查看.o文件的内容,也可以查看可执行文件的内容。主要是可以用来查看gccg++编译出来的符号有什么区别。

    查看符号表
    objdump -t foo.o

    查看正文段
    objdump -S foo.o

    查看所有session
    objdump -D foo.o

    符号入门

    先来看下面这个文件foo.c

    #include <stdio.h>
    
    void foo()
    {
        printf("foo
    ");
    }
    

    g++ -c foo.c编译结果如下

    0000000000000000 <_Z3foov>:
       0:	55                   	push   %rbp
       1:	48 89 e5             	mov    %rsp,%rbp
       4:	bf 00 00 00 00       	mov    $0x0,%edi
       9:	e8 00 00 00 00       	callq  e <_Z3foov+0xe>
       e:	90                   	nop
       f:	5d                   	pop    %rbp
      10:	c3                   	retq
    

    gcc -c foo.c编译结果如下

    0000000000000000 <foo>:
       0:	55                   	push   %rbp
       1:	48 89 e5             	mov    %rsp,%rbp
       4:	bf 00 00 00 00       	mov    $0x0,%edi
       9:	e8 00 00 00 00       	callq  e <foo+0xe>
       e:	90                   	nop
       f:	5d                   	pop    %rbp
      10:	c3                   	retq 
    

    这个文件足够简单,可以看到区别就只是函数名而已,这里我们只关注第9行。可以看出,gcc并没有改变函数名foo,而g++在前后加了一些串变成了_Z3foov。其实g++将参数信息插在函数名的尾部了,_Z3foov中的后缀v就代表了void(PS: 我不知道_Z3表示啥)。稍微改一下函数的参数,用g++编译一下看看:

    • 如果foo是有1个参数int,那函数名是_Z3fooi
    • 如果foo是有1个参数double,那函数名是_Z3food
    • 如果foo有两个参数int和double,那函数名应该是_Z3fooid。(请自行实验)

    如果参数是个自定义的类呢,比如:

    int foo(My my)
    {
        return 0;
    }
    

    被编译成

    0000000000000047 <_Z3foo2My>:
      47:	55                   	push   %rbp
      48:	48 89 e5             	mov    %rsp,%rbp
      4b:	89 7d f0             	mov    %edi,-0x10(%rbp)
      4e:	b8 00 00 00 00       	mov    $0x0,%eax
      53:	5d                   	pop    %rbp
      54:	c3                   	retq 
    

    可以看到,直接以类名拼接在末尾(PS: 我不知道2表示啥)。

    如果是个std的类呢?比如string

    void foo(std::string my)
    {
        printf("foo%s
    ", my.c_str());
    }
    

    被编译成

    000000000000001a <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE>:
      1a:	55                   	push   %rbp
      1b:	48 89 e5             	mov    %rsp,%rbp
      1e:	48 83 ec 10          	sub    $0x10,%rsp
      22:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
      26:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
      2a:	48 89 c7             	mov    %rax,%rdi
      2d:	e8 00 00 00 00       	callq  32 <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE+0x18>
      32:	48 89 c6             	mov    %rax,%rsi
      35:	bf 00 00 00 00       	mov    $0x0,%edi
      3a:	b8 00 00 00 00       	mov    $0x0,%eax
      3f:	e8 00 00 00 00       	callq  44 <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE+0x2a>
      44:	90                   	nop
      45:	c9                   	leaveq 
      46:	c3                   	retq 
    

    很长很长,因为类名确实很长,这个你用lstrace跑个程序就知道了,很多函数名都很长得看不懂。

    C++调用C

    在C++源文件中是不能直接调用C源文件中的函数的,链接的时候就会报对‘foo()’未定义的引用,因为C++源文件编译时没问题,链接时就找不到符号了,有了上面的符号基础,我们可以知道这是因为gcc和g++编译出来的符号不一致导致的。举个例子,现在有文件main.cpp、foo.h、foo.c。

    main.cpp内容如下:

    #include "foo.h"
    int main()
    {
        foo();
        return 0;
    }
    

    foo.h内容如下:

    #ifndef __FOO__
    #define __FOO__
    void foo();
    #endif
    
    

    foo.c内容如下:

    #include <stdio.h>
    void foo()
    {
        printf("foo
    ");
    }
    

    现在以如下命令编译他们

    g++ -c main.cpp
    gcc -c foo.c
    g++ -o test foo.o main.o  # 这一步会报错
    

    报错内容:

    main.c:(.text+0x10):对‘foo()’未定义的引用
    collect2: error: ld returned 1 exit status
    

    这是因为在链接两个.o文件时,找不到foo这个函数才报的错。foo确实是在foo.o里边的,只不过main.o中其实需要的是函数_Z3foov才对。

    正确的做法之一是修改foo.h文件如下

    #ifndef __FOO__
    #define __FOO__
    
    extern "C" {
    void foo();
    }
    #endif
    

    这样编译出来的foo.o没有任何区别,但是main.o就有区别了,里面的符号_Z3foov全被替换成foo了(用objdump -t查看),这样链接起来就没问题。当然了,在一些古老的库中,头文件那么多,难道要一个个改吗?况且改完头文件还得重新用gcc编译。这是没必要的,此时只需要再新建一个头文件,比如foo_cpp.h,把需要用到的C头文件写在大括号里面就可以了,比如:

    #ifndef __FOO_CPP__
    #define __FOO_CPP__
    
    #ifdef __cplusplus  // 很重要
    extern "C" {
    #endif
    
    #include "foo.h"
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    看到这里,extern "C"的最常见的用法也就清晰了,件即告诉g++编译器,大括号内的符号都以C的符号命名方式去调用。值得注意的是,一般需要在使用extern "C"的时候用宏__cplusplus来判断此时的编译器是不是C++的,这样的做法百利而无害。

    但是,这样就改到了旧的C模块了,这通常不是我们想要的,这里介绍一个更简单的方法。直接改main.cpp,用extern "C"括起需要用到的C头文件,如下

    extern "C" {
    #include "foo.h"
    }
    
    int main()
    {
        foo();
        return 0;
    }
    

    这种方式更加方便快捷,还不用动旧代码,所以用得比较多。

    C调用C++

    C调用C++稍微有些不同,因为C没有extern "C"这个东西,所以还得从C++的代码入手,使得它可以被C代码所调用。

    方法1:

    如果原来的C++代码可以改的话,直接在头文件里面套上extern "C"全部括起来,然后C代码中想用哪个函数就直接将函数声明拷过来,前面加上extern(告诉gcc这个函数是在其他模块定义的)。

    apple.cpp内容如下

    #include "apple.h"
    
    Apple::Apple() : m_nColor(0)
    {
    }
     
    void Apple::SetColor(int color)
    {
        m_nColor = color;
    }
     
    int Apple::GetColor(void)
    {
        return m_nColor;
    }
    
    int set_and_get_color(int n)
    {
        Apple a;
        a.SetColor(n);
        return a.GetColor();
    }
    

    apple.h内容如下

    #ifndef __APPLE_H__
    #define __APPLE_H__
    
    #ifdef __cplusplus       // 新加上的
    extern "C" {
    #endif
    
    class Apple
    {
    public:
        Apple();
        int GetColor(void);
        void SetColor(int color);
        
    private:
        int m_nColor;
    };
    
    
    int set_and_get_color(int);
    
    #ifdef __cplusplus     // 新加上的
    }
    #endif
    #endif
    

    main.c内容如下

    #include <stdio.h>
    extern int set_and_get_color(int);  // 函数如果太多可以弄个头文件单独写
    
    int main(void)
    {
        printf("color: %d
    ", set_and_get_color(5666));
        return 0;
    }
    

    按照如下命令编译它们

    g++ -c apple.cpp
    gcc -c main.c
    gcc -o test main.o apple.o -lstdc++    # 链接
    

    注意到,旧的C++的代码仍然是用g++编译,新的C代码是用gcc编译,最后再把它们链接起来就是可执行程序。这个方法的缺点就是需要改到旧的C++头文件,导致C++代码重新编译,问题也不是很大,可以考虑。

    方法2:

    如果旧的C++代码太古董了,一点都不想改的话,可以专门写个wrapper模块,单独给main.cpp用。这个wrapper模块是C++代码(g++编译)。

    apple.cpp和上面方法1是一样的。
    apple.h内容如下

    #ifndef __APPLE_H__
    #define __APPLE_H__
    
    class Apple
    {
    public:
        Apple();
        int GetColor(void);
        void SetColor(int color);
        
    private:
        int m_nColor;
    };
    
    int set_and_get_color(int);
    #endif
    

    wrapper.cpp的内容如下

    #include "wrapper.h"
    #include "apple.h"  // 这行不能放头文件,编不过的
    
    int c_set_and_get_color(int x)
    {
        return set_and_get_color(x);
    }
    

    wrapper.h的内容如下

    #ifdef __cplusplus      // 这个宏判断必不可少
    extern "C" {
    #endif
    
    int c_set_and_get_color(int);
    
    #ifdef __cplusplus
    }
    #endif
    

    main.c的内容如下

    #include <stdio.h>
    #include "wrapper.h"
    
    int main(void)
    {
        printf("color: %d
    ", c_set_and_get_color(5666));
        return 0;
    }
    

    按照如下命令编译它们

    g++ -c apple.cpp
    g++ -c wrapper.cpp
    gcc -c main.c
    gcc -o test main.o apple.o wrapper.o -lstdc++    # 链接
    

    注意到,wrapper.o是用g++编的,main.o是用gcc编的,而且wrapper.hwrapper.omain.o中都有用到,所以__cplusplus宏判断是必不可少的,因为对于wrapper.o来说,extern "C"是需要的,对于main.o来说又是不需要的。

    总结

    C++调C很简单,在需要调用C接口时用extern "C"将头文件括起来就行了,不需要改原来的代码(当然了,你想改也没问题)。

    C调C++较麻烦,有两种方法:

    • 改旧的C++头文件,插入extern "C"后,C代码可直接用C++接口(以extern声明一下函数即可)。
    • 用C++写个wrapper模块,提供一些C接口,里面再去调用C++接口。
  • 相关阅读:
    李连杰开始做慈善事业了!
    世界是平的,这本书主要是写给美国人看的
    [问题征解]请解释下ubuntu 510 firefox的flash不发音的问题
    中医治疗慢性病很有效
    清理downloader病毒几百个,2个小时
    firefox2.0的拖放式搜索怎么不行了?是设置问题吗?
    spring live上有个入门的整合SSH的例子
    cctv的健康之路节目知识性和可看性都不错!
    跟你分享一下养生的经验
    cctv: 西医拒绝治疗的小孩,中医三天见效
  • 原文地址:https://www.cnblogs.com/xcw0754/p/9960208.html
Copyright © 2020-2023  润新知