混合 C 和 C++

如何混合使用 C 和 C++

混合 C 和 C++ 代码时需要知道什么?

以下是一些重点(尽管有些编译器供应商可能不需要所有这些;请查阅您的编译器供应商的文档)

  • 编译 main() 时必须使用 C++ 编译器(例如,用于静态初始化)
  • 您的 C++ 编译器应指导链接过程(例如,以便它可以获取其特殊库)
  • 您的 C 和 C++ 编译器可能需要来自同一供应商并具有兼容版本(例如,以便它们具有相同的调用约定)

此外,您需要阅读本节的其余部分,以了解如何使您的 C 函数可以被 C++ 调用和/或您的 C++ 函数可以被 C 调用。

顺便说一下,还有另一种处理整个事情的方法:使用 C++ 编译器编译所有代码(甚至是您的 C 风格代码)。这几乎消除了混合 C 和 C++ 的需要,此外它会使您在 C 风格代码中更仔细(并且可能——希望如此!——发现一些错误)。缺点是您需要在某些方面更新您的 C 风格代码,基本上是因为C++ 编译器比您的 C 编译器更仔细/更挑剔。重点是清理 C 风格代码所需的工作量可能小于混合 C 和 C++ 所需的工作量,而且作为额外的好处,您获得了清理过的 C 风格代码。显然,如果您无法更改 C 风格代码(例如,如果它来自第三方),您别无选择。

如何从 C++ 调用 C 函数?

只需将 C 函数声明为 extern "C"(在您的 C++ 代码中)并调用它(从您的 C 或 C++ 代码中)。例如

    // C++ code

    extern "C" void f(int); // one way

    extern "C" {    // another way
        int g(double);
        double h();
    };

    void code(int i, double d)
    {
        f(i);
        int ii = g(d);
        double dd = h();
        // ...
    }

函数的定义可能如下所示

    /* C code: */

    void f(int i)
    {
        /* ... */
    }

    int g(double d)
    {
        /* ... */
    }

    double h()
    {
        /* ... */
    }

请注意,使用 C++ 类型规则,而不是 C 规则。因此,您不能使用错误的参数数量调用声明为 extern "C" 的函数。例如

    // C++ code

    void more_code(int i, double d)
    {
        double dd = h(i,d); // error: unexpected arguments
        // ...
    }

如何从 C 调用 C++ 函数?

只需将 C++ 函数声明为 extern "C"(在您的 C++ 代码中)并调用它(从您的 C 或 C++ 代码中)。例如

    // C++ code:

    extern "C" void f(int);

    void f(int i)
    {
        // ...
    }

现在 f() 可以这样使用

    /* C code: */

    void f(int);

    void cc(int i)
    {
        f(i);
        /* ... */
    }

当然,这只适用于非成员函数。如果您想从 C 调用成员函数(包括虚函数),您需要提供一个简单的包装器。例如

    // C++ code:

    class C {
        // ...
        virtual double f(int);
    };

    extern "C" double call_C_f(C* p, int i) // wrapper function
    {
        return p->f(i);
    }

现在 C::f() 可以这样使用

    /* C code: */

    double call_C_f(struct C* p, int i);

    void ccc(struct C* p, int i)
    {
        double d = call_C_f(p,i);
        /* ... */
    }

如果您想从 C 调用重载函数,您必须为 C 代码提供具有不同名称的包装器以供使用。例如

    // C++ code:

    void f(int);
    void f(double);

    extern "C" void f_i(int i) { f(i); }
    extern "C" void f_d(double d) { f(d); }

现在 f() 函数可以这样使用

    /* C code: */

    void f_i(int);
    void f_d(double);

    void cccc(int i,double d)
    {
        f_i(i);
        f_d(d);
        /* ... */
    }

请注意,即使您不能(或不想)修改 C++ 头文件,这些技术也可以用于从 C 代码调用 C++ 库。

如何在我的 C++ 代码中包含标准 C 头文件?

#include 标准头文件(例如 <cstdio>),您无需做任何不寻常的事情。例如,

// This is C++ code

#include <cstdio>                // Nothing unusual in #include line

int main()
{
  std::printf("Hello world\n");  // Nothing unusual in the call either
  // ...
}

如果您来自 C,std::printf() 调用中的 std:: 部分可能看起来不寻常,但这是在 C++ 中编写它的正确方法。

如果您正在使用 C++ 编译器编译 C 代码,您不想将所有这些调用从 printf() 调整为 std::printf()。幸运的是,在这种情况下,C 代码将使用旧式头文件 <stdio.h> 而不是新式头文件 <cstdio>,命名空间的魔力将处理一切

/* This is C code that I'm compiling using a C++ compiler */

#include <stdio.h>          /* Nothing unusual in #include line */

int main()
{
  printf("Hello world\n");  /* Nothing unusual in the call either */
  // ...
}

