CFA LogoCFA Logo Computer
Загрузка поиска
Новости Компьютеры Прайс-лист [Новое] Прайс-лист [Б/У] Для ноутбуков Конфигуратор ПК Заказ, Оплата, Доставка Сервис объявления Драйвера Статьи Как нас найти Контакты
Новости
RSS канал новостей
Компания Hewlett-Packard выпустила в продажу ноутбук модели HP Envy x360, основой для которого послужил ...
Компания G.Skill в эти дни объявила о выпуске новых представителей серии оперативной памяти Trident ...
Список материнских плат компании Biostar пополнился свежими моделями под поколения процессоров Intel ...
Похоже, что компания Gionee в эти дни очень сильно занята. Только недавно мы сообщали об анонсе ...
Компания Enermax в своем коротеньком пресс-релизе рассказала общественности о старте серии недорогих ...
Самое интересное
Программаторы 25 SPI FLASH Адаптеры Optibay HDD Caddy Драйвера nVidia GeForce Драйвера AMD Radeon HD Игры на DVD Сравнение видеокарт Сравнение процессоров

АРХИВ СТАТЕЙ ЖУРНАЛА «МОЙ КОМПЬЮТЕР» ЗА 2003 ГОД

Язык, на котором говорят везде

Тихон ТАРНАВСКИЙ tarnav@bigmir.net

Продолжение, начало см. в МК 1-3, 5, 7, 9, 11, 14, 16, 18 (224-226, 228, 230, 232, 234, 237, 239, 241).

Сегодня мы продолжим начатый в прошлый раз разговор о массивах и плавно перейдем к еще одной очень полезной препроцессорной директиве.

У сишных массивов есть один существенный минус — их размеры (те, которые в квадратных скобках) обязаны быть константами. Это часто бывает неудобно, так как порой хочется, чтобы тот или иной массив при каждом запуске программы был разного размера. То есть, иногда бывает удобнее не задавать заранее размер массива, а этого-то как раз в сях и нельзя. Сейчас есть, правда, новый стандарт Си, отличный от Си++, где допустимы массивы «плавающих» размеров, но мы же с вами говорим о «первозданных» сях. Конечно, и в простых сях из этой ситуации есть выход, но такими выкрутасами мы займемся попозже, а пока продолжим разговор о более простых вещах.

Поэтому обсудим сейчас другие варианты, которые, может быть, и менее оптимальны, но значительно проще. Один из распространенных методов выкрутиться из этого положения — задать размер массива «с запасом», а уже потом запросить у пользователя его конкретную величину. У этого способа есть один существенный недостаток: если вы зарезервируете «на всякий случай», скажем, 10000 ячеек, а пользователь введет, к примеру, 30, в памяти все равно разместится 10000, и реально будет использовано всего 0.3% занятой памяти.

Другой, не такой мобильный и не менее простой вариант, но существенно более экономичный — задать размер массива жестко, но всего один раз, в начале программы. Тогда если возникнет желание изменить размер массива, достаточно будет просто изменить одно число в начале исходника и заново скомпилировать программу. Несмотря на свою «неинтерактивность», этот способ на практике очень удобен для программ, в которых эти размеры не очень часто надо менять, и которые не слишком велики (читай: не слишком долго компилируются); а так как мы пока больших программ не пишем, нам это вполне на руку. Способ этот в свою очередь предполагает два варианта реализации. Первый — это просто ввести для размера массива дополнительную переменную и присвоить ей значение в самом начале программы. В этом варианте часто используют специальное ключевое слово const, вот так:

Это слово означает, что переменная является на самом деле не переменной, а неизменной, то есть константой — значение такой переменной нельзя будет изменить (а почему я все же назвал ее переменной, вы сейчас поймете); имя_типа после слова const может отсутствовать — тогда подразумевается int. Целесообразность этого const кажется мне сомнительной, тем более что, как мы увидим позже, значение такой переменной все равно можно изменить, если обратиться к ней не напрямую, а через указатель.

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

15. Давайте определяться

Директива эта называется define (определять, давать определение). Простейший ее вариант выглядит вот так:

Здесь символическое_имя — это не совсем имя, вернее даже, совсем не имя, а просто любой именеподобный набор символов (в том смысле, что он не будет использоваться как имя); а значение_для_подстановки — тоже не совсем значение, а любое «что-угодно».

