Может ли код, действительный как на C, так и на C ++, вызывать различное поведение при компиляции на каждом языке?

664

C и C ++ имеют много различий, и не весь действительный код C является допустимым кодом C ++.
(Под «допустимым» я подразумеваю стандартный код с определенным поведением, то есть не зависящим от реализации / неопределенным / и т. Д.)

Есть ли сценарий, в котором фрагмент кода, действительный как на C, так и на C ++, будет иметь разное поведение при компиляции со стандартным компилятором на каждом языке?

Чтобы сделать это разумное / полезное сравнение (я пытаюсь изучить что-то практически полезное, а не пытаться найти очевидные лазейки в вопросе), давайте предположим:

  • Ничего не связанного с препроцессором (что означает отсутствие хаков #ifdef __cplusplus, прагм и т. Д.)
  • Все, что определяется реализацией, одинаково в обоих языках (например, числовые ограничения и т. Д.)
  • Мы сравниваем разумно последние версии каждого стандарта (например, C ++ 98 и C90 или более поздние
    версии ). Если версии имеют значение, то, пожалуйста, укажите, какие из версий имеют различное поведение.
user541686
источник
11
Кстати, может быть полезно программировать на диалекте, который является C и C ++ одновременно. Я делал это в прошлом и в одном текущем проекте: язык TXR. Интересно, что разработчики языка Lua сделали то же самое, и они называют этот диалект «Чистый С». Вы получаете преимущество лучшей проверки времени компиляции и, возможно, дополнительной полезной диагностики от компиляторов C ++, но сохраняете переносимость C.
Каз
9
Я слил старый вопрос в этот вопрос, так как у него больше мнений и ответов с повышенным голосованием. Это все еще пример неконструктивного вопроса, но он довольно пограничный, так как да, он действительно чему-то учит пользователей SO. Я закрываю это как не конструктивное только для того, чтобы отразить состояние вопроса до слияния. Не стесняйтесь не соглашаться и вновь открывать.
Джордж Стокер
13
Голосование возобновить, поскольку я думаю, что на него можно объективно ответить «да» и последующим примером (как показано ниже). Я думаю, что это конструктивно, потому что люди могут извлечь из этого соответствующее поведение.
Андерс Абель
6
@AndersAbel Чистое количество ответов, все из которых являются правильными, однозначно демонстрирует, что это остается вопросом «составить список». Вы не могли бы задать этот вопрос, не получив список.
dmckee --- котенок экс-модератора
2
@dmckee Для чего это стоит, я согласен с вами. Тем не менее, люди с тегами C ++ ... скажем так ... отважные .
Джордж Стокер

Ответы:

397

Следующее, допустимое в C и C ++, (скорее всего) приведет к различным значениям iв C и C ++:

int i = sizeof('a');

См. Размер символа ('a') в C / C ++ для объяснения различия.

Еще один из этой статьи :

#include <stdio.h>

int  sz = 80;

int main(void)
{
    struct sz { char c; };

    int val = sizeof(sz);      // sizeof(int) in C,
                               // sizeof(struct sz) in C++
    printf("%d\n", val);
    return 0;
}
Алексей Фрунзе
источник
8
Определенно не ожидал этого! Я надеялся на что-то более драматичное, но это все еще полезно, спасибо. :) +1
user541686
17
+1 второй пример хорош тем, что C ++ не требует structструктурных имен.
Сет Карнеги
1
@ Андрей Я тоже так думал некоторое время назад и протестировал его, и он работал на GCC 4.7.1 без std, вопреки моим ожиданиям. Это ошибка в GCC?
Сет Карнеги
3
@SethCarnegie: Несоответствующая программа не обязательно должна работать, но она также не гарантируется.
Андрей Вихров
3
struct sz { int i[2];};будет означать, что C и C ++ должны выдавать разные значения. (Принимая во внимание, что DSP с sizeof (int) == 1, мог бы произвести то же самое значение).
Мартин Боннер поддерживает Монику
464

Вот пример, который использует разницу между вызовами функций и объявлениями объектов в C и C ++, а также тот факт, что C90 позволяет вызывать необъявленные функции:

#include <stdio.h>

struct f { int x; };

int main() {
    f();
}

int f() {
    return printf("hello");
}

В C ++ это ничего не печатает, потому что временный объект fсоздается и уничтожается, но в C90 он печатает, helloпотому что функции могут быть вызваны без объявления.

В случае, если вам интересно, что имя fиспользуется дважды, стандарты C и C ++ явно разрешают это и создают объект, который вы должны сказать, struct fчтобы устранить неоднозначность, если вы хотите структуру, или пропустить, structесли вы хотите функцию.

Сет Карнеги
источник
7
Строго говоря, в C это не скомпилируется, потому что объявление «int f ()» после определения «int main ()» :)
Sogartar
15
@ Согартар, правда? codepad.org/STSQlUhh Компиляторы C99 предупредят вас, но все равно позволят вам скомпилировать.
2012 года
22
@Sogartar в функциях C допускается неявно объявлять.
Алекс Б
11
@AlexB Не в C99 и C11.
6
@jrajav Тогда это не компиляторы C99. Компилятор C99 обнаруживает необъявленные идентификаторы как синтаксическую ошибку. Компилятор, который этого не делает, является либо компилятором C89, либо предстандартным, либо другим типом несовместимого компилятора.
430

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

int a = 10 //* comment */ 2 
        + 3;

В C ++ все от //конца до конца строки является комментарием, так что это работает так:

int a = 10 + 3;

Поскольку C90 не имеет однострочных комментариев, это только /* comment */комментарий. Первый /и 2оба являются частями инициализации, так что получается:

int a = 10 / 2 + 3;

Таким образом, правильный компилятор C ++ даст 13, но строго правильный компилятор C90 8. Конечно, я просто выбрал здесь произвольные числа - вы можете использовать другие числа по своему усмотрению.

Джерри Гроб
источник
34
Вау это умопомрачительно !! Из всех возможных вещей, о которых я никогда бы не подумал, комментарии могут быть использованы для изменения поведения, хаха. +1
user541686
89
даже без 2, он будет читаться как 10 / + 3действительный (унарный +).
Бенуа
12
Теперь ради интереса, измените его так, чтобы C и C ++ оба вычисляли разные арифметические выражения для вычисления одного и того же результата.
Райан К. Томпсон
21
@RyanThompson Trivial. с /
2/1
4
@ Mehrdad Я не прав или комментарии связаны с препроцессором? Таким образом, они должны быть исключены в качестве возможного ответа на ваш вопрос! ;-)
Ale
179

С90 против С ++ 11 ( intпротив double):

#include <stdio.h>

int main()
{
  auto j = 1.5;
  printf("%d", (int)sizeof(j));
  return 0;
}

В C autoозначает локальную переменную. В C90 можно опустить переменную или тип функции. По умолчанию оно int. В C ++ 11 autoозначает нечто совершенно иное, он говорит компилятору выводить тип переменной из значения, используемого для ее инициализации.

detunized
источник
10
С90 имеет auto?
Сет Карнеги
22
@SethCarnegie: Да, это класс хранения; это то, что происходит по умолчанию, когда вы его опускаете, поэтому никто не использовал его, и они изменили его значение. Я думаю, что это intпо умолчанию. Это умно! +1
user541686
5
C11 не имеет неявного int.
R .. GitHub ОСТАНОВИТЬ ЛЬДА
23
@KeithThompson Ах, я думаю, вы имеете в виду вывод int. Тем не менее, в реальном мире, где есть тонны унаследованного кода, а лидер рынка до сих пор не внедрил C99 и не собирается этого делать, говорить об «устаревшей версии C» абсурдно.
Джим Балтер
11
«Каждая переменная ДОЛЖНА иметь явный класс хранения. С уважением, высшее руководство».
btown
120

Другой пример, о котором я еще не упоминал, этот, подчеркивающий разницу препроцессора:

#include <stdio.h>
int main()
{
#if true
    printf("true!\n");
#else
    printf("false!\n");
#endif
    return 0;
}

