Присваивание указателей друг другу 6 страница

#define SUM(x, у) ((х) + (у))

Вызываем его в главной программе, передавая макросу в качестве фактических параметров константы int, float и char типов:

printf("3 + 5 = %d\n", SUM(3, 5));

printf("3.4 + 5.7 = %0.1f\n", SUM(3.4, 5.7));

printf("’a’ + ‘0’ = ‘%c’\n", SUM(‘a’, ‘0’));

После выполнения программы на экране будет отображено следующее:

3 + 5 = 8

3.4 + 5.7 = 9.1

'a'+’0’ = 'q’

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

S = CIRC(a++);

предполагая рассчитать площадь и затем операцией постфиксного инкремента увеличить радиус на 1. Макрос будет расширен следующим образом:

S = (3.14 * (a++) * (a++));

Теперь выведем на экран полученную площадь S и переменную a (допустим до этого a == 2):

printf(“\n%f\n%d”, S, a);

получим:

12.56

При этом площадь вычислена верно, но постфиксный инкремент вычислялся два раза. В результате значение радиуса a увеличилось не на 1, а на 2.

Если мы попробуем вывести следующим образом (до этого a == 2):

printf(“\n%f\n%d”, CIRC(a++), a);

то получим:

18.84

В этом случае один инкремент произошел прямо при подсчёте площади круга.

Если же этот макрос вызвать следующим образом:

S = CIRC(++a);

предполагая увеличить радиус на 1 и вычислить площадь круга с таким увеличенным радиусом. Макрос будет расширен следующим образом:

S = (3.14159 * (++a) * (++a));

Теперь выведем на экран полученную площадь S и переменную a (допустим до этого a == 1):

printf(“\n%f\n%d”, S, a);

получим:

28.56

При этом макрос расширился до:

S = (3.14159 * (a + 2) * (a + 2));

Если мы попробуем вывести следующим образом (до этого a == 1):

printf(“\n%f\n%d”, CIRC(a++), a);

то получим:

18.84

При этом макрос расширился до:

S = (3.14159 * (a + 1) * (a + 2));

Всех этих побочных эффектов не будет, если вместо макроса использовать описанную выше функцию circ.

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

Хороший стиль программирования: Избегайте применения сложных макросов с параметрами, так как они могут приводить к нежелательным побочным эффектам. Вместо макросов в этих случаях лучше использовать встраиваемые функции inline.

Преимущества макросов с параметрами перед функциями:

1. экономия времени выполнения.

Недостатки:

2. увеличение объема программного кода;

3. неправильная интерпретация изменяющихся параметров;

4. отсутствие контроля согласования типов.

11.3.3. Директива #undef

Определения символических констант и макросов могут быть аннулированы при помощи директивы препроцессора #undef, имеющей вид:

#undef идентификатор

Директива отменяет определение символической константы или макроса с указанным идентификатором. Таким образом, область действия символической константы или макроса начинается с места их определения и заканчивается явным их аннулированием директивой #undef или концом файла. После аннулирования соответствующий идентификатор может быть снова использован в директиве #define.

Например, возможен следующий код:

#define MyConst 128

// Здесь константа MyConst равна 128

...

#undef MyConst

// Здесь константу MyConst использовать нельзя

...

#define MyConst 64

// Здесь константа MyConst равна 64.

11.4. Условная компиляция

Условная компиляция дает возможность программисту управлять выполнением директив препроцессора и компиляцией программного кода. Каждая условная директива препроцессора вычисляет значение целочисленного константного выражения. Операции преобразования типов, операция sizeof, константы перечислимого типа и переменные не могут участвовать в выражениях, вычисляемых в директивах препроцессора, так как эти операции и иницилизация переменных происходят на этапе компиляции или выполнения программы, в то время, как обработка директив препроцессора происходит перед компиляцией. Поэтому, при условной компиляции используются операции сравнения (>, <, >=, <=, ==), операции логического И(&&), ИЛИ(||) и отрицания(!) в сочетании с константными переменными или символическими константами, определёнными с помощью директивы #define

Условная директива препроцессора #if во многом похожа на оператор if. Ее синтаксис имеет вид:

#if условие

фрагмент кода

#endif

Предупреждение: в условии могут содержаться только константные величины – либо символические константы, определённые с помощью оператора #define, либо константные переменные.

