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

17. По указанию свыше...

Сегодня мы продолжаем начатый в прошлый раз разговор об указателях. Закрепим теорию практикой. Пока мы говорили только об одном применении указателей — при написании функции, которая должна «вернуть» несколько значений. Давайте теперь напишем программку с такой функцией. Пусть эта функция занимается вводом с клавиатуры числа в заданной системе счисления. В программировании вообще часто приходится иметь дело с различными системами счисления, в основном с тремя — двоичной, восьмеричной и шестнадцатеричной; нередка и необходимость перевода из них чисел в привычную большинству простых смертных десятичную, и обратно. У некоторых «закоренелых» компьютерщиков бывают иногда и более странные привычки. Вспомните анекдот: «Чем отличается обычный человек от программиста? — Тем, что обычный человек думает, что в килобайте 1000 байт, а программист уверен, что в километре 1024 метра». Так вот, у меня был один знакомый системщик, который, встретив в повседневной жизни два числа, которые надо было сложить или перемножить, переводил их в уме в шестнадцатеричную систему счисления, там производил все необходимые действия, а затем перебрасывал результат обратно.

Ладно, шутки в сторону. Итак, давайте таки напишем эту функцию-переводчик — может, и в жизни когда пригодится. Но уж писать так писать: сделаем-ка эту функцию универсальной, способной понимать числа не только в трех упомянутых, но и вообще в любых системах. Откровенно говоря, если нужен ввод только в этих трех системах, то тогда можно обойтись и стандартной функцией scanf, но ведь наша основная задача сейчас — написать функцию с указателем в качестве аргумента. Цифры будем вводить по аналогии с принятой в шестнадцатеричной системе записью: первые десять — цифрами, а дальше буквами. Таким образом, основание системы счисления у нас ограничится числом 36 — по количеству возможных «цифр». Если кому покажется мало, такие энтузиасты могут в качестве домашнего задания переписать порожденную нами функцию так, чтобы она большие и махонькие буковки отличала друг от друга — получите количество цифр 62 и сможете запрограммировать даже шестидесятеричную систему, которая, если верить преданию, была в ходу у древних египтян.

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

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

Еще одна оговорка: в функции ввода у нас не будет проверки переполнения, так как Си вообще не предоставляет возможности такой проверки: прибавив, например, единицу к 0xffffffffL (самое большое число, которое помещается в тип unsigned long), мы получим в результате ноль — все, что вылезет за положенные четыре байта, просто сбросится. Единственная возможность реагировать на переполнение в сишной программе — при помощи ассемблерной вставки с переходом по переполнению jo или jno, но мы пока не будем лезть в дебри сисемблера (так в шутку называют написание программ на смеси Си и ассемблера). Так что мы просто скажем пользователю, каков максимум, а если он этот максимум превысит, то сам же и виноват, но об этом мы ему лучше тоже скажем. И вообще, не стоит забывать правило программиста: «Не думай, что пользователь не глупее тебя — пользователи бывают разные». То есть, защищать от дурака все, что только можно.

Новые функции, с которыми я вас обещал познакомить, находятся в уже знакомом нам заголовочном файле stdio.h и называются так: int getchar(void) int putchar(int) Чем они занимаются, можно догадаться из названий: getchar переводится как «брать символ», а putchar — как «класть символ». Работают они со стандартными потоками ввода-вывода: getchar берет из stdin (а если брать оттуда нечего — ждет, пока туда что-нибудь положат), putchar кладет в stdout. При этом getchar, если что-то взял, возвращает взятое, а если ничего взять не получилось, а получилась ошибка — возвращает символ конца файла (EOF). Этот же конец файла можно ввести и вручную (и посмотреть, как программа реагирует на ошибку ввода): в Линуксе при помощи Ctrl+D, в ДОСе —Ctrl+Z или F6. Ну а putchar что положил, то и возвращает; если вдруг случится какая-то ошибка — тоже возвращает EOF.

Обратите внимание, что тут всюду используется тип int, хотя символ — это char. В возвращаемых значениях это сделано для того, чтобы помимо символов поместился еще и EOF (который, напомню, обычно равен –1); а в аргументе putchar'а — просто для совместимости (работает она все равно только с младшим байтом).

Ну вот и познакомились. А теперь — программа:

