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

Основы программирования. Борисенко

.pdf
Скачиваний:
1610
Добавлен:
09.04.2015
Размер:
9.31 Mб
Скачать

3.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.