Новые книги

Самоучитель UML

Первое издание.

В книге рассматриваются основы UML – унифицированного языка моделирования для описания, визуализации и документирования объектно-ориентированных систем и бизнес-процессов в ходе разработки программных приложений. Подробно описываются базовые понятия UML, необходимые для построения объектно-ориентированной модели системы с использованием графической нотации. Изложение сопровождается примерами разработки отдельных диаграмм, которые необходимы для представления информационной модели системы. Цель книги – помочь программистам освоить новую методологию разработки корпоративных программных приложений для последующего применения полученных знаний с использованием соответствующих CASE-инструментов.
В книге даны ответы на часто задаваемые вопросы начинающих предпринимателей. Материалы книги позволят вам пошагово организовать дистанционную работу. Здесь рассмотрены различные виды бизнеса – как с финансовыми вложениями, так и без таковых. Дистанционный бизнес предполагает целый ряд особенностей: организация работы; ведение бухгалтерского и налогового учета; особенности заключения договоров, а также реклама этого вида деятельности. Простые рекомендации помогут не только организовать дистанционный бизнес для получения дополнительной прибыли, но и сделать ваше предприятие высокорентабельным.

Книга адресована широкому кругу читателей.

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

1.100.

Поговорим более подробно про область видимости имен.
    int x = 12;
    f(x){  int y = x*x;
           if(x) f(x - 1);
    }
    main(){ int x=173, z=21; f(2); }
Локальные переменные и аргументы функции отводятся в стеке при вызове функции и уничтожаются при выходе из нее:
           -+               +- вершина стека
            |локал    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. Аргументы функции служат копиями фактических параметров (т.е. являются локальными переменными функции, проинициализированными значениями фактических параметров), поэтому их изменение не приводит к изменению фактического параметра. Чтобы изменять фактический параметр, надо передавать его адрес!

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);
(указатели указывают неизвестно на что).

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

Это полезно потому, что иногда трудно понять - зачем параметр описан как указатель. То ли по нему выдается из функции информация, то ли это просто указатель на данные (массив), передаваемые в функцию. В первом случае указуемые данные будут изменены, а во втором - нет. В первом случае указатель должен указывать на зарезервированную нами область памяти, в которой будет размещен результат. Пример на эту тему есть в главе "Текстовая обработка" (функция 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));
    }

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

  • в 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 (т.к. приоритет у присваивания выше, чем у запятой). Обратите внимание, что выражение без присваивания (которое может вообще не иметь эффекта или иметь только побочный эффект) вполне законно:

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

* Для трансляции программы, использующей стандартные математические функции sin, cos, exp, log, sqrt, и.т.п. следует задавать ключ компилятора -lm

	cc file.c -o file -lm

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

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

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