Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лафоре Р. - Объектно-ориентированное программир...doc
Скачиваний:
0
Добавлен:
01.01.2020
Размер:
40.77 Mб
Скачать

Глава 10 Указатели

  • Адреса и указатели

  • Операция получения адреса &

  • Указатели и массивы

  • Указатели и функции

  • Указатели на строки

  • Управление памятью: операции new и delete

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

  • Связный список

  • Указатели на указатели

  • Пример разбора строки

  • Симулятор: лошадиные скачки

  • UML-диаграммы

  • Отладка указателей

Для программистов на C++ указатели являются настоящим кошмаром, кото- рый заставит растеряться кого угодно. Но бояться не стоит. В этой главе мы по- пытаемся прояснить тему указателей и рассмотреть их применение в програм- мировании.

Для чего нужны указатели? Вот наиболее частые примеры их использования:

  • доступ к элементам массива;

  • передача аргументов в функцию, от которой требуется изменить эти аргу- менты;

  • передача в функции массивов и строковых переменных;

  • выделение памяти;

  • создание сложных структур, таких, как связный список.

Указатели — это важная возможность языка C++, поскольку многие другие языки программирования, такие, как Visual Basic или Java, вовсе не имеют указа-

телей. (В Java используются ссылки.) Действительно ли указатели являются столь необходимыми? В предыдущих главах мы видели, что многое можно сделать и без них. Некоторые операции, использующие указатели, могут быть выполне- ны и другими путями. Например, доступ к элементу массива мы можем полу- чить и не используя указатели (разницу мы рассмотрим позднее), а функция может изменить аргумент, переданный не только по указателю, но и по ссылке.

Однако в некоторых ситуациях указатели являются необходимым инстру- ментом увеличения эффективности программ на языке C++. Замечательным примером является создание таких структур данных, как связные списки или бинарные деревья. Кроме того, некоторые ключевые возможности языка C++, такие, как виртуальные функции, операция new, указатель this (которые мы об- судим в главе 11 «Виртуальные функции»), требуют использования указателей. Поэтому, хотя мы и можем многое сделать без них, указатели будут нам необхо- димы для более эффективного использования языка программирования.

В этой главе мы познакомимся с указателями, начав с основных концепций и закончив сложными случаями с применением указателей.

Если вы уже знакомы с языком C, то вы можете лишь поверхностно озна- комиться с первой частью главы. Однако вам следует остановиться на разде- лах второй половины главы, посвященных таким вопросам, как операции new и delete, доступ к функциям классов с использованием указателей, массивы указателей на объекты и связные списки.

Адреса и указатели

Идея указателей несложна. Начать нужно с того, что каждый байт памяти компьютера имеет адрес. Адреса — это те же числа, которые мы используем для домов на улице. Числа начинаются с 0, а затем возрастают — 1, 2, 3 и т. д. Если у нас есть 1 Мбайт памяти, то наибольшим адресом будет число 1 048 575 (хотя обычно памяти много больше).

Загружаясь в память, наша программа занимает некоторое количество этих адресов. Это означает, что каждая переменная и каждая функция нашей про- граммы начинается с какого-либо конкретного адреса. На рис. 10.1 показано, как это выглядит.

Операция получения адреса &

Мы можем получить адрес переменной, используя операцию получения адреса &. Рассмотрим небольшую программу VARADDR, показывающую, как это сделать:

// varaddr.cpp // адрес переменной #include <iostream> using namespace std;

int main()

{

int var1 = 11; // определим три переменных

int var2 = 22; // и присвоим им некоторые значения

int var3 = 33;

cout << &var1 << endl // напечатаем адреса этих переменных

<< &var2 << endl

<< &var3 << endl;

return 0;

}

В этой простой программе определены три целочисленные переменные, ко- торые инициализированы значениями 11, 22 и 33. Мы выводим на экран адреса этих переменных.

Реальные адреса, занятые переменными в программе, зависят от многих фак- торов, таких, как компьютер, на котором запущена программа, размер оператив- ной памяти, наличие другой программы в памяти и т. д. По этой причине вы, скорее всего, получите совершенно другие адреса при запуске этой программы (вы даже можете не получить одинаковые адреса, запустив программу несколь- ко раз подряд). Вот что мы получили на нашей машине:

0x8f4ffff4 - адрес переменной var1 0x8f4ffff2 - адрес переменной var2 0x8f4ffff0 - адрес переменной var3

Запомните, что адреса переменных — это не то же самое, что их значение. Содержимое трех переменных — это 11, 22 и 33. На рис. 10.2 показано размеще- ние трех переменных в памяти.

Рис. 10.1. Адреса в памяти

Рис. 10.2. Адреса и содержимое переменных

Использование операции << позволяет показать адреса в шестнадцатеричном представлении, что видно из наличия префикса 0x перед каждым числом. Это обычная форма записи адресов памяти. Не беспокойтесь, если вы не знакомы с шестнадцатеричной системой. Вам всего лишь нужно помнить, что каждая пе- ременная имеет уникальный адрес. Однако вы могли заметить в выводе на дис- плей, что адреса отличаются друг от друга двумя байтами. Это произошло пото- му, что переменная типа int занимает два байта в памяти (в 16-битной системе). Если бы мы использовали переменные типа char, то значения адресов отлича- лись бы на единицу, так как переменные занимали бы по 1 байту, а если бы мы использовали тип double, то адреса отличались бы на 8 байтов.

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

Не путайте операцию адреса переменной, стоящую перед ее именем, и опера- цию ссылки, стоящую за именем типа в определении или прототипе функции (ссылки мы обсуждали в главе 5 «Функции»).

Переменные указатели

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

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

Какого же типа может быть переменная-указатель? Она не того же типа, что и переменная, адрес которой мы храним: указатель на int не имеет типа int. Воз- можно, вы думаете, что указатели принадлежат некоторому типу pointer или ptr. Однако здесь все немного сложнее. В программе PTRVAR показан синтаксис пе- ременных-указателей.

// ptrvar.cpp

// указатели (адреса переменных)

#include <iostream>

using namespace std;

int main()

{

int var1 = 11; // две переменные

int var2 = 22;

cout << &var1 << endl // покажем адреса переменных

<< &var2 << endl << endl;

int* ptr; // это переменная-указатель на целое

ptr = &var1; // присвоим ей значение адреса var1

cout << ptr << endl; // и покажем на экране

ptr = &var2; // теперь значение адреса var2

cout << ptr << endl; // и покажем на экране

return 0;

}

В этой программе определены две целочисленных переменных var1 и var2, которые инициализированы значениями 11 и 22. Затем программа выводит на дисплей их адреса.

Далее в программе определена переменная-указатель в строке

int* ptr;

Для непосвященных это достаточно странный синтаксис. Звездочка означает указатель на. Таким образом, в этой строке определена переменная ptr как ука- затель на int, то есть эта переменная может содержать в себе адрес переменной типа int.

Почему же идея создания типа pointer, который бы включал в себя указатели на данные любого типа, оказалась неудачной? Если бы этот тип существовал, то мы могли бы записать объявление переменной этого типа как:

pointer ptr;

Проблема в том, что компилятору нужны сведения о том, какого именно типа переменная, на которую указывает указатель (мы поймем почему, когда будем рассматривать указатели и массивы). Синтаксис, используемый в C++, позволяет нам объявить указатель следующим образом:

char* cptr; // указатель на символьную переменную

int* iptr; // указатель на целую переменную

float* fptr; // указатель на вещественную переменную

Distance* distptr; // указатель на переменную класса Distance

Недостатки синтаксиса

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

char *charptr;

Это не принципиально для компилятора, но звездочка, расположенная сра- зу за названием типа переменной, сигнализирует о том, что это не просто тип, а указатель на него.

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

char* ptr1, * ptr2, * ptr3; // три переменных указателя

И в таком случае можно использовать стиль написания, при котором звез- дочка, ставится рядом с именем:

char *ptr1, *ptr2, *ptr3;

Указатели должны иметь значение

Пусть у нас есть адрес 0x8f4ffff4, мы можем его назвать значением указателя. Указатель ptr называется переменной-указателем. Как переменная var1 типа int может принимать значение, равное 11, так переменная-указатель может прини- мать значение, равное 0x8f4ffff4.

Пусть мы определили переменную для хранения некоторого значения (по- ка мы ее не инициализировали). Она будет содержать некое случайное число. В случае с переменной-указателем это случайное число является неким адресом в памяти. Перед использованием указателя необходимо инициализировать его определенным адресом. В программе PTRVAR указателю ptr присваивается адрес переменной var1:

ptr = &var1; // помещает в переменную ptr адрес переменной var1

Затем программа выводит на дисплей значение, содержащееся в переменной ptr, это будет адрес переменной var1. Далее указатель принимает значение адре- са переменной var2 и выводит его на экран. На рис. 10.3 показаны действия про- граммы PTRVAR, а ниже мы приведем результат ее работы:

0x8f51fff4 - адрес переменной var1 0x8f51fff2 - адрес переменной var2

0x8f51fff4 - значение ptr равно адресу переменной var1 0x8f51fff2 - значение ptr равно адресу переменной var2

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

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

Рис. 10.3. Изменение значений указателя ptr

Доступ к переменной по указателю

Предположим, что мы не знаем имени переменной, но знаем ее адрес. Можем ли мы получить доступ к значению этой переменной? (Возможно, мы потеряли список имен переменных.)

Существует специальная запись, предназначенная для доступа к значению переменной, используя ее адрес вместо имени. В программе PTRACC показано, как это можно сделать:

// ptracc.cpp

// доступ к переменной через указатель

#include <iostream>

using namespace std;

int main()

{

int var1 = 11; // две переменные

int var2 = 22;

int* ptr; // указатель на целое

ptr = &var1; // помещаем в ptr адрес переменной var1

cout << *ptr << endl; // показываем содержимое переменной через указатель

ptr = &var2; // помещаем в ptr адрес переменной var2

cout << *ptr << endl; // показываем содержимое переменной через указатель

return 0;

}

Эта программа очень похожа на PTRVAR, за исключением того, что вместо вы- вода на дисплей адресов, хранящихся в переменной ptr, мы выводим значения, хранящиеся по адресу, на который указывает ptr. Результат работы программы будет следующим:

11 22

Выражение *ptr, дважды встречающееся в нашей программе, позволяет нам получить значения переменных var1 и var2.

Звездочка, стоящая перед именем переменной, как в выражении *ptr, называ- ется операцией разыменования. Эта запись означает: взять значение переменной, на которую указывает указатель. Таким образом, выражение *ptr представляет собой значение переменной, на которую указывает указатель ptr. Если ptr указы- вает на переменную var1, то значением выражения *ptr будет 11. На рис. 10.4 показано, как работает операция разыменования.

Указатели можно использовать не только для получения значения перемен- ной, на которую он указывает, но и для выполнения действий с этими перемен- ными. Программа PTRTO использует указатель для присвоения значения одной переменной, а затем другой:

// ptrto.cpp

// еще один пример доступа через указатель

#include <iostream>

using namespace std;

int main()

{

int var1, var2; // две переменные

int* ptr; // указатель на целое

ptr = &var1; // пусть ptr указывает на var1

*ptr = 37; // то же самое, что var1 = 37;

var2 = *ptr; // то же самое, что var2 = var1;

cout << var2 << endl; // убедимся, что var2 равно 37

return 0;

}

Запомните, что звездочка, используемая в операции разыменования, — это не то же самое, что звездочка, используемая при объявлении указателя. Операция

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

int* ptr; // обьявление: указатель на int

*ptr = 37; // разыменование: значение переменной, адресованной через ptr

Рис. 10.4. Доступ к переменным через указатели

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

Рассмотрим небольшие примеры того, что мы изучили:

int v; // определим переменную v типа int

int* р; // определим переменную типа указатель на int

р = &v; // присвоим переменной р значение адреса переменной v

v = 3; // присвоим v значение 3

*р = 3; // сделаем то же самое, но через указатель

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

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

Указатель на void

Перед тем как мы продолжим рассматривать работу указателей, необходимо от- метить одну их особенность. Адрес, который помещается в указатель, должен быть одного с ним типа. Мы не можем присвоить указателю на int адрес пере- менной типа float.

float flovar = 98.6;

int* ptrint = &flovar; // Так нельзя; типы int* и float* несовместимы

Однако есть одно исключение. Существует тип указателя, который может указывать на любой тип данных. Он называется указателем на void и определя- ется следующим образом:

void* ptr; // указатель, который может указывать на любой тип данных

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

Рассмотрим пример использования указателя на void. В этой программе так- же показано, что если вы не используете указатель на void, то вам нужно быть осторожными, присваивая указателю адрес того же типа, что и указатель. Вот листинг программы PTRVOID:

// ptrvoid.cpp

// указатель на void

#include <iostream>

using namespace std;

int main()

{

int intvar; // целочисленная переменная

float flovar; // вещественная переменная

int* ptrint; // указатель на int

float* ptrflo; // указатель на float

void* ptrvoid; // указатель на void

ptrint = &intvar; // так можно: int* = int*

// ptrint = &flovar; // так нельзя: int* = float*

// ptrflo = &intvar; // так нельзя: float* = int*

ptrflo = &flovar; // так можно: float* = float*

ptrvoid = &intvar; // так можно: void* = int*

ptrvoid = &flovar; // так можно: void* = float*

return 0;

}

Мы можем присвоить адрес intvar переменной ptrint, потому что обе этих пе- ременных имеют тип int*. Присвоить же адрес flovar переменной ptrint мы не можем, поскольку они разных типов: переменная flovar типа float*, а переменная

ptrint типа int*. Однако указателю ptrvoid может быть присвоено значение любо- го типа, так как он является указателем на void.

Если вам по некоторым причинам необходимо присвоить одному типу указа- теля другой тип, то вы можете использовать функцию reinterpret_cast. Для строк программы PTRVOID, которые мы закомментировали, это будет выглядеть сле- дующим образом:

ptrint = reinterpret_cast<int*>(&flovar); ptrflo = reinterpret_cast<float*>(&intvar);

Использование функции reinterpret_cast в этом случае нежелательно, но мо- жет оказаться выходом из сложной ситуации. Нединамические вычисления не работают с указателями. В старой версии C с вычислениями можно было так поступать, но для C++ это будет плохим тоном. Мы рассмотрим примеры функ- ции reinterpret_cast в главе 12 «Потоки и файлы», где она используется для изме- нения способа интерпретации данных из буфера.

Указатели и массивы

Указатели и массивы очень похожи. В главе 7 «Массивы и строки» мы рассмот- рели, как можно получить доступ к элементам массива. Вспомним это на приме- ре ARRNOTE:

// arrnote.cpp

// обычный доступ к элементам массива

#include <iostream>

using namespace std;

int main()

{

int intarray[5] = { 31, 54, 77, 52, 93 }; // набор целых чисел

for(int j = 0; j < 5; j++) // для каждого элемента массива

cout << intarray[j] << endl; // напечатаем его значение

return 0;

}

Функция cout выводит элементы массива по очереди. Например, при j, рав- ном 3, выражение intarray[j] принимает значение intarray[3], получая доступ к четвертому элементу массива, числу 52. Рассмотрим результат работы програм- мы ARRNOTE:

31 54 77 52 93

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

// ptrnote.cpp

// доступ к элементам массива через указатель

#include <iostream>

using namespace std;

int main()

{

int intarray[5] = { 31, 54, 77, 52, 93 }; // набор целых чисел

for(int j = 0; j < 5; j++) // для каждого элемента массива

cout << *(intarray + j) << endl; // напечатаем его значение

return 0;

}

Результат действия выражения * (intarray + j) — тот же, что и выражения intarray[j] в программе ARRNOTE, при этом результат работы программ одинаков. Что же представляет из себя выражение *(intarray + j)? Допустим, j равно 3, тогда это выражение превратится в *(intarray + 3). Мы предполагаем, что оно содержит в себе значение четвертого элемента массива (52). Вспомним, что имя массива является его адресом. Таким образом, выражение intarray + j — это адрес чего-то в массиве. Вы можете ожидать, что intarray + З будет означать 3 байта массива intarray. Но это не даст нам результат, который мы хотим получить: intarray — это массив элементов типа int, и три байта в этом массиве — середина второго элемента, что не очень полезно для нас. Мы хотим получить четвертый элемент массива, а не его четвертый байт, что показано на рис. 10.5 (на этом рисунке int занимает 2 байта).

Рис. 10.5. Отсчет no int

Компилятору C++ достаточно получить размер данных в счетчике для вы- полнения вычислений с адресами данных. Ему известно, что intarray — массив

типа int. Поэтому, видя выражение intarray + 3, компилятор интерпретирует его как адрес четвертого элемента массива, а не четвертого байта.

Но нам необходимо значение четвертого элемента, а не его адрес. Для его по- лучения мы используем операцию разыменования (*). Поэтому результатом вы- ражения *(intarray + 3) будет значение четвертого элемента массива, то есть 52.

Теперь рассмотрим, почему при объявлении указателя мы должны указать тип переменной, на которую он будет указывать. Компилятору необходимо знать, на переменные какого типа указывает указатель, чтобы осуществлять правиль- ный доступ к элементам массива. Он умножает значение индекса на 2, в случае типа int, или на 8, в случае типа double.

Указатели-константы и указатели-переменные

Предположим, что мы хотим использовать операцию увеличения вместо прибав- ления шага j к имени intarray. Можем ли мы записать *(intarray++)?

Сделать так мы не можем, поскольку нельзя изменять константы. Выраже- ние intarray является адресом в памяти, где ваш массив будет храниться до окон- чания работы программы, поэтому intarray — это указатель константы. Мы не можем сказать intarray++, так же как не можем сказать 7++. (В многозадачных системах адресная переменная может менять свое значение в течение выполне- ния программы. Активная программа может обмениваться данными с диском, а затем снова загружать их уже в другие участки памяти. Однако этот процесс невидим в нашей программе.)

