Обмен данными в Cи между оперативной и внешней памятью. С точки зрения ОС минимальная единица информации – файл. Файл – именованная область памяти, которая характеризуется: - именем; - типом (расширением); - размером; - временем создания (модификации). В ОС с файлами операции: - создание; - чтение; - удаление; - переименование; - копирование. допустимы следующие ОС умеет работать с файлом в целом. Файл в Cи – это просто поток ввода-вывода и идентифицируется указателем на переменную базового типа FILE. typedef struct { short level; /* уровень буфера ввода-вывода (заполнен / пуст) */ unsigned flags; /* флаг статуса */ char fd; /* дескриптор файла */ unsigned char hold; /* символ который возвращается в буфер */ short bsize; /* размер буфера в байтах */ unsigned char *buffer; /* указатель на начало буфера */ unsigned char *curp; /* текущий указатель в буфере */ unsigned istemp; /* индикатор временного файла */ short token; /* используется для проверки файла */ } FILE; Различают два уровня ввода-вывода: - физический; - логический. Физический уровень ввода-вывода обеспечивает перемещение информации между внешней памятью и буфером ввода-вывода. На логическом уровне ввода-вывода перемещение информации выполняется между буфером ввода-вывода и переменными программы. 1.txt Оперативная память Физический уровень Буфер ввода-вывода Логический уровень char name[80] … В Cи можно работать с файлами 2-х типов: - текстовыми (последовательность символов, разбитая на строки символом ‘\n’); - бинарными (двоичными) (внутреннее представление этого файла – цепочка нулей и единиц). Текстовые файлы мобильны. Они содержат в своих байтах коды символов, поэтому могут быть прочитаны в любом текстовом редакторе. У файлов различают 2 метода доступа: - прямой; - последовательный. В последовательных файлах элемент с номером n может быть обработан только после обработки элементов с номером n-1 (системный ввод и системный вывод). В файлах прямого доступа любой элемент доступен по своему «номеру». Все действия поддерживаются stdio.h. связанные с файлами стандартной библиотекой В stdio.h определены (буферы, потоки): - stdin – ввода; - stdout – вывода; - stderr – ошибки. стандартные файлы Открытие файла: fopen(p1,p2); p1: строка = имя файла. p2: строка = способ открытия файла: “r’’ – для чтения (read); “w’’ – для записи (write); “a’’ – для добавления (append); “r+w’’ (или “r+’’) - для чтения и записи; “w+’’ – создать пустой файл для чтения и записи; “a+’’ – для чтения и добавления данных. В языке Си нет функции создания файла. Эту работу выполняет функция fopen(). “r”, “r+” – открытие существующего файла. “w”, “w+”, “a” или “a+” – открытие существующего, если он был, или создание нового, если такого файла не было. “a+’’ – все операции записи выполняются в конец файла, позицию для чтения можно изменить с помощью FSEEK). При открытии “w” или “w+” существующего файла: запись в начало (создание нового с удалением старой версии). Функция fopen() возвращает: - указатель на файл, с помощью которого дальше можно ссылаться на этот файл. - NULL, если файл не открыт. FILE *input, *output; input= fopen(“in.txt”, “r”); if (input==NULL) printf(“файл не открыт\n“); output= fopen(“out.txt”, “w”); if (output==NULL) printf(“файл не открыт\n“); !!! Возвращаемые значения следует проверять! После работы с файлом связь между ОС и программой следует разорвать. Закрытие файла: fclose(<указатель на файл>); Возвращаемое значение: 0 – в случае успеха; -1 – иначе. !!! Обязательно нужно закрывать файлы, открытые на запись, т.к. в этот момент происходит перемещение последней порции информации из буфера ввода-вывода во внешнюю память. Функции форматированного ввода/вывода : fscanf() – ввод из файла. fprintf() – вывод в файл. Количество аргументов функций переменное: - указатель на файл; строка формата со спецификацией преобразования данных; - сохраняемые (загружаемые) значения. Число и типы элементов списка ввода-вывода должны соответствовать спецификациям в командной строке. Формат – это некая инструкция преобразования данных: %[<выравнивание>][+| ] [<ширина>] [<.точность>] <модификатор типа> Прочитать строку из файла: char str[100]; fscanf (input,”%[A-z .,;!]”,str); Функции fscanf() и fprintf() применимы для текстовых файлов. Неформатированный ввод-вывод Используется для последовательных файлов, но только для символов и строк. Ввод символа: int fgetc(<указатель на файл>); int getc(<указатель на файл>); Из файла считывается один символ из потока и переводится в int. Если ошибка или конец файла - EOF. Вывод символа: int fputc(char ch, <указатель на файл>); int putc(char ch, <указатель на файл>); Символ ch записывается в выходной поток. Если успешно, то возвращается значение ch типа int, иначе – EOF. Пример 76: char c; FILE *input, *output; c = getc(input); putc(c, output); Ввод-вывод строки: char* fgets(a1, a2, a3); int fputs(a1, a3); a1 – указывает на место ОЗУ, куда строка считывается с помощью fgets или откуда она записывается с помощью fputs. a3 – указывает на файл, который читает функция fgets или файл, в который пишет fputs. a2 – максимальная длина считываемой строки. Функция после: fgets прекращает ввод символов - после считывания символа новой строки (эскейп-символ ‘\n’); - после считывания символа конца файла (EOF); - после ввода строки длиной a2-1. fgets сохраняет введенный символ ‘\n’. fputs не ставит символ ‘\0’, записывает в файл строку из a1. Функция char* gets(s) считывает из стандартного файла stdin строку в переменную s. Если строка завершается символом ‘\n’, то он отбрасывается. Функция int puts(s) выводит в стандартный файл stdout строку s. Пример 77. Перезапись содержимого одного файла в другой: #define LSTR 80 … FILE *input, *output; char string[LSTR+1]; … if ((input = fopen(“bx”, “r”)) == NULL) { printf(“Файл bx не открыт!\n”); exit(-1); } if ((output = fopen(“bix”, “w”)) == NULL) { printf(“Файл bix не открыт!\n”); exit(-1); } while (fgets (string, LSTR, input) != EOF) fputs (string, output); … fclose(input); fclose(output); Функция ungetc int ungetc (char ch, <указатель на файл>); Символ ch «выталкивается» во входной поток => будет считан первым следующей операцией ввода. Если нет ошибки => возращается значение ch типа int. Если ошибка, то EOF. Ввод-вывод и стандартные файлы связан с клавиатурой Не требуют stdin определения в stdout программе stderr пользователя связаны с терминалом Стандартные файлы можно переназначить на другие устройства – внешние. Это выполняется в командной строке при вызове исполнимого файла: - stdin: связан с клавиатурой my_prog < input_file После этого все функции ввода программы, которые вводили из stdin, будут вводить из указанного файла. - stdout: my_prog > output_file Все функции вывода программы, которые работали с stdout будут выводить в output_file (файл пользователя/принтер). - stderr: не терминал. перенаправляется, scanf – ввод со stdin printf – вывод в stdout всегда – Отличие от fscanf, fprintf: нет имени указателя на файл Ввод-вывод с моделированием потока в памяти (ОЗУ) sscanf (char* s, char* format, <аргументы>); sprintf (char* s, char* format, <аргументы>); Аналоги fscanf, fprintf, но поток ввода-вывода заменен строкой s. #define N 100 #define MARKING "------------------------------------------------" struct input { char district[20]; float year1, year2; }; int main() { int n = 0; setlocale(LC_ALL, "Rus"); FILE* f; fopen_s(&f, "input.txt", "r"); if (f == NULL) { printf("Файл базы данных \"input.txt\" не найден."); _getch(); return 1; } struct input *a = new input[N]; printf("%s\n| Запас газоносных районов СССР в процентах |\n%s\n| 1940 | 1958 MARKING); Район | |\n%s\n", MARKING, MARKING, while (fscanf(f, "%s %f %f", a[n].district, &a[n].year1, &a[n].year2) != EOF) { if (a[n].year1 <= 100 && a[n].year2 <= 100) { printf("| %-19s |%10.1f |%10.1f |\n", a[n].district, a[n].year1, a[n].year2); printf("%s\n", MARKING); n++; } else { printf("!!! Далее таблица содержит ошибочные значения: процент запаса отдельного района от общего запаса СССР не может быть больше 100 \n\n"); break; } } fclose(f); if (n < 2) { printf("Количество районов должно быть не меньше 2. \nЗаполните базу данных и повторите"); _getch(); } fopen_s(&f, "output.txt", "w"); printf("Районы, процент запасов которых в 1958 не снизился по отношению к 1940:\n"); fprintf(f, "Районы, процент запасов которых в 1958 не снизился по отношению к 1940:\n"); for (int i = 0; i < n; i++) { if (a[i].year1 <= a[i].year2) { printf("%s, с ростом показателей в %0.1f\n", a[i].district, a[i].year2 - a[i].year1); fprintf(f, "%s, с ростом показателей в %0.1f\n", a[i].district, a[i].year2 - a[i].year1); } } Файлы прямого доступа и бинарные файлы Особенности файла в Си – бестиповый. Единственный признак – свойство потока: текстовый или двоичный. Тип файла указывается при открытии (создании) с помощью fopen() добавлением к способу открытия буквы ‘t’ для текстовых файлов и ‘b’ для бинарных. FILE *input, *output; input = fopen(“in.txt”, “r+t”); output = fopen(“out.txt”, “wb”); Открытие/закрытие файлов прямого доступа выполняется теми же функциями, что и для файлов последовательного доступа. Функции ввода \ вывода Запись и чтение в бинарный файл осуществляется с помощью функций fread() и fwrite(). size_t fread (void *ptr, size_t size, size_t FILE *stream); n, Читает из файла stream в массив ptr n объектов-записей размера size. Возвращает количество действительно прочитанных блоков заданного размера (результат может быть отличен от n). Состояние потока после завершения операции чтения необходимо проверять другими функциями – feof() и ferror(). size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream); Записывает в файл stream из массива ptr n объектов-записей размера size. Возвращает количество действительно записанных блоков заданного размера (результат может быть отличен от n) Пример 78. Запись несимвольных данных в файл и последующее их чтение. #include <stdio.h> #include <stdlib.h> int main() { FILE *fp; double d = 12.23; int i = 101; long l = 123023L; if((fp=fopen("test","wb+"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(-1); } fwrite(&d, sizeof(double), 1, fp); fwrite(&i, sizeof(int), 1, fp); fwrite(&l, sizeof(long), 1, fp); rewind(fp); // fp в начальное положение fread(&d, sizeof(double), 1, fp); fread(&i, sizeof(int), 1, fp); fread(&l, sizeof(long), 1, fp); printf("%f %d %ld", d, i, l); fclose(fp); return 0; } void rewind(FILE *stream); Устанавливает указатель в начало файла. int ftell(FILE *stream); Возвращает текущую позицию stream. При ошибке возвращает –1L. fseek() и произвольный доступ Можно выполнять операции произвольного чтения и записи с помощью fseek(), устанавливающей текущую файловую позицию. int fseek(FILE *fp, long offset, int wherefrom); fp – это указатель на файл, возвращенный fopen(). Аргументы offset и wherefrom зависят от того, текстовый файл или двоичный. Для двоичного файла: offset – число байтов смещения от точки, определяемой wherefrom: - SEEK_SET – начало файла; - SEEK_CUR – текущая позиция; - SEEK_END – конец файл. Двоичные файлы можно читать и в обратном порядке. Для текстового файла: offset – может быть либо =0, либо результату, который возвращает функция ftell. wherefrom – SEEK_SET. // Открыть существующий как двоичный для чтения и записи FILE *fd; fd = fopen("a.dat","rb+wb"); // Создать новый как двоичный для записи и чтения fd = fopen("a.dat","wb+"); Пример 79. Получение клиента из списка по номеру. struct addr { char name[40], street[40], city[40]; char state[3]; char zip[10]; } info; void find(long int client_num) { FILE *fp; if((fp=fopen("mail", "rb")) == NULL) { printf("Не удается открыть файл.\n"); exit(1); } fseek(fp, client_num*sizeof(struct addr), SEEK_SET); fread(&info, sizeof(struct addr), 1, fp); fclose(fp); } Функции обработки ошибок int ferror(FILE *stream); Возвращает 0, если нет ошибок, и число, отличное от 0, если ошибки есть. Вызывается после функции, которая может вызвать ошибку, например, fread() и fwrite(). void clearerr(FILE *stream); Очищает состояние признака ошибки в stream. int feof(FILE *stream); Возвращает EOF если конец файла, иначе 0. Особенности обработки файлов прямого доступа Последовательный файл считывают с помощью цикла с неизвестным числом повторений до наступления условия «прочитан конец файла». У файлов прямого доступа следует использовать цикл с известным числом повторений для обработки такого файла. Можно получить размер файла: 1) С помощью fseek установить позицию на конец файла: fseek(stream, 0L, SEEK_END); текущую 2) Запросить значение текущего указателя (размер файла) с помощью ftell: size = ftell(stream); Далее в цикле позиционировать текущую позицию файла с помощью fseek, пересчитывая каждый раз величину смещения – второй аргумент (позиционировать от начала). Пример сортировки struct Record { //структуры данных char country[20]; //Название страны float el55; //Произведено электроэнергии в 1955 float el58; //в 1958 } a, b; int cmp(const void * a, const void * b) { return strcmp(((Record*)a)->country, ((Record*)b)->country); int i=0, j, pos,n; if ((fv = fopen(name,"rb+")) == NULL) //Открытие файла для чтения { printf("Error open file!\n"); exit(-1); } fseek(fv,0,SEEK_END); n = ftell(fv)/sizeof(a); //Определение количества записей в файле fseek(fv,0,SEEK_SET); for (i=0; i<n; i++) { fread(&rec[i],sizeof(Record),1,fv); puts(rec[i].country); } qsort(rec,n,sizeof(Record),cmp()); fseek(fv,0,SEEK_SET); for (i=0; i<n; i++) { puts(rec[i].country); fwrite(&rec[i],sizeof(Record),1,fv); } free(rec); fclose(fv); void qsort ( void * first, size_t number, size_t size, int ( * comparator ) first указатель на 1й элемент сортируемого массива. number количество элементов в сортируемом массиве, на который ссылается указатель first. Size размер одного элемента массива в байтах. Comparator Функция, которая сравнивает два элемента. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ – это данные, размер которых может увеличиваться или уменьшаться при исполнении программы. Связанный список – это набор элементов данных, в любом месте которых можно производить вставки и удаления. startPtr – указатель на голову (первый элемент) списка Информационная часть 17 29 … Ссылочная часть 93 Связанный список – это линейный набор ссылающихся на себя структур, называемых узлами, и объединённых указателем-связкой. Доступ к связанному списку обеспечивается указателем на первый элемент списка (голову). Каждый узел списка необходимости. создаётся по мере Узел может содержать данные любого типа, включая другие структуры. Список – рекурсивная структура данных, поскольку каждый элемент указывает на такой же элемент. Хвост Хвост Голова Голова Голова 1 10 … 10 Хвост 93 Элемент списка – это структура, ссылающаяся на себя, т.е. одним из элементов структуры является указатель, который ссылается на элемент того же типа. struct node { int data; struct node *next; }; next содержит адрес следующего элемента списка. Поле next последнего элемента списка должно быть равно NULL. Создание и использование динамических структур данных требует возможности выделять и удалять память во время работы программы. Выделение памяти: void* malloc(size_t); Освобождение памяти: void free(void* memblock); struct node *p = malloc(sizeof(struct node)); if (p==NULL) printf(“Память не выделена!\n”); … free(p); !!! Следует всегда освобождать память, которая перестала использоваться, во избежание утечки памяти. Операции над списками 1. Добавление элемента в конец списка {3, 10, -8} + 20 = {3, 10, -8, 20} (5) (1) -8 10 3 (2) (3) (7) (6) 20 (4) 2. Добавление элемента в начало списка 1 + {3, 10, -8} = {1, 3, 10, -8} (3) (5) (1) (4) -8 10 1 3 (2) 3. Удаление элемента в конце списка {3, 10, -8, 20} - 20 = {3, 10, -8} (1) (3) (2) 3 (5) 10 -8 (4) 20 4. Удаление элемента в начале списка {1, 3, 10, -8} - 1 = {3, 10, -8} (3) (2) (1) 1 3 (4) 10 -8 Преимущества списка перед массивом: - использует ровно столько памяти сколько нужно в текущий момент; - переполнение происходит только, если не хватает ресурсов ОС. Массив проецируется в ОП на вектор => необходимо изначально выделить непрерывную область памяти заранее известного размера. «Довыделение» памяти (расширение массива) – невозможно. Список – связная структура данных, элементы списка расположены в памяти в произвольном порядке (связь между элементами обеспечивается за счет указателя на следующий элемент). Расширение списка – динамически выделяется память под новый элемент списка, добавляемый элемент связывается с остальной частью списка. Недостатки списка по сравнению с массивом: - так как элементы списка в памяти обычно расположены в разброс, то доступ к элементу списка осуществляется медленнее, чем к элементу массива. Обратиться к элементу массива – рассчитать адрес этого элемента в памяти (адрес начала массива + смещение элемента относительно начала массива) – быстро. Обратиться к элементу списка – выполнить последовательный проход по элементам списка (медленно). Стеки – это списки, добавление и удаление элементов в которых выполняются только с начала (из головы). Для стеков реализован принцип LIFO: Last Input First Output. Очереди – это списки, добавление элементов в которых выполняется в конец (в хвост), а удаление элементов - с начала (из головы). Для очередей реализован принцип FIFO: First Input First Output. Реализация стека с помощью связанного списка Базовые операции: - push (поместить элемент на верхушку стека); - pop (извлечь элемент из верхушки стека и получить его значение – выборка и удаление); - peek (считать значение элемента, расположенного на верхушке стека – без извлечения). Операция push Состояние стека До операции После операции 27 17 7 Порядок: 7, 17, 27 37 37 27 17 7 pop 37 27 17 7 37 37 27 17 7 peek 37 27 17 7 37 37 27 17 7 Пример 80. Реализация стека на языке Си Будем использовать технологию раздельной компиляции, программный комплекс будет состоять из трех файлов: - описание типа данных stack и прототипы функций для работы со стеком (заголовочный файл); - определение функций для работы со стеком (файл реализации); - главный модуль программы, содержащий функцию main (файл реализации). /* Файл stack.h */ #define STACK struct stack STACK { int info; /* информационная часть */ STACK *next; /* ccылочная часть */ } extern void push(STACK **s, int item); extern int pop(STACK **s, int *error); extern int peek(STACK **s, int *error); /* Файл stack.c */ #include <alloc.h> #include “stack.h” void push(STACK **s, int item) /*(1)*/ { STACK *new_item; new_item = (STACK*)malloc(sizeof(STACK)); /*(2)*/ new_item -> info = item; /*(3)*/ new_item ->next = *s; /*(4)*/ *s = new_item; /*(5)*/ } int pop(STACK **s, int *error) /* error = 1, если неуспех, иначе error = 0 */ { STACK *old_item = *s; int old_info = 0; *error = 1; if (*s) /* Стек не пустой */ { old_info = old_item -> info; *s = (*s) -> next; free(old_item); *error = 0; } return old_info; } int peek(STACK **s, int *error) { if (*s) /* Стек не пустой */ { *error = 0; return (*s) -> info; } else /* Стек пуст */ { *error = 1; return 0; } } /* файл program.c*/ #include <stdio.h> #include "stack.h" STACK *st = NULL; /* st NULL */ int main() { int err; push(&st, -8); /* st */ -8 printf("%d\n", peek(&st, &err)); /*-8 */ /* err = 0 */ push(&st, 10); /* st */ 10 -8 printf("%d\n", peek(&st, &err)); /* 10 */ /* err = 0 */ 3 10 push(&st, 3); /* st */ -8 printf("%d\n", peek(&st, &err)); /* 3 */ /* err = 0 */ printf("%d\n", pop(&st, &err)); /* 3 */ /* err = 0 */ 10 -8 /* st */ printf("%d\n", pop(&st, &err)); /* 10 */ /* err = 0 */ /* st */ -8 printf("%d\n", pop(&st, &err)); /* -8 */ /* err = 0 */ /* st NULL */ printf("%d\n", pop(&st, &err)); /* 0 */ /* err = 1 */ return 0; } Реализация очереди с помощью связанного списка Базовые операции: - insert (добавить в конец очереди); - take_off (извлечь из начала очереди). Операция insert take_off Состояние очереди До операции После операции 3 10 3 10 -8 начало конец -8 3 3 10 -8 3 10 -8 Пример 81. Реализация очереди на языке Си /* файл queue.h */ #define QUEUE struct queue QUEUE { int info; QUEUE *next; }; extern void insert(QUEUE **q,int item); extern int take_off(QUEUE **q,int *err); /* файл queue.c */ #include <alloc.h> #include <stdio.h> #include "queue.h" void insert(QUEUE **q, int item) { QUEUE *current=*q; QUEUE *previous=NULL; QUEUE *new_node; while(current) { previous=current; current=current->next; } new_node=(QUEUE*)malloc(sizeof(QUEUE)); new_node->info=item; /* previous – указатель на последний элемент */ if (previous) /* Если очередь не пуста */ { new_node->next=previous->next; previous->next=new_node; } else /* Иначе */ { *q=new_node; (*q)->next=NULL; } } int take_off(QUEUE **q, int *err) { int value=0; QUEUE *old_header=*q; *err=1; if (*q) { value=old_header->info ; *q=(*q)->next; free(old_header); *err=0; } return value; } /* program.c */ #include <stdio.h> #include "queue.h" QUEUE *q=NULL; /* q NULL */ /* q /* q /* q 3 3 3 int main() { int err; insert(&q,3); insert(&q,10); insert(&q,-8); 10 10 -8 */ */ */ printf("%d\n",take_off(&q,&err)); /* 3 */ /*err = 0 */ /* q */ 10 -8 printf("%d\n",take_off(&q,&err)); /* 10 */ /*err = 0 */ /* q */ -8 printf("%d\n",take_off(&q,&err)); /* -8 */ /*err = 0 */ /* q NULL */ printf("%d\n",take_off(&q,&err)); /* 0 */ /*err = 1 */ return 0; } Вариант из методички #include "stdafx.h" #include "stdlib.h" #include "conio.h" struct list { int value; struct list *next; } list; void add(list **head, int item) { list *new_item = (list*)malloc(sizeof(list)); new_item->value = item; new_item->next = *head; *head = new_item; } void print(const list* head) { while (head) { printf("%d->", head->value); head = head->next; } printf("NULL"); } void dubl(list *head) { if (!head) return; list *new_item, *temp = head; while (temp) { new_item = (list*)malloc(sizeof(list)); new_item->value = temp->value; new_item->next = temp->next; temp->next = new_item; temp = temp->next->next; } } int _tmain(int argc, _TCHAR* argv[]) { list *head = NULL; int key, item; printf(«введите список."); do { printf("\n\nэлемент = "); while (scanf_s("%d", &item) != 1) { printf(«Ошибка ввода. повторите\n"); printf("Element = "); fflush(stdin); } add(&head, item); do { printf("\nхотите ввести еще раз? (y/n): "); key = _getch(); _putch(key); } while (key != 'Y' && key != 'y' && key != 'N' && key != 'n') } while (key != 'N' && key != 'n'); printf("\n\n исходный список:\n"); print(head); dubl(head); printf("\n\n полученный список:\n"); print(head); return 0; Vvedite spisok. Element = 5 Eshe vvesti? (y/n): y Element = 3 Eshe vvesti? (y/n): y Element = 8 Eshe vvesti? (y/n): y Element = 10 Eshe vvesti? (y/n): n Ishodniy spisok: 10->8->3->5->NULL Polu4ennuy spisok: 10->10->8->8->3->3->5->5->NULL Препроцессор Си Препроцессор (preprocessor) – это программа, которая выполняет предварительную обработку. На вход компилятору подается конструкций двух языков: смесь из 1) язык Си; 2) директив препроцессора. Препроцессор обрабатывает директивы своего языка и заменяет их инструкциями языка Си. Поэтому на вход компилятору поступает только текст на языке Си. Директивы препроцессора Язык препроцессора состоит из директив (операторов) или команд: - с первой позиции строки; - начинаются с «#»; - после «#» - непосредственно наименование директивы; - разделитель частей директивы – пробел; - символ продолжения «\» используется для переноса части директивы новую строку: #include \ <conio.h> - не заканчивается «;». Виды директив: - включения файлов; - макроподстановка; - управляемые компиляцией. Включение файлов Объединение нескольких исходных файлов в один: #include “<имя_файла>“ – поиск выполняется в текущей директории; имени #include <имя_файла> – поиск имени выполняется в сист. директории заголовочных файлов. Семантика: - замена строки директивы текстом из включаемого файла; - директивы могут быть вложенными, но не рекурсивно вложенными. Макроподстановка # define . . . . . . . Два вида: простая и с аргументами. Простая макроподстановка #define <имя> <строка> - <имя> - цепочка символов (заглавных букв); - конец <имени> - пробел; - <строка> - любая конструкция допустимая в Си. Все последующие вхождения <имени> заменяется <строкой>, исключения: 1) если <имя> встречено внутри другой лексемы; 2) если <имя> встречается внутри символьной или строковой константы. Пример 88. #define NULL 0 #define EOF (-1) … if (p==NULL) x=y; z=zNULL; printf (“NULL\n”); /* выполняется */ не выполняется Пример 89. #define PROV (a>b && b>c && (a * b)!=0) void main() { int a,b,c; ....... if (PROV) printf (b); else printf (c); } Макроподстановка с аргументами #define <имя>(<имя_арг1>,...,<имя_аргn>) <строка> Cинтаксис: - Нельзя ставить пробел между <именем> и ( ); - Обычно в <строке> должны встретиться <имена_арг> Семантика: - аналог функции. - синонимы: псевдофункция, макро, макросы. Могут быть вложенными, но не рекурсивно вложенными. Пример 90. Перевод в верхний регистр заглавных букв #define up(x) ((x)-”a”+”A”) В отличие от полноценных функций макроподстановки с аргументами обрабатываются препроцессором. Для полноценной функции настройка аппарата формальных и фактических параметров, вызов функции и возврат из нее результата в среднем стоит порядка 60 машинных команд. Побочные эффекты в макроподстановке Если объявление макроподстановки выполнено не аккуратно, то ее использование приводит к побочным эффектам. Побочными эффектами обладают процедуры и функции: это тот случай, когда процедура или функция изменяет и-или использует значения внешних переменных. 2 вида побочных эффектов: - периода лексического анализа; - периода выполнения. Побочные эффекты лексического анализа Пример 91. #define POW3(y) y*y*y int a=3; int b; b=POW3(a+2); /* a+2*a+2*a+2 b=3+2*3+2*3+2=17 */ 2 способа исправить побочный эффект: /* Способ 1 */ int POW3(int y) { return y*y*y; } /* Способ 2 */ #define POW3(y) ((y)*(y)*(y)) В первом случае мы имеем большие накладные расходы при вызове функции. Второй вариант – исправить макрос (каждое вхождение аргумента в строке должно быть заключено в круглые скобки и вся строка в круглые скобки). Побочные эффекты периода выполнения Эти ошибки обусловлены многократным вычислением одних и тех же аргументов макроса. Пример 92. #define max(x,y) ((x)<(y)?(y):(x)) int i=10, y; y=max(i++,2); Результат: i=12 и y=12. Пример 93. #define max(x,y) ((x)<(y)?(y):(x)) int c; c=max(getchar(), ‘k’); Функция getchar будет вызвана 2 раза, если считанный символ >= ‘k’. Директива #undef Cинтаксис: #undef <имя> Семантика: Отмена определения, связанного с этим именем. Пример 94. #include <stdio.h> #undef getchar int getchar (void) {.....} Условная компиляция Директивы условной компиляции указывают компилятору определенные фрагменты текста, которые должны компилироваться при выполнении различных условий: 1) 2) 3) 4) 5) 6) 7) #if; #ifdef; #ifndef; #endif; #elif; #defined; #else. 1) #if Условная подстановка фрагмента текста в зависимости от значения константного выражения, записанного после #if; 2) #ifdef Условная подстановка текста, если макрос определен; фрагмента 3) #ifndef Условная подстановка фрагмента текста, если макрос не определен; 4) #endif Обозначение конца подстановки фрагмента текста; условной 5) #elif Альтернатива для #ifdef или #ifndef после которых записывается проверяемое условие (константное выражение); 6) #defined Может использоваться вместе с #if, она проверяет является ли имя именем макроса; 7) #else Альтернатива #if без дополнительных условий. Семантика: - вычисляется константное целое выражение, записанное в #if (оно не должно содержать ни одного оператора sizeof или (тип) и именованных констант); - если выражение отлично от нуля, то в компиляцию включаются все последующие строки до #endif , или #elif , или #else; - выражение #defined (<имя>), которое используется в #if или #elif есть 1, если <имя> было определено в макросе. Пример 95. #if !defined (HDR) #define HDR . . /* Это определение HDR.h */ . #endif /* Исключаем повторное определение HDR */ Пример 96. #ifndef HDR #define HDR . . /* Это определение hdr.h */ . #endif Пример 97. /* Это пример цепочки проверенных систем для выбора нужного заголовочного файла, для его включения в программу */ #if SYSTEM == SYSV #define HDR “sysv.h” #elif SYSTEM == MSDOS #define HDR “msdos.h” #else #define HDR “default.h” #endif #include HDR