На странице 33 K&R (Язык программирования C, 2e) они отмечают, что
Если программа находится в нескольких исходных файлах, а переменная определена в файле1 и используется в файле2 и file3, то в файлах file2 и file3 необходимы объявления extern для связи вхождений файла переменная. Обычной практикой является сбор внешних объявлений переменных и функций в файле. отдельный файл, исторически называемый заголовком, который включается #include в начале каждого исходный файл. Суффикс .h является традиционным для имен заголовков. Функции стандарта библиотеки, например, объявляются в заголовках типа <stdio.h>.
Я пытаюсь понять, как связаны первое и второе предложения выше. В частности, является ли предположение о том, что «обычная практика», упомянутая во втором предложении, способом обойти требование использования внешних объявлений, отмеченное в первом предложении?
Позвольте мне перефразировать и расширить. Означает ли первое предложение, что если я не использую #include (как во втором предложении), то нужно использовать объявления extern, чтобы компоновщик (?) знал, что ему нужно искать указанную переменную в другом объектный файл? Напротив, возможно, в этом нет необходимости, если я использую объявление #include заголовка, который определяет эту переменную, потому что тогда это подстановка текста (препроцессором), и поэтому переменная объявляется в том же объектном файле, что и тот, в котором она находится. использовал?
В конце концов я подозреваю, что моя путаница возникает из-за не до конца понимания процесса компиляции (и процесса компоновки).
Вам необходимо понимать разницу между исходным файлом, заголовком и единицей перевода (TU). TU — это исходный файл плюс все включенные в него заголовки (прямо или косвенно) — и это то, что на самом деле компилирует компилятор. Если вы включаете объявления extern
в заголовок и включаете этот заголовок в исходный файл (прямо или косвенно), то объявления extern
видны в TU так же уверенно, как если бы вы поместили объявления в исходный файл. Причиной объявления их в заголовке является снижение риска несогласованности — разных объявлений в разных файлах.
Я до сих пор не уверен, что понимаю @JonathanLeffler. Допустим, у меня есть глобальная переменная var
, которую я использую в своем main()
. Насколько я понимаю, мне не нужно будет использовать объявление extern var;
в main()
, если я объявлю var
в том же исходном файле или если я использую #include, как вы описываете, но мне нужно будет использовать extern var;
, если var
были объявлены в другом объекте файл, который не был #включен в мой текущий файл (т. е. если они были подключены только компоновщиком). Это больше о том, о чем мой вопрос, имеет ли это смысл?
@n.m.couldbeanAI — но некоторые глобальные переменные более или менее необходимы — подумайте stdin
, stdout
, stderr
. Да, вы, вероятно, могли бы добавить функции, предполагающие stderr
для выходного потока (уже есть функции, предполагающие stdin
для входного потока и stdout
для выходного потока), но это сделало бы API более громоздким и, следовательно, более трудным для изучения.
@JonathanLeffler Да, но они идут вместе с stdio.h, не нужно думать, где их объявить и как. Насколько нам известно, они могут быть встроенными в компилятор.
Copying and expanding on a comment:
Вам необходимо понимать разницу между исходным файлом, заголовком и единицей перевода (ТУ). TU — это исходный файл плюс все включенные в него заголовки (прямо или косвенно) — и это то, что на самом деле компилирует компилятор. Если вы включаете объявления
extern
в заголовок и включаете этот заголовок в исходный файл (прямо или косвенно), то объявленияextern
видны в TU так же уверенно, как если бы вы поместили объявления в исходный файл. Причиной объявления их в заголовке является снижение риска несогласованности — разных объявлений в разных файлах.
Но за этим последовало :
Я до сих пор не уверен, что понял тогда. Допустим, у меня есть глобальная [целочисленная] переменная
var
, которую я использую в своемmain()
. Насколько я понимаю, мне не нужно было бы использовать объявлениеextern int var;
вmain()
, если бы я либо объявилvar
в том же исходном файле, либо если бы я использовал#include
, как вы описываете, но мне нужно было бы использоватьextern int var;
, если быvar
было объявлено в другом объектном файле чего не было#included
в моем нынешнем файле (т. е. если бы они были связаны только компоновщиком). Это больше о том, о чем мой вопрос, если это имеет смысл?
Вам нужно прочитать Как использовать extern для совместного использования переменных между исходными файлами? , что, в свою очередь, ссылается на В чем разница между определением и декларацией?
Я тихо предположил, что var
имеет тип int
— не так уж важно, какой это тип, но ему нужен тип.
Где-то среди коллекции объектных файлов, которые вы связываете для создания вашей программы, должно быть одно (и только одно) определение области файла переменной var
с внешней связью (что не то же самое, что которому предшествует extern
) .
Это может быть написано:
int var; // Roughly equivalent to int var = 0;
или
int var = 37; // Or any other relevant value
Формально первый вариант представляет собой предварительное определение; оно перестает быть предварительным, если к концу TU не будет альтернативного, непредварительного определения.
Заголовок будет содержать:
extern int var; // No initializer
Все файлы, использующие var
, и файл, определяющий var
, будут включать единственный заголовок, объявляющий переменную.
Это дает жизненно важную перекрестную проверку, необходимую для обеспечения того, чтобы все было согласовано. Потребуется определенная осторожность, чтобы организовать содержимое заголовков так, чтобы небольшие исходные файлы не были завалены обильными неиспользуемыми объявлениями (и определениями типов и т. д.). Но с практикой это становится легко. Вы также должны убедиться, что ваши заголовки автономны, идемпотентны и минимальны.
ИМО, вы никогда не должны писать объявление какой-либо функции с внешней связью в любом исходном файле. Если функция будет использоваться из нескольких исходных файлов, должен быть заголовок для объявления функции, который должен быть включен как там, где функция определена, так и там, где она используется. Если функция не будет использоваться из нескольких исходных файлов, ее следует определить (и, возможно, объявить) как static
. Если функция static
определена до ее использования, нет необходимости объявлять ее отдельно, хотя некоторые проекты могут предпочесть объявлять все статические функции в верхней части исходного файла, даже если это не является строго необходимым. Если функция static
вызывается до того, как она определена, вы должны объявить ее перед использованием (в C99 или более поздних версиях — хотя компиляторы, особенно старые компиляторы, с трудом обеспечивают соблюдение этого без помощи опций предупреждения).
Собираюсь принять это, но мне еще предстоит многому научиться, и, если возможно, у меня есть один дополнительный вопрос. Из вашего ключевого момента: почему необходимо включать заголовок (который имеет объявление extern) в файл, который определяет var? Означает ли это, что переменную можно «экспортировать» в другие единицы/файлы перевода? Я бы подумал, что файлу, который однозначно определяет var, не нужно делать объявление extern (то есть не нужно включать заголовок, который делает это объявление extern).
На каком-то уровне вы правы: определяющему файлу не нужен заголовок. Однако все равно включите его — перекрестная проверка будет завершена. Если ваш исходный файл определяет double var = 3.14;
, но заголовок объявляет int var;
и используется всеми файлами, на которые ссылается var
, кроме того, который его определяет, то вы находитесь в кошмарном сценарии. Если вы включите в файл заголовок, определяющий переменную, компилятор будет жаловаться на несовпадающие типы, что избавит вас от многих проблем. Заголовки — это клей, который скрепляет ваш код, обеспечивая согласованность.
Именно поэтому внешние функции должны быть объявлены в заголовке, и этот заголовок должен быть включен как там, где функция определена, так и там, где она используется. Если вы не будете следовать этому правилу, вам придется писать объявления функций в нескольких исходных файлах, и это нормально, пока вам не придется изменить интерфейс функции. На этом этапе вам (а) придется отредактировать все файлы, чтобы привести их в порядок, и (б) компилятор не сможет сказать вам, что вы облажались. Если объявления находятся в заголовке, несовместимые изменения будут помечены как ошибки компиляции.
В этом есть смысл. Еще раз спасибо за помощь!
Включение заголовка с объявлениями — это способ помещения этих объявлений в исходный файл. Это не устраняет необходимости внешних деклараций; он заполняет потребность во внешних декларациях. Объявления фактически находятся в исходном файле независимо от того, вводятся ли они туда вручную или включаются через заголовок.
… чтобы сообщить компоновщику (?) о том, что ему нужно искать указанную переменную в другом объектном файле?…
Объявление предоставляет информацию компилятору, а не компоновщику.
Компоновщик всегда ищет определения в предоставленных ему файлах. Всякий раз, когда он видит использование имени в каком-либо объектном модуле, он ищет определение этого имени в каком-либо объектном модуле (возможно, в том же самом, возможно, в более раннем объектном модуле в его процессе, возможно, в более позднем).
Объявление сообщает компилятору, к какому типу сущности относится имя — функция ли это, объект, имя типа или что-то еще, а также тип функции или объекта и т. д. Некоторые объявления также являются определениями, поскольку они сообщают компилятору создать объект, на который ссылается имя. Некоторые объявления не являются определениями и просто сообщают компилятору о характеристиках объекта, но фактическое определение можно найти в другом месте.
По сути, препроцессор делает с помощью директивы
#include
все содержимое включаемого файла в исходный файл, делая его единой единицей (единицей перевода ). Если заголовочный файл содержит объявлениеextern
нужной вам переменной, то это похоже на то, что исходный файл содержит ее, и вам не нужно другое объявление.