модули исходного кода программы

C Урок 19. Модульное программирование. Раздельная компиляция

Теперь мы с вами подошли к такой ситуации, что код наших проектов достиг такой величины, что уже сложно стало его читать, потому что все функции, причём разнообразного назначения, все константы, макросы, глобальные переменные у нас находятся в одном файле main.c. Дальше такое продолжаться не может, и нам нужно теперь будет как-то разбить наш проект на какие то части по их функциональному назначению. Такие части в языке C существуют, они также поддерживаются всеми средами программирования, системами сборки и компиляторами. Они именуются модулями.

Модуль в языке C – это как правило совокупность файла с исходным кодом, как правило имеющим расширение c, и заголовочного файла. Также модуль может быть и с закрытым исходным кодом. Это статическая библиотека. Но о них будет отдельный разговор скорее всего в отдельном занятии.

Заголовочный файл, или как его ещё называют header-файл – это файл, в котором обычно находятся подключения всяческих других заголовочных файлов, библиотек, прототипы функций, некоторые глобальные переменные, структуры, массивы, указатели, макросы и прочие объявления, которые вполне могли бы находиться и в файле с исходным кодом, но, во-первых они его загромождают чрезмерной информационной нагрузкой, а также, благодаря заголовочному файлу, при его подключении в другие файлы становятся доступными многие ресурсы из модуля, частью которого данный файл является. Заголовочные файлы как правило имеют расширение h.

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

Процесс раздельной компиляции можно изобразить в виде вот такой диаграммы

модули исходного кода программы

Пока мы сегодня будем собирать наш проект также с помощью командного файла, но вскоре перейдём к более серьёзному инструменту – системе сборки – утилите make, с помощью которой полностью будет иметь смысл наша раздельная компиляция. Настоящая раздельная компиляция имеет цель не просто скомпилировать раздельно каждый модуль, но и компилировать только те модули, в которых произошли изменения. Неизменённые модули компилировать незачем, так как таких модулей может быть до тысячи и тогда процесс компиляции будет продолжаться огромное количество времени. Пока же с помощью командного файла у нас будет происходить только мнимая раздельная компиляция. Да у нас и модулей-то будет немного.

Пока мы создаём проект, как и прежде, из проекта прошлого занятия с именем MYPROG18 и присвоим ему имя MYPROG19.

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

int main()

return 0 ; //Return an integer from a function

Функцию menu() тоже удалим.

Источник

CHESTUDIO

Блог компании

Раздельная компиляция программ на C++

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

Т ермины

Пара слов о терминах. Ниже даны определения терминов так, как они используются в данной статье. В некоторых случаях эти определения имеют более узкий смысл, чем общепринятые. Это сделано намеренно, дабы не утонуть в деталях и лишних уточнениях.

Исходный код — программа, написанная на языке программирования, в текстовом формате. А также текстовый файл, содержащий исходный код.

Компилятор — программа, выполняющая компиляцию (неожиданно! не правда ли?). На данный момент среди начинающих наиболее популярными компиляторами C/C++ являются GNU g++ (и его порты под различные ОС) и MS Visual Studio C++ различных версий. Подробнее см. в Википедии статьи: Компиляторы, Компиляторы C++.

Компиляция — преобразование исходного кода в объектный модуль.

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

Компоновщик (редактор связей, линкер, сборщик) — это программа, которая производит компоновку («линковку», «сборку»): принимает на вход один или несколько объектных модулей и собирает по ним исполнимый модуль. (подробности)

Исполняемый модуль (исполняемый файл) — файл, который может быть запущен на исполнение процессором под управлением операционной системы. (подробности)

Препроцессор — программа для обработки текста. Может существовать как отдельная программа, так и быть интегрированной в компилятор. В любом случае, входные и выходные данные для препроцессора имеют текстовый формат. Препроцессор преобразует текст в соответствии с директивами препроцессора. Если текст не содержит директив препроцессора, то текст остаётся без изменений. Подробнее см. в Википедии: Препроцессор и Препроцессор Си.

IDE (англ. Integrated Development Environment) — интегрированная среда разработки. Программа (или комплекс программ), предназначенных для упрощения написания исходного кода, отладки, управления проектом, установки параметров компилятора, линкера, отладчика. Важно не путать IDE и компилятор. Как правило, компилятор самодостаточен. В состав IDE компилятор может не входить. С другой стороны с некоторыми IDE могут быть использованы различные компиляторы. (подробности)

Объявление — описание некой сущности: сигнатура функции, определение типа, описание внешней переменной, шаблон и т.п. Объявление уведомляет компилятор о её существовании и свойствах.