Действие этой директивы заключается в том, что она перед компиляцией, как и положено любой препроцессорной директиве, каждое вхождение символического_имени заменит значением_для_подстановки. Это затем, чтобы при необходимости поменять какое-то значение до компиляции программы не пришлось менять его бесконечное число раз по всей программе; хватит одного раза, после слова define. Другой вариант — можно любой трудно запоминающейся константе дать более запоминаемое обозначение.

Значения для подстановки там может вообще не быть — тогда оно считается пустым, и соответствующее символическое_имя просто устраняется из программы. Как это можно использовать, хорошо иллюстрируется все в той же «сишной библии» Кернигана-Ричи: «Так, например, любители Алгола (а также и Паскаля, которого тогда просто еще не было —прим. Т. Тарнавского) могут объявить:

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

Но все-таки и в плюсах дефайн работает (хоть это и не рекомендуется), якобы для «обратной совместимости» (хотя на практике, как мы увидим позже, полной обратной совместимости в стандарте плюсов все равно не получается, тоже из излишней осторожности). И хорошо, что работает, ибо, как мы скоро увидим, эта директива может замещать не только константы, но и простенькие функции. Но обо всем по порядку.

Тут надо сделать еще одну оговорку. Дело в том, что в некоторых совсем старых компиляторах директива define при замене почти ничего не анализирует, разве что не портит вхождения внутри кавычек, а все остальное меняет не глядя. Таким образом, если вы, например, напишете

то заменится не только символическое имя f, но и любая буква f внутри любой лексемы, в том числе каждый if у вас превратится в i50, а каждый for — в 50or. Чтобы избежать таких казусов, в сях принято все дефиниции писать заглавными буквами (возможно, еще одна из причин, по которым define не рекомендуется в плюсах, — это принятые в ООП имена в стиле ВсеСБольшойБуквы).

Но есть еще один нюанс. Например, если написать так:

то когда в программе встретится F1, первый define найдет и заменит входящий в него F, и получится 501. В результате второму define’у ничего не останется.

О панацее от всех бед, как в предыдущем случае («использовать в дефайнах большие буквы, а везде — маленькие») тут, увы, не может быть речи, нужна просто упомянутая толика внимания; например, в приведенном выше кусочке кода достаточно поменять местами эти две строчки:

Тогда тот дефайн, который с F1, успеет заменить все что надо, пока ему не нагадят, и все будет нормально. Отсюда и более общий рецепт: если надо заменить несколько псевдоимен, одни из которых являются составными частями других, следует писать их «по убыванию», начиная с того, которое включает в себя все остальные, и заканчивая тем, которое не включает ни одного. Например:

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

В общем, смысл, надеюсь, понятен. Основная трудность здесь состоит в том, что директива эта препроцессорная, выполняется до компиляции, а сообщения об ошибках выдаются компилятором и линкером, то есть появляются уже во время и после компиляции. То есть, дефайн позаменяет все, как ему и сказали, а потом уже компилятор начнет ругаться на строку, в которой, на первый взгляд, ошибки никакой и в помине нет (ведь вы же, в отличие от компилятора, видите в исходнике незамененный вариант). Иначе говоря, если использовать, скажем, первый из приведенных выше вариантов (тот, который с маленькой буквой f), то компилятор выдаст ошибку в первой же строке с такой буквой, хотя сама строка отобразится в первозданном виде. А если, к примеру, он (компилятор) выдаст вам Statement missing ; («пропущено ;» — именно это он скажет, если увидит букву сразу после цифры) и установит курсор на слово for — вы, пожалуй, долго будете думать... А если следовать моим советам и следить за порядком директив, все будет «олл-райт, Кристофор Бонифатич».

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

Любое определенное с помощью #define символическое имя можно «отменить» директивой #undef:

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

А теперь о втором воплощении дефайна, о котором я сказал, что дефайном можно замещать маленькие функции. То, что мы сейчас увидим, часто называют «макрос с аргументами», как бы подчеркивая тем самым, что это хотя и не функция (а макрос), все-таки ведет себя в чем-то как функция — то есть принимает какие-то аргументы. Выглядит это так:

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

Тогда фрагмент z=abs(x+y); заменится на z=((x+y)>=0?(x+y):-(x+y));.