Это печатает «false» в C и «true» в C ++ - в C любой неопределенный макрос оценивается в 0. В C ++ есть 1 исключение: «true» оценивается в 1.

godlygeek
источник
2
Интересно. Кто-нибудь знает обоснование этого изменения?
antred
3
потому что «истина» является ключевым словом / допустимым значением, поэтому оно оценивается как истинное, как любое «истинное значение» (как любое положительное целое число). Вы все еще можете сделать #define true false, чтобы напечатать «false» также в C ++;)
CoffeDeveloper
22
#define true false ಠ_ಠ
Брайан
2
@DarioOO не приведет ли такое переопределение к UB?
Руслан
3
@DarioOO: Да, вы не правы. Переопределение ключевых слов не допускается, наказание оставлено на произвол судьбы (UB). Препроцессор, являющийся отдельной фазой компиляции, не выдерживает.
Дедупликатор
108

Согласно стандарту C ++ 11:

а. Оператор запятой выполняет преобразование lvalue в rvalue в C, но не в C ++:

   char arr[100];
   int s = sizeof(0, arr);       // The comma operator is used.

В C ++ значение этого выражения будет 100, а в C это будет sizeof(char*).

б. В C ++ тип перечислителя - это его перечисление. В С типом перечислителя является int.

   enum E { a, b, c };
   sizeof(a) == sizeof(int);     // In C
   sizeof(a) == sizeof(E);       // In C++

Это означает, что sizeof(int)не может быть равен sizeof(E).

с. В C ++ функция, объявленная с пустым списком параметров, не принимает аргументов. В C пустой список параметров означает, что количество и тип функциональных параметров неизвестен.

   int f();           // int f(void) in C++
                      // int f(*unknown*) in C
Кирилл Кобелев
источник
Первый также определяется реализацией, как у Алексея. Но +1.
Сет Карнеги
1
@ Seth, Все вышеперечисленные материалы взяты непосредственно из Приложения C.1 стандарта C ++ 11.
Кирилл Кобелев
Да, но это все еще определяется реализацией. sizeof(char*)может быть 100, в этом случае первый пример будет производить то же наблюдаемое поведение в C и C ++ (то есть, хотя метод получения sбудет другим, sв конечном итоге будет 100). ОП упомянул, что этот тип поведения, определяемого реализацией, был хорош, поскольку он просто хотел избежать ответов юристов, поэтому первый вариант подходит для его исключения. Но второй хорош в любом случае.
Сет Карнеги
5
Это легко исправить - просто измените пример на:char arr[sizeof(char*)+1]; int s = sizeof(0, arr);
Mankarse
5
Чтобы избежать различий, связанных с реализацией, вы также можете использовать void *arr[100]. В этом случае элемент имеет тот же размер, что и указатель на тот же элемент, поэтому, если имеется 2 или более элементов, массив должен быть больше адреса его первого элемента.
Финнв
53

Эта программа печатает 1на C ++ и 0на C:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int d = (int)(abs(0.6) + 0.5);
    printf("%d", d);
    return 0;
}

Это происходит потому, что double abs(double)в C ++ есть перегрузка, поэтому он abs(0.6)возвращается, 0.6тогда как в C он возвращается 0из-за неявного преобразования типа double в int перед вызовом int abs(int). В C вы должны использовать fabsдля работы double.

Павел Чикулаев
источник
5
пришлось отлаживать код другого человека с этой проблемой. О, как я любил это. В любом случае, ваша программа также печатает 0 в C ++. C ++ должен использовать заголовок "cmath", см. Сравнение сначала один возвращается в 0 ideone.com/0tQB2G 2-й возвращается 1 ideone.com/SLeANo
CoffeDeveloper
Рад / жаль слышать, что я не единственный, кто находит эту разницу с помощью отладки. Только что протестированный в VS2013, пустой файл с единственным содержимым будет содержать 1, если расширение - .cpp, и 0, если расширение - .c. Похоже, <math.h> включен косвенно в VS.
Павел Чикулаев
И, похоже, в VS C ++ <math.h> включает в себя C ++ в глобальное пространство имен, а GCC - нет. Однако не уверен, что это стандартное поведение.
Павел Чикулаев
2
Этот конкретный пример кода зависит от реализации: stdlib.hтолько определяет abs(int)и abs(long); версия abs(double)объявлена math.h. Так что эта программа еще может называть abs(int)версию. Это деталь реализации, которая stdlib.hтакже math.hдолжна быть включена. (Я думаю, что это будет ошибка, если бы abs(double)были вызваны, но другие аспекты math.hне были включены).
ММ
1
Вторичная проблема заключается в том, что, хотя стандарт C ++, кажется, говорит, что включение <math.h>также включает в себя дополнительные перегрузки; на практике оказывается, что все основные компиляторы не включают эти перегрузки, если не используется форма <cmath>.
ММ
38
#include <stdio.h>

int main(void)
{
    printf("%d\n", (int)sizeof('a'));
    return 0;
}

В C это печатает независимо от значения sizeof(int)текущей системы, которая обычно используется 4в большинстве современных систем.

В C ++ это должно вывести 1.

Адам Розенфилд
источник
3
Да, я на самом деле был знаком с этим трюком, потому что 'c' - это int в C и char в C ++, но все же хорошо, чтобы он был здесь перечислен.
Шон,
9
Это сделало бы интересный вопрос для интервью - особенно для людей, которые включили эксперта по c / c ++ в свои резюме
Мартин Беккет
2
Вид закулисного хотя. Цель sizeof заключается в том, что вам не нужно точно знать, насколько велик тип.
Дана Саня
13
В C значение определяется реализацией, а 1 - возможность. (В C ++ он должен печатать 1, как указано.)
Программист Windows,
3
На самом деле он имеет неопределенное поведение в обоих случаях. %dнеправильный спецификатор формата для size_t.
R .. GitHub ОСТАНОВИТЬ ЛЬДА
37

Еще одна sizeofловушка: логические выражения.

#include <stdio.h>
int main() {
    printf("%d\n", (int)sizeof !0);
}

Это равно sizeof(int)C, потому что выражение имеет тип int, но обычно 1 в C ++ (хотя это не обязательно). На практике они почти всегда разные.

Алекс Б
источник
6
Один !должен быть достаточно для bool.
Алексей Фрунзе
4
!! является оператором преобразования int в логическое значение :)
EvilTeach
1
sizeof(0)находится 4как в C, так и в C ++, потому что 0является целочисленным значением. sizeof(!0)находится 4в C и 1в C ++. Логическое НЕ действует на операнды типа bool. Если значение типа int 0неявно преобразуется в false(значение bool r), то оно переворачивается, что приводит к true. Оба trueи falsebool rvalues ​​в C ++ и sizeof(bool)является 1. Однако в C !0оценивается 1, что является значением типа int. Язык программирования C не имеет типа данных bool по умолчанию.
Галактика
26

Язык программирования C ++ (3-е издание) дает три примера:

  1. sizeof ('a'), как упомянул @Adam Rosenfield;

  2. // комментарии, используемые для создания скрытого кода:

    int f(int a, int b)
    {
        return a //* blah */ b
            ;
    }
  3. Структуры и т. Д. Прячут вещи в области видимости, как в вашем примере.

derobert
источник
25

Старый каштан, который зависит от компилятора C, не распознает комментарии конца строки C ++ ...

...
int a = 4 //* */ 2
        +2;
printf("%i\n",a);
...
dmckee --- котенок экс-модератора
источник
21

Еще один из перечисленных в стандарте C ++:

#include <stdio.h>

int x[1];
int main(void) {
    struct x { int a[2]; };
    /* size of the array in C */
    /* size of the struct in C++ */
    printf("%d\n", (int)sizeof(x)); 
}
Йоханнес Шауб - Литб
источник
так вы получаете различия в отступах?
v.oddou
ах, извини, я понял, xнаверху есть еще один . я думал, ты сказал "массив a".
v.oddou
20

Встроенные функции в C по умолчанию имеют внешнюю область, а не те, что в C ++.