Предупреждение: между #if и #endif фигурные скобки не ставятся.

Предупреждение: не забывайте закрывать каждый #if соответствующим #endif.

В этой записи условие является целочисленным выражением. Если это выражение возвращает не нуль (истинно), то фрагмент кода, заключенный между директивой #if и директивой #endif компилируется. Если же выражение возвращает нуль (ложно), то этот фрагмент игнорируется и препроцессором, и компилятором.

В условиях, помимо обычных выражений, можно использовать конструкцию

defined идентификатор

defined возвращает 1, если ранее указанный идентификатор был определен директивой #define, и возвращает 0 в противном случае. Например, возможен следующий код:

#if defined Debug &&!defined MyConst

фрагмент кода

#endif

Фрагмент кода будет выполняться, если ранее была объявлена директива

#define Debug

и не было директивы

#define MyConst

или эта директива была отменена директивой

#undef MyConst

Конструкция #if defined может быть заменена эквивалентной ей директивой #ifdef, а конструкция #if!defined – директивой #ifndef.

Например, тексты

#ifdef Size

...

#endif

и

#if defined Size

...

#endif

эквивалентны.

Оператор #if defined часто используется при включении заголовочных файлов, для исключения повторной вставки кода заголовочного файла. Например:

#if!defined(MYLIB_H)

#define MYLIB_H

/* Текст объявляемых заголовков */

#include "MyBibl.h"

...

#endif /* Конец MYLIB_H */

В первый раз, когда константа MYLIB_H ещё не объявлена!defined(MYLIB_H) возвращает 1 и операторы между #if и #endif выполняются. В том числе оператором #define MYLIB_H объявляется MYLIB_H. Поэтому при достижении препроцессором следующего вхождения этой директивы вложенное в неё содержимое будет игнорироваться.

Можно использовать более сложные конструкции условных директив препроцессора при помощи директив #elif (эквивалент else if в обычной структуре if) и #else (эквивалент else в структуре if). Например, в коде

#if условие 1

фрагмент кода 1

#elif условие 2

фрагмент кода 2

#else

фрагмент кода 3

#endif

фрагмент кода 1 будет компилироваться, если выполняется условие 1, фрагмент кода 2 будет компилироваться, если выполняется условие 2 и не выполняется условие 1, а фрагмент кода 3 будет компилироваться, если не выполняется ни одно из предыдущих условий.

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

#ifdef Debug

операторы отладки

#endif

Тогда, если в начале программы вы введете директиву

#define Debug

операторы отладки будут компилироваться и выполняться. Но когда вы уберете или закомментируете эту директиву #define, определяющую введенный вами идентификатор Debug, все операторы отладки исчезнут из текста.

Конечно, можно поступить иначе: ввести переменную булева типа (переменная, которая может принимать два значения – “истина” и “ложь”. В Си такого типа не существует, однако переменную можно создать используя тип unsigned char или unsigned int и учитывать, что значение “0” и есть “0” (false), а все остальные значения подразумевают “1” (true). Этот же принцип использован в условных операторах языка Си (if; switch/case)) Debug, задать ей в начале выполнения приложения значение true и оформлять процесс отладки следующим образом:

if (Debug)

{

операторы отладки

}

Если в дальнейшем заменить задаваемое значение Debug на false, то операторы отладки перестанут выполняться. Отличие этого подхода от использования директив препроцессора заключается в том, что коды операторов отладки в этом случае останутся в тексте программы, увеличивая размер выполняемого модуля. А директивы условной компиляции просто уберут отладочный код из программы.

11.5. Директивы # и ##

Ранее нами уже были рассмотрены макросы и способы их применения в программах на языке Си. Для расширения возможностей макросов существуют ещё две директивы: # и ##.

Оператор # превращает аргумент, которому он предшествует, в строку, заключенную в кавычки. Например:

#define mkstr(s) # s

Данный оператор преобразует аргумент в строку. Вызов mkstr(123) раскроется в ("123").

Оператор ## используется для конкатенации двух лексем. Например:

#define concat(a,b) a ## b

Данный оператор соединяет ("склеивает") свои аргументы, может использоваться для формирования идентификаторов. Вызов concat(x,4) раскроется в (x4).

