Пора познакомиться с указателями. Это очень удобный элемент языка, который мы будем часто использовать. В этой главе будут рассматриваться указатели на структуры, которые располагаются в динамической памяти.
Но прежде чем что-то объяснять, рассмотрим, зачем нужны указатели. Давайте вспомним про процедуры, а именно, как происходит их вызов. Допустим, у вас есть процедура с именем муРгос, у которой есть два параметра: число и строка. Как происходит вызов такой процедуры и как ей передаются эти параметры? Очень просто. Сначала параметры принимаются в стек (напомню, что стек — это область памяти для хранения временных или локальных переменных). Первым заносится первый параметр, затем второй и после этого вызывается процедура. Прежде чем процедура начнет свое выполнение, она извлекает эти параметры из стека в обратном порядке.
Теперь вспомним о наших параметрах. Первый — это число, которое будет занимать 2 байта. Когда происходит его запись в стек, оно займет там свои положенные 2 байта. Второй параметр — строка. Каждый символ строки — это отдельный байт. Допустим, что строка состоит из 10 символов. Это значит, что для передачи такой строки в процедуру в стеке понадобится 10 байт плюс 1 байт для указания конца строки или ее размера (это зависит от типа строки). Всего для передачи в процедуру понадобится в стеке как минимум 12 байт. Это не так уж и много, поэтому такое можно себе позволить.
А теперь представьте, что строка, которую надо передать в процедуру, состоит из 1000 символов. Вот тут нам понадобится в стеке уже около килобайта. При нынешних размерах памяти на это никто не обращает внимания, но программисты забывают про то, что такая строка сначала копируется в память стека, а потом извлекается из него. Такое копирование большого размера памяти отнимает достаточно много времени, и ваша программа тратит лишнее время на бессмысленное клонирование в памяти большой строки.
А если нам нужно передать в процедуру фотографию размером в 3 мегабайта? Что ее тоже копировать в стек? Несколько фотографий высокого качества — и стек закончится.
Выход из сложившейся ситуации достаточно прост. Можно не передавать строку, а только передать указатель на область памяти, где находится эта строка. Любой указатель занимает всего 4 байта, а это уже существенная экономия. Мы просто даем нашей процедуре понять, где найти строку.
Указатель в Delphi объявляется как Pointer. Например, давайте объявим переменную р типа указатель:
var р:Pointer
Для того чтобы получить адрес переменной или объекта, необходимо перед его именем поставить знак Например, у вас есть строка str, и чтобы присвоить ее адрес в указатель р, надо выполнить следующее: p:=@str. Теперь в указателе находится адрес строки. Если вы будете напрямую читать указатель, то увидите адрес, а для того чтобы увидеть содержащиеся по этому адресу данные, надо разыменовывать указатель. Для этого надо написать р^. Итак, мы пришли к следующему:
- р: = @str — получить адрес строки;
- р — указатель на строку;
- р^ — данные, содержащиеся по адресу, указанному в р.
Давайте создадим маленькое приложение, которое будет работать с указателями. Для этого на форму надо поместить кнопку с заголовком "Работа со ссылками" и строку ввода.
Для события — нажатие кнопки Работа со ссылками — напишем код, представленный в листинге:
В этом примере в первой строке мы присваиваем указателю р ссылку на строку str. После этого меняем содержимое строки. В последней строке выводится содержащийся по адресу р текст. Для этого приходится явно указывать, что по адресу р находится именно строка string (р^). Вспомните приведение схожих типов. Таким же образом можно приводить строку string к pchar. Здесь указывается, что по адресу переменной р находится строка, а не какой-нибудь другой тип данных. Это необходимо, потому что данные, расположенные по определенному адресу, могут иметь совершенно любой тип. Как видите, "жесткое" указание типа похоже на преобразование типов, поэтому никаких проблем с этим не должно возникнуть.
Здесь надо отметить, что мы изменяем строку после присваивания адреса строки в переменную-указатель, и измененные данные все равно будут отражены в указателе. Это потому, что указатель всегда показывает на начало строки. Если мы ее изменим, указателю будет все равно, потому что новые данные будут расположены по тому же адресу, и р будет указывать на измененную строку.
Мы уже не раз использовали указатели, но не углублялись в их изучение. Каждая переменная типа объект — это тоже указатель на объект. Просто его использование стандартизовано, чтобы не смущать пользователей лишним типом адресации и разыменовыванием.
Любой переменной-указателю можно присвоить нулевое значение, только это не число 0, a nil — нулевой указатель, например, p:=nil (в принципе, nil это тот же О, просто используется для обнуления указателей). Когда вы присваиваете нулевое значение, то как бы уничтожаете ссылку. Точно так же, если переменной-объекту присвоить нулевое значение, вы его уничтожите. Никогда не обнуляйте переменные указатели, которые указывают на существующие объекты. Сначала уничтожьте объекты (освободите память), а потом можно указателю присвоить nil.
А зачем указателям присваивать нулевое значение? В принципе, на память это не влияет, но поможет с точки зрения безопасности. Дело в том, что после освобождения памяти, указатель содержит какое-то число (какой-то адрес), но данные по этому адресу уже не действительны. Если обратиться по ним, то может произойти ошибка. Чтобы было видно, что указатель недействительный, после освобождения обнуляйте указатель.
До Windows 2000 в программах Windows можно было использовать один интересный трюк— после освобождения памяти, ее еще можно было использовать в течение короткого промежутка времени (пока другая программа не выделит себе этот же участок). Но потом разработчики посчитали, что это небезопасно, и запретили использование памяти после ее освобождения.