5  Структурное программирование

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

Используя язык высокого уровня (такой как Фортран) программисты  могли писать программы до несколько тысяч строк длиной. Однако язык программирования, легко понимаемый в коротких программах, когда дело касается больших программ, становится нечитабельным (и неуправляемым). Избавление от таких неструктурированных программ пришло после создания в 1960 году языков структурного программирования. К ним относятся языки Алгол, Паскаль и С.

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

Структурное программирование тесно связано такими понятиями как «нисходящее проектирование» и «модульное программирование».

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

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

Функциональная структура алгоритма приложения разрабатыается в следующей последовательности:

1) определяются цели автоматизации предметной области и их иерархия;

2) устанавливается состав приложений (задач обработки), обеспечивающих реализацию поставленных целей;

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

4) определяются необходимые для решения задач функции обработки данных;

5) выполняется декомпозиция функций обработки до необходимой структурной сложности, реализуемой предполагаемым инструментарием.

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

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

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

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

· линейной (следование);

· нелинейной (развилка);

· циклической (цикл, или повторение).

Эта теорема была сформулирована в 1966 г. Боймом и Якопини (Corrado Bohm, Guiseppe Jacopini). Главная идея теоремы – преобразовать каждую часть программы в одну из трех основных структур или их комбинацию так, чтобы неструктурированная часть программы уменьшилась. После достаточного числа таких преобразований оставшаяся неструктурированной часть либо исчезнет, либо становится ненужной. В теореме доказывается, что в результате получится программа, эквивалентная  исходной  и  использующая  лишь упоминавшиеся основные структуры.

Комбинации правильных программ, полученные с использованием этих трех основных структур, также являются правильными программами. Применяя итерацию и вложение основных структур, можно получить программу любого размера и сложности. При использовании только указанных структур отпадает необходимость в безусловных переходах и метках. Поэтому иногда структурное кодирование понимают в узком смысле как программирование без «GOTO».

В алгоритмическом языке С (С++) для реализации структурного кодирования используются следующие операторы:

· составной-оператор;

· оператор-выражение;

· оператор-выбора;

· оператор-с-меткой;

· оператор-перехода;

· оператор-итерации;

· asm-оператор;

· объявление (только в С++).

Структура «следование» (рис. 5.1, а) реализуется составным оператором, оператором-выражение, asm-оператором и др.

Составной оператор, или блок, представляет собой список (возможно, пустой) операторов, заключенных в фигурные скобки {…}. Синтаксически блок рассматривается как единый оператор, но он влияет на контекст идентификаторов, объявленных в нем. Блоки могут иметь любую глубину вложенности.

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

<выражение>;

Компилятор языка C++ выполняет операторы-выражения, вычисляя выражения. Все побочные эффекты от этого вычисления завершаются до начала выполнения следующего оператора. Большинство операторов-выражений представляют собой операторы присваивания или вызовы функций (например, printf(), scanf() ). Особым случаем является пустой оператор, состоящий из одной точки с запятой (;). Пустой оператор не выполняет никаких действий. Однако он полезен в тех случаях, когда синтаксис C++ ожидает наличия некоторого оператора, но по программе он не требуется (например, бесконечный цикл for ).

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

Структура «развилка» (рис. 5.1, б, в) реализуется операторами выбора. Операторы выбора, или операторы управления потоком, выполняют выбор одной из альтернативных ветвей программы, проверяя для этого определенные значения. Существует два типа операторов выбора: if…else и switch.

Базовый оператор if (рис. 5.1, б) имеет следующий формат:

if(условное_выражение)оператор_если_"истина"<else>оператор_если_"ложь";

Рис. 5.1. Структуры, используемые при разработке программ:

а – «следование»; б – «развилка» на две ветви; в – «развилка» на n ветвей; г – «повторение»

Язык C++ в отличие от, например, языка Паскаль не имеет специального булевого типа данных. В условных проверках роль такого типа может играть целочисленная переменная или указатель на тип. Условное_выражение должно быть записано в круглых скобках. Это выражение вычисляется. Если оно является нулевым (или пустым в случае типа указателя), мы говорим, что условное_выражение ложно (false); в противном случае оно истинно (true).

Если предложение else отсутствует, а условное_выражение дает значение "истина", то выполняется оператор_если_"истина"; в противном случае он игнорируется.

Если задано предложение <else> оператор_если_"ложь", а условное_выражение дает значение "истина", то выполняется оператор_если_"истина"; в противном случае выполняется оператор_если"ложь".

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

if (!ptr)… или if (ptr = = 0)….

Оператор_если_"ложь" и оператор_если_"истина" сами могут являться операторами if, что позволяет организовывать любую глубину вложенности условных проверок. При использовании вложенных конструкций if…else следует быть внимательным и обеспечивать правильный выбор выполняемых операторов. Любая неоднозначность конструкции "else" разрешается сопоставлением else с последним найденным на уровне данного блока if без else.

