Основы разработки программного обеспечения на примере языка С

Препроцессор, оформление программы и средства ввода/вывода

Разбить на страницы
Показывать лекцию целиком

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

В программах, написанных на языке Си, вы часто встречаете директивы препроцессора, или, другими словами, символьного макропроцессора. Все эти директивы записываются с новой строки текста и начинаются с символа "#".

4.1. Макроподстановки

Типичным примером подобной директивы является строка

#include <параметры>  
    

или

#include "параметры"  
    

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

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

Самой простой директивой можно считать строку

#define < левая часть >    < правая  часть >  
    

В ней <левая часть> и <правая часть> - не что иное, как строки символов, разделенные пробелами. Левая часть определяет имя или прототип макроподстановки, а правая - соответствующие подставляемые значения, например:

#define TRUE (1)
#define FALSE (0)
#define NULL (0)
#define FOREVER while(l)  
    

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

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

/* Manipulation of single bits: */
/* (b) is the bit to be manipulated (0..?, rvalue) */
/* (w) is the value who's bits are to be altered (LVALUE) */
/* (v) is the value who's bits are to be tested (rvalue) */
#define BITSET(b, w) ( (w) |= (1 << (b)) ) /* VOID type */
#define BITCLR(b, w) ( (w) = ~(1 << (b)) ) /* VOID type */
#define ISBITSET(b, v) ( (v)  (1 << (b)) ) /* BOOL type */
#define ISBITCLR(b, v) (~(v)  (1 << (b)) ) /* BOOL type */
#define GETBITS(b, n, v) ( ((v) >> (b))  ( (1 << (n)) – 1))
#define PUTBITS(b, n, v, x) ( (((x)  ((1 << (n)) – 1)) << (b) ) \
        | ( (v)  ~(((1 << (n)) - 1) << (b))) )  
    

Директивами #if (#ifdef, #ifndef), #else, #endif можно гибко управлять подстановкой текста, включая или исключая из него требуемые фрагменты. При этом директивы #define и #undef позволяют управлять предысторией процесса, устанавливая или сбрасывая установки объектов макроподстановки.

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

4.2. h-файлы и программные модули

Для этой цели в языке Си предусмотрено использование заголовочных файлов (h-file). Такой файл содержит описания заголовков (прототипов) процедур и описания пользовательских типов, использующихся при обращении к этим процедурам. В состав программы пользователя подобный заголовочный (header) файл включается с помощью директивы #include. Имя подобного файла обычно совпадает с именем модуля.

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

Для средней системы (50-100 тысяч строк программного текста) обычно оказывается достаточно поддерживать в процессе разработки три уровня заголовочных файлов. Нижний уровень соответствует программным модулям. Промежуточный - функциональным областям (ФО). Функциональная область - это группа модулей, отвечающих за реализацию некоторой локальной функции системы, например расчет местоположения, планирование маневра, управление двигателем и т.п. Верхний уровень (обычно не более двух файлов) содержит определения типов и констант, общих для всей системы. В том числе в одном из файлов верхнего уровня могут вводиться принятые в проекте соглашения об именах (см. определения типа BITSET). Во втором - описания типов данных: структуры, перечислимые типы, именные константы, которые являются общими для всего проекта в целом.

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

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

4.3. Ввод/вывод

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

#include <stdio.h>  
    

Так модулю пользователя (включившему описания стандартного ввода/вывода) становятся доступными определение типа FILE, процедуры потокового ввода и вывода данных fopen, putc, getc, putchar, getchar, fclose и ряд констант: NULL, EOF.

Кроме того, импортирующий модуль может воспользоваться процедурами форматного преобразования printf, fprintf и sprintf, описания которых тоже включены в stdio. Процедуры printf и fprintf обеспечивают выполнение форматных преобразований при выводе в файл, а sprintf производит преобразование в форму выходной строки - своего первого параметра. Процедура printf не требует указывать имя файла вывода, так как осуществляет его стандартный выходной поток stdout.

По умолчанию в программе связываются с терминалом пользователя три файла: входной - stdin, выходной - stdout и файл сообщений об ошибках - stderr. Все они относятся к потоковому вводу/выводу. При этом данные, переносимые из входного буфера (a) в зону обработки программы (b) и из зоны (b) в выходной буфер обмена (с), рассматриваются как непрерывная последовательность символов (рис. 4.1).

(рис 4.1) Схема форматированного ввода/вывода

На самом деле система ввода/вывода (вместе с исполнительной средой операционной системы компьютера) обменивается с внешними устройствами блоками данных, используя для этого системные области (буфера обмена). По мере потребности по запросам команд ввода программы данные из текущей точки буфера (а) переносятся в зону обработки в память программы (b). При этом среда поддержки ввода/вывода, реализованная в библиотеке stdio Си (например, процедуры scanf, fscanf), может выполнять форматные преобразования символов потока.

Так последовательность символов "3" и "5" может быть перенесена в зону (b) без изменений в виде двух байт с кодом:

0 0 1 1 0 0 1 1 0 0 1 1 0 1 0 1

или может быть преобразована по числовому формату %2d к двоичному числу:

0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1

Т.е. в первом случае они остаются символами в кодировке ASCII, а во втором преобразуются в двоичное значение числа 35, записанного в прямом коде.

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

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

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

Предположим, нам надо ввести три целых числа A, B и C. Очевидно, можно для этого использовать фрагмент программы:

а)

int A, B, C, Count;
Count = scanf("%d%d%d", A, B, C) ;
    

Значение переменной Count при успешном вводе будет равно 3 - количеству введенных переменных. Следовательно, проверяя далее значение Count, можно судить об удачности попытки ввода всех переменных (Count равно 3) или только части из них (Count равно 2 или 1).

В ряде случаев удобнее решать вопрос по принципу "все или ничего". Тогда проще прибегнуть к чисто логической интерпретации результата ввода:

б)

Count = scanf("%d", A) scanf("%d", B)scanf("%d", C);  
    

Значение Count, отличное от 0, будет говорить об успешности ввода всех значений. Заметим, что выражение

в)

Count = scanf("%d", A) + scanf("%d", B) +
+ scanf("%d", C);  
    

по результату будет практически эквивалентно варианту (а) совместного ввода. Так, при вводе строки вида "12 13a 14bС" в случаях (а) и (в) значение переменной Count будет равно 2, параметры ввода A и B приобретут значение 12 и 13 соответственно, а переменная C останется без изменений.

