КаталогИндекс раздела
НазадОглавлениеВперед


Лабораторная работа ╧12

Указатели, символьные строки и функции

1. Цель работы

     Целью лабораторной работы является совершенствование практических навыков в работе с указателями и получение навыков в работе с функциями пользователя.

2. Теми для предварительной проработки

3. Задания для выполнения

     Разработать функцию, которая выполняет ту обработку символьной строки, которая определена в Вашем индивидуальном задании. При реализации функции запрещается пользоваться функциями библиотек языка C.

     Примечания:

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

4. Варианты индивидуальных заданий

 1        2        3        4        5        6        7        8        9       10      
11       12       13       14       15       16       17       18       19       20      
21       22       23       24       25       26       27       28       29       30      

5. Пример решения задачи (вариант 30)

5.1. Разработка спецификаций функции.

     Дадим функции, которую мы создаем, имя substr(). Состав и типы ее параметров достаточно просто могут быть определены из задания: это должны быть строка-источник данных (src), строка-результат (dest), начальная позиция (pos) и длина результата (len).

     Строки должны быть определены в той функции, которая вызывает нашу, следовательно, функции substr() передаются указатели на эти строки. Остальные параметры должны быть целыми числами. Обратите внимание на наши соображения по поводу размещения символьных строк. Вполне понятно, что память для строки-источника должна быть выделена во внешней функции. Но то же самое мы предусматриваем и для строки-результата - почему? Если мы объявим строку-результат как локальную в функции substr(), то память для нее будет выделена в нашей функции, и эта память будет освобождена, когда выполнение функции закончится, следовательно, та функция, которая вызвала нашу, воспользоваться результатом не сможет. Если же мы выделим память в нашей функции явным образом (с помощью malloc()), то память сохранится, но тогда на ту функцию, которая вызвала нашу, возлагается обязанность явным образом освободить эту память, когда строка-результат уже не будет нужна. Проанализируем возможности некорректного задания параметров при вызове функции. Во-первых, параметры pos и len не могут иметь отрицательного значения - оно просто не имеет смысла. Во-вторых, возможно такое значение параметра pos, которое будет превышать длину строки-источника. Будем считать все эти случаи ошибочным заданием параметров. Что должна делать функция при ошибочном задании параметров? Можно предложить три варианта:
     1. Функция аварийно завершает работу всей программы.
     2. Функция никак не реагирует на ошибку.
     3. Функция возвращает какой-то признак ошибки.

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

     Установим, что это значение будет 0 при нормальной работе функции, а при ошибке в параметрах это значение будет -1, а строка-результат будет пустой.

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

     Еще одна проблема: если память для результата выделяет внешняя функция, что делать, если длина результата будет больше объема выделенной памяти? Чтобы контролировать эту ситуацию нужно ввести еще один параметр функции - максимальную длину строки-результата, а это нежелательно. Примем решение не контролировать такую ситуацию, перекладывая ответственность за нее на ту функцию, которая вызывает нашу. Это решения базируется на том, что именно так поступают и библиотечные функции языка С.

     В итоге разработки спецификации для функции мы формулируем такое описание функции substr():

    int substr(
      char *src,
      char *dest,
      int pos,
      int len);

5.2. Разработка алгоритма решения.

     Мы разрабатываем алгоритм только для функции substr(), игнорируя внешнюю функцию, которая вызывает ее.

     При разработке алгоритма мы сразу же "заглядываем вперед", имея в виду его последующую реализацию в программном коде, т.к. уже в задании обусловлены некоторые детали реализации.

     Функция начинается с цикла (блоки 2 -5), цель которого - установить указатель на символ с номером pos. Как параметр цикла используется параметр функции pos, который "работает на уменьшение". В каждой итерации цикла pos уменьшается на 1 (блок 5), а указатель src увеличивается на 1 (блок 4). Когда pos уменьшится до 0 (блок 3) - указатель должен быть установлен на нужный символ. Но есть возможность того, что мы достигнем конца строки раньше, чем символа с номером pos. Эта возможность проверяется отдельно (блок 2) - не показывает ли src на символ с кодом 0 - признак конца строки. Следовательно, выход из цикла возможен либо по достижению нужного символа (блок 3), либо по достижению конца строки, причем, последнее возможно только при некорректном задании параметров. После выхода из цикла проверяется корректность задания параметров: не задана ли отрицательная длина подстроки (блок 6) и не выходит ли pos за пределы строки (блок 7). Отметим, что если значение pos ошибочно задано отрицательным, то цикл блокЁв 2 - 5 не выполнится ни одного раза и в блоке 7 значение pos будет ненулевым. Оно также будет ненулевым, если pos превышает длину строки-источника. В любом случае некорректного задания возвращаемое значение ret устанавливается в -1 (блок 8), и управление переходит на завершение функции.

     Если же параметры заданы корректно, выполняется второй цикл (блоки 9 - 13). Этот цикл имеет параметром параметр функции len, который тоже " работает на уменьшение ". В каждой итерации символ, на который показывает указатель src, пересылается туда, куда показывает указатель dest (блок 11) после чего оба эти указателя увеличиваются на 1 (блок 12), а len уменьшается на 1. Когда значение len достигнет 0, это означает, что из источника в результат переслано уже len символов и происходит выход из цикла (блок 10). Другая возможность выхода - если будет найден конец строки прежде, чем закончится пересылка (блок 9). После выхода из цикла проверяется остаток в переменной len (блок 14). Если он нулевой, значение для ret устанавливается в 0 (блок 16), если нет - это означает, что переслано меньшее количество символов, и значение ret устанавливается в 1 (блок 17).

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

