char *string = "абвгдежзиклмноп";
setlocale(LC_ALL, "");
for(;c = *string;string++){
#ifdef DEBUG
printf("%c %d %d\n", *string, *string, c);
#endif
if(isprint(c)) printf("%c - печатный символ\n", c);
}
return 0;
}
Эта программа неожиданно печатает
% a.out
в - печатный символ
з - печатный символ
И все. В чем дело???
Рассмотрим к примеру символ 'г'. Его код '\307'. В операторе
c = *string;
Символ c получает значение -57 (десятичное), которое ОТРИЦАТЕЛЬНО. В системном файле
/usr/include/ctype.h макрос isprint определен так:
#define isprint(c) ((_ctype + 1)[c] & (_P|_U|_L|_N|_B))
И значение c используется в нашем случае как отрицательный индекс в массиве, ибо
индекс приводится к типу int (signed). Откуда теперь извлекается значение флагов -
нам неизвестно; можно только с уверенностью сказать, что НЕ из массива _ctype.
А. Богатырев, 1992-95 - 129 - Си в UNIX
Проблему решает либо использование
isprint(c & 0xFF)
либо
isprint((unsigned char) c)
либо объявление в нашем примере
unsigned char c;
В первом случае мы явно приводим signed к unsigned битовой операцией, обнуляя лишние
биты. Во втором и третьем - unsigned char расширяется в unsigned int, который оста-
нется положительным. Вероятно, второй путь предпочтительнее.
3.12. Итак, снова напомним, что русские буквы char, а не unsigned char дают отрица-
тельные индексы в массиве.
char c = 'г';
int x[256];
...x[c]... /* индекс < 0 */
...x['г']...
Поэтому байтовые индексы должны быть либо unsigned char, либо & 0xFF. Как в следую-
щем примере:
/* Программа преобразования символов в файле: транслитерация
tr abcd prst заменяет строки
xxxxdbcaxxxx -> xxxxtrspxxxx
По мотивам книги М.Дансмура и Г.Дейвиса.
*/
#include <stdio.h>
#define ASCII 256 /* число букв в алфавите ASCII */
/* BUFSIZ определено в stdio.h */
char mt[ ASCII ]; /* таблица перекодировки */
/* начальная разметка таблицы */
void mtinit(){
register int i;
for( i=0; i < ASCII; i++ )
mt[i] = (char) i;
}
А. Богатырев, 1992-95 - 130 - Си в UNIX
int main(int argc, char *argv[])
{
register char *tin, *tout; /* unsigned char */
char buffer[ BUFSIZ ];
if( argc != 3 ){
fprintf( stderr, "Вызов: %s что наЧто\n", argv[0] );
return(1);
}
tin = argv[1]; tout = argv[2];
if( strlen(tin) != strlen(tout)){
fprintf( stderr, "строки разной длины\n" );
return(2);
}
mtinit();
do{
mt[ (*tin++) & 0xFF ] = *tout++;
/* *tin - имеет тип char.
* & 0xFF подавляет расширение знака
*/
} while( *tin );
tout = mt;
while( fgets( buffer, BUFSIZ, stdin ) != NULL ){
for( tin = buffer; *tin; tin++ )
*tin = tout[ *tin & 0xFF ];
fputs( buffer, stdout );
}
return(0);
}
3.13.
int main(int ac, char *av[]){
char c = 'г';
if('a' <= c && c < 256)
printf("Это одна буква.\n");
return 0;
}
Увы, эта программа не печатает НИЧЕГО. Просто потому, что signed char в сравнении (в
операторе if) приводится к типу int. А как целое число - русская буква отрицательна.
Снова решением является либо использование везде (c & 0xFF), либо объявление
unsigned char c. В частности, этот пример показывает, что НЕЛЬЗЯ просто так сравни-
вать две переменные типа char. Нужно принимать предохранительные меры по подавлению
расширения знака:
if((ch1 & 0xFF) < (ch2 & 0xFF))...;
Для unsigned char такой проблемы не будет.
3.14. Почему неверно:
А. Богатырев, 1992-95 - 131 - Си в UNIX
#include <stdio.h>
main(){
char c;
while((c = getchar()) != EOF)
putchar(c);
}
Потому что c описано как char, в то время как EOF - значение типа int равное (-1).
Русская буква "Большой твердый знак" в кодировке КОИ-8 имеет код '\377' (0xFF).
Если мы подадим на вход этой программе эту букву, то в сравнении signed char со зна-
чением знакового целого EOF, c будет приведено тоже к знаковому целому - расширением
знака. 0xFF превратится в (-1), что означает, что поступил символ EOF. Сюрприз!!!
Посему данная программа будет делать вид, что в любом файле с большим русским твердым
знаком после этого знака (и включая его) дальше ничего нет. Что есть досадное заблуж-
дение.
Решением служит ПРАВИЛЬНОЕ объявление int c.
3.15. Изучите поведение программы
#define TYPE char
void f(TYPE c){
if(c == 'й') printf("Это буква й\n");
printf("c=%c c=\\%03o c=%03d c=0x%0X\n", c, c, c, c);
}
int main(){
f('г'); f('й');
f('z'); f('Z');
return 0;
}
когда TYPE определено как char, unsigned char, int. Объясните поведение. Выдачи в
этих трех случаях таковы (int == 32 бита):
c=г c=\37777777707 c=-57 c=0xFFFFFFC7
Это буква й
c=й c=\37777777712 c=-54 c=0xFFFFFFCA
c=z c=\172 c=122 c=0x7A
c=Z c=\132 c=090 c=0x5A
c=г c=\307 c=199 c=0xC7
c=й c=\312 c=202 c=0xCA
c=z c=\172 c=122 c=0x7A
c=Z c=\132 c=090 c=0x5A
и снова как 1 случай.
Рассмотрите альтернативу
if(c == (unsigned char) 'й') printf("Это буква й\n");
где предполагается, что знак у русских букв и у c НЕ расширяется. В данном случае
фраза 'Это буква й' не печатается ни с типом char, ни с типом int, поскольку в срав-
нении c приводится к типу signed int расширением знакового бита (который равен 1).
Слева получается отрицательное число!
В таких случаях вновь следует писать
if((unsigned char)c == (unsigned char)'й') printf("Это буква й\n");
А. Богатырев, 1992-95 - 132 - Си в UNIX
3.16. Обычно возникают проблемы при написании функций с переменным числом аргумен-
тов. В языке Си эта проблема решается использованием макросов va_args, не зависящих
от соглашений о вызовах функций на данной машине, и использующих эти макросы специ-
альных функций. Есть два стиля оформления таких программ: с использованием
<varargs.h> и <stdarg.h>. Первый был продемонстрирован в первой главе на примере
функции poly(). Для иллюстрации второго приведем пример функции трассировки, записы-
вающей собщение в файл:
#include <stdio.h>
#include <stdarg.h>
void trace(char *fmt, ...) {
va_list args;
static FILE *fp = NULL;
if(fp == NULL){
if((fp = fopen("TRACE", "w")) == NULL) return;
}
va_start(args, fmt);
/* второй аргумент: арг-т после которого
* в заголовке функции идет ... */
vfprintf(fp, fmt, args); /* библиотечная ф-ция */
fflush(fp); /* вытолкнуть сообщение в файл */
va_end(args);
}
main(){ trace( "%s\n", "Go home.");
trace( "%d %d\n", 12, 34);
}
Символ `...' (троеточие) в заголовке функции обозначает переменный (возможно пустой)
список аргументов. Он должен быть самым последним, следуя за всеми обязательными
аргументами функции.
Макрос va_arg(args,type), извлекающий из переменного списка аргументов `...'
очередное значение типа type, одинаков в обоех моделях. Функция vfprintf может быть
написана через функцию vsprintf (в действительности обе функции - стандартные):
int vfprintf(FILE *fp, const char *fmt, va_list args){
/*static*/ char buffer[1024]; int res;
res = vsprintf(buffer, fmt, args);
fputs(buffer, fp); return res;
}
Функция vsprintf(str,fmt,args); аналогична функции sprintf(str,fmt,...) - записывает
преобразованную по формату строку в байтовый массив str, но используется в контексте,
подобном приведенному. В конец сформированной строки sprintf записывает '\0'.
3.17. Напишите функцию printf, понимающую форматы %c (буква), %d (целое), %o (вось-
меричное), %x (шестнадцатеричное), %b (двоичное), %r (римское), %s (строка), %ld
(длинное целое). Ответ смотри в приложении.
3.18. Для того, чтобы один и тот же исходный текст программы транслировался на раз-
ных машинах (в разных системах), приходится выделять в программе системно-зависимые
части. Такие части должны по-разному выглядеть на разных машинах, поэтому их оформ-
ляют в виде так называемых "условно компилируемых" частей:
#ifdef XX
... вариант1
#else
... вариант2
#endif
А. Богатырев, 1992-95 - 133 - Си в UNIX
Эта директива препроцессора ведет себя следующим образом: если макрос с именем XX был
определен
#define XX
то в программу подставляется вариант1, если же нет - вариант2. Оператор #else не обя-
зателен - при его отсутствии вариант2 пуст. Существует также оператор #ifndef, кото-
рый подставляет вариант1 если макрос XX не определен. Есть еще и оператор #elif -
else if:
#ifdef макро1
...
#elif макро2
...
#else
...
#endif
Определить макрос можно не только при помощи #define, но и при помощи ключа компиля-
тора, так
cc -DXX file.c ...
соответствует включению в начало файла file.c директивы
#define XX
А для программы
main(){
#ifdef XX
printf( "XX = %d\n", XX);
#else
printf( "XX undefined\n");
#endif
}
ключ
cc -D"XX=2" file.c ...
эквивалентен заданию директивы
#define XX 2
Что будет, если совсем не задать ключ -D в данном примере?
Этот прием используется в частности в тех случаях, когда какие-то стандартные
типы или функции в данной системе носят другие названия:
cc -Dvoid=int ...
cc -Dstrchr=index ...
В некоторых системах компилятор автоматически определяет специальные макросы: так
компиляторы в UNIX неявно подставляют один из ключей (или несколько сразу):
-DM_UNIX
-DM_XENIX
-Dunix
-DM_SYSV
-D__SVR4
-DUSG
... бывают и другие
А. Богатырев, 1992-95 - 134 - Си в UNIX
Это позволяет программе "узнать", что ее компилируют для системы UNIX. Более под-
робно про это написано в документации по команде cc.
3.19. Оператор #ifdef применяется в include-файлах, чтобы исключить повторное вклю-
чение одного и того же файла. Пусть файлы aa.h и bb.h содержат
aa.h bb.h
#include "cc.h" #include "cc.h"
typedef unsigned long ulong; typedef int cnt_t;
А файлы cc.h и 00.c содержат
cc.h 00.c
... #include "aa.h"
struct II { int x, y; }; #include "bb.h"
... main(){ ... }
В этом случае текст файла cc.h будет вставлен в 00.c дважды: из aa.h и из bb.h. При
компиляции 00.c компилятор сообщит "Переопределение структуры II". Чтобы include-
файл не подставлялся еще раз, если он уже однажды был включен, придуман следующий
прием - следует оформлять файлы включений так:
/* файл cc.h */
#ifndef _CC_H
# define _CC_H /* определяется при первом включении */
...
struct II { int x, y; };
...
#endif /* _CC_H */
Второе и последующие включения такого файла будут подставлять пустое место, что и
требуется. Для файла <sys/types.h> было бы использовано макроопределение
_SYS_TYPES_H.
3.20. Любой макрос можно отменить, написав директиву
#undef имяМакро
Пример:
#include <stdio.h>
#undef M_UNIX
#undef M_SYSV
main() {
putchar('!');
#undef putchar
#define putchar(c) printf( "Буква '%c'\n", c);
putchar('?');
#if defined(M_UNIX) || defined(M_SYSV)
/* или просто #if M_UNIX */
printf("Это UNIX\n");
#else
printf("Это не UNIX\n");
#endif /* UNIX */
}
Обычно #undef используется именно для переопределения макроса, как putchar в этом
примере (дело в том, что putchar - это макрос из <stdio.h>).
Директива #if, использованная нами, является расширением оператора #ifdef и
подставляет текст если выполнено указанное условие:
А. Богатырев, 1992-95 - 135 - Си в UNIX
#if defined(MACRO) /* равно #ifdef(MACRO) */
#if !defined(MACRO) /* равно #ifndef(MACRO) */
#if VALUE > 15 /* если целая константа
#define VALUE 25
больше 15 (==, !=, <=, ...) */
#if COND1 || COND2 /* если верно любое из условий */
#if COND1 && COND2 /* если верны оба условия */
Директива #if допускает использование в качестве аргумента довольно сложных выраже-
ний, вроде
#if !defined(M1) && (defined(M2) || defined(M3))
3.21. Условная компиляция может использоваться для трассировки программ:
#ifdef DEBUG
# define DEBUGF(body) \
{ \
body; \
}
#else
# define DEBUGF(body)
#endif
int f(int x){ return x*x; }
int main(int ac, char *av[]){
int x = 21;
DEBUGF(x = f(x); printf("%s equals to %d\n", "x", x));
printf("x=%d\n", x);
}
При компиляции
cc -DDEBUG file.c
в выходном потоке программы будет присутствовать отладочная выдача. При компиляции
без -DDEBUG этой выдачи не будет.
3.22. В языке C++ (развитие языка Си) слова class, delete, friend, new, operator,
overload, template, public, private, protected, this, virtual являются зарезервиро-
ванными (ключевыми). Это может вызвать небольшую проблему при переносе текста прог-
раммы на Си в систему программирования C++, например:
#include <termio.h>
...
int fd_tty = 2; /* stderr */
struct termio old, new;
ioctl (fd_tty, TCGETA, &old);
new = old;
new.c_lflag |= ECHO | ICANON;
ioctl (fd_tty, TCSETAW, &new);
...
Строки, содержащие имя переменной (или функции) new, окажутся неправильными в C++.
Проще всего эта проблема решается переименованием переменной (или функции). Чтобы не
производить правки во всем тексте, достаточно переопределить имя при помощи директивы
define:
А. Богатырев, 1992-95 - 136 - Си в UNIX
#define new new_modes
... старый текст ...
#undef new
При переносе программы на Си в C++ следует также учесть, что в C++ для каждой функции
должен быть задан прототип, прежде чем эта функция будет использована (Си позволяет
опускать прототипы для многих функций, особенно возвращающих значения типов int или
void).
А. Богатырев, 1992-95 - 137 - Си в UNIX
4. Работа с файлами.
Файлы представляют собой области памяти на внешнем носителе (как правило магнит-
ном диске), предназначенные для:
- хранения данных, превосходящих по объему память компьютера (меньше, разумеется,
тоже можно);
- долговременного хранения информации (она сохраняется при выключении машины).
В UNIX и в MS DOS файлы не имеют предопределенной структуры и представляют собой
просто линейные массивы байт. Если вы хотите задать некоторую структуру хранимой
информации - вы должны позаботиться об этом в своей программе сами. Файлы отличаются
от обычных массивов тем, что
- они могут изменять свой размер;
- обращение к элементам этих массивов производится не при помощи операции индекса-
ции [], а при помощи специальных системных вызовов и функций;
- доступ к элементам файла происходит в так называемой "позиции чтения/записи",
которая автоматически продвигается при операциях чтения/записи, т.е. файл прос-
матривается последовательно. Есть, правда, функции для произвольного изменения
этой позиции.
Файлы имеют имена и организованы в иерархическую древовидную структуру из каталогов и
простых файлов. Об этом и о системе именования файлов прочитайте в документации по
UNIX.
4.1. Для работы с каким-либо файлом наша программа должна открыть этот файл - уста-
новить связь между именем файла и некоторой переменной в программе. При открытии
файла в ядре операционной системы выделяется "связующая" структура file "открытый
файл", содержащая:
f_offset:
указатель позиции чтения/записи, который в дальнейшем мы будем обозначать как
RWptr. Это long-число, равное расстоянию в байтах от начала файла до позиции
чтения/записи;
f_flag:
режимы открытия файла: чтение, запись, чтение и запись, некоторые дополнительные
флаги;
f_inode:
расположение файла на диске (в UNIX - в виде ссылки на I-узел файла|-);
и кое-что еще.
У каждого процесса имеется таблица открытых им файлов - это массив ссылок на
упомянутые "связующие" структуры|=. При открытии файла в этой таблице ищется
____________________
|- I-узел (I-node, индексный узел) - своеобразный "паспорт", который есть у каждого
файла (в том числе и каталога). В нем содержатся:
- длина файла long di_size;
- номер владельца файла int di_uid;
- коды доступа и тип файла ushort di_mode;
- время создания и последней модификации
time_t di_ctime, di_mtime;
- начало таблицы блоков файла char di_addr[...];
- количество имен файла short di_nlink;
и.т.п.
Содержимое некоторых полей этого паспорта можно узнать вызовом stat(). Все I-узлы
собраны в единую область в начале файловой системы - так называемый I-файл. Все I-
узлы пронумерованы, начиная с номера 1. Корневой каталог (файл с именем "/") как
правило имеет I-узел номер 2.
|= У каждого процесса в UNIX также есть свой "паспорт". Часть этого паспорта нахо-
дится в таблице процессов в ядре ОС, а часть - "приклеена" к самому процессу, однако
не доступна из программы непосредственно. Эта вторая часть паспорта носит название
"u-area" или структура user. В нее, в частности, входят таблица открытых процессом
файлов
А. Богатырев, 1992-95 - 138 - Си в UNIX
свободная ячейка, в нее заносится ссылка на структуру "открытый файл" в ядре, и
ИНДЕКС этой ячейки выдается в вашу программу в виде целого числа - так называемого
"дескриптора файла".
При закрытии файла связная структура в ядре уничтожается, ячейка в таблице счи-
тается свободной, т.е. связь программы и файла разрывается.
Дескрипторы являются локальными для каждой программы. Т.е. если две программы
открыли один и тот же файл - дескрипторы этого файла в каждой из них не обязательно
совпадут (хотя и могут). Обратно: одинаковые дескрипторы (номера) в разных програм-
мах не обязательно обозначают один и тот же файл. Следует учесть и еще одну вещь:
несколько или один процессов могут открыть один и тот же файл одновременно несколько
раз. При этом будет создано несколько "связующих" структур (по одной для каждого
открытия); каждая из них будет иметь СВОЙ указатель чтения/записи. Возможна и ситуа-
ция, когда несколько дескрипторов ссылаются к одной структуре - смотри ниже описание
вызова dup2.
fd u_ofile[] struct file
0 ## -------------
1---##---------------->| f_flag |
2 ## | f_count=3 |
3---##---------------->| f_inode---------*
... ## *-------------->| f_offset | |
процесс1 | ------!------ |
| ! V
0 ## | struct file ! struct inode
1 ## | ------------- ! -------------
2---##-* | f_flag | ! | i_count=2 |
3---##--->| f_count=1 | ! | i_addr[]----*
... ## | f_inode----------!-->| ... | | адреса
процесс2 | f_offset | ! ------------- | блоков
-------!----- *=========* | файла
! ! V
0 ! указатели R/W ! i_size-1
@@@@@@@@@@@!@@@@@@@@@@@@@@@@@@@@@!@@@@@@
файл на диске
/* открыть файл */
int fd = open(char имя_файла[], int как_открыть);
... /* какие-то операции с файлом */
close(fd); /* закрыть */
Параметр как_открыть:
#include <fcntl.h>
O_RDONLY - только для чтения.
O_WRONLY - только для записи.
O_RDWR - для чтения и записи.
O_APPEND - иногда используется вместе с
открытием для записи, "добавление" в файл:
O_WRONLY|O_APPEND, O_RDWR|O_APPEND
Если файл еще не существовал, то его нельзя открыть: open вернет значение (-1),
____________________
struct file *u_ofile[NOFILE];
ссылка на I-узел текущего каталога
struct inode *u_cdir;
а также ссылка на часть паспорта в таблице процессов
struct proc *u_procp;
А. Богатырев, 1992-95 - 139 - Си в UNIX
сигнализирующее об ошибке. В этом случае файл надо создать:
int fd = creat(char имя_файла[], int коды_доступа);
Дескриптор fd будет открыт для записи в этот новый пустой файл. Если же файл уже
существовал, creat опустошает его, т.е. уничтожает его прежнее содержимое и делает
его длину равной 0L байт. Коды_доступа задают права пользователей на доступ к файлу.
Это число задает битовую шкалу из 9и бит, соответствующих строке
биты: 876 543 210
rwx rwx rwx
r - можно читать файл
w - можно записывать в файл
x - можно выполнять программу из этого файла
Первая группа - эта права владельца файла, вторая - членов его группы, третяя - всех
прочих. Эти коды для владельца файла имеют еще и мнемонические имена (используемые в
вызове stat):
#include <sys/stat.h> /* Там определено: */
#define S_IREAD 0400
#define S_IWRITE 0200
#define S_IEXEC 0100
Подробности - в руководствах по системе UNIX. Отметим в частности, что open() может
вернуть код ошибки fd < 0 не только в случае, когда файл не существует
(errno==ENOENT), но и в случае, когда вам не разрешен соответствующий доступ к этому
файлу (errno==EACCES; про переменную кода ошибки errno см. в главе "Взаимодействие с
UNIX").
Вызов creat - это просто разновидность вызова open в форме
fd = open( имя_файла,
O_WRONLY|O_TRUNC|O_CREAT, коды_доступа);
O_TRUNC
означает, что если файл уже существует, то он должен быть опустошен при откры-
тии. Коды доступа и владелец не изменяются.
O_CREAT
означает, что файл должен быть создан, если его не было (без этого флага файл не
создастся, а open вернет fd < 0). Этот флаг требует задания третьего аргумента
коды_доступа|-. Если файл уже существует - этот флаг не имеет никакого эффекта,
но зато вступает в действие O_TRUNC.
Существует также флаг
O_EXCL
который может использоваться совместно с O_CREAT. Он делает следующее: если
файл уже существует, open вернет код ошибки (errno==EEXIST). Если файл не
____________________
|- Заметим, что на самом деле коды доступа у нового файла будут равны
di_mode = (коды_доступа & ~u_cmask) | IFREG;
(для каталога вместо IFREG будет IFDIR), где маска u_cmask задается системным вызовом
umask(u_cmask);
(вызов выдает прежнее значение маски) и в дальнейшем наследуется всеми потомками дан-
ного процесса (она хранится в u-area процесса). Эта маска позволяет запретить доступ
к определенным операциям для всех создаваемых нами файлов, несмотря на явно заданные
коды доступа, например
umask(0077); /* ???------ */
делает значащими только первые 3 бита кодов доступа (для владельца файла). Остальные
биты будут равны нулю.
Все это относится и к созданию каталогов вызовом mkdir.
А. Богатырев, 1992-95 - 140 - Си в UNIX
существовал - срабатывает O_CREAT и файл создается. Это позволяет предохранить
уже существующие файлы от уничтожения.
Файл удаляется при помощи
int unlink(char имя_файла[]);
У каждой программы по умолчанию открыты три первых дескриптора, обычно связанные
0 - с клавиатурой (для чтения)
1 - с дисплеем (выдача результатов)
2 - с дисплеем (выдача сообщений об ошибках)
Если при вызове close(fd) дескриптор fd не соответствует открытому файлу (не был отк-
рыт) - ничего не происходит.
Часто используется такая метафора: если представлять себе файлы как книжки
(только чтение) и блокноты (чтение и запись), стоящие на полке, то открытие файла -
это выбор блокнота по заглавию на его обложке и открытие обложки (на первой стра-
нице). Теперь можно читать записи, дописывать, вычеркивать и править записи в сере-
дине, листать книжку! Страницы можно сопоставить блокам файла (см. ниже), а "полку"
с книжками - каталогу.
4.2. Напишите программу, которая копирует содержимое одного файла в другой (новый)
файл. При этом используйте системные вызовы чтения и записи read и write. Эти сис-
вызовы пересылают массивы байт из памяти в файл и наоборот. Но любую переменную можно
рассматривать как массив байт, если забыть о структуре данных в переменной!
Читайте и записывайте файлы большими кусками, кратными 512 байтам. Это уменьшит
число обращений к диску. Схема:
char buffer[512]; int n; int fd_inp, fd_outp;
...
while((n = read (fd_inp, buffer, sizeof buffer)) > 0)
write(fd_outp, buffer, n);
Приведем несколько примеров использования write:
char c = 'a';
int i = 13, j = 15;
char s[20] = "foobar";
char p[] = "FOOBAR";
struct { int x, y; } a = { 666, 999 };
/* создаем файл с доступом rw-r--r-- */
int fd = creat("aFile", 0644);
write(fd, &c, 1);
write(fd, &i, sizeof i); write(fd, &j, sizeof(int));
write(fd, s, strlen(s)); write(fd, &a, sizeof a);
write(fd, p, sizeof(p) - 1);
close(fd);
Обратите внимание на такие моменты:
- При использовании write() и read() надо передавать АДРЕС данного, которое мы
хотим записать в файл (места, куда мы хотим прочитать данные из файла).
- Операции read и write возвращают число действительно прочитанных/записанных байт
(при записи оно может быть меньше указанного нами, если на диске не хватает
места; при чтении - если от позиции чтения до конца файла содержится меньше
информации, чем мы затребовали).
- Операции read/write продвигают указатель чтения/записи
RWptr += прочитанное_или_записанное_число_байт;
При открытии файла указатель стоит на начале файла: RWptr=0. При записи файл
А. Богатырев, 1992-95 - 141 - Си в UNIX
если надо автоматически увеличивает свой размер. При чтении - если мы достигнем
конца файла, то read будет возвращать "прочитано 0 байт" (т.е. при чтении указа-
тель чтения не может стать больше размера файла).
- Аргумент сколькоБайт имеет тип unsigned, а не просто int:
int n = read (int fd, char *адрес, unsigned сколькоБайт);
int n = write(int fd, char *адрес, unsigned сколькоБайт);
Приведем упрощенные схемы логики этих сисвызовов, когда они работают с обычным диско-
вым файлом (в UNIX устройства тоже выглядят для программ как файлы, но иногда с осо-
быми свойствами):
4.2.1. m = write(fd, addr, n);
если( ФАЙЛ[fd] не открыт на запись) то вернуть (-1);
если(n == 0) то вернуть 0;
если( ФАЙЛ[fd] открыт на запись с флагом O_APPEND ) то
RWptr = длина_файла; /* т.е. встать на конец файла */
если( RWptr > длина_файла ) то
заполнить нулями байты файла в интервале
ФАЙЛ[fd][ длина_файла..RWptr-1 ] = '\0';
скопировать байты из памяти процесса в файл
ФАЙЛ[fd][ RWptr..RWptr+n-1 ] = addr[ 0..n-1 ];
отводя на диске новые блоки, если надо
RWptr += n;
если( RWptr > длина_файла ) то
длина_файла = RWptr;
вернуть n;
4.2.2. m = read(fd, addr, n);
если( ФАЙЛ[fd] не открыт на чтение) то вернуть (-1);
если( RWptr >= длина_файла ) то вернуть 0;
m = MIN( n, длина_файла - RWptr );
скопировать байты из файла в память процесса
addr[ 0..m-1 ] = ФАЙЛ[fd][ RWptr..RWptr+m-1 ];
RWptr += m;
вернуть m;
4.3. Найдите ошибки в фрагменте программы:
#define STDOUT 1 /* дескриптор стандартного вывода */
int i;
static char s[20] = "hi\n";
char c = '\n';
struct a{ int x,y; char ss[5]; } po;
scanf( "%d%d%d%s%s", i, po.x, po.y, s, po.ss);
write( STDOUT, s, strlen(s));
write( STDOUT, c, 1 ); /* записать 1 байт */
Ответ: в функции scanf перед аргументом i должна стоять операция "адрес", то есть &i.
Аналогично про &po.x и &po.y. Заметим, что s - это массив, т.е. s и так есть адрес,
поэтому перед s операция & не нужна; аналогично про po.ss - здесь & не требуется.
В системном вызове write второй аргумент должен быть адресом данного, которое мы
хотим записать в файл. Поэтому мы должны были написать &c (во втором вызове write).
Ошибка в scanf - указание значения переменной вместо ее адреса - является
довольно распространенной и не может быть обнаружена компилятором (даже при использо-
вании прототипа функции scanf(char *fmt, ...), так как scanf - функция с переменным
А. Богатырев, 1992-95 - 142 - Си в UNIX
числом аргументов заранее не определенных типов). Приходится полагаться исключительно
на собственную внимательность!
4.4. Как по дескриптору файла узнать, открыт он на чтение, запись, чтение и запись
одновременно? Вот два варианта решения:
#include <fcntl.h>
#include <stdio.h>
#include <sys/param.h> /* там определено NOFILE */
#include <errno.h>
char *typeOfOpen(fd){
int flags;
if((flags=fcntl (fd, F_GETFL, NULL)) < 0 )
return NULL; /* fd вероятно не открыт */
flags &= O_RDONLY | O_WRONLY | O_RDWR;
switch(flags){
case O_RDONLY: return "r";
case O_WRONLY: return "w";
case O_RDWR: return "r+w";
default: return NULL;
}
}
char *type2OfOpen(fd){
extern errno; /* см. главу "системные вызовы" */
int r=1, w=1;
errno = 0; read(fd, NULL, 0);
if( errno == EBADF ) r = 0;
errno = 0; write(fd, NULL, 0);
if( errno == EBADF ) w = 0;
return (w && r) ? "r+w" :
w ? "w" :
r ? "r" :
"closed";
}
main(){
int i; char *s, *p;
for(i=0; i < NOFILE; i++ ){
s = typeOfOpen(i); p = type2OfOpen(i);
printf("%d:%s %s\n", i, s? s: "closed", p);
}
}
Константа NOFILE означает максимальное число одновременно открытых файлов для одного
процесса (это размер таблицы открытых процессом файлов, таблицы дескрипторов). Изу-
чите описание системного вызова fcntl (file control).
4.5. Напишите функцию rename() для переименования файла. Указание: используйте сис-
темные вызовы link() и unlink(). Ответ:
А. Богатырев, 1992-95 - 143 - Си в UNIX
rename( from, to )
char *from, /* старое имя */
*to; /* новое имя */
{
unlink( to ); /* удалить файл to */
if( link( from, to ) < 0 ) /* связать */
return (-1);
unlink( from ); /* стереть старое имя */
return 0; /* OK */
}
Вызов
link(существующее_имя, новое_имя);
создает файлу альтернативное имя - в UNIX файл может иметь несколько имен: так каждый
каталог имеет какое-то имя в родительском каталоге, а также имя "." в себе самом.
Каталог же, содержащий подкаталоги, имеет некоторое имя в своем родительском ката-
логе, имя "." в себе самом, и по одному имени ".." в каждом из своих подкаталогов.
Этот вызов будет неудачен, если файл новое_имя уже существует; а также если мы
попытаемся создать альтернативное имя в другой файловой системе. Вызов
unlink(имя_файла)
удаляет имя файла. Если файл больше не имеет имен - он уничтожается. Здесь есть одна
тонкость: рассмотрим фрагмент
int fd;
close(creat("/tmp/xyz", 0644)); /*Создать пустой файл*/
fd = open("/tmp/xyz", O_RDWR);
unlink("/tmp/xyz");
...
close(fd);
Первый оператор создает пустой файл. Затем мы открываем файл и уничтожаем его
единственное имя. Но поскольку есть программа, открывшая этот файл, он не удаляется
немедленно! Программа далее работает с безымянным файлом при помощи дескриптора fd.
Как только файл закрывается - он будет уничтожен системой (как не имеющий имен).
Такой трюк используется для создания временных рабочих файлов.
Файл можно удалить из каталога только в том случае, если данный каталог имеет
для вас код доступа "запись". Коды доступа самого файла при удалении не играют роли.
В современных версиях UNIX есть системный вызов rename, который делает то же
самое, что и написанная нами одноименная функция.
4.6. Существование альтернативных имен у файла позволяет нам решить некоторые проб-
лемы, которые могут возникнуть при использовании чужой программы, от которой нет
исходного текста (которую нельзя поправить). Пусть программа выдает некоторую инфор-
мацию в файл zz.out (и это имя жестко зафиксировано в ней, и не задается через аргу-
менты программы):
/* Эта программа компилируется в a.out */
main(){
int fd = creat("zz.out", 0644);
write(fd, "It's me\n", 8);
}
Мы же хотим получить вывод на терминал, а не в файл. Очевидно, мы должны сделать файл
zz.out синонимом устройства /dev/tty (см. конец этой главы). Это можно сделать коман-
дой ln:
$ rm zz.out ; ln /dev/tty zz.out
$ a.out
$ rm zz.out
или программно:
А. Богатырев, 1992-95 - 144 - Си в UNIX
/* Эта программа компилируется в start */
/* и вызывается вместо a.out */
#include <stdio.h>
main(){
unlink("zz.out");
link("/dev/tty", "zz.out");
if( !fork()){ execl("a.out", NULL); }
else wait(NULL);
unlink("zz.out");
}
(про fork, exec, wait смотри в главе про UNIX).
Еще один пример: программа a.out желает запустить программу /usr/bin/vi (смотри
про функцию system() сноску через несколько страниц):
main(){
... system("/usr/bin/vi xx.c"); ...
}
На вашей же машине редактор vi помещен в /usr/local/bin/vi. Тогда вы просто создаете
альтернативное имя этому редактору:
$ ln /usr/local/bin/vi /usr/bin/vi
Помните, что альтернативное имя файлу можно создать лишь в той же файловой системе,
где содержится исходное имя. В семействе BSD |- это ограничение можно обойти, создав
"символьную ссылку" вызовом
symlink(link_to_filename,link_file_name_to_be_created);
Символьная ссылка - это файл, содержащий имя другого файла (или каталога). Система
не производит автоматический подсчет числа таких ссылок, поэтому возможны "висячие"
ссылки - указывающие на уже удаленный файл. Прочесть содержимое файла-ссылки можно
системным вызовом
char linkbuf[ MAXPATHLEN + 1]; /* куда поместить ответ */
int len = readlink(pathname, linkbuf, sizeof linkbuf);
linkbuf[len] = '\0';
Системный вызов stat автоматически разыменовывает символьные ссылки и выдает информа-
цию про указуемый файл. Системный вызов lstat (аналог stat за исключением названия)
выдает информацию про саму ссылку (тип файла S_IFLNK). Коды доступа к ссылке не
имеют никакого значения для системы, существенны только коды доступа самого указуе-
мого файла.
Еще раз: символьные ссылки удобны для указания файлов и каталогов на другом
диске. Пусть у вас не помещается на диск каталог /opt/wawa. Вы можете разместить
каталог wawa на диске USR: /usr/wawa. После чего создать символьную ссылку из /opt:
ln -s /usr/wawa /opt/wawa
чтобы программы видели этот каталог под его прежним именем /opt/wawa.
Еще раз:
hard link
- то, что создается системным вызовом link, имеет тот же I-node (и