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

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
238
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

I

200 I

Часть I ^ Введе:

Листинг 6.3. Коммуникации с другим файлом через внешние описания (часть 1)

#include <iostream> using namespace std;

struct

Account

{

/ /

глобальное определение типа

long num;

 

 

 

double bal;

} ;

 

 

extern

void printAverage(double t o t a l ) ;

/ /

определяется где-то еще

const

int MAX = 5;

 

 

Account a[MAX];

/ /

глобальные данные для обработки

int count = 0;

 

/ /

число элементов в наборе данных

char caption[] = "Средний остаток на счетах в долл. ";

/ /

заголовок для печати

long nuni[MAX] = { 800123456, 800123123, 800123333, -1

}

 

double amounts[MAX] = { 1200, 1500, 1800 } ;

/ /

набор данных для загрузки

void printAccountsO

i++)

{ for(int i = 0; i < count;

cout «

a[i].num « " " «

a[i].bal « endl; }

int mainO

 

 

 

{

 

 

 

 

 

double

t o t a l = 0;

 

while

(true)

 

 

{ i f

(num[count] == -1) break;

a[count].num

= num[count];

a[count].bal

= amounts[count++]; }

cout

«

" Данные загружены\п\п";

printAccountsO;

 

cout

«

"\n

Данные обработаны\п\п'';

for

(int i

= 0;

i < count;

i++)

{ t o t a l += a [ i ] . b a l ; }

 

printAverage(total);

 

return

0;

 

 

 

/ / глобальный счетчик

/ /

выход по контрольному

значению

/ /

глобальные а [ ] , num[],

amounts[]

/ /

загрузка данных

 

// локальная функция

// глобальная в другом файле

В применении к переменным ключевое слово extern имеет другой смысл. Во-первых, оно означает, что определенная в данном файле локальная переменная может быть доступна в другом файле; во-вторых, обозначает переменную, опреде­ ленную в другом файле и объявленную в этом файле, чтобы ее могли видеть содер­ жащиеся в файле функции. В первом случае применение ключевого слова extern необязательно, во втором — обязательно.

Кажется сложным? Не беспокойтесь — все достаточно просто. Ключевое сло­ во extern необязз'^ельно в определениях и обязательно в объявлениях. Рассмотрим примеры внешних переменных из листинга 6.3. Глобальные переменные из лис­ тинга 6.3 перечислены в определениях, следовательно, они являются внешними неявно и видимы в других файлах. Использовать ключевое слово extern нет необ­ ходимости, но, если оно указывается и переменная инициализируется, ничего пло­ хого не случится.

extern int count = 0;

/ / OK: это определение

Наличие инициализации указывает компилятору, что это определение, а не объяв­ ление. Если инициализация отсутствует, то без нее определение превраш,ается в объявление и компоновш,ик сообш,ит об отсутствии определения для count:

extern int count;

/ / это объявление

Глава 6 * Управление naivi^Tbio

Отсутствие инициализации и ключевого слова extern снова делает его определени­ ем (конечно, значение можно инициализировать в другом месте), и к переменной можно обраидаться из другого файла (листинг 6.4, где определяется printAverage[ ]).

i nt count;

/ / OK: это определение

О с т о р о ж н о ! Все глобальные переменные являются внешними по умолчанию. Применение ключевого слова extern необязательно. Оно сообщает сопровождающему приложение программисту, что переменная используется в других файлах. Если глобальная переменная не инициализируется

при определении, при включении в строку ключевого слова extern компоновщик принимает ее за объявление.

Массив caption[] из листинга 6.3 инициализируется. Следовательно, это опре­ деление (память для массива выделяется в фиксированной области), массив явля­ ется по умолчанию внешним (extern) и может использоваться в другом файле (листинге 6.4, где определяется printCaption[]). Массивы num[] и amounts[] также глобальные — с ними можно работать в других файлах. Но здесь этого не делается (и не должно, так как они просто содержат для программы данные иници­ ализации). То, что caption[] используется в других файлах, а num[] и amounts[] нет, не вполне очевидно для программиста. Скоро мы исправим этот недостаток, введя статический класс памяти static.

В листинге 6.4 приведена функция printCaption(). Она вызывается из этого файла функцией printAverageO и использует массив caption[], определенный в файле из листинга 6.3. Переменная count также определена как extern без инициализации, что превращает определение в объявление. Пропуск ключевого слова сделал бы его определением, но компоновщик будет помечать два опреде­ ления count как ошибку (даже если их типы разные). Однако компилятор обраба­ тывает исходные файлы по отдельности и пропускает эту проблему. Применение ключевого слова extern позволяет одному файлу обращаться к данным и функци­ ям, определенным в другом файле, но не сообщает сопровождающему приложение гфограммисту, какие глобальные переменные и функции используются в других файлах (как printAverageO), а какие нет (как printCaptionO). Применение

 

 

 

 

ключевого слова static

позволяет решить данную проблему.

Листинг 6.4.

Коммуникации с другим файлом через внешние описания (часть 2)

#inclucle

<iostream>

 

 

 

 

using

namespace std;

 

 

 

 

extern

count;

/ /

определяется

и инициализируется

в другом месте

extern

char

caption[];

/ /

определяется

и инициализируется

в другом месте

void

printCaptionO

/ /

вызывается только из этого файла

 

{ cout

«

caption; }

 

 

 

 

void

printAverage(double sun)

/ /

вызывается из другого файла

 

{ i f

(count

== 0) return;

 

 

 

 

printCaptionO;

 

 

 

 

cout

«

sum/count « endl;

 

 

 

 

Заметим, что объявление массива caption[ ] не требует указания размера мас­ сива, так как при объявлении объекта память не выделяется — оно указывает наличие определения данного объекта в другом месте. Не следует также инициа­ лизировать объекты extern: это превратит объявление в определение (и вызовет конфликт имен).

^ 202 I

Часть I ^ Введение в oporpaivii^HpOiaHHe на C^-i'

Ш^^Ш^ШШШШШЯШШШШШШШШШШШШШШвШШШШШШШШШЯШШШШШШШШШШШШШШЯШШШШШШШШШШШШ^ШШШШШШШШШШШШШКЯЯЙШШШШШШШШШ^

в отличие от определений внешние объявления могут повторяться в других файлах или даже в том же файле. При наличии таких объявлений программный код данного файла может использовать глобальные имена точно так же, как если бы определения содержались в этом файле. Например, в листинге 6.4 функция printAverageO ссылается на count, а функция printCaption() — на переменную caption[], определенную в другом файле (листинг 6.3).

Внешние переменные предоставляют хороший инструмент взаимодействия между функииями в разных файлах большой программы. Их нужно использовать только тогда, когда преимуш^ества распределения функций по разным файлам пе­ ревешивают преимуш,ества их включения в один файл. В листингах 6.3 и 6.4 пред­ ставлен хороший пример избыточных коммуникаций между файлами. Совмеш^ение тех программных элементов, которые должны быть вместе, устраняет необходи­ мость коммуникаций между файлами, описаний extern, упрош,ает проектирование и сопровождение программы, снижает вероятность ошибок.

Статические переменные

Ключевое слово static имеет в С + + пять значений. Для статических перемен­ ных характерны некоторые обш,ие особенности. (Память для них выделятся в фик­ сированной памяти, а не в стеке.) Однако различия между значениями слова static весьма суидественны, и, применяя его в разных контекстах, можно легко запутаться. Как static определяются следуюш,ие объекты C+ + :

• Глобальные переменные, которые должны быть доступны только в том же файле, где они определяются, но не в других файлах

• Локальные переменные, определенные в функции (или в неименованном блоке), значения которых должны сохраняться между вызовами функций (или при переходе из одной области выполнения в другую)

Поля структуры или класса, которые при использовании всех переменных или объектов данного класса должны ссылаться только на одну ячейку памяти

Функции-члены класса, обращаюидиеся только к параметрам, глобальным переменным и статическим переменным класса, но не к нестатическим полям класса

Глобальные функции (не члены класса), доступные для кода KjmcHTa только в том же файле, где они определены, но не в других файлах

Пока что рано обсуждать все эти пункты, поэтому мы остановимся на первых двух. Об остальных рассказано в главе 8.

Первый вариант использования ключевого слова s t a t i c — д л я глобальных переменных — представляет моидное средство, позволяюндее создавать закрытые (для файла) переменные. Никакой другой файл не сможет обрандаться к таким переменным, объявляя их как extern. Например, в листинге 6.3 определяются глобальные переменные МАХ, а[ ], count, caption[ ], num[ ] и amounts[ ], но не ука­ зывается, какие переменные достугшы из других файлов. Чтобы указать, что пере­ менная count доступна из других файлов, и обеспечить недостугшость прочих глобальных переменных в других файлах (ведь все функции, обраидаюидиеся к этим глобальным переменным, находятся в том же файле), в листинге 6.3 можно опре­ делить эти глобальные переменные как static:

i nt count = 0;

 

/ /

может быть объявлена как extern в других файлах

static

const int МАХ=5;

/ /

нельзя сделать extern в другом месте

static

Account

а[МАХ];

/ /

нет доступа из программ в других файлах

static long num[MAX]-{ 800123456, 800123123. 800123333, -1 } ;

static

double

amounts[MAX]

= {

1200, 1500, 1800 };

Глава 6 . Управление памятью [__20зЗ

При добавлении в определение глобальной переменной слова static не меня­ ется ни место, где выделяется память доя переменной, ни время ее существования (от начала до конца выполнения программы). Единственный результат состоит в том, что переменная не может определяться как extern в других исходных файлах и недоступна из других файлов программы. Настоятельно рекомендуется использовать именно такой метод программирования.

Обратите внимание, что массив caption[] отсутствует среди этих глобальных переменных. Поскольку он используется только функцией printCaption[] (в листинге 6.4), его не следует отделять от этой функции и помещать в файл из листинга 6.3, где ни одна функция к нему не обращается. Этот массив лучше переместить в файл из листинга 6.4. Так как функции, определенные в других

файлах, не обращаются к данному массиву,

он может (и должен) объявляться

в листинге 6.4 как static. Начало листинга 6.4 может выглядеть так:

extern count;

/ /

определяется в другом месте

static char caption[]

/ /

нет extern, определяется и

 

/ /

инициализируется здесь

= "Средний остаток на счетах в долл. ";

/ /

используется локально,

 

/ /

нет в других файлах

Некоторые программисты считают, что это скорее мера безопасности. Приме­ нение подобных переменных static исключает ошибки, предотвращая случайные или несанкционированные изменения в других частях программы. Все это так, но подобные ошибки достаточно редки. Реальная ценность данного метода — в све­ дении к минимуму необходимости координации между разработчиками. Если определять глобальные переменные как static, то можно использовать такие популярные имена как МАХ, а, num, amounts и caption в одном файле программы, не координируя выбора имен.

В общем случае использование глобальных переменных лучше ограничивать. Когда они применяются для коммуникаций между функциями в одном файле, то описание переменных как static уменьшает необходимость взаимодействия между программистами, работающими над разными файлами (главное — не раз­ делять те компоненты, которые должны располагаться вместе). Конечно, если глобальная переменная определяется как static, к ней нельзя обращаться из другого файла. Если же не сделать ее статической, то нет гарантии, что к ней действительно не будет обращений из другого файла, или разработчик программы никак не сообщит, что данная переменная используется только в одном файле. Именно поэтому нужно кропотливо указывать ключевое слово static для гло­ бальных статических переменных.

Метод определения статических глобальных переменных очень важен в язы­ ке С. Именно так в этом языке был впервые использован объектно-ориентирован­ ный подход. Данные и функции связывались в одном файле (как массив caption[ ] и функция printCaptionO после перемещения массива в листинге 6.4). Данные определялись как static, а следовательно, их нельзя увидеть извне. Функции в этом файле могли вызываться из других файлов и обращаться к данным от имени функций-клиентов.

В C + + данные и функции объединяются в классы, что уменьшает необходи­ мость применения глобальных переменных. Пространства имен также снижают потребность в глобальных переменных. Следовательно, в C + + такая техника программирования не столь важна, как в языке С. Тем не менее при определении глобальных переменных не следует забывать о static, чтобы свести к минимуму взаимодействие с другими разработчиками и передать информацию о коммуника­ циях между функциями сопровождающему приложение программисту.

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

I 204

Часть I # Введение в програ^л^лирование на C++

переменную из стека в фиксированную область памяти. Там переменная будет существовать не от начала до конца функции или блока (как автоматические пере­ менные), а от начала программы до конца ее выполнения, т. е. значение в данной области памяти присваивается при первом входе в область действия и будет доступно при повторном входе в нее. Что касается имени переменной, то к нему все равно применимы правила области действия, о которых рассказывалось выше. Имя неизвестно вне фигурных скобок — блока, где она определена. Следовательно, другие независимые области действия (даже в том же файле) могут использовать это имя для своих целей. Более того, несколько переменных в разных областях действия могут определяться как static и носить одно и то же имя. Это не приве­ дет к конфликту имен, хотя память для переменных отводится в фиксированной области. Поскольку они находятся в разных областях действия, эти имена извест­ ны в разные моменты выполнения программы.

Например, функцию printAccountsO в листинге 6.3 можно модифицировать для печати только одного счета. Для этого удобно определить глобальную пере­ менную i и использовать ее как индекс в printAccountsO:

const int MAX = 5; Account a[MAX]; int count = 0;

int i ;

void printAccountsO

{cout « a[i].num « " i++; }

/ /

глобальные данные для обработки

/ /

число элементов в наборе данных

/ /

глобальный индекс

« a [ i ] . b a l

« endl;

/ /

увеличить индекс после использования

В функции mainO удобно вызывать функцию printAccountsO в цикле:

for (int j = 0; j < count; j++) printAccountsO;

Язык позволяет использовать индекс i и в цикле, но текущая версия моего компилятора не дает возможности делать это. (В последнем цикле в листинге 6.3 определяется i.) Недостаток такого подхода состоит в использовании большего числа глобальных переменных ("загрязнении" глобального пространства). Чтобы избежать этого, можно переместить определение индекса i в printAccountsO и избежать потенциальных конфликтов с другими случаями использования этого имени:

void printAccountsO

 

{ int

i = 0;

 

cout

« a[i].num «

« a [ i ] . b a l « endl;

i++;

}

/ / увеличение индекса после использования

Теперь индекс является автоматической переменной. Он получает новое про­ странство в стеке при каждом вызове функции printAccountsO из main(). Следо­ вательно, значение индекса из предыдущего вызова не запоминается. Кроме того, индекс устанавливается в О при каждом вызове данной функции. Ключевое слово static решает обе проблемы:

void printAccountsO

 

{ static

int i = 0;

 

cout

«

a[i].num «

« a [ i ] . b a l « endl;

i++;

}

 

/ / увеличение индекса после использования

На первый взгляд, в этом нет смысла. Как увеличивать индекс, если значение i сбрасывается в О при каждом вызове? Но это не так, как вы подумали.

Глава 6 ^ Управление памятью

[ 205 |

В предыдуш,ей версии printAccountsO начальное значение присваивалось! при каждом вызове. В данной версии, поскольку i — статическая переменная, она присваивается только один раз, хотя, казалось бы, это делается при каждом вызове цикла. На самом деле присваивание происходит не при первом вызове функции printAccountsO, а когда распределяется память для всех глобальных переменных— перед первым оператором в функции main(). Когда вызывается функция printAccountsO, оператор инициализации пропускается и в следующем операторе используется предыдуидее значение данной локальной переменной:

void printAccountsO

 

 

 

{ static

int i = 0;

/ /

выполняется

только один раз

cout

«

a[i].num « " " « a [ i ] . b a l «

endl;

 

 

i++;

}

 

/ /

выполняется

при каждом вызове

В этом случае в явной инициализации даже нет необходимости. Переменные static неявно инициализируются нулем, и следующая версия функции printAccountsO вполне законна:

void printAccountsO

 

 

{ static int i ;

/ /

неявная инициализация нулем

cout

« a[i].num «

" " « a [ i ] . b a l « endl;

i++;

}

/ /

увеличение индекса после использования

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

Применение локальных переменных static не считается хорошей практикой программирования. Оно требует чрезмерной координации клиентских и серверных функций, затрудняет понимание программы. Кроме того, в нем редко возникает необходимость. В большинстве случаев нетрудно найти решение, не требующее применения локальных переменных static. Например, способ печати остатков на счетах в листинге 6.3 (и в предыдущих версиях программы) прост, и в локальных переменных static нет нужды.

Глобальные функции static аналогичны статическим глобальным переменным в том смысле, что они не могут вызываться вне того файла, где определены — имя функции static невидимо для других файлов. Это означает, что данное имя можно использовать в другом файле для каких-то иных целей, без всяких конфликтов имен и прочих помех. Если функция вызывается только теми функциями, которые находятся в том же файле, где она определяется, то полезно явно определить ее как статическую, сделав видимой только в этом файле, а не во всей программе. Так, в листинге 6.3 функцию printAccountsO можно сделать статической:

static

void

printAccountsO

{ static

int

i = 0;

cout

«

a[i].num « " " « a [ i ] . b a l « endl;

i++;

}

 

/ / увеличение индекса после использования

Функцию printCaptionO из листинга 6.4 также можно определить как static:

static

void printCaptionO

/ / вызывается только из этого файла

{ cout

« caption; }

 

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

206

Часть ! ^ ВведенЕ^е в протратттрошаиие на €+4^

эти имена функций без координации. Кроме того, это явно говорит сопровождаю­ щему приложение программисту, что в других файлах нет функций, зависящих от данного файла. Размещение клиентских и серверных функций в одном файле не всегда возможно и желательно. Когда это делается, следует документировать данные действия, определяя функции-серверы как static.

В использовании класса памяти static для связанных с классами функций есть еще один аспект. Они могут обращаться только к статическим полям класса. Подробнее о полях и функциях static рассказывается ниже.

Управление памятью:

использование динамически распределяемой области

 

Правила области действия и разнообразные классы памяти в C++ прошли

 

долгий путь эволюции, чтобы помогать программистам управлять памятью, выде-

^

ляемой для программных объектов. Однако эти средства не решают проблему

 

адекватной реализации динамических структур данных.

 

Реализация массивов динамических структур данных с контрольным значением

 

или счетчиком допустимых записей — мощный и простой инструмент. Когда число

 

элементов в наборе данных увеличивается или уменьшается, они позволяют до­

 

бавлять или удалять компоненты. Однако максимальный размер данных должен

 

быть известен на этапе компиляции. Выбор неоптимального размера влечет опас­

 

ность переполнения или напрасной траты памяти.

 

Динамическое управление памятью решает эту проблему, поскольку память

 

выделяется и освобождается динамически. Когда набор данных заполняет все

 

пространство в массиве, динамически выделяется массив большего размера, туда

 

копируются данные, а старый массив освобождается. Если данных становится

 

меньше и слишком много памяти теряется впустую, выделяется массив меньшего

 

размера, данные копируются в этот новый массив, а массив большего размера

 

освобождается. Такой метод устраняет опасность переполнения и непроизводи­

 

тельной траты памяти.

 

Еще одна проблема непрерывных массивов состоит в том, что они эффективны

 

только тогда, когда новые элементы добавляются в конец массива. Если нужно

 

включить новый элемент в начало или в середину массива, придется сдвигать

 

остальные элементы в конце массива.

 

Когда же элемент удаляется из середины массива, нужно также сдвигать

 

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

 

не в устранении пробела в середине массива, а в использовании еще одного конт­

 

рольного значения, обозначающего удаленные элементы. Это позволяет избежать

 

необходимости сдвига при удалении, но требует дополнительной проверки каждого

 

элемента при поиске. Если массив короткий, то вставка или удаление из середины

 

массива случаются нечасто и эти недостатки несущественны. Для длинных мас­

 

сивов частая вставка и удаление элементов может отрицательно повлиять на

 

производительность.

 

Одно из возможных решений данных проблем состоит в отказе от массива как

 

механизма выделения памяти сразу для большого числа элементов. Вместо этого

 

можно выделять память для элемента, только когда он вставляется в набор дан­

 

ных. Отдельные элементы связываются указателями, содержащими адреса дина­

 

мически распределяемых элементов. С помощью операций с указателями можно

 

вставлять элемент в набор данных, не тратя время на сдвиг других элементов.

 

Когда элемент нужно удалить, его память освобождается для других целей. Опе­

 

рации с указателями также позволяют устранить пробел без сдвига элементов

 

и пометки элемента как удаленного.

Глава 6 ^ Управление па1У1ятью

[ 207 р

Применение указателей для динамических массивов и связанных структур дан­ ных — полезный и популярный метод. Но он сложнее, чем использование масси­ вов фиксированного размера, о которых рассказывалось выше. Ошибки в работе с указателями — это ошибки этапа выполнения, нередко их трудно обнаружить. Частое выделение и освобождение памяти может отрицательно повлиять на про­ изводительность.

Во многих языках (в частности, в Lisp, Eiffel и Java) управление памятью счи­ тается слишком важным для целостности программ, чтобы можно было доверять его делаюш.ему ошибки программисту. В этих языках применяется так называемая автоматическая "сборка мусора": оценивается использование программой памяти

иосвобождаются области, которые нужно вернуть в пул памяти. На самом деле эти механизмы довольно медленные, сложные и неточные.

ВC + + по аналогичной причине используется противоположный подход. Управление памятью здесь считается слишком важным для производительности программы, чтобы его можно было доверять обш,им (и часто неэффективным) алгоритмам. В C++ программист может полностью управлять распределением

иосвобождением памяти. Если программист делает ошибки, они могут привести

кпорче или "утечкам" памяти и аварийному завершению программы. Это плохо, но хорошие программисты не делают много ошибок. Алгоритмы динамического распределения памяти просты. При аккуратной реализации они вполне надежны. Кроме того, стандартные библиотеки предусматривают соответствуюш,ие структу­ ры данных и функции, помогаюидие программисту избежать ошибок.

Сегмент памяти программ для хранения динамических данных, выделяемый в результате явного запроса, называется динамически распределяемой па­ мятью (heap — куча). Именно нечто похожее на "кучу" представляет собой память после многократного выделения и освобождения. Структура динамически распределяемой памяти упрош^ает поиск свободной памяти подходящего размера для удовлетворения следуюш,его запроса.

Размер фиксированного сегмента данных для глобальных и статических пере­ менных вычисляется в процессе компиляции и компоновки. Размер стека и дина­ мически распределяемой области не может вычисляться автоматически. Обычно стек и динамически распределяемая область памяти увеличиваются во встречном направлении (чтобы избежать преждевременного переполнения).

Между переменными, выделяемыми в динамически распределяемой области,

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

Память для обычных переменных (распределяемых в стеке

ификсированной области памяти) выделяется согласно правилам языка. Память для переменных динамически распределяемой области выделяется с помош,ью явных операций, указываемых программистом.

Обычные переменные имеют имена — псевдонимы

(мнемонические ссылки) своих ячеек памяти. Переменные, распределяемые в динамической области,

имен не имеют. На них можно ссылаться посредством указателей.

Указатели C+ +

как типизированные переменные

Указатель — это переменная, содержащая адрес другой переменной, которая

.- может быть обычной (именованной) стековой переменной. Указатели, как правило, ссылаются на переменные, распределяемые в динамической области (неимено­ ванные). Сами указатели обычно размещаются в фиксированной секции данных (как глобальные или статические переменные) или в стеке (автоматические). Использование для указателя динамически распределяемой области — очень редкое явление. Указатели представляют собой просто именованные переменные.

^ 208

ойв-дение в орогра^лг^ировонЕ^е на С-ь-^

В С4-+ указатели в основном используются:

Для динамического распределения массивов, размер которых задается на этапе выполнения

Для создания динамических структур данных, состоящих из несмежных связанных узлов

Для передачи параметров функциям

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

Для создания переменной-указателя, во-первых, задается, что это указатель, во-вторых, указывается тип переменной, на которую он ссылается. Указатели C + + могут ссылаться (с помощью косвенной ссылки) на переменную только одного типа. Тип задается при определении указателя-переменной. Изучив более сложные аспекты C+ + , такие, как наследование и полиморфизм (хотя пока эти слова мало что говорят), можно понять, что данное правило имеет некоторые ин­ тересные исключения, но сейчас полезно запомнить, что указатель определяется как указатель на целое и должен ссылаться на значение типа int, а не double. Соответственно указатель на переменную double должен указывать на значение double, а не на целое.

Для обозначения переменной-указателя используется звездочка (*). Она сле­ дует за именем типа или перед именем переменной; пробелы между звездочкой и именем переменной или типа не имеют значения:

int * pi; char* рс; double*pd; / / годится любое число пробелов

Обратите внимание, что звездочка здесь — даже не операция, а просто обо­ значение того, что переменная — это указатель на тип слева от звездочки. Объяв­ ления с указателями следует читать справа налево. Например, pi — указатель на переменную типа int, а рс — указатель на переменную типа char. Можно сказать также, что pi имеет тип int* или рс — переменная типа char*. Как будет показано позднее, последний способ достаточно полезен.

Но в данных выражениях звездочка — не просто обозначение, это операция, применяемая к переменной-указателю (pi, рс и т. д.) для получения значения ба­ зового типа. Это значение по адресу, на который ссылается переменная-указа­ тель. Имя операции-звездочки, считывающей значение по указателю, называется операцией разыменования (dereference) или снятия косвенности.

Если получение адреса значения называется указанием, то получение значения из адреса— это "разуказание", а не разыменование, но в C + + терминология заимствована из языка С, а С разрабатывался как язык высокого уровня для программистов, использовавших ассемблер. В ассемблере программисты называ­ ли вещи так, как им нравится, так что простым смертным их не понять.

Областью действия указателя-звездочки будет просто одна переменная-указа­ тель. Она применяется к следующему за звездочкой идентификатору, а не к пред­ шествующему имени типа. Это отличается от других определений и объявлений. Например, ниже только рс является указателем на char: pchar имеет тип char, а не char*.

char* PC pchar; / / pchar имеет тип char, a не char*

Это очень распространенная ошибка. Чтобы сделать pchar указателем, нужно записать:

char* рс, *pchar; // both рс и char - указатели

Глава 6«Управление паг^ятью

| 209 |

Указатели — обычные именованные переменные, автоматические или глобаль­ ные. Для них выделяется достаточно памяти, чтобы вместить адрес конкретного типа. Часто размер указателя совпадает с размером целого, однако рассчитывать на это не стоит. Операция sizeof может точно сообщить, так ли это на конкретной машине, но было бы неразумно писать программу, полагающуюся на размер ука­ зателя. Она не будет переносимой.

О с т о р о ж н о ! Переменные-указатели (адреса) часто имеют размер целого, независимо от типа значений, на которые они указывают.

Не используйте размер указателя в своей программе — это может сделать программу непереносимой.

Подобно другим переменным, указатели по определению не имеют полезного значения (содержат О, если они глобальные, и не определены, если автоматиче­ ские). Указатели могут содержать адреса следуюш,их объектов:

Встроенных типов (char* рс)

Типов, определяемых программистов (Account* pa)

Массивов встроенного или определяемого программистом типа (обозначение то же, что и для переменных, например char* рс или Account* pa);

Функций (пока еш,е рано описывать указатели-функции)

Другие указатели; так char** рсс можно использовать как указатель на символьный указатель (как рс выше). Здесь лишь упоминается, что такое возможно, но не торопитесь применять это в своих программах

Для доступа к значению объекта, на который ссылается указатель, нужно про­ извести операцию разыменования (звездочку) к указателю как префиксную опера­ цию (слева от имени указателя). Другими словами, указатель разыменовывается. Например, ниже в переменную типа double (на нее ссылается указатель pd) помеш,ается значение 5.0, значение 20 помещается в переменную типа int, на которую указывает pi, а значение 'а' — в символьную переменную с указате­ лем рс (если значение типа double по указателю pd положительно, а это так):

pd = 5.0; *pi = 20; i f (*pd > 0) *pc = ' a ' ;

/ / нехорошо

Как уже упоминалось выше, если pi — указатель на int, то *pi имеет тип int. Аналогично *pd имеет тип double. С точки зрения типов значений пример коррек­ тен, однако эти указатели нигде не инициализируются, а разыменование неини­ циализированных указателей недопустимо.

Если не инициализируется глобальный указатель, он содержит 0. Разыменова­ ние нулевого указателя немедленно завершает программу:

pd = NULL; *pd = 5.0;

/ / нулевой указатель - исключение *

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

Соседние файлы в предмете Программирование на C++