如何混合使用 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
会被 #define
d)
#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 程序调用。
为什么链接器在 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++ 中,很容易检查名为 dp
的 Derived*
是否指向与名为 bp
的 Base*
指向的同一个对象:只需说 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++ 对象的数据:
如果 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++ 并没有试图让糟糕的程序员无法编写糟糕的程序;它使合理的开发人员能够创建卓越的软件。