Электронный учебно-методический комплекс по учебной дисциплине «Системное программирование» для специальностей 1-40 01 01 «Программное обеспечение информационных технологий», 6-05-0612-01 «Программная инженерия»
.pdf1.6.5 Условная компиляция
Условность компиляции понимается по отношению к включению фрагментов текста в рабочий вариант программы. Операторы условной компиляции и реализуемые правила включения исходного текста:
а) условное включение
#if<предикат_условия> ТЕКСТ
#endif
(если условие истинно, то ТЕКСТ обрабатывается компилятором); б) альтернативное включение
#if<предикат_условия> ТЕКСТ_1
#else
ТЕКСТ_2
#endif
(если условие истинно, то компилятором обрабатывается ТЕКСТ_1, иначе - ТЕКСТ_2).
Виды предикатов условий:
константное_выражение - истина, если его значение не равно нулю;
def идентификатор - истина, если идентификатор был определен ранее оператором #define;
ndef идентификатор - истина, если идентификатор не был определен оператором #define.
Константное_выражение отделяется от ключевого слова if разделителем, а def и ndef - нет.
Пример:
#ifdef DEBUG print_state();
#endif
Элементы исходного текста "ТЕКСТ_1" или "ТЕКСТ_2" могут содержать любые операторы препроцессора.
Примеры:
#ifndef EOF #define EOF -1 #endif
#if UNIT==CON #include "conproc.c"
71
#else
#include "outproc.c" #endif
/* Блокировка повторного включения заголовочного файла */
#ifndef _DESCRIPTOR #define _DESCRIPTOR
/*
Текст
*/
#endif
Предикаты условий могут использовать системно определенные макросы, например:
__DATE__ - дата,
__TIME__ - время компиляции; __FILE__ - имя компилируемого файла;
__LINE__ - целочисленное значение номера текущей строки. Пример использования системных макроопределений:
#include <stdio.h> void main() {
printf("\nФайл %s",__FILE__);
}
Кроме заданных операторами #define и системоопределенных макросов доступными оказываются и идентификаторы, задаваемые в командной строке вызова компилятора и(или) опциями установки интегрированной среды.
В качестве предиката условия в современных препроцессорах может выступать "функция" препроцессора defined():
#if defined(DOS_TARGET) puts(msg);
#else
MessageBox(NULL,msg,"MSG",MB_OK | MB_TASKMODAL); #endi
Использование defined() позволяет сокращать текст записи сложных условий:
#if defined(DOS_TARGET) && !defined(NO_DEBUG) puts(msg);
#endif
Предикат условия может включать логические операции языка C и
операции отношений (==, !=, >, >=, <, <=).
Последнее расширение операторов условной компиляции - оператор #elif:
72
#if выражение_1 |
/* Включение, если выражение_1 |
- |
истина */ |
||
#elif выражение_2 |
/* Включение, |
если |
выражение_2 |
- |
истина */ |
#else |
/* Включение, |
если |
все выражения |
ложны */ |
|
#endif |
|
|
|
|
|
В последней синтаксической конструкции обязательны лишь первый и последний операторы. Типичное применение #elif - альтернативный выбор фрагмента исходного текста [1]:
void sort(void *x,int n) { #if 0
/* Новая версия программы не отлажена */
#elif MODEL==__HUGE__
/* Работа с "дальними указателями" */ #else
/* Использование библиотечной функции */ qsorts(x,n);
#endif
}
1.6.6 Изменение нумерации строк и имени файла
По умолчанию диагностические сообщения компилятора привязываются к номеру строки и имени файла исходного текста.
Оператор
#line номер_строки идентификатор_файла
позволяет с целью более приметной привязки к фрагментам текста изменить номер текущей строки __LINE__ и имя файла __FILE__ на новые значения ("идентификатор_файла" можно опустить) [1].
1.6.7 Расширенные возможности современных процессоров
Современные версии препроцессоров поддерживают следующие возможности:
вывод диагностических сообщений;
преобразование аргументов макроопределений в строку;
конкатенацию(склейку) лексем.
Синтаксис оператора вывода диагностических сообщений:
#error сообщение_об_ошибке
73
(сообщение об ошибке здесь может включать идентификаторы макроопределений).
#if !defined(__HUGE__)
#error Файл __FILE__: компиляция только в режиме HUGE #endif
Рассмотрим возможность преобразования аргументов макроопределений в строку. В операторе макроопределения
#define идентификатор(парам_1,...) строка
именам параметров в "строке" может предшествовать символ '#', что предписывает преобразование аргумента в строку. Результат преобразования объединяется со смежными строками, если он отделен только пробелом:
#define DEBUG_OUT(intvar) \ printf(#intvar "%d\n", (int)(intvar))
void main() {
int alpha=1, betta=2; DEBUG_OUT(alpha);
DEBUG_OUT(betta);
}
Результаты работы программы: alpha=1 betta=2
Конкатенация, или склейка, лексем программируется следующим образом: оператор X##Y объединяет лексемы X и Y, причем результат снова обрабатывается препроцессором.
Пример:
#define DEF_Var(n) int __var_##n #define USE_Var(n) __var_##n
DEF_Var(100);
DEF_Var(200);
USE_Var(100)=1;
USE_Var(200)=USE_Var(100)++;
Приведенный фрагмент программы на вход компилятора поступит в виде
int __var_100; int __var_200;
__var_100=1; __var_200++;
74
Очевидно, что рассмотренные здесь расширенные возможности препроцессора облегчают параметризацию исходного текста программы [1].
1.7 ЗАПУСК И ЗАВЕРШЕНИЕ ПРОГРАММ
1.7.1 Головная функция программ на языке C
Головной функцией любой программы на языке C является функция main, которая может получать аргументы из командной строки вызова программы и среды оболочки. Интерпретация командной строки вызова программы
имя арг_1 арг_1 ... арг_N
средствами языка C выглядит так:
char *argv[argc]={"имя","арг_1",..."арг_N"};
(здесь argc=N+1). Интерпретация переменных среды оболочки
char *envp[]={
"имя_1=значение_1", "имя_2=значение_2",
...
"имя_M=значение_M",
NULL /* Признак конца списка */ };
Набор переменных среды оболочки в MS-DOS может меняться и контролироваться оператором SET: а) добавление или коррекция переменной
set имя=значение
б) исключение переменной из среды
set имя=
в) вывод на экран переменных среды
set
Доступ к аргументам командной строки вызова программы и переменным среды оболочки возможен в случае использования списка формальных параметров в головной функции main. Пример процедуры main с полным списком параметров:
#include <stdio.h>
void main(int argc, char *argv[], char *envp[]) { int i;
char **p;
/* Печать параметров командной строки */
75
for (i=0; i<argc; i++) printf("\nАргумент %d): %s",i,argv[i]);
/* Печать переменных среды оболочки */
printf("\n\nПеременные среды оболочки:"); for (p=envp; *p; p++)
printf("\n %s",*p);
}
Результаты работы программы:
Аргумент 0): D:\RMPL\SP\P1.EXE
Аргумент 1): x1 Аргумент 2): x2
Переменные среды оболочки: winbootdir=C:\WINDOWS COMSPEC=C:\COMMAND.COM PROMPT=$p$g
PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;D:\LEXNEW;E:\CBUILDER\ BIN
TEMP=C:\TMP
INCLUDE=e:\msdev\include
LIB=e:\msdev\lib
windir=C:\WINDOWS CMDLINE=p1.exe x1 x2
Результаты выполнения команды set:
winbootdir=C:\WINDOWS
COMSPEC=C:\COMMAND.COM PROMPT=$p$g
PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;D:\LEXNEW;E:\CBUIL
DER\BIN
TEMP=C:\TMP
INCLUDE=e:\msdev\include
LIB=e:\msdev\lib
windir=C:\WINDOWS
Функция main может вызываться рекурсивно из любой функции:
76
#include <stdio.h>
int n=5;
void main() {
printf("\n %*i",n,n--); if (n) main();
}
Результаты работы программы:
5
4
3
2
1
Следует отметить, что в языке C++ рекурсивный вызов функции main не всегда допускается [1].
1.7.2Порождение и идентификация задач
Воднопользовательской операционной системе MS-DOS процесс запуска задач (программ) активизируется интерпретатором команд COMMAND.COM с использованием по умолчанию консоли CON. Имеется возможность назначения вместо консоли других стандартных устройств COM1,COM2,... AUX:
1) оператором SHELL в файле CONFIG.SYS:
SHELL=[[dos-drive:]dos-path]COMMAND.COMM [device] ...
2) при запуске нового экземпляра COMMAND.COM;
[[dos-drive:]dos-path]COMMAND.COMM [device] ...
3) оператором CTTY:
CTTY device
Здесь device - текущее устройство ввода-вывода команд и сообщений пользователя (терминал). COMMAND.COM запускает корневую задачу иерархии процессов пользователя, интерпретируя входные строки
[[dos-drive:]dos-path]exec_file p1 p2 ... [>stdout] [<stdin]
как запрос на запуск исполнимого файла (программы или пакетного командного файла) exec_file с параметрами p1,p2,... и возможным перенаправлением
77
стандартных потоков ввода-вывода. Любая задача может породить другие задачи-потомки. Примеры функций запуска задач-потомков:
int spawnv(int mode, // Режим запуска
char *path, // Путь к загрузочному модулю char *argv[]); // Аргументы командной строки
int spawnve(int mode, // Режим запуска
char *path, // Путь к загрузочному модулю char *argv[], // Аргументы командной строки char *envp[]); // Переменные среды окружения
Виды режимов запуска (Turbo-C, Borland C++ 3.1): P_WAIT - ожидание момента завершения задачи-потомка;
P_NOWAIT - не реализованное в MS-DOS параллельное выполнение задач (применение приводит к ошибке);
P_OVERLAY - оверлейная загрузка задачи - потомка на место родителя (трансформация задач).
При успешном запуске после исполнения задачи-потомка возвращается значение кода возврата, в противном случае - значение -1 и код ошибки в глобальной переменной errno:
E2BIG - длинный список аргументов; EINVAL - ошибочный аргумент; ENOENT - файл не найден; ENOEXEC - ошибка формата файла; ENOMEM - нехватка памяти.
Задача-потомок в MS-DOS пользуется стандартными файлами stdin, stdout, stderr задачи родителя (их можно переопределить).
Пример программы:
#include <process.h> #include <stdio.h> #include <errno.h>
int main(void) {
if (freopen("TEMP.OUT","w",stdout)) {
if (spawnl(P_WAIT,"work.exe",NULL)<0) { printf("\n Ошибка %d",errno); exit(errno);
}
}
return 0;
}
Приведенная программа моделирует ввод команды
work > temp.out
78
В MS-DOS доступны следующие виды внешнего вмешательства в процесс исполнения активной задачи:
Ctrl+Break - снятие задачи; Pause - приостановка.
Межзадачное взаимодействие, организуемое средствами MS-DOS, сводится лишь к образованию канала обмена данными через временный файл. Например, команда
dir|sort|more
приводит к последовательному выполнению системных программ dir, sort, more и выдачи результата на устройство CON. Пусть файл ds.exe получен в результате трансляции программы
#include <stdio.h>
void main() { int i;
while ((i=getch())!=EOF) putchar(' '), putchar(i);
}
Команда
dir|ds|more
позволит просмотреть результат работы программы dir в виде разреженного текста.
Функции семейства spawn в многозадачных операционных системах позволяют организовать динамическую параллельную структуру программы. Для образования динамической последовательной структуры можно воспользоваться функциями семейства exec:
int execl(char _FAR *__path, char _FAR *__arg0, ...); int execle(char _FAR *__path, char _FAR *__arg0, ...); int execlp(char _FAR *__path, char _FAR *__arg0, ...);
int execlpe(char _FAR *__path, char _FAR *__arg0, ...); int execv(char _FAR *__path, char _FAR *__argv[]);
int execve(char _FAR *__path, char _FAR *__argv[], char _FAR *_FAR *__env);
int execvp(char _FAR *__path, char _FAR *__argv[]); int execvpe(char _FAR *__path, char _FAR *__argv[],
char _FAR *_FAR *__env);
Такие функции возвращают код завершения задачи-потомка. Можно выполнить запуск задачи-потомка через вызов интерпретатора командных строк, используя функцию
int system(char *command);
79
Здесь строка command может содержать любую команду и/или директиву, обрабатываемую интерпретатором командных строк:
system("dir>test.txt");
(в MS-DOS команда dir выполняется непосредственно интерпретатором командных строк) [1].
1.7.3 Завершение программ
Любая функция в языке С обычно завершается после выполнения последнего оператора в теле функции либо оператора return. Возврат из любой точки программы можно выполнить посредством вызова специальных библиотечных функций. Например, в файле stdlib.h определены функции:
void exit(int status) - вывод содержимого буферов, закрытие всех файлов и возврат в порождающий процесс кода завершения status;
void abort(void) - возврат в порождающий процесс с кодом завершения 3
[1].
1.7.4Идентификация задач и виды межзадачных взаимодействий
Вмногозадачных средах каждая задача получает уникальный системный идентификатор (task ident). Например, в операционной системе QNX функции на языке C могут использовать глобальные переменные
unsigned int My_tid, /* Идентификатор текущей задачи */ Dads_tid, /* Идентификатор задачи - родителя */ My_nid; /* Номер узла вычислительной сети */
(QNX - сетевая многозадачная многопользовательская операционная система, быстрореактивная версия операционной системы семейства UNIX) [1]. Во время исполнения задача-родитель информирована об идентификаторах потомков результатом функций порождения. Иногда требуется получить идентификатор некоторой задачи, не связанной с текущей процессом порождения. Например, это может потребоваться для обмена информацией между задачами. Для удобства установления идентификаторов задач QNX предоставляет возможность подсоединения к задаче системного имени в виде строки символов. Набор функций для работы с системными именами:
unsigned name_attach(char *name, unsigned node); unsigned name_detach(char *name, unsigned tid);
unsigned name_locate(char *name, unsigned node, unsigned size);
80