Допускается ставить по одному необязательному пробелу слева и справа от знака ##. При раскрытии макроопределения препроцессор удалит пробелы. Вложенные маровызовы, расположенные в строке макроопределения, раскрываются только в момент раскрытия самого макроса, а не в момент его определения.

12. Модульное программирование

В общем случае, программа на Си может состоять из нескольких отдельно компилируемых модулей. Каждый из них находится в своем текстовом файле, а после компиляции, «превращается» в т.н. объектный файл с расширением.obj. Вот, например, как может выглядеть программа печати строки, если ее исходный код разбить на два модуля:

Module1.c   Module2.c
int main() { myprint(“Joe”); }   void myprint(char* s) { while (*s) putch(s++); }

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

Несомненно, двухэтапный подход к созданию выполнимого файла открывает множество возможностей, таких, например, как создание библиотек. Технически, связывание отдельно скомпилированных модулей, возможно благодаря тому, что в объектные файлы кроме машинных кодов команд и данных программы заносятся т.н. неразрешенные символьные ссылки. В нашем случае объектный файл Module1.obj содержал вместо конкретного указателя имя функции myprint(), а модуль Module2.obj информацию для линковщика о том, что в Module2.obj есть функция myprint(). Линковщику оставалось лишь подставить вместо неразрешенной ссылки из первого модуля адрес функции из второго.

Однако у такого подхода есть и обратная сторона: тот факт, что перед вызовом функции не обязательно иметь её прототип, делает невозможной проверку типов во время компиляции. Так, функция myprint может быть вызвана из модуля Module1.c (не содержащего ее прототипа) с параметрами не только неверного типа, но и количества. Очень остро эта проблема встает в программах, с большим количеством функций, особенно ввиду того, что, обычно, чем больше проект, тем активней в нем используется модульное программирование.

Однако способ устранения данного недостатка модульного программирования лежит на поверхности, и заключается в использовании прототипов (заголовков). Если бы текст Module1.c выглядел так:

Module1.c
void myprint(char *s)   int main() { myprint(“Joe”); }

никаких проблем с неверными параметрами функции возникнуть бы не могло. Встретив прототип, компилятор не допустит неверного преобразования типов. Но это еще не способ решения проблемы: во-первых, переписывая из раза в раз заголовок можно и в нем допустить ошибку, а во-вторых, такой подход просто не удобен, и значит, программист просто поленится переписать заголовки всех функций, которые собирается использовать. Перечислять недостатки можно было бы и дальше, но намного проще устранить их, создав текстовый файл, в котором будут храниться заголовки всех функций данного модуля. Читатель, наверное, уже догадался, что, раз файл должен хранить заголовки функций, его нужно называть заголовочным, и дать ему расширение “h” (от английского слова header – заголовок). В нашем случае заголовочный файл будет иметь имя Module2.h, что вполне логично, т.к. содержать он будет прототипы функций из Module2.c:

Module2.h   Module2.c
void myprint(char *s);   #include “Module2.h”   void myprint(char* s) { while (*s) putch(s++); }

Теперь, добавив соответствующую строку в module1.c, получим намного более удобную и надежную конструкцию, чем та, что была получена вначале.

Module1.c
// Вместо этой строки в файл будет подставлено содержимое // заголовочного файла, т.е. прототипы функций из Module2.c #include “Module2.h” int main() { myprint(“Joe”); }

Заметьте, что заголовочный файл включен в Module2.c не случайно: это не даст кодировщику забыть изменить прототипы, если он изменит заголовок одной из функций модуля. Так, если

void myprint (char* s) {}

по каким-то причинам, будет заменен, на

char* myprint (char* s, int *res)

а заголовок останется прежним, программист увидит сообщение компилятора о том, что прототип из Module2.h и заголовок функции в Module2.c не совпадают.

Обычно IDE предоставляет удобный механизм создания многомодульных программ так, в Borland C++ 3.1 это можно сделать, выбрав в меню пункт project -> open project и введя несуществующее в данном каталоге имя. При этом будут созданы файлы имя.prj и имя.dsk, в которых IDE сохранит служебную информацию о проекте. Далее, в служебном окне project, можно, нажав insert, ввести имена файлов, которые будут содержать исходный код (существующие, или несуществующие). Подробнее о работе с проектами в вашей среде разработки можно прочесть в файле справки, или в документации по системе.

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