Эти скобочные нагромождения здесь нужны затем, чтобы побороть приоритеты любых операций, которые могут встретиться в аргументах. Например, здесь могло встретиться не abs(x+y), а t+abs(x|y) (пожалуй, диковато, но кто знает, как кому взбредет в голову использовать ранее определенный макрос — лучше застраховаться от любых вариантов). Тогда, если бы было написано без скобок (вот так: a>=0?a:-a), то после замены получилось бы t+x|y>=0?x|y:-x|y, что, учитывая приоритеты всех использованных здесь операций, трактовалось бы как (t+x)|(y>=0) ? x|y : (-x)|y. Не совсем то, что бы вы хотели?.. Тем более, что, напомню, вы замененного кода не видите, а по результату его исполнения не всегда поймешь, где она, собака такая, зарыта... Так что о скобках в таких случаях не забывайте; не бойтесь, на самом деле правило их расстановки совсем нехитрое, оно следует из простой логики: во-первых, нужно брать в скобки все выражение, и во-вторых, каждый входящий в него аргумент.

Так что, как видите, макросы ведут себя иначе, чем функции; в этом есть и плюсы, и минусы. Начнем с плохого. После такой замены компилятору придется вычислять каждый аргумент столько раз, сколько он упоминается в выражении (хотя, например, в нашем макросе модуля аргумент вычислится не три, а два раза — ведь из последних двух вхождений вычислено будет только Таблицато, которое надо вернуть). А если вместо него (макроса) использовать функцию, то аргумент посчитается только один раз, перед передачей его значения этой функции (вот только насколько тяжеловесным должен быть аргумент, чтобы компенсировать этим время, потраченное на вызов самой функции?) Теперь о хорошем. Первое, о чем уже вскользь упомянуто, — время на выполнение и «бинарный объем» самой функции, ее оболочки — макрос ведь разворачивается прямо в исходнике, с точки зрения компилятора это то же самое, как если бы вы каждый раз писали полностью упомянутое выражение_с_аргументами (правда, если макрос слишком тяжел и слишком часто используется, его скомпилированные копии могут превысить по весу «функциональную оболочку», но разница в скорости выполнения — уже в пользу макроса — в этом случае тоже будет расти лавинообразно). И второе (это более важно на практике): у любой функции жестко определен тип ее аргументов, а макрос от типов не зависит вообще — приведенный выше модуль можно с одинаковым успехом брать и от любых целочисленных, и от любых «плавающе-точечных» аргументов.

Так что в целом целесообразность использования «аргументированных» макросов зависит от того, как именно надо их применять: если для многих простеньких разнотипных выражений, то, конечно, в них есть смысл; а если для нескольких сложных конструкций одного и того же типа — очевидно, нет.

Опять же, в плюсах не рекомендуют использовать макросы, а советуют все это заменять функциями. Там даже удобное нововведение сделали: если есть несколько разных функций с одинаковыми именами, но разными типами аргументов, то компилируются все эти функции, а какую где вызывать, компилятор решает по типам аргументов в месте вызова (в других случаях довольно полезное качество). Так вот, там рекомендовано писать в таком случае несколько одноименных функций для разных типов (есть еще, правда, шаблоны функций, но на практике получается то же самое). А теперь представьте себе, что вам приспичило в одной программе взять модуль от int-, long-, double- и long-double аргументов — это ж целые четыре разные функции получатся, вместо одного макроса! Вот и думайте, вредны макросы или все-таки полезны.

Дам несколько нужных в жизни примеров макросов:

Последний макрос так и просит показать, как это все работает... показываю (так как ^ — побитная операция, то и показывать я буду на примере отдельных битиков; см. Таблицу).

Как видите, результат налицо. А так как все данные в памяти машины хранятся побитно, то отсюда следует, что такой вариант будет работать для любых типов (конечно, обе переменных должны быть одного типа). По сравнению с традиционным вариантом с использованием дополнительной переменной (t=a;a=b;b=t;) плюсы очевидны: работает быстрее, памяти на эту дополнительную переменную не требуется, а главное — типовая независимость.

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

Кстати сказать, название «макросы с аргументами», как может показаться, не совсем корректно. Дело в том, что такой макрос, как и функция, может и не принимать вообще никаких аргументов. Например, стандартный, определенный в уже слегка знакомой нам библиотеке stdio.h, макрос getchar(), читающий символ из потока стандартного ввода, выглядит так:

То есть, он просто вызывает библиотечную функцию getc(), читающую символ из заданного потока, и насильно задает ей stdin. Вообще-то, здесь скобок после getchar теоретически могло бы и не быть (можно сказать, что они там есть только для того, чтобы все было «традиционно»: везде со скобочками — так пусть и тут будет со скобочками). Можно было бы утверждать, что это не макрос, а обычная дефиниция, но во всех доках getchar() все-таки называют исключительно «макросом».

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

