Перечислимый тип данных
Перечислимый тип данных предназначен для описания объектов из некоторого заданного множества. Он задается ключевым словом enum. Рассморим пример:
enum seasons (spring, summer, autumn, winter);Здесь введен новый тип данных seasons. Теперь можно определить переменные этого типа:
enum seasons а, b, с;Каждая из них (а, b, c) может принимать одно из четырех значений: spring, summer, autumn и winter. Эти переменные можно было определить сразу при описании типа:
enum seasons (spring, summer, autumn, winter) a, b, с;Рассмотрим еще один пример:
enum days {mon, tues, wed, thur, fri, sat, sun} my_week;Имена, занесенные в days (также как и в seasons в предыдущем примере), представляют собой константы целого типа. Первая из них (mon) автоматически устанавливается в нуль, и каждая следующая имеет значение на единицу больше, чем предыдущая (tues=1, wed=2 и т.д.).
Можно присвоить константам определенные значения целого типа (именам, не имеющим их, будут, как и раньше, назначены значения предыдущих констант, увеличенные на единицу). Например:
После этого mon=5, tues=8,wed=10, thur=11, fri=12, sat=13, sun=14.
Тип enum можно использовать для задания констант true=1 и false=0, например:
enum t_f (false, true) а, b;Файлы
Файлом называют способ хранения информации на физическом устройстве. Файл - это понятие, которое применимо ко всему - от файла на диске до терминала.
В языке Си отсутствуют операторы для работы с файлами. Все необходимые действия выполняются с помощью функций, включенных в стандартную библиотеку. Они позволяют работать с различными устройствами, такими, как диски, принтер, коммуникационные каналы и т.д. Эти устройства сильно отличаются друг от друга. Однако файловая система преобразует их в единое абстрактное логическое устройство, называемое потоком.
В Си существует два типа потоков: текстовые (text) и двоичные (binary).
Текстовый поток - это последовательность символов. При передаче символов из потока на экран, часть из них не выводится (например, символ возврата каретки, перевода строки).
Двоичный поток - это последовательность байтов, которые однозначно соответствуют тому, что находится на внешнем устройстве.
Прежде чем читать или записывать информацию в файл, он должен быть открыт и тем самым связан с потоком. Это можно сделать с помощью библиотечной функции fopen( ). Она берет внешнее представление файла (например, c:\my_prog.txt) и связывает его с внутренним логическим именем, которое используется далее в программе. Логическое имя - это указатель на требуемый файл. Его необходимо определить; делается это, например, так:
FILE *fp;Здесь FILE - имя типа, описанное в стандартном заголовочном файле stdio.h, fp - указатель на файл. Обращение к функции fopen( ) в программе осуществляется выражением:
fp = fopen(спецификация файла, "способ использования файла");Спецификация файла (т.е. имя файла и путь к нему) может, например, иметь вид: "c:\\my_prog.txt" - для файла my_prog.txt на диске с:.
Способ использования файла задается следующими символами:
r - открыть существующий файл для чтения;
w - создать новый файл для записи (если файл с указанным именем существует, то он будет переписан);
а - дополнить файл (открыть существующий файл для записи информации, начиная с конца файла, или создать файл, если он не существует);
r+ - открыть существующий файл для чтения и записи;
w+ - создать новый файл для чтения и записи;
a+ - дополнить или создать файл с возможностью чтения и записи;
rb - открыть двоичный файл для чтения;
wb - создать двоичный файл для записи;
аb - дополнить двоичный файл;
r+b - открыть двоичный файл для чтения и записи;
w+b - создать двоичный файл для чтения и записи;
а+b - дополнить двоичный файл с предоставлением возможности чтения и записи;
rt - открыть текстовой файл для чтения;
wt - создать текстовый файл для записи;
at - дополнить текстовый файл;
r+t - открыть текстовой файл для чтения и записи;
w+t - создать текстовый файл для чтения и записи;
a+t - дополнить текстовый файл с предоставлением возможности записи и чтения.
Если режим t или b не задан (например, r, w или а), то он определяется значением глобальной переменной _fmode. Если fmode=0_BINARY, то файлы открываются в двоичном режиме, а если _fmode=0_TEXT - в текстовом режиме. Константы 0_BINARY и 0_ТЕXТ определены в файле fcntl.h.
Строки вида r+b можно записывать и в другой форме: rb+.
Если в результате обращения к функции fopen( ) возникает ошибка, то она возвращает указатель на константу NULL.
Рекомендуется использовать следующий способ открытия файла:
if ((fp = fopen("c:\\my_prog.txt", "rt")) == NULL) { puts("Открыть файл не удалось\n"); exit(1); }После окончания работы с файлом он должен быть закрыт. Это делается с помощью библиотечной функции fclose( ). Она имеет следующий прототип:
int fclose(FILE *fp);При успешном завершении операции функция fclose( ) возвращает значение нуль. Любое другое значение свидетельствует об ошибке.
Рассмотрим другие библиотечные функции, используемые для работы с файлами (все они описаны в файле stdio.h):
1. Функция putc( ) записывает символ в файл и имеет следующий прототип:
int putc(int с, FILE *fp);Здесь fp - указатель на файл, возвращенный функцией fopen( ), с - символ для записи (переменная с имеет тип int, но используется только младший байт). При успешном завершении putc( ) возвращает записанный символ, в противном случае возвращается константа EOF. Она определена в файле stdio.h и имеет значение -1.
2. Функция getc( ) читает символ из файла и имеет следующий прототип:
int getc(FILE *fp);Здесь fp - указатель на файл, возвращенный функцией fopen( ). Эта функция возвращает прочитанный символ. Соответствующее значение имеет тип int, но старший байт равен нулю. Если достигнут конец файла, то getc( ) возвращает значение ЕОF.
3. Функция feof( ) определяет конец файла при чтении двоичных данных и имеет следующий прототип:
int feof(FILE *fp);Здесь fp - указатель на файл, возвращенный функцией fopen( ). При достижении конца файла возвращается ненулевое значение, в противном случае возвращается 0.
4. Функция fputs( ) записывает строку символов в файл. Она отличается от функции puts( ) только тем, что в качестве второго параметра должен быть записан указатель на переменную файлового типа.
Например:
fputs("Ехаmple", fp);При возникновении ошибки возвращается значение EOF.
5. Функция fgets( ) читает строку символов из файла. Она отличается от функции gets( ) тем, что в качестве второго параметра должно быть указано максимальное число вводимых символов плюс единица, а в качестве третьего - указатель на переменную файлового типа. Строка считывается целиком, если ее длина не превышает указанного числа символов, в противном случае функция возвращает только заданное число символов.
Рассмотрим пример:
fgets(string, n, fp);Функция возвращает указатель на строку string при успешном завершении и константу NULL в случае ошибки либо достижения конца файла.
6. Функция fprintf( ) выполняет те же действия, что и функция printf( ), но работает с файлом. Ее отличием является то, что в качестве первого параметра задается указатель на переменную файлового типа.
Например:
fprintf(fp, "%х",а);7. Функция fscanf( ) выполняет те же действия, что и функция scanf(), но работает с файлом. Ее отличием является то, что в качестве первого параметра задается указатель на переменную файлового типа.
Например:
fscanf(fp, "%х", &a);При достижении конца файла возвращается значение EOF.
8. Функция fseek( ) позволяет выполнять чтение и запись с произвольным доступом и имеет следующий прототип:
int fseek(FILE *fp, long count, int access);Здесь fp - указатель на файл, возвращенный функцией fopen( ), count - номер байта относительно заданной начальной позиции, начиная с которого будет выполняться операция, access - способ задания начальной позиции.
Переменная access может принимать следующие значения:
0 - начальная позиция задана в начале файла;
1 - начальная позиция считается текущей;
2 - начальная позиция задана в конце файла.
При успешном завершении возвращается нуль, при ошибке - ненулевое значение.
9. Функция ferror( ) позволяет проверить правильность выполнения последней операции при работе с файлами. Имеет следующий прототип:
int ferror(FILE *fp);В случае ошибки возвращается ненулевое значение, в противном случае возвращается нуль.
10. Функция remove( ) удаляет файл и имеет следующий прототип:
int remove(char *file_name);Здесь file_name - указатель на строку со спецификацией файла. При успешном завершении возвращается нуль, в противном случае возвращается ненулевое значение.
11. Функция rewind( ) устанавливает указатель текущей позиции в начало файла и имеет следующий прототип:
void rewind(FILE *fp);12. Функция fread( ) предназначена для чтения блоков данных из потока. Имеет прототип:
unsigned fread(void *ptr, unsigned size, unsigned n, FILE *fp);Она читает n элементов данных, длиной size байт каждый, из заданного входного потока fp в блок, на который указывает указатель ptr. Общее число прочитанных байтов равно произведению n*size. При успешном завершении функция fread( ) возвращает число прочитанных элементов данных, при ошибке - 0.
13. Функция fwrite( ) предназначена для записи в файл блоков данных. Имеет прототип:
unsigned fwrite(void *ptr, unsigned size, unsigned n, FILE *fp);Она добавляет n элементов данных, длиной size байт каждый, в заданный выходной файл fp. Данные записываются с позиции, на которую указывает указатель ptr. При успешном завершении операции функция fwrite( ) возвращает число записанных элементов данных, при ошибке - неверное число элементов данных.
В языке Си имеются пять стандартных файлов со следующими логическими именами:
stdin - для ввода данных из стандартного входного потока (по умолчанию - c клавиатуры);
stdout - для вывода данных в стандартный выходной поток (по умолчанию - на экран дисплея);
stderr - файл для вывода сообщений об ошибках (всегда связан с экраном дисплея);
stdprn - для вывода данных на принтер;
stdaus - для ввода и вывода данных в коммуникационный канал.
В языке Си имеется также система низкоуровневого ввода/вывода (без буферизации и форматирования данных), соответствующая стандарту системы UNIX. Прототипы составляющих ее функций находятся в файле io.h. К этим функциям относятся:
open( ) - открыть файл;
close( ) - закрыть файл;
read( ) - читать данные;
write( ) - записать данные;
lseek( ) - поиск определенного байта в файле;
unlink( ) - уничтожить файл.
РАЗДЕЛ 4. ФУНКЦИИ
Общие сведения
Программы на языке Си обычно состоят из большого числа отдельных функций (подпрограмм). Как правило, эти функции имеют небольшие размеры и могут находиться как в одном, так и в нескольких файлах. Все функции являются глобальными. В языке запрещено определять одну функцию внутри другой. Связь между функциями осуществляется через аргументы, возвращаемые значения и внешние переменные.
В общем случае функции в языке Си необходимо объявлять. Объявление функции (т.е. описание заголовка) должно предшествовать ее использованию, а определение функции (т.е. полное описание) может быть помещено как после тела программы (т.е. функции main( )), так и до него. Если функция определена до тела программы, а также до ее вызовов из определений других функций, то объявление может отсутствовать. Как уже отмечалось, описание заголовка функции обычно называют прототипом функции.
Функция объявляется следующим образом:
тип имя_функции(тип имя_параметра_1, тип имя_параметра_2, ...);Тип функции определяет тип значения, которое возвращает функция. Если тип не указан, то предполагается, что функция возвращает целое значение (int).
При объявлении функции для каждого ее параметра можно указать только его тип (например: тип функция (int, float, ...), а можно дать и его имя (например: тип функция (int а, float b, ...) ).
В языке Си разрешается создавать функции с переменным числом параметров. Тогда при задании прототипа вместо последнего из них указывается многоточие.
Определение функции имеет следующий вид:
тип имя_функции(тип имя_параметра_1, тип имя_параметра_2,...) { тело функции }Передача значения из вызванной функции в вызвавшую происходит с помощью оператора возврата return, который записывается следующим образом:
return выражение;Таких операторов в подпрограмме может быть несколько, и тогда они фиксируют соответствующие точки выхода. Например:
int f(int a, int b) { if (a > b) { printf("max = %d\n", a); return a; } printf("max = %d\n", b); return b; }Вызвать эту функцию можно следующим образом:
c = f(15, 5); c = f(d, g); f(d, g);Вызвавшая функция может, при необходимости, игнорировать возвращаемое значение. После слова return можно ничего не записывать; в этом случае вызвавшей функции никакого значения не передается. Управление передается вызвавшей функции и в случае выхода "по концу" (последняя закрывающая фигурная скобка).
В языке Си аргументы функции передаются по значению, т.е. вызванная функция получает свою временную копию каждого аргумента, а не его адрес. Это означает, что вызванная функция не может изменить значение переменной вызвавшей ее программы. Однако это легко сделать, если передавать в функцию не переменные, а их адреса. Например:
void swap(int *a, int *b) { int *tmp = *a; *a = *b; *b = *tmp; }Вызов swap(&b, &c) (здесь подпрограмме передаются адреса переменных b и с) приведет к тому, что значения переменных b и c поменяются местами.
Если же в качестве аргумента функции используется имя массива, то передается только адрес начала массива, а сами элементы не копируются. Функция может изменять элементы массива, сдвигаясь (индексированием) от его начала.
Рассмотрим, как функции можно передать массив в виде параметра. Здесь возможны три варианта:
- Параметр задается как массив (например: int m[100];).
- Параметр задается как массив без указания его размерности (например: int m[];).
- Параметр задается как указатель (например: int *m;). Этот вариант используется наиболее часто.
Независимо от выбранного варианта вызванной функции передается указатель на начало массива. Сами же элементы массива не копируются.
Если некоторые переменные, константы, массивы, структуры объявлены как глобальные, то их не надо включать в список параметров вызванной функции.
Классы памяти
В языке Си различают четыре основных класса памяти: внешнюю (глобальную), автоматическую (локальную), статическую и регистровую память.
Внешние (глобальные) переменные определены вне функций и, следовательно, доступны для любой из них. Они могут быть определены только один раз. Выше уже говорилось, что сами функции всегда глобальные. Язык не позволяет определять одни функции внутри других. Область действия внешней переменной простирается от точки во входном файле, где она объявлена, до конца файла. Если на внешнюю переменную нужно ссылаться до ее определения или она определена в другом входном файле, то в подпрограмме или файле она должна быть объявлена как extern.
Например:
extern int a; /* Объявление a; память под переменную не резервируется */Автоматические переменные по отношению к функциям являются внутренними или локальными. Они начинают существовать при входе в функцию и уничтожаются при выходе из нее (для них можно использовать ключевое слово auto). Однако оно практически не используется, так как при отсутствии ключевого слова переменные по умолчанию принадлежат к классу auto.
Статические переменные объявляются с помощью ключевого слова static. Они могут быть внутренними (локальными) или внешними (глобальными). Внутренние статические переменные, как и автоматические, локальны по отношению к отдельной функции. Однако они продолжают существовать, а не возникают и не уничтожаются при каждом ее вызове. Другими словами, они являются собственной постоянной памятью для функции. Внешние статические переменные доступны внутри оставшейся части файла после того, как они в нем объявлены, однако в других файлах они неизвестны. Это, в частности, позволяет скрыть данные одного файла от другого файла.
Регистровые переменные относятся к последнему классу. Ключевое слово register говорит о том, что переменная, о которой идет речь, будет интенсивно использоваться. Если возможно, значения таких переменных помещаются во внутренние регистры микропроцессора, что может привести к более быстрой и короткой программе (разработчики компиляторов фирмы Borland утверждают, что оптимизация компиляторов данной фирмы по использованию регистровых переменных сделана так хорошо, что указание использовать переменную как регистровую может только снизить эффективность создаваемого машинного кода). Для регистровых переменных нельзя взять адрес; они могут быть только автоматическими с допустимыми типами int или char.
Таким образом, можно выделить четыре модификатора класса памяти: extern, auto, static, register. Они используются в следующей общей форме:
модификатор_класса_памяти тип список_переменных;Выше уже говорилось об инициализации, т.е. о присвоении различным объектам начальных значений. Если явная инициализация отсутствует, гарантируется, что внешние и статические переменные будут иметь значение нуль, а автоматические и регистровые - неопределенное значение.
Указатели на функции
В языке Си сама функция не может быть значением переменной, но можно определить указатель на функцию. С ним уже можно обращаться как с переменной: передавать его другим функциям, помещать в массивы и т.п.
Код функции в персональном компьютере занимает физическую память. В этой памяти есть точка входа, которая используется для того, чтобы войти в функцию и запустить ее на выполнение. Указатель на функцию как раз и адресует эту точку входа. Это уже будет обычная переменная и с ней можно делать все, что можно делать с переменной.
Через указатель можно войти в функцию, т.е. запустить ее на выполнение. Объявление вида:
int (*f)( );говорит о том, что f - это указатель на функцию, возвращающую целое значение. Первая пара скобок необходима, без них int *f( ); означало бы, что f - функция, возвращающая указатель на целое значение. После объявления указателя на функцию в программе можно использовать объекты: *f - сама функция; f - указатель на функцию. Для любой функции ее имя (без скобок и аргументов) является указателем на эту функцию.