HTML page

3.  Мобильность и машинная зависимость программ. Проблемы с русскими буквами.
     Программа считается мобильной, если она без каких-либо  изменений  ее  исходного
текста (либо после настройки некоторых констант при помощи #define и #ifdef) трансли-
руется и работает на разных типах машин  (с  разной  разрядностью,  системой  команд,
архитектурой, периферией) под управлением операционных систем одного семейства. Заме-
тим, что мобильными могут быть только исходные тексты программ, объектные модули  для
разных процессоров, естественно, несовместимы!

3.1.  Напишите программу, печатающую размер типов  данных  char,  short,  int,  long,
float, double, (char *) в байтах.  Используйте для этого встроенную операцию sizeof.

3.2.  Составьте мобильную программу, выясняющую значения следующих величин для  любой
машины, на которой работает программа:
1)   Наибольшее допустимое знаковое целое.
2)   Наибольшее беззнаковое целое.
3)   Наибольшее по абсолютной величине отрицательное целое.
4)   Точность значения |x|, отличающегося от 0, где x - вещественное число.
5)   Наименьшее значение e, такое что машина различает числа 1 и 1+e (для  веществен-
     ных чисел).

3.3.  Составьте мобильную программу, выясняющую  длину  машинного  слова  ЭВМ  (число
битов в переменной типа int). Указание: для этого можно использовать битовые сдвиги.

3.4.  Надо ли писать в своих программах определения

    #define EOF  (-1)
    #define NULL ((char *) 0)   /* или ((void *)0) */

Ответ: НЕТ. Во-первых, эти константы уже определены в include-файле, подключаемом  по
директиве

    #include <stdio.h>

поэтому правильнее написать именно эту директиву. Во-вторых, это было бы просто  неп-
равильно:  конкретные  значения  этих  констант на данной машине (в данной реализации
системы) могут быть другими! Чтобы придерживаться тех соглашений, которых придержива-
ются  все  стандартные  функции  данной  реализации, вы ДОЛЖНЫ брать эти константы из
<stdio.h>.
По той же причине следует писать

     #include <fcntl.h>
     int fd = open( имяФайла, O_RDONLY); /* O_WRONLY, O_RDWR */
            вместо
     int fd = open( имяФайла, 0);        /* 1,        2      */


3.5.  Почему может завершаться по защите памяти следующая программа?

    #include <sys/types.h>
    #include <stdio.h>
    time_t t;
    extern time_t time();
          ...
    t = time(0);
    /* узнать текущее время в секундах с 1 Янв. 1970 г.*/

Ответ: дело в том, что прототип системного вызова time() это:

            time_t time( time_t *t );

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



А. Богатырев, 1992-95                  - 123 -                              Си в UNIX

аргумента  0  (типа int).  На машине IBM PC AT 286 указатель - это 2 слова, а целое -
одно.  Недостающее слово будет взято из стека произвольно.  В результате time() полу-
чает в качестве аргумента не нулевой указатель, а мусор. Правильно будет написать:

            t = time(NULL);
      либо (по определению time())
                time( &t );


а еще более корректно так:

            t = time((time_t *)NULL);

Мораль: везде, где требуется нулевой указатель, следует писать NULL (или явное приве-
дение нуля к типу указателя), а не просто 0.

3.6.  Найдите ошибку:

       void f(x, s) long x; char *s;
       {
           printf( "%ld %s\n", x, s );
       }
       void main(){
           f( 12, "hello" );
       }

Эта программа работает на IBM PC 386, но не работает на IBM PC 286.
     Ответ. Здесь возникает та же проблема, что и в примере про sin(12). Дело в  том,
что  f  требует  первый аргумент типа long (4 байта на IBM PC 286), мы же передаем ей
int (2 байта). В итоге в x попадает неверное значение;  но  более  того,  недостающие
байты отбираются у следующего аргумента - s. В итоге и адрес строки становится непра-
вильным, программа обращается по несуществующему адресу и падает.   На  IBM PC 386  и
int и long имеют длину 4 байта, поэтому там эта ошибка не проявляется!
     Опять-таки, это повод для использования прототипов функций (когда вы  прочитаете
про них - вернитесь к этому примеру!). Напишите прототип

            void f(long x, char *s);

