
Основы программирования. Борисенко
.pdf3.3.2. Конструирование новых типов |
|
|
111 |
||
int |
*a, *b, c, d; |
|
|
|
|
char |
*e; |
|
|
|
|
void * f ; |
|
|
|
|
|
В первой |
строке описаны указатели а и b на |
тип int и простые |
пере |
||
менный с |
и d типа int (с и d |
— не указатели!). |
|
||
С указателями возможны следующие два действия: |
|
||||
1) присвоить указателю |
адрес некоторой |
переменной. Д л я |
это¬ |
||
го используется операция |
взятия адреса, которая обозначается |
||||
амперсендом &. Например, |
строка |
|
|
a = &c;
указателю а присваивает значение адреса переменной с;
2)получить объект, адрес которого содержится в указателе; для этого используется операция звездочка '*', которая записыва ется перед указателем. (Заметим, что звездочкой обозначается также операция умножения.) Например, строка
d = *a;
присваивает переменной d значение целочисленной переменной, адрес которой содержится в а. Так как ранее указателю а был присвоен адрес переменной c, то в результате переменной d присваивается значение c, т.е. данная строка эквивалентна сле¬ дующей:
d = c;
Н и ж е будут рассмотрены т а к ж е арифметические операции с указа¬ телями, которые в языке Си чрезвычайно важны .
С л о ж н ые описания
Конструкции массива и указателя при описании типа можно при менять многократно в произвольном порядке. Кроме того, можно описывать прототип функции. Таким образом можно строить слож¬ ные описания вроде «массив указателей», «указатель на указатель», «указатель на массив», «функция, возвращающая значение типа ука¬ затель», «указатель на функцию» и т.д. Правила здесь таковы:
112 |
3.3. Типы переменных |
• для группировки можно использовать круглые скобки, напри¬ мер, описание
int *(x[10]);
означает «массив из 10 элементов типа указатель на int»;
•при отсутствии скобок приоритеты конструкций описания рас¬ пределены следующим образом:
-операция * определения указателя имеет самый низкий приоритет. Например, описание
int *x[10];
означает «массив из 10 элементов типа указатель на int». Здесь к имени переменной x сначала применяется опе рация определения массива [] (квадратные скобки), по¬ скольку она имеет более высокий приоритет, чем звездоч¬ ка. Затем к полученному массиву применяется операция определения указателя . В результате получается «массив указателей», а не указатель на массив! Если нам нужно определить указатель на массив, то следует использовать круглые скобки при описании:
int (*x)[10];
Здесь к имени x сначала применяется операция * опреде¬ ления указателя;
-операции определения массива [] (квадратные скобки по¬ сле имени) и определения функции (круглые скобки после имени) имеют одинаковый приоритет, более высокий, чем звездочка. Примеры:
int f ( ) ;
Описан прототип функции / без аргументов, возвращаю¬ щей значение типа int.
int (*f())[10];
3.3.2. Конструирование новых типов |
113 |
Описан прототип функции / без аргументов, возвращаю¬ щей значение типа указатель на массив из 10 элементов типа int;
• последний пример у ж е не является очевидным. Общий алго¬ ритм разбора сложного описания можно охарактеризовать как «чтение изнутри». Сначала находим описываемое имя. Затем определяем, какая операция применяется к имени первой. Если нет круглых скобок для группировки, то это либо определе¬ ние указателя (звездочка слева от имени), либо определение массива (квадратные скобки справа от имени), либо определе¬ ние функции (круглые скобки справа от имени). Таким обра зом получается первый шаг сложного описания. Затем находим следующую операцию описания, которая применяется к у ж е выделенной части сложного описания, и повторяем это до тех пор, пока не исчерпаем все описание. Проиллюстрируем этот алгоритм на примере:
void (*a[100])(int x);
Описывается переменная a. К ней сначала применяется опера¬
ция описания массива из 100 |
элементов, |
далее — определение |
|
указателя, далее — функция |
от одного |
целочисленного |
аргу¬ |
мента x типа int, наконец — определение возвращаемого |
типа |
||
int. Описание читается следующим образом: |
|
1)a — это
2)массив из 100 элементов типа
3)указатель на
4)функцию с одним аргументом x типа int, возвращающую значение типа
5)void.
Н и ж е расставлены номера операций в порядке их применения в описании переменной a:
void |
(* |
a |
[100])(int x); |
|
5) |
3) |
1) |
2) |
4) |
114 |
3.3. Типы переменных |
Строки
Специального типа данных «строка» в Си нет. Строки представ¬ ляются массивами символов (а символы — их числовыми кодами, см. раздел 1.4.3). Последним символом массива, представляющего строку, должен быть символ с нулевым кодом. Пример:
char str[10];
str[0] = 'e'; str[1] = '2'; str[2] = 'e'; str[3] = '4'; str[4] = 0;
Описан массив str из 10 символов, который может представлять строку длиной не более 9, поскольку один элемент должен быть зарезервирован для терминирующего нуля. Далее в массив str запи сывается строка "e2e4". Строка терминируется нулевым символом. Всего запись строки использует 5 первых элементов массива str с индексами 0... 4. Последние 5 элементов массива не используются. Массив можно инициализировать непосредственно при описании, на¬ пример
char t[] = "abc";
Здесь мы не указываем в квадратных скобках размер массива t, ком¬
пилятор его вычисляет сам. После операции присваивания |
записана |
|
строковая константа "abc", которая заносится |
в массив t. |
В резуль |
тате компилятор создает массив t из четырех |
элементов, |
поскольку |
на строку отводится 4 байта, включая терминирующий ноль. Строковые константы заключаются в Си в двойные апострофы, в
отличие от символьных, которые заключаются в одинарные. Значе¬ нием строковой константы является адрес ее первого символа. Когда компилятор встречает строковую константу в программе, он записы¬ вает ее текст в область статической памяти, обычно защищенную от изменения, и использует этот адрес. Например, в результате следу¬ ющего описания
const char *s = "abcd";
создается указатель s, а также строка символов "abcd", строка по¬ мещается в область статической памяти, защищенную от изменения, а в указатель s помещается адрес начала строки. Строка содержит 5 элементов: коды символов abcd и теминирующий нулевой байт.
3.3.2. Конструирование новых типов |
115 |
М о д и ф и к а т ор const |
|
Константы в Си можно задавать двумя |
способами: |
с помощью директивы #define препроцессора . Например, строка
#define MILLENIUM 1000
задает символическое имя MILLENIUM для константы 1000. Пре¬ процессор всюду в тексте заменяет это имя на константу 1000, используя текстовую подстановку. Это не очень хороший спо соб, поскольку при таком задании отсутствует контроль типов;
с помощью модификатора const. При описании любой переменной можно добавить модификатор типа const. Например, вместо #define можно использовать следующее описание:
const i n t MILLENIUM = 1000;
Модификатор const означает, что переменная M I L L E N I U M яв¬ ляется константой, т.е. менять ее значение нельзя. Попытка присвоить новое значение константе приведет к ошибке компи¬ ляции:
MILLENIUM = 100; // Ошибка: константу
//нельзя изменять
При описании указателя модификатор const, записанный до звез¬ дочки, означает, что описан указатель на константный объект, т.е. на объект, менять который нельзя или запрещено. Например, в строке
const char *p;
описан указатель на константную строку (массив символов, менять который запрещено).
Указатели на константные объекты используются в Си чрезвы¬ чайно часто. Причина состоит в том, что константный указатель поз¬ воляет прочесть объект и при этом гарантирует, что объект не будет испорчен в результате ошибки программирования, т.к. константный указатель не дает возможности изменить объект.
116 |
3.3. Типы переменных |
Константный указатель ссылается на константный объект, од¬ нако, содержимое самого указателя может изменяться. Например, следующий фрагмент вполне корректен:
|
const char *str = "e2e4"; |
|
|
|
|
str |
= "c7c5"; |
|
|
Здесь константный указатель str сначала содержит адрес |
констант |
|||
ной строки "e2e4". Затем в него записывается адрес другой |
констант |
|||
ной |
строки "c7c5". |
|
|
|
|
В Си можно также описать указатель, значение которого |
не мо |
||
жет |
быть |
изменено; дл я этого модификатор const указывается |
после |
|
звездочки. |
Например, фрагмент кода |
|
|
int i ;
int * const p = &i;
навечно записывает в указатель p адрес переменной i , перенаправить указатель p на другую переменную у ж е нельзя. Строка
p = &n;
является ошибкой, т.к. указатель p — константа, а константе нельзя присвоить новое значение. Указатели, значения которых изменять нельзя, используются в Си значительно реже, в основном при запол¬ нении константных таблиц.
М о д и ф и к а т ор volatile |
|
|
Слово volatile |
в переводе |
означает "изменчивый, непостоянный". |
В Си к описанию |
переменной |
следует добавлять слово volatile, если |
ее значение может изменяться не в результате выполнения програм¬ мы, а из-за каких-либо внешних событий. Например, переменная может измениться при выполнении программы-обработчика аппарат¬ ного прерывания (см. раздел 2.5). Другой причиной «внезапного» изменения значения переменной может быть переключение между нитями при параллельном программировании (см. 2.6.2) и модифи¬ кация переменной в параллельной нити.
Необходимо обязательно сообщать компилятору о таких изменчи¬ вых переменных. Дело в том, что процессор выполняет все действия
3.3.2. Конструирование новых типов |
|
|
117 |
||
с регистрами, |
а не с элементами памяти. Оптимизирующий |
компи¬ |
|||
лятор держит |
значения |
большинства переменных в регистрах, |
сводя |
||
к минимуму обращения к памяти. Непостоянная переменная |
может |
||||
изменить свое |
значение |
в памяти, |
но программа будет |
по-прежнему |
|
использовать |
значение |
в регистре, |
которое осталось |
прежним. Из - |
за этого выполнение программы нарушится. Модификатор volatile запрещает д а ж е временно помещать переменную в регистр процес¬ сора.
Пример описания переменной: |
|
|
|||
v o l a t i l e |
i n t inputPort; |
|
|
||
Здесь |
мы описываем целочисленную |
переменную inputPort |
и сооб¬ |
||
щаем |
компилятору, что ее значение |
может внезапно меняться в ре¬ |
|||
зультате каких-либо внешних |
событий. Этим мы запрещаем компи¬ |
||||
лятору помещать переменную |
в регистр процессора в целях |
оптими¬ |
|||
зации |
программы. |
|
|
|
|
Оператор typedef |
|
|
|
||
В языке Си можно задать имя типа, если его описание |
достаточ¬ |
||||
но громоздко |
и его не хочется повторять много раз . В дальнейшем |
||||
можно |
использовать имя типа |
при описании переменных. Д л я опре¬ |
|||
деления типа применяется оператор typedef. Синтаксически |
оператор |
typedef аналогичен обычному описанию переменной, к которому в
самом начале добавлено слово typedef. При этом вместо |
перемен¬ |
|||||
ной |
определяется |
имя нового типа. Сравните следующее |
описание |
|||
переменной "real" и определение нового типа "Real": |
|
|
||||
|
double |
real; |
// Описание переменной |
real |
||
|
typedef |
double Real; // Определение нового |
типа |
Real, |
||
|
|
|
// эквивалентного |
типу |
double. |
|
М ы |
ка к бы описываем переменную, добавляя |
к описанию слово |
||||
typedef. При этом |
описываемое имя становится |
именем нового ти¬ |
||||
па. Его можно использовать затем дл я задания переменных: |
||||||
|
Real x, y, |
z; |
|
|
|
118 |
3.3. Типы переменных |
Ч а щ е всего определение типов с помощью typedef используют, когда описание типа достаточно громоздко. Оператор typedef позво¬ ляет задать его только один раз, что облегчает исправление програм¬ мы при необходимости. Например, следующая строка определяет тип callback как указатель на функцию с одним целым параметром, воз¬ вращающую значение логического типа:
typedef bool (*callback)(int);
Строка, описывающая три переменные p, q, r,
callback p, q, r ;
эквивалентна строке
bool (*p)(int), (*q)(int), (*r)(int);
но первая строка, конечно, понятнее и нагляднее.
Еще одна цель использования оператора typedef состоит в том, чтобы сделать текст программы менее зависимым от особенностей конкретной архитектуры (разрядности процессора, конкретного Си - компилятора и т.п.). Например, в старых Си-компиляторах, которые использовались дл я 16-разрядных процессоров Intel 80286, существо¬
вали так называемые близкие |
(near) и далекие (far) |
указатели . В |
||
эталонном языке Си ключевых |
слов near и far нет, они использова¬ |
|||
лись лишь в Си-компиляторах |
дл я Intel 80286 ка к расширение язы¬ |
|||
ка. Поэтому, |
чтобы тексты программ не зависели |
от компилятора, |
||
в системных |
h - файлах с помощью оператора typedef |
определялись |
||
имена дл я типов указателей, а в текстах программ |
использовались |
не типы эталонного языка Си, а введенные имена |
типов. Например, |
|
тип "далекий указатель на константную строку" |
в соответствии с |
|
соглашениями фирмы Microsoft называется L P C T S T R (Long |
Pointer |
|
to Constant Text STRing). При использовании 16-разрядного |
компи |
|
лятора он определяется в системных h - файлах как |
|
typedef const char fa r *LPCTSTR;
в 32-разрядной архитектуре он определяется без ключевого слова far (поскольку в ней все указатели «далекие»):
typedef const char *LPCTSTR;
3.4 Выражения |
119 |
Во всех программах указатели на константные строки описываются как имеющие тип L P C T S T R :
LPCTSTR s;
благодаря этому программы Microsoft можно использовать как в 16разрядной, так и в 32-разрядной архитектуре.
3.4.Выражения
Выражения в Си составляются из переменных или констант, к
которым |
применяются различные операции. Д л я указания порядка |
операций |
можно использовать круглые скобки. |
Отметим, что, помимо обычных операций, таких, как сложение или умножение, в Си существует ряд операций, несколько непривыч¬ ных для начинающих. Например, запятая и знак равенства (оператор присваивания) являются операциями в Си; помимо операции сложе¬
ния +, |
есть еще |
операция «увеличить на» + = |
и операция увели |
чения |
на единицу |
++ . Зачастую они позволяют |
писать эстетически |
красивые, но не очень понятные для начинающих |
программы. |
Впрочем, эти непривычные операции можно не использовать, за¬ меняя их традиционными.
3.4.1.Оператор присваивания
Оператор присваивания является основой любого алгоритмиче¬ ского языка (см. раздел 1.3). В Си он записывается с помощью сим¬ вола равенства, например, строка
x = 100;
означает присвоение переменной x значения 100. Д л я сравнения двух значений используется двойное равенство = =, например, строка
bool |
f = (2 + 2 == 5); |
|
присваивает логической переменной / |
значение false (поскольку 2 + 2 |
|
не равно |
пяти, логическое выражение |
в скобках ложно). |
Непривычным для начинающих может быть то, что оператор при¬ сваивания "=" в Си — бинарная операция, такая же, как, например,
120 |
3.4. Выражения |
сложение или умножение. Значением операции присваивания = яв¬ ляется значение, которое присваивается переменной, стоящей в ле¬ вой части. Это позволяет использовать знак присваивания внутри выражения, например,
x = (y = sin(z)) + 1.0;
Здесь в скобках стоит выражение y = sin(z) , в результате вычисле¬
ния которого переменной у присваивается значение sin z. Значением |
|
этого выражения является значение, присвоенное переменной |
у, т.е. |
sin z. К этому значению затем прибавляется единица, т.е. в |
резуль¬ |
тате переменной x присваивается значение sin z + 1. |
|
Выражения, подобные приведенному в этом примере, иногда ис¬ пользуются, когда необходимо запомнить значение подвыражения (в данном случае sin(z) ) в некоторой переменной (в данном случае y),
чтобы |
затем |
не вычислять |
его повторно. Ещ е один пример: |
n |
= (k |
= 3) +2; |
|
В результате |
переменной |
k присваивается значение 3, а переменной |
|
n — значение 5. Конечно, |
в нормальных программах такие выраже¬ |
||
ния не встречаются. |
|
3.4.2. Арифметические операции
К четырем обычным арифметическим операциям сложения +, вы¬ читания —, умножения * и деления / в Си добавлена операция нахо¬ ждения остатка от деления первого целого числа на второе, которая обозначается символом процента %. Приоритет у операции вычисле ния остатка % такой же , как и у деления или умножения. Отметим, что операция % перестановочна с операцией изменения знака (унар¬ ным минусом), например, в результате выполнения двух строк
x= -(5 % 3);
y= (-5) % 3;
обеим переменным x и y присваивается отрицательное значение —2.