Обратная совместимость
- 1 year ago
- 0
- 0
Языки программирования C и C++ тесно связаны, но имеют существенные различия. C++ создавался как потомок достандартизированного C, по большей части совместимый с ним на тот момент на уровне исходного кода и компоновки . В связи с этим средства разработки для обоих языков (такие, как среды разработки и компиляторы ) часто интегрируются в один продукт, при этом программист может выбрать C или C++ в качестве языка исходного кода.
Однако, C не является подмножеством C++ , поэтому нетривиальные программы на C не будут компилироваться на C++ без изменений. Также C++ вводит множество возможностей, недоступных в C, и на практике почти весь код, написанный на C++, не соответствует коду на C. Однако в этой статье основное внимание уделяется различиям, которые приводят к тому, что соответствующий код C является неправильно написанным ( англ. ill-formed ) кодом на C++ или соответствующим/хорошо написанным ( англ. conforming/well-formed ) на обоих языках, но может вести себя по-разному на C и C++.
Бьёрн Страуструп , создатель C++, предложил что несовместимость между C и C++ должна быть уменьшена насколько это возможно, чтобы обеспечить максимальное взаимодействие между двумя языками. Другие утверждают, что, поскольку C и C++ — это два разных языка, совместимость между ними полезна, но не жизненно важна; согласно их мнению, усилия по уменьшению несовместимости не должны препятствовать попыткам улучшить каждый язык в отдельности. Третьи утверждают, что почти каждая синтаксическая ошибка, которую можно допустить в Си, была пересмотрена в C++ таким образом, чтобы порождать компилируемый, хоть не обязательно корректный код . Официальное обоснование стандарта C 1999 года ( C99 ) «поддерживает принцип сохранения наибольшего общего подмножества» между C и C++, «сохраняет при этом различия между ними и позволяет развиваться отдельно», там также утверждается, что авторы были «довольны тем, что C++ стал большим и амбициозным языком» .
Некоторые нововведения C99 не поддерживаются в текущем стандарте C++ или конфликтуют с отдельными возможностями C++, например,
массивы переменной длины
, собственные
комплексные типы данных
и
квалификатор типа
restrict
. С другой стороны, C99 уменьшил некоторые другие несовместимости по сравнению с C89, включив такие функции C++, как однострочные комментарии
//
, а также смешение объявлений и кода
.
C++ применяет более строгие правила типизации (никаких неявных нарушений системы статических типов ) и требования к инициализации (принудительная проверка во время компиляции, что у переменных в области видимости не нарушена инициализация, то есть невозможно вернуться к месту до объявления с явной или неявной инициализацией, если не считать блоки, в которые не управляющий поток не попадал) , и поэтому некоторый допустимый код C недопустим в C++. Обоснование этого приведено в Приложении C.1 к стандарту ISO C++ .
void*
любому типу указателя без приведения, в то время как C++ этого не позволяет; эта
идиома
часто встречается в коде на C, использующем для выделения памяти
malloc
, или при передаче контекстных указателей в
pthreads
(POSIX API) и другие фреймворки, использующие
обратные вызовы
. Например, следующее допустимо в C, но не в C++:
void *ptr;
/* Неявное преобразование из void* в int* */
int *i = ptr;
или аналогично:
int *j = malloc(5 * sizeof *j); /* Неявное преобразование из void* в int* */
Чтобы заставить код компилироваться как на C, так и на C++, необходимо использовать явное приведение типа следующим образом (с некоторыми предостережениями в отношении обоих языков ):
void *ptr;
int *i = (int *)ptr;
int *j = (int *)malloc(5 * sizeof *j);
int **
к
const int *const *
, но не допускает небезопасного присваивания
const int **
, в то время как C не допускает ни того, ни другого (хотя компиляторы обычно выдают только предупреждение).
const
, например,
strchr
возвращает
char*
в C, в то время как C++ поступает так, как если бы существовали две перегруженные функции
const char *strchr(const char *)
и
char *strchr(char *)
.
enum
enumerators) всегда имеют тип
int
в C, тогда как в C++ они являются различными типами и могут иметь размер, отличный от размера
int
.
const
-переменная должна быть инициализирована; в C это необязательно.
goto
или
switch
пересекать инициализацию, как в следующем коде на C99:
void fn(void)
{
goto flack;
int i = 1;
flack:
;
}
longjmp()
приводит к неопределённому поведению в C++, если пропущенные (
англ.
jumped-over
)
фреймы стека
содержат объекты с нетривиальными деструкторами
. Имплементация C++ может произвольно определять поведение таким образом, чтобы деструкторы вызывались. Однако это исключает некоторые варианты использования
longjmp()
, которые в противном случае были бы допустимы, например, имплементация
потоков
или
сопрограмм
, переключающихся между отдельными стеками вызовов с помощью
longjmp()
— при переходе из нижнего стека вызовов в верхний в глобальном адресном пространстве деструкторы вызывались бы для каждого объекта в нижнем стеке вызовов. В C такой проблемы не существует.
int N;
int N = 10;
struct
,
union
или
enum
, но это недопустимо в C++, потому что в C типы
struct
,
union
и
enum
должны указываться всякий раз, когда на этот тип ссылаются, тогда как в C++ все объявления таких типов неявно содержат
.
enum BOOL {FALSE, TRUE};
typedef int BOOL;
int foo();
, подразумевает, что аргументы не указаны. Следовательно, допустимо вызывать такую функцию с одним или несколькими
аргументами
, например
foo(42, "hello world")
. Напротив, в C++ прототип функции без аргументов означает, что функция не принимает аргументов, и вызов такой функции с аргументами является некорректным. В C правильный способ объявить функцию, которая не принимает аргументов, — это использовать 'void', как в
int foo(void);
, это также допустимо в C++. Пустые прототипы функций являются устаревшей (
англ.
deprecated
) возможностью в C99 (как и в C89).
struct
, но область действия интерпретируется по-разному: в C++ вложенный тип
struct
определяется только в пределах области видимости/пространства имён внешнего типа
struct
, тогда как в C внутренняя структура также определяется вне внешней структуры.
struct
,
union
и
enum
в прототипах функций, в то время как C++ этого не делает.
C99 и C11 добавили в C несколько дополнительных возможностей, которые не были включены в стандартный C++, таких как комплексные числа, массивы переменной длины (обратите внимание, что комплексные числа и массивы переменной длины обозначены как необязательные расширения в C11), , ключевое слово restrict , квалификаторы параметров массива, составные литералы ( англ. compound literals ) и .
float complex
и
double complex
были добавлены в стандарт
C99
с помощью ключевого слова
_Complex
и макроса
complex
для удобства. В C++ арифметические действия с комплексными числами может быть выполнена с использованием класса комплексных чисел, но эти два метода несовместимы на уровне кода. (Однако стандарты, начиная с
C++11
, требуют бинарной совместимости.)
sizeof
не во время компиляции
.
void foo(size_t x, int a[*]); // Объявление VLA
void foo(size_t x, int a[x])
{
printf("%zu\n", sizeof a); // То же, что и sizeof(int*)
char s[x * 2];
printf("%zu\n", sizeof s); // Будет выведено print x*2
}
struct X
{
int n, m;
char bytes[];
}
restrict
, определённый в C99, не был включён в стандарт C++03, но большинство основных компиляторов, таких как
GCC
,
Microsoft Visual C++
и
Intel C++ Compiler
предоставляют аналогичную функциональность в качестве расширения.
int foo(int a[const]); // аналогично int *const a
int bar(char s[static 5]); // отмечает, что s имеет длину не менее 5 символов
struct X a = (struct X){4, 6}; // Аналогичным в C++ было бы X{4, 6}. Синтаксическая форма C, используемая в C99, поддерживается в качестве расширения в компиляторах GCC и Clang.
foo(&(struct X){4, 6}); // Объект выделяется на стеке, и его адрес может быть передан функции. Не поддерживается в C++.
if (memcmp(d, (int []){8, 6, 7, 5, 3, 0, 9}, n) == 0) {} // Аналогичным в C++ было бы digits = int []; if (memcmp(d, digits{8, 6, 7, 5, 3, 0, 9}, n) == 0) {}
char s[20] = { [0] = 'a', [8] = 'g' }; // Допустимо в C, но не в C++
C++ добавляет множество дополнительных ключевых слов для поддержки своих новых возможностей. Это делает код на C, использующий эти ключевые слова для идентификаторов, недопустимым в C++. Например, такой код:
struct template
{
int new;
struct template* class;
};
template
,
new
и
class
зарезервированы.
Существует несколько синтаксических конструкций, которые допустимы как в C, так и в C++, но дают разные результаты в этих языках.
'a'
, имеют тип
int
в C и тип
char
в C++, это означает, что
sizeof 'a'
обычно даёт разные результаты на двух языках: в C++ это будет
1
, в то время как в C это будет
sizeof(int)
. Как ещё одно следствие этого различия в типах, в C
'a'
всегда будет выражением со знаком, независимо от того, является
char
знаковым или беззнаковым, тогда как для C++ это зависит от реализации компилятора (
англ.
implementation specific
).
const
-переменных в области пространства имён, если только они явно не объявлены как
extern
, в отличие от C, в котором
extern
является вариантом по умолчанию для всех сущностей, имеющих область видимости — файл (
англ.
file-scoped entities
). Заметим, что на практике это не приводит к скрытым семантическим изменениям между идентичным кодом C и C++, но вместо этого приведёт к ошибке компиляции или компоновки.
extern
было вручную добавлено ровно в одну единицу трансляции, чтобы гарантировать, что не-
inline
версия скомпонована, тогда как C++ обрабатывает это автоматически. Если точнее, C различает два вида определений встроенных функций: обычные внешние определения (где явно используется
extern
) и встроенные определения. C++, с другой стороны, предоставляет только встроенные определения для встроенных функций. В C встроенное определение аналогично внутреннему (то есть статическому) определению в том смысле, что оно может сосуществовать в одной и той же программе с одним внешним определением и любым количеством внутренних и встроенных определений одной и той же функции в других единицах трансляции, все из которых могут отличаться. Это не то же самое, что
компоновка
функции, но не полностью независимое понятие. Компиляторам C предоставляется свобода выбора между использованием встроенных и внешних определений одной и той же функции, когда оба они доступны. C++, однако, требует, чтобы если функция с внешней компоновкой объявлена как
inline
в любой единице трансляции, то она должна также быть объявлена (и, следовательно, также определена) в каждой единице трансляции, где используется, и чтобы все определения этой функции были идентичны по правилу одного определения. Обратите внимание, что статические встроенные функции ведут себя одинаково в C и C++.
bool
с константами
true
и
false
, но они определены по-разному. В C++
bool
— это
встроенный тип
и
зарезервированное ключевое слово
. В C99 новое ключевое слово
_Bool
вводится как новый логический тип. Заголовок
stdbool.h
содержит макросы
bool
,
true
и
false
, которые определены как
_Bool
,
1
и
0
, соответственно. Следовательно,
true
и
false
имеют тип
int
в C.
Некоторые другие различия из предыдущего раздела также могут быть использованы для создания кода, который компилируется на обоих языках, но ведёт себя по-разному. Например, следующая функция будет возвращать разные значения в C и C++:
extern int T;
int size(void)
{
struct T { int i; int j; };
return sizeof(T);
/* C: вернёт sizeof(int)
* C++: вернёт sizeof(struct T)
*/
}
Это связано с тем, что C требует наличие
struct
перед тегами структуры (и поэтому
sizeof(T)
ссылается на переменную), но C++ позволяет его опустить (и поэтому
sizeof(T)
ссылается на неявный
typedef
). Имейте в виду, что результат различается, когда объявление
extern
помещается внутрь функции: тогда наличие идентификатора с тем же именем в области видимости функции препятствует вступлению в силу неявного
typedef
для C++, и результат для C и C++ будет одинаковым. Обратите также внимание, что двусмысленность в приведённом выше примере связана с использованием круглых скобок у оператора
sizeof
. При использовании
sizeof T
ожидалось бы, что
T
будет выражением, а не типом, и, следовательно, пример не будет компилироваться на C++.
В то время как C и C++ поддерживают высокую степень совместимости исходных текстов, объектные файлы, создаваемые их компиляторами, могут иметь важные различия, которые проявляются при смешивании кода C и C++. Важные особенности:
Чтобы код на C++ вызывал функцию на C
foo()
, код на C++ должен создавать
прототип
foo()
с помощью
extern "C"
. Аналогично, чтобы код на C вызывал функцию на C++
bar()
, код C++ для
bar()
должен быть объявлен с
extern "C"
.
Обычная практика в заголовочных файлах для поддержания совместимости как с C, так и C++ — добавлять в них объявление с
extern "C"
для всей области видимости заголовка
:
/* Заголовочный файл foo.h */
# ifdef __cplusplus /* Если это компилятор C++, использовать компоновку, как в C */
extern "C" {
# endif
/* У этих функций компоновка, как в языке C */
void foo();
struct bar { /* ... */ };
# ifdef __cplusplus /* Если это компилятор C++, завершите использование компоновки, как в C */
}
# endif
Различия между соглашениями о компоновке и вызовах C и C++ также могут иметь некие последствия для кода, использующего указатели на функции. Некоторые компиляторы дадут нерабочий код, если указатель на функцию, объявленный как
extern "C"
, указывает на функцию из C++, которая не объявлена как
extern "C"
.
Например, следующий код:
void my_function();
extern "C" void foo(void (*fn_ptr)(void));
void bar()
{
foo(my_function);
}
Компилятор C++ от Sun Microsystems выдаёт следующее предупреждение:
$ CC -c test.cc
"test.cc", line 6: Warning (Anachronism): Formal argument fn_ptr of type
extern "C" void(*)() in call to foo(extern "C" void(*)()) is being passed
void(*)().
Это связано с тем, что
my_function()
не объявляется с помощью соглашений о компоновке и вызове языка C, но передаётся C-функции
foo()
.