Наиболее интересное, что есть в языках С и С++ — это указатели. Без понимания принципов работы с указателями нельзя научиться работать на этих двух языках. Справедливости ради надо заметить, что указатели в С++ можно применять реже, чем в С, решая при этом одни и те же задачи. Например, если надо смоделировать работу сложной динамической структуры, то в С++ есть возможность применить какой-нибудь из стандартных классов STL (Стандартная библиотека шаблонов), а в С всё приходится делать самому, используя указатели.
Любая программа, как известно, предназначена для обработки данных. Все данные условно можно подразделить на собственно данные (числа, строки и т.д.) и на адреса в оперативной памяти, где хранятся эти данные. Мы оперируем в программе именами переменных, но для исполнения программы процессором компьютера все имена ещё на этапе компиляции заменены на адреса. Поэтому компьютер работает либо с неким данным, либо с адресом, где хранится это данное. И ничего другого просто нет. Для выполнения различных действий с адресами служат указатели.
Указатель — это беззнаковое целое, используемое для хранения адреса какого-либо участка памяти.
Указатель всегда является переменной величиной. Указатели в 16-разрядной операционной системе MS DOS так же были 16-разрядными. В настоящее время обычно используются 32-разрядные операционные системы (Windows, Linux и др.), в которых указатели занимают 4 байта (32-разрядное целое без знака). И хотя это по внутреннему представлению в точности соответствует типу unsigned int, но указатель и переменная типа unsigned int — это две большие разницы.
Всякий указатель используется для работы с данными, которые имеют какой-то свой тип и, соответственно , свой размер, например, double или int. Следовательно, при описании указателя необходимо сказать, на объекты какого типа он будет настроен.
Формальное описание указателя такое:
Тип_данных * Имя_указателя;
где
Тип_данных — любой ранее определённый тип (стандартный или пользовательский);
* — знак «звёздочка» здесь является признаком того, что речь идёт об указателе;
Имя_указателя — определяется по общим правилам (как для любого идентификатора пользователя).
Дадим описание указателя для работы с данными типа double:
double *u;
Данную запись необходимо понимать так: «u — это указатель на объект типа double».
На какой объект настроен этот указатель? В данный момент ни на какой. Значение указателя u в момент выделения памяти не определено.
Пусть имеется переменная a типа double:
double a = 2.5;
Настроим указатель u на эту переменную a:
u = &a;
Здесь знак & означает вычисление адреса, т.е. мы вычисляем адрес переменной a и записываем его в ячейку u. Пусть a находится по адресу 1000. Вот это число-адрес и станет содержимым u. Теперь указатель u настроен на начальный адрес переменной a. Что это нам даёт? Не много — не мало, а доступ к содержимому переменной a, если применить операцию разыменования:
cout << *u << endl;
Этим оператором мы выводим на экран монитора содержимое переменной a, т.к. указатель u настроен на эту же переменную a.
Можно и изменить значение переменной a, используя указатель u:
*u = 11.3;
cout << a << endl;
На экране будет выведено число 11.3, хотя ранее переменная a имела значение 2.5. Следовательно, пользуясь указателем, мы действительно изменили содержимое переменной a.
Рассмотренный пример работы с указателем особой ценности не имеет и служит лишь для демонстрации работы с указателями.
Хотя всякий указатель по внутреннему представлению — это беззнаковое целое, но по характеру операций он заметно отличается от любых целых типов. Рассмотрим все операции, допустимые для указателей.
1.Присваивание (=). Указателю можно присвоить адрес какой-то переменной или значение другого указателя. Приведем пример:
double a = 2.5;
double *u;
double *v;
u = &a; // Указатель u настроен на переменную a
v = u; // Теперь и указатель v настроен на переменную a
2.Разыменование (*) — эта унарная операция позволяет обратиться к ячейке памяти, на которую предварительно настроен указатель. Например:
*u = a + 2;
Теперь значение переменной a равно 4.5.
3.Вычисление адреса (&).
double b = -1.5;
v = &b; // Теперь указатель v настроен на переменную b.
4.Автоувеличение (++).
Для переменной любого числового типа автоувеличение означает увеличение на 1 значения того, что было в ячейке, например:
int i = 5;
i++; // Теперь i равно 6.
Для указателя операция автоувеличения означает переключение на следующий объект того же типа, что и тип, на который был настроен указатель. Пусть имеется массив x из трёх чисел типа double и указатель u на тип double:
double x[3] = {3.4, 5.2, 7};
double *u;
А теперь поработаем с массивом через указатель (см. так же рисунок ниже):
u = &x[0]; // настроим указатель u на начало массива
cout << *u << endl; // будет напечатано число 3.4
u++; // переключаемся на 1 объект вправо, т.е. на x[1]
cout << *u << endl; // будет напечатано число 5.2
Как
видим, автоувеличение даёт смещение на 1
один объект, а в байтах для указателя на тип double
это будет равно 8
(т.е. смещаемся на 8
байт). Таким образом, адрес, хранящийся в указателе u,
за счёт выполнения операции автоувеличения
(++),
увеличится на 8.
Подобная картина будет и для указателей на другие типы, например: для указателя на тип int шаг смещения при автоувеличении равен 4. Только для указателей на объекты размером в один байт (например, на char) величина смещения будет равна 1 байту, что соответствует обычному представлению об операции автоувеличения.
5.Автоуменьшение (--).
Здесь всё аналогично тому, что было сказано об автоувеличении. Рассмотрите эту операцию самостоятельно.
6.Сложение (+).
К указателю можно прибавить целое число. Если сложение сопоставить с автоувеличением, то несложно понять, что операторы
double *v;
v = &x[0];
v = v + 2;
cout << *v << endl;
приводят к смещению указателя v на 2 объекта вправо, т.е. адрес, записанный в v, увеличится на 16 (2*8 = 16). будет напечатано число 7 (см. рис. выше).
Но нельзя складывать два указателя. Попытка сделать это бессмысленна по своей сути (что может означать сумма значений двух указателей?), поэтому она просто запрещена.
7.Вычитание (-)
Для указателей допустимо как вычитание числа из указателя, так и определение разности двух указателей.
Пример 1:
double *u = &x[2]; // Настроить указатель
// на 2-й элемент массива
int k = 2; // Пусть это будет смещением
u = u – k; // Смещаем влево значение указателя
// на k объектов типа double
Пример 2.
double *u, *v;
// Далее какие-то действия
// ........................
int k = u – v;
Теперь k определяет расстояние между объектами в памяти, на которые были предварительно настроены указатели.
8.Умножение, деление и вычисление остатка для указателей бессмысленны, а потому запрещены.
9.Операции сравнения. Для указателей допустимы все без исключения операции сравнения. Чаще всего используется проверка на равенство (==) и на неравенство (!=). Пример:
double *u;
….............
if(u != NULL) {
// Можно работать с указателем. К примеру, так:
u++;
}
else {
//Работа с указателем невозможна
}
10.Логические операции для указателей не применяют, но оператор типа
if(u && v) { операторы }
ошибки не вызывает, хотя трудно придать смысл такой записи.
Используя механизм указателей, можно решать самые разнообразные задачи. Чаще всего указатели используют для:
работы с массивами и особенно с массивами типа char;
передачи данных в функцию по адресу;
построение сложных динамических структур данных.
Подробности использования указателей по перечисленным темам будут
приведены в последующих темах.