Почему * объявление * данных и функций необходимо на языке Си, когда определение написано в конце исходного кода?

15

Рассмотрим следующий код "C":

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i()определяется в конце исходного кода, и перед его использованием в main(). не предоставляется никаких объявлений . В то самое время , когда компилятор видит Func_i()в main(), он выходит из main()и узнает Func_i(). Компилятор как-то находит значение, возвращаемое Func_i()и передает его printf(). Я также знаю, что компилятор не может найти тип возвращаемого значения Func_i(). Это, по умолчанию принимает (догадок?) В тип возвращаемого из Func_i()быть int. То есть, если бы код имел float Func_i()тогда, компилятор выдаст ошибку: Конфликтующие типы дляFunc_i() .

Из приведенного выше обсуждения мы видим, что:

  1. Компилятор может найти значение, возвращаемое Func_i().

    • Если компилятор может найти значение, возвращаемое путем Func_i()выхода из main()исходного кода и поиска по исходному коду, то почему он не может найти тип Func_i (), который явно упоминается.
  2. Компилятор должен знать, что он Func_i()имеет тип float - поэтому он выдает ошибку конфликтующих типов.

  • Если компилятор знает, что он Func_iимеет тип float, то почему он все еще предполагает Func_i()тип int и выдает ошибку конфликтующих типов? Почему бы не сделать так, Func_i()чтобы он был типа float.

У меня те же сомнения с объявлением переменной . Рассмотрим следующий код "C":

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

Компилятор выдает ошибку: «Data_i» undeclared (первое использование в этой функции).

  • Когда компилятор видит Func_i(), он переходит к исходному коду, чтобы найти значение, возвращаемое Func_ (). Почему компилятор не может сделать то же самое для переменной Data_i?

Редактировать:

Я не знаю деталей внутренней работы компилятора, ассемблера, процессора и т. Д. Основная идея моего вопроса заключается в том, что если я сообщу (напишу) возвращаемое значение функции в исходном коде, наконец, после использования этой функции, то язык «С» позволяет компьютеру найти это значение без каких-либо ошибок. Теперь, почему компьютер не может найти тип аналогичным образом. Почему нельзя найти тип Data_i, так как было найдено возвращаемое значение Func_i (). Даже если я использую extern data-type identifier;оператор, я не говорю значение, которое будет возвращено этим идентификатором (функция / переменная). Если компьютер может найти это значение, то почему он не может найти тип. Зачем нам вообще нужна предварительная декларация?

Спасибо.

user106313
источник
7
Компилятор не «находит» значение, возвращаемое Func_i. это делается во время исполнения.
Джеймс Маклеод
26
Я не отрицал, но вопрос основан на некоторых серьезных недоразумениях о том, как работают компиляторы, и ваши ответы в комментариях показывают, что у вас все еще есть некоторые концептуальные препятствия, которые нужно преодолеть.
Джеймс Маклеод
4
Обратите внимание, что первый пример кода не был действительным, стандартным кодом в течение последних пятнадцати лет; C99 сделал отсутствие возвращаемого типа в определениях функций и неявное объявление Func_iнедействительным. Там никогда не было правила, чтобы неявно объявлять неопределенные переменные, поэтому второй фрагмент всегда был искажен. (Да, компиляторы все еще принимают первый образец, потому что он действителен, хотя и небрежно, в соответствии с C89 / C90.)
Джонатан Леффлер
19
@ user31782: Итог к вопросу: почему язык X делает / требует Y? Потому что это выбор, который сделали дизайнеры. Вы, кажется, утверждаете, что разработчики одного из самых успешных языков когда-либо должны были делать разные выборы десятилетия назад, а не пытаться понять эти выборы в контексте, в котором они были сделаны. Ответ на ваш вопрос: зачем нам предварительная декларация? было дано: потому что C использует однопроходной компилятор. Самый простой ответ на большинство ваших последующих вопросов: Потому что тогда это не был бы однопроходный компилятор.
Мистер Миндор
4
@ user31782 Вы действительно, действительно хотите прочитать книгу дракона, чтобы понять, как на самом деле работают компиляторы и процессоры - просто невозможно объединить все необходимые знания в одном ответе SO (или даже 100). Отличная книга для всех, кто интересуется компиляторами.
Во

Ответы:

26

