Язык программирования Си
3.5. Процедуры и функции
В языке Си все процедуры рассматриваются как функции, т.е. все они имеют результат. В редких случаях, когда требуется прямо указать, что функция не возвращает никакого значения, указывается тип void.
Следует различать в программе два контекста, каждый из которых по-своему влияет на выполнение вычислений функции: контекст определения (описания) и контекст вызова (использования). К сожалению, система программирования Си не гарантирует их обязательное соответствие. Забота об этом во многом лежит на программисте-разработчике программного кода.
При определении функции (процедуры) разработчик должен задать заголовок функции, в котором определяются тип возвращаемого значения, имя функции и список ее параметров, например:
void My Strcpy(char * Destination, char * Source)
Такой заголовок определяет процедуру, перемещающую значение входной строки символов Source в результирующую строку Destination. В качестве формальных параметров рассматриваются указатели на символы, что в языке Си также соответствует символьным массивам.
Вслед за заголовком размещается тело - исполняемая часть процедуры. В нем указывается, как производится обработка параметров. Например, для данного заголовка подойдет тело следующего вида:
{ while ( ( Destination++ = *Source++) != '\0' ) ; }
Как видим, обработка продолжается до тех пор, пока не будет переслан символ с нулевым кодом - признак конца символьной строки.
Общее описание процедуры подобного типа должно содержать не только собственно определение кода, но и блок комментариев, помогающий понять назначение самой процедуры и ее параметров.
/*********************************************************** * Наименование : My_Strcmp * * Назначение : Функция сравнивает значения двух символьных * строк и возвращает TRUE, если они равны, * и FALSE в противном случае. * * Входы : String1 & String2–две строки для сравнения. * * Выходы : Нет * * Возвращаемое значение : * * TRUE Строки одинаковые * FALSE Строки различаются ***********************************************************/ BOOL My_Strcmp( CHAR *String1, CHAR *String2 ) { while ( *String1 != '\0' ) { if ( *String1++ != *String2++ ) { /* обнаружено несовпадение */ return FALSE ; } } /* Дошли до конца строки. Надо проверить, что концы совпали */ return ( *String1 == *String2 ) ; }
3.6. Управление памятью
Для лучшего понимания механизмов связи контекста вызова и контекста описания процедур рассмотрим модель отражения программных объектов (переменных и операторов) в оперативную память вычислительной машины. Предлагаемая модель предполагает множество упрощений, но позволяет лучше понять, что происходит при подготовке программы к выполнению и собственно в процессе работы программы.
Будем считать, что для работы программного кода в оперативной памяти машины выделяется непрерывный сегмент - рабочая область (РО) между адресами L_addr и H_addr. В процессе трансляции и последующей сборки программных модулей в исполняемый машинный код строятся два сегмента: сегмент программного кода (СК) и сегмент данных (СД). Будем считать, что непосредственно перед процессом выполнения программы оба эти сегмента загружаются в память машины, как это показано на рис. 3.1.
Область, расположенная между памятью, занятой СК и СД, представляет собой пространство так называемой динамической памяти (ДП). Программа может использовать ее в процессе вычислений, запрашивая у системы поддержки по ходу вычислений. Для этой цели обычно используется процедура с именем malloc, в качестве параметров которой указывают размер запрашиваемого участка и получают в ответ адрес выделенной памяти из области ДП.
Если выделенная память больше не нужна, то ее можно возвратить, указав процедуре free адрес освобождаемого фрагмента. Из-за того, что отведение и освобождение памяти в ДП происходит по запросам программы, ее еще часто называют управляемой, а программисты-инструментальщики используют термин "куча". Последнее подчеркивает тот факт, что при произвольном характере занятия и освобождения кусков ДП она постепенно приобретает "дырявый" вид. В ней участки занятой памяти перемежаются освободившимися фрагментами различного размера. Управлять эффективным использованием этого пространства весьма сложно. Мы не будем далее углубляться в эти вопросы. Куда более интересно рассмотреть работу с областями СК и СД.
В процессе трансляции компилятор, обрабатывая программный модуль, может построить только сегмент кода тех процедур, которые описаны в нем. В процессе сборки нескольких модулей редактор связей, включенный в состав системы программирования, просматривает код головного модуля (с процедурой main), выделяет в нем обращения к внешним процедурам (таким, как printf, scanf, getch и т.п.) и присоединяет к сегменту головного модуля сегмент кода модуля вызываемых внешних процедур (например, stdio). Далее процедура повторяется по отношению к присоединенному модулю. В итоге структура ДК фактически образует стек сегментов кода собранных модулей (табл. 3.2), необходимых для решения задачи. Стек формируется в процессе подготовки кода к выполнению (сборки программного сегмента) и не изменяется во время решения задач. В этом смысле он статичен.
Второй сегмент - сегмент данных формируется в процессе решения задачи. В его основу ложится статическая часть - переменные, описанные на уровне модуля (на самом деле всех собранных модулей), то есть вне кода процедур. Эта память никогда не перераспределяется. Вплотную к ней примыкает память, соответствующая переменным головной процедуры main. Эта процедура активна во все время работы программы, поэтому тоже, по сути, является статической частью сегмента данных.
Формирование остальной части области переменных происходит в чистом виде по принципу работы стека (последний пришел - первый ушел). Фактически происходит следующее. При обращении к процедуре i+1-го уровня с прототипом
void Fill(char Ch, int K, char *Array);
(будем считать, что процедура main находится на нулевом уровне) в контексте вызова (в вызывающей процедуре) к сформированному на i-м уровне стеку данных добавляются поля данных, размер которых определяется списком передаваемых параметров. Скажем, 1 байт для символа, 4 байта для целого значения, 8 байт для указателя (табл. 3.2).
0 | 1 | 2 | 3 | 4 | ||||||||||
стек i-го уровня | char | int | *char | свободная память | ||||||||||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Далее формируются значения полей 1, 2 и 3 в отведенной области. А в поля 1 и 2 пересылаются значения соответствующих фактических параметров вызова. Если предположить, что процедура Fill обеспечивает заполнение K байт передаваемой в Array области символами Ch, то обращение к этой процедуре может выглядеть так:
void main() { int i; char Filler; char String[100]; i = 20; Filler = '*'; . . . . . . Fill(Filler, i, String); . . . . . . }
Такой вызов приведет к тому, что в поле 1 появится символ "*", в поле 2 - значение "20", а в третьем поле будет сформирован адрес нулевого элемента массива String. Предположим, что сама процедура Fill написана следующим образом:
void Fill(char Ch, int K, char *Array) { int i; Char *Pointer; for (i = 0, Pointer = Array; i < K; i++) *Pointer++ = Ch; }
Тогда при входе в процедуру произойдет следующее. В области свободной памяти (табл. 3.2) будет выделен 12-байтовый фрагмент под размещение локальных переменных i и Pointer (4 и 8 байта). В соответствии с кодом процедуры в поле Pointer будет скопирован адрес массива String головной процедуры. В дальнейшем по этому адресу будут пересылаться символы "*" из поля 1 (*Pointer++ = Ch;), причем каждый раз адрес будет увеличиваться на единицу.
Таким образом, в распоряжении процедуры Fill во время ее выполнения в стеке данных доступны поля с 1-го по 5-е (табл. 3.3) с общей длиной 25 байт. Нулевая область на рисунке по-прежнему соответствует области стека i-го уровня, 6-я - свободной области памяти, а вершиной стека считается адрес 24-го байта выделенного фрейма.
0 | 1 | 2 | 3 | 4 | 5 | 6 | ||||||||
стек i-го уровня | char | K | Array | i | Pointer | свободная память | ||||||||
0 | 1 | ... | 4 | 5 | ... | 12 | 13 | ... | 16 | 17 | ... | 24 |
Поскольку головная программа передала в процедуру адреса своих областей данных, то Fill получает возможность отправлять значения в область адресов стека i-го уровня. Фактически это соответствует передаче параметров по имени (по ссылке) - способу, характерному для случая, когда в результате работы вызываемой процедуры происходит изменение значений передаваемых ей параметров.
По завершению работы процедуры Fill добавленный фрейм (и область параметров, и область локальных переменных) освобождается и вершиной стека снова является последний байт нулевой области (табл. 3.3).
При вызове следующей процедуры механизм управления памятью срабатывает по той же схеме. Поэтому становится понятно, что в тех же адресах памяти могут оказаться совсем другие данные. Если бы процедура Fill при своем выполнении обращалась к процедуре следующего уровня, то очевидно, что длина стека была бы увеличена на еще один фрагмент, включающий область параметров и локальных переменных вызываемой процедуры.
Вопросы и задачи для самостоятельного решения
- Напишите процедуру проверки того, что шкаф с параметрами высота H, ширина W, длина L можно занести в комнату через дверь размером P на Q.
- Чему будет равен х после выполнения этих строчек:
х=5; х-= х++ - —х;
- Реализуйте на языке Си функцию конкатенации двух строк.
-
В int-представлении чисел можно обменять значения двух переменных, не используя промежуточной памяти:
int I, J; ... I = I + J; J = I – J; I = I – J;
Справедливо ли подобное преобразование для float-представления?
- Реализуйте на языке Си функцию замены подстроки в строке на заданную последовательность символов.
- Реализуйте на языке Си следующую задачу: "Заменить в исходной строке все цифры на соответствующее количество символов "* ", равное значению цифры (0 заменять на пустую подстроку, 9 - на *********). Строка передается процедуре-функции как входной/ выходной параметр". Рекомендуемое имя для процедуры - Convert String.