Глава 13 (продолжение 2)
Вверх
После того как вы немного поупражнялись с указателями (вся предыдущая программа
Как эта программа работает
В программе создается связанный список. Каждый элемент этого списка представляет собой структуру, состоящую из координат точки, а также из указателя, который ссылается на
следующий элемент этого списка. Указатель последнего элемента списка имеет значение,
равное нулю. Указатели с таким значениями называются пустыми (или нулевыми).
Первый будет ссылаться на первый элемент списка; таким образом, вы всегда будете знать,
Второй указатель будет каждый раз ссылаться на новый элемент, который добавляется к
Третий указатель будет содержать ссылку на последний элемент списка:
Он также потребуется при добавлении в список нового элемента, так как последний элемент
Если это будет первый элемент списка, указатель poPoints должен ссылаться именно на
Оператор if проверяет, не является ли указатель poPoints нулевым. Если он нулевой, выражение (!poPoints) истинно и инструкция poPoints = poNew выполняется. Если же указатель poPoints уже ссылается на какой-то элемент, новый элемент добавляется в конец списка:
Смысл выполняемых этим кодом действий следующий: определяется элемент, на который
После того как пользователь укажет координаты всех точек, программа отобразит их на
После того как все линии будут нарисованы, необходимо очистить память от ненужной
Код программы
Ниже приведен код всей программы, работа которой обсуждалась в предыдущем разделе.
#using <mscoriib.dll>
using manespace System;
//Создание структуры, представляющей элемент связанного списка
//Получение координат новой точки
//Присвоение указателю нулевого значения
//Отображение точек на экране
//С этой точки начинается выполнение программы
//Структуры для отображения графики
//Обновление значения указателя poLast
if (!pszMore->Equals(S"y"))
while
(poLast)
//Освобождение памяти, которая использовалась для
Рис 13.5 Результат работы программы Draw
Код программы в файле Draw.
Использование ссылочных аргументов
Как уже отмечалось, изменять значения глобальных переменных в процессе выполнения функций - плохая практика. Намного разумнее использовать функции для возвращения значений и затем уже как-то
изменять эти значения вне функций. (
В будущем это поможет вам избежать многих ошибок и без труда
понимать смысл выполняемых функциями действий.)
Если же функция должна изменить значения нескольких элементов или элементов, принадлежащих какой-то структуре, передайте ей в качестве параметра указатель, ссылающийся на нужный элемент. В процессе своего выполнения функция может разыменовать указатель и изменить значение элемента. Это намного лучше, чем изменять значения глобальных переменных
(или переменных, область видимости которых распространяется за пределы этой функции), поскольку при передаче функциям значений указателей вы точно определяете, какие элементы будут изменены. Таким образом, не вникая в подробности выполнения функции, вы или другие программисты сможете сразу же определить, значения каких элементов могут быть изменены этой функцией.
Другой возможностью является использование ссылочных аргументов. При этом функции передаются указатели на аргументы (т.е. их адреса в памяти), но не сами аргументы.
Теперь все изменения, выполненные функцией в отношении переменной Number, будут иметь постоянный характер, и это будет выглядеть так, как будто значение переменной изменяется за пределами функции.
Вверх
Как известно, большие полномочия сопровождаются большей ответственностью, что
Совет:
Запутавшись с указателями, вы можете столкнуться с серьезными проблемами.
РИС. 13.4. Если Windows столкнется с
Неизменяемые ссылочные аргументы
Если вы передаете функциям в качестве аргументов большие структуры данных, это может занять слишком много времени, поскольку при этом компьютер должен создать новые копии всех элементов, входящих в эти структуры. Чтобы ускорить этот процесс, передавайте структуру как ссылочный аргумент. В
этом случае передается только информация об адресе структуры, которая сама по себе занимает очень мало места.
Но теперь функция получает возможность изменять данные, хранящиеся в структуре. Предположим, вам
Вот пример объявления неизменяемого ссылочного аргумента:
Ниже перечислен ряд причин, по которым работа с указателями может обернуться маленькой трагедией.
Старайтесь не делать таких глупостей, иначе проблем не оберешься.
Вверх
В программе Draw было написано немало кодов для того, чтобы очистить память, которая
Чтобы активизировать для объекта эту возможность, при его объявлении наберите в начале строки __gc, например:
Программа с использованием автоматической уборки мусора находится в файле Draw3
Она отличается от предыдущей программы только объявлением класса PointList
(использовано ключевое слово __gc) и исчезла необходимость в "ручной" уборке
мусора (удален блок кода с циклом, в котором производилось освобождение памяти и обнуление указателей).
//Free up the memory
//Now advance
Универсальные указатели
На первый взгляд может показаться, что использовать указатели, которые могут ссылаться на
Вот как можно создать такой указатель (но потом не говорите, что вас не предупреждали):
Указатели void опасны (и одновременно удобны) именно тем, что для них не определен тип
Предположим, что в рассмотренном ранее примере вы используете для создания связанного
Вверх
Если вы занимались программированием раньше, то должны знать, что термином
Этим кодом создается строка, содержащая текст "романтическое путешествие". Указатель
Указания для указателей
Здесь вы найдете несколько советов и напоминаний, которые помогут вам избежать проблем при работе с указателями,
Вам наверняка будет интересно, какие библиотечные функции предназначены для работы
В C++ есть также такой объект, как текстовый класс ANSI, который может помочь вам в
Выделяя память для строк, не забывайте о том, что они должны заканчиваться нулевым
Поскольку можно набирать код char* для создания ссылок на текстовые данные, вы
Если необходимо, например, отобразить все символы строки поочередно (по одному за
Вы уже знаете об указателях достаточно много. Это просто переменные, значениями которых Также
указатели используются для создания связанных списков (они нужны тогда, когда заранее не
Назад |
Начало урока |
Вверх |
Вперед
Связные списки и графика
была построена с их использованием), перейдем к рассмотрению более типичных способов
их применения. В этом разделе внимание будет уделено вопросу создания с помощью указателей связанных списков. Рассмотрено это будет на таком примере: пользователь набирает
координаты произвольного количества точек, для хранения которых используется связанный
список. Когда он закончит, эти точки соединяются между собой с помощью графических команд среды .NET.
Итак, элемент связанного списка должен содержать в себе значения координат точки и
указатель на следующий элемент. Ниже показан код, которым объявляется структура
PointList, содержащая в себе два значения типа integer и указатель на такую же структуру.
class PointList
Программа должна содержать в себе коды, необходимые для добавления в список новых
{
int nX;
int nY;
PointList *poNext;
элементов, и коды, позволяющие находить нужную информацию в этом списке. Для этого
нужны три дополнительных указателя.
где список начинается. Это будет как бы точкой отсчета, дающей ключ ко всем остальным
элементам списка:
PointList *poPoints = 0;
списку:
PointList *poNew;
PointList *poLast;
списка (теперь уже бывший последний) должен ссылаться на вновь добавляемый элемент.
Теперь рассмотрим, как связанный список будет использоваться для хранения вводимой
пользователем информации. Вначале выделяется память для нового элемента списка:
poNew = new PointList ;
него. (Помните, что poPoints всегда указывает только на первый элемент?)
if(!poPoints)
{
//элемент, пускай он ссылается именно на него
poPoints = poNew;
poLast->poNext = poNew;
ссылается указатель poLast, и составляющей poNext этого элемента присваивается адрес
создаваемого элемента, на который в данный момент ссылается указатель poNew.
И наконец, обновляется значение указателя poLast так, чтобы теперь он ссылался на
только что созданный объект, и вызывается функция NewPoint, которая принимает значения координат новой точки:
poLast = poNew;
NewPoint(poNew) ;
экране и соединит между собой прямыми линиями. Этот процесс начнется с отображения
первой точки, после чего будет проведена линия к следующей точке, затем к следующей и
т.д. Процесс рисования завершится, как только программа дойдет до нулевого указателя, поскольку нулевой указатель принадлежит последнему элементу списка.
poLast = poPoints;
while (poLast)
{
DrawPoint(poGraphics, poLast);
//Переход к следующему элементу
poLast = poLast->poNext;
более информации (о том, для чего это нужно делать, речь идет ниже в этой главе):
PoLast = poPoints;
while (poLast)
PointList *poNext;
poNext = poLast->poNext;
//Освобождение памяти
delete poLast;
//Переход к следующему элементу
poLast = poNext;
//Draw
//Применение связанного списка для сохранения
//координат произвольного количества точек
//и их отображение с использованием GDI +
#include "stdafx. h"
#using <System.dll>
#using <Systern.Windows.Forms.dll>
#using <System.Drawing.dll>
using manespace System::Drawing;
using manespace System::Windows::Forms;
class PointList
{
int nX;
int nY;
PointList *poNext;
void NewPoint(PointList *poNew)
{
Console::WriteLine(S"Введите координату X")
poNew->nX = Int32::Parse(Console::ReadLine(
Console::WriteLine(S"Введите координату Y")
poNew->nY = Int32::Parse(Console::ReadLine
())
poNew->poNext = 0;
void DrawPoint(Graphics *poGraphics, PointList *poPoint)
{
if (poPoint->poNext)
{
Pen *poPen = Pen(Color::Red);
//Рисование линии от текущей точки к следующей
poGraphics->DrawLine(poPen, poPoint->nX, poPoint->nY,
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{
PointList *poPoints = 0;
//Указатель для последнего созданного элемента
PointList *poLast;
//Указатель для нового элемента списка
PointList *poNew;
//Указатель для принятия ответа от пользователя
String *pszMore;
Form *poForm = new Form();
Graphics *poGraphics = poForm->CreateGraphics();
while (!fFinished)
{
poNew = new PointList;
if !poPoints)
{
//адреса указателю poPoints
poPoints = poNew;
else
{
poLast->poNext = poNew;
poLast = poNew;
//Принятие от пользователя координат новой точки
NewPoint(poNew);
//Хочет ли пользователь ввести координаты для
//следующей точки?
Console::WriteLine("Нажмите у для определения
следующей точки");
pszMore = Console::ReadLine();
{
//Отображение окна для рисования
poForm->Show();
//Отображение точек на экране
PoLast = poPoints;
{
DrawPoint,(poGraphics, poLast);
//Переход к следующему элементу
poLast = poLast->poNext;
//отображения графики на экране
poGraphics->Dispose();
//Освобождение памяти, занятой элементами списка
PoLast = poPoints;
while (poLast)
{
poNext = poLast->poNext;
//Освобождение памяти
delete poLast;
//Переход к следующему элементу
poLast = poNext;
//Ожидание, пока пользователь не закроет окно формы
Application::Run(poForm);
Поскольку в распоряжение функции попадают адреса элементов, она может изменять их значения непосредственно.
Использовать ссылочные аргументы удобнее, чем передавать в качестве аргументов значения указателей, поскольку в этом случае не нужно разыменовывать указатели внутри функции. Чтобы определить, что функция будет использовать ссылочный аргумент, наберите в списке аргументов перед названием этого аргумента символ &:
int Factorial (int &Number)
{
}
Вопросы безопасности
справедливо и в отношении указателей. Язык C++ обладает большими возможностями, и с
помощью указателей программист получает прямой доступ к памяти компьютера. Это означает,
что использовать их нужно очень осторожно. В этом разделе затрагиваются вопросы, о
которых нужно помнить при работе с указателями, а также описываются возможности среды
.NET, которые помогут вам избежать ошибок.
Освобождение памяти
Если вы видите, что в выделенной ранее памяти больше нет необходимости, сразу же
освобождайте ее. Таким образом, ваши программы не будут занимать больше памяти, чем им
требуется.
Для освобождения памяти используется команда delete , действие которой в точности
противоположно действию оператора new:
//Выделение памяти для элемента PointList
Обратите внимание, что при обнулении указателя не набирается звездочка (*), поскольку
PointList *pnPoint = new PointList;
//Освобождение только что выделенной памяти
delete pnPoint;
//Обнуление указателя
pnPoint = 0;
обнуляется значение самого указателя, а не участок памяти, на который он ссылается.
тех данных, на которые ссылается указатель, в памяти больше нет. Таким образом, указатель
по-прежнему ссылается на какой-то адрес, но этот адрес уже пуст.
Если вы его сейчас разыменуете, то получите совершенно бессмысленное значение.
Поэтому каждый раз, удаляя какой-то элемент, обнуляйте указатель, который
на него ссылался. Благодаря этому вы будете точно знать, что все указатели вашей программы
ссылаются на реальные данные.
Общее нарушение защиты
Такого рода ошибки обозначают термином GPF (General Protection Fault— общее
нарушение защиты). Иногда это означает, что придется закрывать некоторые
приложения или даже перезагружать компьютер. Windows отслеживает возникновение ошибок GPF
и, как только их находит, останавливает выполнение программы. На рис. 13.4 показано, как
может выглядеть диалоговое окно с сообщением о нарушении общей защиты.
GPF, она закроет вашу программу и отобразит
окно с предупреждением
этого не нужно. Чтобы решить этот вопрос, можно обозначить структуру как неизменяемый ссылочный
аргумент. При этом вы как бы сообщаете компилятору: "Я делаю это только для ускорения работы программы. Не изменяй данные, доступ к которым получает функция".
int Drawlt(const PointList &MyPoint)
Генеральная уборка
использовалась в процессе ее выполнения. Если хотите, можете воспользоваться возможностью
среды .NET, называемой сборкой мусора (garbage collection), которая автоматически выполнит
всю "грязную" работу. Если при объявлении объекта вы укажете, что память после него нужно
освобождать автоматически, среда .NET будет отслеживать его в процессе выполнения
программы. Как только .NET увидит, что объект больше не используется, она освободит
занимаемую им память. (Поскольку сборка мусора— дело не только грязное, но и отнимающее
много времени, .NET занимается этим тогда, когда для этого появляется удобный момент.)
__gc class PointList
{
int nX;
int nY;
PointList *poNext;
Теперь этот код для очистки памяти не нужен:
//Теперь очистим память
poLast = poPoints;
while (poLast)
{
poNext = poLast->poNext;
delete poLast;
poLast = poNext;
все, что угодно, - вещь удобная. При объявлении таких указателей вместо слова,
обозначающего тип данных, набирается слово void (что означает "пустой тип данных"), но хотя
эти указатели очень удобны (поскольку вы можете использовать их для получения доступа к
значениям любых типов), они также и очень опасны (поскольку компилятор не может отследить
правильность их использования).
//Пусть pOff ссылается на что угодно
void *pOff;
данных и поэтому возникает реальная возможность неправильного использования памяти.
списка указатели void и случайным образом вместо структур с информацией о координатах точек
добавляете в список числа, строки, записи о сотрудниках и еще неизвестно что. Компилятор
никак не сможет определить, что здесь что-то не так. Но пользователи вашей программы
непременно это заметят.
Подведем итог: используйте указатели void только тогда, когда без этого никак не обойтись.
Кое что о строках
строки обозначается текстовая информация, точнее, наборы следующих друг за
другом символов. В .NET есть встроенный класс String, который предназначен
для работы со строками. Если же вы пишете неуправляемый код, вам придется
делать все самому, используя указатели типа char. Но не расстраивайтесь: есть
множество библиотечных функций, которые специально созданы для обработки
текстовой информации.
Строки хранятся в памяти компьютера как массивы расположенных друг за другом символов,
заканчивающиеся нулевым значением. (Из-за этого нулевого байта в конце строки они
называются также строками с завершающим нулем.) Доступ к текстовой информации можно
получить с помощью указателей, которые ссылаются на первый символ строки. Чтобы создать
строку, наберите такой код:
char *szMyString = "романтическое путешествие";
szMyString ссылается на эту строку. Чтобы отобразить эту строку на экране, наберите
cout << szMyString;
Если вы как-то изменяете значения указателей (добавляете, вычитаете и т.п.), еы изменяете только адреса, но не то, что находится по этим адресам. Обычно, это не является вашей целью: вы хотите обрабатывать именно те данные, на которые ссылаются указатели. Для этого указатели нужно разыменовывать.
со строками и что они могут делать. Их будет легче найти, зная о том, что названия почти
всех из них начинаются с букв str. Например, функция strlen возвращает количество
символов, из которых состоит строка.
работе со строками.
значением, которое занимает один байт памяти. Убедитесь, что вы включили этот байт в общее
количество байтов, выделяемых для размещения текстового значения. В противном случае
возникнут проблемы с использованием памяти.
можете в полной мере использовать все возможности указателей для обработки текстовой
информации. Напомним, что в C++ строки должны заканчиваться нулевым байтом. Именно
по нему библиотечные функции определяют, где находится коней строки.
раз), можно использовать значение указателя этой строки, увеличивая его каждый раз на единицу. Если указатель ссылается на какой-то символ строки, добавив к его значению число 1,
вы получите адрес следующего символа строки. Например:
//Строка
char *szMyString = "hello world";
//Еще един указатель
char *szCharPtr;
//Изменение первого символа строки
szCharPtr = szMyString;
*szCharPtr = "j";
//Теперь переходим к следующему символу
//Для этого значение указателя увеличиваем на 1
szCharPtr++;
//Изменяем второй символ
*szCharPtr = "о";
//Теперь строка выглядит как "jollo world"
//Отображение строки на экране
cout << szMyString;
Итог
являются адреса различных данных, сохраненных в памяти компьютера. Указатели используются
для получения доступа к памяти, которая выделяется динамически (это нужно в
тех случаях,
когда вы заранее не знаете, какой объем памяти должен быть выделен).
известно, сколько элементов будет использовано). Есть еще много задач, которые решаются
именно с использованием указателей. Вы будете встречаться с ними каждый раз, когда будут
использоваться такие возможности .NET, как команды работы с графикой или методы обработки
текстовой информации.
Но, даже если вам кажется, что вы уже стали экспертом по указателям, не обольщайтесь:
вам предстоит еще многому научиться.
Содержание