HTML page

1.  Простые программы и алгоритмы. Сюрпризы, советы.

1.1.  Составьте программу приветствия с использованием функции  printf.  По  традиции
принято печатать фразу "Hello, world !" ("Здравствуй, мир !").

1.2.  Найдите ошибку в программе

    #include <stdio.h>
    main(){
        printf("Hello, world\n");
    }

Ответ: раз не объявлено иначе, функция main  считается  возвращающей  целое  значение
(int).  Но  функция  main  не  возвращает ничего - в ней просто нет оператора return.
Корректно было бы так:

    #include <stdio.h>
    main(){
        printf("Hello, world\n");
        return 0;
    }

или

    #include <stdio.h>
    void main(){
        printf("Hello, world\n");
        exit(0);
    }

а уж совсем корректно - так:

    #include <stdio.h>
    int main(int argc, char *argv[]){
        printf("Hello, world\n");
        return 0;
    }


1.3.  Найдите ошибки в программе

        #include studio.h
        main
        {
            int i
            i := 43
            print ('В году i недель')
        }


1.4.  Что будет напечатано в приведенном  примере,  который  является  частью  полной
программы:

            int n;
            n = 2;
            printf ("%d + %d = %d\n", n, n, n + n);


1.5.  В чем состоят ошибки?





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

            if( x > 2 )
            then    x = 2;
            if  x < 1
                    x = 1;

Ответ: в Си нет ключевого слова then, условия в операторах if, while должны браться в
()-скобки.

1.6.  Напишите программу, печатающую ваше имя, место работы и адрес. В  первом  вари-
анте программы используйте библиотечную функцию printf, а во втором - puts.

1.7.  Составьте программу с использованием следующих постфиксных и префиксных  опера-
ций:

            a = b = 5
            a + b
            a++ + b
            ++a + b
            --a + b
            a-- + b

Распечатайте полученные значения и проанализируйте результат.

1.8.
Цикл for
     ________________________________________________________________________________


         for(INIT; CONDITION; INCR)
                 BODY

     ________________________________________________________________________________


                 INIT;
         repeat:
                 if(CONDITION){
                         BODY;
                 cont:
                         INCR;
                         goto repeat;
                 }
         out:    ;

Цикл while
     ________________________________________________________________________________


         while(COND)
                 BODY

     ________________________________________________________________________________


         cont:
         repeat:
                 if(CONDITION){
                         BODY;
                         goto repeat;
                 }
         out:    ;



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

Цикл do
     ________________________________________________________________________________


         do
                 BODY
         while(CONDITION)

     ________________________________________________________________________________


         cont:
         repeat:
                 BODY;
                 if(CONDITION) goto repeat;
         out:    ;

     В операторах цикла внутри тела цикла BODY могут присутствовать операторы break и
     continue; которые означают на наших схемах следующее:

         #define break     goto out
         #define continue  goto cont


1.9.  Составьте программу печати прямоугольного треугольника из звездочек

              *
              **
              ***
              ****
              *****

используя цикл for. Введите переменную, значением которой является размер катета тре-
угольника.

1.10.  Напишите операторы Си, которые выдают строку длины WIDTH,  в  которой  сначала
содержится  x0  символов '-', затем w символов '*', и до конца строки - вновь символы
'-'.  Ответ:

    int x;
    for(x=0; x < x0;     ++x) putchar('-');
    for(   ; x < x0 + w; x++) putchar('*');
    for(   ; x < WIDTH ; ++x) putchar('-');
    putchar('\n');

либо

    for(x=0; x < WIDTH; x++)
            putchar( x < x0     ? '-' :
                     x < x0 + w ? '*' :
                                  '-' );
    putchar('\n');


1.11.  Напишите программу с циклами, которая рисует треугольник:

                    *
                   ***
                  *****
                 *******
                *********



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

Ответ:

    /* Треугольник из звездочек */
    #include <stdio.h>

    /* Печать n символов c */
    printn(c, n){
            while( --n >= 0 )
                    putchar(c);
    }

    int lines = 10;         /* число строк треугольника */
    void main(argc, argv) char *argv[];
    {
            register int nline;  /* номер строки */
            register int naster; /* количество звездочек в строке */
            register int i;

            if( argc > 1 )
                    lines = atoi( argv[1] );

            for( nline=0; nline < lines ; nline++ ){
                    naster = 1 + 2 * nline;

                    /* лидирующие пробелы */
                    printn(' ', lines-1  - nline);

                    /* звездочки */
                    printn('*', naster);

                    /* перевод строки */
                    putchar( '\n' );
            }
            exit(0);        /* завершение программы */
    }


1.12.  В чем состоит ошибка?

    main(){  /* печать фразы 10 раз */
       int i;
       while(i < 10){
           printf("%d-ый раз\n", i+1);
           i++;
       }
    }

Ответ: автоматическая переменная i не была проинициализирована и  содержит  не  0,  а
какое-то  произвольное  значение.  Цикл может выполниться не 10, а любое число раз (в
том числе и 0 по случайности).  Не забывайте  инициализировать  переменные,  возьмите
описание с инициализацией за правило!

       int i = 0;

Если бы переменная i была статической, она бы имела начальное значение 0.
     В данном примере было бы еще лучше использовать цикл for, в котором все операции
над индексом цикла собраны в одном месте - в заголовке цикла:

    for(i=0; i < 10; i++) printf(...);





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

1.13.  Вспомогательные переменные, не несущие смысловой нагрузки (вроде счетчика пов-
торений  цикла,  не  используемого в самом теле цикла) принято по традиции обозначать
однобуквенными именами, вроде i, j. Более того, возможны даже такие курьезы:

    main(){
      int  _ ;
      for( _ = 0; _ < 10; _++) printf("%d\n", _ );
    }

основанные на том, что подчерк в идентификаторах - полноправная буква.

1.14.  Найдите 2 ошибки в программе:

            main(){
                    int x = 12;

                    printf( "x=%d\n" );
                    int y;
                    y = 2 * x;
                    printf( "y=%d\n", y );
            }

Комментарий: в теле функции все описания должны идти перед всеми выполняемыми  опера-
торами  (кроме операторов, входящих в состав описаний с инициализацией).  Очень часто
после внесения правок в программу некоторые описания  оказываются  после  выполняемых
операторов.   Именно  поэтому  рекомендуется  отделять  строки описания переменных от
выполняемых операторов пустыми строками (в этой книге это часто не делается для  эко-
номии места).

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

    int n;
    n = 12;
    main(){
            int y;
            y = n+2;
            printf( "%d\n", y );
    }

Ответ: выполняемый оператор n=12 находится  вне  тела  какой-либо  функции.   Следует
внести  его  в  main()  после описания переменной y, либо переписать объявление перед
main() в виде

    int n = 12;

В последнем случае присваивание переменной n значения 12 выполнит компилятор  еще  во
время  компиляции программы, а не сама программа при своем запуске. Точно так же про-
исходит со всеми статическими данными (описанными как static, либо расположенными вне
всех функций); причем если их начальное значение не указано явно - то подразумевается
0 ('\0', NULL, "").  Однако нулевые значения не хранятся в скомпилированном выполняе-
мом файле, а требуемая "чистая" память расписывается при старте программы.

1.16.  По поводу описания переменной с инициализацией:

    TYPE x = выражение;

является (почти) эквивалентом для

    TYPE x;         /* описание */
    x = выражение;  /* вычисление начального значения */




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

Рассмотрим пример:

    #include <stdio.h>
    extern double sqrt();   /* квадратный корень */
    double x   = 1.17;
    double s12 = sqrt(12.0);            /* #1 */
    double y   = x * 2.0;               /* #2 */
    FILE  *fp  = fopen("out.out", "w"); /* #3 */
    main(){
      double ss = sqrt(25.0) + x;       /* #4 */
      ...
    }

Строки с метками #1, #2 и #3 ошибочны. Почему?
     Ответ: при инициализации статических данных (а s12, y и fp таковыми и  являются,
так  как описаны вне какой-либо функции) выражение должно содержать только константы,
поскольку оно вычисляется КОМПИЛЯТОРОМ. Поэтому ни использование значений переменных,
ни вызовы функций здесь недопустимы (но можно брать адреса от переменных).
     В строке #4 мы инициализируем автоматическую переменную ss, т.е.  она  отводится
уже  во  время  выполнения программы. Поэтому выражение для инициализации вычисляется
уже не компилятором, а самой программой, что дает нам право использовать  переменные,
вызовы функций и.т.п., то есть выражения языка Си без ограничений.

1.17.  Напишите  программу,  реализующую  эхо-печать  вводимых  символов.   Программа
должна  завершать  работу  при получении признака EOF.  В UNIX при вводе с клавиатуры
признак EOF обычно обозначается одновременным нажатием клавиш CTRL  и  D  (CTRL  чуть
раньше),  что  в  дальнейшем  будет  обозначаться CTRL/D; а в MS DOS - клавиш CTRL/Z.
Используйте getchar() для ввода буквы и putchar() для вывода.

1.18.  Напишите программу, подсчитывающую число символов поступающих со  стандартного
ввода. Какие достоинства и недостатки у следующей реализации:

    #include <stdio.h>
    main(){ double cnt = 0.0;
            while (getchar() != EOF) ++cnt;
            printf("%.0f\n", cnt );
    }

Ответ: и достоинство и недостаток в том, что счетчик имеет тип double.  Достоинство -
можно  подсчитать очень большое число символов; недостаток - операции с double обычно
выполняются гораздо медленнее, чем с int и long  (до  десяти  раз),  программа  будет
работать  дольше.   В  повседневных  задачах  вам  вряд ли понадобится иметь счетчик,
отличный от long cnt; (печатать его надо по формату "%ld").

1.19.  Составьте программу перекодировки вводимых символов со стандартного  ввода  по
следующему правилу:

            a -> b
            b -> c
            c -> d
            ...
            z -> a
     другой символ -> *

Коды строчных латинских букв расположены подряд по возрастанию.

1.20.  Составьте программу перекодировки вводимых символов со стандартного  ввода  по
следующему правилу:






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

            B -> A
            C -> B
            ...
            Z -> Y
     другой символ -> *

Коды прописных латинских букв также расположены по возрастанию.

1.21.  Напишите программу, печатающую номер и код введенного символа в восьмеричном и
шестнадцатеричном  виде.   Заметьте,  что если вы наберете на вводе строку символов и
нажмете клавишу ENTER, то программа напечатает вам на один символ больше, чем вы наб-
рали.   Дело  в  том,  что код клавиши ENTER, завершившей ввод строки - символ '\n' -
тоже попадает в вашу программу (на экране  он  отображается  как  перевод  курсора  в
начало следующей строки!).

1.22.  Разберитесь, в чем состоит разница между символами '0'  (цифра  нуль)  и  '\0'
(нулевой байт). Напечатайте

      printf( "%d %d %c\n", '\0', '0', '0' );

Поставьте опыт: что печатает программа?

    main(){
            int c = 060;  /* код символа '0' */
            printf( "%c %d %o\n", c, c, c);
    }

Почему печатается 0 48 60?  Теперь напишите вместо
  int c = 060;
строчку
  char c = '0';

1.23.  Что напечатает программа?

    #include <stdio.h>
    void main(){
            printf("ab\0cd\nxyz");
            putchar('\n');
    }

Запомните, что '\0' служит признаком конца строки в памяти, а '\n' - в файле.  Что  в
строке "abcd\n" на конце неявно уже расположен нулевой байт:

    'a','b','c','d','\n','\0'

Что строка "ab\0cd\nxyz" - это

    'a','b','\0','c','d','\n','x','y',z','\0'

Что строка "abcd\0" - избыточна, поскольку будет иметь на  конце  два  нулевых  байта
(что  не  вредно,  но зачем?).  Что printf печатает строку до нулевого байта, а не до
закрывающей кавычки.
Программа эта напечатает ab и перевод строки.
Вопрос: чему равен sizeof("ab\0cd\nxyz")?  Ответ: 10.

1.24.  Напишите программу, печатающую целые числа от 0 до 100.

1.25.  Напишите программу, печатающую квадраты и кубы целых чисел.






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

1.26.  Напишите программу, печатающую сумму квадратов первых n целых чисел.

1.27.  Напишите программу, которая переводит секунды в дни, часы, минуты и секунды.

1.28.  Напишите программу, переводящую скорость из километров в час в метры в  секун-
дах.

1.29.  Напишите программу, шифрующую текст файла путем замены значения символа  (нап-
ример, значение символа C заменяется на C+1 или на ~C ).

1.30.  Напишите программу, которая при введении с клавиатуры буквы печатает на терми-
нале  ключевое  слово,  начинающееся с данной буквы. Например, при введении буквы 'b'
печатает "break".

1.31.  Напишите программу, отгадывающую задуманное вами число в пределах от 1 до 200,
пользуясь  подсказкой с клавиатуры "=" (равно), "<" (меньше) и ">" (больше). Для уга-
дывания числа используйте метод деления пополам.

1.32.  Напишите программу, печатающую степени двойки

            1, 2, 4, 8, ...

Заметьте, что, начиная с некоторого n, результат становится отрицательным из-за пере-
полнения целого.

1.33.  Напишите подпрограмму вычисления квадратного  корня  с  использованием  метода
касательных (Ньютона):

       x(0)   =  a

                 1      a
       x(n+1) =  - * ( ----  + x(n))
                 2     x(n)

Итерировать, пока не будет | x(n+1) - x(n) | < 0.001
     Внимание! В данной задаче массив не нужен. Достаточно хранить текущее и предыду-
щее значения x и обновлять их после каждой итерации.

1.34.  Напишите программу, распечатывающую простые числа до 1000.

            1, 2, 3, 5, 7, 11, 13, 17, ...























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

    /*#!/bin/cc primes.c -o primes -lm
     *        Простые числа.
     */
    #include <stdio.h>
    #include <math.h>
    int debug = 0;

    /* Корень квадратный из числа по методу Ньютона */
    #define eps 0.0001
    double  sqrt (x) double x;
    {
        double  sq, sqold, EPS;

        if (x < 0.0)
            return -1.0;
        if (x == 0.0)
            return 0.0;  /* может привести к делению на 0 */
        EPS = x * eps;
        sq = x;
        sqold = x + 30.0;         /* != sq */
        while (fabs (sq * sq - x) >= EPS) {
        /*     fabs( sq - sqold )>= EPS    */
            sqold = sq;
            sq = 0.5 * (sq + x / sq);
        }
        return sq;
    }


    /* таблица прoстых чисел */
    int is_prime (t) register int    t; {
        register int    i, up;
        int             not_div;

        if (t == 2 || t == 3 || t == 5 || t == 7)
            return 1;               /* prime */
        if (t % 2 == 0 || t == 1)
            return 0;               /* composite */
        up = ceil (sqrt ((double) t)) + 1;
        i = 3;
        not_div = 1;
        while (i <= up && not_div) {
            if (t % i == 0) {
                if (debug)
                    fprintf (stderr, "%d поделилось на %d\n",
                                       t,               i);
                not_div = 0;
                break;
            }
            i += 2;  /*
                      * Нет смысла проверять четные,
                      * потому что если делится на 2*n,
                      * то делится и на 2,
                      * а этот случай уже обработан выше.
                      */
        }
        return not_div;
    }






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

    #define COL 6
    int     n;
    main (argc, argv) char **argv;
    {
        int     i,
                j;
        int     n;

        if( argc < 2 ){
            fprintf( stderr, "Вызов: %s число [-]\n", argv[0] );
            exit(1);
        }
        i = atoi (argv[1]); /* строка -> целое, ею изображаемое */
        if( argc > 2 ) debug = 1;

        printf ("\t*** Таблица простых чисел от 2 до %d ***\n", i);
        n = 0;
        for (j = 1; j <= i; j++)
            if (is_prime (j)){

                /* распечатка в COL колонок */
                printf ("%3d%s", j, n == COL-1 ? "\n" : "\t");
                if( n == COL-1 ) n = 0;
                else             n++;
            }
        printf( "\n---\n" );
        exit (0);
    }


1.35.  Составьте программу ввода двух комплексных чисел в виде A + B * I  (каждое  на
отдельной строке) и печати их произведения в том же виде. Используйте scanf и printf.
Перед тем, как использовать scanf, проверьте себя: что неверно в нижеприведенном опе-
раторе?

    int x;
    scanf( "%d", x );

Ответ: должно быть написано "АДРЕС от x", то есть scanf( "%d", &x );

1.36.  Напишите  подпрограмму  вычисления  корня  уравнения  f(x)=0  методом  деления
отрезка пополам.  Приведем реализацию этого алгоритма для поиска целочисленного квад-
ратного корня из целого числа (этот алгоритм может использоваться, например, в машин-
ной графике при рисовании дуг):

    /* Максимальное unsigned long число */
    #define MAXINT (~0L)
    /* Определим имя-синоним для типа unsigned long */
    typedef unsigned long ulong;
    /* Функция, корень которой мы ищем: */
    #define FUNC(x, arg)  ((x) * (x) - (arg))
    /* тогда x*x - arg = 0 означает  x*x = arg, то есть
     * x = корень_квадратный(arg)     */
    /* Начальный интервал. Должен выбираться исходя из
     * особенностей функции FUNC */
    #define  LEFT_X(arg)  0
    #define RIGHT_X(arg) (arg > MAXINT)? MAXINT : (arg/2)+1;

    /* КОРЕНЬ КВАДРАТНЫЙ, округленный вниз до целого.
     * Решается по методу деления отрезка пополам:
     *    FUNC(x, arg) = 0;       x = ?



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

     */
    ulong i_sqrt( ulong arg ) {
       register ulong   mid,   /* середина интервала    */
                        rgt,   /* правый край интервала */
                        lft;   /* левый край интервала  */
       lft = LEFT_X(arg); rgt = RIGHT_X(arg);

       do{ mid = (lft + rgt + 1 )/2;
    /* +1 для ошибок округления при целочисленном делении */
           if( FUNC(mid, arg) > 0 ){
                  if( rgt == mid ) mid--;
                  rgt =  mid ;  /* приблизить правый край */
           } else lft =  mid ;  /* приблизить левый край  */
       } while( lft < rgt );
       return mid;
    }
    void main(){ ulong i;
       for(i=0; i <= 100; i++)
           printf("%ld -> %lu\n", i, i_sqrt(i));
    }

Использованное нами при объявлении переменных ключевое слово register  означает,  что
переменная  является ЧАСТО ИСПОЛЬЗУЕМОЙ, и компилятор должен попытаться разместить ее
на регистре процессора, а не в стеке (за счет чего увеличится  скорость  обращения  к
этой переменной). Это слово используется как

    register тип переменная;
    register переменная; /* подразумевается тип int */

От регистровых переменных нельзя брать адрес: &переменная ошибочно.

1.37.  Напишите программу, вычисляющую числа треугольника Паскаля и печатающую  их  в
виде треугольника.

            C(0,n)   = C(n,n)   = 1          n = 0...
            C(k,n+1) = C(k-1,n) + C(k,n)     k = 1..n
            n - номер строки

В разных вариантах используйте циклы, рекурсию.

1.38.  Напишите функцию вычисления определенного интеграла методом  Монте-Карло.  Для
этого  вам придется написать генератор случайных чисел.  Си предоставляет стандартный
датчик ЦЕЛЫХ равномерно распределенных псевдослучайных чисел: если вы хотите получить
целое число из интервала [A..B], используйте

    int x = A + rand() % (B+1-A);

Чтобы получать разные последовательности следует задавать  некий  начальный  параметр
последовательности (это называется "рандомизация") при помощи

    srand( число ); /* лучше нечетное */

Чтобы повторить одну и ту же последовательность случайных  чисел  несколько  раз,  вы
должны поступать так:

    srand(NBEG); x=rand(); ... ; x=rand();
    /* и повторить все сначала */
    srand(NBEG); x=rand(); ... ; x=rand();

Используемый метод получения случайных чисел таков:




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

    static unsigned long int next = 1L;
    int rand(){
      next = next * 1103515245 + 12345;
      return ((unsigned int)(next/65536) % 32768);
    }
    void srand(seed) unsigned int seed;
    {     next = seed;    }

Для рандомизации часто пользуются таким приемом:

    char t[sizeof(long)];
    time(t); srand(t[0] + t[1] + t[2] + t[3] + getpid());


1.39.  Напишите функцию вычисления определенного интеграла по методу Симпсона.

    /*#!/bin/cc $* -lm
     * Вычисление интеграла по методу Симпсона
     */
    #include <math.h>

    extern double integral(), sin(), fabs();
    #define PI 3.141593

    double myf(x) double x;
    {      return sin(x / 2.0);      }

    int niter;  /* номер итерации */

    void main(){
            double integral();

            printf("%g\n", integral(0.0, PI, myf, 0.000000001));
            /* Заметьте, что myf, а не myf().
             * Точное значение интеграла равно 2.0
             */
            printf("%d итераций\n", niter );
    }


























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

    double integral(a, b, f, eps)
            double a, b;    /* концы отрезка */
            double eps;     /* требуемая точность */
            double (*f)();  /* подынтегральная функция */
    {
            register long i;
            double fab = (*f)(a) + (*f)(b); /* сумма на краях */
            double h, h2;   /* шаг и удвоенный шаг */
            long n, n2;     /* число точек разбиения и оно же удвоенное */
            double Sodd, Seven;  /* сумма значений f в нечетных и в
                                    четных точках */
            double S, Sprev;/* значение интеграла на данной
                               и на предыдущей итерациях */
            double x;       /* текущая абсцисса */

            niter = 0;
            n = 10L;        /* четное число */
            n2 = n * 2;

            h = fabs(b - a) / n2;   h2 = h * 2.0;

            /* Вычисляем первое приближение */
            /* Сумма по нечетным точкам: */
            for( Sodd = 0.0, x = a+h, i = 0;
                                      i < n;
                                      i++, x += h2 )
                    Sodd += (*f)(x);

            /* Сумма по четным точкам: */
            for( Seven = 0.0, x = a+h2, i = 0;
                                        i < n-1;
                                        i++, x += h2 )
                    Seven += f(x);

            /* Предварительное значение интеграла: */
            S = h / 3.0 * (fab + 4.0 * Sodd + 2.0 * Seven );
            do{
                    niter++;
                    Sprev = S;

                    /* Вычисляем интеграл с половинным шагом */
                    h2 = h;      h /= 2.0;
                    if( h == 0.0 ) break;   /* потеря значимости */
                    n  = n2;     n2 *= 2;

                    Seven = Seven + Sodd;
                    /* Вычисляем сумму по новым точкам: */
                    for( Sodd = 0.0, x = a+h, i = 0;
                                              i < n;
                                              i++, x += h2 )
                    Sodd += (*f)(x);

                    /* Значение интеграла */
                    S = h / 3.0 * (fab + 4.0 * Sodd + 2.0 * Seven );

            } while( niter < 31 && fabs(S - Sprev) / 15.0 >= eps );
            /* Используем условие Рунге для окончания итераций */

            return ( 16.0 * S - Sprev ) / 15.0 ;
            /* Возвращаем уточненное по Ричардсону значение */
    }



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

1.40.  Где ошибка?

    struct time_now{
      int  hour, min, sec;
    } X = { 13, 08, 00 }; /* 13 часов 08 минут 00 сек.*/

Ответ: 08 - восьмеричное число (так как начинается с нуля)!  А в восьмеричных  числах
цифры 8 и 9 не бывают.

1.41.  Дан текст:

       int i = -2;
       i <<= 2;
       printf("%d\n", i); /* печать сдвинутого i : -8 */
       i >>= 2;
       printf("%d\n", i); /* печатается -2 */

Закомментируем две строки (исключая их из программы):

       int i = -2;
       i <<= 2;
    /*
       printf("%d\n", i); /* печать сдвинутого i : -8 */
       i >>= 2;
    */
       printf("%d\n", i); /* печатается -2 */

Почему теперь возникает ошибка? Указание: где кончается комментарий?
     Ответ: Си не допускает вложенных комментариев. Вместо этого  часто  используются
конструкции вроде:

    #ifdef COMMENT
      ... закомментированный текст ...
    #endif /*COMMENT*/

и вроде

    /**/ printf("here");/* отладочная выдача включена  */
    /*   printf("here");/* отладочная выдача выключена */

или

    /* выключено();    /**/
       включено();     /**/

А вот дешевый способ быстро исключить  оператор  (с  возможностью  восстановления)  -
конец  комментария  занимает  отдельную  строку,  что позволяет отредактировать такой
текст редактором почти не сдвигая курсор:

       /*printf("here");
        */


1.42.  Почему программа печатает неверное значение для i2 ?










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

    int main(int argc, char *argv[]){
            int i1, i2;

            i1 = 1;         /* Инициализируем i1 /
            i2 = 2;         /* Инициализируем i2 */
            printf("Numbers %d %d\n", i1, i2);
            return(0);
    }

Ответ: в первом операторе присваивания не закрыт комментарий - весь  второй  оператор
присваивания полностью проигнорировался! Правильный вариант:

    int main(int argc, char *argv[]){
            int i1, i2;

            i1 = 1;         /* Инициализируем i1 */
            i2 = 2;         /* Инициализируем i2 */
            printf("Numbers %d %d\n", i1, i2);
            return(0);
    }


1.43.  А вот "шальной" комментарий.

    void main(){
            int n    = 10;
            int *ptr = &n;
            int x, y = 40;

            x = y/*ptr     /* должно быть 4 */  + 1;
            printf( "%d\n", x );    /* пять */
            exit(0);
    }


    /* или такой пример из жизни - взят из переписки в Relcom */
    ...
    cost = nRecords/*pFactor     /* divided by Factor, and  */
           + fixMargin;          /* plus the precalculated  */
    ...

Результат непредсказуем.  Дело в том, что y/*ptr превратилось в  начало  комментария!
Поэтому бинарные операции принято окружать пробелами.

    x = y / *ptr   /* должно быть 4 */  + 1;


1.44.  Найдите ошибки в директивах препроцессора Си [*] (вертикальная черта  обозначает
левый край файла).








____________________
   [*] Препроцессор Си - это программа /lib/cpp





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

            |
            | #include <stdio.h>
            |#include  < sys/types.h >
            |#   define inc (x) ((x) + 1)
            |#define N 12;
            |#define X -2
            |
            |...  printf( "n=%d\n", N );
            |...  p = 4-X;

Ответ: в первой директиве стоит пробел перед #.   Диез  должен  находиться  в  первой
позиции  строки.  Во второй директиве в <&lt;>&gt; находятся лишние пробелы, не относящиеся к
имени файла - препроцессор не найдет такого файла!  В данном случае  "красота"  пошла
во  вред делу.  В третьей - между именем макро inc и его аргументом в круглых скобках
(x) стоит пробел, который изменяет весь  смысл  макроопределения:  вместо  макроса  с
параметром inc(x) мы получаем, что слово inc будет заменяться на (x)((x)+1).  Заметим
однако, что пробелы после # перед именем директивы  вполне  допустимы.   В  четвертом
случае показана характерная опечатка - символ ; после определения. В результате напи-
санный printf() заменится на

            printf( "n=%d\n", 12; );

где лишняя ; даст синтаксическую ошибку.
     В пятом случае ошибки нет, но нас ожидает неприятность в строке  p=4-X;  которая
расширится в строку p=4--2; являющуюся синтаксически неверной.  Чтобы избежать подоб-
ной ситуации, следовало бы написать

            p = 4 - X;  /* через пробелы */

но еще проще (и лучше) взять макроопределение в скобки:

            #define X (-2)


1.45.  Напишите функцию max(x, y), возвращающую большее из  двух  значений.  Напишите
аналогичное  макроопределение.   Напишите  макроопределения min(x, y) и abs(x) (abs -
модуль числа).  Ответ:

    #define abs(x)   ((x)  <  0   ? -(x) : (x))
    #define min(x,y) (((x) < (y)) ?  (x) : (y))

Зачем x взят в круглые скобки (x)? Предположим, что мы написали

    #define abs(x)  (x < 0 ? -x : x )
                вызываем
    abs(-z)                  abs(a|b)
                получаем
    (-z < 0 ? --z : -z )     (a|b < 0 ? -a|b : a|b )

У нас появилась "дикая" операция --z; а выражение a|b<0 соответствует a|(b<0), с сов-
сем  другим порядком операций!  Поэтому заключение всех аргументов макроса в его теле
в круглые скобки позволяет избежать многих неожиданных проблем. Придерживайтесь этого
правила!
     Вот пример, показывающий зачем полезно брать в скобки все определение:

    #define div(x, y)     (x)/(y)

При вызове






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

         z = sizeof div(1, 2);
             превратится в
         z = sizeof(1) / (2);

что равно sizeof(int)/2, а не sizeof(int). Вариант

    #define div(x, y) ((x) / (y))

будет работать правильно.

1.46.  Макросы,  в  отличие  от  функций,  могут  порождать  непредвиденные  побочные
эффекты:

    int sqr(int x){ return x  *  x; }
    #define SQR(x)       ((x) * (x))
    main(){ int y=2, z;
       z = sqr(y++); printf("y=%d z=%d\n", y, z);
       y = 2;
       z = SQR(y++); printf("y=%d z=%d\n", y, z);
    }

Вызов функции sqr печатает "y=3 z=4", как мы и ожидали.  Макрос же SQR расширяется в

       z = ((y++) * (y++));

и результатом будет "y=4 z=6", где z совсем не похоже на квадрат числа 2.

1.47.  ANSI препроцессор[*] языка Си имеет оператор ## - "склейка лексем":

    #define VAR(a, b)       a ## b
    #define CV(x)           command_ ## x
    main(){
      int VAR(x, 31) = 1;
      /* превратится в int x31 = 1; */
      int CV(a) = 2; /* даст int command_a = 2; */
      ...
    }

Старые версии препроцессора не обрабатывают такой оператор, поэтому раньше  использо-
вался такой трюк:

    #define VAR(a, b)       a/**/b

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

1.48.  Напишите программу, распечатывающую максимальное и минимальное из ряда  чисел,
вводимых  с  клавиатуры.  Не  храните  вводимые числа в массиве, вычисляйте max и min
сразу при вводе очередного числа!






____________________
   [*] ANSI - American National Standards Institute, разработавший стандарт на язык  Си
и его окружение.





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

    #include <stdio.h>
    main(){
      int max, min, x, n;
      for( n=0; scanf("%d", &x) != EOF; n++)
            if( n == 0 ) min = max = x;
            else{
               if( x > max ) max = x;
               if( x < min ) min = x;
            }
      printf( "Ввели %d чисел: min=%d max=%d\n",
                     n,            min,   max);
    }

Напишите аналогичную программу для поиска максимума и минимума среди  элементов  мас-
сива, изначально min=max=array[0];

1.49.  Напишите программу, которая сортирует массив  заданных  чисел  по  возрастанию
(убыванию) методом пузырьковой сортировки.  Когда вы станете более опытны в Си, напи-
шите сортировку методом Шелла.

    /*
     * Сортировка по методу Шелла.
     * Сортировке подвергается массив указателей на данные типа obj.
     *      v------.-------.------.-------.------0
     *             !       !      !       !
     *             *       *      *       *
     *            элементы типа obj
     * Программа взята из книги Кернигана и Ритчи.
     */

    #include <stdio.h>
    #include <string.h>
    #include <locale.h>
    #define obj char

    static shsort (v,n,compare)
    int n;              /* длина массива */
    obj *v[];           /* массив указателей */
    int (*compare)();   /* функция сравнения соседних элементов */
    {
            int g,      /* расстояние, на котором происходит сравнение */
                i,j;    /* индексы сравниваемых элементов */
            obj *temp;

            for( g = n/2 ; g > 0  ; g /= 2 )
            for( i = g   ; i < n  ; i++    )
            for( j = i-g ; j >= 0 ; j -= g )
            {
                    if((*compare)(v[j],v[j+g]) <= 0)
                        break;      /* уже в правильном порядке */

                    /* обменять указатели */
                    temp = v[j]; v[j] = v[j+g]; v[j+g] = temp;
                    /* В качестве упражнения можете написать
                     * при помощи curses-а программу,
                     * визуализирующую процесс сортировки:
                     * например, изображающую эту перестановку
                     * элементов массива */
            }
    }




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

    /* сортировка строк */
    ssort(v) obj **v;
    {
            extern less();  /* функция сравнения строк */
            int len;

            /* подсчет числа строк */
            len=0;
            while(v[len]) len++;
            shsort(v,len,less);
    }

    /* Функция сравнения строк.
     * Вернуть целое меньше нуля, если a <  b
     *                      ноль, если a == b
     *               больше нуля, если a >  b
     */
    less(a,b) obj *a,*b;
    {
            return strcoll(a,b);
            /* strcoll - аналог strcmp,
             * но с учетом алфавитного порядка букв.
             */
    }

    char *strings[] = {
            "Яша", "Федя", "Коля",
            "Гриша", "Сережа", "Миша",
            "Андрей Иванович", "Васька",
            NULL
    };
    int main(){
            char **next;

            setlocale(LC_ALL, "");

            ssort( strings );
            /* распечатка */
            for( next = strings ; *next ; next++ )
                    printf( "%s\n", *next );
            return 0;
    }


1.50.  Реализуйте алгоритм быстрой сортировки.



















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

    /* Алгоритм быстрой сортировки. Работа алгоритма "анимируется"
     * (animate-оживлять) при помощи библиотеки curses.
     *      cc -o qsort qsort.c -lcurses -ltermcap
     */
    #include "curses.h"

    #define N 10     /* длина массива */

    /* массив, подлежащий сортировке */
    int target [N] = {
            7, 6, 10, 4, 2,
            9, 3,  8, 5, 1
    };

    int maxim;      /* максимальный элемент массива */

    /* quick sort */
    qsort (a, from, to)
            int a[];      /* сортируемый массив */
            int from;     /* левый начальный индекс */
            int to;       /* правый конечный индекс */
    {
            register i, j, x, tmp;

            if( from >= to ) return;
            /* число элементов <= 1 */

            i = from; j = to;
            x = a[ (i+j) / 2 ];     /* значение из середины */

            do{
                    /* сужение вправо */
                    while( a[i] < x ) i++ ;

                    /* сужение влево */
                    while( x < a[j] ) j--;

                    if( i <= j ){   /* обменять */
                            tmp = a[i]; a[i] = a[j] ; a[j] = tmp;
                            i++;  j--;

                            demochanges();  /* визуализация */
                    }
            } while( i <= j );

            /* Теперь обе части сошлись в одной точке.
             * Длина левой части = j - from + 1
             *       правой      = to - i   + 1
             * Все числа в левой части меньше всех чисел в правой.
             * Теперь надо просто отсортировать каждую часть в отдельности.
             * Сначала сортируем более короткую (для экономии памяти
             * в стеке ). Рекурсия:
             */
            if( (j - from)  <  (to - i) ){
                    qsort( a, from, j );
                    qsort( a, i, to   );
            } else {
                    qsort( a, i,   to );
                    qsort( a, from, j );
            }
    }



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

    int main (){
            register i;

            initscr();      /* запуск curses-а */

            /* поиск максимального числа в массиве */
            for( maxim = target[0], i = 1 ;  i < N ; i++ )
                    if( target[i] > maxim )
                        maxim = target[i];

            demochanges();
            qsort( target, 0, N-1 );
            demochanges();

            mvcur( -1, -1, LINES-1, 0);
            /* курсор в левый нижний угол */

            endwin();       /* завершить работу с curses-ом */
            return 0;
    }

    #define GAPY  2
    #define GAPX 20

    /* нарисовать картинку */
    demochanges(){
            register i, j;
            int h = LINES - 3 * GAPY - N;
            int height;

            erase();        /* зачистить окно */
            attron( A_REVERSE );

            /* рисуем матрицу упорядоченности */
            for( i=0 ; i < N ; i++ )
                    for( j = 0; j < N ; j++ ){
                        move( GAPY + i , GAPX + j * 2 );
                        addch( target[i] >= target[j] ? '*' : '.' );
                        addch( ' ' );
                        /* Рисовать '*' если элементы
                         * идут в неправильном порядке.
                         * Возможен вариант проверки target[i] > target[j]
                         */
                    }
            attroff( A_REVERSE );

            /* массив */
            for( i = 0 ; i < N ; i++ ){
                    move( GAPY + i , 5 );
                    printw( "%4d", target[i] );

                    height = (long) h * target[i] / maxim ;
                    for( j = 2 * GAPY + N + (h - height) ;
                                          j < LINES - GAPY; j++ ){
                            move( j, GAPX + i * 2 );
                            addch( '|' );
                    }
            }
            refresh();      /* проявить картинку */
            sleep(1);
    }



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

1.51.  Реализуйте приведенный фрагмент программы без использования оператора  goto  и
без меток.

            if ( i > 10 ) goto M1;
            goto M2;
            M1: j = j + i; flag = 2; goto M3;
            M2: j = j - i; flag = 1;
            M3:       ;

Заметьте, что помечать можно только оператор (может быть пустой);  поэтому  не  может
встретиться фрагмент

    { ..... Label:  }  а только { ..... Label: ; }


1.52.  В каком случае оправдано использование оператора goto?
     Ответ: при выходе из вложенных  циклов,  т.к.  оператор  break  позволяет  выйти
только из самого внутреннего цикла (на один уровень).

1.53.  К какому if-у относится else?

    if(...) ... if(...) ... else ...

Ответ: ко второму (к ближайшему предшествующему,  для  которого  нет  другого  else).
Вообще же лучше явно расставлять скобки (для ясности):

    if(...){ ... if(...) ...   else ... }
    if(...){ ... if(...) ... } else ...


1.54.  Макроопределение, чье тело представляет собой последовательность операторов  в
{...}  скобках (блок), может вызвать проблемы при использовании его в условном опера-
торе if с else-частью:

    #define MACRO   { x=1; y=2; }

    if(z)   MACRO;
    else    .......;

Мы получим после макрорасширения

    if(z)   { x=1; y=2; } /* конец if-а */ ;
    else    .......;      /* else ни к чему не относится */

то есть синтаксически ошибочный фрагмент, так как должно быть либо

    if(...)  один_оператор;
    else     .....
             либо
    if(...){ последовательность; ...; операторов; }
    else     .....

где точка-с-запятой после } не нужна.  С этим явлением борются, оформляя блок {...} в
виде do{...}while(0)

    #define MACRO do{ x=1; y=2; }while(0)

Тело такого "цикла" выполняется единственный раз, при  этом  мы  получаем  правильный
текст:





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

    if(z)   do{ x=1; y=2; }while(0);
    else    .......;


1.55.  В чем ошибка (для знающих язык "Паскаль")?

    int  x = 12;
         if( x < 20 and x > 10 ) printf( "O'K\n");
    else if( x > 100 or x < 0  ) printf( "Bad x\n");
    else                         printf( "x=%d\n", x);

Напишите

    #define and &&
    #define or  ||


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

    int i = 0;
    char *s = "   3 spaces";
    while(*s == ' ' || *s++ == '\t')
            printf( "Пробел %d\n", ++i);

Ответ: логические операции || и && выполняются слева  направо;  как  только  какое-то
условие  в  ||  оказывается  истинным  (а в && ложным) - дальнейшие условия просто не
вычисляются. В нашем случае условие *s==' ' сразу же верно, и операция s++ из второго
условия не выполняется! Мы должны были написать хотя бы так:

    while(*s == ' ' || *s == '\t'){
            printf( "Пробел %d\n", ++i); s++;
    }

С другой стороны, это свойство || и && черезвычайно полезно, например:

    if( x != 0.0 && y/x < 1.0 ) ... ;

Если бы мы не вставили проверку на 0, мы могли бы получить деление на 0.  В данном же
случае при x==0 деление просто не будет вычисляться.  Вот еще пример:

    int a[5], i;
    for(i=0; i < 5 && a[i] != 0; ++i) ...;

Если i выйдет за границу массива, то сравнение a[i] с нулем уже не будет вычисляться,
т.е. попытки прочесть элемент не входящий в массив не произойдет.
     Это свойство && позволяет писать довольно неочевидные конструкции, вроде

    if((cond) && f());
        что оказывается эквивалентным
    if( cond ) f();

Вообще же

    if(C1 && C2 && C3) DO;
            эквивалентно
    if(C1) if(C2) if(C3) DO;

и для "или"





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

    if(C1 || C2 || C3) DO;
            эквивалентно
         if(C1) goto ok;
    else if(C2) goto ok;
    else if(C3){ ok: DO; }

Вот еще пример, пользующийся этим свойством ||

    #include <stdio.h>
    main(argc, argv) int argc; char *argv[];
    {  FILE *fp;
       if(argc < 2 || (fp=fopen(argv[1], "r")) == NULL){
            fprintf(stderr, "Плохое имя файла\n");
            exit(1); /* завершить программу */
       }
       ...
    }

Если argc==1, то argv[1] не определено, однако в этом случае попытки открыть  файл  с
именем argv[1] просто не будет предпринято!
     Ниже приведен еще один содержательный пример, представляющий собой одну из  воз-
можных схем написания "двуязычных" программ, т.е. выдающих сообщения на одном из двух
языков по вашему желанию. Проверяется переменная окружения MSG (или LANG):

                                    ЯЗЫК:
    1)  "MSG=engl"                английский
    2)   MSG нет в окружении      английский
    3)  "MSG=rus"                 русский

Про окружение и функцию getenv() смотри в главе "Взаимодействие с UNIX", про strchr()
- в главе "Массивы и строки".

    #include <stdio.h>
    int _ediag = 0; /* язык диагностик: 1-русский */
    extern char *getenv(), *strchr();
    #define ediag(e,r) (_ediag?(r):(e))
    main(){  char *s;
    _ediag = ((s=getenv("MSG"))   != NULL   &&
               strchr("rRрР", *s) != NULL);
    printf(ediag("%d:english\n", "%d:русский\n"), _ediag);
    }

Если переменная MSG не определена, то s==NULL и функция strchr(s,...)  не  вызывается
(ее  первый фргумент не должен быть NULL-ом).  Здесь ее можно было бы упрощенно заме-
нить на *s=='r'; тогда если s равно NULL, то обращение *s было бы незаконно  (обраще-
ние  по  указателю NULL дает непредсказуемые результаты и, скорее всего, вызовет крах
программы).

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

    a && b     =     ! ( !a || !b )
    a || b     =     ! ( !a && !b )

а также учитывая, что

    ! !a       =     a
    ! (a == b) =    (a != b)

Например:




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

    if( c != 'a' && c != 'b' && c != 'c' )...;
            превращается в
    if( !(c == 'a' || c == 'b' || c == 'c')) ...;


1.58.  Пример, в котором используются побочные эффекты вычисления выражений.   Обычно
значение  выражения присваивается некоторой переменной, но это не необходимо. Поэтому
можно использовать свойства вычисления && и || в выражениях (хотя это не  есть  самый
понятный способ написания программ, скорее некоторый род извращения). Ограничение тут
таково: все части выражения должны возвращать значения.

    #include <stdio.h>
    extern int errno;       /* код системной ошибки */
    FILE *fp;

    int openFile(){
            errno = 0;
            fp = fopen("/etc/inittab", "r");
            printf("fp=%x\n", fp);
            return(fp == NULL ? 0 : 1);
    }
    int closeFile(){
            printf("closeFile\n");
            if(fp) fclose(fp);
            return 0;
    }

    int die(int code){
            printf("exit(%d)\n", code);
            exit(code);
            return 0;
    }


    void main(){
            char buf[2048];

            if( !openFile()) die(errno); closeFile();
            openFile()    || die(errno); closeFile();
              /* если файл открылся, то die() не вычисляется */
            openFile() ? 0 : die(errno); closeFile();

            if(openFile()) closeFile();
            openFile()  && closeFile();
              /* вычислить closeFile() только если openFile() удалось */
            openFile()  && (printf("%s", fgets(buf, sizeof buf, fp)), closeFile());
    }

В последней строке использован оператор "запятая": (a,b,c) возвращает значение  выра-
жения c.

1.59.  Напишите функцию, вычисляющую сумму массива заданных чисел.

1.60.  Напишите функцию, вычисляющую среднее значение массива заданных чисел.

1.61.  Что будет напечатано в результате работы следующего цикла?

    for ( i = 36; i > 0; i /= 2 )
          printf ( "%d%s", i,
                           i==1 ? ".\n":", ");




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

Ответ: 36, 18, 9, 4, 2, 1.

1.62.  Найдите ошибки в следующей программе:

    main {
         int i, j, k(10);

         for ( i = 0, i <= 10, i++   ){
               k[i] = 2 * i + 3;
               for ( j = 0, j <= i, j++ )
                     printf ("%i\n", k[j]);
         }
    }

Обратите внимание на формат %i, существует ли такой формат?  Есть ли это тот  формат,
по которому следует печатать значения типа int?

1.63.  Напишите программу, которая распечатывает  элементы  массива.  Напишите  прог-
рамму, которая распечатывает элементы массива по 5 чисел в строке.

1.64.  Составьте программу считывания строк символов из стандартного ввода  и  печати
номера введенной строки, адреса строки в памяти ЭВМ, значения строки, длины строки.

1.65.  Стилистическое замечание: в операторе return возвращаемое выражение не  обяза-
тельно должно быть в ()-скобках.  Дело в том, что return - не функция, а оператор.

    return  выражение;
    return (выражение);

Однако если вы вызываете функцию (например, exit) - то аргументы должны быть в  круг-
лых скобках: exit(1); но не exit 1;

1.66.  Избегайте ситуации, когда функция в разных  ветвях  вычисления  то  возвращает
некоторое значение, то не возвращает ничего:

    int func (int x) {
        if( x > 10  ) return x*2;
        if( x == 10 ) return (10);
        /* а здесь - неявный return; без значения */
    }

при x < 10 функция вернет непредсказуемое значение!   Многие  компиляторы  распознают
такие ситуации и выдают предупреждение.

1.67.  Напишите программу, запрашивающую ваше имя и "приветствующую"  вас.   Напишите
функцию чтения строки. Используйте getchar() и printf().
Ответ:

    #include <stdio.h>  /* standard input/output */
    main(){
       char buffer[81]; int i;
       printf( "Введите ваше имя:" );
       while((i = getstr( buffer, sizeof buffer )) != EOF){
              printf( "Здравствуй, %s\n", buffer );
              printf( "Введите ваше имя:" );
       }
    }

    getstr( s, maxlen )
     char *s;    /* куда поместить строку */
     int maxlen; /* длина буфера:



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

                    макс. длина строки = maxlen-1 */
    {  int c;    /* не char! (почему ?) */
       register int i = 0;

       maxlen--; /* резервируем байт под конечный '\0' */
       while(i < maxlen && (c = getchar()) != '\n'
                        &&  c != EOF )
              s[i++] = c;
       /* обратите внимание, что сам символ '\n'
        * в строку не попадет */
       s[i] = '\0';  /* признак конца строки */
       return (i == 0 && c == EOF) ? EOF : i;
       /* вернем длину строки  */
    }

Вот еще один вариант функции чтения строки: в нашем примере ее следует вызывать как
      fgetstr(buffer,sizeof(buffer),stdin);
Это подправленный вариант стандартной функции fgets (в ней строки @1  и  @2  обменяны
местами).

    char *fgetstr(char *s, int maxlen, register FILE *fp){
      register c; register char *cs = s;

      while(--maxlen > 0 && (c = getc(fp)) != EOF){
        if(c == '\n') break;  /* @1 */
        *cs++ = c;            /* @2 */
      }
      if(c == EOF && cs == s) return NULL;
      /* Заметьте, что при EOF строка s не меняется! */
      *cs = '\0'; return s;
    }

Исследуйте поведение этих функций, когда входная строка слишком длинная (длиннее max-
len).  Замечание: вместо нашей "рукописной" функции getstr() мы могли бы использовать
стандартную библиотечную функцию gets(buffer).

1.68.  Объясните, почему d стало отрицательным и почему %X печатает больше F,  чем  в
исходном числе? Пример выполнялся на 32-х битной машине.

    main(){
      unsigned short u = 65535;  /* 16 бит: 0xFFFF */
               short d = u;      /* 15 бит + знаковый бит */
      printf( "%X %d\n", d, d);  /* FFFFFFFF -1 */
    }

Указание: рассмотрите двоичное представление чисел (смотри приложение).  Какие приве-
дения типов здесь происходят?

1.69.  Почему 128 превратилось в отрицательное число?

    main()
    {
      /*signed*/ char c  = 128; /* биты: 10000000 */
      unsigned   char uc = 128;
      int  d = c;   /* используется 32-х битный int */
      printf( "%d %d %x\n", c, d, d );
      /*       -128 -128 ffffff80 */
           d = uc;
      printf( "%d %d %x\n", uc, d, d );
      /*       128 128 80       */
    }



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

Ответ: при приведении char к int расширился знаковый бит (7-ой),  заняв  всю  старшую
часть  слова.  Знаковый бит int-а стал равен 1, что является признаком отрицательного
числа. То же будет происходить со всеми значениями c из диапазона 128..255  (содержа-
щими бит 0200).  При приведении unsigned char к int знаковый бит не расширяется.
     Можно было поступить еще и так:

    printf( "%d\n", c & 0377 );

Здесь c приводится к типу int (потому что при использовании в аргументах функции  тип
char  ВСЕГДА  приводится  к  типу  int), затем &0377 занулит старший байт полученного
целого числа (состоящий из битов 1), снова превратив число в положительное.

1.70.  Почему

    printf("%d\n", '\377' == 0377 );
    printf("%d\n", '\xFF' == 0xFF );

печатает 0 (ложь)?  Ответ: по той же причине, по которой

    printf("%d %d\n", '\377', 0377);

печатает -1 255, а именно: char '\377' приводится в выражениях к  целому  расширением
знакового бита (а 0377 - уже целое).

1.71.  Рассмотрим программу

    #include <stdio.h>
    int main(int ac, char **av){
            int c;

            while((c = getchar()) != EOF)
                    switch(c){
                    case 'ы': printf("Буква ы\n"); break;
                    case 'й': printf("Буква й\n"); break;
                    default:  printf("Буква с кодом %d\n", c); break;
                    }
            return 0;
    }

Она работает так:

    % a.out
    йфыв
    Буква с кодом 202
    Буква с кодом 198
    Буква с кодом 217
    Буква с кодом 215
    Буква с кодом 10
    ^D
    %

Выполняется всегда default, почему не выполняются case 'ы' и case 'й'?
     Ответ: русские буквы имеют восьмой бит (левый) равный 1. В case такой байт  при-
водится  к  типу  int  расширением  знакового бита.  В итоге получается отрицательное
число.  Пример:

    void main(void){
            int c = 'й';
            printf("%d\n", c);
    }
    печатает -54



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

Решением служит подавление расширения знакового бита:

    #include <stdio.h>
    /* Одно из двух */
    #define U(c)    ((c) & 0xFF)
    #define UC(c)   ((unsigned char) (c))
    int main(int ac, char **av){
            int c;

            while((c = getchar()) != EOF)
                    switch(c){
                    case U('ы'):    printf("Буква ы\n"); break;
                    case UC('й'):   printf("Буква й\n"); break;
                    default:        printf("Буква с кодом %d\n", c); break;
                    }
            return 0;
    }

Она работает правильно:

    % a.out
    йфыв
    Буква й
    Буква с кодом 198
    Буква ы
    Буква с кодом 215
    Буква с кодом 10
    ^D
    %

Возможно также использование кодов букв:

    case 0312:

но это гораздо менее наглядно. Подавление знакового бита необходимо также и в  опера-
торах if:

    int c;
            ...
    if(c == 'й') ...

            следует заменить на

    if(c == UC('й')) ...

Слева здесь - signed int, правую часть компилятор тоже приводит к signed int.  Прихо-
дится явно говорить, что справа - unsigned.

1.72.  Рассмотрим программу, которая должна напечатать числа от 0 до 255.   Для  этих
чисел в качестве счетчика достаточен один байт:

    int main(int ac, char *av[]){
        unsigned char ch;

        for(ch=0; ch < 256; ch++)
            printf("%d\n", ch);
        return 0;
    }

Однако эта программа зацикливается, поскольку в момент, когда ch==255,  это  значение
меньше  256.  Следующим шагом выполняется ch++, и ch становится равно 0, ибо для char



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

вычисления ведутся по модулю 256 (2 в 8 степени).  То есть в данном случае 255+1=0
     Решений существует два: первое - превратить unsigned char в int.  Второе - вста-
вить явную проверку на последнее значение диапазона.

    int main(int ac, char *av[]){
        unsigned char ch;

        for(ch=0; ; ch++){
            printf("%d\n", ch);
            if(ch == 255) break;
        }
        return 0;
    }


1.73.  Подумайте, почему для

    unsigned a, b, c;
    a < b + c    не эквивалентно   a - b < c

(первое - более корректно). Намек в виде примера (он выполнялся на 32-битной машине):

    a = 1; b = 3; c = 2;
    printf( "%u\n", a - b );     /* 4294967294, хотя в
                       нормальной арифметике 1 - 3 = -2 */
    printf( "%d\n", a < b + c ); /* 1 */
    printf( "%d\n", a - b < c ); /* 0 */

Могут ли unsigned числа быть отрицательными?

1.74.  Дан текст:

    short x = 40000;
    printf("%d\n", x);

Печатается -25536. Объясните эффект. Указание: каково наибольшее представимое  корот-
кое целое (16 битное)? Что на самом деле оказалось в x?  (лишние слева биты - обруба-
ются).

1.75.  Почему в примере

            double x = 5 / 2;
            printf( "%g\n", x );

значение x равно 2 а не 2.5 ?
     Ответ: производится целочисленное деление, затем в присваивании  целое  число  2
приводится  к  типу double. Чтобы получился ответ 2.5, надо писать одним из следующих
способов:

            double x = 5.0 / 2;
            x = 5 / 2.0;
            x = (double) 5 / 2;
            x = 5 / (double) 2;
            x = 5.0 / 2.0;

то есть в выражении должен быть хоть один операнд типа double.
     Объясните, почему следующие три оператора выдают такие значения:







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

            double g = 9.0;
            int t = 3;
            double dist = g *  t * t / 2;    /* 40.5 */
                   dist = g * (t * t / 2);   /* 36.0 */
                   dist = g * (t * t / 2.0); /* 40.5 */

В каких случаях деление целочисленное, в каких - вещественное?  Почему?

1.76.  Странслируйте пример на машине с длиной слова int равной 16 бит:

            long n  = 1024 * 1024;
            long nn =  512 *  512;
            printf( "%ld %ld\n", n, nn );

Почему печатается 0 0 а не 1048576 262144?
     Ответ: результат умножения (2**20 и 2**18) - это целое число; однако оно слишком
велико  для  сохранения  в  16  битах, поэтому старшие биты обрубаются. Получается 0.
Затем в присваивании это уже обрубленное значение приводится к типу long (32 бита)  -
это все равно будет 0.
     Чтобы получить корректный результат, надо чтобы выражение справа от = уже  имело
тип long и сразу сохранялось в 32 битах. Для этого оно должно иметь хоть один операнд
типа long:

            long n = (long) 1024 * 1024;
            long nn =        512 *  512L;


1.77.  Найдите ошибку в операторе:

    x - = 4;   /* вычесть из x число 4 */

Ответ: между `-' и `=' не должно быть пробела. Операция вида

    x @= expr;

означает

    x = x @ expr;

(где @ - одна из операций + - * / % ^ >&gt;>&gt; <&lt;<&lt; & |), причем x здесь вычисляется  единст-
венный раз (т.е. такая форма не только короче и понятнее, но и экономичнее).
     Однако имеется тонкое отличие a=a+n от a+=n; оно заключается в том, сколько  раз
вычисляется a. В случае a+=n единожды; в случае a=a+n два раза.





















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

    #include <stdio.h>

    static int x = 0;
    int *iaddr(char *msg){
            printf("iaddr(%s) for x=%d evaluated\n", msg, x);
            return &x;
    }
    int main(){
            static int a[4];
            int *p, i;

            printf( "1: "); x = 0; (*iaddr("a"))++;
            printf( "2: "); x = 0; *iaddr("b") += 1;
            printf( "3: "); x = 0; *iaddr("c") = *iaddr("d") + 1;

            for(i=0, p = a; i < sizeof(a)/sizeof(*a); i++) a[i] = 0;
            *p++ += 1;
            for(i=0; i < sizeof(a)/sizeof(*a); i++)
                    printf("a[%d]=%d ", i, a[i]);
            printf("offset=%d\n", p - a);

            for(i=0, p = a; i < sizeof(a)/sizeof(*a); i++) a[i] = 0;
            *p++ = *p++ + 1;
            for(i=0; i < sizeof(a)/sizeof(*a); i++)
                    printf("a[%d]=%d ", i, a[i]);
            printf("offset=%d\n", p - a);

            return 0;
    }

Выдача:

    1: iaddr(a) for x=0 evaluated
    2: iaddr(b) for x=0 evaluated
    3: iaddr(d) for x=0 evaluated
    iaddr(c) for x=0 evaluated
    a[0]=1 a[1]=0 a[2]=0 a[3]=0 offset=1
    a[0]=1 a[1]=0 a[2]=0 a[3]=0 offset=2

Заметьте также, что

    a[i++] += z;

            это
    a[i] = a[i] + z; i++;

            а вовсе не
    a[i++] = a[i++] + z;


1.78.  Операция y = ++x; эквивалентна

    y = (x = x+1, x);

а операция y = x++; эквивалентна

    y = (tmp = x, x = x+1, tmp);
    или
    y = (x += 1) - 1;

где tmp - временная псевдопеременная того же типа,  что  и  x.  Операция  `,'  выдает



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

значение последнего выражения из перечисленных (подробнее см. ниже).
     Пусть x=1. Какие значения будут присвоены x и y после выполнения оператора

            y = ++x + ++x + ++x;


1.79.  Пусть i=4. Какие значения будут присвоены x и i после выполнения оператора

            x = --i + --i + --i;


1.80.  Пусть x=1. Какие значения будут присвоены x и y после выполнения оператора

            y = x++ + x++ + x++;


1.81.  Пусть i=4. Какие значения будут присвоены i и y после выполнения оператора

            y = i-- + i-- + i--;


1.82.  Корректны ли операторы

    char *p = "Jabberwocky"; char s[] = "0123456789?";
    int i = 0;

    s[i] = p[i++]; или *p   = *++p;
                   или s[i] = i++;
              или даже *p++ = f( *p );

Ответ: нет, стандарт не предусматривает, какая  из  частей  присваивания  вычисляется
первой: левая или правая.  Поэтому все может работать так, как мы и подразумевали, но
может и иначе!  Какое i используется в s[i]: 0 или уже 1 (++ уже сделан или нет),  то
есть

    int i = 0; s[i] = i++;     это
    s[0]  = 0;      или же     s[1] = 0;  ?

Какое p будет использовано в левой части *p: уже продвинутое или  старое?  Еще  более
эта идея драматизирована в

    s[i++] = p[i++];

Заметим еще, что в

    int i=0, j=0;
    s[i++] = p[j++];

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

    if( a[i++] < b[i] )...;

Порядок вычисления операндов не определен, поэтому неясно, что будет сделано  прежде:
взято  значение  b[i]  или  значение a[i++] (тогда будет взято b[i+1] ).  Надо писать
так, чтобы не полагаться на особенности вашего компилятора:

    if( a[i] < b[i+1] )...;  или  *p = *(p+1);
    i++;                          ++p;





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

     Твердо усвойте, что i++ и ++i не только выдают значения i и i+1  соответственно,
но  и  изменяют  значение  i.  Поэтому эти операторы НЕ НАДО использовать там, где по
смыслу требуется i+1, а не i=i+1.  Так для сравнения соседних элементов массива

       if( a[i] < a[i+1] ) ... ;  /* верно */
       if( a[i] < a[++i] ) ... ;  /* неверно */


1.83.  Порядок вычисления операндов в бинарных выражениях не  определен  (что  раньше
вычисляется - левый операнд или же правый ?).  Так пример

    int f(x,s) int x; char *s;
    {  printf( "%s:%d ", s, x ); return x; }

    main(){
       int x = 1;
       int y = f(x++, "f1") + f(x+=2, "f2");
       printf("%d\n", y);
    }

может печатать либо

            f1:1 f2:4 5
               либо
            f2:3 f1:3 6

в зависимости от особенностей поведения вашего компилятора (какая из двух f()  выпол-
нится первой: левая или правая?).  Еще пример:

            int y = 2;
            int x = ((y = 4) * y );
            printf( "%d\n", x );

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

            y = 4;  x = y * y;


1.84.  Законен ли оператор

            f(x++, x++);   или  f(x, x++);

Ответ: Нет, порядок вычисления аргументов функций не определен.  По той же причине мы
не можем писать

            f( c = getchar(), c );

а должны писать

            c = getchar();  f(c, c);

(если мы именно это имели в виду). Вот еще пример:

            ...
            case '+':
                    push(pop()+pop()); break;
            case '-':
                    push(pop()-pop()); break;
            ...




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

следует заменить на

            ...
            case '+':
                    push(pop()+pop()); break;
            case '-':
                    { int x = pop(); int y = pop();
                      push(y - x); break;
                    }
            ...

И еще пример:

      int x = 0;
      printf( "%d %d\n", x = 2, x );   /* 2 0 либо 2 2 */

Нельзя также

      struct pnt{ int x; int y; }arr[20]; int i=0;
      ...
      scanf( "%d%d", & arr[i].x, & arr[i++].y );

поскольку i++ может сделаться раньше, чем чтение в x.  Еще пример:

            main(){
               int i = 3;
               printf( "%d %d %d\n", i += 7, i++, i++ );
            }

который показывает, что на IBM PC [*] и PDP-11 [**] аргументы функций  вычисляются  справа
налево  (пример  печатает  12  4  3).  Впрочем, другие компиляторы могут вычислять их
слева направо (как и подсказывает нам здравый смысл).

1.85.  Программа печатает либо x=1 либо x=0 в зависимости от КОМПИЛЯТОРА  -  вычисля-
ется ли раньше правая или левая часть оператора вычитания:

    #include <stdio.h>
    void main(){
            int c = 1;
            int x = c - c++;

            printf( "x=%d c=%d\n", x, c );
            exit(0);
    }

Что вы имели в виду ?

    left = c; right = c++; x = left - right;
            или
    right = c++; left = c; x = left - right;

А если компилятор еще и распараллелит вычисление left и right - то одна  программа  в
разные моменты времени сможет давать разные результаты.

____________________
   [*] IBM ("Ай-би-эм") - International Buisiness Machines  Corporation.   Персональные
компьютеры IBM PC построены на базе микропроцессоров фирмы Intel.
     [**] PDP-11 - (Programmed Data Processor) - компьютер фирмы DEC (Digital  Equipment
Corporation), у нас известный как СМ-1420.  Эта же фирма выпускает машину VAX.





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

     Вот еще достойная задачка:

    x = c-- - --c;  /* c-----c */


1.86.  Напишите программу, которая устанавливает в 1 бит 3 и сбрасывает в  0  бит  6.
Биты в слове нумеруются с нуля справа налево.  Ответ:

            int x = 0xF0;

            x |=  (1 << 3);
            x &= ~(1 << 6);

В программах часто используют битовые маски как флаги некоторых параметров (признак -
есть или нет). Например:

    #define A  0x08             /* вход свободен  */
    #define B  0x40             /* выход свободен */
    установка флагов            : x |=    A|B;
    сброс флагов                : x &=  ~(A|B);
    проверка флага A            : if( x & A ) ...;
    проверка, что оба флага есть: if((x & (A|B)) == (A|B))...;
    проверка, что обоих нет     : if((x & (A|B)) == 0 )...;
    проверка, что есть хоть один: if( x & (A|B))...;
    проверка, что есть только A : if((x & (A|B)) == A)...;
    проверка, в каких флагах
      различаются x и y         : diff = x ^ y;


1.87.  В программах иногда требуется использовать "множество": каждый допустимый эле-
мент  множества  имеет номер и может либо присутствовать в множестве, либо отсутство-
вать.  Число вхождений не учитывается.  Множества  принято  моделировать  при  помощи
битовых шкал:

    #define   SET(n,a) (a[(n)/BITS] |=  (1L <<((n)%BITS)))
    #define   CLR(n,a) (a[(n)/BITS] &= ~(1L <<((n)%BITS)))
    #define ISSET(n,a) (a[(n)/BITS] &   (1L <<((n)%BITS)))
    #define BITS 8 /* bits per char (битов в байте) */
    /* Перечислимый тип */
    enum fruit { APPLE, PEAR, ORANGE=113,
                 GRAPES, RAPE=125, CHERRY};
    /* шкала:   n из интервала 0..(25*BITS)-1 */
    static char fr[25];
    main(){
      SET(GRAPES, fr);  /* добавить в множество */
      if(ISSET(GRAPES, fr)) printf("here\n");
      CLR(GRAPES, fr);  /* удалить из множества */
    }


1.88.  Напишите программу, распечатывающую все возможные перестановки  массива  из  N
элементов.  Алгоритм  будет  рекурсивным, например таким: в качестве первого элемента
перестановки взять i-ый элемент массива. Из оставшихся элементов массива (если  такие
есть)  составить  все  перестановки  порядка N-1.  Выдать все перестановки порядка N,
получающиеся склейкой i-ого элемента и всех (по очереди)  перестановок  порядка  N-1.
Взять следующее i и все повторить.
     Главная проблема здесь - организовать оставшиеся после извлечения i-ого элемента
элементы  массива  в удобную структуру данных (чтобы постоянно не копировать массив).
Можно использовать, например, битовую шкалу уже выбранных  элементов.   Воспользуемся
для этого макросами из предыдущего параграфа:




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

    /* ГЕНЕРАТОР ПЕРЕСТАНОВОК ИЗ n ЭЛЕМЕНТОВ ПО m */

    extern void *calloc(unsigned nelem, unsigned elsize);
    /* Динамический выделитель памяти, зачищенной нулями.
     * Это стандартная библиотечная функция.
     * Обратная к ней - free();
     */
    extern void free(char *ptr);
    static int N, M, number;
    static char *scale;     /* шкала выбранных элементов */
           int  *res;       /* результат */

    /* ... текст определений SET, CLR, ISSET, BITS ... */

    static void choose(int ind){
      if(ind == M){ /* распечатать перестановку */
         register i;
         printf("Расстановка #%04d", ++number);
         for(i=0; i < M; i++) printf(" %2d", res[i]);
         putchar('\n'); return;
      } else
      /* Выбрать очередной ind-тый элемент перестановки
       * из числа еще не выбранных элементов.
       */
      for(res[ind] = 0; res[ind] < N; ++res[ind])
         if( !ISSET(res[ind], scale)) {
            /* элемент еще не был выбран */
            SET(res[ind], scale);      /* выбрать */
            choose(ind+1);
            CLR(res[ind], scale);      /* освободить */
         }
    }
    void arrange(int n, int m){
      res   = (int *)  calloc(m, sizeof(int));
      scale = (char *) calloc((n+BITS-1)/BITS, 1);
      M = m; N = n; number = 0;
      if( N >= M ) choose(0);
      free((char *) res); free((char *) scale);
    }
    void main(int ac, char **av){
       if(ac != 3){ printf("Arg count\n"); exit(1); }
       arrange(atoi(av[1]), atoi(av[2]));
    }

Программа должна выдать n!/(n-m)!  расстановок, где x! = 1*2*...*x - функция  "факто-
риал".   По  определению 0! = 1.  Попробуйте переделать эту программу так, чтобы оче-
редная перестановка печаталась по запросу:

    res = init_iterator(n, m);
    /* печатать варианты, пока они есть */
    while( next_arrangement (res))
           print_arrangement(res, m);
    clean_iterator(res);


1.89.  Напишите макроопределения циклического сдвига переменной типа unsigned int  на
skew бит влево и вправо (ROL и ROR).  Ответ:

    #define BITS 16     /* пусть целое состоит из 16 бит */
    #define ROL(x,skew) x=(x<<(skew))|(x>>(BITS-(skew)))
    #define ROR(x,skew) x=(x>>(skew))|(x<<(BITS-(skew)))



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

    Вот как работает ROL(x, 2) при BITS=6
            |abcdef|        исходно
           abcdef00          << 2
             0000abcdef      >> 4
             ------         операция |
             cdefab         результат

В случае signed int потребуется накладывать маску при сдвиге вправо из-за  того,  что
левые  биты при >&gt;>&gt; не заполняются нулями.  Приведем пример для сдвига переменной типа
signed char (по умолчанию все char - знаковые) на 1 бит влево:

    #define CHARBITS 8
    #define ROLCHAR1(x) x=(x<<1)|((x>>(CHARBITS-1)) & 01)
         соответственно для сдвига
    на 2 бита надо делать &  03
    на 3                  &  07
    на 4                  & 017
    на skew               & ~(~0 << skew)


1.90.  Напишите программу, которая инвертирует (т.е. заменяет 1 на 0  и  наоборот)  N
битов,  начинающихся  с  позиции  P,  оставляя  другие биты без изменения.  Возможный
ответ:

    unsigned x, mask;
    mask = ~(~0 << N) << P;
    x = (x & ~mask) | (~x & mask);
                 /*   xnew   */

Где маска получается так:

      ~0            = 11111....11111
      ~0 << N       = 11111....11000  /* N нулей */
    ~(~0 << N)      = 00000....00111  /* N единиц */
    ~(~0 << N) << P = 0...01110...00
    /* N единиц на местах P+N-1..P */


1.91.  Операции умножения * и деления / и % обычно достаточно медленны.  В  критичных
по  скорости  функциях  можно  предпринять  некоторые ручные оптимизации, связанные с
представлением чисел в двоичном коде (хороший компилятор делает это сам!) - пользуясь
тем, что операции +, &, >&gt;>&gt; и <&lt;<&lt; гораздо быстрее.  Пусть у нас есть

    unsigned int x;

(для signed операция >&gt;>&gt; может не заполнять освобождающиеся левые биты нулем!) и  2**n
означает 2 в степени n.  Тогда:

    x * (2**n)  = x << n
    x / (2**n)  = x >> n
    x % (2**n)  = x - ((x >> n) << n)
    x % (2**n)  = x & (2**n - 1)
                  это  11...111  n двоичных единиц

Например:









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

    x * 8   = x << 3;
    x / 8   = x >> 3; /* деление нацело     */
    x % 8   = x &  7; /* остаток от деления */
    x * 80  = x*64 + x*16  = (x << 6) + (x << 4);
    x * 320 = (x * 80) * 4 = (x * 80) << 2 =
                             (x << 8) + (x << 6);
    x * 21  = (x << 4) + (x << 2) + x;

    x & 1   = x % 2 = четное(x)? 0:1 = нечетное(x)? 1:0;

    x & (-2) = x & 0xFFFE = | если x = 2*k      то 2*k
                            | если x = 2*k + 1  то 2*k
                            | то есть округляет до четного

Или формула для вычисления количества дней в году (високосный/простой):

    days_in_year = (year % 4 == 0) ? 366 : 365;
            заменяем на
    days_in_year = ((year & 0x03) == 0) ? 366 : 365;

Вот еще одно полезное равенство:

    x = x & (a|~a) = (x & a) | (x & ~a) = (x&a) + (x&~a)
            из чего вытекает, например
    x - (x % 2**n) = x - (x &  (2**n - 1)) =
                   =      x & ~(2**n - 1)  = (x>>n) << n
    x - (x%8) = x-(x&7) = x & ~7

Последняя строка может быть использована в функции untab() в главе  "Текстовая  обра-
ботка".

1.92.  Обычно мы вычисляем min(a,b) так:

    #define min(a, b) (((a) < (b)) ? (a) : (b))

или более развернуто

    if(a < b) min = a;
    else      min = b;

Здесь есть операция сравнения и условный переход.  Однако, если (a < b)  эквивалентно
условию (a - b) < 0, то мы можем избежать сравнения.  Это предположение верно при

    (unsigned int)(a - b) <= 0x7fffffff.

что, например, верно если a и b - оба неотрицательные числа  между  0  и  0x7fffffff.
При этих условиях

    min(a, b) = b + ((a - b) & ((a - b) >> 31));

Как это работает?  Рассмотрим два случая:













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

    Случай 1: a < b

            Здесь (a - b) < 0, поэтому старший (левый, знаковый) бит
            разности (a - b) равен 1.
            Следовательно, (a - b) >> 31 == 0xffffffff,
            и мы имеем:

            min(a, b)       = b + ((a - b) & ((a - b) >> 31))
                            = b + ((a - b) & (0xffffffff))
                            = b + (a - b)
                            = a
            что корректно.


    Случай 2: a >= b

            Здесь (a - b) >= 0, поэтому старший бит разности
            (a - b) равен 0.  Тогда (a - b) >> 31 == 0, и мы имеем:

            min(a, b)       = b + ((a - b) & ((a - b) >> 31))
                            = b + ((a - b) & (0x00000000))
                            = b + (0)
                            = b

            что также корректно.

Статья предоставлена by Jeff Bonwick.

1.93.  Есть ли быстрый способ определить, является ли X степенью двойки? Да, есть.

    int X является степенью двойки
    тогда и только тогда, когда

            (X & (X - 1)) == 0

(в частности 2 здесь окажется степенью двойки).  Как это работает? Пусть X != 0. Если
X - целое, то его двоичное представление таково:

    X = bbbbbbbbbb10000...

где 'bbb' представляет некие биты, '1' - младший бит, и все остальные биты  правее  -
нули. Поэтому:

    X               = bbbbbbbbbb10000...
    X - 1           = bbbbbbbbbb01111...
    ------------------------------------
    X & (X - 1)     = bbbbbbbbbb00000...

Другими словами, X & (X-1) имеет эффект обнуления последнего единичного бита. Если  X
-  степень двойки, то он содержит в двоичном представлении ровно ОДИН такой бит, поэ-
тому его гашение обращает результат в ноль.  Если X - не степень двойки, то  в  слове
есть  хотя  бы ДВА единичных бита, поэтому X & (X-1) должно содержать хотя бы один из
оставшихся единичных битов - то есть не равняться нулю.
     Следствием этого служит программа, вычисляющая число единичных битов в слове X:

    int popc;
    for (popc = 0; X != 0; X &= X - 1)
            popc++;

При этом потребуется не 32 итерации (число бит в int), а ровно столько, сколько  еди-
ничных битов есть в X.  Статья предоставлена by Jeff Bonwick.



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

1.94.  Функция для поиска номера позиции старшего единичного бита в слове.  Использу-
ется  бинарный  поиск:  позиция  находится  максимум за 5 итераций (двоичный логарифм
32х), вместо 32 при линейном поиске.

    int highbit (unsigned int x)
    {
            int i;
            int h = 0;

            for (i = 16; i >= 1; i >>= 1) {
                    if (x >> i) {
                            h += i;
                            x >>= i;
                    }
            }
            return (h);
    }

Статья предоставлена by Jeff Bonwick.

1.95.  Напишите функцию, округляющую свой аргумент вниз до степени двойки.

    #include <stdio.h>
    #define INT short
    #define INFINITY (-999)

    /* Функция, выдающая число, являющееся округлением вниз
     * до степени двойки.
     * Например:
     *      0000100010111000110
     *      заменяется на
     *      0000100000000000000
     * то есть остается только старший бит.
     * В параметр power2 возвращается номер бита,
     * то есть показатель степени двойки. Если число == 0,
     * то эта степень равна минус бесконечности.
     */



























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

    unsigned INT round2(unsigned INT x, int *power2){
    /* unsigned - чтобы число рассматривалось как
     * битовая шкала, а сдвиг >> заполнял левые биты
     * нулем, а не расширял вправо знаковый бит.
     * Идея функции: сдвигать число >> пока не получится 1
     * (можно было бы выбрать 0).
     * Затем сдвинуть << на столько же разрядов, при этом все правые
     * разряды заполнятся нулем, что и требовалось.
     */
            int n = 0;
            if(x == 0){
                    *power2 = -INFINITY; return 0;
            }
            if(x == 1){
                    *power2 = 0; return 1;
            }
            while(x != 1){
                x >>= 1;
                n++;
                if(x == 0 || x == (unsigned INT)(-1)){
                    printf("Вижу %x: похоже, что >> расширяет знаковый бит.\n"
                           "Зациклились!!!\n", x);
                    return (-1);
                }
            }
            x <<= n;

            *power2 = n; return x;
    }


    int counter[ sizeof(unsigned INT) * 8];
    int main(void){
            unsigned INT i;
            int n2;
            for(i=0; ; i++){
                round2(i, &n2);
                if(n2 == -INFINITY) continue;
                counter[n2]++;

                /* Нельзя писать for(i=0; i < (unsigned INT)(-1); i++)
                 * потому что такой цикл бесконечен!
                 */
                if(i == (unsigned INT) (-1)) break;
            }
            for(i=0; i < sizeof counter/sizeof counter[0]; i++)
                    printf("counter[%u]=%d\n", i, counter[i]);
            return 0;
    }


1.96.  Если некоторая вычислительная функция будет вызываться много раз,  не  следует
пренебрегать  возможностью  построить  таблицу решений, где значение вычисляется один
раз для каждого входного значения, зато потом берется непосредственно из таблицы и не
вычисляется  вообще.   Пример:  подсчет числа единичных бит в байте.  Напоминаю: байт
состоит из 8 бит.








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

    #include <stdio.h>

    int nbits_table[256];

    int countBits(unsigned char c){
        int nbits = 0;
        int bit;

        for(bit = 0; bit < 8; bit++){
                if(c & (1 << bit))
                        nbits++;
        }
        return nbits;
    }
    void generateTable(){
        int c;
        for(c=0; c < 256; c++){
            nbits_table[ (unsigned char) c ] = countBits(c);
            /* printf("%u=%d\n", c, nbits_table[ c & 0377 ]); */
        }
    }


    int main(void){
        int c;
        unsigned long bits  = 0L;
        unsigned long bytes = 0L;

        generateTable();

        while((c = getchar()) != EOF){
                bytes++;
                bits += nbits_table[ (unsigned char) c ];
        }
        printf("%lu байт\n", bytes);
        printf("%lu единичных бит\n", bits);
        printf("%lu нулевых бит\n", bytes*8 - bits);
        return 0;
    }


1.97.  Напишите макрос swap(x, y), обменивающий значениями два своих  аргумента  типа
int.

    #define swap(x,y) {int tmp=(x);(x)=(y);(y)=tmp;}
     ... swap(A, B); ...

Как можно обойтись без временной переменной?  Ввиду некоторой курьезности  последнего
способа, приводим ответ:

            int x,  y;      /*  A    B   */

            x = x ^ y;      /*  A^B  B   */
            y = x ^ y;      /*  A^B  A   */
            x = x ^ y;      /*    B  A   */

Здесь используется тот факт, что A^A дает 0.

1.98.  Напишите функцию swap(x, y) при помощи указателей.  Заметьте, что в отличие от
макроса ее придется вызывать как




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

     ... swap(&A, &B); ...

Почему?

1.99.  Пример объясняет разницу между формальным и  фактическим  параметром.   Термин
"формальный"  означает,  что имя параметра можно произвольно заменить другим (во всем
теле функции), т.е. само имя не существенно. Так

    f(x,y)      { return(x   +    y); }     и
    f(муж,жена) { return(муж + жена); }

воплощают одну и ту же функцию.  "Фактический" - означает  значение,  даваемое  пара-
метру в момент вызова функции:

    f(xyz, 43+1);

В Си это означает, что формальным параметрам (в качестве локальных переменных)  прис-
ваиваются начальные значения, равные значениям фактических параметров:

    x = xyz; y = 43 + 1; /*в теле ф-ции их можно менять*/

При выходе из функции формальные параметры (и локальные  переменные)  разопределяются
(и  даже  уничтожаются,  см.  следующий параграф).  Имена формальных параметров могут
"перекрывать" (делать невидимыми,  override)  одноименные  глобальные  переменные  на
время выполнения данной функции.
     Что печатает программа?

    char str[] = "строка1";
    char lin[] = "строка2";
    f(str) char str[];      /* формальный параметр. */
    {       printf( "%s %s\n", str, str );         }


    main(){
            char *s = lin;
                          /* фактический параметр: */
            f(str);       /* массив str            */
            f(lin);       /* массив lin            */
            f(s);         /* переменная s          */
            f("строка3"); /* константа             */
            f(s+2);       /* значение выражения    */
    }

Обратите внимание, что параметр str из f(str) и массив str[]  -  это  две  совершенно
РАЗНЫЕ  вещи, хотя и называющиеся одинаково.  Переименуйте аргумент функции f и пере-
пишите ее в виде

    f(ss) char ss[];      /* формальный параметр. */
    {       printf( "%s %s\n", ss, str );         }

Что печатается теперь? Составьте аналогичный пример с целыми числами.

1.100.  Поговорим более подробно про область видимости имен.

    int x = 12;
    f(x){  int y = x*x;
           if(x) f(x - 1);
    }
    main(){ int x=173, z=21; f(2); }

Локальные переменные и аргументы функции отводятся  в  стеке  при  вызове  функции  и



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

уничтожаются при выходе из нее:

           -+               +- вершина стека
            |локал    y=0   |
            |аргумент x=0   |  f(0)
            |---------------|---------
    "кадр"  |локал    y=1   |
     frame  |аргумент x=1   |  f(1)
            |---------------|---------
            |локал    y=4   |
            |аргумент x=2   |  f(2)
            |---------------|---------
            |локал    z=21  |
    auto:   |локал    x=173 |  main()
    ================================== дно стека
    static:  глобал   x=12
    ==================================

Автоматические локальные переменные и аргументы функции видимы только  в  том  вызове
функции,  в котором они отведены; но не видимы ни в вызывающих, ни в вызываемых функ-
циях (т.е. видимость их ограничена рамками своего "кадра" стека).   Статические  гло-
бальные  переменные  видимы в любом кадре, если только они не "перекрыты" (заслонены)
одноименной локальной переменной (или формалом) в данном кадре.
     Что напечатает программа?  Постарайтесь ответить  на  этот  вопрос  не  выполняя
программу на машине!

                                            x1 x2 x3 x4 x5
    int x = 12;                    /* x1 */ |  .  .  .  .
    f(){                                    |___  .  .  .
      int x = 8;       /* x2, перекрытие */ :  |  .  .  .
      printf( "f: x=%d\n", x );    /* x2 */ :  |  .  .  .
      x++;                         /* x2 */ :  |  .  .  .
    }                                       :--+  .  .  .
    g(x){                          /* x3 */ :______  .  .
      printf( "g: x=%d\n", x );    /* x3 */ :     |  .  .
      x++;                         /* x3 */ :     |  .  .
    }                                       :-----+  .  .
    h(){                                    :_________  .
      int x = 4;                   /* x4 */ :        |  .
      g(x);                        /* x4 */ :        |___
      { int x = 55; }              /* x5 */ :        :  |
      printf( "h: x=%d\n", x );    /* x4 */ :        |--+
    }                                       :--------+
    main(){                                 |
      f(); h();                             |
      printf( "main: x=%d\n", x ); /* x1 */ |
    }                                    ----

Ответ:

    f: x=8
    g: x=4
    h: x=4
    main: x=12

Обратите внимание на функцию g.  Аргументы функции служат копиями  фактических  пара-
метров (т.е. являются локальными переменными функции, проинициализированными значени-
ями фактических параметров), поэтому их изменение не приводит к  изменению  фактичес-
кого параметра.  Чтобы изменять фактический параметр, надо передавать его адрес!





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

1.101.  Поясним последнюю фразу. (Внимание! Возможно, что данный  пункт  вам  следует
читать  ПОСЛЕ  главы про указатели). Пусть мы хотим написать функцию, которая обмени-
вает свои аргументы x и y так, чтобы выполнялось x < y. В качестве  значения  функция
будет выдавать (x+y)/2. Если мы напишем так:

    int msort(x, y) int x, y;
    { int tmp;
      if(x > y){ tmp=x; x=y; y=tmp; }
      return (x+y)/2;
    }
    int x=20, y=8;
    main(){
      msort(x,y); printf("%d %d\n", x, y); /* 20 8 */
    }

то мы не достигнем желаемого эффекта. Здесь переставляются x и  y,  которые  являются
локальными переменными, т.е. копиями фактических параметров.  Поэтому вне функции эта
перестановка никак не проявляется!
     Чтобы мы могли изменить аргументы, копироваться в локальные переменные должны не
сами значения аргументов, а их адреса:

    int msort(xptr, yptr) int *xptr, *yptr;
    { int tmp;
      if(*xptr > *yptr){tmp= *xptr;*xptr= *yptr;*yptr=tmp;}
      return (*xptr + *yptr)/2;
    }
    int x=20, y=8, z;
    main(){
      z = msort(&x,&y);
      printf("%d %d %d\n", x, y, z); /* 8 20 14 */
    }

Обратите внимание, что теперь мы передаем в функцию не значения x и y, а их адреса &x
и &y.
     Именно поэтому (чтобы x смог измениться)  стандартная  функция  scanf()  требует
указания адресов:

    int x; scanf("%d", &x); /* но не scanf("%d", x); */

Заметим, что адрес от арифметического выражения или от константы (а не от переменной)
вычислить нельзя, поэтому законны:

    int xx=12, *xxptr = &xx, a[2] = { 13, 17 };
    int *fy(){ return &y; }
            msort(&x, &a[0]);       msort(a+1, xxptr);
            msort(fy(), xxptr);

но незаконны

            msort(&(x+1), &y);   и  msort(&x, &17);

Заметим еще, что при работе с адресами мы можем направить указатель в неверное  место
и получить непредсказуемые результаты:

            msort(&xx - 20, a+40);

(указатели указывают неизвестно на что).
     Резюме: если аргумент служит только для передачи значения В  функцию  -  его  не
надо  (хотя  и  можно) делать указателем на переменную, содержащую требуемое значение
(если только это уже не указатель).  Если же аргумент служит для передачи значения ИЗ
функции  -  он  должен  быть  указателем  на  переменную  возвращаемого  типа  (лучше



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

возвращать значение как значение функции - return-ом, но иногда надо возвращать  нес-
колько значений - и этого главного "окошка" не хватает).
     Контрольный вопрос: что печатает фрагмент?

    int a=2, b=13, c;
    int f(x, y, z) int x, *y, z;
    {
            *y += x; x *= *y; z--;
            return (x + z - a);
    }
    main(){ c=f(a, &b, a+4); printf("%d %d %d\n",a,b,c); }

(Ответ: 2 15 33)

1.102.  Формальные аргументы функции - это такие же локальные переменные.   Параметры
как бы описаны в самом внешнем блоке функции:

    char *func1(char *s){
            int s;        /* ошибка: повторное определение имени s */
            ...
    }
    int func2(int x, int y){
            int z;
            ...
    }
    соответствует
    int func2(){
            int x = безымянный_аргумент_1_со_стека;
            int y = безымянный_аргумент_2_со_стека;
            int z;
            ...
    }

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

1.103.  Все параметры функции можно разбить на 3 класса:
-    in - входные;
-    out - выходные, служащие для возврата значения из функции;  либо  для  изменения
     данных, находящихся по этому адресу;
-    in/out - для передачи значения в функцию и из функции.

Два последних типа параметров должны быть указателями.  Иногда (особенно в прототипах
и в документации) бывает полезно указывать класс параметра в виде комментария:

    int f( /*IN*/    int  x,
           /*OUT*/   int *yp,
           /*INOUT*/ int *zp){
       *yp = ++x + ++(*zp);
       return (*zp *= x) - 1;
    }
    int x=2, y=3, z=4, res;
    main(){  res = f(x,  &y,  &z);
     printf("res=%d  x=%d y=%d z=%d\n",res,x,y,z);
             /*  14     2    8   15   */
    }

Это полезно потому, что иногда трудно понять - зачем параметр описан  как  указатель.
То  ли  по  нему выдается из функции информация, то ли это просто указатель на данные
(массив), передаваемые в функцию. В первом случае указуемые данные будут изменены,  а
во втором - нет. В первом случае указатель должен указывать на зарезервированную нами



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

область памяти, в которой будет размещен результат.  Пример на эту тему есть в  главе
"Текстовая обработка" (функция bi_conv).

1.104.  Известен такой стиль оформления аргументов функции:

    void func(   int    arg1
             ,   char  *arg2      /* argument 2 */
             ,   char  *arg3[]
             ,   time_t time_stamp
             ){ ... }

Суть его в том, что запятые пишутся в столбик и в одну линию с (  и  )  скобками  для
аргументов.   При  таком  стиле легче добавлять и удалять аргументы, чем при версии с
запятой в конце.  Этот же стиль применим, например, к перечислимым типам:

    enum {  red
         ,  green
         ,  blue
         };

Напишите программу, форматирующую заголовки функций таким образом.

1.105.  В чем ошибка?

    char *val(int x){
         char str[20];
         sprintf(str, "%d", x);
         return str;
    }
    void main(){
         int x = 5; char *s = val(x);
         printf("The values:\n");
         printf("%d %s\n", x, s);
    }

Ответ: val возвращает указатель на автоматическую переменную. При выходе  из  функции
val()  ее локальные переменные (в частности str[]) в стеке уничтожаются - указатель s
теперь указывает на испорченные данные!  Возможным решением проблемы является превра-
щение str[] в статическую переменную (хранимую не в стеке):

    static char str[20];

Однако такой способ не позволит писать конструкции вида

    printf("%s %s\n", val(1), val(2));

так как под оба вызова val() используется один и тот же буфер  str[]  и  будет  печа-
таться  "1 1"  либо  "2 2",  но  не "1 2".  Более правильным будет задание буфера для
результата val() как аргумента:

    char *val(int x, char str[]){
          sprintf(str, "%d", x);
          return str;
    }
    void main(){
         int x=5, y=7;
         char s1[20], s2[20];
         printf("%s %s\n", val(x, s1), val(y, s2));
    }





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

1.106.  Каковы ошибки (не синтаксические) в программе[*]?

            main() {
               double y; int x = 12;
               y = sin (x);
               printf ("%s\n", y);
            }

Ответ:
-    стандартная библиотечная функция sin() возвращает значение типа  double,  но  мы
     нигде  не  информируем об этом компилятор.  Поэтому он считает по умолчанию, что
     эта функция возвращает значение типа int и делает в присваивании y=sin(x) приве-
     дение типа int к типу левого операнда, т.е. к double.  В результате возвращаемое
     значение (а оно на самом деле - double) интерпретируется неверно (как int), под-
     вергается  приведению  типа  (которое портит его), и результат получается совер-
     шенно не таким, как надо.  Подобная же ошибка возникает при использовании  функ-
     ций,  возвращающих указатель, например, функций malloc() и itoa().  Поэтому если
     мы пользуемся библиотечной функцией, возвращающей не int,  мы  должны  предвари-
     тельно (до первого использования) описать ее, например[**]:

                 extern double sin();
                 extern long   atol();
                 extern char  *malloc(), *itoa();

     Это же относится и к нашим собственным функциям, которые мы  используем  прежде,
     чем  определяем  (поскольку  из  заголовка функции компилятор обнаружит, что она
     выдает не целое значение, уже после того, как странслирует обращение к ней):

         /*extern*/ char *f();
                    main(){
                         char *s;
                         s = f(1); puts(s);
                    }
                    char *f(n){ return "knights" + n; }

     Функции, возвращающие целое, описывать не  требуется.   Описания  для  некоторых
     стандартных  функций  уже помещены в системные include-файлы. Например, описания
     для  математических  функций  (sin,  cos,  fabs,   ...)   содержатся   в   файле
     /usr/include/math.h. Поэтому мы могли бы написать перед main

                 #include <math.h>
         вместо
                 extern double sin(), cos(), fabs();

-    библиотечная функция sin() требует аргумента типа  double,  мы  же  передаем  ей
     аргумент  типа int (который короче типа double и имеет иное внутреннее представ-
     ление).  Он будет неправильно  проинтерпретирован  функцией,  т.е.  мы  вычислим
     синус отнюдь НЕ числа 12.  Следует писать:

                 y = sin( (double) x );
              и  sin(12.0); вместо sin(12);

____________________
   [*] Для трансляции программы, использующей стандартные математические  функции  sin,
cos, exp, log, sqrt, и.т.п. следует задавать ключ компилятора -lm
   cc file.c -o file -lm
   [**] Слово extern ("внешняя") не является обязательным, но является  признаком  хоро-
шего  тона  -  вы сообщаете программисту, читающему эту программу, что данная функция
реализована в другом файле, либо вообще является стандартной и берется из библиотеки.





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

-    в printf мы печатаем значение типа  double  по  неправильному  формату:  следует
     использовать  формат  %g  или  %f (а для ввода при помощи scanf() - %lf).  Очень
     частой ошибкой является печать значений типа long по формату %d вместо %ld .

Первых двух проблем в современном Си удается избежать  благодаря  заданию  прототипов
функций  (о них подробно рассказано ниже, в конце главы "Текстовая обработка").  Нап-
ример, sin имеет прототип

            double sin(double x);

Третяя проблема (ошибка в формате) не может быть локализована средствами Си  и  имеет
более-менее приемлемое решение лишь в языке C++ (streams).

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

            int sum(x,y,z){  return(x+y+z); }
            main(){
                    int s = sum(12,15);
                    printf("%d\n", s);
            }

Заметим, что если бы для функции sum() был задан прототип, то  компилятор  поймал  бы
эту  нашу оплошность! Заметьте, что сейчас значение z в sum() непредсказуемо. Если бы
мы вызывали

            s = sum(12,15,17,24);

то лишние аргументы были бы просто проигнорированы (но и тут  может  быть  сюрприз  -
аргументы могли бы игнорироваться с ЛЕВОГО конца списка!).
     А вот пример опасной ошибки, которая не ловится даже прототипами:

            int x;   scanf("%d%d", &x );

Второе число по формату %d будет считано  неизвестно  по  какому  адресу  и  разрушит
память  программы.  Ни  один компилятор не проверяет соответствие числа %-ов в строке
формата числу аргументов scanf и printf.

1.108.  Что здесь означают внутренние (,,) в вызове функции f() ?

    f(x, y, z){
            printf("%d %d %d\n", x, y, z);
    }
    main(){ int t;
            f(1, (2, 3, 4), 5);
            f(1, (t=3,t+1), 5);
    }

Ответ: (2,3,4) - это оператор "запятая", выдающий значение  последнего  выражения  из
списка перечисленных через запятую выражений. Здесь будет напечатано 1 4 5. Кажущаяся
двойственность возникает из-за того, что аргументы функции тоже  перечисляются  через
запятую, но это совсем другая синтаксическая конструкция. Вот еще пример:

    int y = 2, x;
    x = (y+4, y, y*2); printf("%d\n", x);      /*  4 */
    x =  y+4, y, y*2 ; printf("%d\n", x);      /*  6 */
    x = (x=y+4, ++y, x*y); printf("%d\n", x);  /* 18 */

Сначала обратим внимание на первую строку. Это - объявление переменных x и y  (причем
y  -  с  инициализацией),  поэтому  запятая здесь - не ОПЕРАТОР, а просто разделитель
объявляемых переменных!  Далее следуют три строки выполняемых операторов.   В  первом
случае выполнилось x=y*2; во втором x=y+4 (т.к.  приоритет у присваивания выше, чем у



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

запятой). Обратите внимание, что выражение без присваивания (которое может вообще  не
иметь эффекта или иметь только побочный эффект) вполне законно:

    x+y;   или   z++;   или   x == y+1;   или   x;

В частности, все вызовы функций-процедур именно таковы (это выражения  без  оператора
присваивания, имеющие побочный эффект):

    f(12,x);        putchar('Ы');

в отличие, скажем, от x=cos(0.5)/3.0; или c=getchar();
     Оператор "запятая" разделяет выражения, а не просто операторы, поэтому если хоть
один из перечисленных операторов не выдает значения, то это является ошибкой:

    main(){ int i, x = 0;
      for(i=1; i < 4; i++)
          x++, if(x > 2) x = 2; /* используй { ; } */
    }

оператор if не выдает значения. Также логически ошибочно использование  функции  типа
void (не возвращающей значения):

    void f(){}
      ...
      for(i=1; i < 4; i++)
          x++, f();

хотя компилятор может допустить такое использование.
     Вот еще один пример того, как можно переписать один и тот же фрагмент,  применяя
разные синтаксические конструкции:

    if( условие ) { x = 0; y = 0; }
    if( условие )   x = 0, y = 0;
    if( условие )   x = y = 0;


1.109.  Найдите опечатку:

            switch(c){
            case 1:
                    x++; break;
            case 2:
                    y++; break;
            defalt:
                    z++; break;
            }

Если c=3, то z++ не происходит. Почему?  (Потому, что defalt: - это метка, а не  клю-
чевое слово default).

1.110.  Почему программа зацикливается и печатает совсем не то, что нажато на клавиа-
туре, а только 0 и 1?

            while ( c = getchar() != 'e')
                    printf("%d %c\n, c, c);

Ответ: данный фрагмент должен был выглядеть так:

            while ((c = getchar()) != 'e')
                    printf("%d %c\n, c, c);




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

Сравнение в Си имеет высший приоритет, нежели присваивание!  Мораль: надо быть внима-
тельнее к приоритетам операций.  Еще один пример на похожую тему:

           вместо
    if(  x & 01  == 0 ) ...     if( c&0377  > 0300)...;
           надо:
    if( (x & 01) == 0 ) ...     if((c&0377) > 0300)...;

И еще пример с аналогичной ошибкой:

    FILE *fp;
    if( fp = fopen( "файл", "w" ) == NULL ){
        fprintf( stderr, "не могу писать в файл\n");
        exit(1);
    }
    fprintf(fp,"Good bye, %s world\n","cruel"); fclose(fp);

В этом примере файл открывается, но fp  равно  0  (логическое  значение!)  и  функция
fprintf() не срабатывает (программа падает по защите памяти[*]).
     Исправьте аналогичную ошибку (на приоритет операций) в следующей функции:

    /* копирование строки from в to */
    char *strcpy( to, from ) register char *from, *to;
    {
         char *p = to;
         while( *to++ = *from++ != '\0' );
         return p;
    }


1.111.  Сравнения с нулем (0, NULL, '\0') в Си принято опускать (хотя это  не  всегда
способствует ясности).

        if( i == 0 ) ...;    -->    if( !i ) ... ;
        if( i != 0 ) ...;    -->    if(  i ) ... ;

например, вместо

    char s[20], *p ;
    for(p=s; *p != '\0'; p++ ) ... ;
         будет
    for(p=s; *p; p++ ) ... ;

и вместо

    char s[81], *gets();
    while( gets(s) != NULL ) ... ;
         будет
    while( gets(s)) ... ;

Перепишите strcpy в этом более лаконичном стиле.


____________________
   [*] "Падать" - программистский жаргон. Означает "аварийно завершаться".  "Защита па-
мяти"  - обращение по некорректному адресу.  В UNIX такая ошибка ловится аппаратно, и
программа будет убита одним из сигналов: SIGBUS, SIGSEGV,  SIGILL.   Система  сообщит
нечто  вроде  "ошибка  шины".  Знайте, что это не ошибка аппаратуры и не сбой, а ВАША
ошибка!





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

1.112.  Истинно ли выражение

            if( 2 < 5 < 4 )

Ответ: да!  Дело в том, что Си не имеет логического типа, а вместо "истина" и  "ложь"
использует  целые  значения  "не 0" и "0" (логические операции выдают 1 и 0).  Данное
выражение в условии if эквивалентно следующему:

            ((2 < 5) < 4)

Значением (2 < 5) будет 1. Значением (1 < 4) будет тоже 1 (истина).  Таким образом мы
получаем совсем не то, что ожидалось. Поэтому вместо

            if( a < x < b )

надо писать

            if( a < x && x < b )


1.113.  Данная программа должна печатать коды вводимых символов.   Найдите  опечатку;
почему цикл сразу завершается?

      int c;
      for(;;) {
          printf("Введите очередной символ:");
          c = getchar();
          if(c = 'e') {
             printf("нажато e, конец\n"); break;
          }
          printf( "Код %03o\n", c & 0377 );
      }

Ответ: в if имеется опечатка: использовано `=' вместо `=='.
     Присваивание в Си (а также операции +=, -=, *=, и.т.п.)  выдает  новое  значение
левой части, поэтому синтаксической ошибки здесь нет!  Написанный оператор равносилен

          c = 'e'; if( c ) ... ;

и, поскольку 'e'!= 0, то условие оказывается истинным!  Это еще и следствие того, что
в Си нет специального логического типа (истина/ложь).  Будьте внимательны: компилятор
не считает ошибкой использование оператора = вместо == внутри условий  if  и  условий
циклов (хотя некоторые компиляторы выдают предупреждение).
     Еще аналогичная ошибка:

    for( i=0; !(i = 15) ; i++ ) ... ;

(цикл не выполняется); или

    static char s[20] = "   abc"; int i=0;
    while(s[i] = ' ') i++;
    printf("%s\n", &s[i]); /* должно напечататься abc */

(строка заполняется пробелами и цикл не кончается).
     То, что оператор присваивания имеет значение, весьма удобно:

    int x, y, z;           это на самом деле
    x = y = z = 1;         x = (y = (z = 1));






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

или[*]

    y=f( x += 2 );       // вместо x+=2; y=f(x);
    if((y /= 2) > 0)...; // вместо y/=2; if(y>0)...;

Вот пример упрощенной игры в "очко" (упрощенной - т.к. не учитывается  ограниченность
числа карт каждого типа в колоде (по 4 штуки)):

    #include <stdio.h>
    main(){
      int sum = 0, card; char answer[36];
      srand( getpid());  /* рандомизация */
      do{  printf( "У вас %d очков. Еще? ", sum);
           if( *gets(answer) == 'n' ) break;
           /* иначе маловато будет */
           printf( "  %d очков\n",
                   card = 6 + rand() % (11 - 6 + 1));
      } while((sum += card) < 21);      /* SIC ! */
      printf ( sum == 21 ? "очко\n"   :
               sum >  21 ? "перебор\n":
                           "%d очков\n", sum);
    }

Вот еще пример, использующийся для подсчета  правильного  размера  таблицы.  Обратите
внимание,  что  присваивания  используются  в сравнениях, в аргументах вызова функции
(printf), т.е. везде, где допустимо выражение:

    #include <stdio.h>
    int width = 20; /* начальное значение ширины поля */
    int len; char str[512];
    main(){
      while(gets(str)){
        if((len = strlen(str)) > width){
    fprintf(stderr,"width увеличить до %d\n", width=len);
        }
        printf("|%*.*s|\n", -width, width, str);
      }
    }

Вызывай эту программу как
  a.out < входнойФайл > /dev/null

1.114.  Почему программа "зависает" (на самом деле - зацикливается) ?

            int x = 0;
            while( x < 100 );
                    printf( "%d\n", x++ );
            printf( "ВСЕ\n" );

Указание: где кончается цикл while?
     Мораль: не надо ставить ; где попало. Еще  мораль:  даже  отступы  в  оформлении
программы не являются гарантией отсутствия ошибок в группировке операторов.

1.115.  Вообще, приоритеты операций в Си  часто  не  соответствуют  ожиданиям  нашего
здравого смысла. Например, значением выражения:

            x = 1 << 2 + 1 ;
____________________
   [*] Конструкция //текст, которая будет изредка попадаться в дальнейшем - это коммен-
тарий  в  стиле  языка  C++.  Такой  комментарий  простирается от символа // до конца
строки.



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

будет 8, а не 5, поскольку сложение выполнится первым.  Мораль: в  затруднительных  и
неочевидных случаях лучше явно указывать приоритеты при помощи круглых скобок:

            x = (1 << 2) + 1 ;

Еще пример: увеличивать x на 40, если установлен флаг, иначе на 1:

            int bigFlag = 1, x = 2;
            x = x + bigFlag ? 40 : 1;
            printf( "%d\n", x );

ответом будет 40, а не 42, поскольку это

            x = (x + bigFlag) ? 40 : 1;

а не

            x = x + (bigFlag ? 40 : 1);

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

    long a = 50, x; int b = 4;
    x = (a * 100) / b;
      /* деление целочисленное с остатком ! */
    и как   x = (a * 100)/b = 5000/4 = 1250
    и как   x = (a/b) * 100 = 12*100 = 1200

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

    { long a100 = a * 100; x = a100 / b; }


1.116.  Составьте программу вычисления тригонометрической функции.  Название  функции
и  значение  аргумента  передаются в качестве параметров функции main (см. про argv и
argc в главе "Взаимодействие с UNIX"):

            $ a.out sin 0.5
            sin(0.5)=0.479426

(здесь и далее значок $ обозначает  приглашение,  выданное  интерпретатором  команд).
Для  преобразования строки в значение типа double воспользуйтесь стандартной функцией
atof().

      char *str1, *str2, *str3; ...
      extern double atof();    double x = atof(str1);
      extern long   atol();    long   y = atol(str2);
      extern int    atoi();    int    i = atoi(str3);

либо

    sscanf(str1, "%f",  &x);
    sscanf(str2, "%ld", &y); sscanf(str3,"%d", &i);

К слову заметим, что обратное преобразование - числа в текст - удобнее всего делается
при  помощи  функции  sprintf(),  которая  аналогична  printf(), но сформированная ею
строка-сообщение не выдается на экран, а заносится в массив:



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

            char represent[ 40 ];
            int i = ... ;
            sprintf( represent, "%d", i );


1.117.  Составьте программу вычисления полинома n-ой степени:

               n          n-1
     Y = A  * X + A    * X    + ... + A0
          n        n-1

         схема (Горнера):

     Y = A0 + X * ( A1 + X * ( A2 + ... + X * An )))...)

Оформите алгоритм как функцию с переменным числом параметров:

    poly( x, n, an, an-1, ... a0 );

О том, как это сделать - читайте раздел руководства по UNIX man varargs.  Ответ:

    #include <varargs.h>
    double poly(x, n, va_alist)
           double x; int n; va_dcl
    {
      va_list args;
      double sum = 0.0;
      va_start(args); /* инициализировать список арг-тов */
      while( n-- >= 0 ){
         sum *= x;
         sum += va_arg(args, double);
         /* извлечь след. аргумент типа double */
      }
      va_end(args);   /* уничтожить список аргументов */
      return sum;
    }


    main(){
                            /* y = 12*x*x + 3*x + 7 */
      printf( "%g\n", poly(2.0, 2, 12.0,    3.0,  7.0));
    }

Прототип этой функции:

    double poly(double x, int n, ... );

В этом примере использованы макросы va_нечто.   Часть  аргументов,  которая  является
списком переменной длины, обозначается в списке параметров как va_alist, при этом она
объявляется как va_dcl в списке типов параметров. Заметьте, что точка-с-запятой после
va_dcl  не нужна!  Описание va_list args; объявляет специальную "связную" переменную;
смысл ее машинно зависим.  va_start(args) инициализирует эту переменную списком  фак-
тических  аргументов,  соответствующих va_alist-у.  va_end(args) деинициализирует эту
переменную (это надо делать обязательно, поскольку инициализация могла быть связана с
конструированием  списка  аргументов при помощи выделения динамической памяти; теперь
мы должны уничтожить этот список и освободить память).  Очередной аргумент типа  TYPE
извлекается из списка при помощи

    TYPE x = va_arg(args, TYPE);

Список аргументов просматривается  слева  направо  в  одном  направлении,  возврат  к



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

предыдущему аргументу невозможен.
Нельзя указывать в качестве типов char, short, float:

    char ch = va_arg(args, char);

поскольку в языке Си аргументы функции таких типов автоматически расширяются  в  int,
int, double соответственно. Корректно будет так:

    int ch = va_arg(args, int);


1.118.  Еще об одной ловушке в языке Си на PDP-11 (и в компиляторах бывают ошибки!):

            unsigned x = 2;
            printf( "%ld %ld",
                    - (long) x,
                    (long)  -x
            );

Этот фрагмент напечатает числа -2 и 65534.  Во втором случае при  приведении  к  типу
long  был  расширен  знаковый  бит.   Встроенная операция sizeof выдает значение типа
unsigned.  Подумайте, каков будет эффект в следующем фрагменте программы?

            static struct point{ int  x,  y    ;}
                          p =  {     33, 13   };
            FILE *fp = fopen( "00", "w" );

            /* вперед на длину одной структуры */
            fseek( fp, (long)  sizeof( struct point ), 0 );

            /* назад на длину одной структуры */
     /*!*/  fseek( fp, (long) -sizeof( struct point ), 1 );

            /* записываем в начало файла одну структуру */
            fwrite( &p, sizeof p, 1, fp );

            /* закрываем файл */
            fclose( fp );

Где должен находиться минус во втором вызове fseek для получения  ожидаемого  резуль-
тата?  (Данный пример может вести себя по-разному на разных машинах, вопросы касаются
PDP-11).

1.119.  Обратимся к указателям на функции:

    void g(x){ printf("%d: here\n", x); }
    main(){
      void (*f)() = g;  /* Указатель смотрит на функцию g() */
      (*f)(1); /* Старая форма вызова функции по указателю */

    f (2); /* Новая форма вызова */

/* В обоих случаях вызывается g(x); */ } Что печатает программа? typedef void (*(*FUN))(); /* Попытка изобразить рекурсивный тип typedef FUN (*FUN)(); */ FUN g(FUN f){ return f; } void main(){ FUN y = g(g(g(g(g)))); if(y == g) printf("OK\n"); А. Богатырев, 1992-95 - 60 - Си в UNIX } Что печатает программа? char *f(){ return "Hello, user!"; } g(func) char * (*func)(); { puts((*func)()); } main(){ g(f); } Почему было бы неверно написать main(){ g(f()); } Еще аналогичная ошибка (посмотрите про функцию signal в главе "Взаимодействие с UNIX"): #include <signal.h> f(){ printf( "Good bye.\n" ); exit(0); } main(){ signal ( SIGINT, f() ); ... } Запомните, что f() - это ЗНАЧЕНИЕ функции f (т.е. она вызывается и нечто возвращает return-ом; это-то значение мы и используем), а f - это АДРЕС функции f (раньше это так и писалось &f), то есть метка начала ее машинных кодов ("точка входа"). 1.120. Что напечатает программа? (Пример посвящен указателям на функции и массивам функций): int f(n){ return n*2; } int g(n){ return n+4; } int h(n){ return n-1; } int (*arr[3])() = { f, g, h }; main(){ int i; for(i=0; i < 3; i++ ) printf( "%d\n", (*arr[i])(i+7) ); } 1.121. Что напечатает программа? extern double sin(), cos(); main(){ double x; /* cc -lm */ for(x=0.0; x < 1.0; x += 0.2) printf("%6.4g %6.4g %6.4g\n", (x > 0.5 ? sin : cos)(x), sin(x), cos(x)); } то же в варианте А. Богатырев, 1992-95 - 61 - Си в UNIX extern double sin(), cos(); main(){ double x; double (*f)(); for(x=0.0; x < 1.0; x += 0.2){ f = (x > 0.5 ? sin : cos); printf("%g\n", (*f)(x)); } } 1.122. Рассмотрите четыре реализации функции факториал: или n! = n * (n-1)! где 0! = 1 Все они иллюстрируют определенные подходы в программировании: /* ЦИКЛ (ИТЕРАЦИЯ) */ int factorial1(n){ int res = 1; while(n > 0){ res *= n--; } return res; } /* ПРОСТАЯ РЕКУРСИЯ */ int factorial2(n){ return (n==0 ? 1 : n * factorial2(n-1)); } /* Рекурсия, в которой функция вызывается рекурсивно * единственный раз - в операторе return, называется * "хвостовой рекурсией" (tail recursion) и * легко преобразуется в цикл */ /* АВТОАППЛИКАЦИЯ */ int fi(f, n) int (*f)(), n; { if(n == 0) return 1; else return n * (*f)(f, n-1); } int factorial3(n){ return fi(fi, n); } /* РЕКУРСИЯ С НЕЛОКАЛЬНЫМ ПЕРЕХОДОМ */ #include <setjmp.h> jmp_buf checkpoint; void fact(n, res) register int n, res; { if(n) fact(n - 1, res * n); else longjmp(checkpoint, res+1); } int factorial4(n){ int res; if(res = setjmp(checkpoint)) return (res - 1); else fact(n, 1); } 1.123. Напишите функцию, печатающую целое число в системе счисления с основанием base. Ответ: А. Богатырев, 1992-95 - 62 - Си в UNIX printi( n, base ){ register int i; if( n < 0 ){ putchar( '-' ); n = -n; } if( i = n / base ) printi( i, base ); i = n % base ; putchar( i >= 10 ? 'A' + i - 10 : '0' + i ); } Попробуйте написать нерекурсивный вариант с накоплением ответа в строке. Приве- дем рекурсивный вариант, накапливающий ответ в строке s и пользующийся аналогом функ- ции printi: функция prints - такая же, как printi, но вместо вызовов putchar(нечто); в ней написаны операторы *res++ = нечто; и рекурсивно вызывается конечно же prints. Итак: static char *res; ... текст функции prints ... char *itos( n, base, s ) char *s; /* указывает на char[] массив для ответа */ { res = s; prints(n, base); *res = '\0'; return s; } main(){ char buf[20]; printf( "%s\n", itos(19,2,buf); } 1.124. Напишите функцию для побитной распечатки целого числа. Имейте в виду, что число содержит 8 * sizeof(int) бит. Указание: используйте операции битового сдвига и &. Ответ: printb(n){ register i; for(i = 8 * sizeof(int) - 1; i >= 0; --i) putchar(n & (1 << i) ? '1':'0'); } 1.125. Напишите функцию, склоняющую существительные русского языка в зависимости от их числа. Например: printf( "%d кирпич%s", n, grammar( n, "ей", "", "а" )); Ответ: char *grammar( i, s1, s2, s3 ) char *s1, /* прочее */ *s2, /* один */ *s3; /* два, три, четыре */ { i = i % 100; if( i > 10 && i <= 20 ) return s1; i = i % 10; if( i == 1 ) return s2; if( i == 2 || i == 3 || i == 4 ) return s3; return s1; } А. Богатырев, 1992-95 - 63 - Си в UNIX 1.126. Напишите оператор printf, печатающий числа из интервала 0..99 с добавлением нуля перед числом, если оно меньше 10 : 00 01 ... 09 10 11 ... Используйте условное выражение, формат. Ответ: printf ("%s%d", n < 10 ? "0" : "", n); либо printf ("%02d", n ); либо printf ("%c%c", '0' + n/10, '0' + n%10 ); 1.127. Предостережем от одной ошибки, часто допускаемой начинающими. putchar( "c" ); является ошибкой. putchar( 'c' ); верно. Дело в том, что putchar требует аргумент - символ, тогда как "c" - СТРОКА из одного символа. Большинство компиляторов (те, которые не проверяют прототипы вызова стан- дартных функций) НЕ обнаружит здесь никакой синтаксической ошибки (кстати, ошибка эта - семантическая). Также ошибочны операторы printf ( '\n' ); /* нужна строка */ putchar( "\n" ); /* нужен символ */ putchar( "ab" ); /* нужен символ */ putchar( 'ab' ); /* ошибка в буквенной константе */ char c; if((c = getchar()) == "q" ) ... ; /* нужно писать 'q' */ Отличайте строку из одного символа и символ - это разные вещи! (Подробнее об этом - в следующей главе). 1.128. Весьма частой является ошибка "промах на единицу", которая встречается в очень многих и разнообразных случаях. Вот одна из возможных ситуаций: int m[20]; int i = 0; while( scanf( "%d", & m[i++] ) != EOF ); printf( "Ввели %d чисел\n", i ); В итоге i окажется на 1 больше, чем ожидалось. Разберитесь в чем дело. Ответ: аргументы функции вычисляются до ее вызова, поэтому когда мы достигаем конца файла и scanf возвращает EOF, i++ в вызове scanf все равно делается. Надо напи- сать while( scanf( "%d", & m[i] ) != EOF ) i++; 1.129. Замечание по стилистике: при выводе сообщения на экран printf( "Hello \n" ); пробелы перед \n достаточно бессмысленны, поскольку на экране никак не отобразятся. Надо писать (экономя память) printf( "Hello\n" ); А. Богатырев, 1992-95 - 64 - Си в UNIX Единственный случай, когда такие пробелы значимы - это когда вы выводите текст инвер- сией. Тогда пробелы отображаются как светлый фон. Еще неприятнее будет printf( "Hello\n " ); поскольку концевые пробелы окажутся в начале следующей строки. 1.130. printf - интерпретирующая функция, т.е. работает она довольно медленно. Поэ- тому вместо char s[20]; int i; ... printf( "%c", s[i] ); и printf( "\n" ); надо всегда писать putchar( s[i] ); и putchar( '\n' ); поскольку printf в конце-концов (сделав все преобразования по формату) внутри себя вызывает putchar. Так сделаем же это сразу! 1.131. То, что параметр "формат" в функции printf может быть выражением, позволяет делать некоторые удобные вещи. Например: int x; ... printf( x ? "значение x=%d\n" : "x равен нулю\n\n", x); Формат здесь - условное выражение. Если x!=0, то будет напечатано значение x по фор- мату %d. Если же x==0, то будет напечатана строка, не содержащая ни одного %-та. В результате аргумент x в списке аргументов будет просто проигнорирован. Однако, нап- ример int x = ... ; printf( x > 30000 ? "%f\n" : "%d\n", x); (чтобы большие x печатались в виде 31000.000000) незаконно, поскольку целое число нельзя печатать по формату %f ни в каких случаях. Единственным способом сделать это является явное приведение x к типу double: printf("%f\n", (double) x); Будет ли законен оператор? printf( x > 30000 ? "%f\n" : "%d\n", x > 30000 ? (double) x : x ); Ответ: нет. Условное выражение для аргумента будет иметь "старший" тип - double. А значение типа double нельзя печатать по формату %d. Мы должны использовать здесь оператор if: if( x > 30000 ) printf("%f\n", (double)x); else printf("%d\n", x); 1.132. Напишите функцию, печатающую размер файла в удобном виде: если файл меньше одного килобайта - печатать его размер в байтах, если же больше - в килобайтах (и мегабайтах). #define KBYTE 1024L /* килобайт */ #define THOUSAND 1024L /* кб. в мегабайте */ А. Богатырев, 1992-95 - 65 - Си в UNIX void tellsize(unsigned long sz){ if(sz < KBYTE) printf("%lu байт", sz); else{ unsigned long Kb = sz/KBYTE; unsigned long Mb = Kb/THOUSAND; unsigned long Dec = ((sz % KBYTE) * 10) / KBYTE; if( Mb ){ Kb %= THOUSAND; printf( Dec ? "%lu.%03lu.%01lu Мб." : "%lu.%lu Мб.", Mb, Kb, Dec ); } else printf( Dec ? "%lu.%01lu Кб.":"%lu Кб.", Kb, Dec); } putchar('\n'); } 1.133. Для печати строк используйте printf("%s", string); /* A */ но не printf(string); /* B */ Если мы используем вариант B, а в строке встретится символ '%' char string[] = "abc%defg"; то %d будет воспринято как формат для вывода целого числа. Во-первых, сама строка %d не будет напечатана; во-вторых - что же будет печататься по этому формату, когда у нас есть лишь единственный аргумент - string?! Напечатается какой-то мусор! 1.134. Почему оператор char s[20]; scanf("%s", s); printf("%s\n", s); в ответ на ввод строки Пушкин А.С. печатает только "Пушкин"? Ответ: потому, что концом текста при вводе по формату %s считается либо \n, либо пробел, либо табуляция, а не только \n; то есть формат %s читает слово из текста. Чтение всех символов до конца строки, (включая пробелы) должно выглядеть так: scanf("%[^\n]\n", s); %[^\n] - читать любые символы, кроме \n (до \n) \n - пропустить \n на конце строки %[abcdef] - читать слово, состоящее из перечисленных букв. %[^abcde] - читать слово из любых букв, кроме перечисленных (прерваться по букве из списка). Пусть теперь строки входной информации имеют формат: Фрейд Зигмунд 1856 1939 Пусть мы хотим считывать в строку s фамилию, в целое y - год рождения, а прочие поля - игнорировать. Как это сделать? Нам поможет формат "подавление присваивания" %*: scanf("%s%*s%d%*[^\n]\n", s, &y ); А. Богатырев, 1992-95 - 66 - Си в UNIX %* пропускает поле по формату, указанному после *, не занося его значение ни в какую переменную, а просто "забывая" его. Так формат "%*[^\n]\n" игнорирует "хвост" строки, включая символ перевода строки. Символы " ", "\t", "\n" в формате вызывают пропуск всех пробелов, табуляций, переводов строк во входном потоке, что можно описать как int c; while((c = getc(stdin))== ' ' || c == '\t' || c == '\n' ); либо как формат %*[ \t\n] Перед числовыми форматами (%d, %o, %u, %ld, %x, %e, %f), а также %s, пропуск пробелов делается автоматически. Поэтому scanf("%d%d", &x, &y); и scanf("%d %d", &x, &y); равноправны (пробел перед вторым %d просто не нужен). Неявный пропуск пробелов не делается перед %c и %[... , поэтому в ответ на ввод строки "12 5 x" пример main(){ int n, m; char c; scanf("%d%d%c", &n, &m, &c); printf("n=%d m=%d c='%c'\n", n, m, c); } напечатает "n=12 m=5 c=' '", то есть в c будет прочитан пробел (предшествовавший x), а не x. Автоматический пропуск пробелов перед %s не позволяет считывать по %s строки, лидирующие пробелы которых должны сохраняться. Чтобы лидирующие пробелы также считы- вались, следует использовать формат scanf("%[^\n]%*1[\n]", s); в котором модификатор длины 1 заставляет игнорировать только один символ \n, а не ВСЕ пробелы и переводы строк, как "\n". К сожалению (как показал эксперимент) этот формат не в состоянии прочесть пустую строку (состоящую только из \n). Поэтому можно сделать глобальный вывод: строки надо считывать при помощи функций gets() и fgets()! 1.135. Еще пара слов про scanf: scanf возвращает число успешно прочитанных им данных (обработанных %-ов) или EOF в конце файла. Неудача может наступить, если данное во входном потоке не соответствует формату, например строка 12 quack для int d1; double f; scanf("%d%lf", &d1, &f); В этом случае scanf прочтет 12 по формату %d в переменную d1, но слово quack не отве- чает формату %lf, поэтому scanf прервет свою работу и выдаст значение 1 (успешно про- чел один формат). Строка quack останется невостребованной - ее прочитают последующие вызовы функций чтения; а сейчас f останется неизмененной. 1.136. Си имеет квалификатор const, указывающий, что значение является не перемен- ной, а константой, и попытка изменить величину по этому имени является ошибкой. Во многих случаях const может заменить #define, при этом еще явно указан тип константы, что полезно для проверок компилятором. А. Богатырев, 1992-95 - 67 - Си в UNIX const int x = 22; x = 33; /* ошибка: константу нельзя менять */ Использование const с указателем: Указуемый объект - константа const char *pc = "abc"; pc[1] = 'x'; /* ошибка */ pc = "123"; /* OK */ Сам указатель - константа char *const cp = "abc"; cp[1] = 'x'; /* OK */ cp = "123"; /* ошибка */ Указуемый объект и сам указатель - константы const char *const cpc = "abc"; cpc[1] = 'x'; /* ошибка */ cpc = "123"; /* ошибка */ Указатель на константу необходимо объявлять как const TYPE* int a = 1; const int b = 2; const int *pca = &a; /* OK, просто рассматриваем a как константу */ const int *pcb = &b; /* OK */ int *pb = &b; /* ошибка, так как тогда возможно было бы написать */ *pb = 3; /* изменить константу b */ 1.137. Стандартная функция быстрой сортировки qsort (алгоритм quick sort) имеет такой формат: чтобы отсортировать массив элементов типа TYPE TYPE arr[N]; надо вызывать qsort(arr,/* Что сортировать? Не с начала: arr+m */ N, /* Сколько первых элементов массива? */ /* можно сортировать только часть: n < N */ sizeof(TYPE),/* Или sizeof arr[0] */ /* размер одного элемента массива*/ cmp); где int cmp(TYPE *a1, TYPE *a2); функция сравнения элементов *a1 и *a2. Ее аргументы - АДРЕСА двух каких-то элементов сортируемого массива. Функцию cmp мы должны написать сами - это функция, задающая упорядочение элементов массива. Для сортировки по возрастанию функция cmp() должна возвращать целое < 0, если *a1 должно идти раньше *a2 < = 0, если *a1 совпадает с *a2 == > 0, если *a1 должно идти после *a2 > Для массива строк элементы массива имеют тип (char *), поэтому аргументы функции имеют тип (char **). Требуемому условию удовлетворяет такая функция: А. Богатырев, 1992-95 - 68 - Си в UNIX char *arr[N]; ... cmps(s1, s2) char **s1, **s2; { return strcmp(*s1, *s2); } (Про strcmp смотри раздел "Массивы и строки"). Заметим, что в некоторых системах программирования (например в TurboC++ [*]) вы должны использовать функцию сравнения с прототипом int cmp (const void *a1, const void *a2); и внутри нее явно делать приведение типа: cmps (const void *s1, const void *s2) { return strcmp(*(char **)s1, *(char **)s2); } или можно поступить следующим образом: int cmps(char **s1, char **s2){ return strcmp(*s1, *s2); } typedef int (*CMPS)(const void *, const void *); qsort((void *) array, ..., ..., (CMPS) cmps); Наконец, возможно и просто объявить int cmps(const void *A, const void *B){ return strcmp(A, B); } Для массива целых годится такая функция сравнения: int arr[N]; ... cmpi(i1, i2) int *i1, *i2; { return *i1 - *i2; } Для массива структур, которые мы сортируем по целому полю key, годится struct XXX{ int key; ... } arr[N]; cmpXXX(st1, st2) struct XXX *st1, *st2; { return( st1->key - st2->key ); } Пусть у нас есть массив long. Можно ли использовать long arr[N]; ... cmpl(L1, L2) long *L1, *L2; { return *L1 - *L2; } Ответ: оказывается, что нет. Функция cmpl должна возвращать целое, а разность двух long-ов имеет тип long. Поэтому компилятор приводит эту разность к типу int (как правило обрубанием старших битов). При этом (если long-числа были велики) результат может изменить знак! Например: main(){ int n; long a = 1L; long b = 777777777L; n = a - b; /* должно бы быть отрицательным... */ printf( "%ld %ld %d\n", a, b, n ); } ____________________ [*] TurboC - компилятор Си в MS DOS, разработанный фирмой Borland International. А. Богатырев, 1992-95 - 69 - Си в UNIX печатает 1 777777777 3472. Функция сравнения должна выглядеть так: cmpl(L1, L2) long *L1, *L2; { if( *L1 == *L2 ) return 0; if( *L1 < *L2 ) return (-1); return 1; } или cmpl(L1, L2) long *L1, *L2; { return( *L1 == *L2 ? 0 : *L1 < *L2 ? -1 : 1 ); } поскольку важна не величина возвращенного значения, а только ее знак. Учтите, что для использования функции сравнения вы должны либо определить функ- цию сравнения до ее использования в qsort(): int cmp(...){ ... } /* реализация */ ... qsort(..... , cmp); либо предварительно объявить имя функции сравнения, чтобы компилятор понимал, что это именно функция: int cmp(); qsort(..... , cmp); ... int cmp(...){ ... } /* реализация */ 1.138. Пусть у нас есть две программы, пользующиеся одной и той же структурой данных W: a.c b.c -------------------------- ------------------------------ #include <fcntl.h> #include <fcntl.h> struct W{ int x,y; }a; struct W{ int x,y; }b; main(){ int fd; main(){ int fd; a.x = 12; a.y = 77; fd = open("f", O_RDONLY); fd = creat("f", 0644); read(fd, &b, sizeof b); write(fd, &a, sizeof a); close(fd); close(fd); printf("%d %d\n", b.x, b.y); } } Что будет, если мы изменим структуру на struct W { long x,y; }; или struct W { char c; int x,y; }; в файле a.c и забудем сделать это в b.c? Будут ли правильно работать эти программы? Из наблюдаемого можно сделать вывод, что если две или несколько программ (или частей одной программы), размещенные в разных файлах, используют общие - типы данных (typedef); - структуры и объединения; - константы (определения #define); - прототипы функций; то их определения лучше выносить в общий include-файл (header-файл), дабы все прог- раммы придерживались одних и тех же общих соглашений. Даже если эти соглашения со А. Богатырев, 1992-95 - 70 - Си в UNIX временем изменятся, то они изменятся во всех файлах синхронно и как бы сами собой. В нашем случае исправлять определение структуры придется только в include-файле, а не выискивать все места, где оно написано, ведь при этом немудрено какое-нибудь место и пропустить! W.h ----------------------- struct W{ long x, y; }; a.c b.c -------------------------- ------------------ #include <fcntl.h> #include <fcntl.h> #include "W.h" #include "W.h" struct W a; struct W b; main(){ ... main(){ ... printf("%ld... Кроме того, вынесение общих фрагментов текста программы (определений структур, конс- тант, и.т.п.) в отдельный файл экономит наши силы и время - вместо того, чтобы наби- вать один и тот же текст много раз в разных файлах, мы теперь пишем в каждом файле единственную строку - директиву #include. Кроме того, экономится и место на диске, ведь программа стала короче! Файлы включения имеют суффикс .h, что означает "header-file" (файл-заголовок). Синхронную перекомпиляцию всех программ в случае изменения include-файла можно задать в файле Makefile - программе для координатора make[*]: all: a b echo Запуск a и b a ; b a: a.c W.h cc a.c -o a b: b.c W.h cc b.c -o b Правила make имеют вид цель: список_целей_от_которых_зависит команда команда описывает что нужно сделать, чтобы изготовить файл цель из файлов список_целей_от_которых_зависит. Команда выполняется только если файл цель еще не существует, либо хоть один из файлов справа от двоеточия является более "молодым" (свежим), чем целевой файл (смотри поле st_mtime и сисвызов stat в главе про UNIX). 1.139. Программа на Си может быть размещена в нескольких файлах. Каждый файл высту- пает в роли "модуля", в котором собраны сходные по назначению функции и переменные. Некоторые переменные и функции можно сделать невидимыми для других модулей. Для этого надо объявить их static: - Объявление переменной внутри функции как static делает переменную статической (т.е. она будет сохранять свое значение при выходе из функции) и ограничивает ее видимость пределами данной функции. - Переменные, описанные вне функций, и так являются статическими (по классу памяти). Однако слово static и в этом случае позволяет управлять видимостью этих переменных - они будут видимы только в пределах данного файла. - Функции, объявленные как static, также видимы только в пределах данного файла. - Аргументы функции и локальные (автоматические) переменные функции и так сущест- вуют только на время вызова данной функции (память для них выделяется в стеке ____________________ [*] Подробное описание make смотри в документации по системе UNIX. А. Богатырев, 1992-95 - 71 - Си в UNIX при входе в функцию и уничтожается при выходе) и видимы только внутри ее тела. Аргументы функции нельзя объявлять static: f(x) static x; { x++; } незаконно. Таким образом все переменные и функции в данном файле делятся на две группы: - Видимые только внутри данного файла (локальные для модуля). Такие имена объяв- ляются с использованием ключевого слова static. В частности есть еще "более локальные" переменные - автоматические локалы функций и их формальные аргументы, которые видимы только в пределах данной функции. Также видимы лишь в пределах одной функции статические локальные переменные, объявленные в теле функции со словом static. - Видимые во всех файлах (глобальные имена). Глобальные имена образуют интерфейс модуля и могут быть использованы в других моду- лях. Локальные имена извне модуля недоступны. Если мы используем в файле-модуле функции и переменные, входящие в интерфейс другого файла-модуля, мы должны объявить их как extern ("внешние"). Для функций опи- сатели extern и int можно опускать: // файл A.c int x, y, z; // глобальные char ss[200]; // глоб. static int v, w; // локальные static char *s, p[20]; // лок. int f(){ ... } // глоб. char *g(){ ... } // глоб. static int h(){ ... } // лок. static char *sf(){ ... } // лок. int fi(){ ... } // глоб. // файл B.c extern int x, y; extern z; // int можно опустить extern char ss[]; // размер можно опустить extern int f(); char *g(); // extern можно опустить extern fi(); // int можно опустить Хорошим тоном является написание комментария - из какого модуля или библиотеки импор- тируется переменная или функция: extern int x, y; /* import from A.c */ char *tgetstr(); /* import from termlib */ Следующая программа собирается из файлов A.c и B.c командой[**] ____________________ [**] Можно задать Makefile вида CFLAGS = -O AB: A.o B.o cc A.o B.o -o AB A.o: A.c cc -c $(CFLAGS) A.c B.o: B.c cc -c $(CFLAGS) B.c и собирать программу просто вызывая команду make. А. Богатырев, 1992-95 - 72 - Си в UNIX cc A.c B.c -o AB Почему компилятор сообщает "x дважды определено"? файл A.c файл B.c ----------------------------------------- int x=12; int x=25; main(){ f(y) int *y; f(&x); { printf("%d\n", x); *y += x; } } Ответ: потому, что в каждом файле описана глобальная переменная x. Надо в одном из них (или в обоих сразу) сделать x локальным именем (исключить его из интерфейса модуля): static int x=...; Почему в следующем примере компилятор сообщает "_f дважды определено"? файл A.c файл B.c ---------------------------------------------------- int x; extern int x; main(){ f(5); g(77); } g(n){ f(x+n); } f(n) { x=n; } f(m){ printf("%d\n", m); } Ответ: надо сделать в файле B.c функцию f локальной: static f(m)... Хоть в одном файле должна быть определена функция main, вызываемая системой при запуске программы. Если такой функции нигде нет - компилятор выдает сообщение "_main неопределено". Функция main должна быть определена один раз! В файле она может нахо- диться в любом месте - не требуется, чтобы она была самой первой (или последней) функцией файла[**]. 1.140. В чем ошибка? файл A.c файл B.c ---------------------------------------------------- extern int x; extern int x; main(){ x=2; f(){ f(); printf("%d\n", x); } } Ответ: переменная x в обоих файлах объявлена как extern, в результате память для нее нигде не выделена, т.е. x не определена ни в одном файле. Уберите одно из слов extern! 1.141. В чем ошибка? файл A.c файл B.c ---------------------------------------------------- int x; extern double x; ... ... Типы переменных не совпадают. Большинство компиляторов не ловит такую ошибку, т.к. каждый файл компилируется отдельно, независимо от остальных, а при "склейке" файлов в ____________________ [**] Если вы пользуетесь "новым" стилем объявления функций, но не используете прото- типы, то следует определять каждую функцию до первого места ее использования, чтобы компилятору в точке вызова был известен ее заголовок. Это приведет к тому, что main() окажется последней функцией в файле - ее не вызывает никто, зато она вызывает кого-то еще. А. Богатырев, 1992-95 - 73 - Си в UNIX общую выполняемую программу компоновщик знает лишь имена переменных и функций, но не их типы и прототипы. В результате программа нормально скомпилируется и соберется, но результат ее выполнения будет непредсказуем! Поэтому объявления extern тоже полезно выносить в include-файлы: файл proto.h ------------------ extern int x; файл A.c файл B.c ------------------ ------------------ #include "proto.h" #include "proto.h" int x; ... то, что переменная x в A.c оказывается описанной и как extern - вполне допустимо, т.к. в момент настоящего объявления этой переменной это слово начнет просто игнориро- ваться (лишь бы типы в объявлении с extern и без него совпадали - иначе ошибка!). 1.142. Что печатает программа и почему? int a = 1; /* пример Bjarne Stroustrup-а */ void f(){ int b = 1; static int c = 1; printf("a=%d b=%d c=%d\n", a++, b++, c++); } void main(){ while(a < 4) f(); } Ответ: a=1 b=1 c=1 a=2 b=1 c=2 a=3 b=1 c=3 1.143. Автоматическая переменная видима только внутри блока, в котором она описана. Что напечатает программа? /* файл A.c */ int x=666; /*глоб.*/ main(){ f(3); printf(" ::x = %d\n", x); g(2); g(5); printf(" ::x = %d\n", x); } g(n){ static int x=17; /*видима только в g*/ printf("g::x = %2d g::n = %d\n", x++, n); if(n) g(n-1); else x = 0; } /* файл B.c */ extern x; /*глобал*/ f(n){ /*локал функции*/ x++; /*глобал*/ { int x; /*локал блока*/ x = n+1; /*локал*/ А. Богатырев, 1992-95 - 74 - Си в UNIX n = 2*x; /*локал*/ } x = n-1; /*глобал*/ } 1.144. Функция, которая - не содержит внутри себя статических переменных, хранящих состояние процесса обработки данных (функция без "памяти"); - получает значения параметров только через свои аргументы (но не через глобальные статические переменные); - возвращает значения только через аргументы, либо как значение функции (через return); называется реентерабельной (повторно входимой) или чистой (pure). Такая функция может параллельно (или псевдопараллельно) использоваться несколькими "потоками" обра- ботки информации в нашей программе, без какого-либо непредвиденного влияния этих "потоков обработки" друг на друга. Первый пункт требований позволяет функции не зависеть ни от какого конкретного процесса обработки данных, т.к. она не "помнит" обработанных ею ранее данных и не строит свое поведение в зависимости от них. Вторые два пункта - это требование, чтобы все без исключения пути передачи данных в функцию и из нее (интерфейс функции) были перечислены в ее заголовке. Это лишает функцию "побочных эффектов", не предусмотренных программистом при ее вызове (программист обычно смотрит только на заголовок функции, и не выискивает "тайные" связи функции с программой через глобальные переменные, если только это специально не оговорено). Вот пример не реентерабельной функции: FILE *fp; ... /* глобальный аргумент */ char delayedInput () { static char prevchar; /* память */ char c; c = prevchar; prevchar = getc (fp); return c; } А вот ее реентерабельный эквивалент: char delayedInput (char *prevchar, FILE *fp) { char c; c = *prevchar; *prevchar = getc (fp); return c; } /* вызов: */ FILE *fp1, *fp2; char prev1, prev2, c1, c2; ... x1 = delayedInput (&prev1, fp1); x2 = delayedInput (&prev2, fp2); ... Как видим, все "запоминающие" переменные (т.е. prevchar) вынесены из самой функции и подаются в нее в виде аргумента. Реентерабельные функции независимы от остальной части программы (их можно скопи- ровать в другой программный проект без изменений), более понятны (поскольку все зат- рагиваемые ими внешние переменные перечислены как аргументы, не надо выискивать в теле функции глобальных переменных, передающих значение в/из функции, т.е. эта функ- ция не имеет побочных влияний), более надежны (хотя бы потому, что компилятор в сос- тоянии проверить прототип такой функции и предупредить вас, если вы забыли задать какой-то аргумент; если же аргументы передаются через глобальные переменные - вы можете забыть проинициализировать какую-то из них). Старайтесь делать функции реен- терабельными! А. Богатырев, 1992-95 - 75 - Си в UNIX Вот еще один пример на эту тему. Не-реентерабельный вариант: int x, y, result; int f (){ static int z = 4; y = x + z; z = y - 1; return x/2; } Вызов: x=13; result = f(); printf("%d\n", y); А вот реентерабельный эквивалент: int y, result, zmem = 4; int f (/*IN*/ int x, /*OUT*/ int *ay, /*INOUT*/ int *az){ *az = (*ay = x + *az) - 1; return x/2; } Вызов: result = f(13, &y, &zmem); printf("%d\n", y); 1.145. То, что формат заголовка функции должен быть известен компилятору до момента ее использования, побуждает нас помещать определение функции до точки ее вызова. Так, если main вызывает f, а f вызывает g, то в файле функции расположатся в порядке g() { } f() { ... g(); ... } main(){ ... f(); ... } Программа обычно разрабатывается "сверху-вниз" - от main к деталям. Си же вынуждает нас размещать функции в программе в обратном порядке, и в итоге программа читается снизу-вверх - от деталей к main, и читать ее следует от конца файла к началу! Так мы вынуждены писать, чтобы удовлетворить Си-компилятор: #include <stdio.h> unsigned long g(unsigned char *s){ const int BITS = (sizeof(long) * 8); unsigned long sum = 0; for(;*s; s++){ sum ^= *s; /* cyclic rotate left */ sum = (sum<<1)|(sum>>(BITS-1)); } return sum; } void f(char *s){ printf("%s %lu\n", s, g((unsigned char *)s)); } int main(int ac, char *av[]){ int i; for(i=1; i < ac; i++) f(av[i]); return 0; } А вот как мы разрабатываем программу: А. Богатырев, 1992-95 - 76 - Си в UNIX #include <stdio.h> int main(int ac, char *av[]){ int i; for(i=1; i < ac; i++) f(av[i]); return 0; } void f(char *s){ printf("%s %lu\n", s, g((unsigned char *)s)); } unsigned long g(unsigned char *s){ const int BITS = (sizeof(long) * 8); unsigned long sum = 0; for(;*s; s++){ sum ^= *s; /* cyclic rotate left */ sum = (sum<<1)|(sum>>(BITS-1)); } return sum; } и вот какую ругань производит Си-компилятор в ответ на эту программу: "0000.c", line 10: identifier redeclared: f current : function(pointer to char) returning void previous: function() returning int : "0000.c", line 7 "0000.c", line 13: identifier redeclared: g current : function(pointer to uchar) returning ulong previous: function() returning int : "0000.c", line 11 Решением проблемы является - задать прототипы (объявления заголовков) всех функций в начале файла (или даже вынести их в header-файл). #include <stdio.h> int main(int ac, char *av[]); void f(char *s); unsigned long g(unsigned char *s); ... Тогда функции будет можно располагать в тексте в любом порядке. 1.146. Рассмотрим процесс сборки программы из нескольких файлов на языке Си. Пусть мы имеем файлы file1.c, file2.c, file3.c (один из них должен содержать среди других функций функцию main). Ключ компилятора -o заставляет создавать выполняемую прог- рамму с именем, указанным после этого ключа. Если этот ключ не задан - будет создан выполняемый файл a.out cc file1.c file2.c file3.c -o file Мы получили выполняемую программу file. Это эквивалентно 4-м командам: cc -c file1.c получится file1.o cc -c file2.c file2.o cc -c file3.c file3.o cc file1.o file2.o file3.o -o file Ключ -c заставляет компилятор превратить файл на языке Си в "объектный" файл А. Богатырев, 1992-95 - 77 - Си в UNIX (содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда "склеивает" объектные файлы в единое целое - выполняемую программу[*]. При этом, если какие-то функции, используемые в нашей программе, не были определены (т.е. спрограм- мированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных функций. Если же каких-то функций не окажется и там - будет выдано сообщение об ошибке. Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать только новые Си-файлы: cc -c file4.c cc file1.o file2.o file3.o file4.o -o file или (что то же самое, но cc сам разберется, что надо делать) cc file1.o file2.o file3.o file4.c -o file Существующие у нас объектные файлы с отлаженными функциями удобно собрать в библио- теку - файл специальной структуры, содержащий все указанные файлы (все файлы склеены в один длинный файл, разделяясь специальными заголовками, см. include-файл <ar.h>): ar r file.a file1.o file2.o file3.o Будет создана библиотека file.a, содержащая перечисленные .o файлы (имена библиотек в UNIX имеют суффикс .a - от слова archive, архив). После этого можно использовать библиотеку: cc file4.o file5.o file.a -o file Механизм таков: если в файлах file4.o и file5.o не определена какая-то функция (функ- ции), то просматривается библиотека, и в список файлов для "склейки" добавляется файл из библиотеки, содержащий определение этой функции (из библиотеки он не удаляется!). Тонкость: из библиотеки берутся не ВСЕ файлы, а лишь те, которые содержат определения недостающих функций[**]. Если, в свою очередь, файлы, извлекаемые из библиотеки, будут содержать неопределенные функции - библиотека (библиотеки) будут просмотрены еще раз и.т.д. (на самом деле достаточно максимум двух проходов, так как при первом просмотре библиотеки можно составить ее каталог: где какие функции в ней содержатся и кого вызывают). Можно указывать и несколько библиотек: cc file6.c file7.o \ file.a mylib.a /lib/libLIBR1.a -o file Таким образом, в команде cc можно смешивать имена файлов: исходных текстов на Си .c, объектных файлов .o и файлов-библиотек .a. Просмотр библиотек, находящихся в стандартных местах (каталогах /lib и /usr/lib), можно включить и еще одним способом: указав ключ -l. Если библиотека называется /lib/libLIBR1.a или /usr/lib/libLIBR2.a то подключение делается ключами -lLIBR1 и -lLIBR2 ____________________ [*] На самом деле, для "склейки" объектных файлов в выполняемую программу, команда /bin/cc вызывает программу /bin/ld - link editor, linker, редактор связей, компонов- щик. [**] Поэтому библиотека может быть очень большой, а к нашей программе "приклеится" лишь небольшое число файлов из нее. В связи с этим стремятся делать файлы, помещаемые в библиотеку, как можно меньше: 1 функция; либо "пачка" функций, вызывающих друг друга. А. Богатырев, 1992-95 - 78 - Си в UNIX соответственно. cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o файлов. Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее при сборке, если какие-то функции, использованные вами, не были вами определены), то есть просматривается всегда (именно эта библиотека содержит коды, например, для printf, strcat, read). Многие прикладные пакеты функций поставляются именно в виде библиотек. Такие библиотеки состоят из ряда .o файлов, содержащих объектные коды для различных функций (т.е. функции в скомпилированном виде). Исходные тексты от большинства библиотек не поставляются (так как являются коммерческой тайной). Тем не менее, вы можете исполь- зовать эти функции, так как вам предоставляются разработчиком: - описание (документация). - include-файлы, содержащие форматы данных используемые функциями библиотеки (именно эти файлы включались #include в исходные тексты библ. функций. Теперь уже вы должны включать их в свою программу). Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы часто используете printf(), но задумываетесь ли вы о ее внутреннем устройстве?). Некоторые библиотечные функции могут быть вообще написаны не на Си, а на ассемблере или другом языке программирования[*][*]. Еще раз обращаю ваше внимание, что библиотека содержит не исходные тексты функций, а скомпилированные коды (и include-файлы содер- жат (как правило) не тексты функций, а только описание форматов данных)! Библиотека может также содержать статические данные, вроде массивов строк-сообщений об ошибках. Посмотреть список файлов, содержащихся в библиотеке, можно командой ar tv имяФайлаБиблиотеки а список имен функций - командой nm имяФайлаБиблиотеки Извлечь файл (файлы) из архива (скопировать его в текущий каталог), либо удалить его из библиотеки можно командами ar x имяФайлаБиблиотеки имяФайла1 ... ar d имяФайлаБиблиотеки имяФайла1 ... где ... означает список имен файлов. "Лицом" библиотек служат прилагаемые к ним include-файлы. Системные include- файлы, содержащие общие форматы данных для стандартных библиотечных функций, хранятся в каталоге /usr/include и подключаются так: для /usr/include/файл.h надо #include <файл.h> для /usr/include/sys/файл.h #include <sys/файл.h> ____________________ [*][*] Обратите внимание, что библиотечные функции не являются частью ЯЗЫКА Си как та- кового. То, что в других языках (PL/1, Algol-68, Pascal) является частью языка (встроено в язык)- в Си вынесено на уровень библиотек. Например, в Си нет оператора вывода; функция вывода printf - это библиотечная функция (хотя и общепринятая). Та- ким образом мощь языка Си состоит именно в том, что он позволяет использовать функ- ции, написанные другими программистами и даже на других языках, т.е. является функци- онально расширяемым. А. Богатырев, 1992-95 - 79 - Си в UNIX (sys - это каталог, где описаны форматы данных, используемых ядром ОС и системными вызовами). Ваши собственные include-файлы (посмотрите в предыдущий раздел!) ищутся в текущем каталоге и включаются при помощи #include "файл.h" /* ./файл.h */ #include "../h/файл.h" /* ../h/файл.h */ #include "/usr/my/файл.h" /* /usr/my/файл.h */ Непременно изучите содержимое стандартных include-файлов в своей системе! В качестве резюме - схема, поясняющая "превращения" Си-программы из текста на языке программирования в выполняемый код: все файлы .c могут использовать общие include-файлы; их подстановку в текст, а также обработку #define произведет препро- цессор cpp file1.c file2.c file3.c | | | "препроцессор" | cpp | cpp | cpp | | | "компиляция" | cc -c | cc -c | cc -c | | | file1.o file2.o file3.o | | | -----------*----------- | Неявно добавятся: ld |<----- /lib/libc.a (библ. станд. функций) | /lib/crt0.o (стартер) "связывание" | "компоновка" |<----- Явно указанные библиотеки: | -lm /lib/libm.a V a.out 1.147. Напоследок - простой, но жизненно важный совет. Если вы пишете программу, которую вставите в систему для частого использования, поместите в исходный текст этой программы идентификационную строку наподобие static char id[] = "This is /usr/abs/mybin/xprogram"; Тогда в случае аварии в файловой системе, если вдруг ваш файл "потеряется" (то есть у него пропадет имя - например из-за порчи каталога), то он будет найден программой проверки файловой системы - fsck - и помещен в каталог /lost+found под специальным кодовым именем, ничего общего не имеющим со старым. Чтобы понять, что это был за файл и во что его следует переименовать (чтобы восстановить правильное имя), мы при- меним команду strings имя_файла Эта команда покажет все длинные строки из печатных символов, содержащиеся в данном файле, в частности и нашу строку id[]. Увидев ее, мы сразу поймем, что файл надо переименовать так: mv имя_файла /usr/abs/mybin/xprogram 1.148. Где размещать include-файлы и как программа узнает, где же они лежат? Стан- дартные системные include-файлы размещены в /usr/include и подкаталогах. Если мы пишем некую свою программу (проект) и используем директивы #include "имяФайла.h" А. Богатырев, 1992-95 - 80 - Си в UNIX то обычно include-файлы имяФайла.h лежат в текущем каталоге (там же, где и файлы с программой на Си). Однако мы можем помещать ВСЕ наши include-файлы в одно место (скажем, известное группе программистов, работающих над одним и тем же проектом). Хорошее место для всех ваших личных include-файлов - каталог (вами созданный) $HOME/include где $HOME - ваш домашний каталог. Хорошее место для общих include-файлов - каталог /usr/local/include Как сказать компилятору, что #include "" файлы надо брать из определенного места, а не из текущего каталога? Это делает ключ компилятора cc -Iимя_каталога ... Например: /* Файл x.c */ #include "x.h" int main(int ac, char *av[]){ .... return 0; } И файл x.h находится в каталоге /home/abs/include/x.h (/home/abs - мой домашний ката- лог). Запуск программы на компиляцию выглядит так: cc -I/home/abs/include -O x.c -o x или cc -I$HOME/include -O x.c -o x Или, если моя программа x.c находится в /home/abs/progs cc -I../include -O x.c -o x Ключ -O задает вызов компилятора с оптимизацией. Ключ -I оказывает влияние и на #include <&lt;>&gt; директивы тоже. Для ОС Solaris на машинах Sun программы для оконной системы X Window System содержат строки вроде #include <&lt;X11/Xlib.h>&gt; #include <&lt;X11/Xutil.h>&gt; На Sun эти файлы находятся не в /usr/include/X11, а в /usr/openwin/include/X11. Поэ- тому запуск на компиляцию оконных программ на Sun выглядит так: cc -O -I/usr/openwin/include xprogram.c \ -o xprogram -L/usr/openwin/lib -lX11 где -lX11 задает подключение графической оконной библиотеки Xlib. Если include-файлы находятся во многих каталогах, то можно задать поиск в нес- кольких каталогах, к примеру: cc -I/usr/openwin/include -I/usr/local/include -I$HOME/include ...