Что нужно знать для смешивания в проекте кода 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++:
extern "C" void f(int); // один из способов декларации
extern "C"
{
// другой способ
int g(double);
double h();
};
void code(int i, double d)
{
f(i);
int ii = g(d);
double dd = h();
// ...
}
Реализация функций может быть совершенно стандартная для C:
// Код на языке C:
void f(int i)
{
/* ... */
}
int g(double d)
{
/* ... */
}
double h()
{
/* ... */
}
Чаще всего директива extern "C" используется в только заголовочных файлах. Чтобы сделать заголовочный файл универсальным, используют общепринятое обрамление блока декларации функций через extern "C" с помощью операторов условной компиляции, которые проверяют режим компиляции (какой используется компилятор - C++ или C).
// Содержимое заголовочного файла *.h, который может подключаться
// как компилятором C, так и компилятором C++:
#ifdef __cplusplus
extern "C" {
#endif
int g(double);
double h();
...
#ifdef __cplusplus
}
#endif
Здесь директива условной компиляции #ifdef __cplusplus сработает, если активен компилятор C++, и будет подключен блок директивы extern "C". В случае компиляции в режиме C блок директивы extern "C" подключаться не будет.
Также имейте в виду, что действуют правила типов C++, не правила типов C. Таким образом, Вы не сможете вызвать функцию, которая определена через extern "C", с неправильным количеством аргументов. Например:
// Код на языке C++:
void more_code(int i, double d)
{
double dd = h(i,d); // error: unexpected arguments
// ...
}
Как вызывать функцию C++ из кода C? Просто декларируйте функцию C++ с директивой extern "C" (в коде C++) и вызовите её (из кода C или C++). Пример:
// Код на языке C++:
extern "C" void f(int);
void f(int i)
{
// ...
}
Теперь функция f() может использоваться следующим образом:
// Код на языке C:
void f(int);
void cc(int i)
{
f(i);
// ...
}
В реальности это работает только для функций, которые не являются членами класса. Если Вы хотите вызвать функцию - члена класса (включая функции virtual) из C, то понадобится реализовать простую обертку. Например:
// Код на языке C++:
class C
{
// ...
virtual double f(int);
};
// Функция-обертка над функцией f(int):
extern "C" double call_C_f(C* p, int i)
{
return p->f(i);
}
Теперь функция C::f() может быть вызвана следующим образом:
// Код на языке C:
double call_C_f(struct C* p, int i);
void ccc(struct C* p, int i)
{
double d = call_C_f(p,i);
// ...
}
Если Вы хотите вызвать перегруженные (overloaded) функции из C, то нужно предоставить обертки с разными именами для каждой реализации перегруженной функции, чтобы их можно было использовать в коде C. Например:
// Код на языке C++:
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:
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++? Для подключения стандартного хедера (такого как < cstdio>) Вы не должны делать ничего необычного. Например:
// Код на языке C++:
#include < cstdio> // самая обычная строка подключения заголовка
int main()
{
std::printf("Hello world\n"); // в этом вызове также нет ничего особенного
// ...
}
Если Вы думаете, что часть std:: в вызове std::printf() необычным, то лучшее, что следует сделать, это просто привыкнуть к этому. Другими словами, это стандартный способ использовать имена в стандартной библиотеке, так что начинайте к этому привыкать.
Однако если Вы компилируете код C компилятором C++, то скорее всего не захотите переделывать все эти вызовы из printf() в std::printf(). К счастью, в этом случае код C будет использовать заголовочный файл старого стиля < stdio.h> вместо заголовочного файла нового стиля < cstdio>, и обо всем остальном позаботится магия пространства имен (namespaces):
// Этот код C автор [1] пропускал через компилятор C++:
#include < stdio.h> // обычная строка подключения заголовка
int main()
{
printf("Hello world\n"); // в вызове также ничего необычного
// ...
}
Последнее замечание: если у Вас есть заголовочные файлы C, которые не являются часть стандартной библиотеки, то понадобится соблюсти некоторые другие рекомендации. Могут быть два случая: либо у Вас нет возможности изменить хедер, либо Вы можете поменять хедер.
Как можно подключить не системный заголовочный файл C в код C++? Если Вы используете заголовочный файл C, который не предоставляется вместе с системой программирования, то может понадобиться сделать обрамление строки #include конструкцией блока extern "C" { /*...*/ }. Это укажет компилятору C++, что функции, декларируемые в заголовке, являются C-функциями.
// Код на языке C++:
extern "C" {
// Следующая строка вводит декларацию для функции f(int i, char c, float x):
#include "my-C-code.h"
}
int main()
{
f(7, 'x', 3.14); // Обратите внимание: совершенно обычный вызов функции
// ...
}
Как изменить файл заголовка C, чтобы его было проще подключать в код C++? Если Вы подключаете хедер C, который не предоставляется системой программирования ("нестандартный" заголовок, т. е. не относящийся к стандартной библиотеке), и если Вы можете поменять этот хедер C, то обязательно следует добавить логику extern "C" {...} в хедер, чтобы пользователям C++ было проще подключать его в свой код C++. Поскольку компилятор C не захочет понимать конструкцию extern "C", то Вы должны обернуть строки extern "C" { и } в условный оператор препроцессора #ifdef, чтобы они не были видны обычным компилятором C.
Шаг 1: поместите следующие строки в начало заголовочного файла C (символ __cplusplus автоматически определяется компилятором C++, компилятор C этот символ не определяет):
#ifdef __cplusplus
extern "C" {
#endif
Шаг 2: Поместите следующие строки в конец заголовочного файла C:
#ifdef __cplusplus
}
#endif
После этого Вы можете свободно подключать оформленный таким образом заголовок как из кода C, так и из кода C++, не заботясь ни о каких директивах extern "C":
// Код на языке C++:
// Следующая строка вводит декларацию функции f(int i, char c, float x)
#include "my-C-code.h" // Заметьте: обычная строка #include
int main()
{
f(7, 'x', 3.14); // В вызове функции также ничего необычного
// ...
}
К сожалению, многочисленные конструкции из условных операторов препроцессора совсем не украшают код и затрудняют его чтение, но в некоторых случаях это наименьшее зло в программировании.
Как вызывать не системную C-функцию f(int,char,float) из кода C++? Если есть отдельная C-функция, которую Вы хотите вызвать, и по какой-либо причине не хотите подключать (директивой #include) заголовочный файл C, в котором эта функция декларирована, то можно декларировать эту отдельную функцию C в коде C++, используя синтаксис extern "C". Для этого надо использовать полный прототип функции:
extern "C" void f(int i, char c, float x);
Несколько таких функций можно сгруппировать в блок фигурными скобками:
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);
// ...
}
Как создать C++ функцию f(int,char,float), которую можно вызывать из кода C? Компилятор C++ должен четко знать, что функция f(int,char,float) будет вызываться из кода компилятора C. Такое указание компилятору C++ делает конструкция extern "C":
// Код на языке C++:
// Декларация f(int,char,float) с использованием extern "C":
extern "C" void f(int i, char c, float x);
// ...
// Определение f(int,char,float) в каком-нибудь модуле C++:
void f(int i, char c, float x)
{
// ...
}
Строка extern "C" говорит компилятору C++, что информация, посылаемая линкеру, должна использовать соглашения о вызовах C, и также должна использоваться декорация имен языка C (name mangling, т. е. присоединение к имени функции префикса одиночного нижнего подчеркивания). Поскольку перегрузка имен функций не поддерживается языком C, то Вы не можете сделать некоторые перегружаемые функции одновременно вызываемыми из программы на C.
Почему линкер выдает ошибки на функции C/C++, которые вызываются из кода C++/C? Если Вы не всегда правильно используете extern "C" (если вообще делаете это), то будете иногда сталкиваться с ошибками линкера вместо ошибок компиляции, когда смешиваете код C и C++. Причина здесь в том, что фактически компиляторы C++ обычно декорируют ("mangle") имена функций не так, как это делают компиляторы C, и поэтому линкер не находит ожидаемые имена в объектных или библиотечных файлах.
См. два предыдущих вопроса о том, как использовать extern "C".
Как передать объект класса C++ в функцию C и из неё? Ниже дан пример (как использовать extern "C", см. предыдущие вопросы).
Заголовочный файл Fred.h:
/* Этот заголовок может обработать как компилятор C, так и компилятор C++. */
#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 */
extern Fred* cplusplus_callback_function(Fred*);#else
extern void c_function(); /* Стиль Кернигана & Ричи */
extern Fred* cplusplus_callback_function();
#endif
#ifdef __cplusplus
}
#endif
#endif /*FRED_H*/
Модуль Fred.cpp:
// Это код языка C++:
#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:
// Это код языка C++:
#include "Fred.h"
int main()
{
Fred fred;
c_function(&fred);
// ...
}
c-function.c:
/* Это код языка C */
#include "Fred.h"
void c_function(Fred* fred)
{
cplusplus_callback_function(fred);
}
В отличие от кода C++, коду на C нельзя указать, что два указателя ссылаются на тот же самый объект, если указатели не будут иметь совершенно одинаковый тип. Например, на C++ просто проверить, указывает ли dp (указатель на объект производного класса Derived*) на тот же объект, что и указатель bp (указатель на объект базового класса Base*), это делается обычной проверкой if (dp == bp) .... Компилятор C++ автоматически преобразует оба указателя в одинаковый тип, в этом случае к базовому типу, после чего проверит их равенство. В зависимости от подробностей реализации компилятора C++, это преобразование иногда меняет биты значения указателя.
Технический момент: большинство компиляторов C++ используют двоичную структуру объекта, которая заставляет это преобразование происходить с поддержкой множественного наследования (multiple inheritance) и/или виртуального наследования (virtual inheritance). Однако язык C++ не навязывает ограничения на реализацию этого принципа преобразования, так что оно может произойти даже с не виртуальным одиночным наследованием (non-virtual single inheritance).
Все просто: компилятор C ничего не знает о том, как сделать преобразование указателя, т. е. преобразование указателя из производного объекта в базовый. Так что преобразование должно произойти при компиляции C++, но не при компиляции C.
Важное замечание: особенно осторожным следует быть при преобразовании обоих указателей в void*, поскольку такое приведение типов не даст возможности ни компилятору C, ни компилятору C++, выполнить правильное преобразование! Сравнение (x == y) может быть false даже если (b == d) равно true:
void f(Base* b, Derived* d)
{
if (b == d) { // Корректное сравнение Base* и Derived*
// ...
}
void* x = b;
void* y = d;
if (x == y) { // Некорректно. НИКОГДА ТАК НЕ ДЕЛАЙТЕ!
// ...
}
}
Как было показано выше, эти преобразования указателя обычно будут происходить с множественным и/или виртуальным наследованием, однако не рассматривайте эти случаи как исчерпывающий список преобразований указателей, которые будут происходить. По крайней мере, Вас предупредили.
Если Вы все-таки очень хотите использовать указатели void*, вот безопасный способ:
void f(Base* b, Derived* d)
{
void* x = b;
// Если преобразование необходимо, то это случится в static_cast< >
void* y = static_cast< Base*>(d);
if (x == y) { // Корректное сравнение Base* и Derived*
// ...
}
}
Может ли моя функция C получить прямой доступ к данным объекта класса C++? Не всегда. Базовые инструкции по передаче данных объектов C++ в функцию C и из функции C см. в предыдущих вопросах.
Вы можете безопасно получить доступ к данным объекта C++ из функции C, если этот класс C++:
• Не имеет виртуальных функций (включая унаследованные виртуальные функции). • Помещает все свои данные на одном и том же уровне доступа (private/protected/public). • Не имеет полностью встроенных подчиненных объектов (fully-contained subobjects) с виртаульными функциями.
Если класс C++ основан на любом базовом классе (или если любые полностью встроенные подчиненные объекты основаны на базовом класса), то доступ к данным класса технически будет не портируемым, потому что двоичная структура наследуемых объектов не не определяется синтаксисом языка. Однако на практике все компиляторы C++ делают это одинаково: объект базового класса появляется первым (в порядке слева направо при событии множественного наследования), и за ним идут объекты члены класса.
Кроме того, если класс (или любой его базовый класс) содержит любые виртуальные функции, почти все компиляторы C++ помещают указатель void* в объект либо на месте первой виртуальной функции, или в самом начале объекта. Опять-таки, этот принцип не определяется языком, просто "все делают это".
Если класс имеет любые виртуальные базовые классы, то все становится еще сложнее и менее портируемо. Общая техника реализации для объектов - помещать объект виртуального базового класса (V) последним (независимо от того, где обнаруживается V как виртуальный базовый класс в иерархии наследования). Остальная часть объекта появляется в нормальном порядке. Каждый производный класс, для которого V является виртуальным базовым классом, в реальности имеет указатель на V конечного объекта.
Почему с языком C++ кажется, что я "удаляюсь от машины", в отличие от языка C? Ни почему, просто это так и есть.
Как объектно-ориентированный язык программирования, C++ позволяет Вам моделировать ситуацию в домене решаемой проблемы, что (теоретически) дает возможность программировать на языке домена проблемы, а не на языке домена вычислительной платформы и средства программирования.
Сила языка C в том, что у него фактически нет "скрытых механизмов": что видите, то и получаете. Вы можете читать программу C и "видеть", что в ней происходит на каждом такте процессора. С языком C++ все не так; старые C-программисты (которыми почти все мы когда-то были) часто настороженно (можно даже сказать - "враждебно"?) относятся к этой особенности C++. Однако после того, как они сделали переход к объектно-ориентированному мышлению, они часто понимают, что хотя C++ скрывает многие механизмы выполнения кода от программиста, C++ также дает новый удобный уровень абстракции, экономный в выражении и снижающий усилия на поддержку кода без снижения его рабочей производительности.
В сущности Вы можете писать плохой код на любом языке; C++ не гарантирует какой-то определенный уровень качества, возможности повторного использования кода, абстракции, или любых подобных полезных "плюшек".
C++ не пытается запретить плохим программистам писать плохие программы. Вместо этого C++ дает возможность разумным разработчикам создавать превосходные программы.
[Ссылки]
1. How to mix C and C++ site:isocpp.org. 2. IAR: совместное использование кода C и кода C++. |