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.

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

    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;
    }
    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.

Почему неверно:
    #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");

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

Эта директива препроцессора ведет себя следующим образом: если макрос с именем 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
            ... бывают и другие

Это позволяет программе "узнать", что ее компилируют для системы 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 и подставляет текст если выполнено указанное условие:

    #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:

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

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

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

Назад | Содержание | Вперед