5.3. Функция substr(). Текст программы.

     Заголовок функции substr() полностью соответствует ее описанию, сформулированному при разработке спецификаций функции. В функции объявляется только одна локальная переменная - ret - в которой формируется то значение, которое возвращает функция.

     Цикл, который на схеме алгоритма представлен блоками 2 - 5, реализован одним оператором:

    for(; pos&&*src; pos--, src++);

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

    pos&&*src, 
что эквивалентно:
    (pos==0)&&(*src=0), 
в конце каждой итерации уменьшается pos и увеличивается src. Тело этого цикла пустое.

     Единственный условный оператор:

    if (pos||(len<0)) ret=-1;
является программной реализацией проверок некорректного задания параметров (блоки 6 - 8).

     Если параметры корректны, следующий оператор цикла выполняет пересылку символов из источника в результат (блоки 9 - 13):

    for(;len&&*src; *dest++=*src++,len--);
Этот цикл тоже имеет пустое тело, ибо все действия, которые нужно в нем выполнять заданы в заголовке цикла.

     После цикла проверяется остаток и устанавливается значение ret (блоки 14 - 16):

    ret = len ? 1 : 0;

     Перед возвратом еще записывается признак конца строки (символ с кодом 0) в результат (блок 17):

    *dest=0;

     При завершении функция возвращает (блок 18) значение ret:

    return ret;

5.4. Функция main()

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

     5.4.1. Переменные функции main()
     В функции main() должны быть объявлены переменные для:

     Пользуясь случаем, сделаем небольшое отступление, чтобы предупредить Вас про возможность ошибки, которую часто допускают начинающие программисты. Зная, что обращение к символьным строкам в языке C происходит через указатель на начало строки, такие программисты иногда объявляют символьную строку как char *. Но такое объявление, например:

   char *s1;
выделяет память только для размещения указателя, но не для размещения самих символов строки. Если далее мы введем, например, 80 символов функцией gets(s1), то символы разместятся там, куда показывает указатель s1, но значение этого указателя не определено, следовательно, и символы разместятся неизвестно где. Следовательно, символьная строка обязательно должна быть объявлена как массив символов - этим выделяется для нее память, а уже обращаться к ней можно через указатель.

     Не следует также забывать про необходимость резервировать в строке дополнительную позицию для признака конца строки.

     5.4.2. Текст функции main()
     После объявления переменных текст функции main() состоит из единственного бесконечного цикла. В каждой его итерации прежде всего выводится приглашение не ввод строки-источника. Следующий оператор, возможно, требует более детального рассмотрения:

    if (!strcmp(gets(s1),"***")) break;
При его выполнении прежде всего выполняется функция gets(s1), которая вводит данные в строку s1. Эта функция возвращает указатель на строку s1. Строка, на которую показывает этот указатель, сравнивается с помощью функции strcmp() со строковой константой "***". Если они равны, strcmp() возвращает 0, тогда значение логического выражения в условном операторе истинное и выполняется выход из бесконечного цикла. Следовательно, цикл будет выполняться, пока не будет введена строка "***".

     Потом вводятся значения переменных n и l и выполняется вызов функции substr(), которой передаются параметры s1, s2, n, l. Значения, которое возвращает substr(), присваивается переменной r.

     Строка-источник, строка-результат и возвращенное значение выводятся на экран, и цикл повторяется. (Обратите внимание на то, что при выводе символьных строк мы берем их в "скобки": >> <<. Это сделано для того, чтобы на экране можно было разглядеть символы-пробелы, которые могут быть в начале и в конце строк.)

     Обращение к функции gets() в конце цикла - "технологическое". Дело в том, что функция scanf() оставляет в буфере ввода последний код клавиши Enter, которым был закончен ввод. Если не будет "технологического" gets(), то gets() в следующей итерации цикла прочитает этот символ, как пустую строку. Так что "технологическое" gets() удаляет из буфера код клавиши Enter. Чтобы убедиться, попробуйте его убрать и посмотрите, что получится.

     5.4.3. Общие объявления
     В функции main() применяются библиотечные функции ввода-вывода и функция сравнения строк, так что включим в программу файлы stdio.h и string.h. Кроме того, следует включить и описание функции substr(). Хотя все эти описания нужны только для функции main(), стиль программирования на языке C требует размещения их в начале программы, вне программных блоков.

     Полный текст программы приведен ниже.