Определение — реализация некой сущности: переменная, функция, метод класса и т.п. При обработке определения компилятор генерирует информацию для объектного модуля: исполняемый код, резервирование памяти под переменную и т.д.

От исходного кода к исполняемому модулю

Создание исполняемого файла издавна производилось в три этапа: (1) обработка исходного кода препроцессором, (2) компиляция в объектный код и (3) компоновка объектных модулей, включая модули из объектных библиотек, в исполняемый файл. Это классическая схема для компилируемых языков. (Сейчас уже используются и другие схемы.)

Часто компиляцией программы называют весь процесс преобразования исходного кода в исполняемы модуль. Что неправильно. Обратите внимание, что в IDE этот процесс называется построение (build) проекта.

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

Итак, допустим, у нас есть программа на C++ «Hello, World!»:

Затем результат работы препроцессора передаётся компилятору. Компилятор производит весь положенный комплекс работ: от синтаксического разбора и поиска ошибок до создания объектного файла (понятно, что если имеются синтаксические ошибки, то объектный файл не создаётся). В объектном файле обычно имеется таблица внешних ссылок — некая таблица, в которой, в частности, перечислены имена подпрограмм, которые используются в объектном модуле, но код которых отсутствует в данном объектном модуле. Эти подпрограммы внешние по отношению к модулю.

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

Что бы получить нормальный исполняемый модуль, необходимо «разрешить» внешние ссылки. Т.е. добавить в исполняемый модуль код отсутствующих подпрограмм и настроить соответствующим образом все ссылки на этот код. Этим занимается компоновщик. Он анализирует таблицу внешних ссылок объектного модуля, ищет в объектных библиотеках недостающие модули, копирует их в исполняемый модуль и настраивает ссылки. После этого исполняемый модуль готов.

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

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

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

Р азделение текста программы на модули

Разделение исходного текста программы на несколько файлов становится необходимым по многим причинам:

Я намеренно использовал слово «модуль», поскольку модулем может быть как класс, так и набор функций — вопрос используемой технологии программирования.

Как только мы решаем разделить исходный текст программы на несколько файлов, возникают две проблемы:

Первая проблема — чисто техническая. Она решается чтением руководств по компилятору и/или линкеру, утилите make или IDE. В самом худшем случае просто придётся проштудировать все эти руководства. Поэтому на решении этой проблемы мы останавливаться не будем.

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

Во-первых, нужно определить какие части программы выделить в отдельные модули. Что бы это получилось просто и естественно, программа должна быть правильно спроектирована. Как правильно спроектировать программу? — на эту тему написано много больших и правильных книг. Обязательно поищите и почитайте книги по методологии программирования — это очень полезно. А в качестве краткой рекомендации можно сказать: вся программа должна состоять из слабо связанных фрагментов. Тогда каждый такой фрагмент может быть естественным образом преобразован в отдельный модуль (единицу компиляции). Обратите внимание, что под «фрагментом» подразумевается не просто произвольный кусок кода, а функция, или группа логически связанных функций, или класс, или несколько тесно взаимодействующих классов.

Во-вторых, нужно определить интерфейсы для модулей. Здесь есть вполне чёткие правила.

Интерфейс и реализация

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

Таким образом, модуль состоит из двух файлов: заголовочного (интерфейс) и файла реализации.

Заголовочный файл, как правило, имеет расширение .h или .hpp, а файл реализации — .cpp для программ на C++ и .c, для программ на языке C. (Хотя в STL включаемые файлы вообще без расширений, но, по сути, они являются заголовочными файлами.)

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

Ч то может быть в заголовочном файле

Правило 1.

Заголовочный файл может содержать только объявления. Заголовочный файл не должен содержать определения.

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

Единственным «исключением» из этого правила является определение метода в объявлении класса. Но по стандарту языка, если метод определён в объявлении класса, то для этого метода используется инлайновая подстановка. Поэтому, такое объявление не порождает исполняемого кода — код будет генерироваться компилятором только при вызове этого метода.

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

Правило 2.

Заголовочный файл должен иметь механизм защиты от повторного включения.

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

Заголовочный файл сам по себе не является единицей компиляции.

Что может быть в файле реализации

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

Правило 3.

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

Понятно, что объявления, которые видны снаружи модуля, должны быть также доступны и внутри.

Правило также гарантирует соответствие между описанием и реализацией. При несовпадении, допустим, сигнатуры функции в объявлении и определении компилятор выдаст ошибку.

Правило 4.

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

При выполнении Правила 3, нарушение Правила 4 приведёт к ошибкам компиляции.

Практический пример

Допустим, у нас имеется следующая программа:

main.cpp

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

Итак, что у нас имеется?

Теперь пробуем разделить программу на модули.

Сначала, как наиболее связанные сущности (используются во многих местах программы), выносим глобальную константу cint и глобальную переменную global_var в отдельную единицу компиляции.

globals.h

globals.cpp

С объявлением констант в заголовочном файле существует одна тонкость. Если константа тривиального типа, то её можно объявить в заголовочном файле. В противном случае она должна быть определена в файле реализации, а в заголовочном файле должно быть её объявление (аналогично, как для переменной). «Тривиальность» типа зависит от стандарта (см. описание того стандарта, который используется для написания программы).

Также обратите внимание (1) на защиту от повторного включения заголовочного файла и (2) на включение заголовочного файла в файле реализации.

funcs.h

funcs.cpp

Поскольку переменная module_var используется только этими двумя функциями, её объявление в заголовочном файле отсутствует. Из этого модуля «на экспорт» идут только две функции.

Наконец выносим в отдельный модуль класс CClass :

CClass.h

CClass.cpp

Обратите внимание на следующие моменты.

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

(2) Класс имеет статический член класса. Т.е. для всех экземпляров класса эта переменная будет общей.

Её инициализация выполняется не в конструкторе, а в глобальной области модуля.

Классы практически всегда выделяются в отдельные единицы компиляции.

main.cpp

Последний шаг: необходимо изменить «проект» построения программы так, что бы он отражал изменившуюся структуру файлов исходного кода. Детали этого шага зависят от используемой технологии построения программы и используемого ПО. Но в любом случае сначала должны быть откомпилированы четыре единицы компиляции (четыре cpp-файла), а затем полученные объектные файлы должны быть обработаны компоновщиком для получения исполняемого файла.

Типичные ошибки

Ошибка 1. Определение в заголовочном файле.

Эта ошибка в некоторых случаях может себя не проявлять. Например, когда заголовочный файл с этой ошибкой включается только один раз. Но как только этот заголовочный файл будет включён более одного раза, получим либо ошибку компиляции «многократное определение символа …», либо ошибку компоновщика аналогичного содержания, если второе включение было сделано в другой единице компиляции.

Ошибка 2. Отсутствие защиты от повторного включения заголовочного файла.

Тоже проявляет себя при определённых обстоятельствах. Может вызывать ошибку компиляции «многократное определение символа …».

Ошибка 3. Несовпадение объявления в заголовочном файле и определения в файле реализации.

Обычно возникает в процессе редактирования исходного кода, когда в файл реализации вносятся изменения, а про заголовочный файл забывают.

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

Ошибка 5. Отсутствие необходимого модуля в проекте построения программы.

Вызывает ошибку компоновки «не определён символ …». Обратите внимание, что имя символа в сообщении компоновщика почти всегда отличается от того, которое определено в программе: оно дополнено другими буквами, цифрами или знаками.

Ошибка 6. Зависимость от порядка включения заголовочных файлов.

Не совсем ошибка, но таких ситуаций следует избегать. Обычно сигнализирует либо об ошибках в проектировании программы, либо об ошибках при разделении исходного кода на модули.

Заключение

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

Источник

Модули исходного кода программы

Самый верхний уровень организации программы касается только достаточно больших проектов. Это разделение программы на более-менее независимые части (модули), их независимое проектирование и трансляция.

Иерархия

Любая сложная система не обходится без иерархии, без нее большая система превращается в нечто аморфное, необозримое и слабо управляемое.

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

Способы модульной организации программы и взаимодействия ее частей в значительной степени обусловлены особенностями трансляции программы, поэтому здесь необходимы минимальные знания о трансляции и связывании программы и ее элементов.

Сущность трансляции. Компиляция и интерпретация

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

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

· для выполнения программы, написанной на определенном формальном языке после ее компиляции необходим интерпретатор, выполняющий эту программу, но уже записанную на выходном языке компилятора;

· процессор и память любого компьютера (а в широком смысле и вся программная среда, создаваемая операционной системой, является интерпретатором машинного кода);

· в практике построения трансляторов часто встречается случай, когда программа компилируется с входного языка на некоторый промежуточный уровень (внутренний язык), для которого имеется программный интерпретатор. Многие языковые системы программирования, называемые интерпретаторами, на самом деле имеют фазу компиляции во внутренне представление, на котором производится интерпретация.

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

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

· для обеспечения совместимости и переносимости трансляторов на компьютеры с различной архитектурой или с различными операционными системами был разработан универсальный внутренний язык (P-код). Для каждой такой архитектуры необходимо реализовать свой интерпретатор P-кода. При этом все разнообразие имеющихся компиляторов с языков высокого уровня на P-код может быть использовано без каких-либо изменений.

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

· программист контролирует эффективность полученного программного кода;

· программист контролирует размерности и размещение данных в памяти;