最后评论:如果您有非标准库的 C 头文件,我们有略微不同的指南。有两种情况:要么您无法更改头文件,要么您可以更改头文件

如何在我的 C++ 代码中包含非系统 C 头文件?

如果您包含的 C 头文件不是系统提供的,您可能需要将 #include 行包装在 extern "C" { /*...*/ } 构造中。这会告诉 C++ 编译器,头文件中声明的函数是 C 函数。

// This is C++ code

extern "C" {
  // Get declaration for f(int i, char c, float x)
  #include "my-C-code.h"
}

int main()
{
  f(7, 'x', 3.14);   // Note: nothing unusual in the call
  // ...
}

注意:对于系统提供的 C 头文件(例如 <cstdio>您可以更改的 C 头文件,适用略有不同的指南。

如何修改我自己的 C 头文件以便更容易地在 C++ 代码中 #include 它们?

如果您包含的 C 头文件不是系统提供的,并且如果您能够更改 C 头文件,您应该强烈考虑在头文件中添加 extern "C" {...} 逻辑,以便 C++ 用户更容易地将其 #include 到他们的 C++ 代码中。由于 C 编译器无法理解 extern "C" 构造,您必须将 extern "C" {} 行包装在 #ifdef 中,以便普通 C 编译器看不到它们。

步骤 #1:将以下行放在 C 头文件的最顶部(注意:如果且仅当编译器是 C++ 编译器时,符号 __cplusplus 会被 #defined)

#ifdef __cplusplus
extern "C" {
#endif

步骤 #2:将以下行放在 C 头文件的最底部

#ifdef __cplusplus
}
#endif

现在您可以在 C++ 代码中 #include 您的 C 头文件,而无需任何 extern "C" 的麻烦

// This is C++ code

// Get declaration for f(int i, char c, float x)
#include "my-C-code.h"   // Note: nothing unusual in #include line

int main()
{
  f(7, 'x', 3.14);       // Note: nothing unusual in the call
  // ...
}

注意:对于系统提供的 C 头文件(例如 <cstdio>您无法更改的 C 头文件,适用略有不同的指南。

注意:#define 宏以 4 种不同方式邪恶邪恶 #1邪恶 #2邪恶 #3邪恶 #4。但它们有时仍然有用。使用后洗手即可。

如何从我的 C++ 代码中调用非系统 C 函数 f(int,char,float)

如果您有一个想要调用的单个 C 函数,并且由于某种原因您没有或不想 #include 声明该函数的 C 头文件,您可以使用 extern "C" 语法在您的 C++ 代码中声明该单个 C 函数。当然,您需要使用完整的函数原型

extern "C" void f(int i, char c, float x);

通过大括号可以将一组几个 C 函数分组

extern "C" {
  void   f(int i, char c, float x);
  int    g(char* s, const char* s2);
  double sqrtOfSumOfSquares(double a, double b);
}

之后,您只需像调用 C++ 函数一样调用该函数

int main()
{
  f(7, 'x', 3.14);   // Note: nothing unusual in the call
  // ...
}

如何创建一个 C++ 函数 f(int,char,float),使其可被我的 C 代码调用?

C++ 编译器必须知道 f(int,char,float) 将由 C 编译器使用extern "C" 构造调用

// This is C++ code

// Declare f(int,char,float) using extern "C":
extern "C" void f(int i, char c, float x);

// ...

// Define f(int,char,float) in some C++ module:
void f(int i, char c, float x)
{
  // ...
}

extern "C" 行告诉编译器,发送到链接器的外部信息应使用 C 调用约定和名称修饰(例如,前面有一个下划线)。由于 C 不支持名称重载,因此您不能同时使多个重载函数可被 C 程序调用。

如果您没有正确使用 extern "C",有时会收到链接器错误而不是编译器错误。这是因为 C++ 编译器通常会以与 C 编译器不同的方式“修饰”函数名称(例如,为了支持函数重载)。

请参阅前两个关于如何使用 extern "C" 的常见问题。

如何将 C++ class 的对象传递给 C 函数或从 C 函数传回?

这是一个例子(关于 extern "C" 的信息,请参阅前两个常见问题)。

Fred.h:

/* This header can be read by both C and C++ compilers */
#ifndef FRED_H
#define FRED_H

#ifdef __cplusplus
  class Fred {
  public:
    Fred();
    void wilma(int);
  private:
    int a_;
  };
#else
  typedef
    struct Fred
      Fred;
#endif

#ifdef __cplusplus
extern "C" {
#endif

#if defined(__STDC__) || defined(__cplusplus)
  extern void c_function(Fred*);   /* ANSI C prototypes */
  extern Fred* cplusplus_callback_function(Fred*);
#else
  extern void c_function();        /* K&R style */
  extern Fred* cplusplus_callback_function();
#endif

#ifdef __cplusplus
}
#endif

#endif /*FRED_H*/

Fred.cpp:

// This is C++ code

#include "Fred.h"

Fred::Fred() : a_(0) { }

void Fred::wilma(int a) { }

Fred* cplusplus_callback_function(Fred* fred)
{
  fred->wilma(123);
  return fred;
}

main.cpp:

// This is C++ code

#include "Fred.h"

int main()
{
  Fred fred;
  c_function(&fred);
  // ...
}

c-function.c:

/* This is C code */

#include "Fred.h"

void c_function(Fred* fred)
{
  cplusplus_callback_function(fred);
}

与您的 C++ 代码不同,您的 C 代码将无法判断两个指针是否指向同一个对象,除非这些指针的类型完全相同。例如,在 C++ 中,很容易检查名为 dpDerived* 是否指向与名为 bpBase* 指向的同一个对象:只需说 if (dp == bp) ...。C++ 编译器会自动将两个指针转换为相同的类型,在这种情况下是 Base*,然后进行比较。根据 C++ 编译器的实现细节,这种转换有时会改变指针值的位。

(技术旁注:大多数 C++ 编译器使用一种二进制对象布局,导致这种转换发生在多重继承和/或虚继承中。然而,C++ 语言不强制这种对象布局,因此原则上,即使是非虚单继承,也可能发生转换。)

重点很简单:您的 C 编译器不知道如何进行指针转换,因此从 Derived*Base* 的转换(例如)必须在用 C++ 编译器编译的代码中进行,而不是在用 C 编译器编译的代码中进行。

注意:您在转换为 void* 时必须格外小心,因为这种转换不会允许 C 或 C++ 编译器进行适当的指针调整!即使 (b == d)true,比较 (x == y) 也可能为 false

void f(Base* b, Derived* d)
{
  if (b == d) {   ☺ Validly compares a Base* to a Derived*
    // ...
  }

  void* x = b;
  void* y = d;
  if (x == y) {   ☹ BAD FORM! DO NOT DO THIS!
    // ...
  }
}

如上所述,上述指针转换通常会发生在多重继承和/或虚继承中,但不要将其视为指针转换唯一发生的穷举列表。

你已经被警告了。

如果你真的想使用 void* 指针,这里是安全的方法

void f(Base* b, Derived* d)
{
  void* x = b;
  void* y = static_cast<Base*>(d);  // If conversion is needed, it will happen in the static_cast<>
  if (x == y) {   // ☺ Validly compares a Base* to a Derived*
    // ...
  }
}

我的 C 函数可以直接访问 C++ class 对象中的数据吗?

有时可以。

(有关将 C++ 对象传递给 C 函数或从 C 函数传回的基本信息,请阅读上一个常见问题解答)。

如果 C++ 类满足以下条件,您可以安全地从 C 函数访问 C++ 对象的数据:

  • 没有virtual 函数(包括继承的 virtual 函数)
  • 所有数据都位于相同的访问级别部分(private/protected/public)
  • 没有包含virtual 函数的完整子对象

如果 C++ 类有任何基类(或任何完整包含的子对象有基类),访问数据在技术上是不可移植的,因为继承下的 class 布局不受语言强制。然而,在实践中,所有 C++ 编译器都以相同的方式实现:基类对象首先出现(多重继承时按从左到右的顺序),然后是成员对象。

此外,如果类(或任何基类)包含任何 virtual 函数,几乎所有 C++ 编译器都会在对象中放置一个 void*,要么在第一个 virtual 函数的位置,要么在对象的开头。同样,这不是语言要求的,但这是“每个人”都这样做的方式。

如果类有任何 virtual 基类,它会更加复杂且可移植性更差。一种常见的实现技术是对象最后包含 virtual 基类 (V) 的对象(无论 V 在继承层次结构中以 virtual 基类的形式出现在何处)。对象的其余部分以正常顺序出现。每个以 V 作为 virtual 基类的派生类实际上都有一个指向最终对象的 V 部分的指针

为什么我觉得在 C++ 中比在 C 中“离机器更远”?

因为你确实如此。

作为一种面向对象编程语言,C++ 允许您对问题领域本身进行建模,这使您可以用问题领域的语言而不是解决方案领域的语言进行编程。

C 的一大优点是它“没有隐藏机制”:所见即所得。您可以阅读 C 程序并“看到”每一个时钟周期。C++ 则不然;老式 C 程序员(我们许多人曾经是)通常对此功能模棱两可(你能说“敌对”吗?)。然而,在他们转向面向对象思维后,他们通常会意识到,虽然 C++ 向程序员隐藏了一些机制,但它也提供了一个抽象和表达的层次,这在不牺牲运行时性能的情况下降低了维护成本。

当然,您可以用任何语言编写糟糕的代码;C++ 不保证任何特定的质量、可重用性、抽象程度或任何其他“优良性”度量。

C++ 并没有试图让糟糕的程序员无法编写糟糕的程序;它使合理的开发人员能够创建卓越的软件。