Program.c   Modl.h
#include “Modl.h” int main() { myprint(“Joe”); }   void myprint(char *s) { while (*s) putch(s++); }

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

Чтобы проверить работоспособность примера программы из нескольких модулей, создадим проект с именем test, а Module1.c, Module2.c и Module2.h переименуем в main.c, unit.c и unit.h соответственно. Если сохранить все файлы в одном каталоге, должна получиться примерно такая картина:

P:\TC\C\Projects>dir

Содержимое папки P:\TC\C\Projects

22.02.2003 19:45 <DIR>.

22.02.2003 19:45 <DIR>..

22.02.2003 19:43 180 Main.c

22.02.2003 19:45 27 533 Test.dsk

22.02.2003 19:45 4 143 Test.prj

22.02.2003 19:43 82 Unit.c

22.02.2003 19:43 23 Unit.h

Теперь, если открыт проект (при этом ни одно окно с кодом может быть не открыто) при запуске процесса компиляции будет создан исполнимый файл. Процесс компиляции проходит следующим образом: вначале, по отдельности, компилируются все модули (файлы, включенные в проект), затем, полученные объектные файлы передаются линковщику, который разрешает неразрешенные ссылки и создает исполнимый файл. Следует упомянуть, что компиляция каждого модуля проходит вне зависимости от остальных – так, как если бы был открыт не проект, а лишь этот файл. Естественно, если на этапе компиляции любого из модулей будут найдены ошибки, или, при попытке создания выполнимого файла, линковщик обнаружит, что ни в одном из модулей нет тела функции, вызванной где-либо, IDE сообщит об ошибке.

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

Program.c   Modl.h
#include “Modl.h #include “Another.h” int main() { myprint(“Joe”); }   void myprint(char *s);

В данном примере таким заголовочным файлом является Another.h – он необходим как в Modl.h, так и в Program.c. С одной стороны, если Modl.h построен правильно и не содержит определений, это не грозит никакими ошибками, т.к. непротиворечивые объявления, могут встретиться в модуле несколько раз. С другой стороны, многократное включение одного и того же файла, ведет к неоправданным потерям времени при компиляции.

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

Another.h
#ifndef ANOTHERH #define ANOTHERH //------------------------------------- int *someheader(int, int*, char); void oneheader(char*); const int *twoheader(int, char); //... //------------------------------------- #endif

Компилятор заходит в блок #ifndef, если символическая константа ANOTHERH еще не объявлена, и, первым делом, объявляет ее. Теперь, сколько бы раз не встретился заголовочный файл, работа по включению его содержимого в модуль будет произведена лишь однажды.

Сегодня модульное программирование применяется очень широко. Это обусловлено множеством различных факторов, важнейшими из которых являются:

· Необходимость совместной работы множества программистов.

· Возможность разбить исходный код на несколько частей.

· Удобство создания библиотек общеиспользуемых функций.

Вопрос о том, как именно распределить код по модулям и какие объявления поместить в заголовочных файлах вовсе не является тривиальным. Однако есть несколько типичных ситуаций, который встречаются довольно часто, самой простой из которых, является создание библиотечного модуля. Фактически она уже была рассмотрена выше: для файла с исходным кодом, создается заголовочный файл, который должен быть включен во все модули, использующие определенные в нем функции. В дальнейшем, будем называть эти файлы lib.c и lib.h.

Очень часто, для сокрытия исходного кода распространяемых на коммерческой основе библиотек, lib.c компилируют и распространяют вместо него lib.obj и, естественно, lib.h.

Следует помнить о том, что модуль может содержать не только функции, но и переменные, доступ к которым может быть получен извне. Однако, т.к. тип переменной компилятор должен «знать» до первого ее использования опустить объявление переменной нельзя:

Lib.h   Lib.c
#ifndef LIBH #define LIBH //----------------------- // Заголовочный файл не // содержит объявления // useful_var //----------------------- #endif   #include “Lib.h” int useful_var;

Однако объявить в заголовочном файле переменную не так просто, ведь при включении заголовочного файла в программу, будет создана локальная копия переменной:

Lib.h   Program.c
#ifndef LIBH #define LIBH //----------------------- #include “Lib.h” // Объявление? int useful_var; //----------------------- #endif   #include “Lib.h void someFunction(void) { useful_var = 0; }

