Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Электронный учебно-методический комплекс по учебной дисциплине «Системное программирование» для специальностей 1-40 01 01 «Программное обеспечение информационных технологий», 6-05-0612-01 «Программная инженерия»

.pdf
Скачиваний:
0
Добавлен:
28.12.2025
Размер:
3.06 Mб
Скачать

1.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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]