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

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

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

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

  • Ничего не связанного с препроцессором (что означает отсутствие взлома с #ifdef __cplusplus, прагмы и пр.)
  • Все, что определено реализацией, одинаково для обоих языков (например, числовые ограничения и т. Д.)
  • Мы сравниваем достаточно свежие версии каждого стандарта (например, C++98 и C90 или более поздние версии)
    Если версии имеют значение, пожалуйста, укажите, какие из версий имеют различное поведение.

19 ответов

Решение

Следующее, допустимое в 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;
}

Вот пример, который использует разницу между вызовами функций и объявлениями объектов в 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 если вы хотите функцию.

Для 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. Конечно, я просто выбрал здесь произвольные числа - вы можете использовать другие числа по своему усмотрению.

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

#include <stdio.h>

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

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

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

#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.

Согласно стандарту 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 в C++ и 0 в С:

#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,

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

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

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

#include <stdio.h>

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

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

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

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

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

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

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

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

...
int a = 4 //* */ 2
        +2;
printf("%i\n",a);
...

Еще один из перечисленных в стандарте 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)); 
}

Встроенные функции в 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 по умолчанию.

#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;
}

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

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

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.

Не забывайте о различии между глобальными пространствами имен 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
int main(void) {
    const int dim = 5; 
    int array[dim];
}

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

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

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

В языке программирования C операторы предварительного увеличения и последующего увеличения возвращают значения r, а не lvalue. Это означает, что они не могут быть на левой стороне = оператор присваивания. Оба эти утверждения приведут к ошибке компилятора в 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. Он больше не представляет текущее содержимое переменной.

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

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

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

int a;
a = 2;
++a = 2;  // Valid in C++.

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

#include <stdio.h>

typedef struct {} Foo;

int main()
{
    printf("%zd\n", sizeof(Foo));
    return 0;
}
Другие вопросы по тегам