Почему эта программа C соответствует требованиям и работает

С любопытством к определению и области действия typedef я написал ниже код C в 2 файлах .c:

main.c

#include <stdio.h>

int main()
{
    int a = 5, b = 6;
    printf("a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
}

swap.c

typedef T;

void swap(T* a, T* b)
{
    T t = *a;
    *a = *b;
    *b = t;
}

К моему удивлению, файлы кода можно было скомпилировать с помощью компилятора Visual Studio C (cl.exe /Tc main.c swap.c)

И программа работает корректно! Насколько я понимаю, typedef требует 2 параметра, но почему этот код компилируется и запускается?

Для дальнейшего анализа в основной функции я объявил еще 2 переменные с плавающей запятой и попытался также поменять местами обе после замены двух целых чисел, но на этот раз не удалось скомпилировать (с помощью cl.exe). Удивительно, что код можно скомпилировать и запустить с помощью Tiny C (tcc.exe main.c swap.c), так что он работает как шаблонный метод!


person Shuping    schedule 14.05.2013    source источник
comment
Это совсем не похоже на шаблоны.   -  person Iskar Jarak    schedule 14.05.2013


Ответы (2)


Typedef на самом деле является объявлением (он создает псевдонимы для существующих типов) и никоим образом не ограничивается двумя «параметрами». См. раздел объявления Typedef (C). и Основной синтаксис операнда typedef.

Если вы пишете typedef T;, вы объявляете T неуказанным типом (называемым "не указанный тип" в спецификации C89). Это немного (и только очень немного, но концептуально это может вам помочь) похоже на то, что #define X определяет X, но препроцессор заменит его пустой строкой (т.е. удалит X).

Итак, вы typedef делаете T неопределенным, что делает аргументы вашей swap функции неопределенного типа.

Здесь вы видите, что в C89 (но не в C99, где это приводит к неопределенному поведению — отличие ANSI 3.5.2 от ISOC99 6.7.2) неуказанные типы по умолчанию имеют значение int, поэтому ваш метод работает с целыми числами, но не с плавающей запятой в Visual Studio (предположительно, по умолчанию он запрещает неявную типизацию целых чисел). Однако GCC скомпилирует его с числами с плавающей запятой, если вы не используете -Werror, что вам, вероятно, следует делать.

Я настоятельно рекомендую включить некоторые предупреждения: -Wall в gcc, среди прочего, выдаст следующее

swap.c:1:9: warning: type defaults to ‘int’ in declaration of ‘T’ [-Wimplicit-int]

Причина, по которой он «работает» с числами с плавающей запятой, заключается в том, что числа с плавающей запятой и целые числа, вероятно, имеют одинаковый размер (32 бита) на вашей машине. Попробуйте с двойным. Или чар. Или короткий.

swap действительно такой:

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

с тобой звонишь

swap((int*)&a, (int*)&b);

Попробуйте сами и сравните результаты.


Редактировать: я только что попробовал это в tcc. К сожалению, -Wall не предупреждает вас о неявном типе int.

person Iskar Jarak    schedule 14.05.2013
comment
Попробуй и увидишь — это опасная философия, которая приводит к неопределенности и непереносимости кода. Возможно, вы могли бы использовать стандарт C в качестве ссылки... Более конкретно, 6.3.2.1p2 говорит, что если lvalue имеет неполный тип и не имеет типа массива, поведение не определено. Следовательно, поведение не неопределенное, а неопределенное. - person autistic; 14.05.2013
comment
согласен ре. try it and see как философию, но я имел в виду в духе if you don't believe me, please observe the results before discussing it further. Однако ре. неопределенные и неуказанные типы, я считаю, что в C89 ни один спецификатор типа не совпадает с неопределенным. Никакой спецификатор типа не рассматривается как int, хотя у меня нет под рукой публикации ISO. FWIW Google сообщает мне 3.5.2 черновика 89 года на основе ANSI (flash-gordon. me.uk/ansi.c.txt) согласен (да, другая нумерация и не полностью идентичная ISO C89/C90, и я знаю, что это не авторитетно). - person Iskar Jarak; 14.05.2013
comment
Ооо, я вижу. Я путал это с предварительным объявлением. В таком случае будет ли typedef foo; синтаксической ошибкой в ​​C99 и C11? - person autistic; 14.05.2013
comment
Ну, 6.7.2 ISO C99 не включает никакой спецификатор типа в список спецификаторов типа, а также говорит, что по крайней мере один спецификатор типа должен быть указан в спецификаторах объявлений в каждом объявлении.. ., а нарушение должно привести к неопределенному поведению (см. раздел 4). Например, GCC с std=c99, по-видимому, (не очень глубоко заглядывал, не цитируйте меня) реализовал указанное поведение undefined с поведением C89 и жаловался на неявную типизацию int по умолчанию (т.е. без включенного -Wimplicit-int) . Я подозреваю, что Visual Studio реализовала это как синтаксическую ошибку. - person Iskar Jarak; 14.05.2013
comment
@IskarJarak спасибо за отличный ответ! для целей обучения я иногда использую компилятор Tiny C, с его помощью я могу поменять местами 2 числа с плавающей запятой, не теряя прореживания. Но, судя по вашему объяснению, параметры приводятся к (int *), они должны потерять прореживание. Зачем? или мне следует избегать использования компилятора Tiny C, но использовать более стандартный компилятор? - person Shuping; 14.05.2013
comment
Когда вы вызываете swap, вы выполняете приведение float* к int* (преобразование из одного типа в другой). То есть из указателя на число с плавающей запятой в указатель на целое число, а не из float в int. Целочисленная переменная — это всего лишь несколько битов, представляющих целое число, когда мы интерпретируем его таким образом. Переменная с плавающей запятой — это всего лишь несколько битов, которые представляют число, интерпретируемое как число с плавающей запятой, но представление целого числа сильно отличается от представления числа с плавающей запятой. - person Iskar Jarak; 14.05.2013
comment
Когда выполняется swap, вы копируете биты, на которые указывает a, и решаете временно сохранить их как целое число перед записью в место, на которое указывает b. Это очень отличается от чтения их как числа с плавающей запятой, приведения этого значения к целому числу, сохранения результата в виде целого числа и записи его в место, на которое указывает b (шаг приведения заставит вас потерять десятичную часть, и попытка позже прочитать ее в main как число с плавающей запятой не даст ожидаемого результата, потому что вы будете читать целочисленное битовое представление как число с плавающей запятой. - person Iskar Jarak; 14.05.2013
comment
Нет ничего плохого в том, чтобы использовать TCC для экспериментов и изучения вещей — это хороший легкий компилятор для подобных вещей — но я думаю, что вы узнали бы больше, если бы использовали компилятор с лучшими параметрами для предупреждения флаги. Я бы рекомендовал -Wall -Werror -ansi -pedantic для написания C89 с помощью GCC, хотя это далеко не единственный вариант. Я не знаю эквивалентных флагов для cl.exe навскидку, извините. Но вы можете многому научиться, читая предупреждения компилятора! - person Iskar Jarak; 14.05.2013

В C90 (это то, что MSVC использует в качестве основы при компиляции кода C) одним из возможных спецификаторов типа является (C90 6.5.2 «Спецификаторы типов» — добавлен курсив):

  • int, signed, signed int или без спецификаторов типа

поэтому, если в объявлении не указан спецификатор типа (включая typedef), то тип по умолчанию равен int. это обычно известно как "неявное объявление int". Обратите внимание, что C99 удалил поддержку неявного int (по умолчанию GCC выдает предупреждение об этом только при компиляции в режиме C99).

Ваше определение типа:

typedef T;

эквивалентно:

typedef int T;

Таким образом, ваше определение swap() эквивалентно:

void swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

Как это бывает, при вызове функции, которая не была объявлена ​​или прототипирована (как это происходит, когда вы вызываете swap() в main.c), компилятор применяет продвижение аргументов по умолчанию к арифметическим аргументам и предполагает, что функция возвращает int. Ваш вызов swap() передает два аргумента типа int*, поэтому продвижение не происходит (это аргументы-указатели, а не арифметические). Это именно то, что ожидает определение для swap(), поэтому вызов функции работает (и это хорошо определенное поведение).

Теперь вызывающий код ожидает, что swap() вернет int, поскольку объявления не было видно, а ваша функция swap() ничего не возвращает (void). Это неопределенное поведение, но в данном случае очевидной проблемы нет (хотя это все еще ошибка в вашем коде). Однако, если вы измените определение swap() так, чтобы оно возвращало int:

int swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

поведение undefined исчезает, хотя swap(), похоже, ничего не возвращает. Поскольку ничего не делается с результатом в месте вызова, C90 позволяет функции возвращаться с выражением. C90 позволяет это для поддержки предстандартного кода, в котором не было такого понятия, как тип void.

person Michael Burr    schedule 14.05.2013
comment
Спасибо за ваш быстрый ответ! - person Shuping; 14.05.2013