Смекни!
smekni.com

Методические указания к выполнению контрольных работ по дисциплине "Основы программирования" (стр. 26 из 40)

#include "filename"

заменяется содержимым файла с именем filename. (Кавычки обязательны). Часто одна или две строки такого вида появляются в начале каждого исходного файла, для того чтобы включить общие конструкции #define и описания extern для глобальных переменных. Допускается вложенность конструкций #include.

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

5.11.2. Макроподстановка. Определение вида:

#define tes 1

приводит к макроподстановке самого простого вида – замене имени на строку символов. Имена в #define имеют ту же самую форму, что и идентификаторы в «С»; заменяющий текст совершенно произволен. Нормально заменяющим текстом является остальная часть строки; длинное определение можно продолжить, поместив \ в конец продолжаемой строки. «Область действия» имени, определенного в #define, простирается от точки определения до конца исходного файла. Имена могут быть переопределены, и определения могут использовать определения, сделанные ранее. Внутри заключенных в кавычки строк подстановки не производятся, так что если, например, yes – определенное имя, то в printf("yes") не будет сделано никакой подстановки.

Так как реализация #define является частью работы макропредпроцессора, а не собственно компилятора, имеется очень мало грамматических ограничений на то, что может быть определено. Так, например, любители АЛГОЛА или ПАСКАЛЯ могут объявить:

#define begin {

#define end ;}

и затем написать

if (i > 0) then

begin

a = 1;

b = 2

end

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

Пример 5-10. Определим макрос с именем max следующим образом:

#define max(a, b) ((a) > (b) ? (a) : (b))

когда строка

x = max(p+q, r+s);

будет заменена строкой

x = ((p+q) > (r+s) ? (p+q) : (r+s));

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

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

Пример 5-11. Рассмотрим макрос

#define square(x) x*x

при обращении к нему, как square(z+1)). Здесь возникают даже некоторые чисто лексические проблемы: между именем макро и левой круглой скобкой, открывающей список ее аргументов, не должно быть никаких пробелов.


Тем не менее аппарат макросов является весьма ценным. Один практический пример дает описываемая в главе 9 стандартная библиотека ввода-вывода, в которой getchar и putchar определены как макросы (очевидно PUTCHAR должна иметь аргумент), что позволяет избежать затрат на обращение к функции при обработке каждого символа.

Другие возможности макропроцессора описаны в подробных справочниках.

Упражнение 5-9. Определите макрос swap(x,y), который обменивает значениями два своих аргумента типа int (в этом случае поможет блочная структура).

5.12. Заголовочные файлы


Теперь представим себе, что компоненты программы-калькулятора имеют существенно большие размеры, и зададимся вопросом, как в этом случае распределить их по нескольким файлам. Программу main поместим в файл, который мы назовем main.сpp. Функции push, pop и их переменные расположим во втором файле: stack.сpp; a getop – в третьем: getop.сpp. Наконец, getch и ungetch разместим в четвертом файле getch.сpp; мы отделили их от остальных функций, поскольку в реальной программе они будут получены из заранее скомпилированной библиотеки. В результате получим программу, файловая структура которой показана на рис. 5.1.

Существует еще один момент, о котором следует предупредить читателя, – определения и объявления совместно используются несколькими файлами. Мы бы хотели, насколько это возможно, централизовать эти объявления и определения так, чтобы для них существовала только одна копия. Тогда программу в процессе ее развития будет легче и исправлять, и поддерживать в нужном состоянии. Для этого общую информацию расположим в заголовочном файле calc.h, который будем по мере необходимости включать в другие файлы (строка #include была рассмотрен выше).

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

6. УКАЗАТЕЛИ И МАССИВЫ

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

Указатели обычно смешивают в одну кучу с операторами goto, характеризуя их как «чудесный» способ написания программ, которые невозможно понять. Это, безусловно, справедливо, если указатели используются беззаботно; очень просто ввести указатели, которые указывают на что-то совершенно неожиданное. Однако, при определенной дисциплине, использование указателей помогает достичь ясности и простоты. Именно этот аспект мы попытаемся здесь проиллюстрировать.

6.1. Указатели и адреса

Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель. Предположим, что х - переменная, например, типа int, а рх - указатель, созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта, так что оператор

рх = &х;

присваивает адрес х переменной рх; говорят, что рх «указывает» на х. Операция & применима только к переменным и элементам массива, конструкции вида &(х-1) и &3 являются незаконными. Нельзя также получить адрес регистровой переменной.

Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если y тоже имеет тип int, то

y = *рх;

присваивает y содержимое того, на что указывает рх. Так последовательность

рх = &х;

y = *рх;

присваивает y то же самое значение, что и оператор

y = x;

Переменные, участвующие во всем этом необходимо описать:

int x, y;

int *px;

с описанием для x и y мы уже неодонократно встречались. Описание указателя

int *px;

является новым и должно рассматриваться как мнемоническое; оно говорит, что комбинация *px имеет тип int. Это означает, что если px появляется в контексте *px, то это эквивалентно переменной типа int. Фактически синтаксис описания переменной имитирует синтаксис выражений, в которых эта переменная может появляться. Это замечание полезно во всех случаях, связанных со сложными описаниями. Например,

double atof(), *dp;

говорит, что atof() и *dp имеют в выражениях значения типа double.

Вы должны также заметить, что из этого описания следует, что указатель может указывать только на определенный вид объектов. Указатели могут входить в выражения. Например, если px указывает на целое x, то *px может появляться в любом контексте, где может встретиться x. Так операторы

· y = *px + 1 присваивает y значение, на 1 большее значения x;

· printf("%d\n", *px) печатает текущее значение x;

· d = sqrt((double) *px) получает в d квадратный корень из x (причем до передачи функции sqrt значение x преобразуется к типу double, см. главу 3).

В выражениях вида

y = *px + 1

унарные операции * и & связаны со своим операндом более крепко, чем арифметические операции, так что такое выражение берет то значение, на которое указывает px, прибавляет 1 и присваивает результат переменной y. Мы вскоре вернемся к тому, что может означать выражение

y = *px + 1) .

Ссылки на указатели могут появляться и в левой части присваиваний. Если px указывает на x, то

*px = 0

полагает X равным нулю, а

*px += 1

увеличивает его на единицу, как и выражение

(*px)++

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


И наконец, так как указатели являются переменными, то с ними можно обращаться, как и с остальными переменными. Если py – другой указатель на переменную типа int, то

py = px

копирует содержимое px в py, в результате чего py указывает на то же, что и px.

6.2. Указатели и аргументы функций