Компилирование следующих двух файлов вместе напечатало бы «Я в строке» в случае GNU C, но ничего для C ++.

Файл 1

#include <stdio.h>

struct fun{};

int main()
{
    fun();  // In C, this calls the inline function from file 2 where as in C++
            // this would create a variable of struct fun
    return 0;
}

Файл 2

#include <stdio.h>
inline void fun(void)
{
    printf("I am inline\n");
} 

Кроме того, C ++ неявно обрабатывает любой constглобальный staticобъект, если он явно не объявлен extern, в отличие от C, который externиспользуется по умолчанию.

ФКЛ
источник
Я действительно так не думаю. Возможно, вы упустили момент. Речь идет не об определении struct st, которое просто используется, чтобы сделать код допустимым c ++. Дело в том, что он подчеркивает различное поведение встроенных функций в c vs c ++. То же относится и к extern. Ни одно из них не обсуждается ни в одном из решений.
ФКЛ
2
Каково различное поведение встроенных функций, и externэто демонстрируется здесь?
Сет Карнеги
Это написано довольно четко. «Встроенные функции в c по умолчанию имеют внешнюю область видимости, в отличие от функций в c ++ (код показывает это). Также C ++ неявно обрабатывает любой глобальный const как область файла, если только он явно не объявлен как extern, в отличие от C, в котором extern является значением по умолчанию. пример может быть создан для этого ". Я озадачен - Разве это не понятно?
ФКЛ
12
@fayyazkl Показанное поведение только из-за различий поиска ( struct funvs fn) и не имеет никакого отношения к тому, является ли функция встроенной. Результат идентичен, если вы удалите inlineклассификатор.
Алекс Б
3
В ISO C эта программа плохо сформирована: inlineне была добавлена ​​до C99, но в C99 fun()не может быть вызвана без прототипа по объему. Поэтому я предполагаю, что этот ответ относится только к GNU C.
ММ
16
struct abort
{
    int x;
};

int main()
{
    abort();
    return 0;
}

Возвращает с кодом выхода 0 в C ++ или 3 в C.

Этот трюк, вероятно, можно было бы использовать для создания чего-то более интересного, но я не мог придумать хорошего способа создания конструктора, который был бы приемлем для C. Я попытался сделать такой же скучный пример с конструктором копирования, который позволил бы аргумент быть переданным, хотя довольно непереносимым способом:

struct exit
{
    int x;
};

int main()
{
    struct exit code;
    code.x=1;

    exit(code);

    return 0;
}

VC ++ 2005 отказался компилировать это в режиме C ++, хотя и жаловался на то, как «код выхода» был переопределен. (Я думаю, что это ошибка компилятора, если я вдруг не забыл, как программировать.) Он завершился с кодом завершения процесса 1, хотя и скомпилирован как C.


источник
Ваш второй пример, использующий exit, к сожалению, не компилируется на gcc или g ++. Это хорошая идея.
Шон
1
exit(code)является допустимым объявлением переменной codeтипа exit, по-видимому. (См. «Самый неприятный анализ», это другая, но похожая проблема).
user253751
16
#include <stdio.h>

struct A {
    double a[32];
};

int main() {
    struct B {
        struct A {
            short a, b;
        } a;
    };
    printf("%d\n", sizeof(struct A));
    return 0;
}

Эта программа prints 128( 32 * sizeof(double)) при компиляции с использованием компилятора C ++ и4 при компиляции с использованием компилятора C.

Это потому, что у C нет понятия разрешения области. В Си структуры, содержащиеся в других структурах, попадают в область действия внешней структуры.

wefwefa3
источник
Это интересно! (Я думаю, что вы имеете в виду, 32*sizeof(double)а не 32, хотя :))
user1354557
3
обратите внимание, что вы получаете UB, печатая size_tс%d
phuclv
7

Не забывайте о разнице между глобальными пространствами имен C и C ++. Предположим, у вас есть foo.cpp

#include <cstdio>

void foo(int r)
{
  printf("I am C++\n");
}

и foo2.c

#include <stdio.h>

void foo(int r)
{
  printf("I am C\n");
}

Теперь предположим, что у вас есть main.c и main.cpp, которые выглядят так:

extern void foo(int);

int main(void)
{
  foo(1);
  return 0;
}

При компиляции как C ++ он будет использовать символ в глобальном пространстве имен C ++; в C он будет использовать C one:

$ diff main.cpp main.c
$ gcc -o test main.cpp foo.cpp foo2.c
$ ./test 
I am C++
$ gcc -o test main.c foo.cpp foo2.c
$ ./test 
I am C
user23614
источник
Вы имеете в виду спецификацию связей?
user541686
искажение имени. Имена C ++ имеют префиксы и суффиксы, в то время как C нет
CoffeDeveloper
Перенос имени не является частью спецификации C ++. Это запрещено в C?
Скайкинг
5
Это неопределенное поведение (множественное определение foo). Не существует отдельных «глобальных пространств имен».
ММ
4
int main(void) {
    const int dim = 5; 
    int array[dim];
}

Это довольно своеобразно тем, что оно действительно в C ++ и в C99, C11 и C17 (хотя и необязательно в C11, C17); но не действует в C89.

В C99 + он создает массив переменной длины, который имеет свои особенности по сравнению с обычными массивами, поскольку имеет тип времени выполнения вместо типа времени компиляции и sizeof arrayне является целочисленным константным выражением в C. В C ++ тип является полностью статическим.


Если вы попытаетесь добавить инициализатор здесь:

int main(void) {
    const int dim = 5; 
    int array[dim] = {0};
}

допустим C ++, но не C, потому что массивы переменной длины не могут иметь инициализатор.

Антти Хаапала
источник
0

Это касается lvalues ​​и rvalues ​​в C и C ++.

В языке программирования C операторы предварительного увеличения и последующего увеличения возвращают значения, а не значения. Это означает, что они не могут быть слева от =оператора присваивания. Оба эти утверждения приведут к ошибке компилятора в C:

int a = 5;
a++ = 2;  /* error: lvalue required as left operand of assignment */
++a = 2;  /* error: lvalue required as left operand of assignment */

Однако в C ++ оператор предварительного увеличения возвращает значение l , а оператор постинкремента возвращает значение r. Это означает, что выражение с оператором предварительного увеличения может быть размещено слева от =оператора присваивания!

int a = 5;
a++ = 2;  // error: lvalue required as left operand of assignment
++a = 2;  // No error: a gets assigned to 2!

Теперь, почему это так? Постинкремент увеличивает переменную и возвращает переменную, которая была до того, как произошло увеличение. На самом деле это просто ценность. Прежнее значение переменной a копируется в регистр как временное, а затем увеличивается на a. Но прежнее значение a возвращается выражением, это rvalue. Он больше не представляет текущее содержимое переменной.

Предварительный инкремент сначала увеличивает переменную, а затем возвращает переменную такой, какой она стала после того, как произошло увеличение. В этом случае нам не нужно сохранять старое значение переменной во временном регистре. Мы просто получаем новое значение переменной после его увеличения. Таким образом, предварительное приращение возвращает lvalue, оно возвращает саму переменную a. Мы можем использовать присвоение этого lvalue чему-то другому, это похоже на следующее утверждение. Это неявное преобразование lvalue в rvalue.

int x = a;
int x = ++a;

Поскольку предварительное приращение возвращает lvalue, мы также можем присвоить ему что-то. Следующие два утверждения идентичны. Во втором присваивании сначала увеличивается значение a, затем его новое значение перезаписывается на 2.

int a;
a = 2;
++a = 2;  // Valid in C++.
галактика
источник
3
Там нет "действительным в C" здесь.
o11c
0

Пустые структуры имеют размер 0 в C и 1 в C ++:

#include <stdio.h>

typedef struct {} Foo;

int main()
{
    printf("%zd\n", sizeof(Foo));
    return 0;
}
Даниэль Фрушинский
источник
1
Нет, разница в том , что C делает не пустые структуры, кроме как расширение компилятора, т.е. этот код не совпадает «действует как в C и C ++»
Antti Haapala