А.А. Белеванцев, С.С. Гайсарян, Л.С. Корухова, Е.А. Кузьменкова, В.С. Махнычев. Семинары по курсу Алгоритмы и алгоритмические языки (1108027), страница 15
Текст из файла (страница 15)
макроподстановка). Так, строка DEBUGPRINT; будет заменена настроку fprintf (stderr, "debug output:");. Макроподстановка не будетвыполнена только в том случае, если имя макроса будет найдено внутри строки,заключенной в двойные кавычки: строка printf ("DEBUGPRINT: x = %d\n", x);не будет изменена препроцессором.На имя макроса налагаются те же ограничения, что и на имена переменных в Си –имя состоит из цифр, букв и знака подчеркивания (_), первый символ имени не можетбыть цифрой. Традиционно в именах макросов используются заглавные буквы.
Макроссчитается определенным от строки, содержащей соответствующую директиву #define,до конца программного файла. Если необходимо переопределить макрос или сделать егоне определенным, то необходимо использовать директиву #undef macro для удаленияопределения макроса macro, после чего можно будет снова определить макрос с тем жеименем, но другим телом.Если в ходе макроподстановки получившаяся строка снова содержит известныепрепроцессору имена макросов, то к этим именам рекурсивно применяетсямакроподстановка до получения строки, не содержащей имена макросов. Например, послеобработки препроцессором нижеследующего участка кода строкаprintf ("DEBUG\n"); будет вставлена 16 раз.#define ONE printf ("DEBUG\n")#define TWO ONE; ONE#define FOUR TWO; TWO#define SIXTEEN FOUR; FOUR; FOUR; FOURSIXTEEN;Выше мы рассматривали макросы, тела которых всегда раскрываются одинаково.70Макроподстановки можно варьировать, задавая формальные аргументы макроса (вскобках после имени макроса) и используя их в теле макроса.
При макроподстановкевместе с именем макроса указываются фактические аргументы в скобках, и вподставляемом теле макроса все вхождения формальных аргументов заменяются нафактические. При этом фактические аргументы макроса могут быть любымипоследовательностями символов или выражениями Си.Например, описанный макрос DEBUGPRINT можно усовершенствовать, введя аргументX: #define DEBUGPRINT(X) fprintf (stderr, "debug value:%d\n", X) ииспользуя его как DEBUGPRINT (i+5);(будет создана строка fprintf (stderr, "debug value:%d\n", i+5);).Необходимо подчеркнуть, что препроцессор выполняет исключительно текстовыеподстановки, не заботясь о содержимом фактических аргументов.
Например, для макроса#define HALF(X) X/2макроподстановка для строки y = HALF (z + 4);будет выглядеть как y = z + 4/2;, при этом из-за разных приоритетов деления исложения предполагаемое значение y будет неверным. Во избежание таких ситуаций и длячеткой расстановки приоритетов операций необходимо заключать в скобки как теломакроса, так и все вхождения в него формальных аргументов:#define HALF(X) ((X)/2).Кроме того, необходимо избегать написания макросов, в телах которых аргументывычисляются более одного раза, а также передавать в макросы в качестве фактическихаргументов выражения с побочным эффектом. Например, для макроса#define ABS(X) ((X) >= 0 ? (X) : -(X)),вычисляющего модуль своего аргумента, использование его как ABS(y--); приведет ктому, что побочный эффект выражения y-- будет вычислен два раза. Следовательно,пользоваться макросами необходимо с осторожностью, избегая их бездумногоприменения, которое может быть заменено функциями.
Основным сценариемиспользования является автоматическая генерация однотипного кода, которыйнежелательно писать вручную, или обеспечение переносимости программы (см. ниже).Некоторые макросы уже определены компилятором Си, и их можно использовать влюбой программе. Как правило, такие макросы содержат информацию либо о свойствахкомпилятора, например, о поддерживаемой им версии Си (макрос __STDC_VERSION__),либо о свойствах компилируемого файла и окружения и используются при отладке,например, макросы__FILE__, __LINE__, __DATE__и другие.Каждый компилятор часто добавляет собственные заранее определенные макросы кстандартным.
Например, для компилятора GCC узнать полный список таких макросовможно, запустив команду препроцессирования пустого файла с записью макросов:gcc -dM -E - < /dev/null.Кроме этого, препроцессор также позволяет организовать включение определенныхчастей файла в окончательный текст файла в зависимости от некоторых условий. Чащевсего эта возможность служит для повышения переносимости программы. Припервоначальной настройке программы на конкретную архитектуру компьютера спомощью ряда тестов создается конфигурационный заголовочный файл, в котором спомощью директив #define заводятся имена-макросы, означающие наличие или71отсутствие определенных свойств архитектуры или операционной системы. Директивы#ifdef/#else/#endif позволяют проверить, определено ли конкретное имя, и взависимости от этого включить или не включить текст между #ifdef и #endif вокончательный текст файла.
Вариант с директивой #else позволяет включитьальтернативный текст при невыполнении условия. Например, если в конфигурационномфайле есть определение #define HAVE_LONG_DOUBLE 1, то следующий фрагменткода позволяет переносимо определить тип longest_float:#ifdef HAVE_LONG_DOUBLEtypedef long double longest_float;#elsetypedef double longest_float;#endifДругим важным случаем использования условной компиляции является защита отдвойного включения заголовочного .h файла в .c файл реализации. Заголовочные файлысодержат объявления переменных и функций, которые могут привести к ошибкам в случаеих неоднократного включения в текст программы. Для большой программы со сложнымисвязями между отдельными заголовочными файлами ситуация двойного включения одногофайла, возможно, через другие заголовочные файлы, может легко возникнуть.
Воизбежание такой ошибки обычно используют следующий прием: для каждогозаголовочного файла определяют уникальное имя макроса, и весь текст файлаподключается лишь при условии, что данное имя не определено. Например, длястандартного файла stdio.h:#ifndef _STDIO_H#define _STDIO_H<... текст файла ...>#endif11.2. КомпоновщикКомпоновщик обеспечивает сборку программы в едином исполняемом файле изнабора объектных файлов.
Для задания правил компоновки глобальных переменных ифункций используются понятия внутренней и внешней компоновки (третья ситуация, безкомпоновки, используется для локальных и регистровых переменных, которые не могутбыть видимы за пределами текущего файла). Глобальные переменные, объявленные сквалификатором static, подлежат внутренней компоновке – их имена должны бытьуникальны в пределах файла.
Глобальные статические переменные с одинаковым именемв разных файлах разрешены, при компоновке получат разные области статической памятии могут использоваться независимо.Глобальные переменные, объявленные без квалификатора или с квалификаторомextern, подлежат внешней компоновке. Во всех компонуемых файлах конкретное имяможет иметь лишь одна глобальная внешняя переменная. При этом такая переменнаяможет быть объявлена в нескольких файлах, обязательно с квалификатором extern. Этиобъявления не выделяют памяти для переменной, но лишь уведомляют компилятор о72наличии в статической памяти переменной с определенным типом и именем.Определением глобальной внешней переменной является ее объявление безквалификатора extern, такое определение может содержать инициализацию переменнойконстантным выражением и должно быть единственно для всех компонуемых файлов.Такое определение выделяет необходимый для переменной объем статической памяти, ивсе объявления этой глобальной переменной с квалификатором extern ссылаются наэтот участок памяти.
Таким образом, обеспечивается использование одной переменнойразными файлами программы.Функции, как и переменные, должны быть объявлены без квалификатора staticили с квалификатором extern для того, чтобы их можно было вызывать из другихфайлов (т.н. внешние функции). По соглашению функции, определенные в других файлах,объявляются с квалификатором extern и, как правило, включаются в соответствующиезаголовочные файлы. Среди компонуемых файлов может быть лишь единственноеопределение (но много объявлений) внешней функции с данным именем. Функция же,объявленная или определенная с квалификатором static, является статической иневидима для функций из других файлов. Тем не менее, например, она может бытьвызвана через указатель, переданный в функцию из другого файла. Разрешено определятьстатические функции с одинаковым именем в разных программных файлах.Хорошей практикой программирования является объявление функций иглобальных переменных, которые не нужны для других программных файлах, какстатических (с использованием квалификатора static).
Только функции и переменные,которые составляют интерфейс данного файла, должны быть объявлены как внешние.11.3. Понятие об отладке. Отладчик gdbОтладкой называется процесс поиска и удаления ошибок из программы. Какправило, при отладке известны входные данные, на которых программа работаетнекорректно или генерирует неверные выходные данные. Основная сложностьзаключается в обнаружении причины неверного поведения – набора точек программы, вкоторых данные обрабатываются неправильно, что ведет к неверному выходу программы.Собственно исправление найденных ошибочных мест программы часто требует меньшеусилий.Базовым способом отладки является т.н.
отладочная печать – вставка в программуоператоров, выводящих информацию о текущем значении переменных программы ипринимаемых ею решениях. Отладочную печать принято выполнять на стандартный потокошибок. Для удобного включения/отключения отладочной печати во всей программевыдачу печати обычно контролируют единым макросом, опцией командной строки илипеременной окружения. Часто используют разные уровни детализации информации приотладочной печати, также управляемые опциями командной строки или переменнымиокружения.Основным инструментом отладки программ являются отладчики. Отладчикпозволяет получать информацию о поведении программы на заданном наборе входныхданных, не меняя (в идеальном случае) ее поведения.
Как правило, отладчик запускаетпрограмму на заданных входных данных в управляемом режиме, позволяя в любоймомент времени остановить или возобновить выполнение, пошагово (с точностью доотдельных строк кода на Си или ассемблере целевой машины) выполнять программу,останавливать выполнение по достижении заданных точек программы (т.н. точкиостанова) безусловно или при выполнении некоторого условия на значения заданных73ячеек памяти, останавливать выполнение при изменении значения в заданных ячейкахпамяти (т.н. точки наблюдения), просматривать или изменять (во время остановкивыполнения) значения переменных, ячеек памяти, просматривать текущий стек вызовов.Нужно отметить, что отладчик управляет программой в бинарном виде (машинныхкодах и ячейках памяти), а программисту требуется получать информацию в терминахпрограммы языка Си (строк и операторов исходного кода программы, переменных).