Лекция 7 Модульность Причины модульности • Структурированность Данные, разделенные по отдельным файлам проще поддаются структурированию, анализу и навигации • Абстракция и сокрытие реализации Возможность использования функций без указания их реализации Возможность создания библиотек • Частичная компиляция Наличие «промежуточного формата» и этапа «связывания» позволяет проводить компиляцию только части проекта Последовательность сборки • Препроцессирование Обработка включаемых файлов и директив компилятора Подготовка «чистого» кода • Компиляция Синтаксическая и семантическая обработка Трансляция реализаций в машинные коды • Компоновка Связывание имен и реализаций Подготовка конечного исполнимого файла Исходный код *.c,*.h,*.cpp,*.hpp Препроцессирование Единица трансляции *.i Компиляция Промежуточный файл *.obj Компоновка Исполнимый файл *.exe, *.dll, *.lib Фактическая схема file1.h common.h file1.cpp file2.h file2.cpp file3.h file3.cpp Препроцессор + Компилятор (Compiler) file1.obj file2.obj Компоновщик (Linker) file.exe file3.obj Препроцессирование Этапы препроцессирования • Начальное процессирование Подготовка текста программы к разбору • Лексический разбор (токенизация) Разбиение текста программы на лексические единицы (лексемы или токены) – ключевые слова, имена, операторы. • Подстановка Включение заголовочных файлов, условная компиляция, раскрытие макросов Начальное процессирование • Чтение файла в память Файл прочитывается как последовательность байт • Разбиение на строки Последовательность байт разбивается на последовательность строк. Разделителями являются принятые в ОС символы (CR,LF,CR LF) • Подстановка триграфов и диграфов Тройные символы типа ??/ заменяются на \ • Склеивание «разбитых» строк Строка, заканчивающаяся символом \, присоединяет к себе следующую строку • Замена комментариев на пробелы Однострочные комментарии от // до конца строки Многострочные комментарии от /* до первого появления */ Скрытые ловушки • Склеивание комментариев int f = 9 ; // What the f, David Blaine????????/ f = 12 ; • Вложенные комментарии /* I don’t need this int f () { /* Nobody needs this */ int a = 9 ; } */ Лексический разбор (токенизация) • Разделение по whitespace Whitespace (пробельными) символами являются символы пробела, табуляции и переноса строки • «Жадный» разбор в неоднозначных случаях В неоднозначных случаях выбирается допустимая лексема максимальной длины. Например a+++b будет разобрано как a ++ + b a ++ + b • Сообщение об ошибке, если лексема не принадлежит языку Например, в случае лексем @ ` и т.п. Макроподстановка • • • • Включение заголовочных файлов Директивы (команды) комилятору Подстановка макросов Условная компиляция #include #pragma #define #undef #error #if #else #elif #endif #ifdef #ifndef Включение заголовочных файлов • Полное включение Директива #include полностью замещается текстом включаемого файла в текущей единице трансляции. • Поиск стандартных заголовочных файлов #include <file.h> Заголовочный файл ищется среди списка каталогов стандартных библиотек путем добавления к ним пути в угловых скобках. • Поиск локальных заголовочных файлов #include "file.h" Аналогично предыдущему, но в начало поиска добавлюятся каталоги с файлом, в котором указана директива, а также «каталоги дополнительного включения». Специальные директивы • Директива генерации ошибки #error message Генерация ошибки с текстовым сообщением. Используется при условной компиляции (например, чтобы сообщить о неподдерживаемых компиляторе или платформе). • Директива компилятора #pragma command parameters Специфична для каждого компилятора. Используется для управления поведением компилятора. #pragma #pragma #pragma #pragma #pragma once warning ( 4290:disable ) pack (push,1) pack (pop) comment ( lib, “libpng.lib" ) Макросы • Непараметризованный макрос #define name text Указывает препроцессору, что в дальнейшем коде следует заменять лексему name на соответствующий текст text. • Параметризованный макрос #define name(x,y) x+y Указывает препроцессору, что в дальнейшем коде следует заменять последовательность лексем name(параметры) на текст text, в котором лексемы-параметры заменяются аргументы макроса • Отмена макроса #undef name Отменяет дальнейшую подстановку имени Примеры #define NULL 0 int * t = NULL ; // int * t = 0 ; int BERNULLY = 8 ; // Нет подстановки const char p[] = “NULL” ; // Нет подстановки #define test() hell test()o // hell o , а не hello #define max(a,b) ((a)>(b)?(a):(b)) //директивы компилятора всегда однострочны! #define max_fn(type) type max ( type x, type y )\ { \ return max( x,y ); \ } Макрооператоры # и ## • Преобразование в строку #define as_str(x) # x printf ( as_str(c u l 8 r)); // printf("c u l 8 r"); Параметр шаблона преобразуется в строку (заключается в кавычки) • Конкатенация лексем Две лексемы-имени, между которыми находится символ ##, соединяются в одну #define infix(x) x ## _ ## x infix(O) // O_O infix(o O) // o O_o O #define as_instr(x) “Who “ #x “ are” as_instr ( you ) // “Who you are” Недостатки макросов #define max(a,b) a > b ? a : b • Отсутствие типизации max (7,”Hello”) max (7,int) • Досинтаксическая подстановка int a = max (7,2); int b = max (7,2)+4; // int a = 7 > 2 ? 7 : 2 ; // int b = 7 > 2 ? 7 : 2 + 4 ; #undef max #define max(a,b) ((a) > (b) ? (a) : (b)) int c=max(a++,++b);//int c=(a++)>(++b)?(a++):(++b); Условная компиляция #define DEBUG #define MSVC_VER 0500 #if 0 printf (“yet another commented line”); #endif #if defined(DEBUG) # define MODE “Debuggy” #elif MSVC_VER < 0500 # define MODE “Ye olde” #else # define MODE “Whatever” #endif Условная компиляция #define DEBUG #if defined(DEBUG) printf (“debug”); #else printf (“release”); #endif #ifdef DEBUG printf (“debug”); #endif #ifndef DEBUG printf (“release”); #endif Компиляция Этапы компиляции • Синтаксический анализ Выделение инструкций, проверка их синтаксиса • Семантический анализ Разрешение имен, соответствие типов, приоритет операций и т.п. • Трансляция в машинный код Перевод содержимого функций единицы трансляции в машинные коды с сохранением имен и прототипов. Все исользуемые имена на этом этапе должны быть объявлены, но могут не быть определены. Результатом компиляции является промежуточный («объектный») файл, содержащий откомпилированный код и таблицу имен всех объектов, доступных в этой единице трансляции. Код может содержать символические ссылки на неразрешенные имена (определенные, например, в других единицах трансляции). Компоновка Этапы компоновки • Разрешение имен Поиск соответствий имен в разных единицах трансляции. На этом этапе все имена должны быть разрешены • Связывание Подстановка адресов соответствующих объектов (переменных, функций) вместо символических имен. • Сборка Подготовка финального исполнимого файла (добавление заголовка, связывание с точкой входа, связывание с runtime-функциями и т.п.) Внутренняя и внешняя компоновка • Внешняя компоновка Имя, которое может быть использовано в других единицах трансляции, считается именем со внешней компоновкой • Ко внешним данным относятся: Глобальные переменные (объявленные вне функций) Функции Глобальные пользовательские типы • Все остальное – внутренние данные int a ; void * b ; enum S { A, B }; void foo () { int c = 9 ; } Ключевое слово extern • Используется как модификатор при объявлениях переменных • Применяется только к глобальным объектам • Указывает, что объект определен в другой единице компиляции int a ; // extern int extern int extern int определена в этой единице b ; // определена в другой единице c = 9 ; // extern теряет смысл – c определено sum(int,int); // необязательно Ключевое слово static В контексте глобальных объектов ключевое слово static означает внутреннюю компононовку объекта. int a ; // определена в этой единице extern int b ; // определена в другой единице static int c = 9 ; // определена в этой единице // допускает объявление глобального // имени c в другой единице static int max(int,int); // прототип функции с внутренней // компоновкой Правило одного определения • ODR (One Definition Rule) Каждый объект (тип, переменная, функция) должен быть определен только один раз. При этом ему может предшествовать любое количество идентичных объявлений. • Решение при многократном включении заголовочного файла #ifndef __SOME_HEADER_H__ #define __SOME_HEADER_H__ // тело файла #endif #include “file.h” #include “file.h” #include “file.h” file.h file.cpp Примеры File1.cpp int sum(int, int); // объявление ф-ции и другой единицы extern int a ; // внешняя a (из file2.cpp) extern int c ; // ошибка (с не определена) int d ; // ошибка (d определена дважды) int eight () { return sum(3,5); } // Ok void bar () { foo(); } // Ошибка: foo не определена File2.cpp int a ; // внешняя линковка, используется в file1.cpp extern int c ; // ошибка (с не определена) int d ; // ошибка (d определена дважды) double eight ; // ошибка – внешнее имя из file1.cpp static int bar = 7 ; // все хорошо – внутренняя комп. int sum (int a, int b) // функция с внешней компоновкой { return a + b ; } void foo () {}; // функция с внешней компоновкой