Присваивание указателей друг другу 5 страница

}

else

printf("Входной файл открыт \n");

fclose(in);

}

10.3. Функции ввода-вывода для работы
с текстовыми файлами

Для работы с текстовыми файлами в библиотеке языка Си содержится множество удобных функций, далее будут рассмотрены самые распространенные: fprintf(), fscanf(), fgets(), fputs(), fgetc(), fputc(). Формат параметров этих функций очень похож на формат функций printf(), scanf(), gets(), puts (), getc() и putc().

Схожи не только параметры, но и действия. Отличие лишь в том, что printf(), scanf() и другие работают по умолчанию с консолью (экран, клавиатура), а fprintf(), fscanf() — с файлами, поэтому у них добавлен параметр, являющийся указателем на структуру FILE, которая была рассмотрена выше.

Функции для работы с текстовыми файлами удобны для создания текстовых файлов, ведения файлов-протоколов (log-файлов) и т.п.

Прототипы вышеперечисленных функций:

int fputs(const char *cmp, FILE *file);

cmp – указатель на строку;

file – указатель на файл;

fputs() пишет в файл, указанный в file, строку, на которую указывает cmp. Возвращает код последнего записанного в файл символа. В случае возникновения ошибки – возвращается значение EOF.

int fputс(char ch, FILE *file);

ch – символ;

file – указатель на файл;

fputc() пишет в файл, указанный в file, символ ch.

Возвращаемое значение – записанный символ.

В случае возникновения ошибки – возвращается значение EOF.

char *fgets(char *cmp, int length, FILE *file);

cmp – указатель на строку;

file – указатель на файл;

Функция fgets() читаетиз файла file строку, и делает это до тех пор, пока не будет прочитан символ новой строки или количество прочитанных символов не будет равно length -1 либо достигнут конец файла.

При успешном выполнении функция возвращает cmp, а в случае ошибки – пустой указатель (NULL).

int fgetс(FILE *file);

file – указатель на файл;

fgetc() возвращает символ прочитанный из файла file, находящийся после текущей позиции в файле.

В случае возникновения ошибки, либо по достижении конца файла – возвращается значение EOF.

Функцию fgetс() можно использовать при работе с текстовыми файлами для прочтения символов до конца файла.

Для этого нужно написать следующий код в теле программы

do

{

ch = fgetc(f); // f-указатель на файл;

}

while (ch!= EOF);

Однако при ошибке функция fgetc() также возвращает EOF, следовательно она не очень удобна в работе. Для проверки того, произошла ли ошибка нужно использовать функцию ferror().

Еще одним важным недостатком такого определения конца файла является невозможность работать с бинарными файлами.

Физически это, конечно, возможно, однако при работе с двоичным файлом может быть прочитано целое значение равное EOF.

Поэтому во многих случаях удобнее использовать функцию feof (), описание которой приведено ниже.

Ниже приведен пример программы записи строковых данных в текстовый файл.

#include <stdio.h>

#include <conio.h> // заголовочный файл для работы функции

// очистки экрана (clrscr())

void main(void)

{

char s[25];

FILE *out;

clrscr();

out = fopen("d:\\Ex2.txt", "wt");

for (int i = 0; i < 3; i++)

{

gets(s);

fprintf(out, "%s \n", s);

}

getch();

fclose(out); // открытый файл закрывается

puts("Reading from file");

// тот же файл открывается для чтения

out = fopen("d:\\Ex2.txt", "rb");

for (i = 0; i < 3; i++)

{

fscanf(out, "%s", s);

printf("%s \n", s);

}

fclose(out);

getch();

}

В программе поочередно считываются и заносятся в файл три строки.

Если открыть созданный файл (d:\ex2.txt), к примеру редактором WORDPAD, то мы увидим, что данные располагаются точно так же, как мы видели их на экране.

10.4. Произвольный доступ к файлу

Каждый открытый файл имеет так называемый указатель на текущую позицию в файле. Все операции над файлами (чтение и запись) работают с данными с этой позиции. При открытии файла указатель текущей позиции в файле устанавливается на начало файла (кроме режимов ‘a’ и ‘a+’).