и ошибки не будет.
     В данном примере мы использовали тип void, которого  не  сушествовало  в  ранних
версиях  языка  Си.  Этот  тип  означает, что функция не возвращает значения (то есть
является "процедурой" в смысле языков Pascal или Algol).  Если мы  не  напишем  слово
void  перед  f,  то компилятор будет считать функцию f возвращающей целое (int), хотя
эта функция ничего не возвращает (в ней нет оператора return).  В большинстве случаев
это не принесет вреда и программа будет работать.  Но зато если мы напишем

       int x = f((long) 666, "good bye" );

то x получит непредсказуемое значение. Если же f описана как void, то написанный опе-
ратор заставит компилятор сообщить об ошибке.
     Тип (void *) означает указатель на что угодно (понятно, что к  такому  указателю
операции  [],  *,  -> неприменимы: сначала следует явно привести указатель к содержа-
тельному типу "указатель на тип"). В частности, сейчас  стало  принято  считать,  что
функция  динамического выделения памяти (memory allocation) malloc() (которая отводит
в куче[**] область памяти заказанного размера и выдает указатель на нее) имеет прототип:

____________________
   [*] В данной книге слова "указатель" и "ссылка"  употребляются  в  одном  и  том  же
смысле.   Если  вы  обратитесь  к  языку Си++, то обнаружите, что там эти два термина
(pointer и reference) означают разные понятия (хотя и сходные).
____________________



А. Богатырев, 1992-95                  - 124 -                              Си в UNIX

    void *malloc(unsigned size); /* size байт */
    char      *s = (char *)      malloc( strlen(buf)+1 );
    struct ST *p = (struct ST *) malloc( sizeof(struct ST));
                                  /* или sizeof(*p) */

хотя раньше принято было char *malloc();

3.7.  Поговорим про оператор sizeof.  Отметим распространенную ошибку,  когда  sizeof
принимают  за  функцию.   Это  не так! sizeof вычисляется компилятором при трансляции
программы, а не программой во время выполнения. Пусть

       char a[] = "abcdefg";
       char *b  = "hijklmn";

Тогда

       sizeof(a)    есть 8  (байт \0 на конце - считается)
       sizeof(b)    есть 2  на PDP-11 (размер указателя)
       strlen(a)    есть 7
       strlen(b)    есть 7

Если мы сделаем

       b = "This ia a new line";
       strcpy(a, "abc");

то все равно

       sizeof(b) останется равно 2
       sizeof(a)                 8

Таким образом sizeof выдает количество зарезервированной  для  переменной  памяти  (в
байтах), независимо от текущего ее содержимого.
     Операция sizeof применима даже к выражениям. В этом  случае  она  сообщает  нам,
каков будет размер у результата этого выражения.  Само выражение при этом не вычисля-
ется, так в

    double f(){ printf( "Hi!\n"); return 12.34; }
    main(){
            int x = 2; long y = 4;
            printf( "%u\n", sizeof(x + y + f()));
    }

будет напечатано значение, совпадающее с sizeof(double), а фраза "Hi!" не будет напе-
чатана.
     Когда оператор sizeof применяется к переменной (а не к  имени  типа),  можно  не
писать круглые скобки:

    sizeof(char *);   но   sizeof x;


3.8.  Напишите объединение, в котором может храниться  либо  указатель,  либо  целое,
либо действительное число.  Ответ:

    union all{
          char *s; int i; double f;
____________________
   [**] "Куча" (heap, pool) - область статической памяти, увеличивающаяся по мере надоб-
ности, и предназначенная как раз для хранения динамически отведенных данных.





А. Богатырев, 1992-95                  - 125 -                              Си в UNIX

    } x;
    x.i = 12  ; printf("%d\n", x.i);
    x.f = 3.14; printf("%f\n", x.f);
    x.s = "Hi, there"; printf("%s\n", x.s);
    printf("int=%d double=%d (char *)=%d all=%d\n",
       sizeof(int), sizeof(double), sizeof(char *),
       sizeof x);

В данном примере вы обнаружите, что размер переменной x равен максимальному из разме-
ров типов int, double, char *.
     Если вы хотите использовать одну и ту же переменную для хранения  данных  разных
типов, то для получения мобильной программы вы должны пользоваться только объединени-
ями и никогда не привязываться к длине слова и представлению  этих  типов  данных  на
конкретной  ЗВМ! Раньше, когда программисты не думали о мобильности, они писали прог-
раммы, где в одной переменой типа int хранили в зависимости от нужды то целые  значе-
ния,  то  указатели  (это было на машинах PDP и VAX).  Увы, такие программы оказались
непереносимы на машины, на которых  sizeof(int) != sizeof(char *),  более  того,  они
оказались  весьма  туманны  для понимания их другими людьми.  Не следуйте этому стилю
(такой стиль американцы называют "poor style"), более того,  всеми  силами  избегайте
его!
     Сравните два примера, использующие два стиля программирования.  Первый стиль  не
так плох, как только что описанный, но все же мы рекомендуем использовать только вто-
рой:

    /* СТИЛЬ ПЕРВЫЙ: ЯВНЫЕ ПРЕОБРАЗОВАНИЯ ТИПОВ */
    typedef void *PTR; /* универсальный указатель */
    struct a { int x, y;    PTR pa; } A;
    struct b { double u, v; PTR pb; } B;
    #define Aptr(p) ((struct a *)(p))
    #define Bptr(p) ((struct b *)(p))
    PTR ptr1, ptr2;
    main(){
        ptr1 = &A; ptr2 = &B;
        Bptr(ptr2)->u = Aptr(ptr1)->x = 77;
        printf("%f %d\n", B.u, A.x);
    }


    /* СТИЛЬ ВТОРОЙ: ОБ'ЕДИНЕНИЕ   */
    /* предварительное объявление: */
    extern struct a; extern struct b;
    /* универсальный тип данных:   */
    typedef union everything {
        int i; double d; char *s;
        struct a *ap; struct b *bp;
    } ALL;
    struct a { int x, y;    ALL pa; } A;
    struct b { double u, v; ALL pb; } B;
    ALL ptr1, ptr2, zz;
    main(){
        ptr1.ap = &A; ptr2.bp = &B; zz.i = 77;
        ptr2.bp->u = ptr1.ap->x = zz.i;
        printf("%f %d\n", B.u, A.x);
    }


3.9.  Для выделения классов символов (например цифр), следует пользоваться  макросами
из include-файла <ctype.h> Так вместо

      if( '0' <= c   &&    c <= '9' ) ...




А. Богатырев, 1992-95                  - 126 -                              Си в UNIX

следует использовать

      #include <ctype.h>
           .....
      if(isdigit(c)) ...

и вместо

      if((c >='a' &&  c <= 'z') || (c >= 'A' && c <= 'Z')) ...

надо

      if(isalpha(c)) ...

Дело в том, что сравнения < и > зависят от расположения  букв  в  используемой  коди-
ровке.  Но  например,  в  кодировке  КОИ-8  русские буквы расположены НЕ в алфавитном
порядке.  Вследствие этого, если для

            char c1, c2;
               c1 < c2

то это еще не значит, что буква c1 предшествует букве c2 в алфавите! Лексикографичес-
кое сравнение требует специальной перекодировки букв к "упорядоченной" кодировке.
     Аналогично, сравнение

            if( c >= 'а' && c <= 'я' )

скорее всего не даст ожидаемого результата.  Макроопределения же в <ctype.h>  исполь-
зуют массив флагов для каждой буквы кодировки, и потому не зависят от порядка букв (и
работают быстрее). Идея реализации такова:

    extern unsigned char _ctype[];  /*массив флагов*/
    #define US(c)  (sizeof(c)==sizeof(char)?((c)&0xFF):(c))
    /* подавление расширения знакового бита */
            /* Ф Л А Г И */
    #define _U  01  /* uppercase: большая буква    */
    #define _L  02  /* lowercase: малая буква      */
    #define _N  04  /* number:    цифра            */
    #define _S 010  /* space:     пробел           */
                    /* ... есть и другие флаги ... */
    #define isalpha(c) ((_ctype+1)[US(c)] & (_U|_L)   )
    #define isupper(c) ((_ctype+1)[US(c)] &  _U       )
    #define islower(c) ((_ctype+1)[US(c)] &     _L    )
    #define isdigit(c) ((_ctype+1)[US(c)] &        _N )
    #define isalnum(c) ((_ctype+1)[US(c)] & (_U|_L|_N))
    #define tolower(c) ((c) + 'a' - 'A' )
    #define toupper(c) ((c) + 'A' - 'a' )

где массив _ctype[] заполнен заранее (это проинициализированные статические данные) и
хранится в стандартной библиотеке Си.  Вот его фрагмент:

    unsigned char _ctype[256 /* размер алфавита */ + 1] = {
     /* EOF   код (-1)        */       0,
                  ...
     /* '1'   код  061 0x31   */      _N,
                  ...
     /* 'A'   код 0101 0x41   */      _U,
                  ...
     /* 'a'   код 0141 0x61   */      _L,
                  ...
    };



А. Богатырев, 1992-95                  - 127 -                              Си в UNIX

Выигрыш в скорости получается вот почему: если мы определим[*]

    #define isalpha(c) (((c) >= 'a' && (c) <= 'z') || \
                        ((c) >= 'A' && (c) <= 'Z'))

то этот оператор состоит из 7 операций. Если же мы используем  isalpha  из  <ctype.h>
(как  определено  выше)  -  мы  используем только две операции: индексацию и проверку
битовой маски &.  Операции _ctype+1 и _U|_L вычисляются до констант еще при  компиля-
ции, и поэтому не вызывают генерации машинных команд.
     Определенные выше toupper и tolower работают верно лишь в  кодировке  ASCII[**],  в
которой все латинские буквы расположены подряд и по алфавиту.  Обратите внимание, что
tolower имеет смысл применять только к большим буквам, а toupper - только  к  малень-
ким:

    if( isupper(c) )  c = tolower(c);

Существует еще черезвычайно полезный макрос isspace(c), который можно было бы опреде-
лить как

    #define isspace(c) (c==' ' ||c=='\t'||c=='\f'|| \
                        c=='\n'||c=='\r')
            или
    #define isspace(c) (strchr(" \t\f\n\r",(c)) != NULL)

На самом деле он, конечно, реализован через флаги в _ctype[].   Он  используется  для
определения  символов-пробелов,  служащих  заполнителями  промежутков  между  словами
текста.
     Есть еще два нередко используемых макроса: isprint(c), проверяющий, является  ли
c  ПЕЧАТНЫМ  символом,  т.е. имеющим изображение на экране; и iscntrl(c), означающий,
что символ c является управляющим, т.е. при его выводе на терминал ничего не  изобра-
зится,  но терминал произведет некоторое действие, вроде очистки экрана или перемеще-
ния курсора в каком-то направлении.  Они нужны, как правило, для отображения управля-
ющих ("контроловских") символов в специальном печатном виде, вроде ^A для кода '\01'.
     Задание: исследуйте кодировку  и <ctype.h> на  вашей  машине.  Напишите  функцию
лексикографического сравнения букв и строк.
     Указание: пусть буквы имеют такие коды (это не соответствует реальности!):

      буква:      а   б   в   г   д   е
      код:        1   4   2   5   3   0

      нужно:      0   1   2   3   4   5

Тогда идея функции Ctou перекодировки к упорядоченному алфавиту такова:

        unsigned char UU[] = { 5, 0, 2, 4, 1, 3 };
        /* в действительности - 256 элементов: UU[256] */

        Ctou(c) unsigned char c; { return UU[c]; }

        int strcmp(s1, s2) char *s1, *s2; {
            /* Проигнорировать совпадающие начала строк */
            while(*s1 && *s1 == *s2) s1++, s2++;
            /* Вернуть разность [не]совпавших символов  */
            return Ctou(*s1) - Ctou(*s2);
____________________
   [*] Обратите внимание, что символ \ в конце строки макроопределения  позволяет  про-
должить макрос на следующей строке, поэтому макрос может состоять из многих строк.
   [**] ASCII - American Standard Code for Information Interchange - наиболее  распрост-
раненная в мире кодировка (Американский стандарт).





А. Богатырев, 1992-95                  - 128 -                              Си в UNIX

        }

Разберитесь с принципом формирования массива UU.

3.10.  В современных UNIX-ах с поддержкой различных языков таблица ctype  загружается
из  некоторых  системных файлов - для каждого языка своя.  Для какого языка - выбира-
ется по содержимому переменной окружения LANG.  Если переменная не задана - использу-
ется значение "C", английский язык.  Загрузка таблиц должна происходить явно, вызовом

            ...
            #include <locale.h>
            ...
            main(){
                    setlocale(LC_ALL, "");
                    ...
                    все остальное
                    ...
            }


3.11.  Вернемся к нашей любимой проблеме со знаковым битом у типа char.

    #include <stdio.h>
    #include <locale.h>
    #include <ctype.h>

    int main(int ac, char *av[]){
            char c;
            char *string = "абвгдежзиклмноп";

            setlocale(LC_ALL, "");

            for(;c = *string;string++){
    #ifdef DEBUG
                    printf("%c %d %d\n", *string, *string, c);
    #endif
                    if(isprint(c)) printf("%c - печатный символ\n", c);
            }
            return 0;
    }

Эта программа неожиданно печатает

    % a.out
    в - печатный символ
    з - печатный символ

И все.  В чем дело???
Рассмотрим к примеру символ 'г'. Его код '\307'.  В операторе

    c = *string;

Символ c получает значение -57 (десятичное), которое ОТРИЦАТЕЛЬНО.  В системном файле
/usr/include/ctype.h макрос isprint определен так:

    #define isprint(c)      ((_ctype + 1)[c] & (_P|_U|_L|_N|_B))

И значение c используется в нашем случае как  отрицательный  индекс  в  массиве,  ибо
индекс  приводится  к  типу int (signed). Откуда теперь извлекается значение флагов -
нам неизвестно; можно только с уверенностью сказать, что НЕ из массива _ctype.




А. Богатырев, 1992-95                  - 129 -                              Си в UNIX

Проблему решает либо использование

    isprint(c & 0xFF)

либо

    isprint((unsigned char) c)

либо объявление в нашем примере

    unsigned char c;

В первом случае мы явно приводим signed к unsigned битовой операцией, обнуляя  лишние
биты.   Во втором и третьем - unsigned char расширяется в unsigned int, который оста-
нется положительным. Вероятно, второй путь предпочтительнее.

3.12.  Итак, снова напомним, что русские буквы char, а не unsigned char дают  отрица-
тельные индексы в массиве.

    char c = 'г';
    int x[256];

            ...x[c]...            /* индекс < 0 */
            ...x['г']...

Поэтому байтовые индексы должны быть либо unsigned char, либо & 0xFF.  Как в  следую-
щем примере:

    /* Программа преобразования символов в файле: транслитерация
                      tr abcd prst  заменяет строки
                      xxxxdbcaxxxx -> xxxxtrspxxxx
       По мотивам книги М.Дансмура и Г.Дейвиса.
    */
    #include <stdio.h>

    #define ASCII 256 /* число букв в алфавите ASCII */
    /* BUFSIZ определено в stdio.h */
    char mt[ ASCII ];       /* таблица перекодировки */

    /* начальная разметка таблицы */
    void mtinit(){
            register int i;
            for( i=0; i < ASCII; i++ )
                    mt[i] = (char) i;
    }



















А. Богатырев, 1992-95                  - 130 -                              Си в UNIX

    int main(int argc, char *argv[])
    {
            register char *tin, *tout; /* unsigned char */
            char buffer[ BUFSIZ ];

            if( argc != 3 ){
                    fprintf( stderr, "Вызов: %s что наЧто\n", argv[0] );
                    return(1);
            }
            tin  = argv[1]; tout = argv[2];

            if( strlen(tin) != strlen(tout)){
                    fprintf( stderr, "строки разной длины\n" );
                    return(2);
            }

            mtinit();
            do{
                    mt[ (*tin++) & 0xFF ]  = *tout++;
                    /*   *tin - имеет тип char.
                     *   & 0xFF подавляет расширение знака
                     */
            } while( *tin );

            tout = mt;
            while( fgets( buffer, BUFSIZ, stdin ) != NULL ){
                    for( tin = buffer; *tin; tin++ )
                            *tin = tout[ *tin & 0xFF ];
                    fputs( buffer, stdout );
            }
            return(0);
    }


3.13.

    int main(int ac, char *av[]){
            char c = 'г';
            if('a' <= c && c < 256)
                    printf("Это одна буква.\n");
            return 0;
    }

Увы, эта программа не печатает НИЧЕГО. Просто потому, что signed char в сравнении  (в
операторе if) приводится к типу int.  А как целое число - русская буква отрицательна.
Снова  решением  является  либо  использование  везде  (c & 0xFF),  либо   объявление
unsigned char c.   В частности, этот пример показывает, что НЕЛЬЗЯ просто так сравни-
вать две переменные типа char. Нужно принимать предохранительные меры  по  подавлению
расширения знака:

    if((ch1 & 0xFF) < (ch2 & 0xFF))...;

Для unsigned char такой проблемы не будет.

3.14.  Почему неверно:









А. Богатырев, 1992-95                  - 131 -                              Си в UNIX

    #include <stdio.h>
    main(){
            char c;

            while((c = getchar()) != EOF)
                    putchar(c);
    }

Потому что c описано как char, в то время как EOF - значение типа int равное (-1).
     Русская буква "Большой твердый знак" в кодировке КОИ-8 имеет код '\377'  (0xFF).
Если  мы подадим на вход этой программе эту букву, то в сравнении signed char со зна-
чением знакового целого EOF, c будет приведено тоже к знаковому целому -  расширением
знака.   0xFF  превратится  в (-1), что означает, что поступил символ EOF. Сюрприз!!!
Посему данная программа будет делать вид, что в любом файле с большим русским твердым
знаком после этого знака (и включая его) дальше ничего нет. Что есть досадное заблуж-
дение.
     Решением служит ПРАВИЛЬНОЕ объявление int c.

3.15.  Изучите поведение программы

    #define TYPE char

    void f(TYPE c){
            if(c == 'й') printf("Это буква й\n");
            printf("c=%c c=\\%03o c=%03d c=0x%0X\n", c, c, c, c);
    }

    int main(){
            f('г'); f('й');
            f('z'); f('Z');
            return 0;
    }

когда TYPE определено как char, unsigned char, int.  Объясните  поведение.  Выдачи  в
этих трех случаях таковы (int == 32 бита):

    c=г c=\37777777707 c=-57 c=0xFFFFFFC7
    Это буква й
    c=й c=\37777777712 c=-54 c=0xFFFFFFCA
    c=z c=\172 c=122 c=0x7A
    c=Z c=\132 c=090 c=0x5A

    c=г c=\307 c=199 c=0xC7
    c=й c=\312 c=202 c=0xCA
    c=z c=\172 c=122 c=0x7A
    c=Z c=\132 c=090 c=0x5A

    и снова как 1 случай.

Рассмотрите альтернативу

            if(c == (unsigned char) 'й') printf("Это буква й\n");

где предполагается, что знак у русских букв и у c НЕ расширяется.   В  данном  случае
фраза  'Это буква й' не печатается ни с типом char, ни с типом int, поскольку в срав-
нении c приводится к типу signed int расширением знакового бита  (который  равен  1).
Слева получается отрицательное число!
     В таких случаях вновь следует писать

            if((unsigned char)c == (unsigned char)'й') printf("Это буква й\n");




А. Богатырев, 1992-95                  - 132 -                              Си в UNIX

3.16.  Обычно возникают проблемы при написании функций с переменным  числом  аргумен-
тов.   В языке Си эта проблема решается использованием макросов va_args, не зависящих
от соглашений о вызовах функций на данной машине, и использующих эти  макросы  специ-
альных   функций.   Есть  два  стиля  оформления  таких  программ:  с  использованием
<varargs.h> и <stdarg.h>.  Первый был продемонстрирован в  первой  главе  на  примере
функции poly().  Для иллюстрации второго приведем пример функции трассировки, записы-
вающей собщение в файл:

    #include <stdio.h>
    #include <stdarg.h>
    void trace(char *fmt, ...) {
        va_list args;
        static FILE *fp = NULL;

        if(fp == NULL){
           if((fp = fopen("TRACE", "w")) == NULL) return;
        }
        va_start(args, fmt);
        /* второй аргумент: арг-т после которого
         * в заголовке функции идет ... */
        vfprintf(fp, fmt, args); /* библиотечная ф-ция */
        fflush(fp);     /* вытолкнуть сообщение в файл */
        va_end(args);
    }


    main(){ trace( "%s\n", "Go home.");
            trace( "%d %d\n", 12, 34);
    }

Символ `...' (троеточие) в заголовке функции обозначает переменный (возможно  пустой)
список  аргументов.  Он  должен  быть  самым последним, следуя за всеми обязательными
аргументами функции.
     Макрос va_arg(args,type), извлекающий из  переменного  списка  аргументов  `...'
очередное  значение типа type, одинаков в обоех моделях.  Функция vfprintf может быть
написана через функцию vsprintf (в действительности обе функции - стандартные):

    int vfprintf(FILE *fp, const char *fmt, va_list args){
        /*static*/ char buffer[1024]; int res;
        res = vsprintf(buffer, fmt, args);
        fputs(buffer, fp); return res;
    }

Функция vsprintf(str,fmt,args); аналогична функции sprintf(str,fmt,...) -  записывает
преобразованную по формату строку в байтовый массив str, но используется в контексте,
подобном приведенному.  В конец сформированной строки sprintf записывает '\0'.

3.17.  Напишите функцию printf, понимающую форматы %c (буква), %d (целое), %o  (вось-
меричное),  %x  (шестнадцатеричное),  %b  (двоичное),  %r (римское), %s (строка), %ld
(длинное целое).  Ответ смотри в приложении.

3.18.  Для того, чтобы один и тот же исходный текст программы транслировался на  раз-
ных  машинах  (в разных системах), приходится выделять в программе системно-зависимые
части.  Такие части должны по-разному выглядеть на разных машинах, поэтому их  оформ-
ляют в виде так называемых "условно компилируемых" частей:

    #ifdef XX
            ... вариант1
    #else
            ... вариант2
    #endif



А. Богатырев, 1992-95                  - 133 -                              Си в UNIX

Эта директива препроцессора ведет себя следующим образом: если макрос с именем XX был
определен
    #define XX
то в программу подставляется вариант1, если же нет - вариант2. Оператор #else не обя-
зателен  - при его отсутствии вариант2 пуст. Существует также оператор #ifndef, кото-
рый подставляет вариант1 если макрос XX не определен.  Есть еще и  оператор  #elif  -
else if:

    #ifdef макро1
      ...
    #elif  макро2
      ...
    #else
      ...
    #endif

Определить макрос можно не только при помощи #define, но и при помощи ключа  компиля-
тора, так

    cc -DXX file.c ...

соответствует включению в начало файла file.c директивы

    #define XX

А для программы

    main(){
    #ifdef XX
            printf( "XX = %d\n", XX);
    #else
            printf( "XX undefined\n");
    #endif
    }

ключ

    cc -D"XX=2" file.c ...

эквивалентен заданию директивы

    #define XX 2

Что будет, если совсем не задать ключ -D в данном примере?
     Этот прием используется в частности в тех случаях,  когда  какие-то  стандартные
типы или функции в данной системе носят другие названия:

    cc -Dvoid=int ...
    cc -Dstrchr=index ...

В некоторых системах компилятор автоматически  определяет  специальные  макросы:  так
компиляторы в UNIX неявно подставляют один из ключей (или несколько сразу):

            -DM_UNIX
            -DM_XENIX
            -Dunix
            -DM_SYSV
            -D__SVR4
            -DUSG
            ... бывают и другие




А. Богатырев, 1992-95                  - 134 -                              Си в UNIX

Это позволяет программе "узнать", что ее компилируют для системы  UNIX.   Более  под-
робно про это написано в документации по команде cc.

3.19.  Оператор #ifdef применяется в include-файлах, чтобы исключить повторное  вклю-
чение одного и того же файла.  Пусть файлы aa.h и bb.h содержат

           aa.h                        bb.h
    #include "cc.h"                 #include "cc.h"
    typedef unsigned long ulong;    typedef int cnt_t;

А файлы cc.h и 00.c содержат

           cc.h                        00.c
           ...                      #include "aa.h"
    struct II { int x, y; };        #include "bb.h"
           ...                      main(){ ... }

В этом случае текст файла cc.h будет вставлен в 00.c дважды: из aa.h и из  bb.h.  При
компиляции  00.c  компилятор  сообщит "Переопределение структуры II".  Чтобы include-
файл не подставлялся еще раз, если он уже однажды  был  включен,  придуман  следующий
прием - следует оформлять файлы включений так:

    /* файл   cc.h */
    #ifndef  _CC_H
    # define _CC_H  /* определяется при первом включении */
            ...
            struct II { int x, y; };
            ...
    #endif /* _CC_H */

Второе и последующие включения такого файла будут подставлять  пустое  место,  что  и
требуется.    Для   файла   <sys/types.h>   было   бы  использовано  макроопределение
_SYS_TYPES_H.

3.20.  Любой макрос можно отменить, написав директиву

        #undef имяМакро

Пример:

    #include <stdio.h>
    #undef M_UNIX
    #undef M_SYSV
    main() {
            putchar('!');
    #undef  putchar
    #define putchar(c) printf( "Буква '%c'\n", c);
            putchar('?');

    #if defined(M_UNIX) || defined(M_SYSV)
    /* или просто #if M_UNIX */
            printf("Это UNIX\n");
    #else
            printf("Это не UNIX\n");
    #endif /* UNIX */
    }

Обычно #undef используется именно для переопределения макроса,  как  putchar  в  этом
примере (дело в том, что putchar - это макрос из <stdio.h>).
     Директива #if, использованная нами,  является  расширением  оператора  #ifdef  и
подставляет текст если выполнено указанное условие:



А. Богатырев, 1992-95                  - 135 -                              Си в UNIX

    #if  defined(MACRO)  /* равно #ifdef(MACRO)  */
    #if !defined(MACRO)  /* равно #ifndef(MACRO) */
    #if VALUE > 15       /* если целая константа
                            #define VALUE 25
                            больше 15 (==, !=, <=, ...) */
    #if COND1 || COND2   /* если верно любое из условий */
    #if COND1 && COND2   /* если верны оба условия      */

Директива #if допускает использование в качестве аргумента довольно  сложных  выраже-
ний, вроде

    #if !defined(M1) && (defined(M2) || defined(M3))


3.21.  Условная компиляция может использоваться для трассировки программ:

    #ifdef DEBUG
    # define DEBUGF(body)   \
    {                       \
            body;           \
    }
    #else
    # define DEBUGF(body)
    #endif

    int f(int x){   return x*x; }
    int main(int ac, char *av[]){
            int x = 21;
            DEBUGF(x = f(x); printf("%s equals to %d\n", "x", x));
            printf("x=%d\n", x);
    }

При компиляции

    cc -DDEBUG file.c

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

3.22.  В языке C++ (развитие языка Си) слова class, delete,  friend,  new,  operator,
overload,  template,  public, private, protected, this, virtual являются зарезервиро-
ванными (ключевыми).  Это может вызвать небольшую проблему при переносе текста  прог-
раммы на Си в систему программирования C++, например:

    #include <termio.h>
      ...
    int fd_tty = 2;   /* stderr */
    struct termio old, new;
    ioctl (fd_tty, TCGETA, &old);
    new = old;
    new.c_lflag |= ECHO | ICANON;
    ioctl (fd_tty, TCSETAW, &new);
      ...

Строки, содержащие имя переменной (или функции) new, окажутся  неправильными  в  C++.
Проще всего эта проблема решается переименованием переменной (или функции).  Чтобы не
производить правки во всем тексте, достаточно переопределить имя при помощи директивы
define:






А. Богатырев, 1992-95                  - 136 -                              Си в UNIX

    #define new    new_modes
      ... старый текст ...
    #undef new

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