Чи може код, що є дійсним як для C, так і для C ++, спричинити різну поведінку при компіляції на кожній мові?


664

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

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

Щоб зробити його розумним / корисним порівнянням (я намагаюся дізнатися щось практично корисне, а не намагатися знайти очевидні лазівки у питанні), припустимо:

  • Нічого, що стосується препроцесорів (що означає відсутність хакерів #ifdef __cplusplus, прагм тощо)
  • Будь-яка визначена реалізація однакова в обох мовах (наприклад, числові обмеження тощо)
  • Ми порівнюємо досить недавні версії кожного стандарту (наприклад, C ++ 98 та C90 або новіші).
    Якщо версії мають значення, то будь ласка, зазначте, які версії кожної з них мають різну поведінку.

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
користувач541686

17
+1, другий приклад є хорошим для того, що C ++ не вимагає structструктурних імен.
Сет Карнегі

1
@Andrey я думав те саме саме тому і перевірив це, і він працював на 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
@Sogartar, справді? Код- компілятори codepad.org/STSQlUhh C99 попередить вас, але вони все одно дозволять вам скласти його.
jrajav

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 */це лише коментар. Перша /і the 2обидві частини ініціалізації, тому виходить:

int a = 10 / 2 + 3;

Отже, правильний компілятор C ++ дасть 13, але строго правильний компілятор C90 8. Звичайно, я тут просто вибрав довільні числа - ви можете використовувати інші числа, як вважаєте за потрібне.


34
ХТО це розумно !! З усіх можливих речей, я б ніколи не думав, що коментарі можуть бути використані для зміни поведінки ха-ха. +1
користувач541686

89
навіть без цього 2воно буде читати як 10 / + 3дійсне (unary +).
Бенуа

12
Тепер для задоволення змініть його так, щоб C і C ++ обидва обчислювали різні арифметичні вирази і оцінювали на один і той же результат.
Райан К. Томпсон

21
@RyanThompson Trivial. s /
2/1

4
@Mehrdad Я помиляюся чи коментарі стосуються препроцесорів? Таким чином, їх слід виключити як можливу відповідь із вашого запитання! ;-)
Але

179

C90 проти C ++ 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означає щось зовсім інше, воно вказує компілятору вивести тип змінної зі значення, яке використовується для її ініціалізації.


10
C90 має auto?
Сет Карнегі

22
@SethCarnegie: Так, це клас зберігання; це те, що відбувається за замовчуванням, коли ви його опускаєте, тому ніхто не користувався ним, і вони змінили його значення. Я думаю, що це intза замовчуванням. Це розумно! +1
користувач541686

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.


2
Цікаво. Хтось знає обґрунтування цієї зміни?
антред

3
оскільки "true" - це ключове слово / дійсне значення, тому його оцінюють як true як і будь-яке "справжнє значення" (так, як і будь-яке додатне ціле число). Ви все ще можете зробити #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 ++ типом перелік є його перерахунок. В 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 ++ функція, оголошена з порожнім списком парам, не бере аргументів. У списку порожніх парам 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 або більше елементів, масив повинен бути більшим, ніж адреса його першого елемента.
finnw

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через неявне перетворення подвійного на-int перед викликом int abs(int). В C ви повинні використовувати fabsдля роботи double.


5
довелося налагоджувати код когось іншого з цим питанням. О, як я це любив. У будь-якому разі ваша програма також надрукує 0 на C ++. C ++ повинні використовувати заголовок "cmath". Див. Порівняння перший onein returnin 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 полягає в тому, що вам не потрібно точно знати, наскільки великий тип.
Dana Sane

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(bol rvalue), то воно перевертається, в результаті чого true. Обидва trueі falseє значеннями bool в 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. Структури тощо приховують речі в межах, як у вашому прикладі.



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за замовчуванням.


Я не думаю, що так. Напевно, ви пропустили суть. Мова не йде про визначення stru st, яке використовується лише для того, щоб зробити код дійсним c ++. Справа в тому, що вона висвітлює різну поведінку вбудованих функцій в c vs c ++. Це ж стосується і зовнішньої. Жодне з них не обговорюється в жодному з рішень.
fkl

2
Чим відрізняється поведінка вбудованих функцій і externщо продемонстровано тут?
Сет Карнегі

Це написано досить чітко. "Вбудовані функції в c за замовчуванням до зовнішньої області, де, як ті, що в c ++ не є (код показує, що). Також C ++ неявно розглядає будь-який const global як область файлу, якщо це явно не оголошено зовнішнім, на відміну від C, у якому extern є типовим." приклад можна створити для цього ". Я спантеличений - чи це не зрозуміло?
fkl

12
@fayyazkl Поведінка показана лише через різницю пошуку ( struct funvs fn) і не має нічого спільного з тим, чи функція вбудована. Результат ідентичний, якщо ви вилучите inlineкваліфікатор.
Алекс Б

3
У ISO C ця програма погано сформована: inlineне була додана до C99, але в C99 fun()не може бути названа без прототипу за обсягом. Тож я припускаю, що ця відповідь стосується лише GNU C.
MM

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. (Див. "Найбільш роздратований синтаксичний аналіз", який є іншим, але подібним питанням).
користувач253751

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

Ця програма друкує 128( 32 * sizeof(double)) при компіляції за допомогою компілятора C ++ та 4при компіляції за допомогою компілятора C.

Це тому, що C не має поняття роздільної здатності. У структурах С, що містяться в інших структурах, потрапляють в область зовнішньої структури.


Цей цікавий! (Я думаю, ви маєте на увазі, 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:

$ 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

Ви маєте на увазі специфікацію зв'язку?
користувач541686

назва mangling. У іменах C ++ є префікси та суфікси, тоді як C ні
CoffeDeveloper

Ім'я mangling не є частиною специфікації 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 ​​і rvalus в C і C ++.

У мові програмування на С і оператори до збільшення, і операції після збільшення повертають rvalues, а не lvalues. Це означає, що вони не можуть знаходитись зліва від =оператора призначення. Обидва ці твердження дадуть помилку компілятора в 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 ++ оператор попереднього збільшення повертає значення " lvalue" , тоді як оператор "post-increment" повертає rvalue. Це означає, що вираз з оператором попереднього збільшення може бути розміщено зліва від =оператора присвоєння!

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 повертається виразом, воно є оцінкою. Він більше не представляє поточний вміст змінної.

Попередній приріст спочатку збільшує змінну, а потім повертає змінну, як це було після того, як приріст відбувся. У цьому випадку нам не потрібно зберігати старе значення змінної у тимчасовий регістр. Ми просто отримуємо нове значення змінної після її збільшення. Отже, попередній приріст повертає lvalue, він повертає змінну a собі. Ми можемо використовувати присвоєння цього значення іншому, це як наступне твердження. Це неявна конверсія lvalue у rvalue.

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

Оскільки попередній приріст повертає значення, ми можемо також призначити йому щось. Наступні два твердження ідентичні. У другому призначенні спочатку a збільшується, потім його нове значення перезаписується на 2.

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

3
Тут немає "дійсних на С".
o11c

0

Порожні структури мають розмір 0 в С і 1 в С ++:

#include <stdio.h>

typedef struct {} Foo;

int main()
{
    printf("%zd\n", sizeof(Foo));
    return 0;
}

1
Ні, різниця полягає в тому, що C не має порожніх структур, за винятком розширення компілятора, тобто цей код не відповідає "дійсний як для C, так і для C ++"
Анті Хаапала
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.