16. Раз, два, три, четыре, пять, начинаем выбирать

(компилять — не компилять)

В сях сам исходник может содержать указания на то, что некоторые его части нужно компилировать только при определенных условиях. Зачем? Да мало ли зачем. Например, в заголовочных файлах — для того чтобы не компилировать два раза весь такой файл, если он был два раза присоединен (может быть, по ошибке, а может быть и нет: многие заголовочные файлы по своей инициативе присоединяют другие, которые им нужны, и если эти другие понадобятся нескольким из присоединенных вами… для этого там и стоит такая проверка). Или другой вариант: некоторые вещи в плюсах пишутся иначе, чем в обычных сях. Поэтому большинство плюсовых компиляторов при компиляции в плюсовом режиме (т.к. многие из них имеют и отдельный режим для компиляции программ на «простых» сях) задают специальное символическое имя (обычно _cplusplus), по наличию которого можно определить, что мы сейчас компиляемся по-плюсовому. Осталось только научиться определять само это наличие. А именно этим и занимаются директивы, о которых сейчас пойдет речь. Директивы эти используются в совокупности, каждая из них чем-то напоминает один из кусочков уже знакомого нам оператора ветвления. Так что и рассмотрим их сейчас все вместе.

Встретив одну из первых двух директив, препроцессор проверяет, определено ли символическое_имя, и, в случае с #ifdef отдает компилятору последующие строки, только если оно определено, в случае же с #ifndef — наоборот, только если не определено. Так он поступает со всеми строками до тех пор, пока не встретится #else или #endif. Собственно, #ifdef и #ifndef — это сокращения от аглийских фраз if defined («если определено») и if not defined («если не определено»). Инструкция #else, как и ее аналог в упомянутом операторе ветвления, переводится как «иначе» и действует аналогично (в частности, директива эта тоже не обязательна). Ну а #endif в переводе означает «конец «если»». Кстати, блоки директив #if...#else...#endif, как и их операторные тезки, могут быть вложенными — таким образом, на определенность-неопределенность директив тоже можно налагать сколь угодно сложные условия.

Примерчик. Самый что ни на есть жизненный будет примерчик. А именно, поговорим-ка мы сейчас об отладке — и без того пора бы уже о ней поговорить, тем более что, как гласит народная мудрость, «если программа больше чем из двух десятков строк с первого раза написалась без ошибок, значит, вы чего-то не заметили».

При отладке любой программы может пригодиться какой-нибудь кусок кода, который в готовом проекте не нужен: например, вывод на экран значений промежуточных переменных. Так вот, для того чтобы такие дополнительные куски кода каждый раз не убирать и заново не вставлять, когда вылезла очередная бага (а кто сказал, что сейчас вылезла последняя?), придумали их (куски) вставлять в «скобки» #ifdef...#endif. В этом случае для отладки надо просто задать какое-нибудь символическое имя, а чтобы посмотреть на программу в «окончательном» варианте — закомментировать эту дефиницию. Это символическое имя принято называть DEBUG (англ. отладка). К примеру, есть у вас в программке поиск элемента массива по каким-нибудь условиям, и для отладки вам понадобилось узнать номер найденного элемента. Пишем что-то вот такое:

Тогда, если DEBUG определено, препроцессор передаст компилятору этот printf в таком виде:

а если не определено, то вот в таком:

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

Стоит сказать сейчас еще об одной директиве, которую есть смысл использовать только в сочетании с условными инструкциями. Это директива #error. Наткнувшись на нее, препроцессор спотыкается, как об настоящую ошибку; в виде сообщения об ошибке выдается текст, написанный сразу после этой директивы.

Например, многие досовские компиляторы тех времен, когда уже существовали Win 3.1-95, автоматически определяют специальное символическое имя, если задать им какую-нибудь из опций выдачи виндового бинарника. Если вашу программу нельзя компилять под винду (или наоборот, можно только под винду), можете воспользоваться этой директивой, например:

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

Рекомендуем ещё прочитать:






Данную страницу никто не комментировал. Вы можете стать первым.

Ваше имя:
Ваша почта:

RSS
Комментарий:
Введите символы: *
captcha
Обновить





Хостинг на серверах в Украине, США и Германии. © www.sector.biz.ua 2006-2015 design by Vadim Popov