Тема 5.1 Программирование циклов. Циклы с предусловием.
Структурное программирование Методология структурного программирования основана на предположении, что логичность и понятность программы обеспечивает надежность, облегчает модификацию и ускоряет разработку программы. Характерными чертами структурного программирования являются: отказ от неструктурных передач управления; ограниченное использование глобальных переменных; модульность. Цикл с предусловием Существует три вида циклов: while, for и do. Цикл while имеет следующую форму: while (e) s; Оператор s выполняется до тех пор, пока значение выражения e равно " истина". Значение e вычисляется перед каждым выполнением оператора s. В предшествующих лекциях мы пользовались этой формой цикла. Рассмотрим еще один пример его работы: /* угадывание числа */ #include < stdio. h> main( ) { int i = 1; char res; printf(" Задумайте целое число от 1 до 100. Компьютер попытается угадать его. \n" ); printf (" Отвечайте y, если догадка правильна и" ); printf(" \n n, если программа ошибается \n" ); printf(" Итак, ваше число %d? \n", i); /*получение ответа */ while((res = getchar( ))! ='y') if(res! ='\n') /* пропуск символа новая строка */ printf(" Ну тогда оно равно %d\n" , ++i); printf(" Число угадано! \n" ); }
В наших примерах до сих пор использовались условные выражения, но вообще говоря, это могут быть выражения произвольного типа. В качестве оператора можно использовать простой оператор с символом " точка с запятой" в конце или составной оператор, заключенный в фигурные скобки. Если выражение истинно (или в общем случае равно единице), то оператор, входящий в цикл while, выполняется один раз, а затем выражение проверяется снова. Эта последовательность действий, состоящая из проверки и выполнения оператора, периодически повторяется до тех пор, пока выражение не станет ложным (или в общем случае равным нулю). Каждый такой шаг называется итерация. Данная структура аналогична структуре оператора if. Основное отличие заключается в том, что в операторе if проверка условия и (возможное) выполнение оператора осуществляется только один раз, а в цикле while эти действия производятся, вообще говоря, неоднократно.
Цикл while является условным циклом, использующим предусловие, т. е. условие на входе. Он называется условным, потому что выполнение оператора зависит от истинности условия, описываемого с помощью выражения. Подобное выражение задает предусловие, поскольку выполнение этого условия должно быть проверено перед началом выполнения тела цикла.
Подведем итоги.
Оператор while определяет операции, которые циклически выполняются до тех пор, пока проверяемое выражение не станет ложным, или равным нулю. Оператор while - это цикл с предусловием. Решение, выполнить ли в очередной раз тело цикла, принимается перед началом его прохождения. Поэтому вполне возможно, что тело цикла не будет выполнено ни разу. Оператор, образующий тело цикла, может быть либо простым, либо составным. Форма записи: while (выражение) оператор Выполнение оператора циклически повторяется до тех пор, пока выражение не станет ложным, или равным нулю. Цикл со счетчиком Оператор цикла for for(e1; e2; e3) s является удобной сокращенной записью для цикла while вида e1; while(e2) { s; e3; } Выражение e1 служит для задания начальных условий выполнения цикла, выражение e2 обеспечивает проверку условия выхода из цикла, а выражение e3 модифицирует условия, заданные выражением e1. Любое из выражений e1, e2, e3 может быть опущено. Если опущено e2, то по умолчанию вместо него подставляется значение TRUE. Например, цикл for for(; e2; ) s; с опущенными e1, e3 эквивалентен циклу while(e2) s; Цикл for(;; ) s; со всеми опущенными выражениями эквивалентен циклу while(TRUE) s; т. е. эквивалентен бесконечному циклу. Такой цикл может быть прерван только явным выходом из него с помощью операторов break, goto, return, содержащихся в теле цикла s.
44. Циклы с постусловием. Циклы с параметром. Вложенные циклы.
В Си существуют все три типа операторов цикла: цикл с предусловием, цикл с постусловием и цикл с параметром. Цикл с предусловием. Формат оператора цикла с предусловием: while (выражение) оператор; Цикл повторяет свое выполнение, пока значение выражения отлично от нуля, т. е. заключенное в нем условие цикла истинно. В качестве примера использования оператора цикла рассмотрим программу вычисления факториала целого положительного числа N!. Сопоставим программу решения этой задачи, написанную на Си.
// Программа вычисления факториала #include < iostream. h> void main() { long int F; int i, N; cout< < " N="; cin> > N; F=i=l; while(i< =N) F=F*i++; cout< < " \n" < < N< < " ! =" < < F; } Обратите внимание на операторы в теле цикла. Конечно, и в Си-программе можно было написать два оператора присваивания, объединив их фигурными скобками. Однако использованный способ записи более лаконичен и более характерен для Си. Этот же самый оператор можно было записать еще короче: F*=i++ При практическом использовании этой программы не следует забывать, что факториал — очень быстро растущая функция, и поэтому при определенных значениях N выйдет из диапазона, соответствующего типу long int. Задав для переменной F тип unsigned long, можно сдвинуть эту границу, но этого может оказаться недостаточно. Предлагаем в качестве самостоятельного задания исследовать предельные значения N для двух указанных типов переменной F. Интересно свойство следующего оператора: while(1); Это бесконечный пустой цикл. Использование в качестве выражения константы 1 приводит к тому, что условие повторения цикла все время остается истинным и работа цикла никогда не заканчивается. Тело в этом цикле представляет собой пустой оператор. При исполнении такого оператора программа будет «топтаться на месте». Рассмотрим еще один пример использования оператора цикла while. Вернемся к задаче итерационного вычисления суммы гармонического ряда: 1 + 1/2 + 1/3 +... с заданной точностью ε. Снова сопоставим программу на С++. // сумма гармонического ряда #include < iostream. h> #include < limits. h> void main() {int n=l; double S=0, eps; cout< < " Toчность: "; cin> > eps; while(1. 0/n> eps & & n< INT_MAX) S+=l. /n++; cout< < " \nCyммa=" < < S; } Файл limits. h, подключаемый препроцессором, содержит определения предельных констант для целых типов данных. В частности, константа с именем int_max равна максимальному значению типа int в данной реализации компилятора. Если для типа int используется двухбайтовое представление, то int_max=327 67. В этом же заголовочном файле определены и другие константы: INT_MIN=-327 68; LONG_MAX=214 74 8 3 64 7 и т. д.
Цикл с постусловием. Формат оператора цикла с постусловием: do оператор while (выражение); Цикл выполняется до тех пор, пока выражение отлично от нуля, т. е. заключенное в нем условие цикла истинно. Выход из Цикла происходит после того, как значение выражения станет ложным, иными словами равным нулю. В качестве примера рассмотрим программу вычисления N!, в которой используется цикл с постусловием. #include < iostream. h> void main() { long int F; int i, N; cout< < " N="; cin> > N; F=i=1; do F*=i++; while(i< =N); cout< < " \n" < < N< < "! =" < < F; } Цикл с параметром. Формат оператора цикла с параметром: for (выражение_1; выражение_2; выражение_3) оператор; Выражение 1 выполняется только один раз в начале цикла. Обычно оно определяет начальное значение параметра цикла (инициализирует параметр цикла). Выражение 2 — это условие выполнения цикла. Выражение 3 обычно определяет изменение параметра цикла, оператор — тело цикла, которое может быть простым или составным. В последнем случае используются фигурные скобки.
Рис 4 Алгоритм выполнения цикла for представлен на блок-схеме на рис. 4. Обратите внимание на то, что после вычисления выражения 3 происходит возврат к вычислению выражения 2 — проверке условия повторения цикла. С помощью цикла for нахождение N! можно организовать следующим образом: F=l; for(i=l; i< =N; i++) F=F*i; Используя операцию «запятая», можно в выражение 1 внести инициализацию значений сразу нескольких переменных: for(F=l, i=l; i< =N; i++) F=F*i; Некоторых элементов в оператора for может не быть, однако разделяющие их точки с запятой обязательно должны присутствовать. В следующем примере инициализирующая часть вынесена из оператора for: F=1; i=1; for(; i< =N; i++) F=F*i; Ниже показан еще один вариант вычисления N!. В нем на месте тела цикла находится пустой оператор, а вычислительная часть внесена в выражение 3. for(F=l, i=l; i< =N; F=F*i, i++); Этот же оператор можно записать в следующей форме: for(F=l, i=l; i< =N; F*=i++); В языке Си оператор for является достаточно универсальным средством для организации циклов. Вот пример вычисления суммы элементов гармонического ряда, превышающих заданную величину ε: for(n=l, S=0; 1. 0/n> eps & & n< INT_MAX; n++) S+=1. 0/n; И наконец, эта же самая задача с пустым телом цикла: for(n=l, S=0; 1. 0/n> eps & & n< INT_MAX; S+=l. 0/n++); Следующий фрагмент программы на Си++ содержит два вложенных цикла for. В нем запрограммировано получение на экране таблицы умножения.
for(х=2; х< =9; х++) for(y=2; y< =9; y++) cout< < " \n" < < x< < " *" < < y< < " = " < < x*y; На экране будет получен следующий результат: 2*2=4 2*3=6 ……… 9*8=72 9*9=81 Оператор continue. Если выполнение очередного шага цикла требуется завершить до того, как будет достигнут конец тела цикла, используется оператор continue. Следующий фрагмент программы обеспечивает вывод на экран всех четных чисел в диапазоне от 1 до 100. for< i=l; i< =100; i {if(i%2) continue; cout< < " \t" < < i; } Для нечетных значений переменной i остаток от деления на 2 будет равен единице, этот результат воспринимается как значение «истина» в условии ветвления, и выполняется оператор continue. Он завершит очередной шаг цикла, выполнение цикла перейдет к следующему шагу. Оператор goto. Оператор безусловного перехода goto существует в языке Си, как и во всех других языках программирования высокого уровня. Однако с точки зрения структурного подхода к программированию его использование рекомендуется ограничить. Формат оператора: goto метка; Метка представляет собой идентификатор с последующим двоеточием, ставится перед помечаемым оператором. Одна из ситуаций, в которых использование goto является оправданным — это необходимость «досрочного» выхода из вложенного цикла. Вот пример такой ситуации: for(... ) { while (... ) { for(... ) {... goto exit... } } } exit: cout< < " Bыход из цикла"; При использовании оператора безусловного перехода необходимо учитывать следующие ограничения: o нельзя входить внутрь блока извне; o нельзя входить внутрь условного оператора (if... else... ); o нельзя входить внутрь переключателя; o нельзя входить внутрь цикла.
45. Организация подпрограмм
Процедура может размещаться в любом месте программы, но так, чтобы на нее случайным образом не попало управление. Если процедуру просто вставить в общий поток команд, то процессор воспримет команды процедуры как часть этого потока и, соответственно, начнет выполнять эти команды. Учитывая это обстоятельство, есть следующие варианты размещения процедуры в программе: · в начале программы (до первой исполняемой команды); · в конце программы (после команды, возвращающей управление операционной системе); · промежуточный вариант — внутри другой процедуры или основной программы (в этом случае необходимо предусмотреть обход процедуры с помощью команды безусловного перехода JМР); · в другом модуле (библиотеке DLL). Размещение процедуры в начале сегмента кода предполагает, что последовательность команд, ограниченная парой директив PROC и ENDP, будет размещена до метки, обозначающей первую команду, с которой начинается выполнение программы. Эта метка должна быть указана как параметр директивы END, обозначающей конец программы: model small . stack 100h . data . code my_proc procnear ret my_proc endp start: end start Объявление имени процедуры в программе равнозначно объявлению метки, поэтому директиву PROC в частном случае можно рассматривать как завуалированную форму определения программной метки. Поэтому сама исполняемая программа также может быть оформлена в виде процедуры, что довольно часто и делается с целью пометить первую команду программы, с которой должно начаться выполнение. При этом не забывайте, что имя этой процедуры нужно обязательно указывать в заключительной директиве END. Так, последний рассмотренный фрагмент эквивалентен следующему: model small . stack 100h . data . code my_proc procnear ret my_proc endp start proc start endp end start В этом фрагменте после загрузки программы в память управление будет передано первой команде процедуры с именем start. Размещение процедуры в конце программы предполагает, что последовательность команд, ограниченная директивами PROC и ENDP, находится следом за командой, возвращающей управление операционной системе: model small . stack 100h . data . code start: mov ax, 4c00h int 21h; возврат управления операционной системе my_proc procnear ret my_proc endp end start Промежуточный вариант расположения тела процедуры предполагает ее размещение внутри другой процедуры или основной программы. В этом случае необходимо предусмотреть обход тела процедуры, ограниченного директивами PROC и ENDP, с помощью команды безусловного перехода JМР:
46. Передача параметров через регистры, статическую память, стек, область за командой вызова.
Процедура, часто называемая также подпрограммой, — это основная функциональная единица декомпозиции (разделения на несколько частей) некоторой задачи. Процедура представляет собой группу команд для решения конкретной подзадачи Процедура - именованная, правильным образом оформленная группа команд, которая, будучи однократно описана, при необходимости может быть вызвана по имени любое количество раз из различных мест программы.
Описание процедуры Для описания последовательности команд в виде процедуры в языке ассемблера используются две директивы: PROC и ENDP. Синтаксис описания процедуры таков < имя> proc [тип] < тело процедуры> < имя> endp
< Тип> может принимать значения near или far и характеризует возможность обращения к процедуре из другого сегмента кода. Тип near позволяет обращаться к процедуре только из того сегмента кода, в котором она описана. К процедуре типа FAR можно обращаться и из других сегментов тоже. По умолчанию тип принимает значение near. Замечание. Имя процедуры по сути является меткой, т. е. с помощью простых команд перехода можно осуществить переход на первую команду процедуры.
Размещение в сегменте кода Процедура может размещаться в любом месте программы, но так, чтобы на нее случайным образом не попало управление. Если процедуру просто вставить в общий поток команд, то микропроцессор будет воспринимать команды процедуры как часть этого потока и соответственно будет осуществлять выполнение команд процедуры. Если имеется несколько процедур их обычно размещают рядом. 1. В начале сегмента кода !!! Процедура должна быть расположена до первой выполняемой команды Пример. . code name proc; ближняя по умолчанию . . . name endp start: . . . end start 2. В конце сегмента кода !!! Процедура должна быть размещена после команды, возвращающей управление ОС Пример. code start: . . . mov ax, 4C00h int 21h name proc near . . name endp end start
3. Можно разместить процедуру внутри сегмента кода !!! Необходимо предусмотреть обход процедуры Пример. code start: . . . jmp metka name proc . . . name endp metka: . . . end start 4. Процедура может быть описана в другом сегменте кода (об этом позже)
Вызов процедур В принципе можно было бы обратиться к процедуре, просто переходом на метку - имя процедуры. Но!!! Необходимо сохранить где-то адрес возврата, т. е. адрес команды, следующей после процедуры. В систему команд микропроцессора была введена специальная команда Вызов процедуры, или переход с возвратом CALL [модификатор] < имя процедуры> Команда записывает адрес следующей за ней команды в стек, а затем осуществляет переход по метке < имя процедуры>. Вспомним, что адрес следующей выполняемой команды задается парой CS: IP. Если процедура описана как дальняя, то в стек по очереди заносятся два значения CS, IP. короткая - только значение IP И при этом соответственно переход считается коротким или дальним. Поэтому в с-ме команд имеется два варианта команды CALL. !!! Ассемблер выберет правильный вариант, если процедура была описана до вызова. Если процедура будет описана позже транслятор считает процедуру ближней и формирует команду ближнего вызова. Если процедура окажется дальней, будет зафиксирована ошибка. Для указания транслятору на то, что процедура, описанная ниже, будет дальней можно использовать < модификатор> со значением FAR PTR Например, CALL FAR PTR NAME Ì î ä è ô è ê à ò î ð ì î æ å ò ï ð è í è ì à ò ü ñ ë å ä ó þ ù è å ç í à ÷ å í è ÿ: •near ptr — ï ð ÿ ì î é ï å ð å õ î ä í à ì å ò ê ó â í ó ò ð è ò å ê ó ù å ã î ñ å ã ì å í ò à ê î ä à. Ì î ä è ô è ö è ð ó å ò ñ ÿ ò î ë ü ê î ð å ã è ñ ò ð eip/ip •far ptr — ï ð ÿ ì î é ï å ð å õ î ä í à ì å ò ê ó â ä ð ó ã î ì ñ å ã ì å í ò å ê î ä à. À ä ð å ñ ï å ð å õ î ä à ç à ä à å ò ñ ÿ ð å ã è ñ ò ð ами cs è ip/eip; •word ptr — ê î ñ â å í í û é ï å ð å õ î ä í à ì å ò ê ó â í ó ò ð è ò å ê ó ù å ã î ñ å ã ì å í ò à ê î ä à. Ì î ä è ô è ö è ð ó å ò ñ ÿ ò î ë ü ê î eip/ip. •dword ptr — ê î ñ â å í í û é ï å ð å õ î ä í à ì å ò ê ó â ä ð ó ã î ì ñ å ã ì å í ò å ê î ä à. Ì î ä è ô è ö è ð ó þ ò ñ ÿ (ç í à ÷ å í è å ì è ç ï à ì ÿ ò è — è ò î ë ü ê î è ç ï à ì ÿ ò è, è ç ð å ã è ñ ò ð à í å ë ü ç ÿ ) î á à ð å ã è ñ ò ð à, cs è eip/ip.
Возврат из процедур Адрес возврата в вызывающую программу хранится в стеке. Команда RET [число] возвращает управление вызывающей программе. Она считывает адрес возврата из вершины стека, загружает его в регистры CS и IP (теперь выполняться будет следующая за CALL команда в программе) адрес возврата при этом удаляется из стека(! ), затем стек очищается на указанное число байт и выполняется переход по адресу возврата. Команда RET соответствует команде RET 0 В зависимости от того, дальняя или ближняя была описана процедура, ассемблер формирует одну из возможных команд RET. В первом случае из стека извлекается два слова, которые загружаются в регистры CS и IP, во втором - одно слово в регистр IP. Команды CALL и RET действуют согласованно, за соответствием между командами следит транслятор. Необязательный параметр [число] задает значение, удаляемое из стека при возврате из процедуры - в байтах (use16) или в словах (use32)
Параметры процедур Если в процедуру необходимо передать некоторую переменную, то для процедуры она является формальным параметром. А конкретное значение данной переменной, которое передается в процедуру для замещения соответствующего формального параметра, является фактическим параметром. Например, если описать некоторую процедуру для сложения двух чисел как SUM(x, y), то переменные x, y явл. формальными параметрами, а при обращении к процедуре для вычисления суммы 4 и 5 SUM(4, 5) значения 4 и 5 являются фактическими парметрами. Поэтому одну и ту же процедуру можно использовать многократно для разных наборов значений этих переменных, SUM(5, 7) SUM (100, 45) Передача значений в процедуру называется передачей параметров. Результатом выполнения процедуры может быть некоторое значение, которое необходимо передать вызывающей программе. Передача результата из процедуры в программу тоже организовывается как передача параметров. Передавать параметры можно через регистры через стек через общую область памяти с помощью директив extern и public
Существует два способа передачи данных: 1. Передача параметров по значению В этом случае передаются сами данные, т. е. их значения !!! При передаче параметров по значению в процедуре обрабатываются их копии. Поэтому значения переменных в основной программе не изменяются!!! 2. Передача параметров по ссылке В этом случае передаются адреса (ссылки) параметров !!! Здесь обрабатываются не копии, а сами данные, поэтому при изменении данных в процедуре они автоматически изменяются и в вызывающей программе!!! Т. Е. Передача параметра по ссылке означает передачу адреса ячейки, соответствующей фактическому параметру.
Передача параметров через регистры Самый простой способ передачи данных. Его суть заключается в следующем: Основная программа записывает фактические параметры в какие-нибудь регистры, а процедура берет их оттуда и обрабатывает. Аналогично поступают с результатом. Процедура помещает результат в какой-то регистр, а программа извлекает его оттуда. !!! Если размер данных превышает 16 или 32 бит (размер регистра), то передавать нужно не сами данные, а ссылки на них. Совет 1 Рекомендуется тщательно комментировать процедуру и основную программу. Необходимо описать в какие регистры какие параметры помещаются, в какие регистры помещается результат. Совет 2. Рекомендуется использовать при небольшом количестве параметров, т. к. число доступных регистров ограничено. Пример1 Передача параметров по значению Вычислить max(a, b) + max(5, a-1) Решение. Опишем процедуру вычисления максимума двух переменных. Для передачи первого параметра будем исп-ть регистр ax, для второго - регистр BX. Результат процедура будет помещать в регистр AX. model small . stack 128 . data A dw? B dw? REZ dw? . code ; процедура вычисляет максимальное из двух чисел ; первое число в AX, второе - в BX ; результат помещается в AX MAX proc cmp ax, bx jge M mov ax, bx M: ret MAX endp start: . . . mov ax, A mov ax, B call MAX; результат в AX mov rez, ax mov ax, 5 mov bx, A dec bx call MAX; результат в AX add rez, ax . . . end start Пример 2 Передача параметра по ссылке Число (неотриц) разделить на 16. И оставить там же, где находилось Для сравнения с паскалем procedure DEL (var x: integer) Параметр-переменная Осн. программа записывает в регистр адрес фактической переменной. В качестве регистра необходимо выбирать регистр-модификатор. Пусть BX. Процедура; делит неотр. xисло X на 16 ; в регистре BX - адрес X Результат записывается в X DEL proc near mov cl, 4 shr word ptr [bx], cl ret DEL endp Основная программа; адрес числа в регистре BX lea bx, A call DEL lea bx, B call DEL Задача. Даны два массива однобайтовых чисел без знака. Найти сумму их максимальных элементов. Опишем нахождение максимального элемента массива процедурой. Формальными параметрами процедуры являются 1) начальный адрес массива 2) количество элементов массива Будем передавать параметры через регистры. Адрес массива - через регистр BX Кол-во элементов - через CX Результат возвращаем основной программе через AL (эл-ты однобайтовые). Masm model small . stack 128 . data n equ 10 m equ 15 mas1 db n dup (? ) mas2 db m dup (? ) ; процедура находит максимальный элемент массива ; адрес массива находится в регистре BX ; кол-во элементов - в CX ; результат возвращаем через AL max proc mov al, 0; sled: cmp [bx], al jle new mov al, [bx] new: inc bx loop sled ret max endp start: . . . lea bx, mas1 mov cx, n call max; результат в al mov dl, al lea bx, mas2 mov cx, m call max add dl, al . . . end start
Передача параметров через стек
Суть: осн. программа записывает параметры в стек, а процедура извлекает их оттуда. !!! Результат лучше передавать через регистр. Иначе нужно осторожно чистить стек при выходе из процедуры. Перед обращением к процедуре основная программа должна поместить в стек фактические параметры. Модель передачи (т. е. в каком порядке) определяет программист. ; основная программа . . . push par1 push par2 . . . push parN call name Замечание! При вызове процедуры в стек запишется адрес возврата, т. о. параметры будут находиться «под» ним. Процедура должна получить доступ к этим элементам. Т. к. они не на вершине стека, будем пользоваться регистром BP. Замечание! Регистр BP может использоваться в основной программе, поэтому предварительно следует запомнить его старое значение. Лучше тоже в стеке, но надо будет это учитывать! Таким образом, обратиться к первому (сверху) (а на самом деле это N параметр списку) элементу стека можно [BP+4], к следующему - [BP+6] и т. д. !!! Если процедура является дальней, то следует еще +2, т. к. для адреса возврата поместят два элемента в стек (CS, IP). Для доступа к параметру N - [BP+6] и т. д. Перед завершением процедуры необходимо выполнить действия по корректному возврату из процедуры. Необходимо 1) поместить на вершину стека адрес возврата из процедуры 2) очистить стек от параметров (они уже стали ненужными) (мы к ним только обращались, но не удаляли из стека) 1) mov SP, BP; восстановить SP pop BP; восстановление старого BP Теперь на вершине стека адрес возврата и можем выполнять RET 2) Очистить стек от параметров может основная программа ADD SP, 2*N Но если много обращений к процедуре, то в осн. Программе будет много таких команд. Поэтому лучше очищать стек в самой процедуре. Можно воспользоваться параметром [число] команды RET. Этот параметр задает число байтов, на которое очищается стек. Замечание! Адрес возврата не учитывать, т. к. Ret считывает его до очистки стека. Таким образом необходимо выполнить возврат из процедуры RET 2*N Итак, схема процедуры name proc push bp mov bp, sp . . . ; mov ax, [bp+4]; [bp+6], . . . mov sp, bp pop bp ret 2*n name endp
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|