Партнерка на США и Канаду по недвижимости, выплаты в крипто

  • 30% recurring commission
  • Выплаты в USDT
  • Вывод каждую неделю
  • Комиссия до 5 лет за каждого referral

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

fn = sin; /* Настройка указателя на функцию sin */

a = fn(x); /* Вызов функции sin через указатель */

fn = cos; /* Настройка указателя на функцию cos */

b = fn(x); /* Вызов функции cos через указатель */

Можно описать массив указателей на функцию и проинициализировать его:

double (*fnArray[3])(double x) = { sin, cos, tan };

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

for(i=0; i<3; i++)

printf( "F(x) = %lf\n", fnArray[i](x) );

Можно описать функцию возвращающую значение указателя на функцию:

double (*fnFunc(int i)) (double x)

{

switch(i)

{

case 0 : return sin;

case 1 : return cos;

case 2 : return tan;

}

}

Описанная функция имеет параметр типа int и возвращает значение указателя на функцию с аргументом типа double, возвращающую значение типа double.

После описания функции fnFunc становится возможным следующий цикл:

for(i=0; i<3; i++)

printf( "F(x) = %lf\n", fnFunc(i)(x) );

9.10.  Оператор typedef

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

typedef описание_одного_имени

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

НЕ нашли? Не то? Что вы ищете?

typedef double DArray[100];

...

DArray A, B, C;

Если бы в первом описании отсутствовало бы ключевое слово typedef, то имя DArray представляло бы имя массива из 100 элементов типа double, для которого бы выделялся соответствующий объем памяти. При наличии typedef компилятор будет воспринимать имя DArray как имя нового типа данных, а именно, типа массива из 100 элементов типа double. Очевидно, никакой памяти при этом не выделяется.

Во втором описании используется имя нового типа DArray. Каждое из определяемых имен A, B и C будет считаться массивом из ста элементов типа double, и для каждого из них будет выделен соответствующий объем памяти.

Описания указателей на функции из предыдущего раздела можно существенно упростить, используя оператор typedef:

typedef double (*Fun)(double x); /*Тип указателя*/

Fun fnArray[3] = { sin, cos, tan }; /*Массив функций*/

Fun fnFunc(int i) /* Функция, возвращающая функцию */

{

switch(i)

{

case 0 : return sin;

case 1 : return cos;

case 2 : return tan;

}

}

Совершенно очевидно, что последние описания значительно понятнее.

9.11.  Дополнительные описания указателей для IBM PC

Рассмотрим некоторые особенности режимов работы процессоров, используемых в компьютерах IBM PC. При этом следует учитывать, что процессоры фирмы Intel с типом ниже 80386 обеспечивают 16-ти битный режим работы, а процессоры 80386 и выше - как 16-ти, так и 32-битный режимы.

Типичный режим работы процессора - 16-битный, который обеспечивается собственно системой MS DOS, или DOS-сессией эмулируемой 32-битной системой Windows-95, Windows NT или OS/2.

В этом режиме процессор может использовать и двухбайтовые и четырехбайтовые адреса.

При использовании четырехбайтовых адресов процессор в 16-ти битном режиме может адресовать область памяти не более, чем в 1 мегабайт. При этом, адрес состоит из двух частей: так называемой сегментной части, которая находится в старших 2 байтах адреса, и смещения, содержащегося в младших 2 байтах адреса.

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

Физический_адрес = seg * 16 + offs, где seg - двухбайтовый сегментный адрес, offs - двухбайтовое смещение.

Одно двухбайтовое смещение может адресовать не более, чем 64 килобайта памяти (216), то есть так называемый сегмент. Добавление сегментной части к смещению по вышеприведенной формуле и обеспечивает адресацию 1M памяти. Однако, при такой трактовке адреса различные адреса могут указывать на один и тот же байт памяти. Рассмотрим три адреса 246:330, 256:170 и 266:10. И сегментная часть адреса, и смещение в этих адресах записаны в десятичной системе счисления, через двоеточие. Рассчитаем физические адреса для каждого из этих значений:

246 * 16 + 330 = 4266

256 * 16 + 170 = 4266

266 * 16 + 10 = 4266

Из расчета видно, что разные адреса определяют один и тот же физический адрес памяти. Это и объясняет, почему в некоторых случаях сравнение указателей может происходить некорректно. Для устранения этого противоречия вводится понятие нормализованного адреса, то есть такого адреса, у которого значение смещения не превышает 16. Последний из трех адресов примера - нормализованный.

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

Для обозначения соответствующих адресов, используются специальные ключевые слова: near - обозначает 2-х байтовый (близкий) адрес, far - 4-х байтовый (дальний) адрес.

Все действия над адресами типа far выполняются так, что их сегментная часть не меняется. Это позволяет несколько ускорить операции с указателями, но накладывает ограничение в 64K на массив, адресуемый указателем. Если массив должен быть больше 64K, то следует использовать указатели типа huge, которые автоматически поддерживают нормализацию адреса и, поэтому, могут адресовать массив больший 64K.

Примеры описания подобных указателей:

int near *pi; /* 2-х байтовый указатель */

char far *name; /* 4-х байтовый без нормализации */

double huge *pA; /* 4-х байтовый с нормализацией */

Использовать вышеприведенные описатели указателей можно только при полной уверенности в своих действия. Значительно более просто использовать различные типы адресов, меняя модели памяти. Рассмотрим 16-разрядные модели памяти IBM PC.

В крошечной (Tiny) модели памяти сегментные части всех адресов указывают на один и тот же сегмент, размером не более 64K, в котором располагается и код программы, и данные, и стек. Все адреса двухбайтовые (near).

В маленькой (Small) модели памяти сегментная часть адреса кода указывает на один сегмент, размером не более 64K, сегментная часть адресов данных указывает на другой сегмент, размером не более 64K, в котором располагаются данные и стек. Все адреса двухбайтовые (near).

В средней (Medium) модели памяти адреса кода 4-х байтовые (far), то есть размер кода может достигать 1M. Сегментная часть адресов данных указывает на сегмент, размером не более 64K, в котором располагаются данные и стек. Адреса данных двухбайтовые (near).

В компактной (Compact) модели памяти адреса кода 2-х байтовые (near), то есть размер кода не может превышать 64K. Адреса данных 4-х байтовые (far), то есть размер данных может достигать 1M. Однако, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается значительно меньше, например 4K.

В большой (Large) модели памяти все адреса 4-х байтовые (far), то есть и размер кода, и размер данных может достигать 1M. Однако, как и в предыдущей модели, максимальный размер статических данных и стека не превышает 64K. По умолчанию стек устанавливается размером 4K.

В громадной (Huge) модели памяти все организовано так же как и в большой, но размер статических данных может достигать 1M.

Следует обратить внимание на то, что ни в одной модели памяти (даже в huge) нет указателей на данные типа huge. Поэтому работа с массивами большими 64K требует специального описания указателей.

В 32-битных режимах работы 386 процессоров far-адрес состоит из 2-х байтового селектора сегмента и 4-х байтового смещения в сегменте. При этом размер смещения позволяет адресовать 4-х гигабайтное адресное пространство (232).

Для 32-х разрядных режимов могут существовать все вышеперечисленные модели памяти. Однако на практике чаще всего используется так называемая плоская (flat), безсегментная модель памяти. На самом деле она соответствует модели Small с учетом того, что размер сегмента может достигать 4 гигабайт, а смещение в сегменте имеет размер 4 байта. Можно считать, что во flat модели сегментов нет вообще, размер адреса равен 4-м байтам и соответствует физическому адресу (виртуальному) памяти компьютера.

9.12.  Непосредственная работа с экранной памятью

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

Рассмотрим принцип организации экранной памяти для вывода информации на экран в текстовом режиме. Каждому знакоместу экрана соответствует слово памяти (2 байта). Младший байт содержит код символа, старший атрибут символа, устанавливающий его цвет. В свою очередь, атрибут символа в старших четырех битах содержит цвет фона (самый старший бит может устанавливать режим мигания), а в младших четырех битах - цвет символа. Вычислить атрибут символа, зная цвет фона - fon и цвет символа sym, можно по следующей формуле:

attr = fon * 16 + sym;

или

attr = fon << 4 | sym;

Таким образом, каждой строке экрана соответствует 160 байт экранной памяти. Экранная память начинается с адреса 0xB8000000L. Буква L в конце адресной константы необходима для того, чтобы компилятор создал константу длиной 4 байта.

Ниже приведен текст функции, выводящей строку символов, на которую указывает указатель str, начиная с колонки col из строки row экрана. При выводе строки используется атрибут attr.

void PutsXY (int col, int row, char *str, int attr)

{

char far *p = (char far *) 0xB8000000L;

p+= 160*(row-1)+2*(col-1); /* Начало строки */

while (*str)

{

*(p++) = *(str++); /* Вывод символа */

*(p++) = attr; /* Вывод атрибута */

}

}

Единственное замечание, которое следует сделать, заключается в том, что для адресации экранной области памяти используется дальний (far) указатель на символы. Такой указатель необходимо использовать для того, чтобы независимо от того, в какой 16-разрядной модели памяти создается программа (даже в Tiny), была возможной адресация этой памяти.

Кроме того, следует обратить внимание на то, что целочисленную константу необходимо явно преобразовать к типу длинного указателя на символы. В противном случае компилятор будет считать инициализацию некорректной.

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

void PutAttr (int col, int row, int len, int attr)

{

char far *p = (char far *) 0xB8000000L;

p+= 160*(row-1)+2*(col-1)+1;

while (len--)

{

*p = attr; /* Вывод атрибута */

p += 2; /* Переход к следующему атрибуту */

}

}

Рассмотренную функцию можно использовать для организации всякого рода световых меню.

10.  Дополнительные сведения о функциях

10.1.  Области видимости и глобальные данные

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

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

Имена, объявленные в блоке, видны и могут быть использованы только в этом блоке, причем, в случае конфликта имен, локальное объявление данных создает совершенно новую переменную и перекрывает глобальное объявление или объявление более высокого уровня. После выхода из блока становится доступным старое имя с объявлением более высокого уровня. Это не относится к макросам препроцессора, которые ведут себя, как и при файловом определении (препроцессор не обязан знать синтаксис и семантику языка программирования).

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

#include <stdio. h>

int i=1;

void PrintI(void)

{

printf( "i = %d\n", i );

}

void main(void)

{

int i=10;

printf( "i = %d\n", i );

PrintI();

{

int i=100;

printf( "i = %d\n", i );

PrintI();

}

printf( "i = %d\n", i );

PrintI();

}

будет напечатано

i = 10

i = 1

i = 100

i = 1

i = 10

i = 1

Это произошло потому, что локальные определения имени i перекрывают объявления более высокого уровня, создают и инициализируют новые локальные переменные с именем i. После выхода из очередного блока его локальные переменные уничтожаются и открывается доступ к соответствующей переменной охватывающего блока. Функция PrintI() всегда имеет дело с переменной i, описанной на файловом (глобальном) уровне, потому, что локальной переменной i в функции PrintI() нет. Все три переменные, описанные в примере, соответствуют разным ячейкам памяти, несмотря на одинаковые имена.

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

Никогда не следует делать глобальными вспомогательные переменные функции, например переменные циклов. В следующей программе использование глобальной переменной цикла приводит к ошибке, которую трудно обнаружить:

#include <stdio. h>

int i;

int Sum(int A[], int n)

{

int s = 0; /* Сумма одномерного массива */

for(i=0; i<n; i++) s += A[i];

return s;

}

void main(void)

{

int B[3][2] = { { 1, 1 },

{ 2, 2 },

{ 3, 3 } };

int s = 0; /* Сумма двумерного массива */

for(i=0; i<3; i++) s += Sum(B[i], 2);

printf("s = %d\n", s);

}

Никаких ошибок компиляции в программе нет. Если рассматривать все ее функции по отдельности, то ошибка тоже не видна. Но программа дает неверный результат: s = 2. Это происходит потому, что цикл головной программы выполняется всего один раз, так как после возврата из функции Sum(), значение глобальной переменной i будет равно 3. Если описать переменные циклов i внутри каждой функции, то программа станет выдавать правильную сумму элементов двумерного массива: s = 12.

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

extern double Speed;

говорит о том, что в каком-то из модулей программы выделена память под переменную Speed типа double и функции данного модуля могут ее использовать.

Подобное описание одного и того же имени может повторяться сколько угодно раз при условии, что все последующие описания будут в точности совпадать с первым. Смысл этого описания очень похож на смысл прототипа (описания) для функции. Действительно, это описание является инструкцией компилятору по поводу использования имени глобального данного, находящегося в каком-то другом файле. Как и прототипы функций, описания глобальных данных следует помещать в заголовочный файл программы и включать этот файл в каждый модуль программы с помощью директивы препроцессора #include.

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

static int Count;

к глобальной переменной Count будут иметь доступ только функции того модуля, в котором собственно была выделена память под переменную Count.

Спецификатор static можно использовать и совместно с функциями, тогда их использование тоже будет ограничено одним модулем. Например, при описании

static double cotan(double x);

функция cotan будет доступна только в том модуле (файле), где она определена, то есть, где находится ее тело.

10.2.  Время жизни переменных и классы памяти языка Си

Время жизни переменных программы определяется классом памяти. В языке Си принято различать статические (static), автоматические (auto) и динамические данные.

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

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

Локальную переменную функции или блока тоже можно сделать статической, используя ключевое слово static. В следующем примере используется локальная статическая переменная функции для того, чтобы отличить первый вызов функции от последующих:

#include <stdio. h>

void fun(void)

{

static int first = 1;

if( first )

{

printf("Это первый вызов функции.\n");

first = 0;

}

else

printf("Это не первый вызов функции.\n");

}

void main(void)

{

fun(); fun(); fun();

}

Статическая переменная first функции fun() создается и инициализируется при запуске программы, до выполнения функции main(). Поэтому, при первом вызове функции fun() значение first равно 1, при последующих вызовах fun() переменная first будет сохранять свое значение, измененное функцией fun() на 0. Подобным же образом можно организовать подсчет числа обращений к функции.

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

Все локальные данные и формальные параметры функций автоматические по умолчанию. Никаких специальных описаний для этого не требуется.

Если в предыдущем примере убрать ключевое слово static из описания переменной first, то функция fun() при каждом вызове будет вести себя как и при первом. Это происходит потому, что переменная first стала автоматической, создается и инициализируется при каждом вызове функции fun().

Глобальные данные не могут быть автоматическими, потому что нет блока, которому они принадлежат.

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

10.3.  Передача аргументов в функцию

В правильно организованной функции использование списка аргументов (параметров) и возвращаемого значения является единственным способом связи этой функции с остальными.

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

Рассмотренный алгоритм вызова функции гарантирует сохранение значений фактических параметров независимо от того, что делала функция с соответствующими формальными параметрами. Рассмотрим функцию Sum(), вычисляющую значение суммы элементов массива:

#include <stdio. h>

double Sum(double A[], int nA)

{

double s = 0;

while(nA) s += A[--nA];

return s;

}

void main (void)

{

double B[] = { 1, 2, 3, 4, 5 };

int nB = sizeof(B)/sizeof(B[0]);

printf("Сумма = %lf\n", Sum(B, nB));

printf("nB = %d\n", nB);

}

Функция при своей работе изменяет значение формального параметра nA, в котором передается размер массива. Однако, если запустить программу, то можно увидеть, что значение соответствующего фактического параметра nB осталось неизменным, ввиду того, что функция работает с копиями параметров.

При фактическом параметре-массиве в функцию передается значение его имени, то есть адрес его первого элемента, для которого создается копия в стеке, которая уже не является адресом-константой и допускает изменение своего значения внутри функции. Поэтому функцию Sum можно реализовать и следующим образом:

double Sum(double A[], int nA)

{

double s = 0, *Aend = A + nA;

while( A < Aend ) s += *(A++);

return s;

}

Соответствующий фактический параметр - константный адрес массива B в вызывающей функции не меняет своего значения, то есть никаких противоречий в предыдущей функции нет.

Вообще говоря, для формальных параметров-массивов описания в виде A[] и *A совершенно идентичны и обозначают локальную копию адреса соответствующего типа. Какое из этих описаний использовать, зависит от смысла параметра. Если это массив, то более наглядно использовать описание вида A[].

10.4.  Возврат значений из функций

Возврат результата работы функции в вызывающую программу в виде единственного значения можно осуществить с помощью оператора return. При этом результат возвратится как значение самой функции и должен иметь соответствующий тип.

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

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

void FillArray(double A[], int nA, double val)

{

int i;

for (i=0; i<nA; i++) A[i] = val;

}

void main (void)

{

double B[100];

FillArray(B, 40, 35.4);

/* ... */

FillArray(&B[60], 20, 15.4);

/* ... */

}

Первый вызов FillArray() заполняет 40 первых элементов массива B значением 35.4, второй вызов заполняет 20 элементов массива B, начиная с элемента B[60], значением 15.4. При возврате из функции массив будет изменен, т. к. занесение значения val происходит непосредственно по нужному адресу.

Эту же функцию можно использовать для заполнения строк двумерного массива:

void main (void)

{

double a[10][20];

int n = sizeof(a) / sizeof(a[0]);

int m = sizeof(a[0]) / sizeof(a[0][0]);

int i;

/* ... */

for(i=0; i<n; i++ )

FillArray(a[i], m, 14.6);

/* ... */

}

В примере следует обратить внимание на соответствие типов передаваемых параметров и на способ вычисления числа строк и числа столбцов двумерного массива.

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

void Decart(double *px, double *py, double r, double f)

{

(*px) = r * cos(f);

(*py) = r * sin(f);

}

При обращении к данной функции для параметров px и py нужно передавать адреса:

void main(void)

{

double x, y, r=5, f=0.5;

/* ... */

Decart( &x, &y, r, f );

/* ... */

}

В данном примере при вызове функции создаются локальные копии адресов переменных x и y, а внутри функции происходит обращение к переменным x и y через их адреса (как и в случае массивов), поэтому значения x и y после вызова функции будут изменены.

11.  Работа с динамической памятью

11.1.  Стандартные функции управления динамической памятью

Данные, которые создаются, инициализируются и уничтожаются по требованию программиста называются динамическими. Для управления такими данными используются специальные стандартные функции, прототипы которых описаны в заголовочном файле <malloc. h> (для некоторых компиляторов <alloc. h>).

Для запроса динамической памяти служит функция malloc(), которая имеет следующий прототип:

void * malloc(size_t size);

Функция malloc() выделяет область динамической памяти, размером size байт, и возвращает адрес этой области памяти.

Параметр size, имеет тип size_t, который описан в файле <malloc. h> с помощью оператора typedef и используется для описания размеров, счетчиков и т. д. Обычно тип size_t соответствует типу unsigned int.

В том случае, когда функция malloc() не может удовлетворить запрос на память, она возвращает значение NULL, то есть значение не существующего указателя. Константа NULL описана в заголовочном файле <malloc. h>. Значение NULL возвращается и в том случае, когда значение параметра size нулевое.

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

После того, как выполнена вся работа с выделенной областью памяти, ее следует освободить с помощью функции free(), имеющей следующий прототип:

void free(void *block);

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

Если при вызове функции free() значение указателя block не соответствует адресу, возвращенному функцией выделения памяти, то результат выполнения функции free() непредсказуем, а область динамической памяти может быть вообще разрушена.

Не допускается также освобождать уже освобожденный блок памяти.

Значение параметра block равное NULL не вызывает никаких действий со стороны функции free();

Рассмотрим типичную последовательность действий при работе с динамической памятью:

double *A; int n;

...

n = 200;

...

A = (double *) malloc( n * sizeof(double) );

...

/* Работа с массивом A */

...

free(A);

В рассмотренном фрагменте программы выделяется память для хранения n элементов типа double. В целях совместимости никогда не следует явно задавать размер элемента данных. Нужно пользоваться операцией sizeof(). Возвращаемое функцией malloc() значение преобразуется к типу указателя на double.

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

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

void * calloc(size_t nitems, size_t size);

Функция выделяет непрерывный блок памяти для nitems элементов данных размером size байт каждый и заполняет этот блок нулевыми значениями. В остальном работа ее аналогична работе функции malloc().

Функция realloc() служит для изменения размера ранее выделенного блока памяти:

void *realloc(void *block, size_t size);

Здесь block - адрес ранее выделенного блока памяти, size - новый размер блока в байтах. Функция возвращает значение нового указателя на блок памяти, которое может и не совпадать со старым.

Функция гарантирует сохранность данных в блоке, разумеется, сохранность не более size байт. В остальном работа функции совпадает с работой ранее рассмотренных функций выделения памяти.

Все рассмотренные функции могут выделять память размером не более одного сегмента, то есть не более 64K в 16-ти разрядных моделях и не более 4G в 32-х разрядных моделях памяти.

При работе с динамической памятью следует иметь в виду, что в каждом выделенном блоке несколько байт отводится на служебную информацию. Так в 16-ти разрядной Large модели память выделяется блоками по размеру кратными 16 байтам, и в каждом блоке 4 байта служебные.

К сожалению, стандартные средства работы с динамической памятью не предусматривают "сборку мусора", то есть автоматическое перемещение выделенных блоков в динамической памяти так, чтобы между ними не было неиспользуемых промежутков. Поэтому от программиста требуется повышенное внимание к стратегии выделения и освобождения динамической памяти в своих программах. Иначе может получиться так, что требуемый блок памяти невозможно выделить, хотя суммарный объем неиспользуемой памяти допускает это.

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

unsigned coreleft(void); /* Маленьких модели */

unsigned long coreleft(void); /* Большие модели */

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

11.2.  Пример использования динамической памяти

