Назад | Начало урока | Вперед
Содержание

Глава 13

Указатели

В этой главе вы узнаете...

В этой главе вы узнаете о причинах, по которым имеет смысл использовать указатели, и узнаете как их использовать. Так же научитесь рисованию графических элементов в среде .NET. Создадите связный список и используете его для рисования множества линий. Научитесь правильно использовать память и освобождать ее. Узнаете что такое автоматическая сборка мусора. И узнаете кое что новое о строках в неуправляемом коде.

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

Вверх

Почему указатели

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

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

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

Вверх

Указатели и переменные

Чтобы понять, в чем заключается преимущество указателей, нужно разобраться в том, как
работают переменные. Все данные, которые обрабатывает компьютер, хранятся в его памяти.
Когда переменной присваивается какое-то значение, оно заполняет собой некоторую часть
памяти. Когда значение переменной должно быть использовано, оно считывается из памяти.
Таким образом, каждая переменная — это всего лишь имя для определенного участка памяти.

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

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

На рис. 13.1 показаны три переменные: foo, bar и dribble , адреса в памяти, на которые они указывают, и принятые ими значения. Переменная bar, например, ссылается на адрес 4, в котором сохранено значение 17. Также на рисунке показаны два указателя: baz и goo. Первый содержит значение 4, которое представляет собой указатель на адрес 4. Таким образом, указатель baz может быть использован для получения значения, которое хранится в переменной bar. Если поменять значение baz с 4 на 8, его можно будет использовать для получения значения, которое хранится в переменной dribble .


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

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

Вверх

Что вы! Указатели — это очень сложно

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

Информация и ее адрес,

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

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

Например, указатель b a z содержит значение 4 (см. рис. 13.1). Если вы разыменуете его,
то получите значение 17, поскольку именно оно хранится в адресе 4.

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

Вверх

Безымянные данные

Вторая причина, вызывающая трудности при работе с указателями, заключается в том,
что они могут указывать на данные, которые никак не обозначаются (т.е. не имеют никаких
имен). На рис. 13.1 вы видели, как используются указатели для получения доступа к значениям различных переменных. Так, например, указатель baz ссылался на значение переменной
b a r . Следовательно, вы могли разыменовать указатель baz, чтобы получить значение переменной bar. В данном случае указатель ссылается на участок памяти, который обозначен определенным именем (этим именем является имя переменной, т.е. b a r ) .

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

Не засоряйте свой компьютер

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

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

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

Windows пытается отслеживать корректность выполняемых программами действий. Она обычно может отличить "хорошие" указатели от "плохих" и останавливает выполнение программы до того, как она успеет сделать какую-нибудь глупость. Более подробно эти вопросы рассматриваются ниже в главе.

Испугались? То-то. Будьте аккуратны при использовании указателей.

Вверх

Связанный список — размер не ограничен

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

При этом применяются некоторые маленькие хитрости. В момент создания программы
точно не известно, сколько линий захочет нарисовать пользователь. Но сохранить нужно будет информацию обо всех линиях, сколько бы их ни было. Можно, конечно, объявить целое
множество указателей (Линия!, Линия2, ЛинияЗ и т.д.) и затем использовать их один за
другим. Это не самая удачная идея, поскольку количество объявленных переменных априори
должно быть больще количества линий, которые может нарисовать пользователь (что, если
он захочет нарисовать, например, 20 000 линий). А кроме того, придется использовать гигантский оператор switch, чтобы определить, какой указатель должен быть использован
следующим.

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

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

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

Подсказка

Рис. 13.2. Указатели могут указывать на большие и сложные структуры данных. Элементы этого связанного списка состоят из двух указателей; один указывает на информацию
о координатах линии, а второй - на следующий элемент списка

Вверх

Использование указателей в С++

Указатель, ссылающийся на значения определенного типа данных, создается точно так
же, как и переменная того же типа, за исключением того, что перед именем указателя нужно
набрать звездочку (*).
Например, чтобы создать указатель, ссылающийся на значения типа integer, наберите такой код:

int *pnFoo;

Совет:

Если вы хотите придерживаться соглашений о присвоении имен переменным и
указателям, принятым в данной книге, начинайте имена указателей с буквы р
(pointer— указатель), затем набирайте префикс, который соответствует тому типу данных, на значения которого указатель будет ссылаться, и далее указывайте
само имя. Например, указатель, ссылающийся на значения типа integer, должен
начинаться с префикса рn, на значения типа double — с префикса pdbl и т.д.

Объявлять тип данных значений, на которые ссылается указатель, необходимо. Этим
обеспечивается надежность и безопасность создаваемой программы, поскольку в этом случае
компилятор может проверить, не будет ли указатель по ошибке ссылаться на данные не того
типа. Ведь указатель — это всего лишь адрес в памяти компьютера. Если вы определяете для
него тип данных (скажем, integer), а затем по какой-то причине пытаетесь, например, в память, на которую ссылается этот указатель, записать текстовое значение, компилятор выдаст
значение об ошибке. (Это очень хорошая услуга, поскольку именно такие ошибки могут повлечь за собой крайне нежелательные последствия.)

Если вы набрали int *pnFoo, то это не значит, что название указателя —
*pnFoo. На самом деле он называется pnFoo, а звездочка не является частью его
имени и лишь сообщает компилятору, что это указатель.

Вверх

Дайте указателю адрес

Если вы только что объявили указатель, он еще ни на что не ссылается. Пока это указатель в никуда. И в таком состоянии его лучше не оставлять. Прежде чем использовать указатель, ему нужно присвоить значение (это значение будет адресом в памяти компьютера, и таким образом указатель начнет на что-нибудь ссылаться).

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

