4.4. Символы. Строки. Текст

    Любой язык программирования содержит средства представления и обработки текстовой информации. Другое дело, что обычно программист наряду с символами имеет дело с типом данных (формой представления) – строкой, причем особенности ее организации скрыты, а для работы предоставлен стандартный набор функций. В Си, наоборот, форма представления строки является открытой, а программист работает с ней «на низком уровне».

    Представление символов и строк в Си

     Story Немного истории. Символы и строки в Си реализованы в соответствии со сложившимися в 70-годы прошлого века стандартами представления текста. Тогдашний уровень технологии привел к тому, что в качестве единицы представления символа был выбран байт, информационная емкость которого равно 2^8=256. Это означает, что одновременно в программе можно представить не более 256 различных символов. Этого хватает для стандартного набора символов и букв латинского алфавита. Для прочих символов (кириллица, национальные алфавиты, псевдо-графика, математические) используются различные кодовые таблицы, работа с которыми не включена в стандарты языка (т.е. попросту не оговаривается в нем). Аналогично, имеются различные варианты представления символа «конец строки» в различных ОС, что создает проблемы, связанные с переносом текстовых файлов.

    Примечание. Исторически сложившееся «рыночное разнообразие» на момент появления стандарта привело к тому, что имеются несколько кодовых таблиц, представляющих кириллицу:

    • кодовая таблица Windows CP-1251;

    • кодовая таблица DOS CP-866;

    • кодовая таблица Международной организации стандартизации (ISO), используемая семействами мобильных ОС UNIX, Linux, FreeBSD и т.п. - ISO-8859-5;

    • кодовые таблицы «советских» стандартов кодов информационного обмена (KOI-8) - CP KOI-8U и CP KOI-8R.

    Стандартный ввод-вывод. Текстовый файл. Имеющийся в большинстве языков программирования стандартный символьный ввод-вывод соответствует работе программы в режиме командной строки (консольного приложения). Несмотря на то, что такой режим работы не имеет массового распространения, он остается актуальным для «внутреннего» программирования по следующим причинам:

    • работа с текстовыми файлами «вписана» в стандартный ввод-вывод. Например, в Си потоки ввода-вывода могут быть перенаправлены как на текстовый файл, так и на консольный ввод-вывод (клавиатура – экран);

    • текстовыми файлами являются файлы исходных текстов программ (Си-cpp, Паскаль-pas, Бейсик-bas), значительная часть файлов с параметрами настройки различных приложений (ini), командных файлов (файлов последовательностей команд – bat);

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

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

    Символ текста. Базовый тип данных char понимается трояко: как байт - минимальная адресуемая единица представления данных в компьютере, как целое со знаком (в диапазоне –127…+127) и как символ текста. Этот факт отражает общепринятые стандарты на представление текстовой информации, которые «зашиты» как в архитектуре компьютера (клавиатура, экран, принтер), так и в системных программах. Стандартом установлено соответствие между символами и присвоенными им значениями целой переменной (кодами). Любое устройство, отображающее символьные данные, при получении кода выводит соответствующий ему символ. Аналогично клавиатура (совместно с драйвером) кодирует нажатие любой клавиши с учетом регистровых и управляющих клавиш в соответствующий ей код.

    ' '0x20B0x42
    *0x2AY0x59
    00x30Z0x5A
    10x31a0x61
    90x39b0x62
    A0x41z0x7A

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

    C
    char      c;
    
    for (c= 'A'; c <= 'Z'; c++) ...
    
    for (c=0x41; c <=0x5A; c++) ...

    Имеется ряд кодов так называемых неотображаемых символов, которым соответствуют определенные действия при вводе-выводе. Например, символу с кодом 13idx10 (0x0D) -«возврат каретки» соответствует перевод курсора в начало строки. Для их представления в программе используются символьные константы, начинающиеся с обратной косой черты.

    TEXT
    Константа Название  Действие
    
     \a       bel       Звуковой сигнал
    
     \b       bs        Курсор на одну позицию назад              
    
     \f       ff        Переход к началу (перевод формата)
    
     \n       lf        Переход на одну строку вниз(перевод строки)
    
     \r       cr        Возврат на первую позицию строки
    
     \t       ht        Переход к позиции, кратной 8 (табуляция)
    
     \v       vt        Вертикальная табуляция по строкам
    
     \\  \'  \”  \?     Представление символов \, ', ", ?
    
     \nn                Символ с восьмеричным кодом nn 
    
     \xnn               Символ с шестнадцатеричным кодом nn       
    
     \0                 Символ с кодом 0

    Некоторые программы и стандартные функции обработки символов и строк (isdigit,isalpha) используют тот факт, что цифры, прописные и строчные (маленькие и большие) латинские буквы имеют упорядоченные по возрастанию значения кодов:

    TEXT
    '0' - '9'   0x30 - 0x39
    
    'A' - 'Z'   0x41 - 0x5A
    
    'a' - 'z'   0x61 - 0x7A

    Строка. Строкой называется последовательность символов, ограниченная символом с кодом 0, то есть '\0'. Из ее определения видно, что она является объектом переменной размерности. Местом хранения строки является массив символов. Суть взаимоотношений между строкой и массивом символов состоит в том, что строка является структурой данных, а массив – переменной:

    • строка хранится в массиве символов, массив символов может быть инициализирован строкой, а может быть заполнен программно:
    C
    char A[20] = { 'С','т','р','о','к','а','\0' };
    
    char B[80];
    
    for (int i=0; i<20; i++) B[i] = 'A';
    
    B[20] = '\0';
    • строка представляет собой последовательность, ограниченную символом '\0', поэтому работать с ней нужно в цикле, ограниченном не размерностью массива, а условием обнаружения символа конца строки:

    for (i=0; B[i] !='\0'; i++)...

    • соответствие размерности массива и длины строки транслятором не контролируется, за это несет ответственность программа (программист, ее написавший):
    C
    char      C[20],B[]=”Строка слишком длинная для C”;
    
    // следить за переполнением массива
    
    // и ограничить строку его размерностью 
    
    for (i=0; i<19 && B[i]!='\0'; i++) C[i] = B[i];
    
    C[i]='\0';

    Строковая константа - последовательность символов, заключенная в двойные кавычки. Допустимо использование неотображаемых символов. Строковая константа автоматически дополняется символом '\0', ею можно инициализироваться массив, в том числе такой, размерность которого определяется размерностью строки:

    C
    char      A[80] = "123456\r\n";
    
    char      B[] = "aaaaa\033bbbb";
    
    ..."Это строка"...

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

    C
    char      A[20][80];
    
    char      B[][40] = { "Строка","Еще строка","0000","abcdef"};

    Первый индекс двумерного массива соответствует номеру строки, второй - номеру символа в нем:

    C
    for (int i=0; i<20; i++)
    
    for (int k=0; A[i][k] !='\0'; k++) {} // Работа c символами i-й строки

    Текстовые файлы. Формат строки, ограниченной символом '\0', используется для представления ее в памяти программы. При чтении строки или последовательности символов из внешнего потока (клавиатура, экран, файл) ограничителем строки является другой символ – '\n' («перевод строки», line feed (LF)). Здесь возможны различные «тонкости» при вызове функций, работающих со строками. Например, функция чтения из потока-клавиатуры возвращает строку ограниченную единственным символом '\0', а функция чтения из потока-файла – дополнительно помещает символ '\n', если строка полностью поместилась в отведенный буфер (массив символов).

    Функции стандартной библиотеки ввода-вывода обязаны «сглаживать противоречия», связанные с исторически сложившимися формами и анахронизмами в представлении строки в различных устройствах ввода-вывода и операционных системах (текстовый файл, клавиатура, экран) и приводить их к единому внутреннему формату.

    Стандартные приемы обработки строк

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

    • редактировать строку «на месте», реализуя вставку и удаление символов или фрагментов;

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

    Обработка символов с учетом особенностей их кодирования. Некоторые программы используют свойства упорядоченности значений кодов латинских букв и цифр. Такое программирование является, по большому счету, машинно-зависимым, но ввиду «незыблемости» стандарта представления символов может быть отнесено к языку.

    Получить символ десятичной цифры из значения целой переменной, лежащей в диапазоне 0..9:

    int n; char c; c = n + '0';

    Получить символ шестнадцатеричной цифры из значения целой переменной, лежащей в диапазоне 0..15:

    if (n <=9) c = n + '0'; else c = n - 10 + 'A';

    Получить значение целой переменной из символа десятичной цифры:

    if (c >='0' && c <='9') n = c - '0';

    Получить значение целой переменной из шестнадцатеричной цифры:

    C
    if (c >='0' && c <='9') n = c - '0';
    
    else
    
    if (c >='A' && c <='F') c = c - 'A' + 10;

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

    C
    if (c >='a' && c <='z') c = c - 'a' + 'A';

    Подсчет количества слов. Нужно учесть, что программа не умеет просто «видеть слово», для нее необходимо формальное условие его обнаружения. Таковым может быть либо конец слова, либо его начало. Начало слова обнаруживается по сочетанию пары символов: текущий – символ слова (не пробел), перед которым либо пробел, либо – начало строки.

    C
    //------------------------------------------------------44-01.cpp
    
    //--- Подсчет количества слов
    
    int words(char c[]) {
    
      for (int nc = 0, i = 0; c[i] != 0; i++) { // Посимвольный просмотр строки
    
        if (c[i] != ' ' && (i == 0 || c[i - 1] == ' ')) nc++;
    
        // начало слова  - не пробел, начало строки или впереди пробел
    
        // if (c[i]!=' ' && (c[i+1]==0 || c[i+1]==' ')) nc++;
    
        // конец слова  - не пробел, далее - конец строки или пробел
    
      }
    
      return nc;
    }

    Удаление лишних пробелов. Здесь уместно напомнить одно правило: количество индексов определяет количество независимых перемещений по массивам (степеней свободы). Если для входной строки индекс может изменяться в заголовке цикла посимвольного просмотра (равномерное «движение» по строке), то для выходной строки он меняется только в моменты добавления очередного символа. Кроме того, не нужно забывать «закрывать» выходную строку символом конца строки.

    C
    //------------------------------------------------------44-02.cpp
    
    //--- Удаление лишних пробелов при посимвольном переписывании
    
    void nospace(char c1[], char c2[]) {
    
      for (int j = 0, i = 0; c1[i] != 0; i++) { // Посимвольный просмотр строки
    
        if (c1[i] != ' ') { // Текущий символ не пробел
    
          if (i != 0 && c1[i - 1] == ' ') // Первый в слове -
    
            c2[j++] = ' '; // добавить пробел
    
          c2[j++] = c1[i]; // Перенести символ слова
    
        }
      } // в выходную строку
    
      c2[j] = 0;
    }

    Контекст c1[j++]= имеет вполне определенный смысл: добавить к выходной строке очередной символ и переместиться к следующему. Поскольку в процессе переписывания размер «уплотненной» части строки всегда меньше исходной, то можно совместить входную и выходную строку в одном массиве (запись нового содержимого будет происходить поверх просмотренного старого).

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

    C
    //------------------------------------------------------44-03.cpp
    
    //---- Сравнение строк по значениям кодов
    
    int my_strcmp(unsigned char s1[], unsigned char s2[]) {
    
      for (int n = 0; s1[n] != '\0' && s2[n] != '\0'; n++)
    
        if (s1[n] != s2[n]) break;
    
      if (s1[n] == s2[n]) return 0;
    
      if (s1[n] < s2[n]) return -1;
    
      return 1;
    }

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

    C
    //------------------------------------------------------44-04.cpp
    
    //---- Сравнение строк с заданными "весами" символов
    
    int Carry(char c) {
    
      static char ORD[] = "АаБбВвГгДдЕе1234567890";
    
      if (c == '\0') return 0;
    
      for (int n = 0; ORD[n] != '\0'; n++)
    
        if (ORD[n] == c) return n;
    
      return n + 1;
    }
    
    int my_strcmp(char s1[], char s2[]) {
    
      int n;
      char c1, c2;
    
      for (n = 0;
        (c1 = Carry(s1[n])) != '\0' && (c2 = Carry(s2[n])) != '\0'; n++)
    
        if (c1 != c2) break;
    
      if (c1 == c2) return 0;
    
      if (c1 < c2) return -1;
    
      return 1;
    }

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

    Пример: a{b{c}b}a{d{e{g}e}d}a => {c}{b1b}{g}{e3e}{d4d}a2a5a

    Задачу будем решать по частям. Несомненно, нам потребуется функция, которая ищет открывающуюся скобку для самого внутреннего вложенного фрагмента. Имея ее, можно организовать уже известное нам переписывание и «выкусывание». Основная идея алгоритма поиска состоит в использовании переменной-счетчика, которая увеличивает свое значение на 1 на каждую из открывающихся скобок и уменьшает на 1 на каждую из закрывающихся. При этом фиксируется максимальное значение счетчика и позиция элемента, где это происходит.

    C
    //------------------------------------------------------44-05.cpp
    
    //---- возвращается индекс скобки " {" для пары с максимальной глубиной
    
    int find(char c[]) {
    
      int i; // Индекс в строке
    
      int k; // Счетчик вложенности
    
      int max; // Максимум вложенности
    
      int b; // Индекс максимальной " {"
    
      for (i = 0, max = 0, b = -1; c[i] != 0; i++) {
    
        if (c[i] == '}') k--;
    
        if (c[i] == '{') {
    
          k++;
    
          if (k > max) {
            max = k;
            b = i;
          }
        }
    
      }
    
      if (k != 0) return 0; // Защита " от дурака" , нет парных скобок
    
      return b;
    }

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

    C
    //------------------------------------------------------44-06.cpp
    
    //---- возвращается индекс скобки " {" для первой самой внутренней пары
    
    int find(char c[]) {
    
      int i; // Индекс в строке
    
      int b; // Индекс максимальной " {"
    
      for (i = 0, b = -1; c[i] != 0; i++) {
    
        if (c[i] == '}') return b;
    
        if (c[i] == '{') b = i;
    
      }
    
      return b;
    }

    Идея основного алгоритма заключается в последовательной нумерации «выкусываемых» из входной строки фрагментов, при этом на место каждого помещается его номер – значение счетчика, которое для этого переводится во внешнюю форму представления.

    C
    //------------------------------------------------------44-07.cpp
    
    //----- Копирование вложенных фрагментов с " выкусыванием"
    
    void copy(char c1[], char c2[]) {
    
      int i = 0; // Индекс в выходной строке
    
      int k; // Индекс найденного фрагмента
    
      int n; // Запоминание начала фрагмента
    
      int m; // Счетчик фрагментов
    
      for (m = 1;
        (k = find(c1)) != -1; m++) { // Пока есть фрагменты
    
        for (n = k; c1[k] != '}'; k++, i++) c2[i] = c1[k]; // Переписать фрагмент и его "}"
    
        c2[i++] = c1[k++]; //
    
        if (m / 10 != 0) c1[n++] = m / 10 + '0'; // На его место две цифры
    
        c1[n++] = m % 10 + '0'; // номера во внешней форме
    
        for (; c1[k] != 0; k++, n++) c1[n] = c1[k];
    
        c1[n] = 0;
      } // сдвинуть " хвост" к началу
    
      for (k = 0; c1[k] != 0; k++, i++) c2[i] = c1[k]; // перенести остаток
    
      c2[i] = 0;
    } // входной строки

    Практический совет – желательно избегать сложные вычисления над индексами. Лучше всего для каждого фрагмента строки заводить свой индекс и перемещать их независимо друг от друга в нужные моменты. Что, например, сделано при «уплотнении» строки – индекс k после переписывания найденного фрагмента «останавливается» на начале «хвоста» строки, который переносится под индекс n – начало удаляемого фрагмента. Причем записываемые цифры номера смещают это начало на один или два символа. Таким образом, фрагмент заменяется во входной строке на его номер.

    Рис. 24-1. Удаление фрагмента путем переписывания пар символов

    Внешняя и внутренняя форма представления чисел

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

    Внутренняя форма представления числа - представление числа в виде целой или вещественной переменной.

    Внешняя форма представления числа - представление числа в виде строки символов – цифр в заданной системе счисления.

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

    Рис.24-2. Внешняя и внутренняя форма представления числа

    Обратите внимание, что о системе счисления имеет смысл говорить только тогда, когда число рассматривается в виде последовательности цифр, то есть во внешней форме представления числа. Внутренняя форма представления числа – двоичная и нас, грубо говоря, не интересует, поскольку компьютер корректно оперирует с ней и без нашего участия.

    На самом деле алгоритмы ввода-вывода числовых данных (вернее, преобразования данных из внешней формы во внутреннюю, и наоборот) идентичны алгоритмам преобразования чисел из произвольной системы счисления в десятичную (см.1.3). При этом десятичная система играет роль внутренней («родной») формы представления.

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

    C
    char c; int n;
    
    n = c - '0';
    
    c = n + '0';

    Ввод целого числа сопровождается его преобразованием из внешней формы – последовательности цифр – в внутренней – целой переменной, которая «интегрирует» цифры в одно значение с учетом их веса (что зависит, кроме всего прочего, и от системы счисления, в которой представлено вводимое число). В преобразовании используется тот факт, что при добавлении к числу очередной цифры справа старое значение увеличивается в 10 раз и к нему добавляется значение новой цифры, например:

    TEXT
    Число:      '123'  '1234'
    
    Значение:    123    1234 = 123 * 10 + 4               

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

    C
    n = n * 10 + c[i] - '0';
    
    //------------------------------------------------------44-08.cpp
    
    //----- Ввод десятичного целого числа
    
    int StringToInt(char c[]) {
    
      int n, i;
    
      for (i = 0; !(c[i] >= '0' && c[i] <= '9'); i++)
    
        if (c[i] == '\0') return 0; // Поиск первой цифры
    
      for (n = 0; c[i] >= '0' && c[i] <= '9'; i++) // Накопление целого
    
        n = n * 10 + c[i] - '0'; // «цифра за цифрой»
    
      return n;
    }

    Преобразование целого из внутренней формы во внешнюю (вывод целого числа). В преобразовании используется тот факт, что значение младшей цифры целого числа n равно остатку от деления его на 10, вторая цифра - остатку от деления на 10 частного n/10 и т.д.. В основе алгоритма положен цикл, в котором на каждом шаге получается значение очередной цифры справа как остаток от деления числа на 10, а само число уменьшается в 10 раз. Поскольку цифры получаются в обратном порядке (по-арабски), массив символов также необходимо заполнять от конца к началу. Для этого нужно либо вычислить заранее количество цифр, либо заполнять лишние позиции слева нулями или пробелами.

    C
    //------------------------------------------------------44-09.cpp
    
    //---- Вывод целого десятичного числа
    
    void IntToString(char c[], int n)
    
    {
      int nn, k;
    
      for (nn = n, k = 0; nn != 0; k++, nn /= 10); // Подсчет количества цифр числа
    
      c[k] = '\0'; // Конец строки
    
      for (k--; k >= 0; k--, n /= 10) // Получение цифр числа
    
        c[k] = n % 10 + '0'; // в обратном порядке
    
    }

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

    C
    //------------------------------------------------------44-10.cpp
    
    //---- Вывод вещественного десятичного числа
    
    void FloatToString(char c[], double v)
    
    {
      int i, nn, k, kk;
    
      for (nn = v, k = 0; nn != 0; k++, nn /= 10); // Подсчет количества цифр
    
      kk = k - 1;
      c[k++] = '.'; // целой части числа
    
      for (nn = v; kk >= 0; kk--, nn /= 10) // Получение цифр числа
    
        c[kk] = nn % 10 + '0'; // в обратном порядке
    
      v -= (int) v; // Убрать целую часть
    
      for (i = 0; i < 6; i++) {
    
        v *= 10.; // *10 - очередная цифра
    
        c[k++] = (int) v + '0'; // в целой части - записать
    
        v -= (int) v; // и отбросить
    
      }
    
      c[k] = 0;
    }

    Фрагменты вывода целой и дробной частей «сшиваются» путем запоминания местонахождения в строке символа «точка», разделяющего целую и дробную части.

    При приведении дробной части к внутренней форме приходится запоминать значение текущей степени 10^-k для умножения очередной цифры дробной части.

    C
    //------------------------------------------------------44-11.cpp
    
    //----- Ввод десятичного вещественного числа
    
    double StringToFloat(char c[]) {
    
      int i;
    
      double n, v;
    
      for (i = 0; !(c[i] >= '0' && c[i] <= '9'); i++)
    
        if (c[i] == '\0') return 0; // Поиск первой цифры
    
      for (n = 0; c[i] >= '0' && c[i] <= '9'; i++) // Накопление целого
    
        n = n * 10 + c[i] - '0'; // "цифра за цифрой"
    
      if (c[i] != '.') return n;
    
      for (i++, v = 0.1; c[i] >= '0' && c[i] <= '9'; i++) {
    
        n += v * (c[i] - '0'); // Взвешивание цифр   
    
        v = v / 10; // весом разряда дробной части
    
      }
    
      return n;
    }

    Преобразование, в котором внешняя форма числа задана в другой системе счисления, выполняются аналогично, только вместо числа 10 используется основание системы, а для систем счисления, больших 10, используется особое преобразование символов-цифр во внутреннее представление:

    C
    if (n <= 9) c = n + '0';
    else c = n - 10 + 'A'; // Внутренняя во внешнюю
    
    if (c >= '0' && c <= '9') n = c - '0'; // Внешняя во внутреннюю
    
    else if (c >= 'A' && c <= 'F') c = c - 'A' + 10;

    Посимвольная и пословная обработка.

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

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

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

    • при использовании структурной логики устанавливается однозначное соответствие между элементами формата (например, повторениями) и управляющими конструкциями алгоритма (соответственно, циклами). Описание формата как бы «зашивается» в текст алгоритма. Например, при пословной обработке строки должен быть цикл, за каждый шаг которого просматривается одно слово (включая пробелы перед ним). Внутри него должны быть два цикла – по цепочке пробелов и по цепочке символов слова.

    C
    //------------------------------------------------------44-12.cpp
    
    //---- Поиск слова максимальной длины - посимвольная обработка
    
    // Функция возвращает индекс начала слова или 1, если нет слов
    
    // Логика переменной состояния – n – счетчик символов слова
    
    int find(char s[]) {
    
      int i, n, lmax, b;
    
      for (i = 0, n = 0, lmax = 0, b = -1; s[i] != 0; i++) {
    
        if (s[i] != ' ') n++; // символ слова увеличить счетчик
    
        else { // перед сбросом счетчика
    
          if (n > lmax) {
            lmax = n;
            b = i - n;
          }
    
          n = 0; // фиксация максимального значения
    
        }
      } // то же самое для последнего слова
    
      if (n > lmax) {
        lmax = n;
        b = i - n;
      }
    
      return b;
    }
    
    //------------------------------------------------------44-13.cpp
    
    //---- Поиск слова максимальной длины - пословная обработка
    
    // Структурная логика – 3 цикла: просмотр слов, пробелов и символов
    
    int find(char in []) {
    
      int i = 0, k, m, b;
    
      b = -1;
      m = 0;
    
      while ( in [i] != 0) { // Цикл пословного просмотра строки
    
        while ( in [i] == ' ') i++; // Пропуск пробелов перед словом
    
        for (k = 0; in [i] != ' ' && in [i] != 0; i++, k++); // Подсчет длины слова
    
        if (k > m) { // Контекст выбора максимума
    
          m = k;
          b = i - k;
        } // Одновременно запоминается
    
      } // индекс начала
    
      return b;
    }

    По завершении посимвольного просмотра строки последнее слово (если после него нет пробела) оказывается необработанным. Поэтому контекст фиксации максимума повторяется после выхода из цикла.

    Здесь можно проиллюстрировать еще один принцип разработки программ: после ее написания для произвольной «усредненной» ситуации необходимо проверить ее «на крайности». В данном случае, при отсутствии в строке слов (строка состоит из пробелов или пуста), установленное начальное значение b=-1 будет возвращено в качестве результата (что и задумывалось при установке значения –1 как недопустимого).

    Лабораторный практикум

    1. В строке найти последовательности цифр, каждую из них считать числом в той системе счисления, которая соответствует максимальной цифре, заменить числа в строке символами с кодами, полученными из этих чисел. Пример: aaa010101bbb343ccc – двоичная и пятиричная системы счисления.

    2. В строке найти последовательности цифр, каждую из них считать числом в той системе счисления, которая соответствует первой цифре, заменить числа в строке символами с кодами, полученными из этих чисел. Пример: aaa2010101bbb8343ccc – двоичная и восьмиричная системы счисления.

    3. Для двух чисел, представленных в виде своих цифр и символов A…F определить системы счисления каждого, в которых они будут равны, например 134 и 30 - пятиричная и двенадцатиричная.

    4. Найти в строке последовательность одинаковых символов максимальной длины и переписать в выходную строку в виде n1,n2c – начало и длина фрагмента и символ, например abcddddddddddddedfg -> 3,12d. Из исходной строки фрагмент удалить. Повторять этот процесс. Пока в строке есть последовательности, в конце переписать остаток в выходную строку.

    5. Преобразовать строку, содержащую выражение на Си с операциями (=,==,!=,a+=,a-=), в строку, содержащую эти же операции с синтаксисом языка Паскаль (:=,=,#,abc=abc+,abc=abc-). Т.е. идентификатор перед операцией +- или -= должен дублироваться.

    6. Найти в строке два одинаковых фрагмента (не включающих в себя пробелы) длиной более 5 символов, скопировать их в выходную строку и удалить. Повторять этот процесс, пока такие фрагменты находятся. Остаток строки добавить в выходную.

    7. Найти во входной строке самую внутреннюю пару скобок {...} и переписать в выходную строку содержащиеся между ними символы. Во входной строке фрагмент удаляется. Повторять этот процесс, пока во входной строке не останется скобок, остаток также переписать в выходную строку.

    8. Если в строке встречаются подряд два числа, разделенные запятыми – n1,n2, то последующий фрагмент из n2 символов повторяется n1 раз. Если встречается одно число, то n1 раз повторяется символ, например, aaa4,3abcdef преобразуется в aaaabcaabcabcabcdef.

    9. "Перевернуть" в строке все слова. (Например: "Жили были дед и баба" - "илиЖ илиб дед и абаб"). Удалить слова – палинандромы.

    10. Определить, является ли строка палиандромом (например, «я у ребят беру наган») – удалить пробелы, найти фрагменты – палиандромы максимальной длины и удалить.

    11. Функция переписывает строку. Если она находит в строке число, то вместо него она переписывает в выходную строку соответствующее по счету слово из входной строки. (например, "aaa bb1bb cc2cc" - "aaa bbaaabb ccbb1bbcc"). Повторять этот процесс, пока в строке не останется чисел. Отследить возможное зацикливание, когда два слова ссылаются друг на другую.

    12. Программа – шифровальщик. Коды символов записать в промежуточную строку в виде символов-цифр (во внешней форме) в системе счисления основанием N1. Для системы счисления N2 определить максимальное количество цифр числа M, которое может быть записано в виде 1 байта. Каждые M цифр рассматривать как код символа в системе счисления N2. Полученные коды записать в виде зашифрованного текста.

    Вопросы без ответов

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

    Пример оформления тестового задания.

    C
    //------------------------------------------------------24-14.cpp
    
    int F(char c[]) {
      for (int i = 0, ns = 0; c[i] != 0; i++)
        if (c[i] != ' ' && (c[i + 1] == ' ' || c[i + 1] == 0)) ns++;
      return ns;
    }
    
    #include <stdio.h>
    
    void main() {
      printf("words=%d\n", F("aaaa bbb ccc dddd")); // Выведет - 4
    }

    Функция работает со строкой, поскольку в качестве параметра получает массив символов, которую просматривает до обнаружения символа конца строки. Переменная ns является счетчиком. Условие, выполнение которого увеличивает счетчик – текущий символа не является пробелом, а следующий – пробел, либо конец строки. Это условие обнаруживает конец слова. Таким образом, программа подсчитывает количество слов в строке, разделенных пробелами.

    C
    //------------------------------------------------------24-15.cpp
    //------------------------------------------------- 1
    void F1(char c[]) {
      int i, j;
      for (i = 0; c[i] != '\0'; i++);
      for (j = 0, i--; i > j; i--, j++) {
        char s;
        s = c[i];
        c[i] = c[j];
        c[j] = s;
      }
    }
    //------------------------------------------------- 2
    int F2(char s) {
      if (s >= '0' && s <= '9') return s - '0';
      else return -1;
    }
    //------------------------------------------------- 3
    void F3(char c[]) {
      for (int i = 0; c[i] != '\0'; i++)
        if (c[i] >= 'a' && c[i] <= 'z')
          c[i] += 'A' - 'a';
    }
    //------------------------------------------------- 4
    int F4(char c[]) {
      int i, old, nw;
      for (i = 0, old = 0, nw = 0; c[i] != '\0'; i++) {
        if (c[i] == ' ') old = 0;
        else {
        if (old == 0) nw++;
        old = 1;
      }
      if (c[i] == '\0') break;
    
      return nw;
    }
    //------------------------------------------------- 5
    void F5(char c[]) {
      for (int i = 0, j = 0; c[i] != '\0'; i++)
        if (c[i] != ' ') c[j++] = c[i];
      c[j] = '\0';
    }
    //------------------------------------------------- 6
    void F6(char c[], int nn) {
      int k, mm;
      for (mm = nn, k = 0; mm != 0; mm /= 10, k++);
      for (c[k--] = '\0'; k >= 0; k--) {
        c[k] = nn % 10 + '0';
        nn /= 10;
      }
    }
    //------------------------------------------------- 7
    int F7(char c[]) {
      int i, s;
      for (i = 0; c[i] != '\0'; i++)
        if (c[i] >= '0' && c[i] <= '7') break;
      for (s = 0; c[i] >= '0' && c[i] <= '7'; i++)
        s = s * 8 + c[i] - '0';
      return s;
    }
    //------------------------------------------------- 8
    int F8(char c[]) {
      int n, k, ns;
      for (n = 0, ns = 0; c[n] != '\0'; n++) {
        for (k = 0; n - k != 0 && c[n + k] != '\0'; k++)
          if (c[n - k] != c[n + k]) break;
        if (k >= 3) ns++;
      }
      return ns;
    }
    //------------------------------------------------- 9
    int F9(char c1[], char c2[]) {
      int i, j;
      for (i = 0; c1[i] != '\0'; i++) {
        for (j = 0; c2[j] != '\0'; j++)
          if (c1[i + j] != c2[j]) break;
        if (c2[j] == '\0') return i;
      }
      return -1;
    }
    //------------------------------------------------ 10
    char F10(char c[]) {
      char m, z;
      int n, s, i;
      for (s = 0, m = 'A'; m <= 'Z'; m++) {
        for (n = 0, i = 0; c[i] != '\0'; i++)
          if (c[i] == m) n++;
        if (n > s) {
          z = m;
          s = n;
        }
      }
      return z;
    }
    //------------------------------------------------ 11
    void F11(char c[], double x) {
      int i;
      x -= (int) x;
      for (c[0] = '.', x -= (int) x, i = 1; i < 6; i++) {
        x *= 10.;
        c[i] = (int) x + '0';
        x -= (int) x;
      }
      c[i] = '\0';
    }
    //------------------------------------------------ 12
    int F12(char c[]) {
      for (int i = 0; c[i] != 0; i++) {
      if (c[i] == ' ') continue;
      for (int j = i + 1; c[j] == c[i]; j++);
      for (; c[j] != 0; j++) {
        for (int k = 0; i + k < j && c[i + k] == c[j + k]; k++);
        if (k >= 4) return i;
      }
      return -1;
    }
    //----------------------------------------------- 13
    void F13(char c[]) {
      int i, j, cm;
      for (i = j = cm = 0; c[i] != '\0'; i++) {
        if (c[i] == '*' && c[i + 1] == '/') {
          cm--, i++;
          continue;
        }
        if (c[i] == '/' && c[i + 1] == '*') {
          cm++, i++;
          continue;
        }
        if (cm == 0) c[j++] = c[i];
      }
      c[j] = 0;
    }