Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы разработки программного обеспечения на примере языка С. Учебное пособие для СПО
.pdf
массивам.
Вслед за заголовком размещается тело - исполняемая часть процедуры.
В нем указывается, как производится обработка параметров. Например,
для данного заголовка подойдет тело следующего вида:
{
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++ )
{ /* обнаружено несовпадение */
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
81

return FALSE ;
}
}
/* Дошли до конца строки. Надо проверить, что концы совпали */
return ( *String1 == *String2 ) ;
}
3.6. Управление памятью
Для лучшего понимания механизмов связи контекста вызова и контекста
описания процедур рассмотрим модель отражения программных
объектов (переменных и операторов) в оперативную память
вычислительной машины. Предлагаемая модель предполагает
множество упрощений, но позволяет лучше понять, что происходит при
подготовке программы к выполнению и собственно в процессе работы
программы.
Будем считать, что для работы программного кода в оперативной
памяти машины выделяется непрерывный сегмент - рабочая область
(РО) между адресами L_addr и H_addr. В процессе трансляции и
последующей сборки программных модулей в исполняемый машинный
код строятся два сегмента: сегмент программного кода (СК) и сегмент
данных (СД). Будем считать, что непосредственно перед процессом
выполнения программы оба эти сегмента загружаются в память
машины, как это показано на рис. 3.1
.
Рис. 3.1. Модель размещения программы в памяти машины
Область, расположенная между памятью, занятой СК и СД,
представляет собой пространство так называемой динамической
памяти (ДП). Программа может использовать ее в процессе
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
82

вычислений, запрашивая у системы поддержки по ходу вычислений.
Для этой цели обычно используется процедура с именем malloc, в
качестве параметров которой указывают размер запрашиваемого участка
и получают в ответ адрес выделенной памяти из области ДП.
Если выделенная память больше не нужна, то ее можно возвратить,
указав процедуре free адрес освобождаемого фрагмента. Из-за того,
что отведение и освобождение памяти в ДП происходит по запросам
программы, ее еще часто называют управляемой, а программистыинструментальщики используют термин "куча". Последнее подчеркивает
тот факт, что при произвольном характере занятия и освобождения
кусков ДП она постепенно приобретает "дырявый" вид. В ней участки
занятой памяти перемежаются освободившимися фрагментами
различного размера. Управлять эффективным использованием этого
пространства весьма сложно. Мы не будем далее углубляться в эти
вопросы. Куда более интересно рассмотреть работу с областями СК и
СД.
В процессе трансляции компилятор, обрабатывая программный модуль,
может построить только сегмент кода тех процедур, которые описаны в
нем. В процессе сборки нескольких модулей редактор связей,
включенный в состав системы программирования, просматривает код
головного модуля (с процедурой main), выделяет в нем обращения к
внешним процедурам (таким, как printf, scanf, getch и т.п.) и
присоединяет к сегменту головного модуля сегмент кода модуля
вызываемых внешних процедур (например, stdio). Далее процедура
повторяется по отношению к присоединенному модулю. В итоге
структура ДК фактически образует стек сегментов кода собранных
модулей (табл. 3.2
), необходимых для решения задачи. Стек формируется
в процессе подготовки кода к выполнению (сборки программного
сегмента) и не изменяется во время решения задач. В этом смысле он
статичен.
Второй сегмент - сегмент данных формируется в процессе решения
задачи. В его основу ложится статическая часть - переменные,
описанные на уровне модуля (на самом деле всех собранных модулей),
то есть вне кода процедур. Эта память никогда не перераспределяется.
Вплотную к ней примыкает память, соответствующая переменным
головной процедуры main. Эта процедура активна во все время
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
83

работы программы, поэтому тоже, по сути, является статической частью
сегмента данных.
Формирование остальной части области переменных происходит в
чистом виде по принципу работы стека (последний пришел - первый
ушел). Фактически происходит следующее. При обращении к процедуре
i+1-го уровня с прототипом
void Fill(char Ch, int K, char *Array);
(будем считать, что процедура main находится на нулевом уровне) в
контексте вызова (в вызывающей процедуре) к сформированному на i-м
уровне стеку данных добавляются поля данных, размер которых
определяется списком передаваемых параметров. Скажем, 1 байт для
символа, 4 байта для целого значения, 8 байт для указателя (табл. 3.2
).
Таблица 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);
. . . . . .
}
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
84

Такой вызов приведет к тому, что в поле 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-го байта выделенного
фрейма.
Таблица 3.3. Фрейм стека данных процедуры Fill
0 1 2 3 4 5 6
стек i-го
уровня
char K Array i Pointer
свободная
память
0 1 ... 4 5 ... 12 13 ... 16 17 ... 24
Поскольку головная программа передала в процедуру адреса своих
областей данных, то Fill получает возможность отправлять значения
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
85

в область адресов стека 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-
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
86

