Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практикум по программированию на языке Си

..pdf
Скачиваний:
24
Добавлен:
12.11.2023
Размер:
3.53 Mб
Скачать

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

ЗАДАЧА 07-12. Определите и инициализируйте произвольными значениями вещественные переменные с именами a, b, c, d, e, f, g, h. Поменяйте местами значения переменных так, чтобы переменная с именем a приняла максимальное значение, а остальные (в приведенном порядке) соответственно убывали. Напечатайте имена переменных с их новыми значениями.

/* 07_12.c - сортировка значений разноименных переменных */

#include <stdio.h>

#define PRINTF(EXPRESSION) \ printf(#EXPRESSION"=%f\n",EXPRESSION)

int main ()

{

double a=12.0, b=34.1, c=11.9, d=92.4, e=55.5, f=16.5, g=55.5, h=77.2;

double * pArray[] = {&a,&b,&c,&d,&e,&f,&g,&h}; double temp;

int size, i, j; size=sizeof(pArray)/sizeof(*pArray); for (i=0; i<size-1; i++)

for (j=i+1; j<size; j++)

if (*pArray[i] < *pArray[j]) { temp=*pArray[i];

*pArray[i] = *pArray[j]; *pArray[j] = temp;

}

PRINTF(a);

PRINTF(b);

PRINTF(c);

PRINTF(d);

PRINTF(e);

PRINTF(f);

PRINTF(g);

241

PRINTF(h); return 0;

}

Результаты выполнения программы:

a=92.400000

b=77.200000

c=55.500000

d=55.500000

e=34.100000

f=16.500000

g=12.000000

h=11.900000

Основное изменение по сравнению с программой 07_11.с – при сортировке меняются значения не элементов массива указателей, а значения тех переменных, которые ими адресованы. Вспомогательная переменная double temp имеет тот же тип, что и изменяемые переменные. Для печати использован макрос.

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

Совершенно естественно использовать массив указателей для доступа по заданному закону (или по разным законам) не к разноименным переменным (объектам), а к элементам другого массива.

ЗАДАЧА 07-13. Присвойте элементам массива указателей адреса элементов одномерного массива и так упорядочите адреса, чтобы с помощью массива указателей вывести значения элементов анализируемого одномерного массива в порядке их возрастания.

242

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

/* 07_13.c - упорядочение массива с помощью массива указателей */

#include <stdio.h> #define N 5

int main()

{

int j, k;

long ar[N]={64,12,82,4,6}; long *par[N];

long *pi;

for (j=0; j<N; j++) par[j]=&ar[j]; for (j=0;j<N-1;j++)

for (k=j+1;k<N;k++)

if (*par[j] > *par[k])

{ pi=par[j]; par[j]=par[k]; par[k]=pi; }

printf

("\nThe ordered values of array elements:\n"); for (j=0; j<N; j++)

printf("*par[%d]=%ld ",j,*par[j]);

}

Результаты выполнения программы:

The ordered values of array elements:

*par[0]=4 *par[1]=6 *par[2]=12 *par[3]=64 *par[4]=82

ЗАДАНИЕ. Дополните программу печатью значений элементов основного массива до упорядочения и после. Убедитесь, что массив остается без изменений.

ЗАДАНИЕ. Расширьте условие задачи следующим образом. После печати результатов первого упорядочения “перенастрой-

243

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

С помощью массива указателей типа void * можно сортировать объекты и массивы разных типов.

ЗАДАЧА 07-14. Определите и инициализируйте произвольными значениями два массива равной длины с символьными и с вещественными элементами. Используя массив с элементами типа void *, выведите значения элементов массивов в порядке возрастания.

/* 07_14.c - упорядочение с помощью массива типа viod * [] */

#include <stdio.h> int main ()

{

float ar[]={12.0f, 34.1f, 11.9f, 92.4f, 66.6f, 17.0f};

char ch[]={'g', 'e', 's', 'r', 'm', 'k'}; void * arrow[] =

{&ar[0],&ar[1],&ar[2],&ar[3],&ar[4],&ar[5]}; void * pTemp;

int size, i, j; size=sizeof(ar)/sizeof(*ar); for (i=0; i<size-1; i++)

for (j=i+1; j<size; j++)

if (*(float *)arrow[i] > *(float *)arrow[j])

{pTemp=arrow[i]; arrow[i] = arrow[j]; arrow[j] = pTemp;

}

for (i=0; i<size; i++) printf("%c[%d]=%6.2f",i%4?'\t':'\n',i,

*(float *)arrow[i]); for (i=0; i<size; i++)

arrow[i] = &ch[i];

for (i=0; i<size-1; i++) for (j=i+1; j<size; j++)

if (*(char *)arrow[i] > *(char *)arrow[j]) { pTemp=arrow[i];

244

arrow[i] = arrow[j]; arrow[j] = pTemp;

}

for (i=0; i<size; i++) printf("%c[%d]=%c",i%4?'\t':'\n',i,

*(char *)arrow[i]);

return 0;

}

Результаты выполнения программы:

[0]= 11.90

[1]= 12.00

[2]= 17.00

[3]= 34.10

[4]= 66.60

[5]= 92.40

 

 

[0]=e

[1]=g

[2]=k

[3]=m

 

 

[4]=r

[5]=s

 

 

 

 

Сначала массив void

*arrow[] инициализирован адресами

элементов массива float ar[].

При сортировке сравниваются зна-

чения элементов ar[i], доступ к которым обеспечивает выражение *(float *)arrow[i]. Вспомогательный указатель void *pTemp используется для обмена значениями элементов массива указателей. Выполняется печать значений элементов массива ar[], адресованных элементами массива arrow[]. Затем в цикле массив arrow[] “настраивается” на элементы массива char ch[]и по той же схеме упорядочиваются адреса элементов массива ch[]. Для доступа к значениям элементов массива ch[] используется выражение

*(char *)arrow[i].

ЭКСПЕРИМЕНТ. Напечатайте в конце программы массивы ar[ ] и ch[ ], выводя для каждого элемента имя, индекс и значение. Убедитесь, что значения элементов обоих массивов те же, что и при инициализации.

ЗАДАНИЕ. Модифицируйте предыдущую программу, чтобы в ней с помощью массива указателей void * упорядочивались (изменялись) значения элементов вещественного и символьного массивов. Выведите на печать отсортированные массивы, обращаясь к их элементам по имени массива с индексом.

Задачу решает программа 07_14_1.с. Основные изменения – при перестановках используется не адрес arrow[i], а выражения

245

*(float *)arrow[i] и *(char *)arrow[i]. Понадобятся вспо-

могательные переменные fTemp и cTemp тех же типов, что и элементы соответствующего массива.

Массив указателей – это объект, размещенный в памяти, следовательно, он имеет адрес, и его адрес можно присвоить указателю. Тип этого указателя type **, где type – наименование типа объектов, адресуемых элементами массива указателей.

ЗАДАЧА 07-15. Определите и инициализируйте несколько символьных переменных и массив указателей, элементы которого адресуют введенные символьные переменные. Определите указатель на этот массив указателей и с его помощью выведите значения символьных переменных, перебирая их в обратном порядке.

Решение:

/* 07_15.c - указатель на массив указателей */ #include <stdio.h>

int main ()

{

char c1='r',c2='e',c3='b',c4='m',c5='u',c6='n'; char * pChar[] = {&c1,&c2,&c3,&c4,&c5,&c6}; char * * pointer;

int size; size=sizeof(pChar)/sizeof(*pChar); pointer = &pChar[size-1];

do

{ printf("%c",**pointer); } while (pointer-- != pChar); return 0;

}

Результат выполнения программы:

number

В программе указателю pointer присваивается адрес последнего элемента массива указателей pChar[ ], затем в цикле печатаются

246

значения символьных переменных c6,…,c1. Доступ к каждому из значений обеспечивает двойное разыменование указателя pointer. В конце каждой итерации значение pointer сравнивается с адресом начала массива pChar[ ]. После сравнения pointer уменьшается на 1, т.е. "перемещается" на предыдущий элемент массива.

7.5. Указатели и динамическое

распределение памяти

Динамическое выделение и освобождение памяти реализуется в программах на языке Си с помощью функций стандартной библиотеки. Они описаны в заголовочном файле <stdlib.h>. Для выделения памяти применяются функции:

void * malloc(size_t size);

void * calloc(size_t memb, size_t size); void * realloc(void * ptr, size_t size);

Каждая из этих функций выделяет непрерывный блок (участок) памяти и возвращает нетипизированный указатель на его начало. В случае неудачи, например, при отсутствии свободной памяти функции возвращают значение NULL. Тип аргумента (т.е. идентификатор size_t) определен в заголовочном файле <stddef.h> и соответствует типу обобщенного целого значения, которое возвращает операция sizeof. Обычно sizeof вычисляет размер объекта в байтах, поэтому можно считать, что при обращении к любой из перечисленных функций параметр size нужно задавать в байтах.

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

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

247

Функция realloc() нужна в тех случаях, когда динамически выделяемый участок памяти должен изменять размеры в процессе выполнения программы. Если первый параметр void * ptr равен NULL, то функция выполняется как malloc(), т.е. выделяется участок памяти, размер которого определен вторым параметром size_t size. Если в качестве значения первого аргумента использован адрес уже существующего участка памяти (динамически выделенного), то функция realloc() выделит участок памяти размером size, перенесет в этот новый участок содержимое из того участка, который адресован первым аргументом. Затем функция освободит память, адресованную первым аргументом, и вернет адрес начала нового участка памяти.

Для освобождения динамически выделенного участка памяти применяется функция

void * free(void * ptr);

При обращении к функции free() нетипизированный указатель void * ptr может быть заменен указателем-аргументом любого типа. Приведение типов параметров выполняется автоматически. Функция free() освобождает адресованный ее параметром участок памяти, и он становится доступным для новых распределений.

ЗАДАЧА 07-16. Выделите память для значений типа int и double. Напечатайте адреса выделенных участков. Занесите в эти участки конкретные значения соответствующих типов и напечатайте их.

В следующей программе задача решена для типов int и double:

/* 07_16.c - выделение памяти для значений разных типов */

#include <stdio.h>

#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)

#define PRINTD(EXPRESSION) \ printf(#EXPRESSION"=%e\n",EXPRESSION)

#define PRINTP(EXPRESSION) \ printf(#EXPRESSION"=%p\n",EXPRESSION)

#include <stdlib.h> int main ()

248

{

int * pInt; double * pDouble;

pInt=(int *)malloc(sizeof(int)); if (pInt == NULL)

{ printf("\n Error!"); return 1;

}

PRINTP(pInt); *pInt = 12345; PRINTI(*pInt);

pDouble=(double *)malloc(sizeof(double)); if (pDouble == NULL)

{ printf("\n Error!"); return 1;

}

PRINTP(pDouble); *pDouble = 3.1415; PRINTD(*pDouble); free(pInt); free(pDouble); return 0;

}

Результаты выполнения программы:

pInt=8f3e8

*pInt=12345

pDouble=8f3f8

*pDouble=3.141500e+00

В программе выполняется проверка успешности выделения памяти. Функция malloc() вернет значение NULL, если память не будет выделена. В этом случае выполнение программы прекращается с выдачей сообщения "Error!". В среду исполнения программы возвращается (оператор return 1;) ненулевое значение, что свидетельствует об аварийном завершении задачи. В таком простом случае, как в данной задаче, вряд ли встретится затруднение при выделении динамической памяти, однако проверку рекомендуется выполнять всегда. Обратите внимание, что функция malloc() возвращает нетипизированное значение адреса, которое явно преобразуется к типам (int *) и (double *) перед присваиванием значений pInt и pDouble.

249

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

ЗАДАНИЕ. Удалите из программы 07_16.с операторы выделения и освобождения памяти и проверку значений указателей pInt, pDouble. В конец программы добавьте PRINTI (*pInt).

Результаты выполнения программы 07_16_1.с, соответствующей заданию:

pInt=0

*pInt=12345

pDouble=0

*pDouble=3.141500e+00 *pInt=-1065151889

Как видите, указатели pInt и pDouble равны 0, и *pInt изменилось после присваивания значения выражению *pDouble.

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

ЗАДАЧА 07-17. Определите указатели void *, double *, int *. Выделите участок памяти, достаточный для размещения значений типа long double и присвойте его адрес нетипизированному указателю. Последовательно "настраивайте" на выделенный участок памяти указатели double * и int * и заносите с их помощью в него значения соответствующих типов. Напечатайте значения указателей и значения из адресуемого участка памяти.

/* 07_17.c - значения разного типа в одном участке памяти */

#include <stdio.h>

#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)

#define PRINTD(EXPRESSION) \ printf(#EXPRESSION"=%e\n",EXPRESSION)

250