Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 53
Текст из файла (страница 53)
б. (*2) Перепишите программу из 88.5[5] так, чтобы она определяла степень за- трат на перехват исключений, сгенерированных на разных уровнях вложенности вызовов функций. Добавьте строковый объект в каждую функцию и замерьте все снова. 7. (*1) Найдите ошибку в первой версии функции лганья () из 88.3.3.1. 8. ("2) Напишите функцию, которая в зависимости от аргумента либо возвра- шает некоторое значение, либо генерирует исключение в виде объекта, имеющего это же значение. Замерьте разницу во времени исполнения этих двух вариантов. 9. (*2) Модифицируйте версию калькулятора из 88.5[3] так, чтобы использовались исключения.
Фиксируйте свои ошибки. Предложите способ, как можно избежать таких ошибок. 10. (*2.5) Напишите функции р1яз П, пилил(), лгвйр1у() и йгЫе(), контроли- руюшие переполнения (и потерю точности) и генерирующие исключения в этих случаях. 11. (*2) Модифицируйте программу калькулятора так, чтобы она использовала функции из 88.5[!О]. Исходные файлы и пРогРаммы Форма должна следовать за функцаей. — Ле Корбузьер Раздельная компиляция — компоновка — заголовочные файлы — заголовочные файлы стандартной библиотеки — правило одного определения — компоновка с кодом, написанным не на С++ — компоновка и указатели на функции — использование заголовочных файлов для выражения модульности — единственный заголовочный файл — несколько заголовочных файлов — зашита от повторных включений — программы — советы — упражнения. 9.1.
Раздельная компиляция Файл служит традиционной единицей хранения информации на компьютере (в файловой системе) и традиционной единицей компиляции. Хотя в принципе и существуют системы, не хранящие информацию в файлах, и в которых программист не может реализовать программу (и компилировать ее) в виде набора исходных файлов, мы все же ограничимся лишь традиционными системами, ориентированными на файлы. Обычно невозможно расположить текст всей программы в единственном файле. В частности, большинство стандартных библиотек и операционных систем не поставляется в исходных кодах, так что их код нельзя внедрить в текст пользовательской программы. Даже оставаясь в рамках пользовательских программ (реальных размеров), и неудобно, и непрактично сосредотачивать весь код в единственном файле. Рассредоточение разных частей программы по разным файлам подчеркивает ее логическую структуру, помогает человеку понять программу, а также обеспечивает возможность компилятору отслеживать указанную структуру.
Когда единицей компиляции является файл, малейшее его изменение или изменения в файлах, от которых он зависит, требует его перекомпиляции. Даже для программ скромных размеров суммарное время компиляции (и последующих перекомпиляций) заметно уменьшается при разбиении программы на несколько исходных файлов подходящих размеров. Глава 9. Исходные файлы и программы Программист предоставляет в распоряжение компилятора исходный файл (зеигсе !)!е). Затем этот файл препроцессируется, то есть выполняются макроподстановки (87.8) и директивами 1!ас!яде в текст вносится содержимое заголовочных файлов (52.4.1, 59.2.1). В результате препроцессирования образуется то, что принято называть единицей трансляции (ггапз!ш!ол ипй). Содержимое единицы трансляции описывается правилами языка С++ н над ним, собственно, и работает компилятор. В настоящей книге я подчеркиваю разницу между исходным файлом и единицей трансляции только тогда, когда нужно различать, что видит программист, а что «видит» компилятор.
Чтобы раздельная компиляция могла быть реализована, программист должен предоставить объявления с информацией о типах, достаточные для того, чтобы можно было выполнить анализ единицы трансляции отдельно от остальных частей программы. Все объявления в программе, состоящей из нескольких раздельно компилируемых частей, должны согласовываться между собой точно так же, как н в программе с единственным исходным файлом. У вас должны быть программные средства, позволяющие убедиться в этом. В частности, компоновщик может обнаружить большую часть рассогласований. Компоновщик (йд!сег) — это программа, которая связывает воедино раздельно откомпилированные части.
Иногда компоновщик называют (неправильно) загрузчиком (!оаНег). Компоновка может быть полностью завершена до загрузки программы в память компьютера для ее выполнения. С другой стороны, к программе можно добавить новый код («динамическая компоновка») уже после загрузки программы. Организацию программы в виде нескольких исходных файлов часто называют физической садгуктурой (рйузгса! зггисгиге) программы. Физическое разделение программы на отдельные файлы желательно выполнять в соответствии с логической структурой программы.
Те же самые соображения о зависимостях, которые помогают осуществлять композицию программ из пространств имен, помогают и при разбиении программ на исходные файлы. В то же время, логическая и физическая структуры программы не обязаны быть идентичными. К примеру, может оказаться удобным разместить определения функций из пространства имен по нескольким исходным файлам, или сосредоточить в единственном файле несколько определений разных пространств имен, или разбросать определение единственного пространства имен по нескольким файлам (58.2.4). Далее, мы сначала рассмотрим ряд технических вопросов, связанных с компоновкой, а потом обсудим пару способов разбиения нашей программы-калькулятора (86.1, 58.2) на отдельные файлы. 9.2. Компоновка (11пкаяе) Имена функций, классов, шаблонов, пространств имен и перечислений должны согласовываться для всех файлов программы, если только эти имена не объявлены явным образом как локальные.
Программист должен обеспечить правильное объявление имен классов, функций, пространств имен и т.д. в каждой единице трансляции, где они используются, и следить за тем, чтобы все объявления одних и тех же программных объектов были согласованы. Например, рассмотрим два файла: 9.2. Компоновка И)пйайе) // файл2с(е1. с иих= 1; 1ис)() ( /* с(о восиесЬ(ие */ ) // файл 211е2.с: ехсеги 1ис хс асс!'() ! гоЫд() (х =У() ' ) Переменная х и функция у(), используемые в функции д () из файла /1!е2. с, определены в файле у1!е1. с.
Ключевое слово ехсеги означает, что объявления х и !'() есть исключительно (и только) объявления, но не определения (94.9). Если бы в объявлении х присутствовало инициализирующее выражение, то тогда ехсеги было бы проигнорировано, поскольку объявление с инициализируклцим выражением всегда является определением. Любой объект должен определяться в программе ровно один раз.
Объявляться же он может многократно, но типы при этом должны совпадать. Например: // файл/1!е!.сс шсх = 1! исса = 1! ехсеги сис с! // файл/с(е2.с ии х! ехсеги с(оиЫе Ь! ехсеги сис с! //ознанаеос ии х = О; В этом примере имеются три ошибки: переменная х определена дважды, в разных объявлениях переменной Ь указаны разные типы, а переменная с объявлена дважды, но ни разу не определена.
Ошибки такого рода (ошибки этапа компоновки) не могут быть обнаружены на этапе компиляции, ибо компилятор работает с каждым файлом раздельно. Большинство таких ошибок позже выявляются компоновщиком. Следует отметить, что переменная, определяемая без инициализатора в глобальной области видимости или в рамках некоторого пространства имен, инициализируется по умолчанию. Это не относится к локальным переменным (94.9.5, 910.4.2) или к объектам, создаваемым в свободной памяти (в куче — 96.2.6). Например, следующий программный фрагмент содержит две ошибки: // файл/)(е!, с: 1ис хс ии С() (гесиги х! ) // файл !с1е2.с: сис х! 1ис е() (гесиги С'() с ) Вызов функции !() в файле /11е2. с является ошибкой, так как функция С() в этом файле не объявлена. Программа не будет скомпонована также еще и потому, что переменная х определена дважды.
А вот в языке С вызов !'() в файле 2с!е2.с ошибкой не является (9В.2.2). Говорят, что имена, которые можно использовать в единицах трансляции, отличных от той, где они определены, имеют внешнюю колсионовну (ехгегиа1 11и/саЛе). 256 Глава 9. Исходные файлы и программы Все имена в предыдущих примерах компоновались внешним образом. Если же на имя можно ссылаться лишь в единице трансляции, где оно определено, то говорят, что имя имеет внутреннюю компоновку (и!егпа! 1(пкауе). Функции с модификатором гп1ше (57.1.1, 510.2.9) должны определяться — посредством идентичных определений (59.2.3) — в каждой единице трансляции, где они используются (вызываются). Следовательно, следукнций пример не просто демонстрирует плохой стиль программирования, он вообще недопустим: //файл/)1е!.с: ш1те 1п(1(!и(г) (гешгп г; ) // файл/)!е2.с: (п!1пе (п(7'(!и(1) (ге(игп 1е1( ) К сожалению, такого рода ошибки с трудом обрабатываются конкретными реализациями, так что следующий пример (вполне логически корректный)— комбинация внешней компоновки и встраивания, запрещается в угоду компиляторам: //файл/1!е!.с: ех(егп галие ш( у(ш( г); 1п( (г (1пг г) (ге(игп у(г); ) //еггог: у!) не определена е данной единице трансляции // файл/йе2.с: ех(егп гп(ше ги( у (гп(1) ( ге(игп 1(-1) ) По умолчанию соли (55.4) и (урег!еу" (54.9.7) подразумевают внутреннюю компоновку.
В результате следующий пример является допустимым (но сбивающим с толку): // файл/г1е!.с: (урейеу (и( Тг соила 17мх = 7( // файл /1(е2. с: бгрейеу гоЫ Тг соим 1т х = 8( Лучше избегать определения глобальных переменных с внутренней компоновкой. Для обеспечения согласованности лучше помешать глобальные объекты с модификаторами соил( или Ы1Ые только в заголовочные файлы (59.2.1).
С помощью ключевого слова ех(егп объектам с модификатором соил( можно навязать внешнюю компоновку; //файл/1!е!.сг ех(еги соилг гиг а = 77; // файл 1(1е2.с: ех(егп соила гп( а( гоЫу() ( сои(« а « ~и г Функция у() выведет 77. 9.2. Компоновка ((~п)(а9е) С помощью анонимных (неименованных) пространств имен Я8.2.5) можно превращать имена в локальные по отношению к единице компиляции.