В приведенном примере включение в program.c файла lib.h равносильно включению строки “int useful_var;”:

Program.c
int useful_var; // Локальная копия переменной void somefunction(void) { useful_var = 0; }

В памяти будет создана еще одна переменная, к которой и будет обращаться somefunction из примера. Обойти эту проблему можно при помощи ключевого слова extern:

Lib.h
#ifndef LIBH #define LIBH //------------------------------------- #include “Lib.h” // Объявление! extern int useful_var; //------------------------------------- #endif

Теперь somefunction изменит именно переменную из модуля lib.h. Следует помнить, что объявление, содержащее инициализацию, становится определением:

extern int useful_var = 0; // Определение!

Ключевое слово extern можно использовать и с объявлениями функций, хотя и является там избыточным:

extern void somefunction(void);

Библиотечный модуль может содержать функции и переменные, предназначенные лишь для внутреннего использования, их прототипы не следует включать в заголовочные файлы. Более сложной и интересной является ситуация, когда разные функции должны быть доступны из разных модулей. Пусть, например, lib.c содержит три функции:

Lib.c
void f1(void); // Для общего пользования void f2(void); // Для пользования в специальных модулях void f3(void); // Для внутреннего пользования

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

PublicLib.h   ProtectedLib.h
#ifndef PUBLICLIB #define PUBLICLIB //---------------------- void f1(void); //---------------------- #endif   #ifndef PROTECTEDLIB #define PROTECTEDLIB //----------------------- void f1(void); void f2(void); //----------------------- #endif

Не следует забывать и о том, что сам библиотечный модуль должен включать оба заголовочных файла:

Lib.c
#include “PublicLib.h” #include “ProtectedLib.h” void f1(void); void f2(void); void f3(void);

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

Большой интерес также представляет вопрос о том, куда включать заголовочные файлы, если они нужны в модуле, который сам имеет заголовочный файл. Например, если в lib.c используются функции из стандартной библиотеки conio.h, то куда поместить строку

#include <ConIO.h>

в lib.c или в lib.h? Ответ не однозначен, но обычно, директива помещается в заголовочный файл только в том случае, если без этого не обойтись:

Lib.h
//----------------------------------------------------------- #include <ConIO.h> /* Структура text_info объявлена в ConIO.h */ somefunction(struct text_info* sti); /* Перечислимый тип text_modes также объявлен в ConIO.h */ enother_function(text_modes tm); //-----------------------------------------------------------

Следует заметить, что т.к. lib.h включен в lib.c подключать сторонние библиотеки в обоих файлах не стоит:

Lib.h   Program.c
//---------------------- #include <ConIO.h> //----------------------   #include “Lib.h” // Нежелательно... #include <ConIO.h> //...

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

13. Введение
в объектно-ориентированное программирование

13.1. Постановка задачи

В качестве тривиальной задачи, приводящей к необходимости применения технологии объектно-ориентированного программирования (ООП), можно рассмотреть создание стека. Создадим простейший стек (как область памяти, допускающую лишь две операции над собой: поместить элемент в вершину, и взять элемент из вершины), основанный на статическом массиве:

Пример 13.1. Реализация простейшего статического стека.

int stack[50]; // Массив хранит элементы стека

int top; // Номер элемента в вершине стека

/* Функция добавления элемента в стек */

void push(int a);

/* Функция извлечения элемента из стека */

int pop();

Функция main() использует стек, который реализован как статический массив, и две функции, реализующие операции над ним. Однако усложнение программы может вызвать некоторые трудности: так, если возникнет необходимость в использовании не одного, а нескольких стеков, придется создать много массивов, а функциям push() и pop() передавать указатель на массив, хранящий элементы конкретного стека. Если усложнять задачу далее, например, до стека, основанного на динамическом списке, получится программа, имеющая примерно такую структуру:

Пример 13.2. Стек, основанный на связном списке.

struct TItem // Структура элемента списка.

{

int value; // Информационное поле

struct TItem* next;

};

// Функции работы с элементом списка

struct TItem *create_item(struct TItem **item,

struct TItem *nxt,

int num);

struct TItem *delete_item(struct TItem **item);

struct TList // Структура списка

{

struct TItem *front; // Указатель на начало списка

int size; // Число элементов списка


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: