Устраняет ли включение заголовка необходимость в внешних объявлениях?

На странице 33 K&R (Язык программирования C, 2e) они отмечают, что

Если программа находится в нескольких исходных файлах, а переменная определена в файле1 и используется в файле2 и file3, то в файлах file2 и file3 необходимы объявления extern для связи вхождений файла переменная. Обычной практикой является сбор внешних объявлений переменных и функций в файле. отдельный файл, исторически называемый заголовком, который включается #include в начале каждого исходный файл. Суффикс .h является традиционным для имен заголовков. Функции стандарта библиотеки, например, объявляются в заголовках типа <stdio.h>.

Я пытаюсь понять, как связаны первое и второе предложения выше. В частности, является ли предположение о том, что «обычная практика», упомянутая во втором предложении, способом обойти требование использования внешних объявлений, отмеченное в первом предложении?

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

В конце концов я подозреваю, что моя путаница возникает из-за не до конца понимания процесса компиляции (и процесса компоновки).

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

Some programmer dude 19.07.2024 03:17

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

Jonathan Leffler 19.07.2024 04:08

Я до сих пор не уверен, что понимаю @JonathanLeffler. Допустим, у меня есть глобальная переменная var, которую я использую в своем main(). Насколько я понимаю, мне не нужно будет использовать объявление extern var; в main(), если я объявлю var в том же исходном файле или если я использую #include, как вы описываете, но мне нужно будет использовать extern var;, если var были объявлены в другом объекте файл, который не был #включен в мой текущий файл (т. е. если они были подключены только компоновщиком). Это больше о том, о чем мой вопрос, имеет ли это смысл?

EE18 19.07.2024 06:06

@n.m.couldbeanAI — но некоторые глобальные переменные более или менее необходимы — подумайте stdin, stdout, stderr. Да, вы, вероятно, могли бы добавить функции, предполагающие stderr для выходного потока (уже есть функции, предполагающие stdin для входного потока и stdout для выходного потока), но это сделало бы API более громоздким и, следовательно, более трудным для изучения.

Jonathan Leffler 19.07.2024 07:43

@JonathanLeffler Да, но они идут вместе с stdio.h, не нужно думать, где их объявить и как. Насколько нам известно, они могут быть встроенными в компилятор.

n. m. could be an AI 19.07.2024 08:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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).

EE18 19.07.2024 17:05

На каком-то уровне вы правы: определяющему файлу не нужен заголовок. Однако все равно включите его — перекрестная проверка будет завершена. Если ваш исходный файл определяет double var = 3.14;, но заголовок объявляет int var; и используется всеми файлами, на которые ссылается var, кроме того, который его определяет, то вы находитесь в кошмарном сценарии. Если вы включите в файл заголовок, определяющий переменную, компилятор будет жаловаться на несовпадающие типы, что избавит вас от многих проблем. Заголовки — это клей, который скрепляет ваш код, обеспечивая согласованность.

Jonathan Leffler 19.07.2024 17:21

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

Jonathan Leffler 19.07.2024 17:23

В этом есть смысл. Еще раз спасибо за помощь!

EE18 20.07.2024 03:11

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

… чтобы сообщить компоновщику (?) о том, что ему нужно искать указанную переменную в другом объектном файле?…

Объявление предоставляет информацию компилятору, а не компоновщику.

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

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

Другие вопросы по теме

Похожие вопросы

Когда мне следует создать собственное расширение Postgres
Я пытаюсь создать систему входа в систему на языке C, которая скрывает пароль с помощью '*' при вводе и возвращается назад, когда пользователь вводит неверную информацию
Как заставить CMake находить системные библиотеки по умолчанию для связывания без ручных манипуляций со сценариями?
Печать целых чисел дважды в одном операторе printf в программе на языке C
Является ли оператор & важным для использования в качестве оператора адреса?
В языке C первый символ строковой переменной равен 0, но при печати 0 исчезает?
Язык программирования C Неопределенное поведение
Как я могу найти CRC по этим данным
Как/когда именно Cortex-M4 (STM32 F4) переключает свой R13 с MSP на PSP? Нужно ли вручную переключаться при использовании PSP во встроенном asm?
Как исторически люди могли использовать целые числа для хранения указателей C?