В случае (б) значение Count будет равно 0, хотя переменные A, B и C изменятся таким же образом, как в (а) и (в). В буфере обмена во всех случаях останутся символы "a 14b". Любая попытка последующего ввода данных по числовому формату будет натыкаться на нецифровой символ и терпеть неудачу.

На самом деле похожие проблемы возникают в тех случаях, когда при вводе очередного значения, скажем k, возникла ошибка. Вместо входной строки "1 22 333" пользователь набрал "1 22 33w". В результате будут определены все три переменные - объекты ввода (A - 1, B - 22, C - 33), но во входном буфере останется символ "w". И любой последующий перенос данных из системного буфера в память программы будет начинаться именно с этого символа. Поэтому перед следующей операцией ввода системный буфер надо очистить.

Для этой цели можно воспользоваться операцией чтения строки символов:

Char Str[ 100] ;
  scanf("%s", Str);  
    

которая введет оставшуюся в буфере последовательность символов в область переменной Str. А если их там нет? Если пользователь просто ввел строку "1 22 33"? Тогда буфер пуст и система ввода будет ожидать от недоумевающего пользователя ввода символьной строки.

Более корректное решение - использование функций очистки буфера flush (); или fflush(stdin); которые очищают буфер обмена ото всех оставшихся в нем символов.

Но определенные проблемы могут возникнуть при вводе произвольной последовательности символов. Например, если необходимо считать строку со стандартного потока ввода, то первое и самое простое - это использование функции scanf:

#include <stdio.h>
int main()
{
  char Instr[100]; int Flag;
  printf("Input string:\n");
  Flag = scanf("%s", Instr);
  printf("Input=%s\n", Instr);
  return Flag;
}  
    

Но что произойдет, если будет введено больше 300 символов? В функции scanf контроля длины вводимой строки не происходит, она не знает, какого размера в программе переменная Instr. Получается, что все символы, не поместившиеся в массив Instr, будут записываться дальше в память, которая может использоваться другими переменными, в частности Flag. Эта ситуация называется нарушением границ массива.

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

Как же поступать? У функции scanf в спецификаторе ввода можно задать ограничение на длину считываемой строки:

scanf("%99s", Instr);  
    

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

int i = 0;
while ((*(Instr+i) = getc(stdin)) != '\n'  i++ < 99);
* (Instr+i) = '\0';  
    

Считывание происходит посимвольно, пока не встретится символ конца строки (символ '\n' ) или пока количество считанных символов не достигнет 99.

Следует помнить, что в случае использования scanf с форматом %s строка будет считана только до пробела. Дело в том, что пробел, как и символ табуляции, считается разделителем, и многие функции работы со строками используют этот разделитель.

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

fgets(Instr,99,stdin);  
    

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

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

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

char c, ch[10]; int i;
printf("Enter passsword, please\n");
c = getch(); i = 0; /* get first character */
while( c != '\015'  i < 9 ) /* end of input string */
{
  ch[i++] = c; putchar('*');
  c = getch(); /* get next character */
}
ch[i] = '\0'; /* end string marker */
printf("\nYour password is:%s\n",ch);  
    

Цикл ввода продолжается, пока не достигнут символ конца строки (восьмеричный код 015) и есть место в массиве символов ch. При этом на экран выводится символ "*" в каждую введенную позицию. В конце оформленная символьная строка для наглядности выводится на экран.

Еще более интересный прием можно рассмотреть на примере ввода восьмеричного числа.

char c, ch[8]; int i, j;
printf("Enter less then 7 octal digits, please\n");
c = getch(); i = 0;
while(c != '\015')/* end of input string */
{
  if(c <= '7' c >= '0'  i < 7)
  {
    ch[i++] = c; putchar(c);
  }
  if(c == '\010' i > 0)/* back space */
  {
    putchar(c); putchar(' ');putchar(c);i--;
  }
  c = getch();
}
ch[i] = '\0';
printf("\nYour number is:%s\n",ch);
sscanf(ch, "%o", j);
printf("Your integer value is:%d\n", j);  
    

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

Дополнительно в программу введена проверка на символ возврата на одну позицию назад (BS - back space, восьмеричный код 010). При его вводе программа возвращается на одну позицию (i--) в массиве введенных символов ch и выводит сам символ BS, пробел на место ранее введенного символа и еще раз символ BS.

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

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

В качестве еще одного примера рассмотрим чтение с клавиатуры и последующее кодирование символьной строки.

/*******************************************************
Date: 10 January 2013
Description: string reading and encoding sample
*******************************************************/
#include <stdio.h>
const int STARTD = 97; /* 'a' code */
const int ENDD = 122; /* 'z' code */
/******************************************************
* Name : testString
*
* Purpose : checks whether string contains anything
* except English small characters
* Input : str (string to check)
* Output : none
* Return :
* -1 bad string
* 0 good string (only small English characters)
*
******************************************************/
int testString(char *str)
{
  int i = 0;
  for(; *(str+i)!='\0'; i++)
  {
    if ( (*(str+i) < STARTD) || (*(str+i) > ENDD) )
    {
      return -1;
    }
  }
  return 0;
}
/*******************************************************
* Name : replaceLineEnd
*
* Purpose : replaces ending '\n' (if exists)
* symbol by '\0' symbol
* except English small characters
* Input : str (string)
* Output : str (string)
* Return : none
******************************************************/
void replaceLineEnd(char *str)
{
  int i;
  char temp[2];
  for(i=0; ((*(str+i)!='\n')  (*(str+i)!='\0')); i++);
  if ( *(str+i) == '\0' )
  {
    /* empty input buffer */
    while (temp[0] != '\n')
    {
      fgets(temp,2,stdin); /* reads 1 symbol + 1 end */
    }
  }
  if ( *(str+i) == '\n' )
  {
    *(str+i) = '\0';
  }
}
/*******************************************************
* Name : readString
*
* Purpose : reads string from keyboard, and tests
* whether it is a correct string
* Input : str (empty string)
* Output : str (inputted string)
* Return :
* -1 bad string
* 0 good string
******************************************************/
int readString(char *str)
{
  /* reads only 11 chars from keyboard */
    fgets(str,12,stdin);
    replaceLineEnd(str);
    return testString(str);
}
/*******************************************************
* Name : encodeAtbash
* Purpose : encodes string str using Atbash code
* Input : str (string)
* Output : str (encoded string)
* Return : none
******************************************************/  
void encodeAtbash(char *str)
{
int tmp;
  while (*str)
  {
    if ( (int)*str >= STARTD  (int)*str <= ENDD )
    {
      tmp = ENDD + STARTD - (int)*str ;
      *str = (char)tmp;
    }
    str++;
  }
}

