Продолжение, начало см. в МК №№ 1-3, 5, 7, 9, 11, 14, 16, 18, 20, 22, 24 (224-226, 228, 230, 232, 234, 237, 239, 241, 243, 245, 247)
Работа над ошибками
Для начала должен принести свои извинения всем читателям. Дело в том, что в предыдущих своих статьях я допустил несколько неточностей, которые я считаю своим долгом сейчас исправить. Если быть точным, таких неточностей было две. Начну со второй как с более серьезной. В статье в №18 (241) в примере был вот такой кусочек:
`char digit(char pos,unsigned num,char rad) /* pos (от "position") — номер "цифры", которую надо вернуть (считая справа налево, самая правая — под номером ноль); num (от "number" — "число") — число, из которого эту "цифру" надо выдергивать; rad (от "radix" — "основание системы счисления") — то, что оно значит / {char i; / счетчик цикла / unsigned _=num; / копируем сюда переданное число, чтобы дальше с ним творить что хотим */ for(i=0;iимя-файла — тогда, если такой файл уже есть, он перезапишется поверх, или имя-вашей-программы >>имя-файла — тогда вывод допишется в конец существующего файла), только лучше не делайте так с программами, которые у вас чего-нибудь спрашивают, а то и за вопросами придется в этот файл лазить. Либо же, опять-таки, можно передать его какой-нибудь другой программе на стандартный ввод (к примеру, чтобы начало этого вывода не убегало выше монитора, можете передать его программке more, которая будет каждый раз держать его за хвост, пока вы на очередной кусочек не налюбуетесь и не нажмете ей на кнопку имя-вашей-программы |more (в Линуксе удобнее вместо more использовать less). Ну и, конечно, вы можете оба эти варианта комбинировать, например, писать: имя-вашей-программы <отсюда-читать >а-сюда-писать.
Ну а теперь перейдем к нашему строчно-массивному примеру. Для ввода строк в нем можно было бы использовать уже такую родную нам функцию scanf(), она может строку и на слова пробелами порешить. Но, во-первых, ни на что, кроме пробельных символов, она не реагирует, то бишь всякие там цифири, препинаки и прочие плюсы со звездочками воспринимает за полноценные буквы. А во-вторых, дочитав до первого пробела, она спокойно пойдет спать, и на все попытки разузнать, а что же было дальше, будет молчать как партизан. Посему мы пойдем другим путем (особо пространные комментарии превращены в абзацы основного текста —прим. литред.):
#include /* Наш старый знакомый. */ #include /* А здесь лежат всякие разные функции для работы со строками; в этом примере мы одной из них воспользуемся. */
Сейчас мы определим проверку символа на то, а не буква ли он. Тут можно бы вспомнить уже когда-то упомянутый мною файл ctype.h, и, в частности, функцию (или макрос) isalpha(). Но в вин/досовских компиляторах эта функция определяет исключительно английские буквы. В Линуксе ее результат хотя и зависит от текущей локали (локаль — это, грубо говоря, настройки, говорящие системе, на каком языке с вами разговаривать), но по крайней мере в локали ru_RU я взаимности от isalpha() так и не добился. Посему пишем сами. А дабы не особо пока напрягаться с кодировками, согласимся, не мудрствуя лукаво, на всю нижнюю (не-ascii) половину таблицы, тем паче что в ней ничего кроме русских букв с клавиатуры и не введешь. Правда, если вы дадите на ввод, к примеру, файл с псевдографикой, то ее программа тоже примет за самые настоящие буквы. Но тут уж придется пока смириться с издержками производства, или же писать отдельную версию под каждую систему (а в Линуксе — и под каждую русскую локаль), ибо заводиться с кодировками — это для нас пока слишком сложно.
#define isletter(c) ( ((c)>='A' && (c)<='Z') || \ ((c)>='a' && (c)<='z') || ((c) & 0x80) )
Последнее условие ((c) & 0x80) означает, что в c установлен (равен единице) старший бит, то есть c лежит в нижней половине таблицы. В данном конкретном случае это равносильно условию (c)>=0x80, но в других случаях (с «нестаршим» битом) «больше-меньший» вариант запишется уже через два условия (как здесь с буквами).
void main() {unsigned char str[200], /* Будем брать по одной строке и складывать сюда. */ longest[40], /* Здесь будет храниться самое длинное (на данный момент) слово; */ maxlen, /* здесь — длина этого самого длинного слова, */ len, /* а тут — длина текущего слова. */ _; /* Это будет номер текущего символа в строке, */ unsigned words; /* а это — счетчик слов в тексте. */
Здесь мы уже сделали несколько предположений, а именно: что вводимые строки будут не длиннее двухсот символов, что длины слов в этих строках не будут превышать сорока, и что всего этих слов в тексте будет не больше, чем 65535. В серьезной программе так, конечно, делать нельзя, и со временем мы будем во всех таких «узких местах» вводить дополнительные проверки.
double total;
Сюда будем складывать (в смысле, суммировать) длины всех слов в тексте, чтобы потом, поделив на количество слов, получить «статистику» — среднюю длину слова в тексте. Тип double для хранения целочисленных значений выбран неслучайно: во-первых, в double помещается большее целое число, чем даже в unsigned long. А во-вторых, при переполнении любого целого типа «сбросится» старшая цифра (потому как не влезет); дробный же тип в таком случае будет хранить все старшие разряды и терять «точность», то есть младшие цифры, которые нам для вычисления средней длины слова совсем не важны.
total=maxlen=words=0; /* Обнуляем total и words, чтобы потом в них суммировать, и maxlen, чтобы она была точно меньше, чем длина любого слова, которое нам встретится. */ puts("Давайте мне слова, а я их буду считать " "и мерить.\nЕсли вы будете вводить текст " "кнопками, то, когда вам надоест,\n" "нажмите Ctrl+D, если вы в Линуксе, или " "Ctrl+Z, если в Досе/Винде.\nПоехали..."); /* Функция puts() кладет заданную строку на stdout. В отличие от printf(), завершает вывод переводом строки. */ while(gets(str))
Функция (gets()) принимает указатель на строку и читает в нее одну строчку со stdin (стандартного ввода). Когда мы пишем просто имя массива, без скобочек с индексом, подставляется адрес этого массива, что здесь и требуется. Возвращает функция gets() целое значение: ненулевое, если что-то ввелось, и нулевое, если случилась какая-нибудь ошибка или конец файла. Если пользователь будет вводить текст с клавиатуры, то конец «файла» он может устроить, нажав на соответственную «красную кнопку», о чем мы его заранее и предупреждаем. Вообще-то функцией gets() лучше не пользоваться, так как в ней есть один очень существенный «прокол»: она не делает проверку на переполнение массива, в который читается строка; а в серьезных программах ошибка переполнения буфера — любимая лазейка для хакеров. В линуксовой доке к этой функции написано (и правильно написано) «НИКОГДА не используйте gets()» (надо заметить, в доках к виндовым/досовским компиляторам на этой опасности вообще не акцентируется внимание). Но так как эта программа чисто демонстративная, то в ней я позволил себе такую вольность, заодно обратив ваше внимание на этот минус. В дальнейшем мы будем пользоваться более цивилизованными методами, да и в целом не будем пренебрегать никакими проверками.
{_=0; /* Обнуляем номер символа, который (символ) мы будем в этом проходе цикла проверять. */ do /* Запускаем еще один цикл — по символу внутри строки. */ {while(str[_]&&!isletter(str[_])) _++;
Пока «не-буква», идем дальше. Условие str[_] (...не равно нулю) нужно для того, чтобы не проскочить конец строки (который, как вы помните, в Сях обозначается символом с кодом 0), если он вдруг появится раньше, чем буква. Заметьте, тут нельзя было написать isletter(str[_++]), потому как isletter() — макрос, и после того как он развернется этот аргумент вместе с инкрементом будет там стоять аж пять раз, и сам инкремент, таким образом, может выполниться от одного до тех же пяти раз, в зависимости от истинности входящих в макрос условий (если вы помните, у нас уже была подобная ситуация, только там внутри макроса нельзя было писать getchar()).
if(!str[_]) break; /* А вот здесь мы на этот конец строки и среагируем, то есть покинем этот цикл и пойдем читать следующую строку. */ len=0; /* Обнуляем длину слова. */ while(isletter(str[_])) {len++;_++;} /* Так как все "не буквы" мы уже проскочили, то теперь у нас на очереди буквы. Пока там буквы, идем дальше, попутно увеличивая длину слова. */ words++;total+=(double)len;
Все, буквы закончились — значит, теперь у нас есть слово. Соответственно, увеличиваем счетчик слов и суммарную длину всех слов. Насчет (double)len: сколько я ни говорил про автоматическое приведение типов, но вот такое приведение (от целого типа к дробному) многие компиляторы делать не хотят, потому как целые и дробные числа хранятся в памяти совсем по-разному, а если сделать явное приведение, тогда компилятор согласится преобразовывать эти формы хранения.
if(len>maxlen) /* Если это слово длиннее самого длинного,... */ {maxlen=len; /* ...то теперь оно у нас будет самое длинное: сохраняем его длину,... */ strncpy(longest,&(str[_-len]),len); /*...и копируем само слово. */
strncpy() — это как раз и есть та функция для работы со строками, которой я обещал воспользоваться. Она копирует заданное количество символов из начала одной заданной строки в другую заданную строку. Первый ее аргумент — адрес строки-преемника, второй — адрес строки-источника, а третий — количество нужных символов. Но нам надо было скопировать символы не из начала строки, а начиная с len символов тому назад. Так как мы пока работаем со строкой как с массивом, то мы для этого воспользовались адресной операцией — когда мы вернемся к теме указателей, там это можно будет записать по-другому.
longest[len]=0;
Так как strncpy() только копирует заданное количество символов и ничего не добавляет от себя, теперь в ней лежит только нужное слово, а должно лежать и кое-что еще — значит, нам надо добавить к полученной строке завершающий ноль.
} /* Вот и все, что нам надо было сделать с новым кандидатом на звание самого длинного слова… */ }while(str[_]); /* ...и даже все, что надо было сделать вообще с этим словом. Посему, если строка не закончилась — переходим к следующему слову, а если закончилась... */ } /* ...то к следующей строке, и так до конца файла. */ /* Теперь, когда весь ввод закончился, мы можем... */ printf("Максимальная длина слова: %u\n",maxlen); /* ...вывести длину самого длинного слова... */ if(maxlen) /* ...и, если она не ноль, то бишь, если там вообще были слова... */ printf("Первое слово такой длины: %s\n",longest); /* ...то вывести и само самое длинное слово (вернее, первое из слов такой длины, если их было несколько). */
Эта проверка (в предыдущей строке) тут необходима, так как, если слов не было вообще, то в массив longest ни разу ничего не скопируется, и, соответственно, в нем будет лежать то, что лежало изначально, то есть мусор; а мусор нам на экране совсем не нужен.
printf("Всего слов: %u\nСредняя длина слова: " "%lf\n",words,words?total/words:0); /* И последнее: выводим общее количество слов и среднюю длину слова. Опять же проверка, дабы, если слов совсем не было, не появилась ошибка деления на ноль. */ }
Вот так это все работает. На мой взгляд, то, что строка не реализована как отдельный тип, а является массивом символов, очень удобно в работе. Тем более, что с этими массивами можно работать еще и как с указателями (мы, кстати, в этом примере уже так с ними работали: ведь примененные нами функции —gets() и strncpy() — принимают в качестве аргументов именно указатели). А те действия со строками, которые в других языках, в которых строка — тип, реализованы как операции, в Сях тоже никуда не делись и представлены в виде библиотечных функций, довольно солидный набор которых лежит в теперь уже знакомом нам string.h. Единственный небольшой минус такой реализации — чуть менее красивый синтаксис «чисто функционального» варианта работы со строками — полностью упразднен в плюсах, за счет введения классов и перегрузки операций: там вы можете задать любому символу операции какое-нибудь действие на свой вкус — например, назначить операции +, примененной к строкам, соединение (которое в книжках по программированию для большей корявости называют «конкатенацией») этих строк. Но пока все-таки вернемся к чистым Сям.
Сейчас я хочу вернуться еще к одной из моих предыдущих статей, а именно к статье в №14 (237). Там в примере на оператор switch был вот такой кусочек:
printf((num?"нет, это не":"да, это") " ноль, потому что ");
Когда я скормил этот пример линуксовому gcc, он эту строчку кушать не захотел. Почему — не очень-то понятно, ибо аргументы функций должны вычисляться до их передачи самим функциям. Все же, ознакомившись с этим симптомом, в дальнейшем я решил избегать подобных конструкций. И в сегодняшнем примере написал вот так:
printf("Максимальная длина слова: %u\n",maxlen); if(maxlen) printf("Первое слово такой длины: %s\n",longest); printf("Всего слов: %u\nСредняя длина слова: " "%lf\n",words,words?total/words:0);
Вместо того, чтобы писать так:
printf("Максимальная длина слова: %u\n"(maxlen? "Первое слово такой длины: %s\n":"%s") "Всего слов: %u\nСредняя длина слова: " "%lf\n",maxlen,maxlen?longest:"",words, words?total/words:0);
Правда, все вышесказанное (насчет gcc) относится к gcc 2.95, именно на нем я проверял эти программы. Возможно, в gcc 3.x ситуация изменилась; сейчас у меня нет возможности это проверить. Но даже если это так, gcc 2.x все еще довольно-таки распространен, а кроме того никто ведь не гарантирует, что точно так же себя не поведет еще какой-нибудь, совсем другой компилятор.
На сегодня все, а в следующий раз, как я и обещал, речь пойдет о связи массивов с указателями.
(Продолжение следует)
