Date Редакция Категория comp Теги C / K&R

Упражнение 1.1. Выполните программу, печатающую “Hello, world”, в вашей системе. Поэкспериментируйте, удаляя некоторые части программы, и посмотрите, какие сообщения об ошибках вы получите.

Оригинальная программа (hello.c) выглядит так:

#include <stdio.h>

main()
{
    printf("Hello, world\n");
}

hello1.png

Запускается она, и все последующие программы так:

gcc -o hello hello.c

hello0.png

Запускаем, получая в результате исполняемый файл hello. В свою очередь, запускаем hello:

./hello

hello2.png

Но вот в Geany, а я использую в работе Geany — компактную среду разработки, основанную на библиотеке GTK2, так вот — в Geany видны дополнительные подробности:

gcc -Wall -c "hello.c" (в каталоге: /home/dima/work/kr)
hello.c:3:1: предупреждение: по умолчанию возвращаемый тип функции - «int» [-Wreturn-type]
hello.c: В функции «main»:
hello.c:6:1: предупреждение: control reaches end of non-void function [-Wreturn-type]
Сборка прошла успешно.

Компилятор жалуется на то, что по умолчанию тип функции main() — int. Исправляем:

#include <stdio.h>

int main() 
{
    printf("Hello, world!\n");
    return 0;
}

Вот теперь-то "душечка gcc" доволен:

gcc -Wall -c "hello.c" (в каталоге: /home/dima/work/kr)
Сборка прошла успешно.

Насчет типа — это gcc серьезно. Убрать return и отделаться void'ом

#include <stdio.h>

void main() 
{
    printf("Hello, world!\n");
}

без ворчания не получится:

gcc -Wall -o "hello" "hello.c" (в каталоге: /home/dima/work/kr)
hello.c:3:6: предупреждение: return type of «main» is not «int» [-Wmain]
Сборка прошла успешно.

Так что с этого момента не будем забывать, что main() — это int и писать return, что бы там не говорили корифеи КиР.

Думаю, для первого раза поиграли достаточно, идем дальше. Добавлю только, что компиляция/сборка/выполнение проекта в Geany выполняется в меню Сборка (сборка — F9, выполнение — F5).

Упражнение 1.2. Выясните, что произойдет, если в строковую константу аргумента printf вставить \c, где c - символ, не входящий в представленный выше список.

Список, это вот что:

В Си комбинация \n внутри строки символов обозначает символ новой строки и при печати вызывает переход к левому краю следующей строки. <...> Заметим, что \n обозначает только один символ. Такие особые комбинации символов, начинающиеся с обратной наклонной черты, как \n, и называемые эскейп-последовательностями, широко применяются для обозначения трудно представимых или невидимых символов. Среди прочих в Си имеются символы \t, \b, \", , обозначающие соответственно табуляцию, возврат на один символ назад (“забой” последнего символа), двойную кавычку, саму наклонную черту.

По хорошему, надо было бы попытаться вывести все символы, отличные от указанных, и посмотреть, что получится. Но мы пока не знаем, как сделать это достаточно лениво (не перебирать же их, в самом деле). Поэтому выведем один — на пробу, как раз этот самый \c.

Подставим его вместо \n в hello.c. Вот что покажет Geany:

gcc -Wall -o "hello" "hello.c" (в каталоге: /home/dima/work/kr)
hello.c: В функции «main»:
hello.c:5:8: предупреждение: неизвестная экранированная последовательность «c» [по умолчанию включена]
Сборка прошла успешно.

Результат:

hello3.png

Т. е. ничего фатального, но экранирование не сработало.

Упражнение 1.3. Усовершенствуйте программу преобразования температур таким образом, чтобы над таблицей она печатала заголовок.

Программа, над которой предстоит поработать:

  #include <stdio.h>

  /* печать таблицы температур по Фаренгейту и Цельсию для
     fahr = 0, 20, . .., 300; вариант с плавающей точкой   */
  main()
  {
      float fahr, celsius;
      int lower, upper, step;

      lower = 0;   /* нижняя граница таблицы температур */ 
      upper = 300; /* верхняя граница */ 
      step = 20;   /* шаг */

      fahr = lower;
      while (fahr <= upper) { 
          celsius = (5.0/9.0) * (fahr-32.0);
          printf("%3.0f %6.1f\n", fahr, celsius);
          fahr = fahr + step;
      }
  }

Добавим, как и раньше, в нее int и return и проверим в работе:

temper0.png

Если нам нужен только заголовок, то перед циклом вставим строчку

printf("fahr celsius\n");

Но если мы хотим построить полноценную таблицы, то сделаем так

  #include <stdio.h>

  /* печать таблицы температур по Фаренгейту и Цельсию для
     fahr = 0, 20, . .., 300; вариант с плавающей точкой   */
  int main()
  {
      float fahr, celsius;
      int lower, upper, step;

      lower = 0;   /* нижняя граница таблицы температур */ 
      upper = 300; /* верхняя граница */ 
      step = 20;   /* шаг */

      printf("-----------------\n");
      printf("| fahr | celsius|\n");
      printf("-----------------\n");
      fahr = lower;
      while (fahr <= upper) { 
          celsius = (5.0/9.0) * (fahr-32.0);
          printf("| %3.0f  | %6.1f |\n", fahr, celsius);
          fahr = fahr + step;
      }
      printf("-----------------\n");
      return 0;
  }

Конечно, перед циклом можно было обойтись одним printf, но так наглядней.

temper1.png

А теперь решим обратную задачу.

Упражнение 1.4. Напишите программу, которая будет печатать таблицу соответствия температур по Цельсию температурам по Фаренгейту.

#include <stdio.h>

/* печать таблицы температур по Фаренгейту и Цельсию для
   celsius = -40, -20, . .., 140                          */
int main()
{
    float fahr, celsius;
    int lower, upper, step;

    lower =-40;   /* нижняя граница таблицы температур */
    upper = 140;  /* верхняя граница */
    step  = 20;   /* шаг */

    printf("-----------------\n");
    printf("| celsius | fahr |\n");
    printf("-----------------\n");
    celsius = lower;
    while (celsius <= upper)
    {
        fahr = (9.0/5.0) * celsius + 32.0;
        printf("| %3.0f  | %6.1f |\n", celsius, fahr);
        celsius = celsius + step;
    }
    printf("-----------------\n");
    return 0;
}

Упражнение 1.5. Измените программу преобразования температур так, чтобы она печатала таблицу в обратном порядке, т. е. от 300 до 0.

Поскольку мы уже познакомились (если, конечно, вы читаете книгу) с циклом for, то им и воспользуемся

  #include <stdio.h>

  #define LOWER 0   /* нижняя граница таблицы */
  #define UPPER 300 /* верхняя граница */
  #define STEP 20   /* размер шага */

  /* печать таблицы температур по Фаренгейту и Цельсию */
  int main()
  {
      int fahr;
      for (fahr = UPPER; fahr >= LOWER; fahr = fahr - STEP)
          printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
      return 0;    
  }

temper2.png

Упражнение 1.6. Убедитесь в том, что выражение getchar() != EOF получает значение 0 или 1.

Проще всего из примера

#include <stdio.h> 

int main()
{    
    int c;

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

убрать скобки, окружающие c = getchar(). В результате, на каждом шаге будут выводиться единицы, до тех пор, пока файл не закончится (Ctrl+D в Linux, Ctrl+Z — в Windows).

Правда, в этом случае мы получим предупреждение gcc: %red%присваивание, используемое как логическое выражение [-Wparentheses]%%. Оно предупреждает об отсутствии скобок в некоторых ситуациях, например, когда как у нас присваивание употребляется в контексте, где ожидается логическое значение, или если используется сочетание операторов, относительно приоритетов которых программисты довольно часто имеют неправильные представления, что приводит к ошибкам.

Хотя мы и не допустили ошибку, к чему нам предупреждения? Вот вариант, проверяющий ввод одного символа

#include <stdio.h> 

int main()
{    
    int c;

    c = getchar() != EOF;
    if (c == 1)
        printf("1");
    else
        printf("0");

    return 0;    
}

Нуль можно получить, нажав Ctrl+D (в Linux; Ctrl+Z — в Windows).

Упражнение 1.7. Напишите программу, печатающую значение EOF.

Используем printf(), помня о том, что EOF — не char

  #include <stdio.h>

  int main()
  {
      printf ("The value of EOF is %d\n\n" , EOF);
      return 0;
  }

а можно напрямую, с помощью putchar()

#include <stdio.h> 

int main()
{    
    putchar(EOF);
    return 0;    
}

Упражнение 1.8. Напишите программу для подсчета пробелов, табуляций и новых строк.

Пример из книжки посвящен подсчету строк:

  #include <stdio.h>
  /* подсчет строк входного потока */
  int main()
  {
      int c, nl;
      nl = 0;
      while ((c = getchar()) != EOF)
          if (c == '\n')
              ++nl;
      printf("%d\n", nl);
      return 0;
  }

А сейчас нам нужно подсчитать не только строки, но и пробелы и табуляции. Повторим идею трижды, воспользовавшись if-ами для разбора символов по "кучкам".

  #include <stdio.h>

  /* подсчет строк, пробелов и табуляций входного потока */
  int main()
  {
      int c, nl, ns, nt;
      nl = 0;
      ns = 0;
      nt = 0;
      while ((c = getchar()) != EOF)
          if (c == '\n')
              ++nl;
          else if (c == ' ')
              ++ns;
          else if (c == '\t')
              ++nt;
      printf("lines=%d, spaces=%d, tabs=%d\n", nl, ns, nt);
      return 0;
  }

lstcount0.png

Упражнение 1.9. Напишите программу, копирующую символы ввода в выходной поток и заменяющую стоящие подряд пробелы на один пробел.

Введем переменную sflag — флаг, который поднимается (принимает значение 1), если символ является пробелом, тогда следующий пробельный символ с sflag = 1 пропускается

#include <stdio.h>

int main()
{
    int c, sflag;

    sflag = 0;
    while ((c = getchar()) != EOF){
        if (c != ' '){
            putchar(c);
        sflag = 0;
    }
    else if (sflag == 0){
        putchar(c);
        sflag = 1;
    }
    }
    return 0;
}

Первая строка — ввод, вторая — вывод.

spaces0.png

Упражнение 1.10. Напишите программу, копирующую вводимые символы в выходной поток с заменой символа табуляции на \t, символа забоя на \b и каждой обратной наклонной черты на . Это сделает видимыми все символы табуляции и забоя.

Фильтруем ввод с помощью условных операторов. Многосимвольный вывод вместо putchar выполняем printf'ом

/* kr_1_10 */
#include <stdio.h>

int main()
{
    int c;

    while ((c = getchar()) != EOF)
        if (c == '')
            printf("");
        else if (c == '\t')
            printf("t");
        else if (c == '\b')
            printf("b");
        else
            putchar(c);

    return 0;
}

chtabb0.png

Заметим, что символ '\b', как правило, нельзя набрать в терминале (Спасибо пользователю Bill Gates, что обратил на это внимание!) — он там "работает", а не отображается. Поэтому получить его можно из файла или из другого процесса (если не углубляться в настройки терминала).

Рассмотрим второй вариант. Создадим файл prepare_backspace.c, печатающий текст с '\b':

#include <stdio.h>

int main()
{
    printf("Some\b text");
    return 0;
}

Транслируем его и запустим, перенаправив результат работы (стандартный вывод) в стандартный ввод рассмотренной выше программы-фильтра:

prepare_backspace | kr_1_10         # Linux
prepare_backspace.exe | kr_1_10.exe # Windows

Тогда данные, подготовленные prepare_backspace, попадут непосредственно на вход kr_1_10, которая и отфильтрует то, что нужно. В нашем случае, получим

 Some\b text

Упражнение 1.11. Как протестировать программу подсчета слов? Какой ввод вероятнее всего обнаружит ошибки, если они были допущены?

Вот пациент:

  #include <stdio.h>

  #define IN 1    /* внутри слова */
  #define OUT 0   /* вне слова */

  /* подсчет строк, слов и символов */
  int main()
  {
      int c, nl, nw, nc, state;
      state = OUT;
      nl = nw = nc = 0;
      while ((c = getchar()) != EOF) {
          ++nc;
          if (c == '\n')
              ++nl;
          if (c == ' ' || c == '\n' || c == '\t')
              state = OUT;
          else if (state == OUT) {
              state = IN;
              ++nw;
          }
      }
      printf("%d %d %d\n", nl, nw, nc);
      return 0;
  }

Нам необходимо проверить, что у него может быть не так, т. е. попытаться тестировать его.

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

Например:

  • ввод состоит из одного единственного сверхдлинного слова без \n;
  • ввод состоит из пробелов и табуляций без \n
  • ввод не содержит не единого слова;
  • ввод состоит из iinf строк
  • ввод состоит из iinf слов в единственной строке
  • между словами находится iinf пробелов/табуляций ...

iinf — большое число, достаточное, чтобы вызвать переполнение типа int.

Программа, для оценки положительной границы диапазона чисел типа int:

  #include <stdio.h>

  int main()
  {
      int i;

      i = 1;
      while (i+i > i) {
          i = 2*i;
          printf("%d\n", i);
      }
      return 0;
  }

У меня это дает следующий результат:

iinf0.png

Упражнение 1.12. Напишите программу, которая печатает содержимое своего ввода, помещая по одному слову на каждой строке.

Если символ находится в пределах слова (state == IN), то выводим его. Если же выходит за пределы слова — выводим символ новой строки.

#include <stdio.h>

#define IN 1    /* внутри слова */
#define OUT 0   /* вне слова */

/* вывод слов в новой строке */
int main()
{
    int c, state;
    state = OUT;

    while ((c = getchar()) != EOF)
    {
        /* Если это не пробельный символ, то мы внутри слова */
        if (c != ' ' && c != '\n' && c != '\t')
        {
            state = IN;
            putchar(c);
        }
        /* А если это один из пробелов, то мы вне слова */
        else if (state == IN)
        {
            state = OUT;
            putchar('\n');
        }
    }
    return 0;
}

wput0.png

Здесь первая строка, как обычно, ввод, остальные — вывод.

Чтобы стало понятнее, зачем здесь флаг state, поставьте в тестовом вводе подряд два или более пробельных символа.

Упражнение 1.13. Напишите программу, печатающую гистограммы длин вводимых слов. Гистограмму легко рисовать горизонтальными полосами. Рисование вертикальными полосами — более трудная задача.

Воспользуемся программой из упражнения 1.11. Разобьем задачу на две части: сперва разработаем программу, выводящую данные для гистограммы (частоты появления той или иной длины слова во вводе), а затем добавим в нее фрагмент, строящий гистограмму на основе этих данных. Кстати, вполне можно было бы каждый из кусков оформить в виде отдельной программы, а потом перенаправлять вывод первой во ввод второй — как раз в духе Unix, но сейчас не о том.

К этому моменту мы уже познакомились с массивами — очень кстати, будет в чем хранить частоты. Зададимся максимальной длиной слов, которую сможет отслеживать наша программа и по ней укажем размер массива (именованная константа MAXLEN). Наша программа будет работать со словами длиной до MAXLEN-1.

В первом варианте программы обойдемся без флага state. Его роль фактически будет выполнять счетчик длины слова ncw. Однако без state будут подсчитываться слова длины 0 (например, начало ввода). Это можно считать недостатком, зато код станет проще.

  #include <stdio.h>

  #define MAXLEN 10   /* максимальная длина слова + 1*/

  /* вывод длин слов */
  int main()
  {
      int i, c, ncw;
      int wlength[MAXLEN];

      ncw = 0;
      for (i = 0; i < MAXLEN; ++i)
          wlength[i] = 0;

      while ((c = getchar()) != EOF) {
          if (c == ' ' || c == '\n' || c == '\t'){
              wlength[ncw] = wlength[ncw] + 1;
              ncw = 0;
          }
          else 
              ++ncw;
      }
      for (i = 0; i < MAXLEN; ++i)
          printf("length %d : %d\n", i, wlength[i]);
      return 0;
  }

Инициализация массива необходима, так как до нее в массиве хранится всякий мусор. Например, вот что происходит без инициализации:

whist03.png

А вот что — с нею:

whist01.png

Слова, длиной более 9, наша программа не считает:

whist02.png

Итак, данное упражнение дает пример двух важных практических "нужно":

  • задачу делить на части;
  • возможности реализовывать постепенно.

Кстати, о "постепенно". Уберем подсчет слов нулевой длины, реализовав проверку if (ncw > 0). Теперь добавлять частоту можно только для слов ненулевой длины. Освободившуюся нулевую ячейку массива будем заполнять частотой слов, длина которых превосходит установленный максимум. Зная эту частоту, мы можем подрегулировать MAXLEN так, чтобы "запредельных" слов не оставалось (другой вариант использования ячейки: сохранять в ней длину максимального слова, тогда уже в следующем запуске можно установить "правильный" MAXLEN).

  #include <stdio.h>

  #define MAXLEN 10   /* максимальная длина слова + 1*/

  /* вывод длин слов */
  int main()
  {
      int i, c, ncw;
      int wlength[MAXLEN];

      ncw = 0;
      for (i = 0; i < MAXLEN; ++i)
          wlength[i] = 0;

      while ((c = getchar()) != EOF) {
          if (c == ' ' || c == '\n' || c == '\t'){
              if (ncw >= MAXLEN)
                  wlength[0] += 1;
              else if (ncw > 0)
                  wlength[ncw] += 1;
              ncw = 0;
          }
          else 
              ++ncw;
      }
      for (i = 0; i < MAXLEN; ++i)
          printf("length %d : %d\n", i, wlength[i]);
      return 0;
  }

Кроме того, здесь более изящно реализовано приращение (инкремент) переменной wlength[i].

Вот что у нас получилось:

whist04.png

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

  for (i = 0; i < MAXLEN; ++i){
         printf("%d : ", i);
         for (j = 1; j <= wlength[i]; ++j)
             printf("|");
         printf(" > %d\n", wlength[i]);
  }

Результат:

whist05.png

Конечно, украшать это можно до бесконечности. Но мы лучше перейдем к вертикальному варианту гистограммы.

Наша гистограмма будет расти сверху вниз. На каждом шаге цикла for (i = 0; i < MAXLEN; ++i) будут проходится все возможные длины слов. Если слово такой длины во вводе присутствовало, выводим минус, если нет — пробел. После каждого прохода частоты слов будут уменьшаться на единицу (если, конечно, соответствующая частота уже не равна нулю) — ту самую, которую мы только что вывели.

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

  // построение гистограммы
      norm = 1; // произвольное ненулевое значение
      while (norm > 0) {
          norm = 0;
          for (i = 0; i < MAXLEN; ++i){
              if (wlength[i] > 0) {
                  printf("-");
                  wlength[i] -= 1;
              }
              else
                  printf(" ");
              norm += wlength[i];
          }
          printf("\n");
      }

Для наглядности, вот полный текст программы:

  #include <stdio.h>

  #define MAXLEN 10   /* максимальная длина слова + 1*/

  /* вывод длин слов */
  int main()
  {
      int i, norm, c, ncw;
      int wlength[MAXLEN];

      ncw = 0;
      for (i = 0; i < MAXLEN; ++i)
          wlength[i] = 0;

      while ((c = getchar()) != EOF) {
          if (c == ' ' || c == '\n' || c == '\t'){
              if (ncw >= MAXLEN)
                  wlength[0] += 1;
              else if (ncw > 0)
                  wlength[ncw] += 1;
              ncw = 0;
          }
          else 
              ++ncw;
      }

      // заголовок гистограммы
      for (i = 0; i < MAXLEN; ++i)
          printf("%d", i);
      printf("\n");

      // построение гистограммы
      norm = 1; // произвольное ненулевое значение
      while (norm > 0) {
          norm = 0;
          for (i = 0; i < MAXLEN; ++i){
              if (wlength[i] > 0) {
                  printf("-");
                  wlength[i] -= 1;
              }
              else
                  printf(" ");
              norm += wlength[i];
          }
          printf("\n");
      }
      return 0;
  }

Здесь также добавлен заголовок гистограммы.

А вот, что мы получили:

whist06.png

Гляжу на это и думаю: не оттого ли в некоторых языках программирования начало системы координат располагается в левом верхнем углу, а ось Y направлена вниз, что это существенно упрощает вывод результатов?

Строить вертикальную гистограмму снизу вверх я не буду, ибо скучно. Вместо этого лучше освоить какую-нибудь библиотеку деловой графики.

Упражнение 1.14. Напишите программу, печатающую гистограммы частот "встречаемости" вводимых символов.

Вот программа из учебника:

  #include <stdio.h>

  /* подсчет цифр, символов-разделителей и прочих символов */
  int main()
  {
      int c, i, nwhite, nother;
      int ndigit[10];

      nwhite = nother = 0;
      for (i = 0; i < 10; ++i)
          ndigit[i]= 0;

      while ((c = getchar()) != EOF)
          if (c >='0' && c <= '9')
              ++ndigit[c-'0'];
          else if (c == ' ' || c == '\n' || c == '\t')
              ++nwhite;
          else 
              ++nother;

      printf("цифры =");
      for (i = 0; i < 10; ++i)
          printf("%d", ndigit[i]);
      printf(", символы-разделители =%d, прочие =%d\n", nwhite, nother);
      return 0;
  }

При построении гистограммы неудобно, что пробельные и прочие символы считаются отдельно от цифр. Объединим их поэтому в общем массиве и нарисуем горизонтальную гистограмму, как в прошлом упражнении. Для простоты не будем различать отдельные разделители и прочие символы, а подсчитывать общее число разделителей и т. п.

  #include <stdio.h>

  #define LEN 12

  /* построение гистограммы частот употребления цифр, 
   * символов-разделителей и прочих символов */
  int main()
  {
      int c, i, j;
      int nchar[LEN];

      for (i = 0; i < LEN; ++i)
          nchar[i]= 0;

      while ((c = getchar()) != EOF)
          if (c >='0' && c <= '9')
              ++nchar[c-'0'];
          else if (c == ' ' || c == '\n' || c == '\t')
              ++nchar[10];
          else 
              ++nchar[11];

      for (i = 0; i < LEN; ++i){
          printf("%d : ", i);
          for (j = 1; j <= nchar[i]; ++j)
              printf("|");
          printf(" > %d\n", nchar[i]);
      }    

      return 0;
  }

chist0.png

Упражнение 1.15. Перепишите программу преобразования температур, выделив само преобразование в отдельную функцию.

  #include <stdio.h>

  float celsius(float f);

  /* печать таблицы температур по Фаренгейту и Цельсию для
     fahr = 0, 20, . .., 300; вариант с плавающей точкой и функцией */
  int main()
  {
      float fahr;
      int lower, upper, step;

      lower = 0;   /* нижняя граница таблицы температур */ 
      upper = 300; /* верхняя граница */ 
      step = 20;   /* шаг */

      fahr = lower;
      while (fahr <= upper) { 
          printf("%3.0f %6.1f\n", fahr, celsius(fahr));
          fahr = fahr + step;
      }
      return 0;
  }

  /* Преобразует градусы Фаренгейта в градусы Цельсия */
  float celsius(float f)
  {
      return (5.0/9.0) * (f-32.0);
  }

Упражнение 1.16. Перепишите main предыдущей программы так, чтобы она могла печатать самую длинную строку без каких-либо ограничений на ее размер.

Предыдущая — это программа из п. 1.9, которая читает набор текстовых строк и печатает самую длинную из них:

  #include <stdio.h>

  #define MAXLINE 1000 /* максимальный размер вводимой строки */

  int getl(char line[], int maxline);
  void copy(char to[], char from[]);

  /* печать самой длинной строки */
  int main()
  {
      int len; /* длина текущей строки */
      int max; /* длина максимальной из просмотренных строк */
      char line[MAXLINE]; /* текущая строка */
      char longest[MAXLINE]; /* самая длинная строка */

      max = 0;
      while ((len = getl(line, MAXLINE)) > 0)
          if (len > max) {
              max = len;
              copy(longest, line);
          }
      if (max > 0) /* была ли хоть одна строка? */
          printf("%s", longest);
      return 0;
  }

  /* getline: читает строку в s, возвращает длину */
  int getl(char s[], int lim)
  {
      int c, i;

      for (i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; ++i)
          s[i] = c;
      if (c == '\n') {
          s[i] = c;
          ++i;
      }
      s[i] = '\0';
      return i;
  }

  /* copy: копирует из 'from' в 'to'; to достаточно большой */
  void copy(char to[], char from[])
  {
      int i;

      i = 0;
      while ((to[i] = from[i]) != '\0')
          ++i;
  }

В оригинале здесь не getl(), а getline(), однако stdio.h уже содержит свой getline(), и если оставить все как есть, то программа не пожелает компилироваться.

Кроме того, оказалось, что само задание переведено некорректно. Оригинал гласит: "Revise the main routine of the longest-line program so it will correctly print the length of arbitrarily long input lines (курсив мой), and as much as possible of the text". Т. е. напечатать нужно не саму строку (которую нужно еще где-то хранить, а ни динамических массивов, ни файлов мы пока не знаем), а длину строки, что гораздо проще.

Приступим. Переделке подвергается цикл while. На псевдокоде суть переделки выглядит так:

while (есть ли еще фрагмент строки?)
    добавить длину считанного фрагмента к длине строки
    if (длина фрагмента строки не равна максимально возможной длине считываемой строки)
        if (данная строка длинней других)
            запомнить ее длину
            запомнить ее
        обнулить длину строки

Если длина считываемого фрагмента равна максимально возможной длине строки, то мы считываем следующий фрагмент, увеличивая каждый раз счетчик длины.

Ниже приведен код функции main(), поскольку остальная часть программы осталась неизменной.

  #include <stdio.h>

  #define MAXLINE 10 /* максимальный размер вводимой строки */

  int getl(char line[], int maxline);
  void copy(char to[], char from[]);

  /* печать самой длинной строки */
  int main()
  {
      int len; /* длина текущей строки */
      int tmp; /* длина считываемого фрагмента текущей строки */
      int max; /* длина максимальной из просмотренных строк */
      char line[MAXLINE]; /* текущая строка */
      char longest[MAXLINE]; /* самая длинная строка */

      max = 0;
      len = 0;
      while ((tmp = getl(line, MAXLINE)) > 0) {
          len += tmp;
          if (tmp != MAXLINE-1) {
              if (len > max) {
                  max = len;
                  copy(longest, line);
              }
              len = 0;
          }
      }
      if (max > 0) /* была ли хоть одна строка? */
          printf("\nlength=%d,%s", max, longest);
      return 0;
  }

getmaxline10.png

Упражнение 1.17. Напишите программу печати всех вводимых строк, содержащих более 80 символов.

По идее, эта программа — сильно упрощенная предыдущая. Кроме того, нужно добавить счетчик cont, указывающий, когда строка превысила размер массива и требуется продолжение печати. Перепишем только main(), потому что getline() не изменялся:

  int main()
  {
      int len; /* длина текущей строки */
      int cont; /* продолжать ли расчет? */
      int last;
      char line[MAXLINE]; /* текущая строка */

      len = 0;
      cont = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          last = line[len-1];
          if (len == MAXLINE-1) {
              printf("%s", line);
              cont = 1;
          }
          else if (cont == 1) {
              printf("%s\n", line);
              cont = 0;
          }
      }
      return 0;
  }

Все вроде бы нормально, но вот беда: если длина строки в точности равна MAXLINE-1, то следующая короткая строка (т. е. такая, которую печатать не надо) проходит на печать и лишь потом "опускается" флаг продолжения cont.

Чтобы решить эту проблему, рассмотрим последний символ строки. Если это не '\n', то нужно продолжать печатать. Реализуем это:

  int main()
  {
      int len; /* длина текущей строки */
      int cont; /* продолжать ли расчет? */
      int last; /* последний символ строки */
      char line[MAXLINE]; /* текущая строка */

      len = 0;
      cont = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          last = line[len-1];
          if (len == MAXLINE-1 && last != '\n') {
              printf("%s", line);
              cont = 1;
          }
          else if (cont == 1) {
              printf("%s\n", line);
              cont = 0;
          }
      }
      return 0;
  }

prline800.png

Строка вывода начинается с вертикальной черты (добавил для наглядности, в коде выше ее нет). Для удобства отладки ограничил длину строки 10.

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

Упражнение 1.18. Напишите программу, которая будет в каждой вводимой строке заменять стоящие подряд символы пробелов и табуляций на один пробел и удалять пустые строки.

Вопрос в том, как грамотно организовать программу. Можно, например, сделать обработку строки в getl(), но лучше оставить эту функцию неизменной. Пусть она берет символы из ввода и создает из них строку, а обработку строки поручим новой функции — filter(). Следующий вопрос: что считать пустой строкой. Примем, что пустая строка состоит из единственного символа '\n' (так проще, хотя для пользователя пустой строкой может быть строка, состоящая, кроме того, из пробелов и табуляций).

Лишние пробелы и табуляции мы уже удаляли в упражнении 1.9, так что вспомним, как это делается:

  #include <stdio.h>

  #define MAXLINE 1000 /* максимальный размер вводимой строки + 1 */

  int getl(char line[], int maxline);
  int filter(char to[], char from[]);

  int main()
  {
      int len; /* длина текущей строки */
      char line[MAXLINE]; /* текущая строка */
      char newline[MAXLINE]; /* новая строка */

      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          if ((len = filter(newline, line)) != 1)
          printf("| %s", newline);
      }
      return 0;
  }

  /* getline: читает строку в s, возвращает длину */
  int getl(char s[], int lim)
  {
      int c, i;

      for (i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; ++i)
          s[i] = c;
      if (c == '\n') {
          s[i] = c;
          ++i;
      }
      s[i] = '\0';
      return i;
  }

  /* filter: преобразует содержимое 'from' в 'to'
   *         убирает повторные пробелы и табуляции */
  int filter(char to[], char from[])
  {
      int c, i, j, sflag;

      i = 0;
      j = 0;
      sflag = 0;
      while ((c = from[i]) != '\n') {
          if (c == ' ' || c == '\t'){
              if (sflag == 0) {
                  to[j] = ' ';
                  sflag = 1;
                  ++j;
              }
          }
          else {
              to[j] = c;
              sflag = 0;
              ++j;
          }
          ++i;
      }

      if (c == '\n') {
          to[j] = c;
          ++j;
      }
      to[j] = '\0';
      return j;
  }

filtline0.png

Вывод, для наглядности, предваряется вертикальной чертой.

Новая функция filter(), также как и getline(), вычисляет длину строки. Это дублирование функционала связано с тем, что getline() берет символы из ввода, а filter() из массива. Можно подумать над тем, как избежать дублирования... Как-нибудь... В другой раз.

Упражнение 1.19. Напишите функцию reverse(s), размещающую символы в строке s в обратном порядке. Примените ее при написании программы, которая каждую вводимую строку располагает в обратном порядке.

Сразу и функция и программа ее использующая:

  #include <stdio.h>

  #define MAXLINE 100 /* максимальный размер вводимой строки + 1 */

  int getl(char line[], int maxline);
  void reverse(char to[], char from[], int len);

  /* печать обращенных строк */
  int main()
  {
      int len; /* длина текущей строки */
      char line[MAXLINE]; /* текущая строка */
      char rline[MAXLINE]; /* обращенная строка */

      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          reverse(rline, line, len);
          printf("| %s", rline);
      }
      return 0;
  }

  /* getline: читает строку в s, возвращает длину */
  int getl(char s[], int lim)
  {
      int c, i;

      for (i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; ++i)
          s[i] = c;
      if (c == '\n') {
          s[i] = c;
          ++i;
      }
      s[i] = '\0';
      return i;
  }

  /* copy: обращает строку из 'from' и записывает в 'to' */
  void reverse(char to[], char from[], int lim)
  {
      int i;

      i = 0;
      for (i = 0; i < lim-1; ++i) {
          to[lim-2-i] = from[i];
      }
      to[lim-1] = '\n';
      to[lim] = '\0';
  }

и тест:

reverstring0.png

Упражнение 1.20. Напишите программу detab, заменяющую символы табуляции во вводимом тексте нужным числом пробелов (до следующего “стопа” табуляции). Предполагается, что “стопы” табуляции расставлены на фиксированном расстоянии друг от друга, скажем, через n позиций. Как лучше задавать n — в виде значения переменной или в виде именованной константы?

Решая эту задачу, я не разобрался с тем, чего хотят авторы. Я заменил каждый символ табуляции фиксированным количеством пробелов, а нужно было дополнять пробелами до позиции табуляции. Пояснения и правильное решение смотрите в упражнении 5.11. Далее же помещено решение другой задачи. Но сначала напомню себе определение табуляции, чтобы не повторять ошибки в будущем.

Табуляция (горизонтальная) – управляющий символ таблицы ASCII. Встретив этот символ, терминал перемещает курсор вправо на ближайшую позицию табуляции. Представим себе строку в виде ленты, состоящей из ячеек, в каждую из которых можно поместить не более одного символ. Позиции табуляции располагаются через каждые N ячеек, где N — шаг табуляции. Например, при N=8 позиции табуляции находятся в колонках 1, 9, 17, 25…

Итак, решение "не той" задачи.

Идея та же, что и в упражнении 1.18, только вместо filter() используем "детабулятор" detab(). main() пишется сразу:

  #include <stdio.h>

  #define MAXLINE 1000 /* максимальный размер вводимой строки + 1 */
  #define TABSIZE 8    /* размер табуляции в пробелах */

  int getl(char line[], int maxline);
  void detab(char to[], char from[]);

  int main()
  {
      int len; /* длина текущей строки */
      char line[MAXLINE]; /* текущая строка */
      char newline[MAXLINE]; /* новая строка */

      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          detab(newline, line);
          printf("| %s", newline);
      }
      return 0;
  }

Ответ на вопрос, как задавать величину табуляции, зависит от стоящей перед разработчиком задачи. Именованная константа используется, если эту величину предполагается оставить неизменной. Как видно, я предпочел именно этот вариант. Но, если нужно управлять табуляцией, например из командной строки, то эту величину лучше сделать переменной.

getline() уже давно остается без изменений и я его здесь не привожу. А вот первый вариант detab():

  void detab(char to[], char from[])
  {
      int c, i, j, k;

      i = j = 0;
      while ((c = from[i]) != '\0') {
          if (c == '\t') {
              for (k = 0; k < TABSIZE; k++) {
                  to[j] = ' ';
                  ++j;
              }
          }
          else {
              to[j] = c;
              ++j;
          }
          ++i;
      }
      to[j] = '\0';
  }

Аналогичный подход мы применяли в filter(). Убедимся, что это работает:

detab0.png

Приведенный выше код можно и нужно упростить:

  /* detab: преобразует символы табуляции 'from' в заданное 
   *        число пробелов  'to' */
  void detab(char to[], char from[])
  {
      int i, j, k;

      i = j = 0;
      while ((to[j] = from[i]) != '\0') {
          if (to[j] == '\t')
              for (k = 0; k < TABSIZE; ++k, ++j)
                  to[j] = ' ';
          else
              ++j;
          ++i;
      }
      to[j] = '\0';
  }

Все что мы сделали — избавились от промежуточной переменной c и включили инкремент j в заголовок цикла. Побочным эффектом стало избавление от нескольких фигурных скобок, так как соответствующие блоки кода превратились в однострочные. При желании можно вернуться и проделать такую же процедуру с filter().

Упражнение 1.21. Напишите программу entab, заменяющую строки из пробелов минимальным числом табуляций и пробелов таким образом, чтобы вид напечатанного текста не изменился. Используйте те же “стопы” табуляции, что и в detab. В случае, когда для выхода на очередной “стоп” годится один пробел, что лучше — пробел или табуляция?

Решая эту задачу, я не разобрался с тем, чего хотят авторы. Я заменил каждый символ табуляции фиксированным количеством пробелов, а нужно было дополнять пробелами до позиции табуляции. Пояснения и решение — см. упражнение 5.11.

С main(), думается, все понятно (также, как и с getl()):

  #include <stdio.h>

  #define MAXLINE 1000 /* максимальный размер вводимой строки + 1 */
  #define TABSIZE 8    /* размер табуляции в пробелах */

  int getl(char line[], int maxline);
  void entab(char to[], char from[]);

  int main()
  {
      int len; /* длина текущей строки */
      char line[MAXLINE]; /* текущая строка */
      char newline[MAXLINE]; /* новая строка */

      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          entab(newline, line);
          printf("%s", newline);
      }
      return 0;
  }

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

  /* entab: преобразует символы пробелов 'from' в заданное 
   *        число пробелов и табуляций 'to' */
  void entab(char to[], char from[])
  {
      int i, j, k, ns, nt, c, m;

      i = j = ns = nt = 0;
      while ((c = from[i]) != '\0') {
          if (c == ' ')
              ++ns;
          else if (ns > 0) {
              nt = ns/(TABSIZE-1);
              for (m = 0; m < nt; ++m, ++j)
                  to[j] = '\t';
              for (k = 0; k < ns-nt*(TABSIZE-1)-(nt-1); ++k, ++j)
                  to[j] = ' ';
              to[j] = c;
              ns = 0;
              ++j;
          }
          else {
              to[j] = c;
              ++j;
          }
          ++i;
      }
      to[j] = '\0';
  }

Результат:

entab0.png

Строка, заканчивающаяся на t, содержит табуляцию в качестве разделителя. Она нужна в качестве образца, чтобы подобрать нужное число замещающих пробелов. Следом за ней идет результат работы программы. Далее — строка, состоящая из пробелов, заменяющих табуляцию. За ней вновь результат. И т. д.

Упражнение 1.22. Напишите программу, печатающую символы входного потока так, чтобы строки текста не выходили правее n-й позиции. Это значит, что каждая строка, длина которой превышает n, должна печататься с переносом на следующие строки. Место переноса следует “искать” после последнего символа, отличного от символа-разделителя, расположенного левее n-й позиции. Позаботьтесь о том, чтобы ваша программа вела себя разумно в случае очень длинных строк, а также когда до n-й позиции не встречается ни одного символа пробела или табуляции.

Вариант простейший: на n-й позиции выполняем разрыв строки (если она не завершилась сама). Для этого достаточно слегка переделать getl().

  #include <stdio.h>

  #define MAXLINE 20 /* максимальный размер вводимой строки + 1 */

  int getl(char line[], int maxline);

  int main()
  {
      int len; /* длина текущей строки */
      char line[MAXLINE]; /* текущая строка */

      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          printf("%s\n", line);
      }
      return 0;
  }

  /* getline: читает строку в s, возвращает длину */
  int getl(char s[], int lim)
  {
      int c, i;

      for (i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; ++i)
          s[i] = c;
      if (i == lim-2) {
          s[i] = '\n';
          ++i;
      }
      s[i] = '\0';
      return i;
  }

justiline0.png

Первая строка — ввод, остальные — результат.

В принципе, можно было изобразить еще символ переноса.

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

main() подверглась минимальной переработке: переменная len вынесена в заголовок, чтобы сделать ее доступной в новой функции justify(); добавлена константа BORDER — расстояние от правого края строки, на котором ищутся символы разделители (если их там нет, программа просто разрывает строку. Надеюсь, такое поведение достаточно разумно :)). getl() по традиции не публикую.

Особенности реализации: 1) если разделитель приходится на границу строки, то он заменяется символом конца строки; 2) выравнивание выполняется по числу символов, а не по числу позиций. Поэтому строки, содержащие символ табуляции (вывод которого занимает несколько позиций) будут выходить за пределы LINEWIDTH. Чтобы этого избежать, можно заменить табуляции пробелами с помощью detab() вроде того, что мы разработали в упражнении 1.20.

  #include <stdio.h>

  #define MAXLINE 50   /* максимальная длина строки ввода */
  #define LINEWIDTH 10 /* максимальная длина строки вывода */
  #define BORDER 5     /* ширина правого края строки вывода */

  int getl(char line[], int maxline);
  void justify(char to[], char from[]);

  int len; /* длина текущей строки */

  int main()
  {
      char line[MAXLINE]; /* текущая строка */
      char newline[MAXLINE]; /* преобразованная строка */

      printf("0123456789ABCDEF\n"); // шкала позиций символов    
      len = 0;
      while ((len = getl(line, MAXLINE)) > 0) {
          justify(newline, line);
          printf("%s\n", newline);
      }
      return 0;
  }

  /* justify: формирует из строкИ 'from' стрОки, выровненные
   *          по правому краю в 'to'                         */
  void justify(char to[], char from[])
  {
      int i, j, c, ns, nstmp, start;

      j = ns = nstmp = start = 0;
      while (len-ns > LINEWIDTH) {
          ns += LINEWIDTH;
          for (i = ns-BORDER; i < ns; ++i) {
              c = from[i];
              if (c == ' ' || c == '\t' || c == '\n') {
                  nstmp = i;
              }
          }
          if (nstmp > ns-BORDER)
              ns = nstmp+1;
          for (i = start; i < ns-1; ++i, ++j)
              to[j] = from[i];
          to[ns-1] = '\n';
          ++j;
          start = ns;
      }
      i = ns;
      while ((to[j] = from[i]) != '\0') {
          ++i; ++j;
      }
  }

justiline10.png

Упражнение 1.23. Напишите программу, убирающую все комментарии из любой Си-программы. Не забудьте должным образом обработать строки символов и строковые константы. Комментарии в Си не могут быть вложены друг в друга.

Ограничимся только многострочными комментариями: / comment /. Вначале решим упрощенную задачу: научимся убирать комментарии из текста программы, пусть даже и те, что стоят в строках символов. А затем "научим" программу оставлять комментарии внутри строк.

Основная программа изменяется косметически (новые имена функций). Функцию getl() упрощаем до getbuffer(), поскольку программа не обязан помещаться в одну строку. Получив данные из ввода с помощью getbuffer(), обрабатываем их функцией decomment() и печатаем. Итак:

  #include <stdio.h>

  #define BUFSIZE 10000 /* максимальный размер буфера ввода + 1 */

  int getbuffer(char line[], int maxline);
  void decomment(char to[], char from[]);

  int main()
  {
      int len; /* размер содержимого буфера */
      char buffer[BUFSIZE]; /* буфер ввода */
      char newbuffer[BUFSIZE]; /* буфер ввода с удаленными комментариями */

      len = 0;
      while ((len = getbuffer(buffer, BUFSIZE)) > 0) {
          decomment(newbuffer, buffer);
          printf("%s", newbuffer);
      }
      return 0;
  }

  /* getbuffer: считывает содержимое ввода в s, возвращает длину */
  int getbuffer(char s[], int lim)
  {
      int c, i;

      for (i = 0; i < lim && (c = getchar()) != EOF; ++i)
          s[i] = c;
      s[i] = '\0';
      return i;
  }

  void decomment(char to[], char from[])
  {
      int i, j, c;
      int cf;

      i = j = 0;
      cf = 0;
      while ((c = from[i]) != '\0') {
          if (c == '/' && from[i+1] == '*' && cf == 0) {
              cf = 1;
              ++i;
          }
          else if (c == '*' && from[i+1] == '/' && cf == 1) {
              cf = 0;
              ++i;
          }
          else if (cf == 0) {
              to[j] = from[i];
              ++j;
          }
          ++i;
      }
  }

Идея decomment() такая: нашли символы, означающие начало комментария — подняли флаг. Пока он поднят на вывод ничего не попадает. Встретили символы конца комментария — опустили флаг. Пока флаг опущен, копируем данные ввода в вывод.

decomment0.png

Как видите, комментарий из строки печати удален. Теперь научим программу не делать этого.

Ограничимся для простоты только случаем двойных кавычек. Изменениям подвергнется лишь функция decomment():

  void decomment(char to[], char from[])
  {
      int i, j, c;
      int cf, qf;

      i = j = 0;
      cf = qf = 0;
      while ((c = from[i]) != '\0') {
          if (c == '"') {
              if (qf == 0)
                  qf = 1;
              else
                  qf = 0;
              to[j] = from[i];
              ++j;
          }
          else if (c == '/' && from[i+1] == '*' && cf == 0 && qf == 0) {
              cf = 1;
              ++i;
          }
          else if (c == '*' && from[i+1] == '/' && cf == 1 && qf == 0) {
              cf = 0;
              ++i;
          }
          else if (cf == 0) {
              to[j] = from[i];
              ++j;
          }
          ++i;
      }
  }

Как видно, перед проверкой на открытие/закрытие комментария стоит условный оператор, поднимающий или опускающий флаг открытия кавычек. Если флаг поднят (кавычки открыты, qf = 1), то начало и конец комментария рассматриваются как обычные символы и cf = 0.

Проверим:

decomment10.png

Упражнение 1.24. Напишите программу, проверяющую Си-программы на элементарные синтаксические ошибки вроде несбалансированности скобок всех видов. Не забудьте о кавычках (одиночных и двойных), эскейп-последовательностях (...) и комментариях. (Это сложная программа, если писать ее для общего случая.)

Ограничимся проверкой парности круглых скобок. Снова разделим задачу на части: 1) научиться оценивать парность скобок без ограничений; 2) учесть ограничения (кавычки, комментарии, экранирование).

Итак, проверка парности круглых скобок без учета ограничений:

  #include <stdio.h>

  #define BUFSIZE 1000 /* максимальный размер буфера ввода */

  int getbuffer(char line[], int maxline);
  void checker(char code[]);

  int main()
  {
      int len; /* размер содержимого буфера */
      char buffer[BUFSIZE]; /* буфер ввода */

      len = 0;
      while ((len = getbuffer(buffer, BUFSIZE)) > 0) {
          //printf("%s\n", buffer);
          checker(buffer);
      }
      return 0;
  }

  /* checker: проверка парности скобок в s, возвращает сообщения об ошибках */
  void checker(char code[])
  {
      int i, c;
      int nb;

      i = nb = 0;
      while ((c = code[i]) != '\0') {
          if (c == '(')
              ++nb;
          if (c == ')') {
              --nb;
              if (nb < 0)
                 break; // выражение открывается с закрывающей 
          }             // скобки - выходим
          ++i;
      }
      if (nb > 0)
          printf("Незакрытых ')': %d", nb);
      else if (nb < 0)
          printf("Незакрытых '(': %d", -nb);
  }

getbuffer() тот же, что и в предыдущем упражнении.

Результат:

checker0.png

Следует отметить, что я проверял только парность скобок, тогда как можно проверять еще и их вложенность. Например, у меня "([)]" - корректное выражение, но можно потребовать, чтобы было только "()[]" или "([])", "[()]".

Теперь научим программу адекватно реагировать на одинарные и двойные кавычки, комментарии и экранирование. Опишем эти реакции:

  1. Внутри двойных кавычек не работает ничего (одинарные кавычки, комментарии), но могут встречаться экранированные двойные кавычки.
  2. Внутри комментариев не работает ничего.
  3. Внутри одинарных кавычек не работает ничего, но могут быть экранированные одинарные кавычки.

Я мог что-то пропустить. Но тогда это что-то нужно просто добавить в описание, а затем реализовать. Кроме того, предполагается, что все остальное, кроме круглых скобок реализовано по правилам С (например, внутри одиночных кавычек описан единичный символ). А это, вообще говоря, довольно смелое предположение для функции проверки синтаксиса. Что ж, общий случай — это действительно СЛОЖНАЯ задача.

Реализуем обработку двойных кавычек. Открытие/закрытие кавычек отмечается увеличением/уменьшением переменной dqf. Встретив кандидата на закрывающую кавычку, функция проверят, не является ли предыдущий символ экранирующим.

Отладочный вариант нашего checker() выглядит так:

  void checker(char code[])
  {
      int i, c;
      int nb, dqf;
      char prev;

      i = nb = 0;
      dqf = 0;
      while ((c = code[i]) != '\0') {
          printf("%c", c);
          if (c == '"' && dqf == 0){
              ++dqf;
              printf("<%d", dqf);
          }
          else if (c == '"' && dqf > 0 && prev != *) {
              --dqf;
              printf("%d>", dqf);
          }
          if (c == '(' && dqf == 0) {
              ++nb;
              printf("-%d-", nb);
          }
          if (c == ')' && dqf == 0) {
              --nb;
              printf("-%d-", nb);
          }
          prev = c;
          ++i;
      }
      if (nb > 0)
          printf("\nНезакрытых ')': %d", nb);
      else if (nb < 0)
          printf("\nНезакрытых '(': %d", -nb);
  }

Многочисленные printf() нужны нам для отладки.

Вот что получилось:

checker10.png

Добавляем обработку комментариев. Число взаимосвязей возрастает: комментарии не работают внутри кавычек, а кавычки — внутри комментариев. Промежуточный итог:

  void checker(char code[])
  {
      int i, c;
      int nb, dqf, cf;
      char prev;

      i = nb = 0;
      dqf = cf = 0;
      while ((c = code[i]) != '\0') {
          printf("%c", c);
          if (c == '"' && dqf == 0 && cf == 0){
              ++dqf;
              printf("<%d", dqf);
          }
          else if (c == '"' && dqf > 0 && prev != * && cf == 0) {
              --dqf;
              printf("%d>", dqf);
          }
          else if (c == '*' && prev == '/' && dqf == 0)
              ++cf;
          else if (c == '/' && prev == '*' && dqf == 0)
              --cf;
          if (c == '(' && dqf == 0 && cf == 0) {
              ++nb;
              printf("-%d-", nb);
          }
          if (c == ')' && dqf == 0 && cf == 0) {
              --nb;
              printf("-%d-", nb);
          }
          prev = c;
          ++i;
      }
      if (nb > 0)
          printf("\nНезакрытых ')': %d", nb);
      else if (nb < 0)
          printf("\nНезакрытых '(': %d", -nb);
      else
          printf("\nOK");
  }

По-прежнему в тексте много отладочных printf'ов.

checker20.png

Добавим, наконец, учет одинарных кавычек.

  /* checker: проверка парности скобок в s, возвращает сообщения об ошибках */
  void checker(char code[])
  {
      int i, c;
      int nb, dqf, qf, cf;
      char prev;

      i = nb = 0;
      dqf = cf = qf = 0;
      while ((c = code[i]) != '\0') {
          if (c == '"' && dqf == 0 && cf == 0 && qf == 0)
              ++dqf;
          else if (c == '"' && dqf > 0 && prev != * && cf == 0 && qf == 0)
              --dqf;
          else if (c == '*' && prev == '/' && dqf == 0 && qf == 0)
              ++cf;
          else if (c == '/' && prev == '*' && dqf == 0 && qf == 0)
              --cf;
          else if (c == '\* && qf == 0 && dqf == 0 && prev != * && cf == 0)
              ++qf;
          else if (c == '\* && qf > 0 && dqf == 0 && prev != '' && cf == 0)
              --qf;
          if (c == '(' && dqf == 0 && cf == 0 && qf == 0)
              ++nb;
          if (c == ')' && dqf == 0 && cf == 0 && qf == 0)
              --nb;
          prev = c;
          ++i;
      }
      if (nb > 0)
          printf("\nНезакрытых ')': %d", nb);
      else if (nb < 0)
          printf("\nНезакрытых '(': %d", -nb);
      else
          printf("\nOK");
  }

checker30.png

Как видно, совершенствовать эту штуку можно еще долго. Что ж, а мы идем дальше.



Комментарии

comments powered by Disqus