Раздел 7. Указатели и ссылки
Указатели – это переменные специального типа, значениями которых является адреса различных объектов программы. Если мы используем имя того или иного объекта для извлечения его значения или для изменения его значения, то принято говорить о непосредственном (прямом) доступе к объекту. В том случае, когда адрес объекта помещен в указатель, то речь идет о косвенном доступе к объекту, на который "смотрит" указатель.
Идеи такой косвенной адресации зародились еще в архитектуре ЭВМ первого поколения, когда адрес нужной ячейки памяти помещался в специальный регистр (например, РА – регистр адреса в ЭВМ типа М-20). Доступ по содержимому регистра предоставлял программистам более широкие возможности за счет наличия машинных команд изменения содержимого такого регистра (автоматическое увеличение-инкрементирование или уменьшение-декрементирование на 1) и использование РА в командах организации циклов. Наиболее полное воплощение идеи косвенной адресации нашли в проекте адресного языка, разработанного профессором Е.Л. Ющенко (Институт кибернетики АН УССР, Киев).
Объявление указателей
В языках C, C++ различают три категории указателей. Первая категория указателей предназначена для хранения адресов данных определенного типа (по терминологии языка Паскаль – типизированные указатели). При их объявлении указывается тип данных, на которые эти указатели могут "смотреть". Ко второй категории относятся указатели, которые могут "смотреть" на данные любого типа (по терминологии языка Паскаль – не типизированные указатели). При их объявлении используется служебное слово void. Наконец, третью группу составляют указатели, значениями которых могут быть только адреса точек входа в функции (по терминологии языка Паскаль – данные процедурного типа). Объявление и использование указателей разных категорий имеет свою специфику.
Для объявления одного указателя с именем p1 или нескольких указателей с именами p1, p2, …, которые должны будут "смотреть" на объекты типа type1, используется одна из следующих синтаксических конструкций:
type1 *p1;
type1 *p1,*p2,...;
Объявленные указатели еще никуда конкретно не смотрят – в выделенных им участках памяти находится "грязь". И одна из типичных ошибок начинающих программистов – попытка записать что-либо по указателям, которым еще не присвоены значения. Инициализацию указателя можно совместить с его объявлением:
int x=2,y;
int *p1=&x; //инициализация адресом переменной x
int *p2(&x); //инициализация адресом переменной x
int *p3=p1; //инициализация значением другого указателя
Указатель является переменной и его значение можно задать или изменить с помощью оператора присваивания:
p1=&y; //теперь значением p1 является адрес переменной y
Если целочисленному указателю p1 присваивается имя массива a или его адрес, то это эквивалентно засылке в p1 адреса первого элемента массива a[0]:
int a[10];
int *p1=a; //p1 смотрит на начало массива a
int *p2=&a[0]; //p2 тоже смотрит на начало массивa a
int *p3=(int *)&a; //p3 тоже смотрит на начало массивa a
Когда указатель p1 "смотрит" на переменную x, то по значению указателя можно извлечь значение переменной x или изменить его:
int x=5,y;
int *p1=&x; //значением p1 является адрес x
..........
y=*p1; //теперь значение переменной y равно 5
*p1=2; //теперь значение переменной x равно 2
Когда указатель p2 "смотрит" на начало массива q, то доступ к элементам этого массива можно организовать одним из следующих способов:
int q[20];
int *p2=q;
...........
y=*(p2+5); //теперь y=q[5]
x=p2[3]; //теперь x=q[3]
*(p2+1)=7; //теперь q[1]=7
В программах на языке C можно встретить нагромождение символов "*". Пугаться не надо – это просто многоступенчатая адресация:
int x,y;
int *p1=&y;
int **p2=&p1;
x=**p2; //то же, что x=*(*p2)=*(p1)=y
Объявление не типизированного указателя выглядит следующим образом:
void *pu;
Не типизированному указателю может быть присвоено значение указателя любого типа. Однако непосредственно извлечь или изменить значение по не типизированному указателю нельзя. Приходится прибегать к приведению типов:
#include <iostream.h>
#include <conio.h>
void main()
{ int x=5;
void *p=&x;
int *p1;
p1=(int*)p; //приведение указателя p к типу int*
cout<<"x="<<*p1<<endl;
getch();
}
Для объявления указателя pf на функцию типа double f(double x) имя указателя заключается в круглые скобки:
double (*pf)(double x);
Обратите внимание, что вместо имени функции здесь указано имя указателя в круглых скобках. А в качестве его конкретного значения можно задать адрес любой функции с аргументом типа double, которая возвращает значение типа double:
#include <iostream.h>
#include <conio.h>
#include <math.h>
void main()
{ double (*pf)(double x); //объявление указателя на функцию
double x=0.2;
pf=sin; //присвоение значения указателю
cout<<"sin(0.2)="<< pf(x) <<endl; //обращение по указателю
cout<<"sin(0.2)="<<(*pf)(x)<<endl;//обращение по указателю
getch();
}
Из двух возможных вариантов обращения к функции по указателю, наверное, надо отдать предпочтение первому, как более естественному. Хотя второй более выдержан в плане философии использования указателей.
Операции над указателями
Так как значениями указателей являются адреса ячеек оперативной памяти, то указатели можно сравнивать. Очевидно, что сравнение на равенство или на неравенство более информативно, чем сведения о том, какой объект лежит "выше" или "ниже".
Основные операции, чаще всего применяемые к указателям – сложение указателя с целым числом или вычитание из указателя целого числа. Обе они широко применяются в том случае, когда указатель связан с массивом. По сути дела, эти операции эквивалентны аналогичным процедурам над индексами элементов массива. И точно так же как прибавление к индексу означает переход к следующему элементу массива, прибавление 1 к указателю означает увеличение его текущего значения на количество байт, соответствующих типу указателя (точнее, типу данных, на которые указатель обязан смотреть):
int q[6]={1,2,3,4,5,6};
int *p = &q[3];
cout << *p++ <<endl; //сначала выводится 4, потом p=&q[4]
cout << (*p)++ <<endl; //сначала выводится 5, потом p=&q[5]
cout << *(p++) <<endl; //сначала выводится 6, потом p=&q[6]
Если при обработке некоторого массива используются два указателя p1 и p2, продвигаемые навстречу друг другу, то их разность (p2-p1) определяет количество элементов массива расположенных между этим двумя адресами.
Ссылки
Ссылки представляют особый вид данных, напоминающих указатели. Будучи объявлены в функции, они должны быть связаны с адресами конкретных объектов и после этого изменять свои значения не могут. В дальнейшем в рамках этой функции они выступают как синонимы своих объектов – такое ощущение, что одному и тому же объекту присвоено несколько имен:
int x;
int &rx=x; //объявление и инициализация ссылки
Ссылка rx является эквивалентом идентификатору x, т.е. операторы x=5 и rx=5 абсолютно идентичны. В этом варианте особой пользы от ссылки rx довольно мало – ее имя длиннее основного имени переменной. Однако при программировании в среде Borland C++ Builder довольно часто приходится иметь дело с надоедающе длинными обозначениями свойств объектов, и тогда применение разумной ссылки сокращает время набора программы:
TColor old_pc,&pc=TForm1->Image1->Canvas->Pen->Color;
............
//запоминание цвета пера
old_pc=pc; //вместо old_pc=TForm1->Image1->Canvas->Pen->Color;
//смена цвета пера
pc=clRed; //вместо TForm1->Image1->Canvas->Pen->Color=clRed;
............
//восстановление цвета пера
pc=old_pc; //вместо TForm1->Image1->Canvas->Pen->Color=old_pc;
Однако главное преимущество ссылок проявляется при спецификации параметров функций. Если формальный параметр объявлен в заголовке функции как ссылка, то упрощается его использование в теле функции (в отличие от указателей к именам ссылок ничего добавлять не надо) и становится более естественным вызов функций (вместо формальных параметров-ссылок указываются имена переменных).