представления?
Реализуйте на языке Си функцию замены подстроки в строке на
заданную последовательность символов.
Реализуйте на языке Си следующую задачу: "Заменить в исходной
строке все цифры на соответствующее количество символов "* ",
равное значению цифры (0 заменять на пустую подстроку, 9 - на
*********). Строка передается процедуре-функции как входной/
выходной параметр". Рекомендуемое имя для процедуры -
Convert String.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
87

Препроцессор, оформление программы и средства
ввода/вывода
Языки программирования обеспечивают плодородную почву для
спорных идей.
Никлаус Вирт
В программах, написанных на языке Си, вы часто встречаете директивы
препроцессора, или, другими словами, символьного макропроцессора.
Все эти директивы записываются с новой строки текста и начинаются с
символа "#".
4.1. Макроподстановки
Типичным примером подобной директивы является строка
#include <параметры>
или
#include "параметры"
Оба вида означают необходимость подставить в это место текст файла,
определяемого параметрами директивы. В случае использования
угловых скобок файл ищется в директории библиотеки компилятора, а в
случае записи имени файла в кавычках - в текущей директории.
Как правило, подставляемые таким образом файлы в свою очередь
содержат дополнительные директивы, определяющие правила
генерации подставляемого текста.
Самой простой директивой можно считать строку
#define < левая часть > < правая часть >
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
88

В ней <левая часть> и <правая часть> - не что иное, как строки
символов, разделенные пробелами. Левая часть определяет имя или
прототип макроподстановки, а правая - соответствующие
подставляемые значения, например:
#define TRUE (1)
#define FALSE (0)
#define NULL (0)
#define FOREVER while(l)
После подобных макроопределений препроцессор Си везде, где
встретит указанные имена, будет заменять их на соответствующие
значения. Исключение составляют только символьные константы,
внутри них замена не производится.
При подстановке могут использоваться параметры прототипа
макроопределения. Используя такую директиву, можно определить
полезные операции над отдельными битами и группами бит
(параметры в правой части заключены в круглые скобки):
/* Manipulation of single bits: */
/* (b) is the bit to be manipulated (0..?, rvalue) */
/* (w) is the value who's bits are to be altered (LVALUE) */
/* (v) is the value who's bits are to be tested (rvalue) */
#define BITSET(b, w) ( (w) |= (1 << (b)) ) /* VOID type */
#define BITCLR(b, w) ( (w) &= ~(1 << (b)) ) /* VOID type */
#define ISBITSET(b, v) ( (v) & (1 << (b)) ) /* BOOL type */
#define ISBITCLR(b, v) (~(v) & (1 << (b)) ) /* BOOL type */
#define GETBITS(b, n, v) ( ((v) >> (b)) & ( (1 << (n)) – 1))
#define PUTBITS(b, n, v, x) ( (((x) & ((1 << (n)) – 1)) << (b) ) \
| ( (v) & ~(((1 << (n)) - 1) << (b))) )
Директивами #if (#ifdef, #ifndef), #else, #endif можно гибко
управлять подстановкой текста, включая или исключая из него
требуемые фрагменты. При этом директивы #define и #undef
позволяют управлять предысторией процесса, устанавливая или
сбрасывая установки объектов макроподстановки.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
89

Препроцессор активно используется в проектировании модулей
программной системы. Так, для большинства пользователей модуля не
представляет интереса способ реализации экспортируемых им функций,
но для работы с ним необходимо иметь прототипы вызовов и
определения собственных типов (структур) этого модуля.
4.2. h-файлы и программные модули
Для этой цели в языке Си предусмотрено использование заголовочных
файлов (h-file). Такой файл содержит описания заголовков (прототипов)
процедур и описания пользовательских типов, использующихся при
обращении к этим процедурам. В состав программы пользователя
подобный заголовочный (header) файл включается с помощью
директивы #include. Имя подобного файла обычно совпадает с
именем модуля.
Знание состава h-файла модуля позволяет программистампользователям писать свои программы не отвлекаясь на то, как
соответствующая функция будет реализована в модуле. Таким образом
удается распараллелить между несколькими разработчиками написание
программ большой системы. Им оказывается достаточным договориться
о прототипах вызовов и поддерживать в актуальном состоянии
библиотеку h-файлов системы.
Для средней системы (50-100 тысяч строк программного текста) обычно
оказывается достаточно поддерживать в процессе разработки три
уровня заголовочных файлов. Нижний уровень соответствует
программным модулям. Промежуточный - функциональным областям
(ФО). Функциональная область - это группа модулей, отвечающих за
реализацию некоторой локальной функции системы, например расчет
местоположения, планирование маневра, управление двигателем и т.п.
Верхний уровень (обычно не более двух файлов) содержит определения
типов и констант, общих для всей системы. В том числе в одном из
файлов верхнего уровня могут вводиться принятые в проекте
соглашения об именах (см. определения типа BITSET). Во втором описания типов данных: структуры, перечислимые типы, именные
константы, которые являются общими для всего проекта в целом.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
90
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
