Указатели

Наиболее интересное, что есть в языках С и С++ — это указатели. Без понимания принципов работы с указателями нельзя научиться работать на этих двух языках. Справедливости ради надо заметить, что указатели в С++ можно применять реже, чем в С, решая при этом одни и те же задачи. Например, если надо смоделировать работу сложной динамической структуры, то в С++ есть возможность применить какой-нибудь из стандартных классов 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) { операторы }

ошибки не вызывает, хотя трудно придать смысл такой записи.

Область применения указателей

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

  1. работы с массивами и особенно с массивами типа char;

  2. передачи данных в функцию по адресу;

  3. построение сложных динамических структур данных.

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

Hosted by uCoz
Назад
На верх
Вперёд
Hosted by uCoz