При каждом выполнении чтения или записи, указатель смещается на количество прочитанных или записанных байт, то есть устанавливается сразу за прочитанным или записанным блоком данных в файле. Это так называемый последовательный доступ к данным.

Возьмем, к примеру, какой-нибудь текстовый файл.

...

char ch;

...

К примеру, мы вызвали функцию fscanf(f,“%c”, &ch);

f – указатель на файл, открытый функцией fopen();

После этого указатель сместится на sizeof(ch), т.е. на 1 байт.

Последовательный доступ очень удобен, он демонстрируется во всех вышеприведенных примерах чтения и записи в файл.

Но иногда необходимо читать или писать данные в произвольном порядке. Это достигается путем установки указателя на некоторую заданную позицию в файле функцией fseek().

int fseek(FILE *stream, long offset, int whence);

Параметр offset задает количество байт, на которое необходимо сместить указатель в направлении, указанном whence в файле stream.

В таблице 10.2 приведены возможные значения параметра whence:

Таблица 10.2. Значения параметра whence

SEEK_SET   Смещение от начала файла
SEEK_CUR   Смещение от текущей позиции
SEEK_END   Смещение от конца файла

Величина смещения может быть как положительной, так отрицательной, но нельзя сместиться за пределы начала файла.

...

fseek(f, 8, SEEK_SET); // смещает указатель на текущую

// позицию в файле на восемь байт от

// его текущего положения

Очень часто приходится сталкиваться с необходимостью узнать, где сейчас находится указатель на позицию в файле. Для этого в библиотеке Си содержится ряд функций. Ограничимся рассмотрением одной из них

long int ftell(FILE *file);

file – указатель на файл, возвращенный функцией fopen().

Функция ftell() возвращает текущее положение указателя файла в байтах относительно начала файла. В случае ошибки возвращается значение -1L.

#include <stdio.h>

// Функция возвращает число байтов в файле

long filesize(FILE *f1)

{

long pos, length;

pos = ftell(f1);

fseek(f1,0L,SEEK_END);

length = ftell(f1);

fseek(f1,pos,SEEK_SET);

return (length);

}

int main()

{

FILE *f;

f = fopen("D:\\main.cpp","r");

printf("%ld",filesize(f));

fclose(f);

return 0;

}

Также часто возникает потребность в проверке достижения конца файла.

Для этого используется функция feof().

Прототип:

int feof(FILE *file);

file – указатель на файл, открытый функцией fopen();

Если достигнут конец файла, то функция возвращает ненулевое значение, иначе – нуль.

Следующая программа осуществляет запрос у пользователя номера структуры, которую необходимо вывести, смещается в файле до этой структуры, считывает ее и выводит на экран.

#include <stdio.h>

#include <conio.h>

void main(void)

{

struct man

{

char im[10];

char fam[10];

char ot[10];

int vozr;

};

FILE *out;

man m;

int c;

clrscr();

if (!(out = fopen("d:\\Ex2_bin.txt", "rb"))) return;

//открывается файл со структурами, созданными в примере 4

do

{

clrscr();

puts("Vvedite nomer zapisi");

if ((c = getch())!= '0') // считывается номер структуры

if ((c > '0') && (c <= '3')) // т.к у нас было создано

// только 3 структуры

{

c = (c - '0') - 1;

puts("Reading from file");

// смещаемся на кол-во байт, равное размеру одной

// структуры, умноженный на с

fseek(out, sizeof(m) * c, 0);

fread(&m, sizeof(m), 1, out); // считываем структуру

//выводим структуру

printf("%s %s %s, vozrast (let) %i \n",

m.im, m.fam, m.ot, m.vozr);

getch();

}

}

while (c!= '0'); // 0 – признак выхода

fclose(out);

}

10.5. Функции ввода-вывода
для работы с бинарными файлами

Для создания, например, баз данных удобнее и целесообразнее будет использовать не текстовый файл, а бинарный. В этом случае у нас появляется возможность считывать всю структуру за один раз, а также имеется возможность доступа к произвольной структуре.

Для работы с бинарными файлами нужно использовать функции: fwrite () и fread ().

unsigned fwrite(void *ptr,

unsigned size,

unsigned n,

FILE *stream);

Здесь ptr – адрес в памяти, начиная с которого будет прочитано size байт n раз и записано в файл stream. Возвращает количество записанных элементов.

unsigned fread(void *ptr,

unsigned size,

unsigned n,

FILE *stream);

Здесь ptr – адрес в памяти, начиная с которого будет записано size байт n раз из файла stream. Возвращает количество прочитанных элементов.

Эти функции без каких-либо изменений копируют блок данных из оперативной памяти в файл и из файла в память соответственно. По этой причине fread() и fwrite() работают быстрее, чем функции чтения и записи для текстовых файлов.

В нижеприведенной программе создается структура, состоящая из четырех полей, которые описывают имя, фамилию, отчество и возраст человека. Данные запрашиваются три раза, и каждый раз структура заносится в бинарный файл.

#include <stdio.h>

#include <conio.h>

void main(void)

{

struct man // Создаваемая стуктура

{

char im[10]; // Имя

char fam[10]; // Фамилия

char ot[10]; // Отчество

int vozr; // Возраст

};

FILE *out;

man m;

clrscr();

out = fopen("d:\\Ex2_bin.txt", "wb");

for (int i = 0; i < 3; i++)

{

puts("Enter Imia");

scanf("%s", m.im); //Считывается имя

puts("Enter Familiu");

scanf("%s", m.fam); //Считывается фамилия

puts("Enter Otchestvo");

scanf("%s", m.ot); //Считывается отчество

puts("Enter Vozrast");

scanf("%i", &m.vozr); //Считывается возраст

// Осуществляется запись в файл

fwrite(&m, sizeof(m), 1, out);

}

getch();

fclose(out);

clrscr();

puts("Reading from file");

// файл открывается для чтения

out = fopen("d:\\Ex2_bin.txt", "rb");

for (i = 0; i < 3; i++)

{

// считывается кол-во байт, занимаемых одной структурой

fread(&m, sizeof(m), 1, out);

//вывод на экран считанной структуры

printf("%s %s %s, vozrast (let) %i \n",

m.im, m.fam, m.ot, m.vozr);

}

fclose(out);

getch();

}

Все операции чтения/записи производятся через внутренний буфер в оперативной памяти (ОП).

Может возникнуть такая ситуация, что данные, содержащиеся в буфере, могут быть утеряны и, соответственно, не записаны в файл.

Для принудительной записи содержимого буфера в файл используется функция fflush().

int fflush(FILE *file);

fflush() записывает содержимое буфера в файл file, файл при этом остается открытым. Возвращает ноль при успешном выполнении; ненулевое значение – при ошибке.

Если программа завершается нормально, то содержимое буфера дозаписывается автоматически.

Полезно знать, что текстовый файл, в принципе, не отличается от бинарного, поэтому любой бинарный файл может быть открыт для работы с ним, как с текстовым; а текстовый может быть открыт как бинарный. Но такой способ работы с файлом может приводить к ошибкам. Поэтому рекомендуется работать с конкретным файлом в том режиме, в котором он был создан.

11. Директивы препроцессора

11.1. Основные понятия

Препроцессор (макропроцессор) – это программный модуль, обрабатывающий исходный текст программы до того, как он пройдёт через компилятор. Языка программирования Си предполагает, что любой транслятор с этого языка содержит препроцессор.

Препроцессорная обработка (макрообработка) — это преобразование текста путем замены препроцессорных переменных их значениями и выполнения препроцессорных операторов (директив препроцессора).

Директива препроцессора – это любая строка, начинающаяся с символа # (до символа # в строке могут содержаться только пробелы). Синтаксис этих строк не связан с остальным языком: они могут появляться в любом месте и их влияние распространяется (независимо от области действия) до конца исходного программного файла.

