13. Подпрограммы. Функции. Передача параметров. Статические переменные.
13.1. Стековый кадр.
Мы познакомились со стеком в главе 5. Там мы изучили две команды: занесение в стек (push) и извлечение из стека (pop).
Хотелось бы иметь возможность выборки элементов из стека без изменения SP, т.е. без использования команд push и pop. Косвенной адресации через SP не существует (список всех методов косвенной адресации у нас был). Для этой цели используется регистр BP. Он обращается к данным в сегменте стека. Это означает, что команда mov ax,[bp+4] эквивалентна команде mov ax,ss:[bp+4].
Установка нужного значения BP выполняется командой mov bp,sp. Этой командой в BP помещается адрес вершины стека. Теперь относительно BP можно адресовать элементы стека. Содержимое SP может претерпевать изменения, а BP остается опорным пунктом в стековом сегменте: можно выбирать данные выше и ниже BP, отмеривая от BP расстояние в словах. Область памяти, адресуемая с помощью BP, носит название стековый кадр (stack frame).
Пример. Разместим в стеке отрезок натурального ряда чисел от 1 до 5. После числа 3 сохраним в стеке значение BP и пометим в BP адрес этого элемента стека
push 1
push 2
push 3
push bp ; Сохранить BP
mov bp,sp
sub sp,4 ; Выделить в стеке два слова
mov word ptr [bp-2],4
mov word ptr [bp-4],5
mov ax,[bp+4] ; Взять из стека число 2
mov bx,[bp-4] ; Взять из стека число 5
Вот как выглядит стек после выполнения программы.
ss:FFFE 0000 начальное положение SP
bp+6 ss:FFFC 0001
bp+4 ss:FFFA 0002
bp+2 ss:FFF8 0003
bp ss:FFF6 0000 старое значение BP
bp-2 ss:FFF4 0004
bp-4 ss:FFF2►0005 текущее положение SP
BP указывает на старое значение BP. Оно нам, как правило, не нужно. Именно поэтому создатели процессора 8086 пожертвовали методом адресации [BP], чтобы получить комбинацию полей mod и r/m для прямой адресации.
Отсчет от BP ведется в сторону старших и младших адресов. Указатель стека SP перемещен вниз на три слова командой sub sp, 4.
Очистить стек можно так:
mov sp, bp ; Избавляемся от чисел 4 и 5
pop bp ; Восстанавливаем старое значение BP
add sp,6 ; Избавляемся от чисел 1, 2, 3
Проследите в отладчике этот процесс.
13.2. Подпрограммы.
Если некоторая последовательность команд должна выполняться в нескольких местах программы, то целесообразно выделить эту последовательность в подпрограмму.
На первый взгляд, для перехода к выполнению подпрограммы можно воспользоваться командой безусловного перехода. Код подпрограммы начинается с определенного адреса, к этому адресу и нужно переходить (т.е. помещать его в IP или CS:IP). Но как вернуться в вызывающую программу (будем называть ее главной)? Ведь подпрограмма может вызываться из разных мест главной программы. К выполнению какой команды главной программы переходить? Вспомним, что в момент выполнения любой команды счетчик команд IP содержит адрес следующей команды. Именно этот адрес надо сохранить на время выполнения подпрограммы. Где же его хранить? Здесь возможны разные решения, но, оказывается, адрес возврата удобнее всего хранить в стеке. А при возвращении в главную программу восстанавливать адрес возврата из стека. Почему именно стек является наиболее удобной структурой для хранения адреса возврата — к этому вопросу мы еще вернемся.
Для организации связи программы и подпрограммы служит пара команд: вызов (call) и возврат (ret — сокращение от return). Они представлены в нескольких вариантах (точно так же, как имелось несколько вариантов для команды безусловного перехода jmp).
Вызов подпрограммы
внутрисегментный call near opr IP↓
IP ← IP + Data16
CALL — вызов флаги не изменяются
Вызов подпрограммы
межсегментный call far opr CS↓ IP↓
CS ← Data16, IP ← Data16
CALL— вызов флаги не изменяются
Последовательность помещения в стек содержимого CS и IP имеет свое объяснение из общего принципа: в стеке по более старшему адресу будет храниться более значимое число — содержимое CS.
Здесь near (ближний) и far (дальний) — атрибутные операторы. Для каждого типа вызова используется своя команда возврата.
Возврат из подпрограммы, ближний ret (retn) IP↑
RETurn — возврат флаги не изменяются
Возврат из подпрограммы, дальний ret (retf) IP↑ CS↑
RETurn — возврат флаги не изменяются
Упражнение. Введите в отладчике команды ret, retn, retf и посмотрите их коды.
Здесь, конечно, возникает вопрос: почему команды ret, имеющие различные коды и выполняющие различные действия, имеют общую мнемонику ret? Ответ на это мы узнаем при изучении языка Ассемблера. В этом языке имеются директивы, которые диктуют Ассемблеру, какой именно код команды ret генерировать — ближний или дальний.
Для команд ret имеется вариант, когда после выталкивания из стека адреса возврата производится очистка стека от переданных в подпрограмму параметров.
Возврат из подпрограммы, ближний, с очисткой стека от параметров ret (retn) D16 IP↑
SP ← SP + D16
RETurn — возврат флаги не изменяются
Возврат из подпрограммы, дальний, с очисткой стека от параметров ret (retf) D16 IP↑ CS↑
SP ← SP + D16
RETurn — возврат флаги не изменяются
Все вызовы подпрограммы должны быть либо внутрисегментными, либо межсегментными, так как команда ret должна извлекать из стека одно и то же количество слов.
Еще имеется возможность косвенного вызова подпрограммы (например, call [si]). Это может оказаться полезным, если организовать в программе массив адресов подпрограмм.
Пример. Подпрограмма заменяет знаковое число, хранящееся в AX, его абсолютным значением. Главная программа заменяет каждый элемент массива из восьми слов, хранящийся по адресу 200, его абсолютным значением. Главная программа расположена, начиная с адреса 100. Подпрограмму расположим по адресу 180.
Код главной программы:
cs:0100►BE0002 mov si,0200
cs:0103 B90800 mov cx,0008
cs:0106 8B04 mov ax,[si]
cs:0108 E87500 call 0180
cs:010B 8904 mov [si],ax
cs:010D 46 inc si
cs:010E 46 inc si
cs:010F E2F5 loop 0106
cs:0111 90 nop
Код подпрограммы:
cs:0180 3D0000 cmp ax,0000
cs:0183 7D02 jnl 0187
cs:0185 F7D8 neg ax
cs:0187 C3 ret
Проанализируем код команды call 180
……………
Проследите, как при нажатии клавиши F7 (курсор находится на строке с командой call 180) в стек будет помещено число 010Bh, а при выполнении команды ret это число будет вытолкнуто из стека и помещено в IP. Выполнение главной программы возобновится.
13.3. Передача параметров в подпрограмму.
Существует два основных способа передачи параметров в подпрограмму: через стек и через регистры. Результаты своей работы подпрограмма также возвращает через стек и/или регистры.
В предыдущем примере передача параметра и возврат результат происходила через регистр AX.
При передаче параметров через стек он должен быть очищен по окончании работы программы (если это не сделать, то через несколько вызовов подпрограммы стек будет забит "мусором" — уже ненужными значениями параметров). Это можно сделать двумя способами. (Пусть N — количество байтов, выделенных для хранения параметров в стеке.)
1) в вызывающей программе после вызова подпрограммы добавляется команда add sp, N;
2) подпрограмма завершается командой ret N.
В первом случае в подпрограмму можно передавать различное число параметров. Главная программа "знает" их количество и очищает от них стек. Во втором случае подпрограмма всегда вызывается с фиксированным количеством параметров.
Подпрограмма может возвращать ответ типа ДА/НЕТ. Обычно для этого используется флаг CF. Обычное соглашение таково: если программа отработала нормально, "штатно", то возвращается значение CF = 0. Если работа подпрограммы закончилась аварийно (например, подпрограмма не смогла открыть файл, получила на входе неверные данные и т.д.), то CF = 1. При этом номер ошибки возвращается в регистре AX. Вызывающая программа анализирует CF и в зависимости от его значения предпринимает те или иные действия.
Перечислим команды для изменения флага CF.
Сбросить флаг переноса clc CF 0
CLear CF CF = 0
Инвертировать флаг переноса cmc
CoMplement CF CF = 0
Установить флаг переноса stc CF 1
SeT CF CF = 1
Пример. В массиве байтов записаны беззнаковые числа. Заменить числа, большие, чем 40h, на 40h. Сосчитать количество изменений.
Распределим память. Пусть главная программа начинается с адреса 100, подпрограмма — с адреса 200, массив — с адреса 300 и содержит 8 элементов, количество измененных элементов запишем в слово по адресу 400 (будем называть это слово счетчиком). Если массив пустой (т.е. по ошибке передан размер, равный нулю), то главная программа должна поместить в слово по адресу 402 число 1, в противном случае 0.
Подпрограмма получает на вход параметры: количество элементов массива, адрес массива, адрес счетчика (слова для записи количества измененных элементов).
Дадим два решения этой задачи. В первом решении передадим параметры через стек.
Набирать программу будем в Turbo Debugger.
По адресу 300 введем массив для обработки:
ds:0300 01 42 12 44 10 80 11 56
С 100-го адреса вводим код главной программы
cs:0100 6A08 push 0008
cs:0102 680003 push 0300
cs:0105 680004 push 0400
cs:0108 E8F500 call 0200
cs:010B 7208 jb 0115 (ввели jc 115)
cs:010D C70602040000 mov word ptr [0402],0000
cs:0113 EB06 jmp 011B (вводить jmp 11bh, а не jmp 11b!)
cs:0115 C70602040100 mov word ptr [0402],0001
cs:011B 90 nop
Проанализируем код команды вызова подпрограммы
E8F500 call 0200
E8 disp-lo disp-hi
Здесь disp = 00F5h, а так как в момент выполнения этой команды IP = 010Bh (адрес следующей команды), то disp + IP = 0200.
Введем две первые команды подпрограммы
cs:0200 push bp
cs:0201 mov bp,sp
Прежде чем писать дальше текст программы проанализируем, как выглядит стековый кадр.
BP
старое BP
SP
адрес возврата
BP+4
адрес счетчика
BP+6
адрес массива
BP+8
количество элементов
Напоминаем, что на этом рисунке вверху — младшие адреса, внизу — старшие адреса, и стек растет от старших адресов к младшим. (Так принято изображать стек в литературе).
Перед вводом оставшейся (основной) части подпрограммы выполним программу, начиная с 100-го адреса (нажатием F7), до команды с адресом 200 (включительно). Прослеживайте, как заполняется стек. Исходные значения регистров BP = 0, SP = FFFE. Что мы увидим в стеке?
ss:FFFC 0008 количество элементов
ss:FFFA 0300 адрес массива
ss:FFF8 0400 адрес ячейки для количества изменений
ss:FFF6 010B адрес возврата (адрес команды jc 115)
ss:FFF4►0000 старое содержимое BP
Теперь можно набирать программу дальше (адреса показывать не будем, а поставим метки).
mov cx,[bp+08] ; Количество элементов в CX
jcxz e ; Если это количество равно нулю, переход на e
mov si,[bp+06] ; Адрес массива — в SI
xor dx,dx ; Счетчик элементов — в DX
s: cmp byte ptr [si],40 ; Сравнить очередной элемент с пороговым значением
jna k ; Если он «выше» порогового значения,
mov byte ptr [si],40 ; то заменить его пороговым значением
inc dx ; и увеличить счетчик
k: inc si ; Переместить указатель на следующий элемент массива
loop s
mov di,[bp+04] ; Адрес параметра (счетчика измененных элементов) — в DI
mov [di],dx ; Разместить по этому адресу значение счетчика
clc ; Нормальное завершение
jmp f
e: stc ; Аварийное завершение
f: pop bp
ret 6 ; Возврат с очисткой стека от трех параметров
По команде ret 6 сначала из стека восстанавливается адрес возврата (010B), при этом SP = FFF8. Далее SP = SP + 6 = FFF8 + 6 = FFFE. Стек освобожден от параметров. (Если это не сделать, то при каждом вызове стек будет заполняться старыми параметрами.)
Вот какой вид примет подпрограмма в панели кода:
cs:0200 55 push bp
cs:0201 8BEC mov bp,sp
cs:0203 8B4E08 mov cx,[bp+08]
cs:0206 E319 jcxz 0221
cs:0208 8B7606 mov si,[bp+06]
cs:020B 33D2 xor dx,dx
cs:020D 803C40 cmp byte ptr [si],40
cs:0210 7604 jbe 0216
cs:0212 C60440 mov byte ptr [si],40
cs:0215 42 inc dx
cs:0216 46 inc si
cs:0217 E2F4 loop 020D
cs:0219 8B7E04 mov di,[bp+04]
cs:021C 8915 mov [di],dx
cs:021E F8 clc
cs:021F EB01 jmp 0222
cs:0221 F9 stc
cs:0222 5D pop bp
cs:0223 C20600 ret 0006
Рассмотрим второй вариант передачи параметров. Теперь передадим параметры через регистры
mov cx,8
mov si,300
mov di,400
call 200
Изменения в подпрограмме очевидны (убрать все команды загрузки регистров из стекового кадра). Поэтому приводить ее не будем. Внесите изменения самостоятельно и убедитесь в работоспособности программы. Размер программы и подпрограммы уменьшается, увеличивается быстродействие (ведь стек расположен в ОЗУ, а обращение к памяти занимает больше времени, чем обращение к регистрам).
Упражнение. Внесите изменения в код, чтобы стек очищала вызывающая программа.
Отметим особенности работы с подпрограммами в отладчике Turbo Debugger. Если подпрограмма отлажена, то проводить ее трассировку не имеет смысла. Нужно выполнять подпрограмму как одну команду. Для этого нужно вместо клавиши F7 нажимать клавишу F8.
13.4. Вложенные подпрограммы.
Пусть главная программа вызывает подпрограмму A, а подпрограмма A в свою очередь вызывает подпрограмму B. Тогда в стек сначала записывается адрес возврата для подпрограммы A (адрес команды, следующей за call A), а затем адрес возврата для подпрограммы B (адрес команды, следующей за call B). По команде ret (внутри B) из стека выталкивается в IP адрес возврата для B, а по команде ret (внутри A) из стека выталкивается в IP адрес возврата для A. (Сделайте самостоятельно соответствующие картинки). Таким образом, благодаря принципу LIFO из стека каждый раз выталкивается нужный адрес возврата.
Задача. Подпрограмма B преобразует строчные латинские буквы в прописные (на вход поступает код символа, на выход — преобразованный или оставленный без изменений код символа). Подпрограмма А принимает на вход адрес строки и ее длину. Если строка имеет ненулевую длину, то по тому же адресу расположить строку, где все строчные латинские буквы преобразованы в прописные. Главная программа вызывает A, в свою очередь A вызывает B. Напишите соответствующие программы, разместите их в памяти и проследите в Turbo Debugger, как в стек будут помещаться адреса возврата и как они будут выталкиваться из него.
13.5. Автоматические переменные в языке Си.
Переменные, объявляемые внутри тела функции, могут быть регистровыми, автоматическими и статическими.
В первом случае для хранения переменной выделяется регистр ЦП (если такая возможность есть). Для этого, как правило, выделяются регистры SI и DI.
Перейдем к автоматическим переменным. Вот что о них говорится в классической книге Б.Кернигана и Д.Ричи (в программной документации ее часто обозначают как K&R): "Автоматические переменные действительны только внутри функции, они возникают в момент входа в функцию и исчезают при выходе из нее" (с.77)
Эти слова звучат загадочно. Попробуем разобраться, как это реализовано. Для этого создадим небольшую программу prim.c.
int main() {
int k;
k = 2;
k = k + 3;
k += 3;
return 0;
}
Заодно мы посмотрим, будет ли различаться реализация увеличения k на 3 для последних двух инструкций.
Воспользуемся компилятором командной строки
c:prog>bcc -1- -v -r- prim.c
c:prog>td prim.exe
Поясним используемые ключи:
-1- — не использовать инструкции, появившиеся в 286 процессоре.
-v — включать в загрузочный файл отладочную информацию (тогда мы увидим в Turbo Debugger, каким инструкциям Си соответствуют машинные команды)
-r- — запретить использование регистровых переменных (если не указывать этот ключ, то в нашей простенькой программе под переменную k будет отведен регистр)
Упражнение. Как установить такие режимы в интегрированной среде.
После запуска Turbo Debugger перейдем в окно CPU: F10/View/CPU. Мы увидим следующее.
_main: int main()
cs:0239►55 push bp
cs:023a 8bec mov bp,sp
cs:023c 4c dec sp
cs:023d 4c dec sp
#prim#3: k = 2;
cs:023e c746fe0200 mov word ptr [bp-02],0002
#prim#4: k = k + 3;
cs:0243 8b46fe mov ax,[bp-02]
cs:0246 050300 add ax,0003
cs:0249 8946fe mov [bp-02],ax
#prim#5: k += 3;
cs:024c 8346fe03 add word ptr [bp-02],0003
#prim#6: }
cs:0250 8be5 mov sp,bp
cs:0252 5d pop bp
cs:0253 c3 ret
Нажимая клавишу F7, выполним программу до команды с адресом CS:0243. В таблице показано содержимое панелей регистров и стека: в первой колонке — до выполнения программы, во второй — до выполнения команды с адресом CS:0243. (Показано содержимое только тех регистров, которые претерпевают изменения при выполнении указанных инструкций).
До выполнения программы После команды k = 2; Перед командой ret
bp 0000
sp fff8
ip 0239 bp fff6
sp fff4
ip 0243
ss:fffa 0000
ss:fff8 00ff
ss:fff6 3246
ss:fff4 5208 ss:fffa 0000
ss:fff8 00ff
ss:fff6 0000
ss:fff4 0002
Итак, после команды push bp в SP содержится FFF6, а в слово с адресом SS:FFF6 записывается 0 (содержимое BP). В BP помещается адрес FFF6, а из содержимого SP еще раз вычитается два, и теперь SP = FFF4. Далее по адресу SS:FFF4 = BP – 2 записывается число 2. Мы наглядно убедились, что стековый кадр имеет вид:
BP–2 2 SP (переменная k)
BP старое BP
(Обратите внимание, что в отличие от панели стека в Turbo Debugger сейчас направление от младших адресов к старшим — сверху вниз).
Теперь заметим, что команде k = k + 3; соответствуют три машинных команды, а команде k += 3; — только одна.
Наконец, посмотрим в конце программы уничтожение стекового кадра: содержимое BP копируется в SP и из стека выталкивается старое значение BP. Локальная переменная k превратилась в "мусор"! К ней больше нет возможности обратиться и при дальнейших манипуляциях со стеком содержимое этой ячейки будет затерто. По команде ret управление возвращается головному модулю программы, который вызывал функцию main().
13.6. Команды создания и уничтожения стекового кадра для локальных переменных.
В процессоре 80286 появились команды, которые берут на себя всю "черновую" работу по организации стекового кадра.
Создать стековый кадр enter volume, level
ENTER — ввести, volume — объем, level — уровень флаги не изменяются
Здесь volume — размер стековой памяти (в байтах) для размещения локальных переменных. level — уровень вложенности процедуры. В языке Си внутри функции не может быть размещено описание другой функции. Зато в языке Pascal это возможно. Команда enter volume, 0 эквивалентна командам
push bp
mov bp,sp
sub sp, volume
Если параметр level отличен от нуля, то алгоритм работы команды enter весьма сложен, и мы его опустим. См. например [Юров, справочник, Алберт, Морс].
Освободить стековый кадр leave
LEAVE — освободить флаги не изменяются
Команда leave эквивалентна двум командам:
mov sp,bp
pop bp
13.7. Функции в языке Си.
Мы уже работали с функциями. Мы использовали функции puts и printf для вывода информации на экран. Наши программы включали в себя определение только одной функции: main — главной. Теперь дадим достаточно полное описание функций.
13.7.1. Определение и вызов функции. Прототип.
Определение функции выглядит так:
заголовок функции
{ тело функции }
Заголовок функции имеет вид:
тип_возвращаемого_значения имя_ функции (список параметров)
Элемент списка: тип_параметра имя_параметра. Элементы списка разделены запятой.
Возвращаемое значение помещается в теле функции как операнд оператора return.
Если функция не возвращает значения, то тип возвращаемого значения void (пусто).
В теле функции можно размещать любые оператора языка Си. Но нельзя определять другие функции.
Вызов функции имеет вид
[возвращаемое_значение = ] имя_функции(параметр_1, параметр_2, …);
Если вызов функции предшествует ее определению (или определение размещено в другом файле), то вызову должен предшествовать прототип функции, чтобы компилятор мог проверить количество и тип параметров, передаваемых в функцию при вызове. Прототип записывается как заголовок функции, который заканчивается точкой с запятой. Имена параметров в прототипе можно опускать.
Уже в наших первых программах мы столкнулись с необходимостью включать в программу заголовочные файлы, содержащие, в частности, прототипы используемых нами библиотечных функций.
13.7.2. Реализация функции в малой модели 16-разрядной платформы.
Введем обозначения для вызова функции v = f(p1, p2,…, pn). Слова «функция» и «подпрограмма» будем использовать как синонимы.
Вызывающая программа начинает формировать стековый кадр, помещая параметры в стек справа налево. При переходе в подпрограмму в стеке запоминается адрес возврата (в малой модели памяти это одно слово). В подпрограмме необходимо выполнить команды
push bp
mov bp, sp
После этого стековый кадр приобретает вид (в предположении, что каждый параметр занимает слово)
bp старое BP
адрес возврата
bp + 4 p1
bp + 6 p2
…
bp + 2 + 2*n pn
После выполнения тела подпрограммы нужно выполнить команды
mov sp, bp
pop bp
ret
Уничтожением стекового кадра занимается вызывающая программа. В ней выполняется команда
add sp, 2*n , где n — количество параметров.
На рисунке изображён стековый кадр, где каждый параметр занимает слово. Можно передавать двойные слова и другие данные.
Для возвращаемого значения действует простое правило. Если возвращаемое значение размещается в слове, то результат помещается в регистре AX, если двойное слово — в паре DX:AX.
Пример. Функция возвращает абсолютное значение целого числа.
#include <stdio.h>
int abs_int( int num) {
if (num < 0)
num = –num; return num;
}
int main() {
int k = –5, mm;
mm = abs_int(k);
printf("k = %d, mm = %d
", k, mm);
return 0;
}
Описание функции предшествует ее использованию, поэтому прототип в этой программе не нужен. Посмотрим сгенерированный код.
_main: int main() {
cs:02A7►55 push bp
cs:02A8 8BEC mov bp,sp
cs:02AA 83EC02 sub sp,0002 ; память для mm
cs:02AD 56 push si ; в SI размещается k
#P4#8: int k = -5, mm;
cs:02AE BEFBFF mov si,FFFB
#P4#9: mm = abs_int(k);
cs:02B1 56 push si ; k помещается в стек
cs:02B2 E8DEFF call _abs_int
cs:02B5 59 pop cx ; очистка стека
cs:02B6 8946FE mov [bp-02],ax ; возвращаемое значение
; функции – в mm
#P4#10: printf("k = %d, mm = %d
", k, mm);
cs:02B9 FF76FE push word ptr [bp-02] ; mm в стек
cs:02BC 56 push si ; k в стек
cs:02BD B8A800 mov ax,00A8 ; адрес форматной строки
cs:02C0 50 push ax ; в стек
cs:02C1 E8650C call _printf
cs:02C4 83C406 add sp,0006 ; очистка стека
#P4#11: return 0;
cs:02C7 33C0 xor ax,ax ; Возвращаемое значение в AX
cs:02C9 EB00 jmp #P4#12 (02CB)
#P4#12: }
cs:02CB 5E pop si
cs:02CC 8BE5 mov sp,bp
cs:02CE 5D pop bp
cs:02CF C3 ret
_abs_int: int abs_int( int num) {
cs:0293 55 push bp
cs:0294 8BEC mov bp,sp
cs:0296 8B5604 mov dx,[bp+04] ; поместить в DX число num
#P4#3: if (num < 0)
cs:0299 0BD2 or dx,dx ; DX не изменяется, но меняются
cs:029B 7D06 jnl #P4#5 (02A3) ; флаги состояния
#P4#4: num = -num;
cs:029D 8BC2 mov ax,dx
cs:029F F7D8 neg ax
cs:02A1 8BD0 mov dx,ax
#P4#5: return num;
cs:02A3 8BC2 mov ax,dx ; вернуть результат в AX
#P4#6: }
cs:02A5 5D pop bp
cs:02A6 C3 ret
Упражнение. Какое предупреждение выдаст компилятор, если поместить определение функции после функции main? Добавьте перед функцией main прототип int abs_int(int); и убедитесь, что предупреждений нет.
Пример: Функция обменивает значения своих двух параметров.
Сначала опишем функцию swap, а вслед за ней функцию main. При таком порядке расположения описаний прототип не нужен. При компиляции функции main количество и тип параметров известны.
Решение (неверное!).
#include <stdio.h>
void swap( int a, int b) {
int t; t = a;
a = b;
b = t;
}
int main() {
int c = 5, d = 8;
swap( c, d);
printf("c = %d, d = %d
", c, d);
return 0;
}
Результат:
c = 5, d = 8
В чем ошибка? При вызове функции ее параметры (а точнее, их копии) помещаются в стек:
младший адрес адрес возврата
c (5)
старший адрес d (8)
Функция меняет в стеке значения
младший адрес адрес возврата
c (8)
старший адрес d (5)
После этого в главной программе (функции main) стек уничтожается. Исходные переменные, естественно, не претерпевают изменений, меняются лишь их копии в стеке.
Упражнение. Проверьте в TD эти утверждения. Откомпилируйте программу так:
bcc –r– –v swap.c
(т.е. запретите использование регистровых переменных).
Как исправить программу? Надо записывать в стек не сами переменные, а их адреса, и менять значения по этим адресам.
Вот исправленный вариант (заодно покажем необходимость использования прототипа, т.к. расположим описания функций в другом порядке).
#include <stdio.h>
void swap( int *a, int *b); // можно и так: void swap( int *, int *);
int main() {
int c = 5, d = 8;
swap( &c, &d); // вызов изменился!
printf("c = %d, d = %d
", c, d);
return 0;
}
void swap( int *a, int *b){
int t;
t = *a;
*a = *b;
*b = t;
}
13.7. Примеры использования функций.
Функция суммирует элементы массива.
В функцию нужно передать два параметра: адрес массива и его размер.
#include <stdio.h>
int sum( int a[], int n);
int main() {
int a[] = {5, -3, 8}, dim_a = sizeof(a) / sizeof(a[0]);
printf("sum = %d
", sum( a, dim_a));
getchar();
return 0;
}
int sum( int a[], int n) {
int i, summa;
for (i = summa = 0; i < n; i++)
summa += a[i];
return summa;
}
Самостоятельно проанализируйте сгенерированный код.
Замечание. Типичная ошибка начинающих: использовать в функции ее имя как возвращаемое значение
int sum( int a[], int n) {
int i;
for (i = sum = 0; i < n; i++)
sum += a[i];
return sum;
}
Последует сообщение об ошибке: Lvalue required. Имя функции не является объектом, которому можно присвоить значение. Любопытно, что если добавить описание локальной переменной int i, sum;, то компиляция пройдет нормально, и функция будет работать правильно.
Задача. Написать функцию, которая возвращает максимальный элемент массива длинных целых и номер его первой позиции. Главная программа выводит эти значения.
13.8. Классы памяти переменных. Область видимости.
…………
13.9. Функция scanf().
До этого момента мы могли вводить в программу только одиночные символы с помощью getchar(). Теперь мы сможем организовать ввод в программу произвольных значений.
Освоим функцию для ввода scanf (сканирование форматное). Она аналогична функции printf. В списке ее аргументов сначала размещается форматная строка, а затем через запятую перечисляются указатели на вводимые значения. Типичная ошибка начинающих: перечисляют не указатели на переменные, а имена переменных. Функция возвращает количество прочитанных значений.
Вводимые значения пользователь может разделять любым количеством пробелов, табуляций и символов новой строки.
Пример.
файл sc.c
#include <stdio.h>
int main() {
int a, b;
int k;
puts("Input a,b");
k = scanf("%d%d", &a, &b);
printf("k = %d
",k);
printf("a = %d, b = %d
", a, b);
return 0;
}
Примеры диалога с программой
1) С:progp13>SC.EXE
Input a,b
10 20 (значения разделены одним пробелом)
k = 2 (прочитаны значения двух переменных)
a = 10, b = 20
2) Вводимые значения разделены нескольким пробелами, табуляциями и нажатиями клавиши Enter.
С:progp13 >SC.EXE
Input a,b
10
20
k = 2
a = 10, b = 20
3) Вводимые значения разделены запятой.
С:progp13 >SC.EXE
Input a,b
10,20
k = 1 (прочитано значение только одной переменной)
a = 10, b = 0
Исправим вызов функции
k = scanf("%d,%d", &a, &b);
Теперь мы ввели в форматную строку запятую. Самостоятельно убедитесь, что теперь вводимые значения, обязательно должны быть разделены запятой.
Интересное решение дается следующим оператором ввода
k = scanf("%d%*с%d", &a, &b);
Запятая будет прочитана как символ, но не присвоена (присваивание запрещает звездочка перед спецификатором). Теперь правильно будут обработаны следующие попытки ввода
10 20
10 20
10, 20
Но неправильно
10 , 20.
Итак, пользователю нужно явно указывать, как вводить исходные данные. А еще лучше вводить каждую переменную по отдельности.
Отдельно рассмотрим вопрос о вводе строки. Для этого служит спецификатор %s. Рассмотрим пример.
#include <stdio.h>
int main() {
int str[20];
puts("Enter string");
scanf("%s", str);
printf("str = %s
", str);
return 0;
}
Enter string
abc def
str = abc
Функция scanf восприняла пробел как разделитель и проигнорировала оставшуюся часть строки.
Поэтому строки лучше вводить с помощью функции gets(). Замените в вышеприведенной программе строку scanf("%s", str); на gets(str);
Но функция gets в свою очередь обладает опасной особенностью: если введено больше символов, чем размер массива, зарезервированного для хранения строки, то «лишние» символы записываются в непредусмотренную для них область памяти. Рассмотрим пример.
#include <stdio.h>
char str[] = "12345", a[] = "aaaaa";
int main() {
puts("Enter string");
gets(str);
printf("str = %s
", str);
printf("a = %s
", a);
return 0;
}
Вот как работает эта программа:
Enter string
abcdefgh
str = abcdefgh
a = gh
Когда мы изучим работу с файлами, мы узнаем более безопасную функцию fgets, в которой в качестве параметра указывается максимальное количество символов в вводимой строке.
Пример. Определить в программе две переменные типа int, ввести с клавиатуры их значения и вывести (напечатать) их сумму.
#include <stdio.h>
int main () {
int z, x, sum;
printf("z = ");
scanf("%d", &z);
printf("x=");
scanf("%d", &x);
sum = z + x;
printf("sum = %d", sum);
return 0;
}
Результат выполнения программы:
z = 46 <ENTER>
x = -16 <ENTER>
sum = 30
Эксперимент. Что будет, если программист использовал не адрес переменной, а ее имя в качестве аргумента функции scanf()?
В следующей программе сделана эта ошибка:
/* функция scanf() - ошибка в аргументе */
#include <stdio.h>
int main ()
{
int z, x, sum;
printf("z = ");
scanf("%d",z); // !!!!!!!!!
printf("x = ");
scanf("%d",x); // !!!!!!!!!
sum = z + x;
printf("sum = %d",sum);
return 0;
}
Корректная трансляция!!!
Результат выполнения программы:
z = 3 <ENTER>
x = 4 <ENTER>
sum = 3630652
13. Подпрограммы. Функции. Передача параметров. Статические переменные
Лекции по предмету «Информатика»