int main()
{
  char str[12]; /* max length 11 + 1 for ending symbol */
    printf("Enter string to encode: ");
    if (readString(str) < 0)
    {
      printf("Incorrect string, exiting");
      return 0;
    }
  encodeAtbash(str);
  printf("Encoded string: %s",str);
  return 0;
}
    

В данном примере строка хранится в переменной str, для которой выделено 12 байт статической памяти. Выделение происходит при компиляции программы, так как переменная определена как массив char. Можно было бы задать str как указатель на char, в этом случае необходимо выделить память самостоятельно:

str = (char*) malloc(12);  
    

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

Из выделенных 12-ти байт один используется для хранения символа конца строки '\0' , для хранения символов самой строки остается 11 байт.

Для ввода строки сконструирована функция readSnring, использующая в свою очередь библиотечную функцию fgets. Первым параметром fgets указывается область ввода данных (str), вторым – допустимый размер в байтах порции ввода, а третьим задается потоковый файл, откуда производится ввод данных. Таким образом, размер массива str превышен не будет, даже если пользователь введет более 11 символов.

Дополнительная функция replaceLineEnd обеспечивает очистку буфера входной строки и подготовку переменной str для дальнейшей работы. Она проверяет, чем закончилась процедура считывания входного потока. Если в строку был перенесен символ '\n' – признак конца строки потокового файла, то он заменяется на '\0' – признак конца символьной строки в Си.

Кроме того, в процедуре replaceLineEnd происходит дочитывание строки потокового файла до '\n', если пользователь набрал слишком длинную строку:

if ( *(str+i) == '\0' )
{
  /* empty input buffer */
  while (temp[0] != '\n')
  {
    fgets(temp,2,stdin); /* reads 1 symbol + 1 end */
  }
}  
    

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

Для кодирования строки выбран алгоритм "Атбаш". Упоминание об его использовании встречается в Библии. Книга пророка Иеремии, глава 25, стих 26 содержит текст: "И всех царей севера, близких друг к другу и дальних, и все царства земные, которые - на лице земли, а царь Сесаха выпьет после них". Слово "Сесах" не является ни ошибкой, ни искажением библейского текста, хотя такого царя или царства не существовало. Священные тексты древних иудеев шифровались шифром простой замены "Атбаш". Алгоритм этого шифра прост: первая буква алфавита заменялась на последнюю, вторая - на предпоследнюю в алфавите и т.д. После дешифрации на языке оригинала (для успешной дешифрации необходимо знать язык сообщения) слова "Сесах" получается "Вавилон". По смыслу алгоритма функция, реализующая шифровку и зашифровку, одна и та же.

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

В функции кодирования encodeAtbash для доступа к символам строки используется синтаксис работы с указателем. Чтобы получить текущий символ, используется выражение *str. Для перехода к следующему символу выполняется str++. Можно реализовать эту функцию иначе с использованием механизма работы с массивом:

void encodeAtbash(char *str)
{
  int tmp;
  int i;
  i = 0;
  while (str[i])
  {
    if ( (int)str[i] >= STARTD  (int)str[i] <= ENDD )
    {  
      tmp = ENDD + STARTD - (int)str[i];
      str[i] = (char)tmp;
    }
    i++;
  }
}
    

Более того, даже параметр функции можно указать не как ссылку на char, а как "открытый" массив:

void encodeAtbash(char str[ ] )  
    

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

void encodeAtbash(char str[], int arrayLength)
  {
    int i;
    for (i = 0; i < arrayLength; i++)
    {
      . . .
    }
  . . .
}  
    

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

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