Макрообработка состоит в последовательном просмотре (сканировании) исходного текста и выделении в нем лексем (макроопределении). Если выделенная лексема является препроцессорной переменной, она заменяется на свое значение – это называется макрорасширением. Если встречается препроцессорная директива, то она выполняется. Лексемы, не являющиеся препроцессорными переменными или директивами, переносятся в выходной текст без изменения. Результатом такой обработки является текст, не содержащий препроцессорных директив и препроцессорных переменных. После макрообработки должен быть получен синтаксически правильный текст на Си.

Препроцессор позволяет выполнить следующие действия:

a. включить в компилируемый файл другие файлы;

b. определить символические константы и макросы;

c. задать режим условной компиляции программного кода и условного выполнения директив препроцессора;

d. задавать номера строк;

e. выводить сообщения об ошибках;

f. использовать дополнительные команды при помощи директивы #pragma. Количество и функции команд зависят от установленного пакета Си.

Фактически препроцессор расширяет возможности языка Си, реализуя такие функции, которые в других языках входят в состав самого языка.

Директивы препроцессора можно условно разделить на следующие группы:

a. Включение файлов ( #include );

b. Определения символических констант и макросов ( #define, #undef );

c. Условная компиляция ( #if, #endif, #ifdef, #ifndef, #else, #elif);

d. Прочие директивы ( #error, #line, #pragma );

e. Операции препроцессора # и ##.

11.2. Директива #include

Директива #include применяется для включения копии указанного в директиве файла в том месте, где находится эта директива.

Существует 3 формы директивы #include:

a. #include <имя_файла>

b. #include "имя_файла"

c. #include идентификатор_макроса

Последняя форма предполагает, что макрос, идентификатор которого используется в этой форме директивы, предварительно определен и использует одну из первых двух форм директивы #include.

Различие между первыми двумя формами директивы заключается в методе поиска препроцессором включаемого файла. Если имя файла заключено в угловые скобки (< >), как это делается для включения заголовочных файлов стандартной библиотеки, то последовательность поиска препроцессором заданного файла в каталогах определяется заданными каталогами включения (include directories. В Borland C++ 3.1 установить данную директорию можно через меню: Options->Directories).

Если же имя файла заключено в кавычки, препроцессор ищет файл в директории, указанной (для Borland C++ 3.1) через меню: File->Change dir. Если же указан полный путь к файлу, то поиск производится по указанному адресу.

Обработка директивы #include препроцессором сводится к тому, что директива убирается из текста и на ее место заносится копия указанного файла.

Директива #include обычно используется для включения стандартных заголовочных файлов библиотек и для включения заголовочных файлов в файлы их реализации. Директива #include используется также при работе с программами, состоящими из нескольких исходных файлов, которые должны компилироваться вместе.

Рассмотрим примеры директив.

Следующая директива включает файл stdio.h, который ищется в стандартном каталоге включаемых файлов:

#include <stdio.h>

Следующая директива включает пользовательскую библиотеку MyBibl.h, расположенную в каталоге, указанном через меню File->Change dir:

#include "MyBibl.h"

Следующие директивы включают файл C:\Test\my.h, который ищется в каталоге C:\Test:

#define myincl "C:\Test\my.h"

#include myincl

Замечание: Включения с помощью #include могут быть вложенными.

11.3. Директивы препроцессора #define и #undef

11.3.1. Символические константы

Директива препроцессора #defineсоздает символические константы или макросы без параметров, обозначаемые идентификаторами, и макросы – операции, обозначаемые символьными строками. Формат директивы препроцессора #define при объявлении символической константы:

#define идентификатор_константы замещающий_текст

Приведенная форма директивы создает макрос без параметров, называемый обычно символической константой. После появления этой строки в файле все встретившиеся далее в тексте программы имена, совпавшие с элементом директивы идентификатор_константы, будут автоматически заменены на указанный в директиве замещающий_текст прежде, чем начнется компиляция программы. Например, после задания директивы

#define PI 3.14159

все последующие вхождения в текст программы символической константы PI будут заменены на численную константу 3.14159. Замена идентификатора константы не производится в комментариях и строках символов. Если замещающий текст в директиве не задан, то во всем тексте идентификаторы константы просто стираются. После замены всех символических констант их значениями исходный текст программы опять просматривается препроцессором в поисках необходимости новых замен. Таким образом, можно использовать вложенные определения символических констант.

Если замещающий текст сравнительно длинный, его можно перенести на следующую строку, введя символ обратного слеша "\". Например, при запуске программы:

#define DIGIT 2\

...

printf(“\n%d”, DIGIT);

на экран будет выведено: 23

Замечание: если значение символической константы велико и его часть требуется перенести на следующую строку, то при разрыве этого значения между знаком ”\” и символом на следующей строке не должно быть непредусмотренных пробелов.

Предупреждение: в конце директив препроцессора не ставится точка с запятой.

Например, если создать следующую символическую константу:

#define LENGTH 12;

а затем попробовать её применить при объявлении массива,

int array[LENGTH];

то, расширяясь, выражение будет выглядеть как

int array[12;]

что синтаксически неправильно.

Символические константы дают возможность программисту присвоить константе имя и использовать ее далее в программе. Если возникнет необходимость изменить значение константы во всей программе, для этого достаточно будет внести только одно изменение в директиву препроцессора #define и перекомпилировать программу; значение константы будет изменено по всей программе автоматически.

Предупреждение: Учтите, что все, что находится справа от идентификатора символической константы, является замещающим ее текстом. Например, после выполнения директивы #define PI =3.14159, препроцессор заменит все имена PI на текст =3.14159.

Возможен другой метод использования именованных констант. Он заключается в использовании ключевого слова const. Например:

const int PI=3.14159;

Константные переменные являются данными определенного типа и их имена видны отладчику. А если используется символическая константа, то после того, как символическая константа была заменена на соответствующий текст, только этот текст и будет виден отладчику. Следовательно, символическую константу нельзя будет просматривать с помощью окна отладчика просмотра значений переменных watch.

Ниже приведены примеры определения с помощью директивы#define символических констант:

// определение строки текста:

#define ANYKEY "Нажмите любую клавишу"

// идентификатор DELETE в тексте просто удалится:

#define DELETE

// определение директивы #include:

#define GETSTD #include <stdio.h>

Хороший стиль программирования: для наглядности программного кода разумно использовать прописные (большие) буквы для именованных констант, чтобы отличать их от переменных Си.

Итак, какие преимущества в применении символических констант:

1. при использовании символических констант программы становятся более удобочитаемыми (определенному числу или строке ставиться в соответствие осмысленное название);

2. именованные константы позволяют легко модифицировать программы, заменяя значение константы только в одном месте (где она определена), нежели в каждом выражении, где она встречается.

3. символические константы не требуют выделения памяти.

Недостаток: символические константы не существуют как переменные, поэтому их нельзя просматривать при помощи отладчика.

11.3.2. Макросы с параметрами

Формат директивы #define, определяющей макрос с параметрами:

#define идентификатор_макроса(параметры) замещающий_текст

Предупреждение: между идентификатором макроса и открывающейся скобкой не должно быть пробела.

Вызов макроса осуществляется выражением:

идентификатор_макроса(параметры)

Макрос, определяемый директивой препроцессора #define, это символическое имя некоторого набора операторов. Как и в случае символических констант, идентификатор макроса заменяется на замещающий текст до начала компиляции программы. Но сначала в замещающий текст подставляются значения параметров, а затем уже этот расширенный макрос подставляется в текст вместо идентификатора макроса и списка его параметров.

Например, следующий макрос с одним параметром определяет площадь круга, воспринимая передаваемый в него параметр как радиус:

#define CIRC(x) (3.14 * (x) *(x))

Везде в тексте файла, где появится идентификатор CIRC(A), значение параметра A будет использовано для замещения. Например, оператор с макросом в тексте программы

S = CIRC(4);

примет вид:

S = (3.14 * (4) * (4));

Поскольку это выражение состоит только из констант, его значение будет вычислено во время компиляции и полученный результат будет присвоен переменной S во время выполнения программы.

Если вызов имеет вид

S = CIRC(a + b);

то после расширения макроса тест будет иметь вид:

S = (3.14 * (a + b) * (a + b));

В данном случае параметр макроса является выражением, содержащим переменные a и b. Поэтому вычисления будут осуществляться не во время компиляции, а во время выполнения программы.

Следует обратить внимание на круглые скобки вокруг каждого включения параметра x в тексте рассмотренного макроса и вокруг всего выражения. При вызове типа CIRC(4) они кажутся излишними. Но во втором примере вызова при отсутствии скобок расширение привело бы к оператору:

S = 3.14 * a + b * a + b;

Тогда в соответствии со старшинством операций сначала выполнилось бы умножение 3.14 * a, затем b * a, а затем результаты этих умножений сложились бы друг с другом и с b. Конечно, результат вычислений был бы неверным.

Хороший стиль программирования: При объявлении макроса заключите в скобки параметры в замещающем тексте и сам замещающий текст. Это избавит от возможных неприятностей, связанных с неверной последовательностью вычислений при расширении макроса.

Пример_1: макрос, определяющий площадь эллипса через значения его полуосей может быть определен директивой

#define Ell(x, y) (3.14 * (x) * (y))

Вызов этого макроса может иметь вид:

S = Ell(R1, R2);

С точки зрения получаемых результатов вычислений макросы эквивалентны функциям. Например, вычисление площади круга можно было бы оформить функцией:

double circ(double x)

{

return 3.14 * x * x;

}

и вызвать ее оператором:

S = circ(a + b);

Пример_2: следующим образом можно создать элемент связанного списка, используя#define:

#define PTRSTRUCT(name) typedef struct name *name##Ptr

#define STRUCT(name) struct name \

{ \

int value; \

struct name *next; \

}; \

PTRSTRUCT(name)

и работать с ним:

STRUCT(MyStruct);

MyStruct My;

MyStructPtr MyPtr = &My;

My.value = 5;

printf(“\n%d”, My.value);

printf(“\n%d”, (*MyPtr).value);

При этом на экран будет выведено:

Пример_3: использование макросов вместо стандартных функций:

#define READ(val) scanf("%d", &val)

При использовании в тексте программы READ(y);, макрос расширится до scanf("%d",&y);.

Таким образом, возникает вопрос, что выгоднее использовать: макросы или функции?

Вызов функции сопряжен с дополнительными расходами и увеличивает время выполнения программы (если программа использует функцию, то в выполняемую программу помещается только одна копия функции. Каждый раз, при вызове функции, программа помещает параметры в стек и затем выполняет переход к коду функции. После завершения функции, программа удаляет параметры из стека и переходит обратно к оператору, который следует непосредственно за вызовом функции.). Это соображение работает в пользу использования макросов. С другой стороны, макрос расширяется во всех местах текста, где используется его вызов. Если таких мест в программе много, то это увеличивает размер текста и, соответственно размер выполняемого модуля. Так что функции позволяют сократить объем выполняемого файла, а макросы – увеличить скорость выполнения. Правда, макросы тоже могут быть связаны с дополнительными накладными расходами. В приведенном примере “с вычислением площади круга” значение параметра (a + b) вычисляется дважды, в то время, как в функции это вычисление осуществляется только один раз. Конечно, для таких простых вычислений это не существенно. Но если в качестве параметра передается сложное выражение, обращающееся в свою очередь к каким-нибудь сложным функциям, то эти дополнительные накладные расходы могут стать заметными и увеличить вычисления.

Недостатком макросов является отсутствие встроенного контроля согласования типов фактических и формальных параметров. Отсутствие соответствующих предупреждений компилятора может приводить к ошибкам программы, которые трудно отлавливать. Однако это является иногда и преимуществом. Например, если необходимо создать “универсальную функцию”, подобную шаблонным функциям в C++. Опишем пример работы подобной “универсальной функции”. Создаем макрос – суммирование двух элементов:


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: