Interested Article - Совместимость C и C++

Языки программирования 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 недопустим в C++. Обоснование этого приведено в Приложении C.1 к стандарту ISO C++ .

  • Одно из часто встречающихся отличий заключается в том, что C более слабо типизирован в отношении указателей. В частности, 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);
    
  • C++ имеет более сложные правила присваивания указателей, которые добавляют квалификаторы, поскольку C++ позволяет приводить int ** к const int *const * , но не допускает небезопасного присваивания const int ** , в то время как C не допускает ни того, ни другого (хотя компиляторы обычно выдают только предупреждение).
  • C++ изменяет некоторые функции стандартной библиотеки языка Си , добавляя дополнительные перегруженные функции с квалификатором типа const , например, strchr возвращает char* в C, в то время как C++ поступает так, как если бы существовали две перегруженные функции const char *strchr(const char *) и char *strchr(char *) .
  • C++ также более строг в преобразованиях в перечисления: целые числа не могут быть неявно преобразованы в перечисления, как в C. Кроме того, константные перечисления ( enum enumerators) всегда имеют тип int в C, тогда как в C++ они являются различными типами и могут иметь размер, отличный от размера int .
  • В C++ const -переменная должна быть инициализирована; в C это необязательно.
  • Компиляторы C++ запрещают goto или switch пересекать инициализацию, как в следующем коде на C99:
    void fn(void)
    {
        goto flack;
        int i = 1;
    flack:
        ;
    }
    
  • Несмотря на синтаксическую корректность, функция longjmp() приводит к неопределённому поведению в C++, если пропущенные ( англ. jumped-over ) фреймы стека содержат объекты с нетривиальными деструкторами . Имплементация C++ может произвольно определять поведение таким образом, чтобы деструкторы вызывались. Однако это исключает некоторые варианты использования longjmp() , которые в противном случае были бы допустимы, например, имплементация потоков или сопрограмм , переключающихся между отдельными стеками вызовов с помощью longjmp() — при переходе из нижнего стека вызовов в верхний в глобальном адресном пространстве деструкторы вызывались бы для каждого объекта в нижнем стеке вызовов. В C такой проблемы не существует.
  • C допускает несколько предварительных определений одной глобальной переменной в одной единице трансляции , что недопустимо в C++, так как это нарушение правила одного определения ( англ. One Definition Rule, ODR ).
    int N;
    int N = 10;
    
  • В C допустимо объявление нового типа с тем же именем, что и у struct , union или enum , но это недопустимо в C++, потому что в C типы struct , union и enum должны указываться всякий раз, когда на этот тип ссылаются, тогда как в C++ все объявления таких типов неявно содержат .
    enum BOOL {FALSE, TRUE};
    typedef int BOOL;
    
  • Объявления функций без прототипов (в стиле «K&R») недопустимы в C++; они по-прежнему действительны в C , хотя были признаны устаревшими с момента первой стандартизации C в 1990 году. «Устаревший» ( англ. obsolescent ) — это термин, которому даётся определение в стандарте ISO C, он означает языковую возможность, которая «может быть удалена в будущих версиях» стандарта. Аналогично, неявные объявления функций (использование функций, которые не были объявлены) не допускаются в C++ и являются недопустимыми в C с 1999 года.
  • В C прототип функции без аргументов, например int foo(); , подразумевает, что аргументы не указаны. Следовательно, допустимо вызывать такую функцию с одним или несколькими аргументами , например foo(42, "hello world") . Напротив, в C++ прототип функции без аргументов означает, что функция не принимает аргументов, и вызов такой функции с аргументами является некорректным. В C правильный способ объявить функцию, которая не принимает аргументов, — это использовать 'void', как в int foo(void); , это также допустимо в C++. Пустые прототипы функций являются устаревшей ( англ. deprecated ) возможностью в C99 (как и в C89).
  • Как на C, так и на C++ можно определить вложенные типы struct , но область действия интерпретируется по-разному: в C++ вложенный тип struct определяется только в пределах области видимости/пространства имён внешнего типа struct , тогда как в C внутренняя структура также определяется вне внешней структуры.
  • 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
    }
    
  • Последний элемент структурного типа в стандарте C99 с более чем одним элементом может быть , который принимает синтаксическую форму массива с неопределённой длиной. Это служит цели, аналогичной массивам переменной длины, но массивы переменной длины не могут отображаться в определениях типов, и, в отличие от массивов переменной длины, элементы гибкого массива не имеют определенного размера. ISO C++ не имеет такой особенности. Пример:
    struct X
    {
        int n, m;
        char bytes[];
    }
    
  • Квалификатор типа restrict , определённый в C99, не был включён в стандарт C++03, но большинство основных компиляторов, таких как GCC , Microsoft Visual C++ и Intel C++ Compiler предоставляют аналогичную функциональность в качестве расширения.
  • Квалификаторы параметров массива в функциях поддерживаются в C, но не в C++.
    int foo(int a[const]);     // аналогично int *const a
    int bar(char s[static 5]); // отмечает, что s имеет длину не менее 5 символов
    
  • Функциональность составных литералов в C обобщается как на встроенные, так и на пользовательские типы с помощью синтаксиса списочной инициализаци в C++11, хотя и с некоторыми синтаксическими и семантическими различиями.
    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) {}
    
  • Назначенные инициализаторы для массивов допустимы только в C:
    char s[20] = { [0] = 'a', [8] = 'g' };  // Допустимо в C, но не в C++
    
  • Функции, которые не возвращают значение, могут быть отмечены с помощью атрибута noreturn в C++, тогда как C использует другое ключевое слово.

C++ добавляет множество дополнительных ключевых слов для поддержки своих новых возможностей. Это делает код на C, использующий эти ключевые слова для идентификаторов, недопустимым в C++. Например, такой код:

struct template
{
    int new;
    struct template* class;
};
является допустимым кодом на C, но отклоняется компилятором C++, поскольку ключевые слова template , new и class зарезервированы.

Конструкции, которые ведут себя по-разному в C и C++

Существует несколько синтаксических конструкций, которые допустимы как в C, так и в C++, но дают разные результаты в этих языках.

  • , такие как 'a' , имеют тип int в C и тип char в C++, это означает, что sizeof 'a' обычно даёт разные результаты на двух языках: в C++ это будет 1 , в то время как в C это будет sizeof(int) . Как ещё одно следствие этого различия в типах, в C 'a' всегда будет выражением со знаком, независимо от того, является char знаковым или беззнаковым, тогда как для C++ это зависит от реализации компилятора ( англ. implementation specific ).
  • C++ использует внутреннюю компоновку const -переменных в области пространства имён, если только они явно не объявлены как extern , в отличие от C, в котором extern является вариантом по умолчанию для всех сущностей, имеющих область видимости — файл ( англ. file-scoped entities ). Заметим, что на практике это не приводит к скрытым семантическим изменениям между идентичным кодом C и C++, но вместо этого приведёт к ошибке компиляции или компоновки.
  • В C использование встроенных функций требует, чтобы объявление прототипа функции с использованием ключевого слова extern было вручную добавлено ровно в одну единицу трансляции, чтобы гарантировать, что не- inline версия скомпонована, тогда как C++ обрабатывает это автоматически. Если точнее, C различает два вида определений встроенных функций: обычные внешние определения (где явно используется extern ) и встроенные определения. C++, с другой стороны, предоставляет только встроенные определения для встроенных функций. В C встроенное определение аналогично внутреннему (то есть статическому) определению в том смысле, что оно может сосуществовать в одной и той же программе с одним внешним определением и любым количеством внутренних и встроенных определений одной и той же функции в других единицах трансляции, все из которых могут отличаться. Это не то же самое, что компоновка функции, но не полностью независимое понятие. Компиляторам C предоставляется свобода выбора между использованием встроенных и внешних определений одной и той же функции, когда оба они доступны. C++, однако, требует, чтобы если функция с внешней компоновкой объявлена как inline в любой единице трансляции, то она должна также быть объявлена (и, следовательно, также определена) в каждой единице трансляции, где используется, и чтобы все определения этой функции были идентичны по правилу одного определения. Обратите внимание, что статические встроенные функции ведут себя одинаково в C и C++.
  • И C99, и 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++. Важные особенности:

  • Компиляторы 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() .

Примечания

  1. Stroustrup, Bjarne (PDF) 4. Дата обращения: 12 августа 2009. 16 августа 2012 года.
  2. B.Stroustrup. Дата обращения: 17 марта 2019. 21 декабря 2018 года.
  3. Дата обращения: 22 сентября 2019. 6 февраля 2016 года.
  4. B. Stroustrup. Дата обращения: 18 августа 2013. 22 июля 2012 года.
  5. см. The UNIX-HATERS Handbook , с.208
  6. 6 июня 2016 года. , revision 5.10 (April 2003).
  7. . gnu.org . 26 марта 2014 года.
  8. . 7 декабря 2017 года. («It is invalid to jump past a declaration with explicit or implicit initializer (except across entire block not entered). … With this simple compile-time rule, C++ assures that if an initialized variable is in scope, then it has assuredly been initialized.»)
  9. . 7 декабря 2017 года.
  10. . ibm.com .
  11. . faq.cprogramming.com . 5 апреля 2007 года.
  12. (16 апреля 2015). 25 сентября 2016 года.
  13. . www.cplusplus.com . 19 мая 2018 года.
  14. . Дата обращения: 28 июля 2022. 29 марта 2018 года.
  15. . en.cppreference.com . 15 июля 2017 года.
  16. . 9 апреля 2006 года.
  17. 6 августа 2016 года. from Using the GNU Compiler Collection (GCC)
  18. . ibm.com .
  19. . ibm.com .
  20. . Docs.sun.com. Дата обращения: 18 августа 2013. 3 апреля 2009 года.

Ссылки

  • , sentence by sentence, from a C89 Standard perspective.
  • , David R. Tribble (August 2001).
  • , Oracle/Sun compiler docs on linkage scope.
  • , overview by Steve Clamage (ANSI C++ Committee chair).
Источник —

Same as Совместимость C и C++