/*******************************************************
Date: 10 January 2013
Description: string reading and encoding sample 2
*****************************************************/
#include <stdio.h>
/*******************************************************
* Name : isEnglishLetter
*
* Purpose : сhecks whether the symbol belongs to
* English alphabet
* Input : c - symbol
* Output : none
* Return : 1 – belongs, 0 – do not belongs
*
*****************************************************/
int isEnglishLetter(char c)
{
  if( (c >= 'a')  ( c <= 'z' ) )
  {
    return 1;
  }
  if ( (c >= 'A')  (c <= 'Z') )
  {
    return 1;  
  }
  return 0;
}
/*******************************************************
* Name : isRussianLetter
*
* Purpose : checks whether the symbol belongs to
* Russian alphabet
* Input : c - symbol
* Output : none
* Return : 1 – belongs, 0 – do not belongs
******************************************************/
int isRussianLetter(char c)
{
  if( (c >= 'а')  ( c <= 'я' ) )
  {
    return 1;
  }
  if ( (c >= 'А')  (c <= 'Я') )
  {
    return 1;
  }
  return 0;
}
/*******************************************************
* Name : isNumber
*
* Purpose : checks whether the symbol is number
* Input : c - symbol
* Output : none
* Return : 1 – number, 0 – not a number
******************************************************/
int isNumber(char c)
{
  if( ((int)c >= (int)'0')  ( (int)c <= (int)'9' ) )
  {
    return 1;
  }
  return 0;
}
/*******************************************************
* Name : isDelimeter
*
* Purpose : checks whether the symbol is a delimiter
* Input : c - symbol
* Output : none
* Return : 1 – delimiter, 0 – not a delimiter
******************************************************/
int isDelimeter(char c)
{
  if( (c == ',') || (c == '.') || (c == ' ') || (c == '\0') )
  {
    return 1;
  }   else
    {
      return 0;
    }
}
/*******************************************************
* Name : testString
*
* Purpose : test string for correctness
* (according requirements)
* Input : str (string to check)
* Output : none
* Return :
* -1 bad string
* 0 good string (only acceptable characters)
******************************************************/
int testString(char *str)
{
  int start;
  int goodSymb;
  int count;
  count = 0;
  start = 0;
  while(*str)
  {
    goodSymb = 0;
    if (isEnglishLetter(*str))
    {
      start = 1;
      goodSymb = 1;
    }
    if (isRussianLetter(*str))
    {
      start = 1;
      goodSymb = 1;
    }
    if (isNumber(*str))
    {
      start = 1;
      goodSymb = 1;
    }
    if ( (*str) == ' ' )
    {
      goodSymb = 1;
    }
    if ( (isDelimeter(*str))  start )
    {
      goodSymb = 1;
    }
    if (goodSymb == 0)
    {
      return 0;
    }   else
    {
      str++;
      count++;
    }
  }
  if (count > 10)
  {
    return 0;
  } else
  {
    return 1;
  }
}
/*******************************************************
* Name : replaceLineEnd
* Purpose : replaces ending '\n' (if exists)
* symbol by '\0' symbol
* except English small characters
* Input : str (string)
* Output : str (string)
* Return : none
******************************************************/
void replaceLineEnd(char *str)
{
  int i;
  char temp[2];
  for(i=0; ( (*(str+i)!='\n')  (*(str+i)!='\0') ); i++);
  if ( *(str+i) == '\0' )
  {
    /* empty input buffer */
    while (temp[0] != '\n') \
    {
    fgets(temp,2,stdin);
    }
  }
  if ( *(str+i) == '\n' )
  {
    *(str+i) = '\0';
  }
}
/*******************************************************
* Name : readString
*
* Purpose : reads string from keyboard
* Input : str (empty string)
* Output : str (inputted string)
* Return : none
******************************************************/
void readString(char *str)
{
  /* reads only 81 chars from keyboard, 82 sets to '\0' */
  fgets(str,82,stdin);
  replaceLineEnd(str);
}
/*******************************************************
* Name : change231
* Purpose : ncodes word in string str using 231 exchange
* Input : str (string),
* startPos – position in string
* where the word starts from
* Output : str (encoded string)
* Return : encoded word end position in str
******************************************************/
int change231(char *str, int startPos)
{
  int i;
  char temp;
  i = startPos + 1;
  while (!isDelimeter(str[i]))
  {
    temp = str[i];
    str[i] = str[i-1];
    str[i-1] = temp;
    i++;
    if (((i-startPos)%3 == 0)  (!isDelimeter(str[i])))
    {
      i++;
    }
  }  
  return i;
}
/******************************************************
* Name : processString
* Purpose : encode words in string
* leaving delimiters unchanged
* Input : str (string),
* Output : str (encoded string)
* Return : none
******************************************************/
void processString(char *str)
{
  int i;
  i = 0;
  while( *(str+i) != '\0' )
  {
    if (isDelimeter(*(str+i)))
    {
      i++;
    } else
    {
      i = change231(str, i);
    }
  }
}
int main()
{
  /* max length 80 + 1 for ending symbol
    + 1 to test >80 entered symbols */
  char str[82];
  printf("Введите строку для кодирования\n");
  readString(str);
  while(*str != '\0')
  {
    if (testString(str) == 0)
    {
      printf("Ошибка во входной строке\n");
    } else
    {
      processString(str);
      printf("%s\n", str);
    }
      printf("Введите строку для кодирования\n");
      readString(str);
  }
  printf("Работа закончена");
  return 0;
}
    

Проверка корректности строки выделена в функцию testString, которая последовательно рассматривает каждый символ строки и проверяет, принадлежит ли он английскому алфавиту и/или русскому и/или является цифрой и/или разделителем. В начале допускаются в качестве разделителей только пробелы, поэтому используется дополнительная переменная start. Исходно она равна нулю, и только после появления в строке символа алфавита и/или цифры она становится равна единице. Пока start равна нулю, допускается только пробел в качестве разделителя:

if ( (*str) == ' ' )
{
  goodSymb = 1;
}  
    

а когда start равна единице, допускается любой разделитель (точка, запятая, пробел):

if ( (isDelimeter(*str))  start )
{
  goodSymb = 1;
}  
    

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

Сама перестановка символов выполнена последовательно попарно. Требуемый результат - 231 получается, если сначала переставить первый символ со вторым (получив 213) и далее второй с третьим (получив как раз 231). Такой подход оказывается справедлив и в случае, когда остается два символа (12 - 21), т.е. алгоритм перестановки сводится к работе лишь с двумя рядом стоящими символами (перемене их мест). Однако требуется перестановка только в рамках тройки символов, поэтому на каждом третьем шаге следует пропустить одну позицию:

if (((i-startPos)%3 == 0)  (!isDelimeter(str[i])))
i++;  
    

