
книги / Программирование на языке Си
..pdf162 |
Программирование на языке Си |
.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 */
При использовании адресной операции |
в арифметических |
выражениях следует остерегаться случайного сочетания знаков