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

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

..pdf
Скачиваний:
15
Добавлен:
12.11.2023
Размер:
17.16 Mб
Скачать
ВСОРТ__
BCPLUSPLUS
CDECL__
CONSOLE__
DLL__
.MSDOS__
МТ__
OVERLAY

162

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

.TIME__-I строка символов вида "часы:минуты:секунды",

 

определяющая время начала обработки пре­

 

процессором исходного файла;

STDC__-

константа, равная 1, если компилятор работает

в соответствии с ANSI-стандартом. В против­ ном случае значение макроимени__STDC__не определено. Стандарт языка Си предполагает, что наличие и м ен и __STDC__определяется реализацией, так как макрос__STDC__отно­ сится к нововведениям стандарта.

В конкретных реализациях набор предопределенных имен шире. Например, в препроцессор компилятора Borland C++ до­ полнительно включены:

имя, определенное равным 1, если при компиляции используется оптимизация;

числовое значение соответствует версии компилятора (определен для компилято­ ра Си++);

идентифицирует порядок передачи пара­ метров функциям, значение 1 соответст­ вует порядку, принятому в языке Си (в отличие от языка Паскаль);

определено для 32-разрядного компиля­ тора и установлено в 1 для программ консольного приложения;

соответствует работе в режиме Windows DLL;

равно 1 для 16-разрядных компиляторов Borland C++, устанавливается в 0 для 32разрядного компилятора; макрос доступен только для 32-разряд­ ного компилятора;

значение равно 1 в оверлейном режиме;

Глава 3. Препроцессорные средства

163

PASCAL__

- противоположен__CDECL__;

 

TCPLUSPLUS

-

числовое значение соответствует версии

 

 

компилятора (определено для

компиля­

 

 

тора Си++);

 

TLS__

-

определен как истинный для 32-

 

 

разрядного компилятора;

 

TURBOC_

- числовое значение, равное 0x0400 для

 

 

компилятора Borland C++ 4.0

(0x0460 -

 

 

для Borland C++ 4.5 и т.д.);

 

WINDOWS

-

означает генерацию кода для Windows;

WIN32

-

определен для 32-разрядного компилято­

 

 

ра и установлен в 1 для консольных при­

 

 

ложений.

 

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

Глава 4

УКАЗАТЕЛИ, МАССИВЫ, СТРОКИ

В предыдущих главах были введены все базовые (основные) типы языка Си. Для их определения и описания используются служебные слова: char, short, int, long, signed, unsigned, float, double, enum, void.

В языке Си, кроме базовых типов, разрешено вводить и ис­ пользовать производные типы, каждый из которых получен на основе более простых типов. Стандарт языка определяет три способа получения производных типов:

массив элементов заданного типа;

указатель на объект заданного типа;

функция, возвращающая значение заданного типа.

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

4.1. У казатели на объекты

Адреса и указатели. Начнем изучение указателей, обратив­ шись к понятию переменной. Каждая переменная в программе - это объект, имеющий имя и значение. По имени можно обра­ титься к переменной и получить (а затем, например, напечатать) ее значение. В операторе присваивания выполняется обратное действие - имени переменной из левой части оператора при­ сваивания ставится в соответствие значение выражения его пра­ вой части. С точки зрения машинной реализации имя переменной соответствует адресу того участка памяти, который для нее выделен, а значение переменной - содержимому этого участка памяти. Соотношение между именем и адресом условно представлено на рис. 4.1.

Глава 4. Указатели, массивы, строки

165

Программный

Е - т я

 

уровень

 

Переменная

Значение

Участок

Содержимое

памяти

,,

Машинный

 

уровень

&Е - адрес

 

Рис. 4.1. Соотношение между именем и адресом

На рис. 4.1 имя переменной явно не связано с адресом, одна­ ко, например, в операторе присваивания Е=С+В; имя перемен­ ной Е адресует некоторый участок памяти, а выражение С+В определяет значение, которое должно быть помещено в этот участок памяти. В операторе присваивания адрес переменной из левой части оператора обычно не интересует программиста и недоступен. Чтобы получить адрес в явном виде, в языке Си применяют унарную операцию &. Выражение &Е позволяет получить адрес участка памяти, выделенного на машинном уровне для переменной Е.

Операция & применима только к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, константам, битовым полям структур (см. гл. 6), регистровым переменным или внешним объектам (файлам - см. гл. 7), с ко­ торыми может взаимодействовать программа.

Рис. 4.2, взятый с некоторыми изменениями из [6], хорошо иллюстрирует связь между именами, адресами и значениями переменных. На рис. 4.2 предполагается, что в программе ис­ пользована, например, такая последовательность определений (с инициализацией):

char ch='G'; int date=1937;

float summa=2.015E-6;

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

Машинный

 

 

I

 

I

I

 

1А2В

1А2С

[ 1A2D

1А2Е

[ 1A2F

[

1А32

адрес:

 

 

I

 

 

 

 

 

байт

байт

1 байт

байт

1байт

1байт

1байт байт

 

 

 

I

 

I

I

I

Значение в

 

1937

 

2.015*10“

 

памяти:

 

 

 

 

 

 

 

Имя:

ch

date

 

summa

 

Рис. 4.2. Разные типы данных в памяти ЭВМ

В соответствии с приведенным рисунком переменные раз­ мещены в памяти, начиная с байта, имеющего шестнадцатерич­ ный адрес 1А2В. Целые переменные занимают по 2 байта, вещественные с плавающей точкой требуют участков памяти по 4 байта, символьные переменные занимают по одному байту. При таких требованиях к памяти в данном примере & c h = l А2В (адрес переменной ch); &date— 1А2С; &summa— 1А2Е. Адреса имеют целочисленные беззнаковые значения, и их можно обра­ батывать как целочисленные величины.

Имея возможность с помощью операции & определять адрес переменной или другого объекта программы, нужно уметь его сохранять, преобразовывать и передавать. Для этих целей в язы­ ке Си введены переменные типа "указатель", которые для крат­ кости будем называть просто указателями, если это не приводит к неоднозначности или путанице. Указатель в языке Си можно определить как переменную, значением которой служит адрес объекта конкретного типа. Кроме того, значением указателя может быть заведомо не равное никакому адресу значение, при­ нимаемое за нулевой адрес. Для его обозначения в ряде заголо­ вочных файлов, например в файле stdio.h, определена специ­ альная константа NULL.

Как и всякие переменные, указатели нужно определять и описывать, для чего используется, во-первых, разделитель В

Глава 4. Указатели, массивы, строки

167

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

char *z;

/* z — указатель на объект символьного типа */ int

/* k, i - указатели на объекты целого типа */ float *f;

/* f — указатель на объект вещественного типа */

Итак, в определениях и описаниях указателей применяется разделитель который является в этом случае знаком унарной операции косвенной адресации, иначе называемой операцией разыменования или операцией раскрытия ссылки или обраще­ ния по адресу. Операндом операции разыменования всегда яв­ ляется указатель. Результат этой операции - тот объект, который адресует указатель-операнд. Таким образом, *z обо­ значает объект типа char (символьная переменная), на который указывает z; *k - объект типа int (целая переменная), на кото­ рый указывает к, и т.д. Обозначения *z, *i, *f имеют права пе­ ременных соответствующих типов. Оператор *z- засылает символ "пробел" в тот участок памяти, адрес которого опреде­ ляет указатель z. Оператор *k=*i=0; заносит целые нулевые зна­ чения в те участки памяти, адреса которых заданы указателями k, i. Обратите внимание на то, что указатель может ссылаться на объекты того типа, который присутствует в определении указа­ теля. Исключением являются указатели, в определении которых использован тип void - отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования, т.е. операцию

Скрытую в операторе присваивания Е=В+С; работу с адре­ сом переменной левой части можно сделать явной, если .за­ менить один оператор присваивания следующей после­ довательностью:

168

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

/* Определения

переменных и указателя ш: */

int E,C,B,*m;

 

/* Значению m присвоить адрес переменной Е: */

ш—&Е;

/* Переслать значение выражения С+В в участок памяти с адресом равным значению ш: */

*ш=В+С;

Данный пример не объясняет необходимости применения указателей, а только иллюстрирует их особенности. Возможно­ сти и преимущества указателей проявляются при работе с функциями, массивами, строками, структурами и т.д. Перед тем, как привести более содержательные примеры использования указателей, остановимся подробнее на допустимых действиях с указателями.

Операции над указателями. В языке Си допустимы сле­ дующие (основные) операции над указателями: присваивание; получение значения того объекта, на который ссылается указа­ тель (синонимы: косвенная адресация, разыменование, раскры­ тие ссылки); получение адреса самого указателя; унарные операции изменения значения указателя; аддитивные операции и операции сравнений. Рассмотрим перечисленные операции подробнее.

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

Если для имен действуют описания предыдущих примеров, то допустимы операторы:

i=&date; k = i; z=NULL;

Комментируя эти операторы, напомним, что выражение *имяуказателя позволяет получить значение, находящееся по адресу, который определяет указатель. В предыдущих примерах было определено значение переменной date (1937), затем ее ад-

Глава 4. Указатели, массивы, строки

169

рес присвоен указателю i и указателю к, поэтому значением *к является целое 1937. Обратите внимание, что имя переменной date и разыменования *i, *к указателей i, к обеспечивают в этом примере доступ к одному и тому же участку памяти, выделен­ ному только для переменной date. Любая из операций *к^выражение, *[=выражение, АгХе=выражение приведет к из­ менению содержимого одного и того же участка в памяти ЭВМ.

Иногда требуется присвоить указателю одного типа значение указателя (адрес объекта) другого типа. В этом случае исполь­ зуется "приведение типов", механизм которого понятен из сле­ дующего примера:

char *z; /* z - указатель на символ */ int *k; /* k - указатель на целое */

z=(char *)к ; /* Преобразование указателей */

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

 

 

 

 

*А - объект,

 

 

 

 

адресуемый

 

Указатель А

 

 

указателем А

&А - адрес

Значение

Адрес

 

Значение

указателя

объекта

(

указателя

объекта

 

(Значение

 

 

 

указателя А)

 

 

Рис. 4.3. Имя, адрес и значение указателя

• 170

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

С помощью унарных операций '++' и '—' числовые (ариф­ метические) значения переменных типа указатель меняются поразному в зависимости от типа данных, с которыми связаны эти переменные. Если указатель связан с типом char, то при выпол­ нении операций '++' и 1—' его числовое значение изменяется на 1 (указатель z в рассмотренных примерах). Если указатель свя­ зан с типом int (указатели i, к), то операции ++i, i++, —к, к— изменяют числовые значения указателей на 2. Указатель, свя­ занный с типом float или long, унарными операциями '++', '—', изменяется на 4. Таким образом, при изменении указателя на единицу указатель "переходит к началу" следующего (или пре­ дыдущего) поля той длины, которая определяется типом.

Аддитивные операции по-разному применимы к указателям, точнее, имеются некоторые ограничения при их использовании. Две переменные типа указатель нельзя суммировать, однако к указателю можно прибавить целую величину. При этом вычис­ ляемое значение зависит не только от значения прибавляемой целой величины, но и от типа объекта, с которым связан указа­ тель. Например, если указатель относится к целочисленному объекту типа long, то прибавление к нему единицы увеличивает реальное значение на 4, т.е. выполняется "переход" к адресу следующего в памяти объекта типа long.

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

Например, после выполнения операторов

int х[5], *i, *k, j ; i=£x[0] k=£x[4] ; j=k-i;

j принимает значение 4, а не 8, как можно было бы предполо­ жить исходя из того, что каждый элемент массива х[ ] занимает два байта.

Глава 4. Указатели, массивы, строки

171

В данном примере разность указателей присвоена перемен­ ной типа int. Однако тип разности указателей определяется поразному в зависимости от особенностей компилятора. Чтобы сделать язык Си независимым от реализаций, в заголовочном файле stddef.h определено имя (название) ptrdiff_t, с помощью которого обозначается тип разности указателей в конкретной реализации.

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

#±nclude <stdio.h> #include <stddef.h> void main()

{

int x[5]; int *i,*k; ptrdiff_t j ; i=&x[0]; k=Sx[4]; j=k-i;

printf("\nj=%d",(int)j);

)

Результат будет таким:

j=4

Арифметические операции и указатели. Унарные адрес­ ные операции и имеют более высокий приоритет, чем арифметические операции. Рассмотрим следующий пример, ил­ люстрирующий это правило:

float а=4.0, *u, z; u=&z;

*u=5;

а=а + *u + 1;

/* а равно 10, и - не изменилось, z равно 5 */

При использовании адресной операции

в арифметических

выражениях следует остерегаться случайного сочетания знаков

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