Потому что C - это однопроходный , статически типизированный , слабо типизированный , скомпилированный язык.

  1. Однопроходный означает, что компилятор не смотрит вперед, чтобы увидеть определение функции или переменной. Поскольку компилятор не смотрит в будущее, объявление функции должно предшествовать использованию функции, в противном случае компилятор не знает, какова его сигнатура типа. Однако определение функции может быть позже в том же файле или даже в другом файле. Смотрите пункт № 4.

    Единственным исключением является исторический артефакт, согласно которому необъявленные функции и переменные имеют тип «int». Современная практика - избегать неявной типизации, всегда объявляя функции и переменные явно.

  2. Статически типизированный означает, что вся информация о типе вычисляется во время компиляции. Эта информация затем используется для генерации машинного кода, который выполняется во время выполнения. В Си нет понятия о типизации во время выполнения. Однажды int, всегда int, один раз float, всегда float. Однако этот факт несколько скрыт следующим пунктом.

  3. Слабо типизированный означает, что компилятор C автоматически генерирует код для преобразования между числовыми типами, не требуя от программиста явного указания операций преобразования. Из-за статической типизации одно и то же преобразование всегда будет выполняться одинаково каждый раз через программу. Если значение с плавающей запятой преобразуется в значение int в заданной точке кода, значение с плавающей запятой всегда преобразуется в значение int в этой точке кода. Это не может быть изменено во время выполнения. Само значение может меняться от одного выполнения программы к следующему, конечно, и условные операторы могут изменять, какие разделы кода выполняются в каком порядке, но данный отдельный раздел кода без вызовов функций или условных выражений всегда будет выполнять точное те же операции, когда он запускается.

  4. Скомпилировано означает, что процесс анализа читаемого человеком исходного кода и преобразования его в машиночитаемые инструкции полностью выполняется до запуска программы. Когда компилятор компилирует функцию, он не знает, с чем он столкнется дальше в данном исходном файле. Однако после завершения компиляции (и сборки, компоновки и т. Д.) Каждая функция в готовом исполняемом файле содержит числовые указатели на функции, которые она будет вызывать при запуске. Вот почему main () может вызывать функцию ниже в исходном файле. К тому моменту, когда main () будет фактически запущен, он будет содержать указатель на адрес Func_i ().

    Машинный код очень и очень специфичен. Код для добавления двух целых чисел (3 + 2) отличается от кода для добавления двух чисел (3.0 + 2.0). Они оба отличаются от добавления int к float (3 + 2.0) и так далее. Компилятор определяет для каждой точки функции, какая именно операция должна быть выполнена в этой точке, и генерирует код, который выполняет эту операцию. Как только это будет сделано, его нельзя изменить, не перекомпилировав функцию.

Собрав все эти понятия вместе, причина, по которой main () не может «увидеть» дальше, чтобы определить тип Func_i (), заключается в том, что анализ типов происходит в самом начале процесса компиляции. В этот момент только часть исходного файла до определения main () была прочитана и проанализирована, а определение Func_i () еще не известно компилятору.

Причина, по которой main () может «видеть», где Func_i () должна вызывать его, заключается в том, что вызов происходит во время выполнения, после того как компиляция уже разрешила все имена и типы всех идентификаторов, сборка уже преобразовала все функции к машинному коду, и связывание уже вставило правильный адрес каждой функции в каждом месте, где она вызывается.

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

Кроме того, пожалуйста, помните, что то, что я написал выше, относится к C.

В других языках компилятор может сделать несколько проходов через исходный код, и поэтому компилятор может получить определение Func_i () без его предварительного объявления.

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

В других языках типизация может быть сильнее, требуя явного указания преобразования из числа с плавающей точкой в ​​целое. В других языках типизация может быть слабее, что позволяет автоматически выполнять преобразование строки «3.0» в число с плавающей точкой 3.0 в целое число 3.

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

Клемент Черлин
источник
1
Спасибо за ответ «все в одном». Ответ твоего и Никки - это то, что я хотел знать. Например Func_()+1: здесь, во время компиляции, компилятор должен знать тип, Func_i()чтобы генерировать соответствующий машинный код. Может быть , либо это не возможно для сборки на ручку с Func_()+1помощью вызова типа во время выполнения, или это возможно , но сделать это будет сделать программу медленно на время выполнения. Я думаю, мне этого пока достаточно.
user106313
1
Важная деталь неявно объявленных функций Си: предполагается, что они имеют тип int func(...)... т.е. они принимают список аргументов с переменным числом аргументов. Это означает, что если вы определяете функцию как, int putc(char)но забываете объявить ее, она будет вызываться как int putc(int)(потому что символ, передаваемый через список аргументов с переменным числом аргументов, повышается int). Таким образом, хотя пример OP сработал, потому что его подпись соответствовала неявному объявлению, понятно, почему такое поведение не поощрялось (и добавлялись соответствующие предупреждения).
Призрак
37

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

int Func_i();

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

В ваших примерах вы используете две сомнительные возможности языка C, которых следует избегать:

  1. Если функция используется до того, как она была должным образом объявлена, она используется как «неявное объявление». Компилятор использует непосредственный контекст, чтобы выяснить сигнатуру функции. Компилятор не будет сканировать оставшуюся часть кода, чтобы выяснить, что такое настоящее объявление.

  2. Если что-то объявлено без типа, тип принимается за int. Это, например, случай для статических переменных или типов возвращаемых функций.

Итак printf("func:%d",Func_i()), у нас есть неявное объявление int Func_i(). Когда компилятор достигает определения функции Func_i() { ... }, это совместимо с типом. Но если вы написали float Func_i() { ... }в этот момент, у вас объявлена ​​безумие int Func_i()и явно заявлено float Func_i(). Поскольку эти два объявления не совпадают, компилятор выдает ошибку.

Прояснение некоторых заблуждений

  • Компилятор не находит значение, возвращаемое Func_i. Отсутствие явного типа означает, что тип возвращаемого значения intпо умолчанию. Даже если вы сделаете это:

    Func_i() {
        float f = 42.3;
        return f;
    }

    тогда тип будет int Func_i(), а возвращаемое значение будет молча усечено!

  • Компилятор в конце концов узнает реальный тип Func_i, но он не знает реальный тип во время неявного объявления. Только когда он позже достигнет реального объявления, он сможет выяснить, был ли неявно объявленный тип правильным. Но на этом этапе сборка для вызова функции уже может быть написана и не может быть изменена в модели компиляции Си.

Амон
источник
3
@ user31782: Порядок кода имеет значение во время компиляции, но не во время выполнения. Компилятор не работает, когда запускается программа. Во время выполнения функция будет собрана и связана, ее адрес будет разрешен и вставлен в адрес-заполнитель адреса вызова. (Это немного сложнее, но это основная идея.) Процессор может переходить вперед или назад.
Blrfl
20
@ user31782: компилятор не печатает значение. Ваш компилятор не запускает программу!
Легкость гонки с Моникой
1
@LightnessRacesinOrbit Я это знаю. Я по ошибке написал компилятор в моем комментарии выше, потому что я забыл имя процессора .
user106313
3
@Carcigenicate C находился под сильным влиянием языка B, который имел только один тип: целочисленный числовой тип ширины слова, который также можно использовать для указателей. Изначально C копировал это поведение, но теперь оно полностью запрещено со времени стандарта C99. Unitделает хороший тип по умолчанию с точки зрения теории типов, но терпит неудачу в практических аспектах программирования систем, близких к металлическим, для которых были разработаны B и C.
Амон
2
@ user31782: компилятор должен знать тип переменной, чтобы сгенерировать правильную сборку для процессора. Когда компилятор находит неявное Func_i(), он немедленно генерирует и сохраняет код для процессора, чтобы перейти в другое место, затем получить некоторое целое число и затем продолжить. Когда компилятор позже находит Func_iопределение, он проверяет, совпадают ли сигнатуры, и если они это делают, он помещает сборку для Func_i()по этому адресу и говорит ему вернуть некоторое целое число. Когда вы запускаете программу, процессор следует этим инструкциям со значением 3.
Mooing Duck
10

Во-первых, ваши программы действительны для стандарта C90, но не для следующих. неявное int (позволяющее объявить функцию без указания ее возвращаемого типа) и неявное объявление функций (позволяющее использовать функцию без ее объявления) больше не действительны.

Во-вторых, это не работает, как вы думаете.

  1. Тип результата не является обязательным в C90, не давая один означает intрезультат. Это также верно для объявления переменных (но вы должны указать класс хранения staticили extern).

  2. То, что делает компилятор, когда видит, Func_iвызывается без предыдущего объявления, предполагает, что есть объявление

    extern int Func_i();

    в коде он не смотрит дальше, чтобы увидеть, насколько эффективно Func_iобъявлено. Если бы Func_iне было объявлено или определено, компилятор не изменил бы свое поведение при компиляции main. Неявное объявление только для функции, а для переменной - нет.

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