· программный код может выполняться без поддержки какой-либо операционной среды (исполнительной системы языка, библиотек, операционной системы), т.е. на «голой» ( standalone) машине.

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

Что же касается Си++, то там указанные принципы частично нарушаются, хотя он тоже является «чистым» компилятором, но не обеспечивает чистоту программного кода и данных.

Фазы трансляции и выполнения программы

Технология подготовки программ для языков компилирующего типа (к каковым относится Си/Си++) сформировалась в начале 60-х годов и с тех пор не претерпела существенных изменений. Заложенные тогда принципы оказывают влияние на способы использования стандартных библиотечных функций и разработки больших проектов.

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

· объектные модули (с расширением – obj ), полученные в результате независимой трансляции файлов исходного текста.

модули исходного кода программы

Препроцессор

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

#define идентификатор строка_текста

Директива обеспечивает замену встречающегося в тексте программы идентификатора на соответствующую строку текста. Наиболее часто она применяется для символического обозначения константы, которая встречается многократно в различных частях программы. Например, размерность массива:

В данном примере вместо имени SIZE в текст программы будет подставлена строка, содержащая константу 100. Теперь, если нас не устраивает размерность массива, нам достаточно увеличить это значение в директиве define и повторно оттранслировать программу.

#define идентификатор(параметры) строка_с_параметрами

#define FOR(i,n) for(i=0; i

FOR(k,20) A[k]=0; // for(k=0; k

#include имя _ файла >

#include » имя _ файла «

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

Еще один полезное средство препроцессора – условная трансляция. Препроцессор способен устанавливать и проверять наличие определения ( define ) и значения как собственных переменных, так и переменных, содержащих параметры текущего окружения и характеристики транслятора. Например, можно исключить повторное включение кода директивой include, если включаемый текст обрамить такой конструкцией:

# ifndef AA // Код включается только при неопределенной переменной AA

# define AA 0 // Определить переменную препроцессора

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

Трансляция и ее фазы

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

Что касается анализа, то он происходит в три этапа, которые соответствуют трем основным составляющим любого языка программирования.

Фаза синтеза зависит от способа трансляции. В компиляторах он состоит в генерации кода, в интерпретаторах – в непосредственном исполнении (интерпретации) полученного внутреннего представления.

Модульное программирование, компоновка

· программный код, использующий в своей работе только объекты языка (типы данных, переменные, функции), определенные в текущем модуле, полностью переводится во внутреннее (двоичное) представление;

· если объект языка допускает внешний доступ из других модулей, то в объектом модуле создается точка входа, содержащая его имя и внутренний адрес в пространстве объектного модуля;

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

· объединение адресных пространств отдельных модулей (и их содержимого – внутреннего представления программы) в единое адресное пространство программного файла (компоновка);

· «соединение» внешних ссылок и соответствующих им точек входа (редактирование связей);

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

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

Понятие связывания. Статическое и динамическое связывание

· при определении языка;

· при реализации компилятора;

· во время трансляции;

· при компоновке (связывании);

· во время загрузки программы;

· во время выполнения программы, в том числе:

· при входе в модуль (процедуру, функцию);

· в произвольной точке выполнения программы.

В качестве примера рассмотрим простейший фрагмент программы, для которого перечислим более-менее полный перечень времен связывания его различных свойств с элементами архитектуры компьютера:

2. Конкретная размерность переменной int определяется при реализации соответствующего компилятора.

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

· при трансляции переменная привязывается к некоторому относительному адресу в сегменте данных объектного модуля (то есть ее размещение фиксируется только относительно начала модуля)

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

6. Если переменная определяется как автоматическая (локальная внутри тела функции или блока), то она размещается в стеке программы:

· во время трансляции определяется ее размерность и генерируются команды, которые резервируют под нее память в стеке в момент входа в тело функции (блок). То есть в процессе трансляции переменная связывается только с относительным адресом в стеке программы;

· связывание локальным переменной с ее адресом в сегменте стека осуществляется при выполнении в момент входа в тело функции (блок). Благодаря такому способу связывания в рекурсивной функции существует столько «экземпляров» локальных переменных, сколько раз функция вызывает сама себя.

В заключение отметим основные свойства Си с точки зрения понятий «связывание, статический, динамический»:

· язык Си является компилируемым языком с большой долей статического связывания. Даже там, где возможно легко реализовать введение динамических компонент (например, создание локальных массивов изменяемой размерности), это исключается ради поддержания единообразия;

· почти все случаи динамического связывания реализуются явно и требуют программной (технологической) поддержки программистом.

Именно поэтому примеры динамического связывания можно «перечесть по пальцам»:

· динамические переменные и массивы ( 5.6);

· динамическое связывание функций при помощи указателей на функции ( 9.3);

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *