Язык Си - Уэйт, Прата, Мартин. M. уэит с. Прата д. Мартин
Скачать 4.69 Mb.
|
х в диапазоне - 1 <= х < 1. (Мы должны превратить его в тип float, чтобы иметь десятичные дроби.) 2. Добавим 1. Наше новое число удовлетворяет отношению 0 < = х < 2. 3. Разделим на 2. Теперь имеем 0 <= х < 1. 4. Умножим на 6. Имеем 0 <= х < 6. (Близко к тому, что нужно, но 0 не является возможным 206 значением.) 5. Прибавим 1: 1 <= х < 7. (Заметим, что эти числа все еще являются десятичными дробями.) 6. Преобразуем в целые числа. Теперь мы имеем целые в диапазоне от 1 до 6. 7. Для обобщения достаточно заменить значение 6 в п. 4 на число сторон. Вот функция, которая выполняет эти действия: /* электронное бросание костей */ #define SCALE 32768.0 rollem(sides) float sides; { float roll; roll = ((float)rand( )/SCALE + 1.0) * sides/2.0 + 1.0; return((int)roll); } Мы включили в программу два явных описания типа, чтобы показать, где выполняются преобразования типов. Обратимся к программе, которая использует эти средства: /* многократное бросание кости */ main( ) { int dice, count, roll, seed; float sides; printf(" Введите, пожалуйста, значение зерна. \n"); scanf(" %d, &seed); srand(seed); printf(" Введите число сторон кости, 0 для завершения. \n"); scanf(" %d", &sides); while(sides > 0) { printf(" Сколько костей?\n"); scanf(" %d", &dice); for( roll = 0, count = 1; count <= dice; count++) roll + = rollem(sides); /* бросание всех костей набора */ printf(" У вас выпало %d, для %d %.0f-сторонних костей.\n", roll, dice, sides); printf(" Сколько сторон? Введите 0 для завершения.\n"); scanf(" %f", &sides); } printf(" Удачи вам!\n"); } Теперь давайте используем эту программу: Введите значение зерна 1 Введите число сторон кости, 0 для завершения. 6 Сколько костей? 2 У вас выпало 4 для 2 6-сторонних костей. Сколько сторон ? Введите 0 для завершения. 6 Сколько костей ? 2 У вас выпало 7 для 2 6-сторонних костей. Сколько сторон? Введите 0 для завершения. 0 Удачи Вам! Спасибо. Вы можете использовать функцию rollem( ) по-разному. Пусть число сторон (sides) равно двум, тогда бросание) монеты даст следующий результат: "орел" выпал 2 раза, a "peшка" - один (или наоборот, смотря, что вы предпочитаете). Можно легко модифицировать программу, чтобы показать как отдельные результаты, так и итог. Или вы можете построить имитатор игры "крапс". Если вам нужно большое число бросаний, вы можете легко модифипировать свою программу и 207 получить результат, подобный следующему: Введите значение зерна. 10 Введите количество ходов; введите 0 для завершения. 18 Сколько сторон и сколько костей? 6 3 Здесь 18 ходов для 3 6-сторонних костей. 7 5 9 7 12 10 7 12 10 14 9 8 13 9 10 7 16 10 Сколько ходов? Введите 0 для завершения. 0 Использование функции rand( ) [но не rоllem( )] изменило бы вашу программу угадывания чисел: компьютер стал бы выбирать, а вы угадывать, вместо того чтобы сделать наоборот. Разработаем еще некоторые функции. Сначала мы хотим создать функцию, которая читает целые числа. ФУНКЦИЯ ПОЛУЧЕНИЯ ЦЕЛЫХ ЧИСЕЛ: getint( ) Далее Содержание Вероятно, наш проект покажется вам очень простым. В конце концов мы можем использовать функцию scanf( ) с форматом %d, если хотим прочитать целое число. Такой подход очень прост, но он имеет один большой недостаток. Если вы ошибочно напечатали, скажем Т вместо 6, scanf( ) попытается интерпретировать Т как целое число. Мы хотим создать функцию, которая проверяет вводимую информацию и предупреждает вас, если введено нецелое число. Теперь, может быть, наш проект уже не кажется таким простым. Однако не беспокойтесь - мы хорошо начали: у нас есть имя для новой функции. Мы назовем ее getint( ). План Далее Содержание К счастью, мы уже выработали стратегию. Во-первых, заметим, что любую вводимую информацию можно читать как строку символов. Целое число 324, например, можно прочитать как строку из трех символов: символ '3', символ '2' и символ '4'. Это подсказывает нам следующий план: 1. Прочитать вводимую информацию как символьную строку. 2. Проверить, состоит ли строка только из символов цифр и стоит ли перед ними знак плюс или минус. 3. Если все это имеет место, превратить символьную строку в правильное числовое значение. 4. Если нет, выдать предупреждение. Этот план так хорош, что он должен работать. (Тот факт, что он представляет собой стандартный подход, существовавший на протяжении многих лет, придает нам также некоторую уверенность в возможности его выполнения. Но, прежде чем начать программировать, нужно подумать, что будет делать наша функция. В частности, до того как мы займемся содержанием нашей функции getint( ), нужно точно решить, как она должна взаимодействовать со своим окружением: с какой информацией? Какую информацию она должна получать от вызывающей программы? Какую информацию должна возвращать? В каком виде должна быть эта информация? Снова мы рассматриваем функцию как черный ящик. Мы хотим знать, что входит в функцию и что выходит из нее и, наконец, что находится внутри ее. Этот подход помогает обеспечивать более однородное взаимодействие между различными частями программы. Иначе вы можете оказаться в положении человека, пытающегося установить трансмиссию автомашины "Волво" в автомобиль "Тойота". Сама функция правильная, но интерфейс является проблемой. 208 Поток информации для getint( ) Далее Содержание Какой выход должна иметь наша функция? Во-первых, несомненно, что она должна была бы выдавать значение прочитанного числа. Конечно, функция scanf( ) уже делает так. Во-вторых, и это очень существенно, мы собираемся создать такую функцию, которая будет выдавать сообщения о состоянии, т. е. найдено или нет целое число. Чтобы функция была действительно полезной, она должна также сообщать о нахождении ею символа EOF. Тогда мы могли бы использовать функцию getint( ) в цикле while, который читает целые числа до тех пор, пока не обнаружит символ EOF. Короче говоря, нам нужно, чтобы getint( ) возвращала два значения: целое числе и состояние. Так как мы хотим иметь два значения, то с одной функцией return с этой задачей нам не справиться. Нам следует иметь два указателя. Однако полное решение задачи мы получим, если используем указатели для выполнения основной работы функции и функцию return для выдачи некоторого кода состояния. Именно это и делает функция scanf( ). Она возвращает количество символов, которые нашла, и символ EOF, если встречает его. Ранее мы не пользовались такой возможностью, но могли бы, если бы применяли вызов вида status = scanf(" % d", &number); Теперь будем это делать. Тогда наш вызов функции выглядел бы следующим образом: status = getint(&number); Правая часть равенства использует адрес переменной number, чтобы получить ее значение, a return применяется для получения значения переменной status. РИС. 10.2. Создание функции getint( ) Мы должны выбрать коды для выдачи сообщения о состоянии. Так как предполагается, что неописанные функции имеют тип int, наши коды должны состоять из целых чисел. Используем следующие коды для сообщения о состоянии: -1 означает, что найден символ EOF. 1 означает, что найдена строка, содержащая не цифры. 0 означает, что найдена строка, содержащая только цифры. Нашу функцию getint( ) можно представить себе (рис. 10.2) как имеющую один вход и два выхода. На ее вход поступает адрес целой переменной, значение которой считывается. На первом выходе имеем значение считанного целого, полученного через указатель. (Таким образом, аргумент-указатель является двусторонним каналом передачи информации.) На втором выходе получаем код состояния, что обеспечивается функцией return. Отсюда следует, что "скелет" нашей функции должен выглядеть примерно так: getint(ptint) int *ptint; /* указатель на целое число */ { int status; return (status); } 209 Замечательно! Теперь мы должны просто заполнить внутренность функции. Содержание getint( ) Далее Содержание Наш основной план для getint( ) в общих чертах на псевдокоде выглядит примерно так: читаем на входе информацию в виде символов помещаем символы в строку, пока не встретим символ EOF если встретился символ EOF, устанавливаем состояние в STOP в противном случае проверяем строку, преобразуем символы в целое число, если возможно, и выдаем сообщение о состоянии (YESNUM или NONUM). Здесь мы используем STOP, YESNUM и NONUM как символические константы, равные -1, 0 и 1, как описано выше. Рассмотрим еще некоторые вопросы. Как функция будет решать, что она достигла конца входной строки? Должны ли мы ограничивать длину строки? Мы вошли в область, где нам предстоит решать, что предпочесть: удобство программиста или удобство пользователя. Самым простым было бы предложить пользователю нажимать на клавишу [ввод], когда строку надо закончить. Это означало бы один ввод на строку. Для пользователя все же было бы приятнее, если бы функция могла размещать несколько чисел в одной и той же строке: 2 34 4542 2 98 Мы решили предоставить привилегию пользователю. Пусть функция будет считать, что строка начинается с символа, не являющегося пробелом или "новой строкой", и заканчивается символом пробела или "новой строкой". Такой ввод может производиться в одну строку или в несколько строк. Мы ограничим вводимую строку 80 символами. Так как строки заканчиваются нуль-символом, нам нужен массив из 81 символа для включения в него этого символа. Это слишком щедро, потому что нам нужно только 6 символов для 16-разрядного целого числа и знака. Вы можете вводить более длинные числа, но их размер будет сокращен до размера строки. Чтобы сделать программу более модульной, мы поручим преобразование "строка в целое число" другой функции, и назовем ее stoi( ). У нас будет также возврат функцией stoi( ) соответствующего кода состояния в функцию getint( ), a getint( ), в свою очередь, может передать код состояния своей вызывающей программе. Функция stoi( ) выполнит последние две строки нашего плана (на псевдокоде). Рис. 10.3 представляет программу для функции getint( );. Функция stoi( ) будет показана позже: /* getint( ) */ #include #define LEN 81 /* максимальная длина строки */ #define STOP-1 /* коды состояний */ #define NONUM 1 #define YESNUM О getint(ptint) int *ptint; /* указатель на вывод целого числа */ { char intarr[LEN]; /* запоминание вводимой строки */ int ch; int ind = 0; /* индекс массива */ 210 while((ch = getchar( )) = = '\n' || ch == ' ' || ch == '\t'); /* обход начальных символов "новая строка", пробелов и табуляций */ while(ch != EOF && ch != '\n' && ch != ' ' && ind < LEM) { intarr[ind++] = ch; /* запись символа в массив */ ch = getchar( ); /* получение очередного символа */ } intarr[ind] = '\0'; /* конец массива по нуль-символу */ if(ch == EOF) return(STOP); else return(stoi(intarr, ptint) ); /* выполнение преобразования */ } РИС. 10.3. Программа функции getint( ) Мы получаем символ сh. Если он является символом пробела, или "новой строки", или табуляции, мы берем следующий символ и так продолжаем до тех пор, пока не получим символ, отличающийся от перечисленных. Затем, если этот символ не EOF, помещаем его в массив. Продолжаем брать символы и помещать их в массив, пока не найдем запрещенный символ или не достигнем предельного размера строки. Далее помещаем нуль-символ (' \0') в следующую позицию массива, чтобы отметить конец строки. Таким образом, мы создали массив в виде стандартной символьной строки. Если EOF был последним прочитанным символом, возвращаем STOP; иначе идем дальше и пытаемся преобразовать строку. Мы вызываем новую функцию stoi( ), чтобы выполнить эту работу. Что делает stoi( )? При вводе она берет символьную строку и указатель на целую переменную, использует указатель для присваивания значения самой переменной, а также return для пересылки сообщения о состоянии, которое getint( ) передает затем функции getarray( ). Поразительно! Двойная игра! Вот менее компактный способ использования функции stoi( ): status = stoi(intarr, print); return (status); Здесь status была бы переменной типа int. Первый оператор дает значение, на которое указывает ptint; она также присваивает значение переменной status. Второй оператор возвращает это значение программе, которая вызвала getint( ). Наша единственная строка программы имеет точно такой же эффект, за исключением того, что нам не нужна промежуточная переменная status. Теперь напишем функцию stol( ). Преобразование строки в целое: stoi( ) Далее Содержание Сначала опишем, каким должен быть вход и выход у этой функции. Вход будет символьной строкой, поэтому stoi( ) будет иметь символьную строку в качестве аргумента. На выходе должно быть получено два значения: состояние и преобразованное целое число. Мы применяем return для состояния и поэтому должны использовать указатель для возврата другого значения. Таким образом, появится второй аргумент - указатель на целое число. Скелет нашей функции будет выглядеть примерно так: stoi(string, intptr) char string[ ]; /* вводимая строка * / int *intptr; /* указатель на переменную, получающую целое значение*/ { int status; return(status); } Прекрасно, а что можно сказать об алгоритме выполнения преобразования? На некоторое время проигнорируем знак и предположим, что строка содержит только цифры. Возьмем первый символ и 211 преобразуем его в числовой эквивалент. Предположим, это символ '4'. Он имеет в коде ASCII числовое значение 52 и в таком виде запоминается. Если мы из него вычтем 48, то получим 4, т. е. '4' - 48 = 4 Но 48 - это ASCII-код символа '0', поэтому '4' - '0' =4 Действительно, этот последний оператор был бы справедлив в любом коде, в котором используются последовательные числа для представления последовательных цифр. Поэтому если num - числовое значение, a chn - символ цифры, то num = chn - '0'; Итак, мы используем этот метод для преобразования первой цифры в число. Теперь возьмем следующий элемент массива. Если он '\0', то у нас была только одна цифра, и мы закончили работу. Предположим, однако, что этот элемент '3'. Превратим его в числовое значение 3. Но если оно равно 3, то 4 должно было быть числом 40, а оба числа вместе 43: num = 10 * num + chn - '0'; Теперь просто продолжим этот процесс, умножая старое значение num на 10 каждый раз, когда мы находим следующую цифру. Наша функция будет использовать этот метод. Вот определение функции stoi( ). Мы храним ее в том же файле, что и getint( ), так что она может использовать те же самые директивы #define. /* превращает строку в целое число и сообщает о состоянии */ stoi(string, intptr) char string[ ]; /* строка, подлежащая преобразованию в целое*/ int *intptr; /* значение целого */ { int sign = 1; /* проверяет наличие знака + или - */ int index = 0; if(string[index] == '-' || string[index] == '+') sign = (string[index ++] == '-') ? -1 : 1; /* установить знак */ *intptr = 0; /* начальное значение */ while(string[index] >= '0' && string[index] <= '9') *intptr = 10 * (*intptr) + strmg[index++] - '0'; if(string[index] == '\0') { *intptr = sign * (*intptr); return(YESNUM); } else /* найден символ, отличный от цифры, знака или ' \0' */ return(NONUM); } Оператор while продолжает работу, преобразуя цифры в числа, пока не достигнет нецифрового символа. Если это символ '\0', все прекрасно, потому что он означает конец строки. Любой другой нецифровой символ отсылает программу к else для сообщения об ошибке. Стандартная библиотека языка Си содержит функцию atoi( ) (перевод кода ASCII в целое число), очень похожую на stoi( ). Основная разница заключается в том, что stoi( ) проверяет на нецифровые строки, a atoi( ) использует return вместо указателя, для возврата числа, и пропускает пробел, как мы это делали в getint(). Можно было бы осуществить все проверки состояния в getint( ) и использовать atoi( ) вместо stoi( ), но мы полагаем, что было бы интереснее разрабoтать нашу собственную версию. 212 Проверка Далее Содержание Так ли уж правильны наши рассуждения? Давайте проверим нашу функцию на учебной программе: /* проверка функции getint( )*/ #define STOP - 1 #define NONUM 1 #define YESNUM 0 main( ) { int num, status; printf(" Программа прекращает считывание чисел, если встречает EOF. \n" ); while((status = getint(&num)) != STOP) if(status = = YESNUM) printf(" Число %d принято. \n", num); else printf(" Это не целое число! Попытайтесь снова. \n"); printf("Этo оно. \n"); } Вот пример работы программы: Программа прекращает считывание чисел, если встречает EOF. 100 -23 Число 100 принято. Чмсло -23 принято. +892. Число 892 принято. wonk Это не целое число! Попытейтесь снова. 23skidoo Это не целое число! Попытайтесь снова. 775 Число 775 принято. Клавиша [control z] (посылает символ EOF в нашу программу). Это оно. Как видите, программа выполняется. Обратите внимание на то, как мы сумели организовать цикл для неограниченного считывания целых чисел до тех пор, пока не будет введен символ EOF. Это удобное свойство. Есть ли здесь ошибки? По меньшей мере одна. Если непосредственно за числом следует символ EOF без разделяющего пробела или символа новой строки, ввод прекращается, и это число не принимается во внимание: 706 EOF /* 706 принято*/ 706 EOF /* 706 не принято*/ Мы не хотели делать пример слишком сложным, поэтому допустили возможность этой ошибки. Дальнейшую разработку программы, как мы думаем, может осуществить сам читатель в качестве упражнения. Теперь, когда у нас есть удобная функция для получения целых чисел, займемся новой задачей, в которой она будет использоваться. |