/****************************************************/
/*             Лабораторная работа ╧12              */
/*      Указатели, символьные строки и  функции     */
/*         Приклад выполнения. Вариант ╧30.         */
/****************************************************/
#include <stdio.h>
#include <string.h>
int substr(char *, char *, int, int);
/*** главная функция ***/
int main(void) {
  char s1[80],s2[80]; /* источник и результат */
  int n, l, r; /* позиция, длина, результат */
  for (;;) {   /* бесконечный цикл */
     printf("Введите строку >");
     /* ввод строки-источника */
     if (!strcmp(gets(s1),"***")) break;
     /* ввод остальных параметров */
     printf("Введиту pos len>");
     scanf("%d %d",&n,&l);
     /* обращение к функции substr() */
     r=substr(s1,s2,n,l);
     /* вывод результатЁв */ 
     printf("pos=%d, len=%d\n",n,l);
     printf("s1=>>%s<<\n",s1);
     printf("s2=>>%s<<\n",s2);
     printf("R=%d\n\n",r);
     gets(s1);
     }
}
/*** функция выделения подстроки ***/
/* параметры: 
     src - строка-источник
     dest - строка-результат
     pos - позиция, с которой выделяется подстрока
     len - длина подстроки
   функция возвращает: 
     0 - нормальное выполнение
     1 - подстрока имеет меньшую длину, чем задано
    -1 - ошибка в параметрах, результат пустой */
int substr(char *src, char *dest, int pos, int len) {
 int ret; /* значение, которое возвращается */
  /* выход на начальную позицию */
  for(; pos&&*src; pos--, src++);
  /* проверка параметров */
  if (pos||(len<0)) ret=-1;
  else {    /* параметры корректны */
    /* пересылка символов */
    for(;len&&*src; *dest++=*src++,len--);
    /* проверка длины результата */
    ret = len ? 1 : 0;
    }
  /* запись признака конца в результат */
  *dest=0;
  return ret;
}

5.5. Отладка программы

     При отладке программы в пошаговом режиме следует отслеживать:

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

     Очень важно подобрать для отладки такие входные данные, которые позволили бы убедиться в правильном функционировании программы на всех ветвях ее алгоритма. Мы при отладке этой работы использовали во всех случаях строку-источник вида: "0123456789". Такой вид позволяет легко установить по выводу на экран, какое преобразование было выполнено функцией. Параметры pos и len мы задавали в таких вариантах:

pos=2, len=3
- проверка правильности работы при правильных заданиях параметров
pos=0,len=4
- подстрока начинается с начала строки
pos=6, len=4
- подстрока заканчивается на конце строки
pos=0, len=10
- подстрока захватывает всю строку
pos=3, len=0
- подстрока нулевой длины
pos=8, len=4
- особый случай: подстрока будет иметь меньшую длину, чем задано
pos=3, len=-2
- ошибка: отрицательная длина
pos=-1, len=3
- ошибка: отрицательная начальная позиция
pos=10, len=3
- ошибка: позиция начала больше длины источника

5.6. Результаты работы программы

     Результаты работы программы при приведенных выше входных данных такие:

pos=2, len=3
s2=>>0123456789<<
s1=>>234<<
R=0

pos=0, len=4
s2=>>0123456789<<
s1=>>0123<<
R=0

pos=6, len=4
s2=>>0123456789<<
s1=>>6789<<
R=0

pos=0, len=10
s2=>>0123456789<<
s1=>>0123456789<<
R=0

pos=3, len=0
s2=>>0123456789<<
s1=>><<
R=0

pos=8, len=4
s2=>>0123456789<<
s1=>>89<<
R=1

pos=3, len=-2
s2=>>0123456789<<
s1=>><<
R=-1

pos=-1, len=3
s2=>>0123456789<<
s1=>><<
R=-1

pos=10, len=3
s2=>>0123456789<<
s1=>><<
R=1

5.7. Выводы

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


КаталогИндекс раздела
НазадОглавлениеВперед