В результате успешной компиляции текста программы на 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.
- Необходимо решить каким образом разбить текст программы на отдельные файлы.
Первая проблема — чисто техническая. Она решается чтением руководств по компилятору и/или линкеру, утилите make или IDE. В самом худшем случае просто придётся проштудировать все эти руководства. Поэтому на решении этой проблемы мы останавливаться не будем.
Вторая проблема — требует гораздо более творческого подхода. Хотя и здесь существуют определённые рекомендации, несоблюдение которых приводит либо к невозможности собрать проект, либо к трудностям в дальнейшем развитии проекта.
Во-первых, нужно определить какие части программы выделить в отдельные модули. Что бы это получилось просто и естественно, программа должна быть правильно спроектирована. Как правильно спроектировать программу? — на эту тему написано много больших и правильных книг. Обязательно поищите и почитайте книги по методологии программирования — это очень полезно. А в качестве краткой рекомендации можно сказать: вся программа должна состоять из слабо связанных фрагментов. Тогда каждый такой фрагмент может быть естественным образом преобразован в отдельный модуль (единицу компиляции). Обратите внимание, что под «фрагментом» подразумевается не просто произвольный кусок кода, а функция, или группа логически связанных функций, или класс, или несколько тесно взаимодействующих классов.
Во-вторых, нужно определить интерфейсы для модулей. Здесь есть вполне чёткие правила.
Интерфейс и реализация
Когда часть программы выделяется в модуль (единицу компиляции), остальной части программы (а если быть точным, то компилятору, который будет обрабатывать остальную часть программы) надо каким-то образом объяснить что имеется в этом модуле. Для этого служат заголовочные файлы.
Таким образом, модуль состоит из двух файлов: заголовочного (интерфейс) и файла реализации.
Заголовочный файл, как правило, имеет расширение .h или .hpp, а файл реализации — .cpp для программ на C++ и .c, для программ на языке C. (Хотя в STL включаемые файлы вообще без расширений, но, по сути, они являются заголовочными файлами.)
Заголовочный файл должен содержать все объявления, которые должны быть видны снаружи. Объявления, которые не должны быть видны снаружи, делаются в файле реализации.
Что может быть в заголовочном файле
Правило 1. Заголовочный файл может содержать только объявления. Заголовочный файл не должен содержать определения.
То есть, при обработке содержимого заголовочного файла компилятор не должен генерировать информацию для объектного модуля.
Единственным «исключением» из этого правила является определение метода в объявлении класса. Но по стандарту языка, если метод определён в объявлении класса, то для этого метода используется инлайновая подстановка. Поэтому, такое объявление не порождает исполняемого кода — код будет генерироваться компилятором только при вызове этого метода.
Аналогичная ситуация и с объявлением переменных-членов класса: код будет порождаться при создании экземпляра этого класса.
Правило 2. Заголовочный файл должен иметь механизм защиты от повторного включения.
Защита от повторного включения реализуется директивами препроцессора:
Заголовочный файл сам по себе не является единицей компиляции.
Что может быть в файле реализации
Файл реализации может содержать как определения, так и объявления. Объявления, сделанные в файле реализации, будут лексически локальны для этого файла. Т.е. будут действовать только для этой единицы компиляции.
Правило 3. В файле реализации должна быть директива включения соответствующего заголовочного файла.
Понятно, что объявления, которые видны снаружи модуля, должны быть также доступны и внутри.
Правило также гарантирует соответствие между описанием и реализацией. При несовпадении, допустим, сигнатуры функции в объявлении и определении компилятор выдаст ошибку.
Правило 4. В файле реализации не должно быть объявлений, дублирующих объявления в соответствующем заголовочном файле.
При выполнении Правила 3, нарушение Правила 4 приведёт к ошибкам компиляции.
Практический пример
Допустим, у нас имеется следующая программа:
main.cpp
Эта программа не является образцом для подражания, поскольку некоторые моменты идеологически неправильны, но, во-первых, ситуации бывают разные, а во-вторых, для демонстрации эта программа подходит очень неплохо.
Итак, что у нас имеется?
- глобальная константа cint , которая используется и в классе, и в main ;
- глобальная переменная global_var , которая используется в функциях func1 , func2 и main ;
- глобальная переменная module_var , которая используется только в функциях func1 и func2 ;
- функции func1 и func2 ;
- класс CClass ;
- функция main .
Вроде вырисовываются три единицы компиляции: (1) функция main , (2) класс CClass и (3) функции func1 и func2 с глобальной переменной module_var , которая используется только в них.
Не совсем понятно, что делать с глобальной константой cint и глобальной переменной global_var . Первая тяготеет к классу CClass , вторая — к функциям func1 и func2 . Однако предположим, что планируется и эту константу, и эту переменную использовать ещё в каких-то, пока не написанных, модулях программы. Поэтому прибавится ещё одна единица компиляции.
Теперь пробуем разделить программу на модули.
Сначала, как наиболее связанные сущности (используются во многих местах программы), выносим глобальную константу cint и глобальную переменную global_var в отдельную единицу компиляции.
globals.h
globals.cpp
Обратите внимание, что глобальная переменная в заголовочном файле имеет спецификатор extern . При этом получается объявление переменной, а не её определение. Такое описание означает, что где-то существует переменная с таким именем и указанным типом. А определение этой переменной (с инициализацией) помещено в файл реализации. Константа описана в заголовочном файле.
С объявлением констант в заголовочном файле существует одна тонкость. Если константа тривиального типа, то её можно объявить в заголовочном файле. В противном случае она должна быть определена в файле реализации, а в заголовочном файле должно быть её объявление (аналогично, как для переменной). «Тривиальность» типа зависит от стандарта (см. описание того стандарта, который используется для написания программы).
Также обратите внимание (1) на защиту от повторного включения заголовочного файла и (2) на включение заголовочного файла в файле реализации.
Затем выносим в отдельный модуль функции func1 и func2 с глобальной переменной module_var . Получаем ещё два файла:
funcs.h
funcs.cpp
Поскольку переменная module_var используется только этими двумя функциями, её объявление в заголовочном файле отсутствует. Из этого модуля «на экспорт» идут только две функции.
Наконец выносим в отдельный модуль класс CClass :
CClass.h
CClass.cpp
Обратите внимание на следующие моменты.
(1) Из объявления класса убрали определения тел функций (методов). Это сделано по идеологическим причинам: интерфейс и реализация должны быть разделены (для возможности изменения реализации без изменения интерфейса). Если впоследствии будет необходимость сделать какие-то методы инлайновыми, это всегда можно сделать с помощью спецификатора.
(2) Класс имеет статический член класса. Т.е. для всех экземпляров класса эта переменная будет общей. Её инициализация выполняется не в конструкторе, а в глобальной области модуля.
Классы практически всегда выделяются в отдельные единицы компиляции.
В файле main.cpp оставляем только функцию main . И добавляем необходимые директивы включения заголовочных файлов.
main.cpp
Последний шаг: необходимо изменить «проект» построения программы так, что бы он отражал изменившуюся структуру файлов исходного кода. Детали этого шага зависят от используемой технологии построения программы и используемого ПО. Но в любом случае сначала должны быть откомпилированы четыре единицы компиляции (четыре cpp-файла), а затем полученные объектные файлы должны быть обработаны компоновщиком для получения исполняемого файла.
Типичные ошибки
Ошибка 1. Определение в заголовочном файле.
Эта ошибка в некоторых случаях может себя не проявлять. Например, когда заголовочный файл с этой ошибкой включается только один раз. Но как только этот заголовочный файл будет включён более одного раза, получим либо ошибку компиляции «многократное определение символа . », либо ошибку компоновщика аналогичного содержания, если второе включение было сделано в другой единице компиляции.
Ошибка 2. Отсутствие защиты от повторного включения заголовочного файла.
Тоже проявляет себя при определённых обстоятельствах. Может вызывать ошибку компиляции «многократное определение символа . ».
Ошибка 3. Несовпадение объявления в заголовочном файле и определения в файле реализации.
Обычно возникает в процессе редактирования исходного кода, когда в файл реализации вносятся изменения, а про заголовочный файл забывают.
Если необходимый заголовочный файл не включён, то все сущности, которые в нём объявлены, останутся неизвестными компилятору. Вызывает ошибку компиляции «не определён символ . ».
Ошибка 5. Отсутствие необходимого модуля в проекте построения программы.
Ошибка 6. Зависимость от порядка включения заголовочных файлов.
Не совсем ошибка, но таких ситуаций следует избегать. Обычно сигнализирует либо об ошибках в проектировании программы, либо об ошибках при разделении исходного кода на модули.
Заключение
В рамках небольшой статьи невозможно рассмотреть все случаи, возникающие при раздельной компиляции. Бывают ситуации, когда разделение программы или большого модуля на более мелкие кажется невозможным. Обычно это бывает, когда программа плохо спроектирована (в данном случае, части кода имеют сильные взаимные связи). Конечно, можно приложить дополнительные усилия и всё-таки разделить код на модули (или оставить как есть), но эту мозговую энергию лучше потратить более эффективно: на изменение структуры программы. Это принесёт в дальнейшем гораздо большие дивиденды, чем просто силовое решение.
Cуммирование в коде a=0; do a=a+1 while (a<5); повторится:
N будет константой в описании:
Битовая операция исключающего «или» обозначается:
В идентификаторах можно использовать
В именах переменных не допускается использования:
В каких случаях целесообразнее использовать оператор выбора вместо условного оператора?
В какой из следующих строк выполняется обращение к седьмому элементу массива, размер массива равен 10?
В каком случае используются фигурные скобок в операторе выбора if?
В описании алгоритмического языка сказано, что идентификатором может быть любая последовательность латинских букв, цифр или знака подчеркивания (_), начинающаяся с буквы или подчеркивания. Какая из нижеприведенных последовательностей может быть идентификатором?
В переменной типа Int можно хранить число:
В результате успешной компиляции текста программы на C++ с каким расширением будет получен файл?
В языке Си++ лексема - это:
В языке Си++ программа начинает выполняться с функции:
В языке Си++ тело функции ограничено операторными скобками:
Ветвление обязательно должно содержать:
"Выбрать верные утверждения
Выполнение каждой программы на C++ начинается с функции__________.
Выражение Р or (sin(x) > sin(3)) and not sqrt(x + 0.44) < 1.2) ложно при значениях переменных:
Вычисление значения Y по формуле Y=1/(x+1), если х изменяется от 0 до 6 с шагом 1.5:
Вычислить значение переменных А и В в результате выполнения условного оператора, если перед его выполнением А=0.5; В = -1.7; if (A<B) A=B else B=A;
Действие, повторяющееся в цикле, называется:
Для объявления размера массива должна использоваться__________, потому что она делает программу более масштабируемой
Если задан тип данных переменной, то известной является информация о:
Если задан тип данных переменной, то известной является информация о:
Если не будет указан базовый тип, то какой тип будет подразумеваться по умолчанию
Если условие оператора выбора ложное, то:
Идентификатор в C не может начинаться с:
"Из приведенных утверждений
а) Операция взятия по модулю (%) может применяться только к целым числам.
б) Все арифметические операции *, /, %, + и - имеют одинаковый уровень приоритета.
Из чего состоит оператор объявления имени?
К вещественным константам не относятся число:
К простым операторам относятся:
К целочисленным константам не относятся число:
Каждый оператор заканчивается__________
Какая из операций не относится к операции сравнения?
Какая из следующих записей - правильный комментарий в С++?
Какая константа указана НЕ верно?
Какие служебные символы используются для обозначения начала и конца блока кода?
Какими знаками заканчивается большинство строк кода в Си++?
"Каков будет результат выполнения операторов:
"Каков будет результат выполнения операторов:
х/=++у;езультат выполнения операторов:"
"Каков будет результат выполнения операторов:
"Каков будет результат выполнения операторов:
"Каков будет результат выполнения операторов:
"Каков будет результат выполнения операторов:
"Каков будет результат выполнения операторов:
Какого типа будет результат деления 15 на 4 при вычислении на языке С?
Какое из следующих значений эквивалентно зарезервированному слову true?
Какое ключевое слово указывает, что целая переменная не может принимать отрицательные значения?
Какой из ниже перечисленных вариантов ответа, показывает правильно записанный оператор выбора if ?
Какой из ниже перечисленных операторов, не является циклом в С++?
Какой из перечисленных типов данных не является типом данных в С++?
Какой из следующих операторов - оператор сравнения двух переменных?
Какой оператор языка C обозначает цикл с предусловием?
Какой операции нет в C++?
Какой служебный знак ставится после оператора case ?
Какую функцию должны содержать все программы на С++?
Комментарии заключаются в скобки:
Лидирующий нуль в литералах означает:
Логическое «и» обозначается:
Логическое «не равно» обозначается:
Логическое выражение Not A And (B Or C) будет истинным при значениях переменных A, B, C равных:
Логическое выражение Not(A And Not(B Or Not C)) будет ложным при значениях переменных A, B, C равных:
Метки в операторе Switch должны быть:
Многократное исполнение одного и того же участка программы называется:
Найти ошибку в записи константы:
Оператор ________ используется для принятия решений
Оператор while соответствует оператору for:
Оператор выбора________ используется для выполнения одного действия, если его условие истинно, и другого действия, если условие ложно
Оператор вывода cout может печатать несколько значений или переменных в одной команде, используя следующий синтаксис:
Оператор называется пустым, если он:
Оператор, имеющий формат записи if (< выражение >) < оператор > else< оператор > называется оператором:
Операция битового «или» обозначается:
Описанием цикла с предусловием является следующее выражение:
Определить тип результата выражения I*I+J*J+2*K/2 при вычислении на языке С, если известно, что int I,J,K;
Определить тип результата выражения I+SQRT(J) при вычислении на языке С, если известно, что int I,J,K;
Определить тип результата выражения SIN(X)+2*COS(Y)+Z при вычислении на языке С, если известно, что float X,Y,Z;
Определить, какая строка решает задачу: найти среднее значение величин А и В, результат присвоить переменной А:
"Основная идея структурного программирования состоит в том, что основными для написания программ являются три типа операторов:"
Переменная I после выполнения следующей программы: I=0; N=2; FOR (K=1;K<=6;K++) (Используемый тип данных: float) принимает значение:
Повторение набора инструкций заданное число раз называется______ повторений
Понятием «переменная» в традиционных языках программирования называется:
После метки для обозначения помеченного блока программы записывается символ:
При объявлении константы пользователь указал: const int fact=0789. Что имелось в виду?
При объявлении константы пользователь указал: const int fact=0xA. Что имелось в виду?
При помощи ключевого слова const:
При работе с двумерным массивом записан код: int mass2 [3][2]; if (i==j) sum+=mass[i][j]. Что он выполняет?
Простые операторы в C:
Простыми типами данных в С++ являются:
Процесс определения значения ключа, содержащегося в массиве, называется___________
Князев Алексей Александрович. Независимый программист и консультант.
Обзор компиляторов
Существует множество компиляторов с языка C++, которые можно использовать для создания исполняемого кода под разные платформы. Проекты компиляторов можно классифицировать по следующим критериям.
- Коммерческие и некоммерческие проекты
- Уровень поддержки современных тенденций и стандартов языка
- Эффективность результирующего кода
Если на использование коммерческих компиляторов нет особых причин, то имеет смысл использовать компилятор с языка C++ из GNU коллекции компиляторов (GNU Compiler Collection). Этот компилятор есть в любом дистрибутиве Linux, и, он, также, доступен для платформы Windows как часть проекта MinGW (Minumum GNU for Windows). Для работы с компилятором удобнее всего использовать какой-нибудь дистрибутив Linux, но если вы твердо решили учиться программировать под Windows, то удобнее всего будет установить некоммерческую версию среды разработки QtCreator вместе с QtSDK ориентированную на MinGW. Обычно, на сайте производителя Qt можно найти инсталлятор под Windows, который сразу включает в себя среду разработки QtCreator и QtSDK. Следует только быть внимательным и выбрать ту версию, которая ориентирована на MinGW. Мы, возможно, за исключением особо оговариваемых случаев, будем использовать компилятор из дистрибутива Linux.
GNU коллекция компиляторов включает в себя несколько языков. Из них, группу языков Си составляет три компилятора.
Этапы компиляции
Процесс обработки текстовых файлов с кодом на языке C++, который упрощенно называют "компиляцией", на самом деле, состоит из четырех этапов.
Рассмотрим подробнее упомянутые выше стадии обработки текстовых файлов на языке C++.
Препроцессинг
Препроцессинг, это процедура ставшая традиционной для многих обработчиков разного рода описаний, в том числе и текстов с кодами программ. В общем случае, везде, где возникает необходимость в предварительной обработке текстов реализуется некоторый язык препроцессинга элементы которого ищутся препроцессором при обработке файла.
На входе препроцессора мы имеем исходный файл с текстом на языке C++ включающим в себя элементы языка препроцессора.
На выходе препроцессора получаются так называемые компиляционные листы, состоящие исключительно из выражений языка C++, которых должно быть достаточно для создания объектных файлов на следующих этапах обработки. Последнее означает, что на момент использования каких-либо символов языка из других файлов, объявления этих символов должны присутствовать в компиляционном листе выше. Именно такие подстановки и призван осуществлять препроцессор. Часто, на вход препроцессора поступает файл размером в несколько десятков строк, а на выходе получается компиляционный лист из десятков тысяч строк.
Ассемблирование
Процесс ассемблирования с одной стороны достаточно прост для понимания и с другой стороны является наиболее сложным в реализации. По своей сути это процесс трансляции выражений одного языка в другой. Более конкретно, в данном случае, мы имеем на входе утилиты ассемблера файл с текстом на языке C++ (компиляционный лист), а на выходе мы получаем файл с текстом на языке Ассемблера. Язык Ассемблера это низкоуровневый язык который практически напрямую отображается на коды инструкций процессора целевой системы. Отличие только в том, что вместо числовых кодов инструкций используется англоязычная мнемоника и кроме непосредственно кодов инструкций присутствуют еще директивы описания сегментов и низкоуровневых данных, описываемых в терминологии байтов.
Ассемблирование не является обязательным процессом обработки файлов на языке C++. В данном случае, мы наблюдаем лишь общий подход в архитектуре проекта коллекции компиляторов GNU. Чтобы максимально объеденить разные языки в одну коллекцию, для каждого из языков реализуется свой транслятор на язык ассемблера и, при необходимости, препроцессор, а компилятор с языка ассемблера и линковщик делаются общими для всех языков коллекции.
Компиляция
В данном случае, мы имеем компилятор с языка ассемблера. Результатом его работы является объектный файл полученный на основе всего того текста, что был предоставлен в компиляционном листе. Поэтому можно говорить, что каждый объектный файл проекта соответствует одному компиляционному листу проекта.
Исходя из этих определений, в компиляционном листе перед компиляцией должны существовать все объявления (declaration) всех тех сущностей, что используются в этом компиляционном листе. Причем их объявления должны находится до момента использования этих сущностей. Иначе, компилятор не сможет подготовить обращение к соответствующей сущности. Например, не сможет оформить передачу параметров через стек вызова функции и подготовиться к приему возвращаемого функцией значения.
Линковка
На этапе линковки выполняется объединение всех объектных файлов проекта, откомпилированных по соответствующим компиляционным листам проекта в единую сущность. Это может быть приложение, статическая или динамическая библиотека. Разница в бинарных заголовках целевых файлов и несколько различной внутренней организацией. Первичной задачей линковки следует назвать задачу по подстановке адресов вызова внешних объектов, которые были образованы в объектных файлах проекта. Соответствующие реализации сущностей с адресами их размещения должны находится в видимости линковщика. Эти сущности должны быть либо в объектных файлах, тогда они должны быть указаны в списке линковки, либо во внешних библиотеках функций, статических или динамических, тогда они должны быть указаны в списке внешних библиотек.
Средства сборки проекта
Традиционно, программа на языке C++ собирается средствами утилиты make исполняющей сценарий из файла Makefile. Сценарий сборки можно писать самостоятельно,
а можно создавать его автоматически с помощью всевозможных средств организации проекта. Среди наиболее известных средств организации проекта можно указать следующие.
Современные версии QtCreator могут работать с проектами, которые используют как систему сборки QMake, так и систему сборки CMake.
Простой пример компиляции
Рассмотрим простейший проект "Hello world" на языке C++. Для его компиляции мы будет использовать консоль, в которой будем писать прямые команды компиляции. Это позволит нам максимально прочувствовать описанные выше этапы компиляции. Создадим файл с именем main.cpp и поместим в него следующий текст программы.
В представленом примере выполнена нумерация строк, чтобы упростить пояснения по коду. В реальном коде нумерации не должно быть, так как она не входит в синтаксическое описание конструкций языка C++.
В третьей строке программы описана функция main(). В контексте операционной системы, каждое приложение должно иметь точку входа. Такой точкой входа в операционных системах *nix является функция main(). Именно с нее начинается исполнение приложения после его загрузки в память вычислительной системы. Так как операционная система Windows имеет корни тесно переплетенные с историей *nix, и, фактически, является далеким проприентарным клоном *nix, то и для нее справедливо данное правило. Поэтому, если вы пишете приложение, то начинается оно всегда с функции main().
В пятой строке мы обращаемся к предопределенному объекту cout из пространства имен std, который связан с потоком вывода приложения. Используя синтаксис операций, определенных для указанного объекта, мы передаем в него строку "Hello world" и символ возврата каретки и переноса строки.
В седьмой строке мы возвращаем код 0, как код возврата функции main(). В организации процессов в операционной системы, это число будет восприниматься как код возврата приложения.
Следующим шагом проведения эксперимента выполним останов компиляции файла main.cpp после этапа ассемблирования. Для этого воспользуемся ключом -S для компилятора g++. Здесь и далее, знак доллара ($) обозначает стандартное приглашение к вводу команды в консоли *nix. Писать знак доллара не требуется.
Выполнив остановку компиляции после этапа ассемблирование, возможно будет интересно выполнить остановку компиляции и после этапа, который собственно, и выполняет компиляцию, т.е. превращение ассемблерного кода в объектный файл, который впоследствии надо будет слинковать с библиотеками, в которых будет найдено реализация объекта cout, который используется в нашей программе как некий библиотечный объект.
Для остановки компиляции после, собственно, компиляции следует воспользоваться ключом -c для компилятора g++.
Наконец, если нас не интересуют эксперименты с остановками компиляции на разных этапах и если мы просто хотим получить из нашего файла на языке C++ исполняемую программу, то следует выполнить следующую команду.
Для того, чтобы скомпилировать программу на C++ для некоторой архитектуры X, необязательно устанавливать компилятор С++ на компьютер с архитектурой X.
Не каждая программа, написанная на компилируемом языке, переносима. Т.е. не любая программа, написанная на компилируемом языке, будет работать везде одинаково.
Плюсы и минусы компилируемости в машинный код
- эффективность: программа компилируется и оптимизируется для конкретного процессора;
- нет необходимости устанавливать сторонние приложения, такие как интерпретатор или виртуальная машина (т.е. для запуска программы, написаной на компилируемом языке, не требуется установка компилятора).
Общая схема
- над препроцессором;
- над непосредственно компилятором;
- и над линковщиком.
Этап 1: препроцессор
- g++ -E square.cpp -o square_preprocessed.cpp
- g++ -E main.cpp -o main_preprocessed.cpp
Этап 2: компиляция
На вход компилятору поступает код на C++ после обработки препроцессором.
Каждый файл с кодом компилируется отдельно и независимо от других файлов с кодом. Компилируется только файлы с кодом (т.е. *.cpp).
Заголовочные файлы сами по себе ни во что не компилируются, только в составе файлов с кодом.
Если в коде C++ вы вызывает не объявленную функцию, то это ошибка этапа компиляции.
Можно "скормить" файлы с кодом непосредственно компилятору. Для компилятора g++ можно использовать ключ -c.
Этап 3: линковка (компоновка)
На этом этапе все объектные файлы объединяются в один исполняемый (или библиотечный) файл. При этом происходит подстановка адресов функций в места их вызова.
По каждому объектному файлу строится таблица всех функций, которые в нём определены.
На этапе компоновки важно, что каждая функция имеет уникальное имя. В C++ может быть две функции с одним именем, но разными параметрами. Имена функций искажаются (mangle) таким образом, что в их имени кодируются их параметры.
Например, компилятор GCC превратит имя функции foo
в _Z3fooid . Компилятор g++ также предоставляет возможность обратного преобразования.
c++filt -n _Z3fooid
foo(int, double)
Заметим, что в полученной сигнатуре не участвует возвращаемое значение, потому что в C++ не может быть двух функций с одинаковым именем и одинаковыми параметрами, но разными возвращаемыми значениями.
Аналогично функциям в линковке нуждаются глобальные переменные.
Даже для программы, состоящей всего из одного файла и из одной пустой функции int main() < return 0; >все равно требуется ликовка.
Если в коде C++ вы вызываете функцию, которая была объявлена, но не была определена, то это ошибка этапа линковки.
Для того чтобы собрать объектные файлы в один файл их нужно "скормить" компилятору и указать имя исполняемого файла:
g++ square.o main.o -o program
Читайте также: