Программирование на Си++ в Unix

Google
  Главная   Новости   Статьи   Книги   Ссылки  

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

Типы переменных не совпадают. Большинство компиляторов не ловит такую ошибку, т.к. каждый файл компилируется отдельно, независимо от остальных, а при "склейке" файлов в общую выполняемую программу компоновщик знает лишь имена переменных и функций, но не их типы и прототипы. В результате программа нормально скомпилируется и соберется, но результат ее выполнения будет непредсказуем! Поэтому объявления 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;  /*локал*/
        n = 2*x;  /*локал*/
      }
      x = n-1;    /*глобал*/
    }

1.144.

Функция, которая

называется реентерабельной (повторно входимой) или чистой (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) вынесены из самой функции и подаются в нее в виде аргумента.

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

Вот еще один пример на эту тему. Не-реентерабельный вариант:

    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;
    }
А вот как мы разрабатываем программу:
    #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 заставляет компилятор превратить файл на языке Си в "объектный" файл (содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда "склеивает" объектные файлы в единое целое - выполняемую программу*. При этом, если какие-то функции, используемые в нашей программе, не были определены (т.е. спрограммированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных функций. Если же каких-то функций не окажется и там - будет выдано сообщение об ошибке.

Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать только новые Си-файлы:

    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
соответственно.
    cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file
Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o файлов.

Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее при сборке, если какие-то функции, использованные вами, не были вами определены), то есть просматривается всегда (именно эта библиотека содержит коды, например, для printf, strcat, read).

Многие прикладные пакеты функций поставляются именно в виде библиотек. Такие библиотеки состоят из ряда .o файлов, содержащих объектные коды для различных функций (т.е. функции в скомпилированном виде). Исходные тексты от большинства библиотек не поставляются (так как являются коммерческой тайной). Тем не менее, вы можете использовать эти функции, так как вам предоставляются разработчиком:

Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы часто используете 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>

(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"

то обычно 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 <> директивы тоже. Для ОС Solaris на машинах Sun программы для оконной системы X Window System содержат строки вроде

    #include <X11/Xlib.h>
    #include <X11/Xutil.h>

На 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 ...

* На самом деле, для "склейки" объектных файлов в выполняемую программу, команда /bin/cc вызывает программу /bin/ld - link editor, linker, редактор связей, компоновщик.

** Поэтому библиотека может быть очень большой, а к нашей программе "приклеится" лишь небольшое число файлов из нее. В связи с этим стремятся делать файлы, помещаемые в библиотеку, как можно меньше: 1 функция; либо "пачка" функций, вызывающих друг друга.

*** Обратите внимание, что библиотечные функции не являются частью ЯЗЫКА Си как такового. То, что в других языках (PL/1, Algol-68, Pascal) является частью языка (встроено в язык)- в Си вынесено на уровень библиотек. Например, в Си нет оператора вывода; функция вывода printf - это библиотечная функция (хотя и общепринятая). Таким образом мощь языка Си состоит именно в том, что он позволяет использовать функции, написанные другими программистами и даже на других языках, т.е. является функционально расширяемым.

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

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

Используются технологии uCoz