
- •Р. Лафоре
- •Глава 1. Общие сведения 32
- •Глава 3. Циклы и ветвления 92
- •Глава 4. Структуры 142
- •Глава 5. Функции 168
- •Глава 6. Объекты и классы 217
- •Глава 7. Массивы и строки 261
- •Глава 8. Перегрузка операций 312
- •Глава 9. Наследование 361
- •Глава 10. Указатели 411
- •Глава 11. Виртуальные функции 476
- •Глава 12. Потоки и файлы 536
- •Глава 13. Многофайловые программы 596
- •Глава 14. Шаблоны и исключения 640
- •Глава 15. Стандартная библиотека шаблонов (stl) 681
- •Глава 16. Разработка объектно-ориентированного по 752
- •Глава 1 «Общие сведения» включает список тем, касающихся uml, с указа- нием их расположения в книге.
- •Глава 1
- •Глава 2
- •Глава 3
- •If внутри циклов
- •If и else во вложенных ветвлениях
- •Глава 4
- •Глава 5 Функции
- •Глава 6
- •Глава 7
- •123456. Россия
- •123456. Россия
- •Глава 8
- •Глава 9
- •Глава 10 Указатели
- •Main() передает адрес переменной var в ptrd в centimize()
- •Centimize() использует этот адрес для доступа к var
- •Глава 11
- •Глава 12
- •Тип:менеджер Фамилия: Александров Номер:1111
- •Тип:Ученый Фамилия: Лебедев Номер:2222
- •Тип:рабочий Фамилия:Шевелев Номер:3333
- •Глава 13
- •Глава 14
- •Много объектов разных классов в памяти Рис. 14.2. Шаблон класса
- •Алгоритмы используют итераторы для работы с объектами контейнеров. Рис. 15.1. Контейнеры, алгоритмы и итераторы
- •Глава 16
- •Глава 1
- •Глава 2
- •Глава 3
- •Глава 4
- •Глава 5
- •Глава 6
- •Глава 7
- •Глава 8
- •Глава 9
- •Глава 10 Ответы на вопросы
- •Глава 11
- •Глава 12 Ответы на вопросы
- •Глава 13 Ответы на вопросы
- •Глава 14 Ответы на вопросы
- •Глава 15 Ответы на вопросы
- •Глава 16
Адреса
и указатели
Операция
получения адреса &
Указатели
и массивы
Указатели
и функции
Указатели
на строки
Управление
памятью: операции new
и
delete
Указатели
на объекты
Связный
список
Указатели
на указатели
Пример
разбора строки
Симулятор:
лошадиные скачки
UML-диаграммы
Отладка
указателей
Для программистов
на C++
указатели являются настоящим кошмаром,
кото-
рый заставит растеряться кого
угодно. Но бояться не стоит. В этой главе
мы по-
пытаемся прояснить тему
указателей и рассмотреть их применение
в програм-
мировании.
Для чего нужны указатели? Вот наиболее
частые примеры их использования:
доступ к элементам массива;
передача аргументов в функцию, от
которой требуется изменить эти
аргу-
менты;
передача в функции массивов и строковых
переменных;
выделение памяти;
создание сложных структур, таких, как
связный список.
Указатели — это
важная возможность языка C++,
поскольку многие другие
языки
программирования, такие, как Visual
Basic
или Java,
вовсе не имеют
указа-Глава 10 Указатели
телей. (В
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;