книги / Программирование на языке Си
..pdf172 Программирование на языке Си
операций деления 7 и разыменования так как комбинацию '/*' компилятор воспринимает как начало комментария. Напри мер, выражение
а/*и
следует заменить таким:
а/(*и)
Унарные операции и '++' или '— ' имеют одинаковый при оритет и при размещении рядом выполняются справа налево.
Добавление целочисленного значения п к указателю, адре сующему некоторый элемент массива, приводит к тому, что указатель получает значение адреса того элемента, который от стоит от текущего на п позиций (элементов). Если длина эле мента массива равна d байтов, то численное значение указателя изменяется на (d*n). Рассмотрим следующий фрагмент про граммы, иллюстрирующий перечисленные правила:
int х[4]={ |
o, |
2 t 4, б |
), |
*i f y; |
|
*/ |
|
i=£x[0]; |
/* |
i равно |
адресу элемента х [0] |
||||
y=*i; |
/* У |
равно |
0 |
i равно |
fix[0] |
*/ |
|
y=*i++; |
/* У |
равно |
0 |
i равно |
fix[l] |
*/ |
|
y=++*i; |
/* У |
равно |
3 |
i равно' fixtl] |
*/ |
||
y=*++i; |
/* У |
равно |
4 |
i равно |
fix[2] |
*/ |
|
y=(*i)++; |
/* У |
равно |
4 |
i равно |
fix[2] |
*/ |
|
y=++(*i);' |
/* У равно |
6 |
i равно |
fix[2] |
*/ |
Указатели и отношения. К указателям применяются опера ции сравнения ’>', '>=', '<’. Таким образом, указа тели можно использовать в отношениях. Но сравнивать указа тели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулево го адреса.
Приведем пример, в котором используются операции над указателями и выводятся (печатаются) получаемые значения. Обратите внимание, что для вывода значений указателей (адресов) в форматной строке функции printf() используется спецификация преобразования %р.
Глава 4. Указатели, массивы, строки |
173 |
|
#include <stdio.h> |
50.0 }; |
|
float х [ ] = |
{ 10.0, 20.0, 30.0, 40.0, |
|
void main( ) |
|
|
{ |
*u2; |
|
float *ul, |
|
|
int i ; |
|
&u2=%p", |
printf("\n Адреса указателей: &ul=%p |
&ul, &u2 ) ;
printf("\n Адреса элементов массива: \n");
for(i=0; i<5; |
i++) |
|
|
{ |
printf("\n"); |
|
|
if (i==3) |
&x[i]); |
||
printf(" |
&x[%d] |
= %p", i, |
|
} |
|
элементов массива: \n"); |
|
printf("\n Значения |
|||
for(i=0; i<5; |
i++) |
|
|
{ |
printf("\n"); |
|
|
if (i==3) |
i, x[i]); |
||
printf(" x[%d] |
= %5.1f ", |
}
for(ul=&x[0], u2=&x[4]; u2>=&x[0]; ul++, u2— )
printf("\n ul=%p *ul=%5.1f u2=%p *u2=%5.If",ul,*ul,u2,*u2);
printf("\n u2-ul=%d", u2-ul);
}
}
При печати значений разностей указателей и адресов в функ ции printf( ) использована спецификация преобразования %d - вывод знакового десятичного целого.
Возможный результат выполнения программы (конкретные значения адресов могут быть другими):
Адреса указателей: |
&ul=FFF4 &u2=FFF2 |
|||
Адреса элементов массива: |
|
|||
&х[0]=00А8 |
&х[1]=00АС |
&х[2]=00В0 |
||
&х[3]=00В4 |
&х[4]=00В8 |
|
|
|
Значения элементов |
массива: |
|
||
х[0]=10.0 |
х[1]=20.0 |
х[2]=30.0 |
||
х [3]=40.0 |
х[4]=50.0 |
u2=00B8 |
*u2=50.0 |
|
ul=00A8 |
*ul=10.0 |
|||
u2-ul=4 |
|
|
|
|
174 |
|
Программирование на языке Си |
|
ul=0OAC |
*ul=20.0 |
u2=00B4 |
*u2=40.0 |
u2-ul=2 |
*ul=30.0 |
u2=00B0 |
*u2=30.0 |
ul=00B0 |
|||
u2-ul=0 |
*ul=40.0 |
u2=00AC |
*u2=20.0 |
ul=00B4 |
|||
u2-ul=-2 |
*ul=50.0 |
u2=00A8 |
*u2=10.0 |
ul=00B8 |
|||
u2-ul=-4 |
|
|
|
На рис. 4.4 приводится схема размещения в памяти массива float х[5] и указателей до начала выполнения цикла изменения указателей.
|
Основная память |
Адреса |
х[0] |
10.0 |
00А8=&х[0] |
х[1] |
20.0 |
00АС=&х[1] |
х[2] |
30.0 |
00В0=&х[2] |
х[3] |
40.0 |
00В4=&х[3] |
х[4] [—► |
50.0 |
00В8=&х[4] |
ш — |
00В8 |
FFF2=&U2 |
U1 |
00А8 |
FFF4=&U1 |
Рис. 4.4. Схема размещения в памяти массива и указателей
Глава 4. Указатели, массивы, строки |
175 |
4.2. У казатели и м ассивы
Указатели и доступ к элементам массивов. По определе нию, указатель - это либо объект со значением "адрес объекта" или "адрес функции", либо выражение, позволяющее получить адрес объекта или функции. Рассмотрим фрагмент:
int х,у;
int *р =&х;
р=&у;
Здесь р - указатель-объект, а &х, &у - указатели-выражения, т.е. адреса-кОнстанты. Мы уже знаем, что р - переменная того же типа, что и значения &х, &у. Различие между адресом (т.е. указателем-выражением) и указателем-объектом заключается в возможности изменять значения указателей-объектов. Именно поэтому указатели-выражения называют указателямиконстантами или адресами, а для указателя объекта используют название указатель-переменная или просто указатель.
В соответствии с синтаксисом языка Си имя массива без ин дексов является указателем-константой, т.е. адресом его первого элемента (с нулевым индексом). Это нужно учитывать и пом нить при работе с массивами и указателями.
Рассмотрим задачу "инвертирования" массива символов и различные способы ее решения с применением указателей (заметим, что задача может быть легко решена и без указателей - с использованием индексации). Предположим, что длина мас сива типа char равна 80.
Первое решение задачи инвертирования массива:
char z[80],s; char *d,*h;
/* d и h - указатели на символьные объекты */ for (d=z, h=&z[79]; d<h; d++,h— )
{
s=*d;
*d=*h;
*h=S ;
)
176 |
Программирование на языке Си |
В заголовке цикла указателю d присваивается адрес первого (с нулевым индексом) элемента массива z. Здесь можно было бы применить и другую операцию, а именно: d=&z[0]. Указа тель h получает значение адреса последнего элемента массива z. Далее работа с указателями в заголовке ведется, как с обычны ми целочисленными переменными. Цикл выполняется до тех пор, пока d<h. После каждой итерации значение d увеличивает ся, значение h уменьшается на 1. При первой итерации в теле цикла выполняется обмен значений z[0] и z[79], так как d - ад рес z[0], h - адрес z[79]. При второй итерации значением d явля ется адрес z[l], для h - адрес z[78] и т.д.
Второе решение задачи инвертирования массива:
char z[80] ,s, *d, *h; for (d=z, h=&z[79]; d<h;)
{
s=*d; *d++=*h; *h— = s;
)
Приращение указателя d и уменьшение указателя h перене сены в тело цикла. Напоминаем, что в выражениях *d++ и *h— операции увеличения и уменьшения на 1 имеют тот же приори тет, что и унарная адресная операция Поэтому изменяются на 1 не значения элементов массива, на которые указывают d и h, а сами указатели. Последовательность действий такая: по значению указателя d (или h) обеспечивается доступ к элементу массива; в этот элемент заносится значение из правой части оператора присваивания; затем увеличивается (уменьшается) на 1 значение указателя d (или h).
Третье решение задачи инвертирования массива (исполь зуется цикл с предусловием):
char z[80]# s, *d, *h; d=z ;
h=&z[79]; while (d < h)
{
s=*d; |
*d++ = *h; *h— = s; |
)
Глава 4. Указатели, массивы, строки |
177 |
Четвертое решение задачи инвертирования массива (имитация индексированных переменных указателями со сме щениями):
char |
z[80],s; |
int |
i ; |
for |
(i=0; i<40; i++) |
{
s = * (z+i) ;
* (z+i) = * (z+(79—i)); * (z+(79-i)) = s;
)
Последний пример демонстрирует возможность использова ния вместо индексированного элемента z[i] выражения *(z+i). В языке Си, как мы упоминали, имя массива без индексов есть адрес его первого элемента (с нулевым значением индекса). Прибавив к имени массива целую величину, получаем адрес со ответствующего элемента, таким образом, &z[i] и z+i - это две формы определения адреса одного и того же элемента массива, отстоящего на i позиций от его начала.
Итак, в соответствии с синтаксисом языка операция индек сирования Е1[Е2] определена таким образом, что она эквива лентна *(Е1+Е2), где Е1 - имя массива, Е2 - целое. Для многомерного массива правила остаются теми же. Таким об разом, E[n][m][k] эквивалентно *(E[n][m]+k) и, далее *(*(*(E+n)+m)+k).
Отметим, что имя массива не является переменной типа ука затель, а есть константа - адрес начала массива. Таким обра зом, к имени массива не применимы операции '++' (увеличения), '—' (уменьшения), имени массива нельзя присвоить значение, т.е. имя массива не может использоваться в левой части опера тора присваивания.
В рассмотренных примерах указатели относились к символь ным переменным, и поэтому их приращения были единичными. Однако это обеспечивалось только особенностями представле ния в памяти символов - каждый символ занимает в памяти один байт, и поэтому адреса смежных элементов символьного массива отличаются на 1. В случае массивов с другими элемен-
1 2 -3 1 2 4
178 |
Программирование на языке Си |
тами (другого типа) единичному изменению указателя, как уже отмечалось, соответствует большее изменение адреса.
В следующей программе продемонстрируем еще раз особен ности изменения указателей при переходе от элемента к элемент ту в массивах с элементами разных типов:
#include <stdio.h>
/* Изменение значений указателей на элементы массива */
void main( )
{
char |
z[5]; |
int |
m[5]; |
float a [5]; |
|
char |
*uz; |
int |
*um; |
float |
*ua; |
printf("\n Адреса элементов |
символьного " |
"массива:\n");
for (uz=z; uz <= &z[4]; uz++) j?rintf(" %10p",uz);
printf("\n Адреса элементов целочисленного " "массива:\n");
for.(um=m; um <= &m[4]; um++) printf ("%10p",um);
printf("\n Адреса элементов вещественного " "массива:\n");
for (ua=a; ua <= &a[4]; ua++) printf(" %10p",ua);
)
Результат выполнения программы:
Адреса элементов символьного массива : FFFO FFF1 FFF2 FFF3 FFF4
Адреса элементов целочисленного массива:
FFF6 |
FFF8 |
FFFA |
FFEC |
FFFE |
Адреса |
элементов вещественного массива: |
|||
FEDО |
FFD4 |
FFD8 |
FFDC |
FFE0 |
Как подтверждает рассмотренный пример, изменение указа теля на 1 приводит к разным результатам в зависимости от типа объекта, с которым связан указатель. Значение указателя изме няется на длину участка памяти, выделенного для элемента, свя занного с указателем. Символы занимают в памяти по одному байту, поэтому значение указателя uz изменяется на 1 при пере-
Глава 4. Указатели, массивы, строки |
179 |
ходе к соседнему элементу символьного массива. Для целочис ленного массива переход к соседнему элементу изменяет указа тель urn на 2. Для массива вещественных элементов с плавающей точкой переход к соседнему элементу изменяет ука затель иа на 4.
Массивы динамической памяти. В соответствии со стан дартом языка массив представляет собой совокупность элемен тов, каждый из которых имеет одни и те же атрибуты (характеристики). Все элементы размещаются в смежных участ ках памяти подряд, начиная с адреса, соответствующего началу массива, т.е. значению & имямассива [0].
При традиционном определении массива:
тип имя_массива [количество_элементов];
имя_массива становится указателем на область памяти, выде ляемой для размещения элементов массива. Количест во^элементов в соответствии с синтаксисом языка должно быть константным выражением. Тип явно определяет размеры памя ти, выделяемой для каждого элемента массива.
Таким образом, общее количество элементов массива и раз меры памяти, выделяемой для него, полностью и однозначно заданы определением. Это не всегда удобно. Иногда нужно, чтобы память для массива выделялась в таких размерах, какие нужны для решения конкретной задачи, причем потребности в памяти заранее не известны и не могут быть фиксированы.
Формирование массивов с переменными размерами можно организовать с помощью указателей и средств для динамиче ского выделения памяти. Начнем рассмотрение указанных средств с библиотечных функций, описанных в заголовочных файлах alloc.h и stdlib.h стандартной библиотеки (файл alloc.h не является стандартным). В табл. 4.1 приведены сведения об этих библиотечных функциях. Функции malloc(), са11ос() и realloc() динамически выделяют память в соответствии со зна чениями параметров и возвращают адрес начала выделенного участка памяти. Для универсальности тип возвращаемого зна чения каждой из этих функций есть void*. Этот указатель
12*
180 |
Программирование на языке Си |
(указатель такого типа) можно преобразовать к указателю лю бого типа с помощью операции явного приведения типа (тип *).
Функция free( ) решает обратную задачу - освобождает па мять, выделенную перед этим с помощью одной из трех функ ций calloc(), malloc( ) или realloc( ). Сведения об этом участке памяти передаются в функцию free() с помощью указателя - параметра типа void *. Преобразование указателя любого типа к типу void * выполняется автоматически, поэтому вместо фор мального параметра void *Ы можно подставить в качестве фак тического параметра указатель любого типа без операции явного приведения типов.
|
Таблица 4.1 |
|
Функции для выделения и освобождения памяти |
Функция |
Прототип и краткое описание |
malloc |
void * malloc (uusigned s); |
|
Возвращает указатель на начало области (блока) динамической |
|
памяти длиной в s байт. При неудачном завершении возвращает |
|
значение NULL. |
calloc |
void * calloc (uusigned n, unsigned m); |
|
Возвращает указатель на начало области (блока) обнуленной |
|
динамической памяти, выделенной для размещения п элементов |
|
по m байт каждый. При неудачном завершении возвращает зна |
|
чение NULL. |
realloc |
void * realloc (void * bl, unsigned ns); |
|
Изменяет размер блока ранее выделенной динамической памяти |
|
до размера ns байт. Ы - адрес начала изменяемого блока. Если |
|
Ы равен NULL (память не выделялась), то функция выполняет |
|
ся как malloc. |
free |
void * free (void * Ы); |
|
Освобождает ранее выделенный участок (блок) динамической |
|
памяти, адрес первого байта которого равен значению Ы. |
Следующая программа иллюстрирует на несложной задаче особенности применения функций выделения (malloc) и осво бождения (free) динамической памяти. Решается следующая
Глава 4. Указатели, массивы, строки |
181 |
задача: ввести и напечатать в обратном порядке набор вещест венных чисел, количество которых заранее не фиксировано, а вводится до начала ввода самих числовых значений. Текст про граммы может быть таким: .
#include <stdio.h> #include <stdlib.h> void main ( )
{
/* Указатель для выделяемого блока памяти */ float *t;
int i,n;
printf("\nn="); /* n — число элементов */ scanf("%d",&n);
t=(float *)malloc(n*sizeof(float)); for(i=0; i<n; i++) /* Цикл ввода чисел */
{p r i n t f ( ”x [ % d ] = " , i ) ; s c a n f ( " % f " , & t [ i ] ) ;
}
/* Цикл печати результатов */ for(i=n-l; i>=0; i— )
{
if (i%2==0) printf("\n"); printf("\tx[%d]=%f",i,t[i]);
}
free (t); /* Освобождение памяти */
}
Результат выполнения программы (компилятор ВС++3.1 и компилятор из операционной системы Free BSD UNIX):
n=4
x [0]=10
x [1]=20 x[2]=30
x [3]=40
x[3]=40.000000 x[2]=30.000000 x[l]=20.000000 x[0]=10.000000
В программе int n - количество вводимых чисел типа float, t - указатель на начало области, выделяемой для размещения п вводимых чисел. Указатель t принимает значение адреса облас-