Чтобы получить адрес переменной, наберите амперсант (&) и ее имя. Например, можете
набрать такой код:

//Создание указателя на значение типа integer
int *pnPosition;
//Создание переменной типа integer
i n t nXPosition = 3;
//Присвоение указателю адреса переменной nXPosition
pnPosition = &nXPosition;

Здесь создается указатель pnPosition, который может ссылаться на значения типа
integer, и ему присваивается адрес переменной nXPosition. Если вы разыменуете этот указатель, то получите значение переменной nXPosition.

Указатель может указывать на значения только того типа, которые для него объявлены. Так, если для указателя был объявлен тип integer, он может ссылаться только на переменные типа integer.

Вверх

Как получить значение, на которое ссылается указатель

Разыменовывание указателя (т.е. получение значения, на которое он ссылается) выполняется очень просто: нужно только перед его именем набрать звездочку (*). Например, чтобы
получить значение, на которое ссылается указатель pnPosition, наберите такой код:

//Отображение значения на экране
Console::WriteLine((*pnPosition).ToString());

В результате выполнения этого кода на экране отображается значение, полученное при
разыменовывании указателя pnPosition. Этот указатель содержит адрес переменной
nXPosition, ее значение равно числу 3, которое и будет отображено на экране.

Вверх

Пример программы, использующей указатели

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

pnNumber = &nNumber;

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

nNumber.ToString()

Далее, это же значение отображается еще раз, но теперь доступ к нему осуществляется посредством разыменовывания указателя pnNumber:

(*pnNumber).ToString()


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

//Point
//Объявление к разыменовывание указателя
#include "stdafx.h"
#using
using manespace System;
//Ожидание, пока пользователь не остановит
//выполнение программы
void Har.gOut ()
{
Console::WriteLine(3"Нажмите клавишу Enter, чтобы
остановить выполнение программы");
Conso]e::ReadLine();
//С этой точки начинается выполнение программы
#ifdef _UNICODE
int wmain (void)
#else
int mair. (void)
#endif
{
int *pnNumber;
int nNumber;
//Присвоение указателю адреса переменной nNumber
pnNumber = SnNumber;
//Получение числа от пользователя
Console::WriteLine(S"Укажите число");
nNumber = Int32::Parse(Console::ReadLine());
//Отображение полученного числа
Console::WriteLine(S"Введенное число { 0}", nNumber.ToString()); //Отображение числа с помощью указателя
Console::WriteLine(5"Введенное число {0}", (*pnNumber).ToString() HangOut();
return 0;
}
}

Вверх

Изменение значения, на которое ссылается указатель

Можно не только просматривать значения, на которые ссылается указатель, но и изменять
их. Другими словами, можно не только считывать данные из памяти, но и записывать в память новые данные.
Для этого, как и прежде, нужно лишь использовать звездочку (*). Допустим, например,
что указатель pnNumber ссылается на значение типа integer (как в предыдущем примере).
Чтобы изменить это значение, наберите такой код:
*pnNumber = 5;

Вверх

Изменение значений в структурах данных

Если указатель ссылается на структуру, можно изменить отдельный элемент этой структуры.
В приведенном ниже примере MyStruct — структура, a poFoo — указатель, который на эту
структуру ссылается. Это значит, что для получения доступа к данным, которые в этой структуре хранятся, можно использовать код *poFoo. Например, можно набрать код наподобие
(*poFoo).value = 7.6;

Продемонстрируем это на таком примере:

class MyStruct
{

public :
int nData;
double dblValue;
}
//Создание указателя, ссылающегося на такие структуры
MyStruct *poFoo;
//Создание самой структуры
MyStruct oRecordl;
//Присвоение указателю адреса этой структуры
poFoo = &oRecordl;
//Присвоение значения элементу структуры
CpoFoo).dblValue = 7.6;

Использование стрелки
Совет:

Поскольку набирать код (*указатель) .элемент не очень удобно, C++ пред-
лагает упрощенный вариант:
//Изменение значения элемента структуры
poFoo -> dblValue = 7.6;

Код указатель -> элемент можно встретить почти во всех программах C++, исполь-
зующих указатели.

Вверх

Динамическое выделение памяти

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

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

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

//Создание указателя на значение типа integer
int *pnPosition;
//Выделение памяти для значения типа integer и
//присвоение этого адреса указателю pnPosition
pnPosition = new int ;

Если нужно выделить память для структуры PointList, наберите

//Создание указателя на структуру PointList
PointList *poPoint
//Выделение памяти для структуры PointList и
//присвоение этого адреса указателю poPoint
poPoint = new PointList;

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

//Забывчивая программа
//Создание указателя типа integer
int *pnPosition;
//Выделение памяти для значения типа integer
pnPosition = new int ;
//Сохранение значения по этому адресу
*pnPosition = 3;
//Выделение памяти для нового значения
pnPosition = new int ;

Последней строкой этого кода выделяется память для нового значения типа integer. Адрес этого нового участка памяти будет сохранен в указателе pnPosition . Но что случится со значением 3, сохраненным по адресу, на который до этого ссылался указатель pnPosition ? Это значение попрежнему будет храниться в памяти, но, поскольку его адрес потерян (указатель уже ссылается на совсем другой участок памяти), получить к нему доступ не будет никакой возможности.

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

Перед тем как использовать оператор new, убедитесь, что данные, адрес которых пока
еще хранится в указателе, более не нужны. Кроме того, если вы хотите создать действительно
качественную программу, перед использованием оператора new освободите память от тех
данных, которые вам больше не нужны, с помощью оператора delete (более подробно об
операторе delete речь идет ниже в главе).


Назад | Начало урока | Вверх | Вперед
Содержание