В следующем примере динамическая память используется для хранения элементов массива заранее неопределенного размера, ввод которого прекращается при появлении во входном потоке числа больше или равного 1e300. При этом, вначале под массив выделяется сегмент памяти, максимального размера, затем размер блока памяти устанавливается соответствующим фактическому размеру массива.

#include <stdio. h>

#include <stdlib. h>

#include <conio. h>

#include <malloc. h>

#include <limits. h>

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

{ printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

void * Realloc ( void *block, size_t size )

{

void *p = realloc(block, size);

if( !p ) { printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

void main(void)

{

double *A, temp;

unsigned i, n, maxN, goodIO;

A = (double *) Malloc( maxN = UINT_MAX );

maxN /= sizeof(double);

for(goodIO = n = 0; n < maxN; n++)

{

printf("A[%d] = ", n); scanf("%lf", &temp);

if(temp >= 1e300) { goodIO = 1; break; }

A[n] = temp;

}

if(goodIO)

{

A = (double *) Realloc(A, n * sizeof(double));

/* Обработка массива. Для примера - печать. */

for(i = 0; i < n; i++)

{

printf("%10.3lf ", A[i]);

if( (i + 6) % 5 == 0 ) printf("\n");

if( (i + 121) % 120 == 0 ) { getch(); clrscr(); }

}

printf("\n");

}

free(A);

}

Максимальный размер сегмента в байтах всегда равен величине наибольшего беззнакового целого числа, значение которого определяет константа UINT_MAX из заголовочного файла <limits. h>.

В программе используются вспомогательные функции Malloc() и Realloc() для обеспечения контроля выделения памяти. В них функция exit() с прототипом в файле <stdlib. h> используется для прерывания работы программы.

11.3.  Особенности работы с двумерными массивами

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

11.3.1.  Пересчет индексов вручную

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

#include <stdio. h>

#include <stdlib. h>

#include <malloc. h>

#define MAXVAL 1000

void *Malloc ( size_t size );

void RandomMatr ( double *Matr, int n, int l );

void OutMatr ( char *name,

double *Matr, int n, int m );

void main( void )

{

size_t n = 5, m = 6;

double *A;

/* Выделение памяти под матрицу */

A = (double *) Malloc( n*m*sizeof(double) );

/* Заполнение матрицы значениями и распечатка */

RandomMatr(A, n, m);

OutMatr("A", A, n, m);

/* освобождение памяти */

free(A);

}

void RandomMatr (double *Matr, int n, int m)

{

int i, j;

for(i = 0; i < n; i++)

for(j = 0; j < m; j++)

Matr[i*m+j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double *Matr, int n, int m )

{

int i, j;

printf("\nМатрица %s\n----\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < m; j++)

printf("%8.1lf ", Matr[i*m+j]);

printf("\n");

}

}

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

{ printf("Недостаточно памяти!\n"); exit(1); }

return p;

}

Функция rand() с прототипом из <stdlib. h> возвращает псевдослучайное число в диапазоне от 0 до MAXVAL-1.

11.3.2.  Массивы с постоянной длиной строки

Если у массива длина строки постоянная, то адресацию динамического двумерного массива может выполнить компилятор:

#include <stdio. h>

#include <stdlib. h>

#include <malloc. h>

#define MAXVAL 1000

#define STRLEN 6

void *Malloc ( size_t size );

void RandomMatr ( double (*Matr)[STRLEN], int n );

void OutMatr ( char *name,

double (*Matr)[STRLEN], int n );

void main( void )

{

size_t n = 5;

double (*A)[STRLEN];

/* Выделение памяти под матрицу */

A = (double (*)[STRLEN])

Malloc( n*sizeof(double[STRLEN]) );

/* Заполнение матрицы значениями и распечатка */

RandomMatr(A, n);

OutMatr("A", A, n);

/* освобождение памяти */

free(A);

}

void RandomMatr (double (*Matr)[STRLEN], int n)

{

int i, j;

for(i = 0; i < n; i++)

for(j = 0; j < STRLEN; j++)

Matr[i][j] = random(MAXVAL) + 1;

}

void OutMatr( char *name, double (*Matr)[STRLEN], int n )

{

int i, j;

printf("\nМатрица %s\n----\n", name);

for(i = 0; i < n; i++)

{

for(j = 0; j < STRLEN; j++)

printf("%8.1lf ", Matr[i][j]);

printf("\n");

}

}

void * Malloc( size_t size )

{

void *p = malloc(size);

if( !p )

{ printf("Недостаточно памяти!\n"); exit(1); }

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7