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

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

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

172 Программирование на языке Си

операций деления 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 принимает значение адреса облас-

Соседние файлы в папке книги