Алгоритм основной программы уже был определен ранее.

  • Ввести входную строку.
  • Если строка пустая, то закончить работу.
  • Преобразовать входную строку.
  • Вывести преобразованную строку.
  • Ввести очередную входную строку и вернуться к действию (2).
  • Соответственно будет выглядеть и реализация.

  • Ввести входную строку:
    printf("Введите строку для кодирования\n");
    readString(str);  
            
  • Если строка пустая, то закончить работу:
    while(*str != '\0')
    {
    . . .
    }
    printf("Работа закончена");
    return 0;  
            
  • Преобразовать входную строку:
    if (testString(str) == 0)
    {
      printf("Ошибка во входной строке\n");
    } else
    {
      processString(str);
      printf("%s\n", str);
    }  
            
  • Вывести преобразованную строку:
    processString(str);
    printf("%s\n", str);  
            
  • Ввести очередную входную строку и вернуться к действию (2):
    while(*str != '\0')
    {
      ...
      printf("Введите строку для кодирования\n");
      readString(str);
    }  
            
  • Вопросы и задачи для самостоятельного решения

  • Как можно задать числовую константу в Си?
  • Сколько необходимо создать файлов, чтобы реализовать модуль в Си?
  • Зачем нужны заголовочные файлы в Си?
  • В каких случаях возможно переполнение буфера при вводе строки? Какими способами можно этого избежать?
  • Напишите программу перевода десятичного числа в шестнадцатеричное. Входное число должно считываться из стандартного потока ввода (клавиатура). Перевод должен работать для чисел длиной не менее 100 цифр.
  • Напишите программу кодирования слова методом перестановки символов 3-1-2. Входное слово должно считываться из стандартного потока ввода (клавиатура). Кодирование должно работать для слов длиной не менее 300.
  • Страницы:

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

    В программах, написанных на языке Си, вы часто встречаете директивы препроцессора, или, другими словами, символьного макропроцессора. Все эти директивы записываются с новой строки текста и начинаются с символа "#".

    4.1. Макроподстановки

    Типичным примером подобной директивы является строка

    #include <параметры>  
        

    или

    #include "параметры"  
        

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

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

    Самой простой директивой можно считать строку

    #define < левая часть >    < правая  часть >  
        

    В ней <левая часть> и <правая часть> - не что иное, как строки символов, разделенные пробелами. Левая часть определяет имя или прототип макроподстановки, а правая - соответствующие подставляемые значения, например:

    #define TRUE (1)
    #define FALSE (0)
    #define NULL (0)
    #define FOREVER while(l)  
        

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

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

    /* Manipulation of single bits: */
    /* (b) is the bit to be manipulated (0..?, rvalue) */
    /* (w) is the value who's bits are to be altered (LVALUE) */
    /* (v) is the value who's bits are to be tested (rvalue) */
    #define BITSET(b, w) ( (w) |= (1 << (b)) ) /* VOID type */
    #define BITCLR(b, w) ( (w) = ~(1 << (b)) ) /* VOID type */
    #define ISBITSET(b, v) ( (v)  (1 << (b)) ) /* BOOL type */
    #define ISBITCLR(b, v) (~(v)  (1 << (b)) ) /* BOOL type */
    #define GETBITS(b, n, v) ( ((v) >> (b))  ( (1 << (n)) – 1))
    #define PUTBITS(b, n, v, x) ( (((x)  ((1 << (n)) – 1)) << (b) ) \
            | ( (v)  ~(((1 << (n)) - 1) << (b))) )  
        

    Директивами #if (#ifdef, #ifndef), #else, #endif можно гибко управлять подстановкой текста, включая или исключая из него требуемые фрагменты. При этом директивы #define и #undef позволяют управлять предысторией процесса, устанавливая или сбрасывая установки объектов макроподстановки.

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

    4.2. h-файлы и программные модули

    Для этой цели в языке Си предусмотрено использование заголовочных файлов (h-file). Такой файл содержит описания заголовков (прототипов) процедур и описания пользовательских типов, использующихся при обращении к этим процедурам. В состав программы пользователя подобный заголовочный (header) файл включается с помощью директивы #include. Имя подобного файла обычно совпадает с именем модуля.

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

    Для средней системы (50-100 тысяч строк программного текста) обычно оказывается достаточно поддерживать в процессе разработки три уровня заголовочных файлов. Нижний уровень соответствует программным модулям. Промежуточный - функциональным областям (ФО). Функциональная область - это группа модулей, отвечающих за реализацию некоторой локальной функции системы, например расчет местоположения, планирование маневра, управление двигателем и т.п. Верхний уровень (обычно не более двух файлов) содержит определения типов и констант, общих для всей системы. В том числе в одном из файлов верхнего уровня могут вводиться принятые в проекте соглашения об именах (см. определения типа BITSET). Во втором - описания типов данных: структуры, перечислимые типы, именные константы, которые являются общими для всего проекта в целом.

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

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

    4.3. Ввод/вывод

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

    #include <stdio.h>  
        

    Так модулю пользователя (включившему описания стандартного ввода/вывода) становятся доступными определение типа FILE, процедуры потокового ввода и вывода данных fopen, putc, getc, putchar, getchar, fclose и ряд констант: NULL, EOF.

    Кроме того, импортирующий модуль может воспользоваться процедурами форматного преобразования printf, fprintf и sprintf, описания которых тоже включены в stdio. Процедуры printf и fprintf обеспечивают выполнение форматных преобразований при выводе в файл, а sprintf производит преобразование в форму выходной строки - своего первого параметра. Процедура printf не требует указывать имя файла вывода, так как осуществляет его стандартный выходной поток stdout.

    По умолчанию в программе связываются с терминалом пользователя три файла: входной - stdin, выходной - stdout и файл сообщений об ошибках - stderr. Все они относятся к потоковому вводу/выводу. При этом данные, переносимые из входного буфера (a) в зону обработки программы (b) и из зоны (b) в выходной буфер обмена (с), рассматриваются как непрерывная последовательность символов (рис. 4.1).

    (рис 4.1) Схема форматированного ввода/вывода

    На самом деле система ввода/вывода (вместе с исполнительной средой операционной системы компьютера) обменивается с внешними устройствами блоками данных, используя для этого системные области (буфера обмена). По мере потребности по запросам команд ввода программы данные из текущей точки буфера (а) переносятся в зону обработки в память программы (b). При этом среда поддержки ввода/вывода, реализованная в библиотеке stdio Си (например, процедуры scanf, fscanf), может выполнять форматные преобразования символов потока.

    Так последовательность символов "3" и "5" может быть перенесена в зону (b) без изменений в виде двух байт с кодом:

    0 0 1 1 0 0 1 1 0 0 1 1 0 1 0 1

    или может быть преобразована по числовому формату %2d к двоичному числу:

    0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1

    Т.е. в первом случае они остаются символами в кодировке ASCII, а во втором преобразуются в двоичное значение числа 35, записанного в прямом коде.

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

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

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

    Предположим, нам надо ввести три целых числа A, B и C. Очевидно, можно для этого использовать фрагмент программы:

    а)

    int A, B, C, Count;
    Count = scanf("%d%d%d", A, B, C) ;
        

    Значение переменной Count при успешном вводе будет равно 3 - количеству введенных переменных. Следовательно, проверяя далее значение Count, можно судить об удачности попытки ввода всех переменных (Count равно 3) или только части из них (Count равно 2 или 1).

    В ряде случаев удобнее решать вопрос по принципу "все или ничего". Тогда проще прибегнуть к чисто логической интерпретации результата ввода:

    б)

    Count = scanf("%d", A) scanf("%d", B)scanf("%d", C);  
        

    Значение Count, отличное от 0, будет говорить об успешности ввода всех значений. Заметим, что выражение

    в)

    Count = scanf("%d", A) + scanf("%d", B) +
    + scanf("%d", C);  
        

    по результату будет практически эквивалентно варианту (а) совместного ввода. Так, при вводе строки вида "12 13a 14bС" в случаях (а) и (в) значение переменной Count будет равно 2, параметры ввода A и B приобретут значение 12 и 13 соответственно, а переменная C останется без изменений.

    В случае (б) значение Count будет равно 0, хотя переменные A, B и C изменятся таким же образом, как в (а) и (в). В буфере обмена во всех случаях останутся символы "a 14b". Любая попытка последующего ввода данных по числовому формату будет натыкаться на нецифровой символ и терпеть неудачу.

    На самом деле похожие проблемы возникают в тех случаях, когда при вводе очередного значения, скажем k, возникла ошибка. Вместо входной строки "1 22 333" пользователь набрал "1 22 33w". В результате будут определены все три переменные - объекты ввода (A - 1, B - 22, C - 33), но во входном буфере останется символ "w". И любой последующий перенос данных из системного буфера в память программы будет начинаться именно с этого символа. Поэтому перед следующей операцией ввода системный буфер надо очистить.

    Для этой цели можно воспользоваться операцией чтения строки символов:

    Char Str[ 100] ;
      scanf("%s", Str);  
        

    которая введет оставшуюся в буфере последовательность символов в область переменной Str. А если их там нет? Если пользователь просто ввел строку "1 22 33"? Тогда буфер пуст и система ввода будет ожидать от недоумевающего пользователя ввода символьной строки.

    Более корректное решение - использование функций очистки буфера flush (); или fflush(stdin); которые очищают буфер обмена ото всех оставшихся в нем символов.

    Но определенные проблемы могут возникнуть при вводе произвольной последовательности символов. Например, если необходимо считать строку со стандартного потока ввода, то первое и самое простое - это использование функции scanf:

    #include <stdio.h>
    int main()
    {
      char Instr[100]; int Flag;
      printf("Input string:\n");
      Flag = scanf("%s", Instr);
      printf("Input=%s\n", Instr);
      return Flag;
    }  
        

    Но что произойдет, если будет введено больше 300 символов? В функции scanf контроля длины вводимой строки не происходит, она не знает, какого размера в программе переменная Instr. Получается, что все символы, не поместившиеся в массив Instr, будут записываться дальше в память, которая может использоваться другими переменными, в частности Flag. Эта ситуация называется нарушением границ массива.

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

    Как же поступать? У функции scanf в спецификаторе ввода можно задать ограничение на длину считываемой строки:

    scanf("%99s", Instr);  
        

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

    int i = 0;
    while ((*(Instr+i) = getc(stdin)) != '\n'  i++ < 99);
    * (Instr+i) = '\0';  
        

    Считывание происходит посимвольно, пока не встретится символ конца строки (символ '\n' ) или пока количество считанных символов не достигнет 99.

    Следует помнить, что в случае использования scanf с форматом %s строка будет считана только до пробела. Дело в том, что пробел, как и символ табуляции, считается разделителем, и многие функции работы со строками используют этот разделитель.

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

    fgets(Instr,99,stdin);  
        

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

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

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

    char c, ch[10]; int i;
    printf("Enter passsword, please\n");
    c = getch(); i = 0; /* get first character */
    while( c != '\015'  i < 9 ) /* end of input string */
    {
      ch[i++] = c; putchar('*');
      c = getch(); /* get next character */
    }
    ch[i] = '\0'; /* end string marker */
    printf("\nYour password is:%s\n",ch);  
        

    Цикл ввода продолжается, пока не достигнут символ конца строки (восьмеричный код 015) и есть место в массиве символов ch. При этом на экран выводится символ "*" в каждую введенную позицию. В конце оформленная символьная строка для наглядности выводится на экран.

    Еще более интересный прием можно рассмотреть на примере ввода восьмеричного числа.

    char c, ch[8]; int i, j;
    printf("Enter less then 7 octal digits, please\n");
    c = getch(); i = 0;
    while(c != '\015')/* end of input string */
    {
      if(c <= '7' c >= '0'  i < 7)
      {
        ch[i++] = c; putchar(c);
      }
      if(c == '\010' i > 0)/* back space */
      {
        putchar(c); putchar(' ');putchar(c);i--;
      }
      c = getch();
    }
    ch[i] = '\0';
    printf("\nYour number is:%s\n",ch);
    sscanf(ch, "%o", j);
    printf("Your integer value is:%d\n", j);  
        

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

    Дополнительно в программу введена проверка на символ возврата на одну позицию назад (BS - back space, восьмеричный код 010). При его вводе программа возвращается на одну позицию (i--) в массиве введенных символов ch и выводит сам символ BS, пробел на место ранее введенного символа и еще раз символ BS.

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

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

    В качестве еще одного примера рассмотрим чтение с клавиатуры и последующее кодирование символьной строки.

    /*******************************************************
    Date: 10 January 2013
    Description: string reading and encoding sample
    *******************************************************/
    #include <stdio.h>
    const int STARTD = 97; /* 'a' code */
    const int ENDD = 122; /* 'z' code */
    /******************************************************
    * Name : testString
    *
    * Purpose : checks whether string contains anything
    * except English small characters
    * Input : str (string to check)
    * Output : none
    * Return :
    * -1 bad string
    * 0 good string (only small English characters)
    *
    ******************************************************/
    int testString(char *str)
    {
      int i = 0;
      for(; *(str+i)!='\0'; i++)
      {
        if ( (*(str+i) < STARTD) || (*(str+i) > ENDD) )
        {
          return -1;
        }
      }
      return 0;
    }
    /*******************************************************
    * Name : replaceLineEnd
    *
    * Purpose : replaces ending '\n' (if exists)
    * symbol by '\0' symbol
    * except English small characters
    * Input : str (string)
    * Output : str (string)
    * Return : none
    ******************************************************/
    void replaceLineEnd(char *str)
    {
      int i;
      char temp[2];
      for(i=0; ((*(str+i)!='\n')  (*(str+i)!='\0')); i++);
      if ( *(str+i) == '\0' )
      {
        /* empty input buffer */
        while (temp[0] != '\n')
        {
          fgets(temp,2,stdin); /* reads 1 symbol + 1 end */
        }
      }
      if ( *(str+i) == '\n' )
      {
        *(str+i) = '\0';
      }
    }
    /*******************************************************
    * Name : readString
    *
    * Purpose : reads string from keyboard, and tests
    * whether it is a correct string
    * Input : str (empty string)
    * Output : str (inputted string)
    * Return :
    * -1 bad string
    * 0 good string
    ******************************************************/
    int readString(char *str)
    {
      /* reads only 11 chars from keyboard */
        fgets(str,12,stdin);
        replaceLineEnd(str);
        return testString(str);
    }
    /*******************************************************
    * Name : encodeAtbash
    * Purpose : encodes string str using Atbash code
    * Input : str (string)
    * Output : str (encoded string)
    * Return : none
    ******************************************************/  
    void encodeAtbash(char *str)
    {
    int tmp;
      while (*str)
      {
        if ( (int)*str >= STARTD  (int)*str <= ENDD )
        {
          tmp = ENDD + STARTD - (int)*str ;
          *str = (char)tmp;
        }
        str++;
      }
    }
    
    int main()
    {
      char str[12]; /* max length 11 + 1 for ending symbol */
        printf("Enter string to encode: ");
        if (readString(str) < 0)
        {
          printf("Incorrect string, exiting");
          return 0;
        }
      encodeAtbash(str);
      printf("Encoded string: %s",str);
      return 0;
    }
        

    В данном примере строка хранится в переменной str, для которой выделено 12 байт статической памяти. Выделение происходит при компиляции программы, так как переменная определена как массив char. Можно было бы задать str как указатель на char, в этом случае необходимо выделить память самостоятельно:

    str = (char*) malloc(12);  
        

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

    Из выделенных 12-ти байт один используется для хранения символа конца строки '\0' , для хранения символов самой строки остается 11 байт.

    Для ввода строки сконструирована функция readSnring, использующая в свою очередь библиотечную функцию fgets. Первым параметром fgets указывается область ввода данных (str), вторым – допустимый размер в байтах порции ввода, а третьим задается потоковый файл, откуда производится ввод данных. Таким образом, размер массива str превышен не будет, даже если пользователь введет более 11 символов.

    Дополнительная функция replaceLineEnd обеспечивает очистку буфера входной строки и подготовку переменной str для дальнейшей работы. Она проверяет, чем закончилась процедура считывания входного потока. Если в строку был перенесен символ '\n' – признак конца строки потокового файла, то он заменяется на '\0' – признак конца символьной строки в Си.

    Кроме того, в процедуре replaceLineEnd происходит дочитывание строки потокового файла до '\n', если пользователь набрал слишком длинную строку:

    if ( *(str+i) == '\0' )
    {
      /* empty input buffer */
      while (temp[0] != '\n')
      {
        fgets(temp,2,stdin); /* reads 1 symbol + 1 end */
      }
    }  
        

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

    Для кодирования строки выбран алгоритм "Атбаш". Упоминание об его использовании встречается в Библии. Книга пророка Иеремии, глава 25, стих 26 содержит текст: "И всех царей севера, близких друг к другу и дальних, и все царства земные, которые - на лице земли, а царь Сесаха выпьет после них". Слово "Сесах" не является ни ошибкой, ни искажением библейского текста, хотя такого царя или царства не существовало. Священные тексты древних иудеев шифровались шифром простой замены "Атбаш". Алгоритм этого шифра прост: первая буква алфавита заменялась на последнюю, вторая - на предпоследнюю в алфавите и т.д. После дешифрации на языке оригинала (для успешной дешифрации необходимо знать язык сообщения) слова "Сесах" получается "Вавилон". По смыслу алгоритма функция, реализующая шифровку и зашифровку, одна и та же.

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

    В функции кодирования encodeAtbash для доступа к символам строки используется синтаксис работы с указателем. Чтобы получить текущий символ, используется выражение *str. Для перехода к следующему символу выполняется str++. Можно реализовать эту функцию иначе с использованием механизма работы с массивом:

    void encodeAtbash(char *str)
    {
      int tmp;
      int i;
      i = 0;
      while (str[i])
      {
        if ( (int)str[i] >= STARTD  (int)str[i] <= ENDD )
        {  
          tmp = ENDD + STARTD - (int)str[i];
          str[i] = (char)tmp;
        }
        i++;
      }
    }
        

    Более того, даже параметр функции можно указать не как ссылку на char, а как "открытый" массив:

    void encodeAtbash(char str[ ] )  
        

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

    void encodeAtbash(char str[], int arrayLength)
      {
        int i;
        for (i = 0; i < arrayLength; i++)
        {
          . . .
        }
      . . .
    }  
        

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

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

    /*******************************************************
    Date: 10 January 2013
    Description: string reading and encoding sample 2
    *****************************************************/
    #include <stdio.h>
    /*******************************************************
    * Name : isEnglishLetter
    *
    * Purpose : сhecks whether the symbol belongs to
    * English alphabet
    * Input : c - symbol
    * Output : none
    * Return : 1 – belongs, 0 – do not belongs
    *
    *****************************************************/
    int isEnglishLetter(char c)
    {
      if( (c >= 'a')  ( c <= 'z' ) )
      {
        return 1;
      }
      if ( (c >= 'A')  (c <= 'Z') )
      {
        return 1;  
      }
      return 0;
    }
    /*******************************************************
    * Name : isRussianLetter
    *
    * Purpose : checks whether the symbol belongs to
    * Russian alphabet
    * Input : c - symbol
    * Output : none
    * Return : 1 – belongs, 0 – do not belongs
    ******************************************************/
    int isRussianLetter(char c)
    {
      if( (c >= 'а')  ( c <= 'я' ) )
      {
        return 1;
      }
      if ( (c >= 'А')  (c <= 'Я') )
      {
        return 1;
      }
      return 0;
    }
    /*******************************************************
    * Name : isNumber
    *
    * Purpose : checks whether the symbol is number
    * Input : c - symbol
    * Output : none
    * Return : 1 – number, 0 – not a number
    ******************************************************/
    int isNumber(char c)
    {
      if( ((int)c >= (int)'0')  ( (int)c <= (int)'9' ) )
      {
        return 1;
      }
      return 0;
    }
    /*******************************************************
    * Name : isDelimeter
    *
    * Purpose : checks whether the symbol is a delimiter
    * Input : c - symbol
    * Output : none
    * Return : 1 – delimiter, 0 – not a delimiter
    ******************************************************/
    int isDelimeter(char c)
    {
      if( (c == ',') || (c == '.') || (c == ' ') || (c == '\0') )
      {
        return 1;
      }   else
        {
          return 0;
        }
    }
    /*******************************************************
    * Name : testString
    *
    * Purpose : test string for correctness
    * (according requirements)
    * Input : str (string to check)
    * Output : none
    * Return :
    * -1 bad string
    * 0 good string (only acceptable characters)
    ******************************************************/
    int testString(char *str)
    {
      int start;
      int goodSymb;
      int count;
      count = 0;
      start = 0;
      while(*str)
      {
        goodSymb = 0;
        if (isEnglishLetter(*str))
        {
          start = 1;
          goodSymb = 1;
        }
        if (isRussianLetter(*str))
        {
          start = 1;
          goodSymb = 1;
        }
        if (isNumber(*str))
        {
          start = 1;
          goodSymb = 1;
        }
        if ( (*str) == ' ' )
        {
          goodSymb = 1;
        }
        if ( (isDelimeter(*str))  start )
        {
          goodSymb = 1;
        }
        if (goodSymb == 0)
        {
          return 0;
        }   else
        {
          str++;
          count++;
        }
      }
      if (count > 10)
      {
        return 0;
      } else
      {
        return 1;
      }
    }
    /*******************************************************
    * Name : replaceLineEnd
    * Purpose : replaces ending '\n' (if exists)
    * symbol by '\0' symbol
    * except English small characters
    * Input : str (string)
    * Output : str (string)
    * Return : none
    ******************************************************/
    void replaceLineEnd(char *str)
    {
      int i;
      char temp[2];
      for(i=0; ( (*(str+i)!='\n')  (*(str+i)!='\0') ); i++);
      if ( *(str+i) == '\0' )
      {
        /* empty input buffer */
        while (temp[0] != '\n') \
        {
        fgets(temp,2,stdin);
        }
      }
      if ( *(str+i) == '\n' )
      {
        *(str+i) = '\0';
      }
    }
    /*******************************************************
    * Name : readString
    *
    * Purpose : reads string from keyboard
    * Input : str (empty string)
    * Output : str (inputted string)
    * Return : none
    ******************************************************/
    void readString(char *str)
    {
      /* reads only 81 chars from keyboard, 82 sets to '\0' */
      fgets(str,82,stdin);
      replaceLineEnd(str);
    }
    /*******************************************************
    * Name : change231
    * Purpose : ncodes word in string str using 231 exchange
    * Input : str (string),
    * startPos – position in string
    * where the word starts from
    * Output : str (encoded string)
    * Return : encoded word end position in str
    ******************************************************/
    int change231(char *str, int startPos)
    {
      int i;
      char temp;
      i = startPos + 1;
      while (!isDelimeter(str[i]))
      {
        temp = str[i];
        str[i] = str[i-1];
        str[i-1] = temp;
        i++;
        if (((i-startPos)%3 == 0)  (!isDelimeter(str[i])))
        {
          i++;
        }
      }  
      return i;
    }
    /******************************************************
    * Name : processString
    * Purpose : encode words in string
    * leaving delimiters unchanged
    * Input : str (string),
    * Output : str (encoded string)
    * Return : none
    ******************************************************/
    void processString(char *str)
    {
      int i;
      i = 0;
      while( *(str+i) != '\0' )
      {
        if (isDelimeter(*(str+i)))
        {
          i++;
        } else
        {
          i = change231(str, i);
        }
      }
    }
    int main()
    {
      /* max length 80 + 1 for ending symbol
        + 1 to test >80 entered symbols */
      char str[82];
      printf("Введите строку для кодирования\n");
      readString(str);
      while(*str != '\0')
      {
        if (testString(str) == 0)
        {
          printf("Ошибка во входной строке\n");
        } else
        {
          processString(str);
          printf("%s\n", str);
        }
          printf("Введите строку для кодирования\n");
          readString(str);
      }
      printf("Работа закончена");
      return 0;
    }
        

    Проверка корректности строки выделена в функцию testString, которая последовательно рассматривает каждый символ строки и проверяет, принадлежит ли он английскому алфавиту и/или русскому и/или является цифрой и/или разделителем. В начале допускаются в качестве разделителей только пробелы, поэтому используется дополнительная переменная start. Исходно она равна нулю, и только после появления в строке символа алфавита и/или цифры она становится равна единице. Пока start равна нулю, допускается только пробел в качестве разделителя:

    if ( (*str) == ' ' )
    {
      goodSymb = 1;
    }  
        

    а когда start равна единице, допускается любой разделитель (точка, запятая, пробел):

    if ( (isDelimeter(*str))  start )
    {
      goodSymb = 1;
    }  
        

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

    Сама перестановка символов выполнена последовательно попарно. Требуемый результат - 231 получается, если сначала переставить первый символ со вторым (получив 213) и далее второй с третьим (получив как раз 231). Такой подход оказывается справедлив и в случае, когда остается два символа (12 - 21), т.е. алгоритм перестановки сводится к работе лишь с двумя рядом стоящими символами (перемене их мест). Однако требуется перестановка только в рамках тройки символов, поэтому на каждом третьем шаге следует пропустить одну позицию:

    if (((i-startPos)%3 == 0)  (!isDelimeter(str[i])))
    i++;  
        

    Алгоритм основной программы уже был определен ранее.

  • Ввести входную строку.
  • Если строка пустая, то закончить работу.
  • Преобразовать входную строку.
  • Вывести преобразованную строку.
  • Ввести очередную входную строку и вернуться к действию (2).
  • Соответственно будет выглядеть и реализация.

  • Ввести входную строку:
    printf("Введите строку для кодирования\n");
    readString(str);  
            
  • Если строка пустая, то закончить работу:
    while(*str != '\0')
    {
    . . .
    }
    printf("Работа закончена");
    return 0;  
            
  • Преобразовать входную строку:
    if (testString(str) == 0)
    {
      printf("Ошибка во входной строке\n");
    } else
    {
      processString(str);
      printf("%s\n", str);
    }  
            
  • Вывести преобразованную строку:
    processString(str);
    printf("%s\n", str);  
            
  • Ввести очередную входную строку и вернуться к действию (2):
    while(*str != '\0')
    {
      ...
      printf("Введите строку для кодирования\n");
      readString(str);
    }  
            
  • Вопросы и задачи для самостоятельного решения

  • Как можно задать числовую константу в Си?
  • Сколько необходимо создать файлов, чтобы реализовать модуль в Си?
  • Зачем нужны заголовочные файлы в Си?
  • В каких случаях возможно переполнение буфера при вводе строки? Какими способами можно этого избежать?
  • Напишите программу перевода десятичного числа в шестнадцатеричное. Входное число должно считываться из стандартного потока ввода (клавиатура). Перевод должен работать для чисел длиной не менее 100 цифр.
  • Напишите программу кодирования слова методом перестановки символов 3-1-2. Входное слово должно считываться из стандартного потока ввода (клавиатура). Кодирование должно работать для слов длиной не менее 300.
  • Вернуться к учебному плану