книги / Практикум по программированию на языке Си
..pdfint main ()
{
double d=0.1234;
char * pc = (char *)&d; int * pi = (int *)&d; double * pd = &d;
void * pv = &d; PRINTC(*pc); PRINTI(*pi); PRINTD(*pd); PRINTC(*(char *)pv); PRINTI(*(int *)pv); PRINTD(*(double *)pv); return 0;
}
Результаты выполнения программы (DJGPP):
*pc=є *pi=1951633139 *pd=0.123400 *(char *)pv=є
*(int *)pv=1951633139 *(double *)pv=0.123400
Как показывают результаты выполнения программы 07_04.с, указатели pc, pi, pd, pv имеют одинаковое значение. Все они адресуют один объект (переменную d). Но при их разыменовании в программе 07_04_1.с становятся доступными участки памяти разной длины. Только разыменование указателя pd и выражение *(double *)pv позволили получить правильный доступ к значению переменной d.
Для более глубокого понимания соотношений между адресами объектов (переменных) и разными возможностями доступа к значениям объектов (переменных) рассмотрим следующую задачу.
ЗАДАЧА 07-05. Определите переменную типа int и указатель типа int *, инициализированный адресом этой переменной. Продемонстрируйте разные способы доступа к участку памяти, вы-
221
деленному для переменной. (Используйте ее имя и выражения с операциями [ ], *, &.)
Следующая программа предлагает решение задачи.
/* 07_05.c - указатели, адреса, индексация, разыменование */
#include <stdio.h>
#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)
int main ()
{
int number=123;
int * pi = &number; PRINTI(number); PRINTI(*&number); PRINTI((&number)[0]); PRINTI(0[&number]); pi[0] = 456; PRINTI(number); PRINTI(*pi); PRINTI(pi[0]); PRINTI(0[pi]);
return 0;
}
Результаты выполнения программы (DJGPP):
number=123
*&number=123
(&number)[0]=123
0[&number]=123
number=456
*pi=456
pi[0]=456
0[pi]=456
Из приведенных результатов наглядно видно соответствие между адресом переменной &number и указателем pi, "настроенным" на нее. Разыменование (операция *) и индексация (операция []) с нулевым значением индекса одинаково действуют как на фиксированный адрес &number, так и на указатель pi. Внешне экзотическими, но
222
верными являются выражения 0[&number], соответствующее ему
(&number)[0], и 0[pi], эквивалентное pi[0].
ЭКСПЕРИМЕНТ. В программе 07_05.с удалите круглые скобки из выражения (&number)[0]. Выполните трансляцию и объясните результат.
Компилятор выдаст сообщение об ошибке, так как в соответствии с приоритетами операций будет сделана попытка применить операцию [] к имени number, которое не является адресом и не является указателем, например number – не имя массива.
ЗАДАНИЕ. Повторите решение предыдущей задачи, настроив на целочисленную переменную number указатель типа void *.
Программа, решающая задачу:
/* 07_05_1.c - указатели void * и индексация */ #include <stdio.h>
#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)
int main ()
{
int number=123;
void * pv = &number; PRINTI(number); PRINTI(*(int *)pv); ((int *)pv)[0]=789; PRINTI(number); 0[(int *)pv]=555; PRINTI(number);
PRINTI(((int *)pv)[0]); PRINTI(number); PRINTI(0[(int *)pv]); return 0;
}
Результаты выполнения программы (DJGPP):
number=123 *(int *)pv=123
223
number=789
number=555
((int *)pv)[0]=555 number=555
0[(int *)pv]=555
7.2. Массивы и указатели
ЗАДАЧА 07-06. Определите и инициализируйте значениями от 'a' до 'h' символьный массив. Определите два указателя, "настроив" их на первый и последний элементы массива. Используя указатели, "инвертируйте" массив, поменяв местами значения первого элемента и последнего, второго и предпоследнего и т.д. Выведите значения элементов полученного массива, используя указатель и применяя к нему операцию индексации [ ].
Решение:
/* 07_06.c - массивы и указатели */ #include <stdio.h>
int main ()
{
int i; int size;
char memb;
char line[] = {'a','b','c','d','e','f','g','h'}; char * p1 = line, * p2; size=sizeof(line)/sizeof(*line);
p2 = &line[size-1];
for (i=0; i<size/2; i++) { memb = *p1;
*p1++ = *p2; *p2-- = memb;
}
for (i=size-1, p2 = &line[size-1]; i >= 0; i--) printf("%c ",p2[-i]);
return 0;
}
Результаты выполнения программы:
h g f e d c b a
224
При определении указателя char *p1=line; он инициализируется значением адреса &line[0], так как имя массива – всегда константный указатель на его первый элемент (с нулевым индексом).
Чтобы присвоить значение указателю char *p2, приходится определить размер массива (переменная size) и явно записывать выражение &line[size-1]. (Обратите внимание на выражение, использованное для вычисления количества элементов массива.) "Перемещение" указателей p1 и p2 по элементам массива осуществляют в первом цикле выражения p1++ и p2--.
Второй цикл выполняет печать элементов массива: line[0], line[1], …, line[size-1]. Доступ к этим элементам реализован достаточно замысловато через указатель p2, адресующий последний элемент массива (используется отрицательный индекс). Выражение
(p2-size+1) адресует элемент line[0], (p2-size+2) – элемент line[1] и т.д. Именно такие значения принимает выражение p2[-i] в теле цикла с параметром i, уменьшающимся от size-1 до 0.
Другие варианты реализации печати в такой же последовательности:
for(i=0, p1=line; i<size; i++) printf("%c ", p1[i]);
или наиболее естественный:
for(i=0; i<size; i++) printf("%c ", line[i]);
В каждом из вариантов можно было бы так заменить операцию индексации [] разыменованием:
*(p2-i), *(p1+i), *(line+i)
ЭКСПЕРИМЕНТ. Для иллюстрации особой роли имени массива в операции sizeof замените в программе 07_06.с line именем указателя p1 (настроенного на начало массива), т.е. замените
size=sizeof(line)/sizeof(*line);
выражением size=sizeof(p1)/sizeof(*p1);
225
Так как p1 имеет тип char *, то sizeof(*p1) равно 1, а sizeof(p1) равно не размеру массива line, а размеру участка памяти, выделенного для указателя p1 (для компилятора DJGPP равно 4). Таким образом, size будет равно 4, и после компиляции при исполнении программы 07_06_1.с получим такой результат:
d c b a
В прототипах библиотечных функций формальный параметр, который соответствует фактическому параметру – массиву, зачастую специфицируется как указатель. Примеры прототипов таких функций:
void * memchr(const void * s, int c, size_t n);
–функция поиска среди первых n элементов массива, адресованного указателем s, первого вхождения значения c.
int read(int handle, void * buf, unsigned len);
–читает len байтов из файла, связанного с дескриптором handle, в буфер (массив), который адресован указателем buf.
Чтобы привыкнуть к такому использованию указателей вместо имен массивов, целесообразно получить опыт замены операции ин-
дексации [] операцией разыменования *, как это было сделано в предыдущей задаче.
ЗАДАЧА 07-07. Определив и инициализировав одномерный вещественный массив, выведите на печать индекс (не адрес!) и значение его максимального по модулю элемента. Вместо индексации при обращении к элементам массива используйте разыменование их адресов. При печати "изобразите" элемент массива в естественном виде (с индексом).
Решение:
/* 07_07.c - разыменование вместо индексации */ #include <stdio.h>
#include <math.h> int main ()
{
int i; int size;
226
double array[] = {0.0,10,-20,30,40,-50,40,-30}; double * pMax = array; size=sizeof(array)/sizeof(*array);
for (i=0; i<size; i++)
if(fabs(*pMax) < fabs(*(array+i))) pMax=array+i; printf
("\nThe index of max array element: pMax-array=%d", pMax-array);
printf("\nThe max array element *pMax: %f", *pMax);
printf("\nThe element with index: array[%d]=%f", pMax-array, *pMax);
return 0;
}
Результаты выполнения программы:
The index of max array element: pMax-array=5 The max array element *pMax: -50.000000
The element with index: array[5]=-50.000000
В тексте программы обратите внимание на следующие особенности, уже упомянутые в комментарии к предыдущей программе. Указатель pMax, которому будет присвоен адрес искомого элемента, инициализируется именем массива, т.е. адресом начала массива. Значение выражения sizeof(array) – размер памяти, занимаемой массивом. sizeof(*array) – размер первого элемента массива (с нулевым индексом). (array+i) – адрес элемента с индексом i. (pMax-array) – значение индекса (целочисленное) элемента, адресуемого указателем pMax.
ЭКСПЕРИМЕНТ. Раз имя массива ведет себя как указатель, то возникает подозрение, что именно так (как указатель) можно определить массив. Замените в программе 07_07.с определение массива double array[ ]={….} на определение double * array={….}.
Оцените результаты (ошибки и предупреждения) трансляции.
Программа 07_07_1.с соответствует эксперименту.
ЭКСПЕРИМЕНТ. Предыдущий эксперимент заканчивается ошибками компиляции. Вспоминаем, что имя массива является
227
константным указателем. Может быть допустимо такое определение: const double * array={…}. Проверьте это с помощью компилятора.
(Программа 07_07_2.с. Опять ошибки!)
ЭКСПЕРИМЕНТ. Инициализируйте в программе 07_07.с указатель pMax значением адреса элемента массива с нулевым индексом &array[0]. Оцените результат.
(Компиляция и исполнение пройдут успешно, программа
07_07_3.с).
ЗАДАНИЕ. Перепишите программы с одномерными массивами из предыдущей темы, используя разыменование вместо индексации.
ЗАДАЧА 07-08. Напишите программу, меняющую местами значения минимального и максимального элементов вещественного массива. Исходные данные (количество элементов и их значения) вводите с клавиатуры. Печатайте индексы и значения найденных элементов, затем – измененный массив. При определении индексов и обращении к элементам массива не используйте квадратные скобки (применяйте адресную арифметику и операцию разыменования).
Решение:
/* 07_08.c - обработка массива без индексации */ #include <stdio.h>
int main ()
{
int sizeArray; printf("sizeArray="); scanf("%d",&sizeArray);
{int i, j;
double array[sizeArray];
double * pMin = array, * pMax = array; double temp;
printf("The array members:\n");
228
for (i=0; i<sizeArray; i++)
{printf("array[%d]=",i);
scanf("%lf",array+i);
pMin = *pMin > *(array+i) ? array+i : pMin; pMax = *pMax < *(array+i) ? array+i : pMax;
}
printf("\nThe index of Min: %d, array[%d]=%f", pMin-array,pMin-array,*pMin);
printf("\nThe index of Max: %d, array[%d]=%f", pMax-array,pMax-array,*pMax);
temp = *pMax; *pMax = *pMin; *pMin = temp;
printf("\nThe solution:"); for (j=0; j < sizeArray; j++)
printf("%carray[%d]=%6.3f",
j%3?'\t':'\n',j,*(array+j));
return 0;
}
}
Результаты выполнения программы:
sizeArray=5<ENTER> The array members: array[0]=21<ENTER> array[1]=7<ENTER> array[2]=44<ENTER> array[3]=39<ENTER> array[4]=11<ENTER>
The index of Min: 1, array[1]=7.000000 The index of Max: 2, array[2]=44.000000 The solution:
array[0]=21.000 array[1]=44.000 array[2]= 7.000 array[3]=39.000 array[4]=11.000
Обратите внимание, что при вводе значений элементов массива в обращении к функции scanf("%lf", array+i) не требуется операции получения адреса, так как выражение array+i определяет именно адрес i-го элемента.
229
В тернарных (условных) операциях при вычислениях pMin и pMax сравниваются значения адресуемых переменных (за счет разыменования), а результат – выбранный адрес (значение указателя). При обмене значений элементов массива потребовалась вспомогательная переменная double temp.
Почему будет неверным использование вместо переменной temp указателя? Например, так:
double * pTemp:…
pTemp=pMax; pMax=pMin; pMin=pTemp;
ЭКСПЕРИМЕНТ. Выполните предложенную замену переменной temp указателем pTemp и оцените полученные результаты.
Рекомендация: в конце программы выведите значения pMaxarray и pMin-array. Решение приведено в программе 07_08_1.c. Ошибка очевидна – изменяются значения указателей, а элементы массива остаются без изменений (на своих местах).
7.3. Указатели и многомерные массивы
Рассмотрим, как в двумерном массиве размещаются элементы матрицы и каковы особенности разыменования при обращении к элементам многомерных массивов.
ЗАДАЧА 07-09. Определить внешний по отношению к функции main() двумерный массив с целочисленными элементами и фиксированными размерами N на M. Использовать массив для представления матрицы с размерами n на m (n<=N, m<=M), элементы которой принимают случайные значения в диапазоне от –99 до 99. Напечатать значения всех элементов двумерного массива по строкам, демонстрируя размещение матрицы в массиве. При обращении к элементам массива не использовать квадратные скобки, а применять операцию разыменования.
В следующей программе задача решена для случая, когда N и M определены на препроцессорном уровне (N=6, M=5).
230