Например, запись:

if (x == 1)

if (y == 1) puts("x=1 и y=1");

else puts("x != 1");

дает неверное решение, так как else, независимо от стиля записи, сопоставляется не с первым, а со вторым if. Поэтому правильная запись последней строчки должна быть такой:

else puts("x=1 и y!=1");

Однако с помощью фигурных скобок можно реализовать и первую конструкцию:

if (x = = 1)

{

if (y = = 1) puts("x = и y=1");

}

else puts("x != 1");  // правильное решение

Оператор switch (см. рис. 5.1, в) использует следующий базовый формат:

switch (переключающее_выражение) case_оператор;

Он позволяет передавать управление одному из нескольких операторов с меткой case в зависимости от значения переключающего_выражения. Любой оператор в case_операторе (включая пустой оператор) может быть помечен одной (или более) меткой варианта:

case константное_выражение_i : case_оператор_i;

где каждое константное_выражение_i должно иметь уникальное целочисленное значение (преобразуемое к типу переключающего_выражения) в пределах объемлющего оператора switch.

Допускается иметь в одном операторе switch повторяющиеся константы case.

Оператор может иметь также не более одной метки default:

default : оператор_умолчания;

После вычисления переключающего_выражения выполняется сопоставление результата с одним из константных_выражений_i. Если найдено соответствие, то управление передается case_оператору_i с меткой, для которой найдено соответствие. Если  соответствия не найдено и имеется метка default, то управление передается оператору_умолчания. Если соответствие не найдено, а метка default отсутствует, то никакие операторы не выполняются. Для того чтобы остановить выполнение группы операторов для конкретного варианта, следует использовать оператор break.

Пример 1

#include<stdio.h>

void main(void)

{

enum color {red,yellow,green};

int col=0;

switch(col)

{

case red:      puts("Stop! Color is red"); break;

case yellow: puts("Warning! Color is yellow"); break;

case green:  puts("Go! Color is green"); break;

default:        puts("No working"); break;

}

}

Структура «повторение» (см. рис. 5.1, г) реализуется операторами итерации. Операторы итерации позволяют организовывать циклическое выполнение набора операторов программы. Язык C++ имеет три формы операторов итерации: циклы while, do и for. Общий формат оператора while следующий:

while (условное_выражение) оператор_пока_"истина";

Оператор_пока_"истина" будет циклически повторяться до тех пор, пока вычисление условного_выражения не даст значения ноль (ложь). Условное_выражение вычисляется и проверяется первым. Если это значение ненулевое (истина), то выполняется оператор_пока_"истина". Если не встречен оператор перехода, выполняющий выход из цикла, то условное_выражение вычисляется снова. Цикл повторяется до тех пор, пока условное_выражение не даст значения 0.

Как и в случае оператора if, выражения типа указателя могут сравниваться с пустым указателем, так что while (ptr) … эквивалентно while (ptr != NULL)…

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

char str[10]="Borland";

char *ptr=&str[0];

int count=0;

//…

while (*ptr++)  // цикл до конца строки

   count++;

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

Общий формат оператора do while следующий:

do выполняемый_оператор while (условное_выражение);

Выполняемый_оператор циклически повторяется до тех пор, пока вычисление условного_выражения не даст 0 (ложь). Главное отличие этого оператора от оператора while состоит в том, что условное_выражение здесь проверяется не до, а после первого выполнения тела цикла. Гарантировано как минимум одно его выполнение.

Формат оператора for в языке С следующий:

for(<выражение_инициализации>;<выражение_проверки>;

<выражение_инкремента>) оператор;

В языке С++ <выражение_инициализации> может быть как выражением, так и объявлением.

Последовательность действий при выполнении данного оператора следующая:

1) выполняется вычисление выражения_инициализации, если таковое задано. Как следует из его названия, это выражение обычно инициализирует один или несколько счетчиков цикла, но его синтаксис в действительности позволяет любую степень сложности (включая объявления в случае языка С++);

2) выражение_проверки вычисляется по правилам, приводимым для циклов while. Если выражение_проверки ненулевое (истина), то оператор тела цикла выполняется. Пустое выражение трактуется в данном случае как while(1), то есть, как если бы условие проверки выполнялось всегда. Если выражение_проверки дает значение ноль (ложь), то цикл for прекращается;

3) выражение_инкремента выполняет приращения одного или нескольких цикловых счетчиков;

4) выражение "оператор" (возможно, пустое) вычисляется, после чего управление возвращается на шаг 2.

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

for (;;) {           

// то же, что и for (;1;) – вечный цикл

}

Правила для операторов for в языке С применимы и в языке С++. Однако выражение_инициализации в языке С++ может также включать в себя и объявление. Контекст объявленного идентификатора продолжается до конца данного оператора, не далее. Например:

for (int i = 1; i < j; ++i)