#include /* Программа должна работать в любом случае, в каком бы регистре пользователь не ввел буквы-»цифры». Можно было бы использовать для перевода регистра toupper(int) и tolower(int), которые находятся (в зависимости от реализации) либо все в том же stdio.h, либо в отдельном заголовочном файле ctype.h... Но в некоторых старых компиляторах эти макросы не делают никаких проверок — проверять приходится уже в самой программе; во многих новых же они являются функциями — а чем мне макросы нравятся больше функций, я уже объяснял. Поэтому напишем-ка мы сами вот такой lowcase: */ #define lowcase(ch) \ ((ch)>='A'&&(ch)<='Z'?(ch)-'A'+'a':(ch)) /* (в препроцессорных директивах, как и в строках, игнорируется экранированный перевод строки) */ #define BADNUM (-2) /* Код ошибки, который будут возвращать наши функции. Так как все неотрицательные значения — «хорошие», а -1 — это EOF, то для этой ошибки взяли -2. А скобочки — это так, на всякий случай. Лучше перестраховаться. */ int putnumber(unsigned long number,char radix) /* Вывод на stdout числа number в системе счисления с основанием radix. В случае удачного завершения возвращает количество выведенных «цифр». В случае ошибки вывода возвращает EOF. Если задано недопустимое значение radix, возвращает BADNUM. Если задано нулевое значение number, не выводит ничего и возвращает 0. */ {char digit[32]; /* Сюда мы будем складывать циферки нашего числа. Так как самое большое число, которое помещается в тип unsigned long — 0xffffffff, — в самой «некомпактной» системе счисления — двоичной — состоит из 32 единиц, то для этого максимум места и оставим. А вообще, этот массив нам нужен затем, что получать цифры удобнее не в том порядке, в котором выводить, а в противоположном. */ int i,n=0; if(radix<2 || radix>36) return BADNUM; while(number>0) /* Пока остались еще циферки... */ {digit[n++]=number%radix; /* ...кладем последнюю в массив, попутно увеличивая счетчик цифр... */ number/=radix; /* ...и откусываем ее от числа. */ } for(i=n;i>=0;i--) /* Двигаясь от старшей цифры к младшей, кладем на экран очередную цифирь, а если не поклалась, уходим с криком: «EOF!»: */ if(putchar(digit[i]<10?digit[i]+'0': digit[i]-10+'a')==EOF) return EOF; /* Если мы до сих пор не вышли, значит, все нормально. */ return n; /* возвращаем количество цифр */ } int getnumber(unsigned long *number, /* вот он — указатель */ char radix) /* Ввод со stdin числа number в системе счисления с основанием radix. В случае удачного завершения возвращает количество введенных «цифр». В случае ошибки ввода возвращает EOF. Если задано недопустимое значение radix, возвращает BADNUM. Если введен недопустимый символ (не «цифра»), возвращает BADNUM (если до этого было введено что-то хорошее, то оно остается в number). */ {char digit; int n=0; if(radix<2 || radix>36) return BADNUM; *number=0; /* Пока нам вводят что-то похожее на цифры (обратите внимание: написать сразу digit=lowcase(getchar()) нельзя, так как это развернется в ((getchar())>='A'&&(getchar())<='Z'? (getchar())-'A'+'a':(getchar())), а читать три символа нам тут совершенно незачем) */ while(((digit=getchar())>='0'&&digit<='9')|| ((digit=lowcase(digit))>='a'&&digit<='z')) { /* ...переводим digit из символа в цифру... */ if((digit=digit<='9'?digit-'0':digit-'a'+10) >=radix) /* ...и, если она слишком большая... */ return BADNUM; /* ...кричим: «Плохая цифра!»... */ *number=*number*radix+digit; /* ...а если цифра хорошая — приписываем ее к числу (звездочки перед number — это указатели, а перед radix — умножение)... */ n++; /* ...и увеличиваем счетчик цифр. */ } /* Цифры кончились. Что там теперь? Если Ентер — все хорошо: */ if(digit=='\n') return n; return(digit==EOF?EOF: /* Если EOF — плохо... */ BADNUM); /* а если что-то «левое» — тоже плохо, но по-другому */ } /* Ну а теперь... */ void main() {unsigned long number; int radix; printf("В какой системе счисления будем вводить?" " (введите основание — от 2 до 36): "); scanf("%d",&radix); fflush(stdin); /* Эта функция очищает буфер заданного потока. Дело в том, что scanf не убирает за собой в буфере, и там остается Ентер. Если бы дальше в программе был опять scanf, то ему все это как-то до фени, но у нас там будет наш новорожденный getnumber, а он ентот Ентер посчитает и решит, что ему ничего не дали. Предлагаем пользователю вводить число допустимыми цифрами: */ printf("Используя \"цифры\" 0-%s%c, " "введите число от 1 до ", radix<=10?"":"9,a-", /* в итоге там, где 0-%s, будет либо 0-, либо 0-9,a-, а потом — последняя допустимая цифра: */ radix>10?radix+'a'-11:radix+'0'-1); switch(putnumber(0xffffffffL,radix)) /* Покажем максимальное число, которое можно вводить. Обработаем ошибки, если они есть: */ {case EOF: /* С помощью \r (возврат к началу строки) уничтожаем предыдущее сообщение — в случае ошибки оно нам не нужно (толпа пробелов — чтобы от него хвост не остался). */ printf("\rПростите... Ошибка вывода! " " \n"); return; case BADNUM: printf("\rУ вас же попросили число от 2 до" " 36! А это что такое? \n"); return; } /* Если ошибок не было, делаем последнее предупреждение... */ printf("\n(предупреждаю: если вы введете больше," "чем можно,я не отвечаю за результат):\n"); /* ...и берем число */ switch(getnumber(&number, /* Как и положено, передаем адрес. */ radix)) /* Снова обрабатываем ошибки. */ {case EOF: printf("\nПростите... Ошибка ввода!\n"); return; case 0: /* Ноль мы переводить не будем, тем паче что он там будет и в том случае, если пользователь вообще ничего не ввел. */ printf("Что ж это вы ничего не ввели? " "Чего зря работать заставляете?\n"); return; case BADNUM: /* Так как значение radix мы уже проверяли, то сюда мы попадем только в случае, если там ввели чего-то не того. */ printf("Э-э... а что это вы такое вводите? " "Это разве цифры?\nНу да ладно, так " "и быть, переведу, если там " "вообще будет чего переводить.\n"); } printf("Куда будем переводить? (введите основа" "ние системы счисления от 2 до 36): "); scanf("%d",&radix); printf("В %d-ичной системе счисления это число " "выглядит так: ",radix); /* Выводим число и снова обрабатываем ошибки. */ switch(putnumber(number,radix)) {case EOF: printf("\rПростите... Ошибка вывода! " " \n"); return; case BADNUM: printf("\rВас же уже два раза предупреждали, " "что основание может быть\nтолько от " "2 до 36. А вы все равно какую-то " "ерунду вводите...\n"); return; case 0: /* Когда мы брали число, остался единственный недообработанный вариант, при котором тут может быть ноль — вариант с «нехорошими цифрами» («...если там вообще будет, что переводить») */ printf("\rА переводить-то и нечего... " " \n"); return; } putchar('\n'); /* Так как наш putnumber строку за собой не переводит, сделаем это сами. */ } /* фсьо */

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

*number=*number*radix+digit;

На самом деле, если вам пока непривычно, можно для большей наглядности пользоваться пробелами:

*number = *number * radix + digit;

К слову, бытует мнение, что указатели сложны для понимания еще и потому, что обозначаются той же звездочкой, что и умножение. Далась, мол, Ричи эта звездочка — что, других значков на клавиатуре мало? Даже анекдоты ходят по этому поводу. Например: «Программистские гадания: определить, что делает си-программа, по расположению звезд в исходнике». Или: «Программизм — это душевная болезнь. И если вы с первого взгляда понимаете, что значит вот это —*a*=*b**c или ++x+++=++y++, — вы неизлечимы!»

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

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

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

18. Функция по определению и функция по объявлению.

Пока мы с вами использовали только одно описание функции — когда сначала идет имя функции со всякими аргументами, а сразу следом за ним — собственно тело функции, то есть вся ее функциональность. Такой вариант описания называется определением функции, ибо он сразу определяет, чего она делать будет. Есть еще один вариант описания функции, он зовется объявлением и выглядит вот так:

тип_функции имя_функции(аргументы_с_типами);

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

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

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

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

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

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

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

На сем с вами прощаюсь; в следующий раз мы продолжим разговор об указателях.

(Продолжение следует)