Рассмотрим следующий код "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()
.
Из приведенного выше обсуждения мы видим, что:
Компилятор может найти значение, возвращаемое
Func_i()
.- Если компилятор может найти значение, возвращаемое путем
Func_i()
выхода изmain()
исходного кода и поиска по исходному коду, то почему он не может найти тип Func_i (), который явно упоминается.
- Если компилятор может найти значение, возвращаемое путем
Компилятор должен знать, что он
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;
оператор, я не говорю значение, которое будет возвращено этим идентификатором (функция / переменная). Если компьютер может найти это значение, то почему он не может найти тип. Зачем нам вообще нужна предварительная декларация?
Спасибо.
источник
Func_i
недействительным. Там никогда не было правила, чтобы неявно объявлять неопределенные переменные, поэтому второй фрагмент всегда был искажен. (Да, компиляторы все еще принимают первый образец, потому что он действителен, хотя и небрежно, в соответствии с C89 / C90.)Ответы:
Потому что C - это однопроходный , статически типизированный , слабо типизированный , скомпилированный язык.
Однопроходный означает, что компилятор не смотрит вперед, чтобы увидеть определение функции или переменной. Поскольку компилятор не смотрит в будущее, объявление функции должно предшествовать использованию функции, в противном случае компилятор не знает, какова его сигнатура типа. Однако определение функции может быть позже в том же файле или даже в другом файле. Смотрите пункт № 4.
Единственным исключением является исторический артефакт, согласно которому необъявленные функции и переменные имеют тип «int». Современная практика - избегать неявной типизации, всегда объявляя функции и переменные явно.
Статически типизированный означает, что вся информация о типе вычисляется во время компиляции. Эта информация затем используется для генерации машинного кода, который выполняется во время выполнения. В Си нет понятия о типизации во время выполнения. Однажды int, всегда int, один раз float, всегда float. Однако этот факт несколько скрыт следующим пунктом.
Слабо типизированный означает, что компилятор C автоматически генерирует код для преобразования между числовыми типами, не требуя от программиста явного указания операций преобразования. Из-за статической типизации одно и то же преобразование всегда будет выполняться одинаково каждый раз через программу. Если значение с плавающей запятой преобразуется в значение int в заданной точке кода, значение с плавающей запятой всегда преобразуется в значение int в этой точке кода. Это не может быть изменено во время выполнения. Само значение может меняться от одного выполнения программы к следующему, конечно, и условные операторы могут изменять, какие разделы кода выполняются в каком порядке, но данный отдельный раздел кода без вызовов функций или условных выражений всегда будет выполнять точное те же операции, когда он запускается.
Скомпилировано означает, что процесс анализа читаемого человеком исходного кода и преобразования его в машиночитаемые инструкции полностью выполняется до запуска программы. Когда компилятор компилирует функцию, он не знает, с чем он столкнется дальше в данном исходном файле. Однако после завершения компиляции (и сборки, компоновки и т. Д.) Каждая функция в готовом исполняемом файле содержит числовые указатели на функции, которые она будет вызывать при запуске. Вот почему 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.
А в других языках код может интерпретироваться по одной строке за раз, или компилироваться в байт-код, а затем интерпретироваться, или точно компилироваться вовремя, или проходить через множество других схем выполнения.
источник
Func_()+1
: здесь, во время компиляции, компилятор должен знать тип,Func_i()
чтобы генерировать соответствующий машинный код. Может быть , либо это не возможно для сборки на ручку сFunc_()+1
помощью вызова типа во время выполнения, или это возможно , но сделать это будет сделать программу медленно на время выполнения. Я думаю, мне этого пока достаточно.int func(...)
... т.е. они принимают список аргументов с переменным числом аргументов. Это означает, что если вы определяете функцию как,int putc(char)
но забываете объявить ее, она будет вызываться какint putc(int)
(потому что символ, передаваемый через список аргументов с переменным числом аргументов, повышаетсяint
). Таким образом, хотя пример OP сработал, потому что его подпись соответствовала неявному объявлению, понятно, почему такое поведение не поощрялось (и добавлялись соответствующие предупреждения).Конструктивное ограничение языка C заключалось в том, что он должен был компилироваться однопроходным компилятором, что делает его пригодным для систем с очень ограниченным объемом памяти. Следовательно, компилятор в любой момент знает только то, что упоминалось ранее. Компилятор не может перейти вперед в источнике, чтобы найти объявление функции, а затем вернуться, чтобы скомпилировать вызов этой функции. Следовательно, все символы должны быть объявлены до их использования. Вы можете предварительно объявить функцию как
вверху или в заголовочном файле, чтобы помочь компилятору.
В ваших примерах вы используете две сомнительные возможности языка C, которых следует избегать:
Если функция используется до того, как она была должным образом объявлена, она используется как «неявное объявление». Компилятор использует непосредственный контекст, чтобы выяснить сигнатуру функции. Компилятор не будет сканировать оставшуюся часть кода, чтобы выяснить, что такое настоящее объявление.
Если что-то объявлено без типа, тип принимается за
int
. Это, например, случай для статических переменных или типов возвращаемых функций.Итак
printf("func:%d",Func_i())
, у нас есть неявное объявлениеint Func_i()
. Когда компилятор достигает определения функцииFunc_i() { ... }
, это совместимо с типом. Но если вы написалиfloat Func_i() { ... }
в этот момент, у вас объявлена безумиеint Func_i()
и явно заявленоfloat Func_i()
. Поскольку эти два объявления не совпадают, компилятор выдает ошибку.Прояснение некоторых заблуждений
Компилятор не находит значение, возвращаемое
Func_i
. Отсутствие явного типа означает, что тип возвращаемого значенияint
по умолчанию. Даже если вы сделаете это:тогда тип будет
int Func_i()
, а возвращаемое значение будет молча усечено!Компилятор в конце концов узнает реальный тип
Func_i
, но он не знает реальный тип во время неявного объявления. Только когда он позже достигнет реального объявления, он сможет выяснить, был ли неявно объявленный тип правильным. Но на этом этапе сборка для вызова функции уже может быть написана и не может быть изменена в модели компиляции Си.источник
Unit
делает хороший тип по умолчанию с точки зрения теории типов, но терпит неудачу в практических аспектах программирования систем, близких к металлическим, для которых были разработаны B и C.Func_i()
, он немедленно генерирует и сохраняет код для процессора, чтобы перейти в другое место, затем получить некоторое целое число и затем продолжить. Когда компилятор позже находитFunc_i
определение, он проверяет, совпадают ли сигнатуры, и если они это делают, он помещает сборку дляFunc_i()
по этому адресу и говорит ему вернуть некоторое целое число. Когда вы запускаете программу, процессор следует этим инструкциям со значением3
.Во-первых, ваши программы действительны для стандарта C90, но не для следующих. неявное int (позволяющее объявить функцию без указания ее возвращаемого типа) и неявное объявление функций (позволяющее использовать функцию без ее объявления) больше не действительны.
Во-вторых, это не работает, как вы думаете.
Тип результата не является обязательным в C90, не давая один означает
int
результат. Это также верно для объявления переменных (но вы должны указать класс храненияstatic
илиextern
).То, что делает компилятор, когда видит,
Func_i
вызывается без предыдущего объявления, предполагает, что есть объявлениев коде он не смотрит дальше, чтобы увидеть, насколько эффективно
Func_i
объявлено. Если быFunc_i
не было объявлено или определено, компилятор не изменил бы свое поведение при компиляцииmain
. Неявное объявление только для функции, а для переменной - нет.Обратите внимание, что пустой список параметров в объявлении не означает, что функция не принимает параметры (для этого нужно указать
(void)
), это означает, что компилятору не нужно проверять типы параметров, и он будет таким же неявные преобразования, которые применяются к аргументам, передаваемым в функции с переменными числами.источник
extern int Func_i()
. Это нигде не выглядит.-S
(если вы используетеgcc
) позволит вам взглянуть на код сборки, сгенерированный компилятором. Затем вы можете иметь представление о том, как возвращаемые значения обрабатываются во время выполнения (обычно с использованием регистра процессора или некоторого пространства в программном стеке).Вы написали в комментарии:
Это заблуждение: казнь не за строкой. Компиляция выполняется построчно, а разрешение имен выполняется во время компиляции, и оно разрешает только имена, а не возвращаемые значения.
Полезная концептуальная модель такова: когда компилятор читает строку:
он испускает код, эквивалентный:
Компилятор также делает заметку в некоторой внутренней таблице, которая
function #2
еще не объявлена с именем функцииFunc_i
, которая принимает неопределенное количество аргументов и возвращает int (по умолчанию).Позже, когда это анализирует это:
компилятор ищет
Func_i
в таблице, упомянутой выше, и проверяет, совпадают ли параметры и тип возвращаемого значения. Если они этого не делают, он останавливается с сообщением об ошибке. Если это так, он добавляет текущий адрес во внутреннюю таблицу функций и переходит к следующей строке.Таким образом, компилятор не «искал»,
Func_i
когда он анализировал первую ссылку. Он просто сделал пометку в какой-то таблице и продолжил разбор следующей строки. И в конце файла у него есть объектный файл и список адресов перехода.Позже компоновщик берет все это и заменяет все указатели на «функцию # 2» фактическим адресом перехода, поэтому он генерирует что-то вроде:
Намного позже, когда исполняемый файл запускается, адрес перехода уже разрешен, и компьютер может просто перейти к адресу 0x1215. Поиск имени не требуется.
Отказ от ответственности : как я уже сказал, это концептуальная модель, а реальный мир сложнее. Компиляторы и компоновщики делают все виды сумасшедших оптимизаций сегодня. Они даже могут «подпрыгнуть», чтобы искать
Func_i
, хотя я сомневаюсь в этом. Но языки Си определены так, что вы могли бы написать такой супер-простой компилятор. Так что большую часть времени это очень полезная модель.источник
1. call "function #2", put the return-type onto the stack and put the return value on the stack?
printf(..., Func_i()+1);
- компилятор должен знать типFunc_i
, чтобы он мог решить, следует ли ему генерироватьadd integer
илиadd float
инструкцию. Вы можете найти некоторые особые случаи, когда компилятор может работать без информации о типе, но компилятор должен работать для всех случаев.float
значения могут жить в регистре FPU - тогда не было бы никакой инструкции вообще. Компилятор просто отслеживает, какое значение хранится в каком регистре во время компиляции, и выдает такие вещи, как «добавить константу 1 в регистр FP X». Или он может жить в стеке, если нет свободных регистров. Затем будет инструкция «увеличить указатель стека на 4», и значение будет «ссылаться» как «указатель стека - 4». Но все это работает, только если размеры всех переменных (до и после) в стеке известны во время компиляции.Func_i()
или /Data_i
, он должен определить их типы; на ассемблере невозможно выполнить вызов к типу данных. Я должен сам детально изучать вещи, чтобы быть уверенным.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
в нем. Он просто делает то, что вы просите, и если вы сломаете его, у вас есть обе части.Рассмотрим эти биты кода:
Код слева компилируется до вызова,
foo()
а затем копируется результат, предоставленный в соответствии с соглашением о вызове / возврате, в любое место, гдеx
он хранится. Это простой случай.Код справа показывает преобразование типов и поэтому компиляторам нужно знать тип возвращаемого значения функции. Числа с плавающей точкой не могут быть сброшены в память, где другой код будет ожидать,
int
потому что не происходит никакого волшебного преобразования. Если конечный результат должен быть целым числом, должны быть инструкции, которые направляют процессор для выполнения преобразования перед сохранением. Не знаяfoo()
заранее типа возврата , компилятор не знал бы, что код преобразования необходим.Многопроходные компиляторы включают все виды вещей, одним из которых является возможность объявлять переменные, функции и методы после их первого использования. Это означает, что когда компилятор приступает к компиляции кода, он уже видит будущее и знает, что делать. Например, Java предписывает многопроходность в силу того факта, что ее синтаксис допускает объявление после использования.
источник
Func_i()
возвращаемое значение.double foo(); int x; x = foo();
просто выдает ошибку. Я знаю, что мы не можем этого сделать. Мой вопрос заключается в том, что при вызове функции процессор находит только возвращаемое значение; почему он также не может найти тип возврата?foo()
, так что компилятор знает, что с ним делать.