Почему мы должны упоминать тип данных переменной в C

19

Обычно в C мы должны сообщать компьютеру тип данных в объявлении переменной. Например, в следующей программе я хочу вывести сумму двух чисел с плавающей запятой X и Y.

#include<stdio.h>
main()
{
  float X=5.2;
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}

Мне пришлось сообщить компилятору тип переменной X.

  • Разве компилятор не может определить тип Xсамостоятельно?

Да, может, если я сделаю это:

#define X 5.2

Теперь я могу написать свою программу, не сообщая компилятору тип X:

#include<stdio.h>
#define X 5.2
main()
{
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}  

Итак, мы видим, что язык Си имеет какую-то особенность, используя которую он может самостоятельно определять тип данных. В моем случае было установлено, что Xэто тип float.

  • Почему мы должны упоминать тип данных, когда мы объявляем что-то в main ()? Почему компилятор не может определить тип данных переменной самостоятельно, main()как это делается в #define.
user106313
источник
14
На самом деле эти две программы не эквивалентны, так как они могут давать немного разные результаты! 5.2это a double, поэтому первая программа округляет двойные литералы до floatточности, затем добавляет их как числа с плавающей точкой, в то время как вторая округляет двойное представление 5.1 до doubleи добавляет его к doubleзначению 5.2, используя doubleсложение, затем округляет результат этого вычисления до floatточности , Поскольку округление происходит в разных местах, результат может зависеть. Это всего лишь один пример для типов переменных, влияющих на поведение другой идентичной программы.
12
Когда вы это делаете #define X 5.2, Xэто не переменная, а константа, поэтому она буквально заменяется препроцессором на 5.2то, что вы упомянули X. Вы не можете переназначить X.
сценарий
16
Как примечание: это благословение и проклятие. С одной стороны, вы должны ввести несколько символов, когда компилятор действительно мог бы сделать это за вас (C ++ autoфактически делает то, что вы хотите). С другой стороны, если вы думаете, что знаете, что делает ваш код, и вы действительно ввели что-то еще, статическая типизация, подобная этой, обнаружит ошибку раньше, прежде чем станет огромной проблемой. Каждый язык соблюдает баланс: статическая типизация, определение типа, динамическая типизация. Для некоторых задач дополнительная печать действительно стоит того. Для других это пустая трата времени.
Cort Ammon - Восстановить Монику
Изучите Ocaml и / или Haskell .... вы будете рады их способностям вывода типа.
Василий Старынкевич

Ответы:

46

Вы сравниваете объявления переменных с #defines, что неверно. С помощью #defineвы создаете отображение между идентификатором и фрагментом исходного кода. Затем препроцессор C будет буквально заменять любые вхождения этого идентификатора предоставленным фрагментом. Письмо

#define FOO 40 + 2
int foos = FOO + FOO * FOO;

в конечном итоге это то же самое, что и компилятор

int foos = 40 + 2 + 40 + 2 * 40 + 2;

Думайте об этом как об автоматическом копировании и вставке.

Кроме того, обычные переменные могут быть переназначены, а макрос, созданный с #defineпомощью, не может (хотя вы можете #defineэто сделать). Выражение FOO = 7будет ошибкой компилятора, так как мы не можем присвоить «rvalues»: 40 + 2 = 7это недопустимо.

Итак, зачем нам вообще нужны типы? Некоторые языки, очевидно, избавляются от типов, это особенно распространено в скриптовых языках. Однако у них обычно есть нечто, называемое «динамическая типизация», когда переменные не имеют фиксированных типов, а значения имеют. Хотя это гораздо более гибко, но и менее эффективно. C любит производительность, поэтому у него очень простая и эффективная концепция переменных:

Существует участок памяти, называемый «стеком». Каждая локальная переменная соответствует области в стеке. Теперь вопрос в том, сколько байтов должно быть в этой области? В C каждый тип имеет четко определенный размер, который вы можете запросить через sizeof(type). Компилятору необходимо знать тип каждой переменной, чтобы он мог зарезервировать правильный объем пространства в стеке.

Почему константы, созданные с помощью, не #defineнуждаются в аннотации типа? Они не хранятся в стеке. Вместо этого #defineсоздает многократно используемые фрагменты исходного кода в несколько более удобной форме, чем копирование и вставка. Литералы в исходном коде, такие как "foo"или 42.87хранятся компилятором, либо в виде встроенных инструкций, либо в отдельном разделе данных результирующего двоичного файла.

Однако у литералов есть типы. Строковый литерал - это char *. 42это , intно также может быть использована для более коротких типов (сужение преобразования). 42.8будет double. Если у вас есть литерал, и вы хотите, чтобы он имел другой тип (например, чтобы сделать 42.8a floatили 42an unsigned long int), то вы можете использовать суффиксы - букву после литерала, которая изменяет способ обработки компилятором этого литерала. В нашем случае мы могли бы сказать 42.8fили 42ul.

Некоторые языки имеют статическую типизацию, как в C, но аннотации типов являются необязательными. Примерами являются ML, Haskell, Scala, C #, C ++ 11 и Go. Как это работает? Магия? Нет, это называется «вывод типа». В C # и Go компилятор просматривает правую часть присваивания и определяет его тип. Это довольно просто, если правая часть является литералом типа 42ul. Тогда очевидно, каким должен быть тип переменной. Другие языки также имеют более сложные алгоритмы, которые учитывают, как используется переменная. Например, если вы делаете x/2, то xне может быть строкой, но должен иметь некоторый числовой тип.

Амон
источник
Спасибо за объяснение. Я понимаю, что когда мы объявляем тип переменной (локальной или глобальной), мы на самом деле сообщаем компилятору, сколько места он должен зарезервировать для этой переменной в стеке. С другой стороны, у #defineнас есть константа, которая напрямую преобразуется в двоичный код - каким бы длинным он ни был - и сохраняется в памяти как есть.
user106313
2
@ user31782 - не совсем. Когда вы объявляете переменную, тип сообщает компилятору, какие свойства у переменной. Одним из таких свойств является размер; другие свойства включают то, как он представляет значения и какие операции могут быть выполнены с этими значениями.
Пит Беккер
@PeteBecker Тогда откуда компилятор узнает эти другие свойства #define X 5.2?
user106313
1
Это потому, что, передавая неправильный тип, printfвы вызываете неопределенное поведение. На моей машине этот фрагмент каждый раз печатает разные значения, на Ideone происходит сбой после нулевой печати.
Matteo Italia
4
@ user31782 - «Кажется, я могу выполнить любую операцию с любым типом данных». Номер X*Yнедействителен, если Xи Yявляются указателями, но это нормально, если они ints; *Xне допустимо, если Xесть int, но это нормально, если это указатель.
Пит Беккер
4

X во втором примере никогда не является плавающей точкой. Он называется макросом, он заменяет определенное значение макроса 'X' в источнике значением. Читаемая статья о #define находится здесь .

В случае предоставленного кода, перед компиляцией препроцессор меняет код

Z=Y+X;

в

Z=Y+5.2;

и это то, что компилируется.

Это означает, что вы также можете заменить эти «значения» кодом

#define X sqrt(Y)

или даже

#define X Y
Джеймс Снелл
источник
3
Он просто называется макросом, а не переменным макросом. Макрос variadic - это макрос, который принимает переменное число аргументов, например #define FOO(...) { __VA_ARGS__ }.
HVd
2
Мой плохой, исправлю :)
Джеймс Снелл
1

Короткий ответ - C нуждается в типах из-за истории / представления оборудования.

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

Вывод типов во время компиляции был бы возможен, но и без того медленное время компиляции увеличилось бы (см. Карикатуру «компиляции» в XKCD. Раньше она применялась к «hello world» как минимум в течение 10 лет после публикации C ). Вывод типов во время выполнения не соответствовал бы целям системного программирования. Вывод времени выполнения требует дополнительной библиотеки времени выполнения. С пришел задолго до первого ПК. Который имел 256 RAM. Не гигабайты или мегабайты, а килобайты.

В вашем примере, если вы опустите типы

   X=5.2;
   Y=5.1;

   Z=Y+X;

Тогда компилятор мог бы успешно решить, что X & Y являются float, и сделать Z таким же. Фактически, современный компилятор также решит, что X & Y не нужны, и просто установит Z на 10,3.

Предположим, что расчет встроен в функцию. Автор функций может захотеть использовать свои знания об оборудовании или о решаемой проблеме.

Дабл был бы более подходящим чем поплавок? Занимает больше памяти и медленнее, но точность результата будет выше.

Возможно, возвращаемое значение функции может быть int (или long), потому что десятичные дроби не важны, хотя преобразование из float в int не обходится без затрат.

Возвращаемое значение также можно сделать двойным, гарантируя, что float + float не переполняется.

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

ITJ
источник
1
это не объясняет, например, почему объявления типов не были сделаны необязательными, позволяя программисту выбирать либо объявить их явно, либо полагаться на вывод компилятора
gnat
1
На самом деле это не @gnat. Я подправил текст, но в то время делать это было бессмысленно. Домен C был разработан для того, чтобы на самом деле было решено хранить 17 в 1 байте, или 2 байта, или 4 байта, или в виде строки, или как 5 битов в слове.
ITJ
0

C не имеет логического вывода типа (так он называется, когда компилятор угадывает тип переменной для вас), потому что он старый. Он был разработан в начале 1970-х годов

Многие новые языки имеют системы, которые позволяют вам использовать переменные без указания их типа (ruby, javascript, python и т. Д.)

Тристан Бернсайд
источник
12
Ни один из упомянутых вами языков (Ruby, JS, Python) не имеет вывода типов в качестве языковой функции, хотя реализации могут использовать его для повышения эффективности. Вместо этого они используют динамическую типизацию, когда значения имеют типы, а переменные или другие выражения - нет.
Амон
2
JS не позволяет вам опускать тип - он просто не позволяет вам объявить его вообще. Он использует динамическую типизацию, где значения имеют типы (например, trueis boolean), а не переменные (например, var xмогут содержать значения любого типа). Кроме того, вывод типа для таких простых случаев, как те, о которых идет речь, был, вероятно, известен за десять лет до выпуска Си.
сценарий
2
Это не делает утверждение ложным (чтобы что-то форсировать, вы также должны это разрешить). Существующий вывод типов не меняет того факта, что система типов С является результатом ее исторического контекста (в отличие от специально заявленных философских рассуждений или технических ограничений)
Тристан Бернсайд
2
Учитывая, что ML - который почти так же стар, как C - имеет вывод типа, «он старый» - не очень хорошее объяснение. Контекст, в котором C использовался и развивался (маленькие машины, которые требовали очень небольшой площади для компилятора), представляется более вероятным. Не знаю, почему вы упомянули бы языки динамической типизации вместо нескольких примеров языков с выводом типов - Haskell, ML, черт возьми, в C # это есть - вряд ли эта функция более неясна.
Во
2
@BradS. Fortran не является хорошим примером, потому что первая буква имени переменной является объявлением типа, если только вы не используете, implicit noneв этом случае вы должны объявить тип.
dmckee