Мы не можем увеличивать адрес, но можем увеличить указатель, который содержит этот адрес. В примере PTRINC мы покажем, как это работает:

// ptrinc.cpp

// доступ к массиву через указатель

#include <iostream>

using namespace std;

int main()

{

int intarray[5] = { 31, 54, 77, 52, 93 }; // набор целых чисел

int* ptrint; // указатель на int

ptrint = intarray; // пусть он указывает на наш массив

for(int j = 0; j < 5; j++) // для каждого элемента массива

cout << *(ptrint++) << endl; // напечатаем его значение

return 0;

}

Здесь мы определили указатель на int — ptrint — и затем присвоили ему зна- чение адреса массива intarray. Теперь мы можем получить доступ к элементам массива, используя выражение

*(ptrint++)

Переменная ptrint имеет тот же адрес, что и intarray, поэтому доступ к перво- му элементу массива intarray[0], значением которого является 31, мы можем осу- ществлять, как и раньше. Но так как переменная ptrint не является константой, то мы можем ее увеличивать. После увеличения она уже будет показывать на второй элемент массива intarray[1]. Значение этого элемента массива мы можем получить, используя выражение *(ptrint++). Продолжая увеличивать ptrint, мы можем получить доступ к каждому из элементов массива по очереди. Результат работы программы PTRINC будет тем же, что и программы PTRNOTE.

Указатели и функции

В главе 5 мы упомянули, что передача аргументов функции может быть произве- дена тремя путями: по значению, по ссылке и по указателю. Если функция пред- назначена для изменения переменной в вызываемой программе, то эта перемен- ная не может быть передана по значению, так как функция получает только копию переменной. Однако в этой ситуации мы можем использовать передачу переменной по ссылке и по указателю.

Передача простой переменной

Сначала мы рассмотрим передачу аргумента по ссылке, а затем сравним ее с пе- редачей по указателю. В программе PASSREF рассмотрена передача по ссылке.

// passref.cpp

// передача аргумента по ссылке

#include <iostream>

using namespace std;

int main()

{

void centimize(double &); // прототип функции

double var = 10.0; // значение переменной var равно 10 (дюймов)

cout << "var = " << var << "дюймов" << endl;

centimize(var); // переведем дюймы в сантиметры

cout << "var = " << var << "сантиметров" << endl;

return 0;

}

///////////////////////////////////////////////////////////

void centimize(double & v)

{

v *= 2.54; // v — это то же самое, что и var

}

В этом примере мы хотим преобразовать значение переменной var функции main() из дюймов в сантиметры. Мы передаем переменную по ссылке в функ- цию centimize(). (Помним, что &, следующий за типом double в прототипе этой функции, означает, что аргумент передается по ссылке.) Функция centimize()

умножает первоначальное значение переменной на 2.54. Обратим внимание, как функция ссылается на переменную. Она просто использует имя аргумента v; v и var — это различные имена одного и того же.

После преобразования var в сантиметры функция main() выведет полученный результат. Итог работы программы PASSREF:

var = 10 дюймов var = 25.4 сантиметров

В следующем примере PASSPTR мы рассмотрим, как в аналогичной ситуации используются указатели.

// passptr.cpp

// передача аргумента по указателю

#include <iostream>

using namespace std;

int main()

{

void centimize(double *); // прототип функции

double var = 10.0; // значение переменной var равно 10 (дюймов)

cout << "var = " << var << "дюймов" << endl;

centimize(&var); // переведем дюймы в сантиметры

cout << "var = " << var << "сантиметров" << endl;

return 0;

}

///////////////////////////////////////////////////////////

void centimize(double * ptrd)

{

*ptrd *= 2.54; // *ptrd — это то же самое, что и var

}

Результаты работы программ PASSPTR и PASSREF одинаковы. Функция centimize() объявлена использующей параметр как указатель на double:

void centimize(double *); // аргумент - указатель на double

Когда функция main() вызывает функцию centimize(), она передает в качестве аргумента адрес переменной:

centimize(&var);

Помним, что это не сама переменная, как это происходит при передаче по ссылке, а ее адрес.

Так как функция centimize() получает адрес, то она может использовать опе- рацию разыменования *ptrd для доступа к значению, расположенному по этому адресу:

*ptrd *= 2.54; // умножаем содержимое переменной по адресу ptrd на 2.54

Это то же самое, что *ptrd = *ptrd * 2.54;