{

if (i …) …    // здесь обращаться к значению i можно

}

if (i …)               // а здесь нельзя : i вне контекста

Использование операторов итерации рассмотрим позже.

При изменении порядка выполнения операторов используются операторы перехода, реализуемые в структурах «развилка» и «повторение». Существует четыре таких оператора: break, continue, goto и return.

Синтаксис оператора break следующий:

break;

Его можно использовать только внутри операторов итерации (while, do и for – циклы) или с оператором switch. Он прекращает выполнение итерации или оператора switch. Поскольку операторы итерации и оператор switch могут комбинироваться и иметь любую глубину вложенности, следует обращать особое внимание на то, чтобы оператр break выполнял выход именно из нужного цикла или оператора switch. Сущеествует правило, которое состоит в том, что оператор break заканчивает выполнение ближайшего к нему объемлющего цикла итерации или оператора switch.

Синтаксис оператора continue следующий:

continue;

Он может быть использован только внутри оператора итерации и передает управление на проверку условия циклов while и do, либо на выражение инкремента цикла for. При вложенности циклов итерации оператор continue считается принадлежащим ближайшей объемлющей итерации.

Синтаксис оператора goto следующий:

goto метка;

Оператор goto передает управление оператору, имеющему указанную «метку»,  которая должна находиться в пределах той же функции.

Оператор с меткой имеет имя (метку), поэтому к нему можно обращаться с помощью оператора безусловного перехода goto. Формат оператора с меткой:

Идентификатор_метки : оператор;

Идентификатор метки используется оператором goto безусловного перехода. Идентификаторы меток имеют свое собственное пространство имен в контексте функции. Язык С++  дает возможность устанавливать метки любому oператору. Например, бесконечный цикл

for(;;)

{

printf("nУкажите значение N (меньше 32766) > ");

scanf("%d",&N);

if(N<32766) break;

}

можно записать с использованием метки следующим образом:

met: printf("nУкажите значение N (меньше 32766) > ");

scanf("%d",&N);

if(N>32766) goto met;

В языке С++ допустимо обойти объявление с явным или неявным инициализатором, если это объявление не находится во внутреннем блоке, который также обходится.

Если тип возврата функции не равен void, то тело функции должно содержать как минимум один оператор return следующего формата:

return выражение_возврата;

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

Выполнение вызова функции заканчивается, когда встретился оператор return; если оператор return отсутствует, то выполнение «проваливается» к последней закрыващей фигурной скобке тела функции.

Если тип возврата void, то оператор return можно записать без выражения_возврата, либо оператор return вообще может быть опущен, например:

{

return;

}

Наконец, в языке С++ можно использовать объявления  объектов внутри блоков, что дает возможность более рационально расходовать оперативную память ЭВМ.

Пример 2

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

При разработке алгоритма программы перечислим объекты, необходимые для решения данной задачи:

· N – заданное пользователем число. Ограничим его значением, максимально-допустимым форматом int, т.е. значением 32766. Однако чтобы выполнить проверку на ограничение, объявим его как unsigned int;

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

· mas[5] – создадим массив из пяти элементов (максимальное число 32766 имеет пять разрядов) для хранения каждой цифры числа u. Объявим  его как int;

· b – текущий номер цифры числа u при их выделении. Нумерация начинается с нуля, количество цифр числа u равно: b – 1. Объявим ее как int;

· sum – переменная, где ведется расчет суммы кубов цифр mas[i]. При обращении к ней необходимо её инициализировать нулем. Примем тип переменной – int;

· i – индексная переменная, изменяемая от 0 до b. Имеет тип int.

Алгоритм решения этой задачи следующий:

1) запрашивается значение числа N и проверяется его диапазон. Если N больше допустимого, то запрос повторяется;

2) организуется цикл переменной u от 1 до N;

3) расписываются цифры числа u в обратном порядке в массив mas ( в mas[0] – младший разряд, mas[b] – старший);

4) подсчитывается сумма кубов всех цифр, записанных в mas[0]-mas[b] в поле sum;

5) выполняется сравнение (u==sum). Если переменные равны, то выводятся результаты на печать, в противном случае меняется число u (u=u+1).

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

// Пример 2: Циклы for, while, структурное программирование

#include<stdio.h>

#include<math.h>

void main(void)

{

unsigned int N;

int u,b,sum,i,mas[5];

for(;;)

{

printf("nУкажите значение N (меньше 32766) > ");

scanf("%d",&N);

if(N<32766) break;

}

for(u=1;u<=N;u++)

{

b=0; mas[b]=u;

while(mas[b]>=10)

{

mas[b+1]=mas[b]/10;

mas[b++]=mas[b]%10;

}

sum=0;

for(i=0;i<=b;i++) sum+=pow(mas[i],3);

if(u==sum) printf("nПри u=%d сумма кубов цифр равна=%d",u,sum);

}

}