AProgrammer
источник
Если компилятор может найти значение, возвращаемое Func_i (), выйдя из main () и выполнив поиск по исходному коду, то почему он не может найти тип Func_i (), который явно упоминается.
user106313
1
@ user31782 Если предыдущее объявление Func_i не было, то, когда он видит, что Func_i используется в выражении вызова, ведет себя так, как если бы оно было extern int Func_i(). Это нигде не выглядит.
AProgrammer
1
@ user31782, компилятор никуда не подскакивает. Он выдаст код для вызова этой функции; возвращаемое значение будет определено во время выполнения. Что ж, в случае такой простой функции, которая присутствует в том же модуле компиляции, фаза оптимизации может включать функцию, но это не то, о чем вы должны думать при рассмотрении правил языка, это оптимизация.
AProgrammer
10
@ user31782, у вас есть серьезные недоразумения о том, как работают программы. Настолько серьезный, что я не думаю, что p.se является хорошим местом для их исправления (возможно, в чате, но я не буду пытаться это делать).
AProgrammer
1
@ user31782: Написание небольшого фрагмента и его компиляция -S(если вы используете gcc) позволит вам взглянуть на код сборки, сгенерированный компилятором. Затем вы можете иметь представление о том, как возвращаемые значения обрабатываются во время выполнения (обычно с использованием регистра процессора или некоторого пространства в программном стеке).
Джорджио
7

Вы написали в комментарии:

Выполнение выполняется построчно. Единственный способ найти значение, возвращаемое Func_i (), это выпрыгнуть из основного

Это заблуждение: казнь не за строкой. Компиляция выполняется построчно, а разрешение имен выполняется во время компиляции, и оно разрешает только имена, а не возвращаемые значения.

Полезная концептуальная модель такова: когда компилятор читает строку:

  printf("func:%d",Func_i());

он испускает код, эквивалентный:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

Компилятор также делает заметку в некоторой внутренней таблице, которая function #2еще не объявлена ​​с именем функции Func_i, которая принимает неопределенное количество аргументов и возвращает int (по умолчанию).

Позже, когда это анализирует это:

 int Func_i() { ...

компилятор ищет Func_iв таблице, упомянутой выше, и проверяет, совпадают ли параметры и тип возвращаемого значения. Если они этого не делают, он останавливается с сообщением об ошибке. Если это так, он добавляет текущий адрес во внутреннюю таблицу функций и переходит к следующей строке.

Таким образом, компилятор не «искал», Func_iкогда он анализировал первую ссылку. Он просто сделал пометку в какой-то таблице и продолжил разбор следующей строки. И в конце файла у него есть объектный файл и список адресов перехода.

Позже компоновщик берет все это и заменяет все указатели на «функцию # 2» фактическим адресом перехода, поэтому он генерирует что-то вроде:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

Намного позже, когда исполняемый файл запускается, адрес перехода уже разрешен, и компьютер может просто перейти к адресу 0x1215. Поиск имени не требуется.

Отказ от ответственности : как я уже сказал, это концептуальная модель, а реальный мир сложнее. Компиляторы и компоновщики делают все виды сумасшедших оптимизаций сегодня. Они даже могут «подпрыгнуть», чтобы искать Func_i, хотя я сомневаюсь в этом. Но языки Си определены так, что вы могли бы написать такой супер-простой компилятор. Так что большую часть времени это очень полезная модель.

nikie
источник
Спасибо за ваш ответ. Не может компилятор 1. call "function #2", put the return-type onto the stack and put the return value on the stack?
выдать
1
(Продолжение) Кроме того: Что если вы написали printf(..., Func_i()+1);- компилятор должен знать тип Func_i, чтобы он мог решить, следует ли ему генерировать add integerили add floatинструкцию. Вы можете найти некоторые особые случаи, когда компилятор может работать без информации о типе, но компилятор должен работать для всех случаев.
nikie
4
@ user31782: Машинные инструкции, как правило, очень просты: добавить два 32-разрядных целочисленных регистра. Загрузить адрес памяти в 16-битный регистр целых чисел. Перейти к адресу. Кроме того, нет типов : вы можете с радостью загрузить ячейку памяти, которая представляет 32-разрядное число с плавающей запятой, в 32-разрядный целочисленный регистр и выполнить некоторую арифметику с ним. (Это редко имеет смысл.) Нет, вы не можете напрямую генерировать такой машинный код. Вы могли бы написать компилятор, который делает все эти вещи с проверками во время выполнения и дополнительными данными типа в стеке. Но это не будет компилятор Си.
nikie
1
@ user31782: Зависит от IIRC. floatзначения могут жить в регистре FPU - тогда не было бы никакой инструкции вообще. Компилятор просто отслеживает, какое значение хранится в каком регистре во время компиляции, и выдает такие вещи, как «добавить константу 1 в регистр FP X». Или он может жить в стеке, если нет свободных регистров. Затем будет инструкция «увеличить указатель стека на 4», и значение будет «ссылаться» как «указатель стека - 4». Но все это работает, только если размеры всех переменных (до и после) в стеке известны во время компиляции.
nikie
1
Из всего обсуждения, к которому я пришел , пришло понимание: для того, чтобы компилятор сделал правдоподобный ассемблерный код для любого оператора, включая Func_i()или / Data_i, он должен определить их типы; на ассемблере невозможно выполнить вызов к типу данных. Я должен сам детально изучать вещи, чтобы быть уверенным.
user106313
5

C и ряд других языков, требующих деклараций, были разработаны в эпоху, когда процессорное время и память были дороги. Разработка C и Unix шла рука об руку довольно долгое время, и у последнего не было виртуальной памяти до появления 3BSD в 1979 году. Без дополнительного пространства для работы компиляторы имели тенденцию быть однопроходными, потому что они не делали этого. Требуется возможность сохранять некоторое представление всего файла в памяти все сразу.

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

В ранних версиях C (AT & T, K & R, C89) использование функции foo()до объявления приводило к фактическому или неявному объявлению int foo(). Ваш пример работает, когда Func_i()объявлен, intпотому что он соответствует тому, что компилятор объявил от вашего имени. Изменение его на любой другой тип приведет к конфликту, потому что он больше не соответствует тому, что выбрал компилятор при отсутствии явного объявления. Это поведение было удалено в C99, где использование необъявленной функции стало ошибкой.

Так что насчет типов возврата?

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

Еще одной особенностью раннего Си было то, что прототипы, какими мы их знаем, сейчас не существуют. Вы можете объявить тип возвращаемого значения функции (например, int foo()), но не аргументы (т. Е. int foo(int bar)Не было опцией). Это существовало потому, что, как указано выше, программа всегда придерживалась соглашения о вызовах, которое может быть определено аргументами. Если вы вызывали функцию с неверным типом аргументов, это была ситуация с мусором.

Поскольку объектный код имеет понятие возврата, но не типа возврата, компилятор должен знать тип возврата, чтобы иметь дело с возвращаемым значением. Когда вы бежите машинные команды, это все только биты и процессор не заботится ли память , где вы пытаетесь сравнить doubleесть на самом деле intв нем. Он просто делает то, что вы просите, и если вы сломаете его, у вас есть обе части.

Рассмотрим эти биты кода:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

Код слева компилируется до вызова, foo()а затем копируется результат, предоставленный в соответствии с соглашением о вызове / возврате, в любое место, где xон хранится. Это простой случай.

Код справа показывает преобразование типов и поэтому компиляторам нужно знать тип возвращаемого значения функции. Числа с плавающей точкой не могут быть сброшены в память, где другой код будет ожидать, intпотому что не происходит никакого волшебного преобразования. Если конечный результат должен быть целым числом, должны быть инструкции, которые направляют процессор для выполнения преобразования перед сохранением. Не зная foo()заранее типа возврата , компилятор не знал бы, что код преобразования необходим.

Многопроходные компиляторы включают все виды вещей, одним из которых является возможность объявлять переменные, функции и методы после их первого использования. Это означает, что когда компилятор приступает к компиляции кода, он уже видит будущее и знает, что делать. Например, Java предписывает многопроходность в силу того факта, что ее синтаксис допускает объявление после использования.

Blrfl
источник
Спасибо за ваш ответ (+1). Я не знаю деталей внутренней работы компилятора, ассемблера, процессора и т. Д. Основная идея моего вопроса заключается в том, что если я сообщу (напишу) возвращаемое значение функции в исходном коде, наконец, после использования этой функции язык позволяет компьютеру найти это значение без каких-либо ошибок. Теперь, почему компьютер не может найти тип аналогичным образом. Почему нельзя найти тип Data_i, так как было найдено Func_i()возвращаемое значение.
user106313
Я все еще не удовлетворен. double foo(); int x; x = foo();просто выдает ошибку. Я знаю, что мы не можем этого сделать. Мой вопрос заключается в том, что при вызове функции процессор находит только возвращаемое значение; почему он также не может найти тип возврата?
user106313
1
@ user31782: не должно. Есть прототип для foo(), так что компилятор знает, что с ним делать.
Blrfl
2
@ user31782: Процессоры не имеют представления о типе возвращаемого значения.
Blrfl
1
@ user31782 Для вопроса времени компиляции: можно написать язык, на котором весь этот анализ типов может быть выполнен во время компиляции. С не такой язык. Компилятор C не может этого сделать, потому что он не предназначен для этого. Мог ли он быть разработан по-другому? Конечно, но для этого потребовалось бы гораздо больше вычислительной мощности и памяти. Суть в том, что это не так. Он был разработан таким образом, чтобы компьютеры того времени были в состоянии справиться лучше всего.
Мистер Миндор