Главная страница
Навигация по странице:

  • Упражнение 7.6.

  • 7.8. Другие библиотечные функции

  • 7.8.1. Операции со строками

  • 7.8.2. Анализ класса символов и преобразование символов

  • 7.8.4. Исполнение команд операционной системы

  • 7.8.5. Управление памятью

  • 7.8.6. Математические функции

  • 7.8.7. Генератор случайных чисел

  • Упражнение 7.9.

  • 8.1. Дескрипторы файлов

  • 8.2. Нижний уровень ввода-вывода (read и write)

  • Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents


    Скачать 2.33 Mb.
    НазваниеЯзык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
    Дата18.09.2022
    Размер2.33 Mb.
    Формат файлаpdf
    Имя файлаBrian_Kernighan_Dennis_Ritchie-The_C_Programming_Language-RU.pdf
    ТипДокументы
    #683263
    страница19 из 31
    1   ...   15   16   17   18   19   20   21   22   ...   31
    7.6. Управление ошибками (stderr и exit)
    Обработку ошибок в cat нельзя признать идеальной. Беда в том, что если файл по какой-либо причине недоступен, сообщение об этом мы получим по окончании конкатенируемого вывода. Это нас устроило бы, если бы вывод отправлялся только на экран, а не в файл или по конвейеру другой программе.
    Чтобы лучше справиться с этой проблемой, программе помимо стандартного вывода stdout придается еще один выходной поток, называемый stderr
    . Вывод в stderr обычно отправляется на экран, даже если вывод stdout перенаправлен в другое место.
    Перепишем cat так, чтобы сообщения об ошибках отправлялись в stderr
    #include
    /* cat: конкатенация файлов, версия 2 */ main(int argc, char *argv[])
    {
    FILE *fp; void filecopy(FILE *, FILE *); char *prog = argv[0]; /* имя программы */ if (argc ==1) /* нет аргументов; копируется станд. ввод */ filecopy(stdin, stdout); else while (--argc > 0) if ((fp = fopen(*++argv, "r")) == NULL) { fprintf (stderr, "%s: не могу открыть файл %s\n", prog, *argv); exit(t);
    } else { filecopy(fp, stdout); fclose(fp);
    } if (ferror(stdout)) { fprintf (stderr, "%s: ошибка записи в stdout\n", prog); exit(2);
    } exit(0);
    }
    Программа сигнализирует об ошибках двумя способами. Первый — сообщение об ошибке с помощью fprintf посылается в stderr с тем, чтобы оно попало на экран, а не оказалось на конвейере или в другом файле вывода. Имя программы, хранящееся в argv[0]
    , мы включили в сообщение, чтобы в случаях, когда данная программа работает совместно с другими, был ясен источник ошибки.
    Второй способ указать на ошибку — обратиться к библиотечной функции exit
    , завершающей работу программы. Аргумент функции exit доступен некоторому процессу, вызвавшему данный процесс. А следовательно, успешное или ошибочное завершение программы можно проконтролировать с помощью некоей программы, которая рассматривает эту программу в качестве подчиненного процесса. По общей договоренности возврат нуля сигнализирует о том, что работа прошла нормально, в то время как ненулевые значения обычно говорят об ошибках. Чтобы опорожнить буфера, накопившие информацию для всех открытых файлов вывода, функция exit вызывает fclose

    Инструкция return выр
    главной программы main эквивалентна обращению к функции exit(выр)
    Последний вариант (с помощью exit
    ) имеет то преимущество, что он пригоден для выхода и из других функций, и, кроме того, слово exit легко обнаружить с помощью программы контекстного поиска, похожей на ту, которую мы рассматривали в главе 5.
    Функция ferror выдает ненулевое значение, если в файле fp была обнаружена ошибка. int ferror(FILE *fp)
    Хотя при выводе редко возникают ошибки, все же они встречаются (например, оказался переполненным диск); поэтому в программах широкого пользования они должны тщательно контролироваться.
    Функция feof(FILE *)
    аналогична функции ferror
    ; она возвращает ненулевое значение, если встретился конец указанного в аргументе файла. int feof(FILE *fp)
    В наших небольших иллюстративных программах мы не заботились о выдаче статуса выхода, т. е. выдаче некоторого числа, характеризующего состояние программы в момент завершения: работа закончилась нормально или прервана из-за ошибки? Если работа прервана в результате ошибки, то какой? Любая серьезная программа должна выдавать статус выхода.
    7.7. Ввод-вывод строк
    В стандартной библиотеке имеется программа ввода fgets
    , аналогичная программе getline
    , которой мы пользовались в предыдущих главах. char *fgets(char *line, int maxline, FILE *fp)
    Функция fgets читает следующую строку ввода (включая и символ новой строки) из файла fp в массив символов line
    , причем она может прочитать не более
    MAXLINE-1
    символов. Переписанная строка дополняется символом '\0'
    . Обычно fgets возвращает line
    , а по исчерпании файла или в случае ошибки

    NULL
    . (Наша getline возвращала длину строки, которой мы потом пользовались, и нуль в случае конца файла.)
    Функция вывода fputs пишет строку (которая может и не заканчиваться символом новой строки) в файл. int fputs(char *line, FILE *fp)
    Эта функция возвращает
    EOF
    , если возникла ошибка, и неотрицательное значение в противном случае.
    Библиотечные функции gets и puts подобны функциям fgets и fputs
    . Отличаются они тем, что оперируют только стандартными файлами stdin и stdout
    , и кроме того, gets выбрасывает последний символ '\n'
    , a puts его добавляет.
    Чтобы показать, что ничего особенного в функциях вроде fgets и fputs нет, мы приводим их здесь в том виде, в каком они существуют в стандартной библиотеке на нашей системе.
    /* fgets: получает не более n символов из iop */ char *fgets(char *s, int n, FILE *iop)
    { register int c; register char *cs; cs = s; while (--n > 0 && (c = getc(iop)) != EOF)
    if ((*cs++ = c) == '\n' ) break;
    *cs = '\0' ; return (c == EOF && cs == s) ? NULL : s;
    }
    /* fputs: посылает строку s в файл iop */ int fputs(char *s, FILE *iop)
    { int c; while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0;
    }
    Стандарт определяет, что функция ferror возвращает в случае ошибки ненулевое значение; fputs в случае ошибки возвращает
    EOF
    , в противном случае — неотрицательное значение.
    С помощью fgets легко реализовать нашу функцию getline
    :
    /* getline: читает строку, возвращает ее длину */ int getline(char *line, int max)
    { if (fgets(line, max, stdin) == NULL) return 0; else return strlen(line);
    }
    Упражнение 7.6. Напишите программу, сравнивающую два файла и печатающую первую строку, в которой они различаются.
    Упражнение 7.7. Модифицируйте программу поиска по образцу из главы 5 таким образом, чтобы она брала текст из множества именованных файлов, а если имен файлов в аргументах нет, то из стандартного ввода.
    Будет ли печататься имя файла, в котором найдена подходящая строка?
    Упражнение 7.8. Напишите программу, печатающую несколько файлов. Каждый файл должен начинаться с новой страницы, предваряться заголовком и иметь свою нумерацию страниц.
    7.8. Другие библиотечные функции
    В стандартной библиотеке представлен широкий спектр различных функций. Настоящий параграф содержит краткий обзор наиболее полезных из них. Более подробно эти и другие функции описаны в приложении В.
    7.8.1. Операции со строками
    Мы уже упоминали функции strlen
    , strcpy
    , strcat и strcmp
    , описание которых даны в

    Далее, до конца пункта, предполагается, что s
    и t
    имеют тип char *
    , c
    и n
    — тип int strcat(s, t)
    — приписывает t
    в конец s
    strncat(s, t, n)
    — приписывает n
    символов из t
    в конец s
    strcmp(s, t)
    — возвращает отрицательное число, нуль или положительное число для s < t
    , s
    == t или s > t соответственно.
    strncmp(s, t, n)
    — делает то же, что и strcmp
    , но количество сравниваемых символов не может превышать n
    strcpy(s, t)
    — копирует t
    в s
    strncpy(s, t, n)
    — копирует не более n
    символов из t
    в s
    strlen(s)
    — возвращает длину s
    strchr(s, с)
    — возвращает указатель на первое появление символа c
    в s
    или, если c
    нет в s
    ,
    NULL
    strrchr(s, с)
    — возвращает указатель на последнее появление символа c
    в s
    или, если c
    нет в s
    ,
    NULL
    7.8.2. Анализ класса символов и преобразование символов
    Несколько функций из библиотеки

    выполняют проверки и преобразование символов. Далее, до конца пункта, переменная с
    — это переменная типа int
    , которая может быть представлена значением unsigned char или
    EOF
    . Все эти функции возвращают значения типа int isalpha(c)
    — не нуль, если c
    — буква; 0 в противном случае. isupper(c)
    — не нуль, если c
    — буква верхнего регистра; 0 в противном случае. islower(c)
    — не нуль, если c
    — буква нижнего регистра; 0 в противном случае. isdigit(c)
    — не нуль, если c
    — цифра; 0 в противном случае. isalnum(c)
    — не нуль, если или isalpha(c)
    , или isdigit(c)
    истинны; 0 в противном случае. isspace(c)
    — не нуль, если c
    — символ пробела, табуляции, новой строки, возврата каретки, перевода страницы, вертикальной табуляции. toupper(с)
    — возвращает c
    , приведенную к верхнему регистру. tolower(с)
    — возвращает c
    , приведенную к нижнему регистру.
    7.8.3. Функция ungetc
    В стандартной библиотеке содержится более ограниченная версия функции ungetch по сравнению с той, которую мы написали в главе 4. Называется она ungetc
    . Эта функция, имеющая прототип int ungetc(int с, FILE *fp) отправляет символ с
    назад в файл fp и возвращает с
    , а в случае ошибки
    EOF
    . Для каждого файла гарантирован возврат не более одного символа. Функцию ungetc можно использовать совместно с любой из функций ввода вроде scanf
    , getc
    , getchar и т. д.
    7.8.4. Исполнение команд операционной системы
    Функция system(char *s)
    выполняет команду системы, содержащуюся в строке s
    , и затем возвращается к выполнению текущей программы. Содержимое s
    , строго говоря, зависит от конкретной операционной системы. Рассмотрим простой пример: в системе UNIX инструкция system("date");
    вызовет программу date
    , которая направит дату и время в стандартный вывод. Функция возвращает зависящий от системы статус выполненной команды. В системе UNIX возвращаемый статус — это значение, переданное функцией exit.
    7.8.5. Управление памятью
    Функции malloc и calloc динамически запрашивают блоки свободной памяти. Функция malloc void *malloc(size_t n) возвращает указатель на n
    байт неинициализированной памяти или
    NULL
    , если запрос удовлетворить нельзя.
    Функция calloc void *calloc(size_t n, size_t size) возвращает указатель на область, достаточную для хранения массива из n
    объектов указанного размера
    (
    size
    ), или
    NULL
    , если запрос не удается удовлетворить. Выделенная память обнуляется.
    Указатель, возвращаемый функциями malloc и calloc
    , будет выдан с учетом выравнивания, выполненного согласно указанному типу объекта. Тем не менее, к нему должна быть применена операция приведения к соответствующему типу
    12
    , как это сделано в следующем фрагменте программы: int *ip; ip = (int *) calloc(n, sizeofF(int));
    Функция free(p)
    освобождает область памяти, на которую указывает р
    , — указатель, первоначально полученный с помощью malloc или calloc
    . Никаких ограничений на порядок, в котором будет освобождаться память, нет, но считается ужасной ошибкой освобождение тех областей, которые не были получены с помощью calloc или malloc
    Нельзя также использовать те области памяти, которые уже освобождены. Следующий пример демонстрирует типичную ошибку в цикле, освобождающем элементы списка. for (р = head; р != NULL; р = p->next) /* НЕВЕРНО */ free(р);
    Правильным будет, если вы до освобождения сохраните то, что вам потребуется, как в следующем цикле: for (p = head; p != NULL; p = q) { q = p->next; free(p);
    }
    В параграфе 8.7 мы рассмотрим реализацию программы управления памятью вроде malloc
    , позволяющую освобождать выделенные блоки памяти в любой последовательности.
    7.8.6. Математические функции
    В

    описано более двадцати математических функций. Здесь же приведены наиболее употребительные. Каждая из них имеет один или два аргумента типа double и возвращает результат также типа double sin(х)
    — синус х
    , х
    в радианах.
    12
    Как уже отмечалось, замечание о приведении типов значений, возвращаемых функциями malloc или calloc
    ,— неверно. — Примеч. авт.
    cos(x)
    — косинус х
    , х
    в радианах. atan2(y, х)
    — арктангенс y
    /
    х
    , y
    и х
    в радианах. ехр(х)
    — экспоненциальная функция е x
    log(x)
    — натуральный (по основанию е) логарифм х
    (
    х
    >0). log10(x)
    — обычный (по основанию 10) логарифм х
    (
    х
    > 0). pow(x, y)
    — x
    y sqrt(х)
    — корень квадратный х
    (
    х
    > 0). fabs(x)
    — абсолютное значение x
    7.8.7. Генератор случайных чисел
    Функция rand()
    вычисляет последовательность псевдослучайных целых в диапазоне от нуля до значения, заданного именованной константой
    RAND_MAX
    , которая определена в

    . Привести случайные числа к значениям с плавающей точкой, большим или равным 0 и меньшим 1, можно по формуле
    #define frand() ((double) rand() / (RAND_MAX+1.0))
    (Если в вашей библиотеке уже есть функция для получения случайных чисел с плавающей точкой, вполне возможно, что ее статистические характеристики лучше указанной.)
    Функция srand(unsigned)
    устанавливает семя для rand
    . Реализации rand и srand
    , предлагаемые стандартом и, следовательно, переносимые на различные машины, рассмотрены в параграфе 2.7.
    Упражнение 7.9. Реализуя функции вроде isupper
    , можно экономить либо память, либо время. Напишите оба варианта функции.

    8. Интерфейс с системой UNIX
    Свои услуги операционная система UNIX предлагает в виде набора системных вызовов, которые фактически являются ее внутренними функциями и к которым можно обращаться из программ пользователя. В настоящей главе описано, как в Си-программах можно применять некоторые наиболее важные вызовы. Если вы работаете в системе UNIX, то эти сведения будут вам полезны непосредственно и позволят повысить эффективность работы или получить доступ к тем возможностям, которых нет в библиотеке. Даже если вы используете Си в другой операционной системе, изучение рассмотренных здесь примеров все равно приблизит вас к пониманию программирования на Си; аналогичные программы (отличающиеся лишь деталями) вы встретите практически в любой операционной системе. Так как библиотека Си-программ, утвержденная в качестве стандарта ANSI, в основном отражает возможности системы UNIX, предлагаемые программы помогут вам лучше понять и библиотеку.
    Глава состоит из трех основных частей, описывающих: ввод-вывод, файловую систему и организацию управления памятью. В первых двух частях предполагается некоторое знакомство читателя с внешними характеристиками системы UNIX.
    В главе 7 мы рассматривали единый для всех операционных систем интерфейс ввода-вывода. В любой конкретной системе программы стандартной библиотеки пишутся с использованием средств именно этой конкретной системы. В следующих нескольких параграфах мы опишем вызовы системы UNIX по вводу-выводу и покажем, как с их помощью можно реализовать некоторые разделы стандартной библиотеки.
    8.1. Дескрипторы файлов
    В системе UNIX любые операции ввода-вывода выполняются посредством чтения и записи файлов, поскольку все внешние устройства, включая клавиатуру и экран, рассматриваются как объекты файловой системы. Это значит, что все связи между программой и внешними устройствами осуществляются в рамках единого однородного интерфейса.
    В самом общем случае, прежде чем читать или писать, вы должны проинформировать систему о действиях, которые вы намереваетесь выполнять в отношении файла; эта процедура называется открытием файла.
    Если вы собираетесь писать в файл, то, возможно, его потребуется создать заново или очистить от хранимой информации. Система проверяет ваши права на эти действия (файл существует? вы имеете к нему доступ?) и, если все в порядке, возвращает программе небольшое неотрицательное целое, называемое дескриптором
    файла. Всякий раз, когда осуществляется ввод-вывод, идентификация файла выполняется по его дескриптору, а не по имени. (Дескриптор файла аналогичен файловому указателю, используемому в стандартной библиотеке, или хэндлу (handle) в MSDOS.) Вся информация об открытом файле хранится и обрабатывается операционной системой; программа пользователя обращается к файлу только через его дескриптор.
    Ввод с клавиатуры и вывод на экран применяются настолько часто, что для удобства работы с ними предусмотрены специальные соглашения. При запуске программы командный интерпретатор (shell) открывает три файла с дескрипторами 0, 1 и 2, которые называются соответственно стандартным вводом, стандартным выводом и стандартным файлом ошибок. Если программа читает из файла 0, а пишет в файлы 1 и 2 (здесь цифры — дескрипторы файлов), то она может осуществлять ввод и вывод, не заботясь об их открытии.
    Пользователь программы имеет возможность перенаправить ввод-вывод в файл или из файла с помощью значков
    <
    и
    >
    , как, например, в prog outfile
    В этом случае командный интерпретатор заменит стандартные установки дескрипторов 0 и 1 на именованные файлы. Обычно дескриптор файла 2 остается подсоединенным к экрану, чтобы на него шли сообщения об ошибках. Сказанное верно и для ввода-вывода, связанного в конвейер. Во всех случаях замену файла
    осуществляет командный интерпретатор, а не программа. Программа, если она ссылается на файл 0 (в случае ввода) и файлы 1 и 2 (в случае вывода), не знает, ни откуда приходит ее ввод, ни куда отправляется ее вывод.
    8.2. Нижний уровень ввода-вывода (read и write)
    Ввод-вывод основан на системных вызовах read и write
    , к которым Си-программа обращается с помощью функций с именами read и write
    . Для обеих первым аргументом является дескриптор файла. Во втором аргументе указывается массив символов вашей программы, куда посылаются или откуда берутся данные.
    Третий аргумент — это количество пересылаемых байтов. int n_read = read(int fd, char *buf, int n); int n_written = write(int fd, char *buf, int n);
    Обе функции возвращают число переданных байтов. При чтении количество прочитанных байтов может оказаться меньше числа, указанного в третьем аргументе. Нуль означает конец файла, а -1 сигнализирует о какой-то ошибке. При записи функция возвращает количество записанных байтов, и если это число не совпадает с требуемым, следует считать, что запись не произошла.
    За один вызов можно прочитать или записать любое число байтов. Обычно это число равно или 1, что означает посимвольную передачу "без буферизации", или чему-нибудь вроде 1024 или 4096, соответствующих размеру физического блока внешнего устройства. Эффективнее обмениваться большим числом байтов, поскольку при этом требуется меньше системных вызовов. Используя полученные сведения, мы можем написать простую программу, копирующую свой ввод на свой вывод и эквивалентную программе копирования файла, описанной в главе 1 . С помощью этой программы можно копировать откуда угодно и куда угодно, поскольку всегда существует возможность перенаправить ввод-вывод на любой файл или устройство.
    #include "syscalls.h" main() /* копирование ввода на вывод */
    { char buf[BUFSIZ]; int n; while ((n = read(0, but, BUFSIZ)) > 0) write(i, but, n); return 0;
    }
    Прототипы функций, обеспечивающие системные вызовы, мы собрали в файле syscalls.h
    , что позволяет нам включать его в программы этой главы. Однако имя данного файла не зафиксировано стандартом.
    Параметр
    BUFSIZ
    также определен в

    ; в каждой конкретной системе он имеет свое значение. Если размер файла не кратен
    BUFSIZ
    , то какая-то операция чтения вернет значение меньшее, чем
    BUFSIZ
    , а следующее обращение к read даст в качестве результата нуль.
    Полезно рассмотреть, как используются read и write при написании программ более высокого уровня — таких как getchar
    , putchar и т.д. Вот, к примеру, версия программы getchar
    , которая осуществляет небуферизованный ввод, читая по одному символу из стандартного входного потока.
    #include "syscalls.h"
    /* getchar: небуферизованный ввод одного символа */ int getchar(void)
    { char с;
    return (read(0, &c, 1) == 1) ? (unsigned char) с : EOF;
    }
    Переменная c
    должна быть типа char
    , поскольку read требует указателя на char
    . Приведение c
    к unsigned char перед тем, как вернуть ее в качестве результата, исключает какие-либо проблемы, связанные с распространением знака.
    Вторая версия getchar осуществляет ввод большими кусками, но при каждом обращении выдает только один символ.
    #include "syscalls.h"
    /* getchar: простая версия с буферизацией */ int getchar(void)
    { static char buf[BUFSIZ]; static char *bufp = buf; static int n = 0; if (n == 0) { /* буфер пуст '*/ n = read(0, buf, sizeof buf); bufp = buf;
    } return (--n >= 0) ? (unsigned char) *bufp++ : EOF;
    }
    Если приведенные здесь версии функции getchar компилируются с включением заголовочного файла

    и в этом заголовочном файле getchar определена как макрос, то нужно задать строку
    #undef с именем getchar
    1   ...   15   16   17   18   19   20   21   22   ...   31


    написать администратору сайта