Пойди туда, сам знаешь куда

Для любого типа T тип T* означает «указатель на объект типа Т». Указатель является объектом (переменной), хранящим адрес другого объекта.

Для обращения к объекту, на который указывает указатель (адрес которого хранится в указателе), используется оператор * (звёздочка), называемый оператором разыменования или косвенным обращением.

Для получения адреса объекта используется унарный оператор &. Таким образом, для объекта

1
int value;

Запись &value означает «адрес объекта value». Воспользовавшись этим можно сохранить адрес объекта value следующим образом:

1
2
int value;
int *p_value = &value;

что означает, что переменная p_value, имеющая тип «указатель на объект типа int» инициализируется адресом переменной value.

Замечание

С точки зрения синтаксиса языка безразлично, где будет находиться пробел – справа или слева от символа *. Его вообще может не быть. Однако, я рекомендую всегда символ * присоединять к имени переменной, а не к имени типа, что кажется более логичным. Причина этого следующая. В приведённой записи:

1
2
int* p,q;
int *r,s;

переменные p и r имеют тип «указатель на тип int», а переменные q и s имеют тип int. Хотя по записи (особенно в первой строке) может показаться, что все четыре переменные должны иметь тип «указатель на тип int». Для того, чтобы все четыре переменные имели тип «указатель на тип int» необходимо написать:

1
2
int* p, *q;
int *r, *s;

это синтаксическая проблема языка и проще всего обойти её, привыкнув присоединять символ * к объекту, а не к типу.

Теперь указатель p_value может быть использован для доступа к объекту, на который он указывает:

1
*p_value = 1;

Запись означает: поместить единицу в объект типа int, адрес которого хранится в объекте p_value. Поскольку в p_value хранится адрес объекта value, то будет изменено значение именно этой переменной – в неё будет помещена единица.

Точно также можно считать значение переменной по указателю:

1
cout << “Value=” << *p_value;

Указатели придуманы с целью непосредственного отражения механизмов адресации компьютеров, на которых исполняются программы.

Гарантируется, что нет объектов с нулевым адресом. Следовательно, указатель, равный нулю можно интерпретировать как указатель, который ни на что не ссылается.

Указатели на массивы

Имя массива можно рассматривать как указатель на его первый элемент.

Рассмотрим пример:

1
2
3
4
int v[] = { 1,2,3,4 };
int *p1 = v;    // Указатель на первый элемент
int *p2 = &v[0] // Указатель на первый элемент
int *p3 = &v[4] // Указатель на элемент, следующий за последним

В противоположность указателю на массив, определение массива указателей выглядит следующим образом:

1
int *ap[15]; // Массив из 15 указателей на int

Результат применения операторов -, +, --, ++ к указателю зависит от типа объекта, на который ссылается указатель. Если к указателю p типа T* применяется арифметическая операция, предполагается, что он указывает на элемент массива объектов типа Т; p+1 указывает на следующий элемент массива, а p-1 на предыдущий. То есть целое значение p+1 будет на sizeof(T) больше, чем целое значение р.

Рассмотрим пример обнуления элементов массива с использованием индексов

1
2
3
	
int arr[ArraySize];
for (int i=0; i < ArraySize; ++i)
  arr[i] = 0;

и с использованием указателей:

1
2
3
4
int arr[ArraySize];
int *p=arr;
for (int i=0; i < ArraySize; ++i)
  *p++ = 0;

Интересна запись *p++. Унарные операторы * и ++ имеют одинаковый приоритет, однако они правоассоциативны. То есть в данном случае первым будет выполняться оператор ++, увеличивающий значение указателя. Указатель будет сдвинут на следующий элемент массива. Но поскольку это оператор постинкремента, то для разыменования будет использовано старое значение указателя. Таким образом, в одном выражении записано сразу два действия: передвинуть указатель и разыменовать указатель. В объект, на который указывает указатель помещается ноль.

Указатели и константы

В операциях с указателями участвуют два объекта: сам указатель и объект, на который он ссылается. Помещение ключевого слова const перед объявлением указателя делает константным объект, а не указатель. Для объявления самого указателя в качестве константы, используется оператор объявления * const, а не просто *.

Примеры:

1
2
3
4
5
const int *p1;  // указатель на константу типа int
int const *p2;  // указатель на константу типа int
int *const p3;  // константный указатель на объект типа int
const int *const p4; // константный указатель на константу типа int
int const *const p4; // константный указатель на константу типа int

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

Третий вариант указывает, что константен указатель, то есть его нельзя установить на другой объект – в него нельзя занести другой адрес в памяти.

Четвёртый и пятый вариант являются синонимами и указывают, что константен как указатель, так и указываемый объект.

Указатели на строки

Поскольку текстовая строка имеет тип const char [] и является массивом, к ней применимы все ранее приведённые соображения о массивах и указателях.

Строковый литерал можно присвоить переменной типа char *. Это сделано для совместимости с ранними версиями языка C, в которых не было ключевого слова const.

Однако изменение строкового литерала через такой указатель является ошибкой:
1
2
3
4
void f() {
  char *p=”text”;
  p[3] = ‘a’;  // ОШИБКА
}

Память под строковые литералы выделяется статически, поэтому их свободно можно возвращать в качестве значения функции:

1
2
3
4
const char *access(int i) {
   …
   return “access denied”;
}

Будут ли одинаковые литералы записываться в одно место памяти или нет – зависит от реализации.

Строки оканчиваются нуль-байтом (‘\0’), что делает их удобными для использования указателей. Рассмотрим копирование строк:

1
2
3
4
5
6
const char src[]=”Строка, которую надо скопировать”;
char dst[sizeof src];
const char *p_src = src;
char *p_dst = dst;
while (*p_dst++ = *p_src++)
  ;

Создаются два указателя – константный указатель p_src, который хранит адрес копируемого элемента и указатель p_dst, хранящий адрес, куда будет скопирован элемент. Копирование будет продолжаться до тех пор, пока не будет скопирован нуль-байт, завершающий строку.

Указатель на void

Указатель на объект любого типа можно присвоить переменной типа void*.

void* можно присваивать, сравнивать и явно преобразовать в указатель любого другого типа.

Массивы, как параметры функции

При передаче массива как аргумента функции происходит неявное преобразование имени массива в указатель на его начальный элемент с потерей информации о размере массива. Таким образом, массив всегда передаётся по указателю – его копия не создаётся .

Указатели на функции

С функцией можно выполнить только две операции: вызывать её и получить её адрес. Адрес функции может быть использован для вызова функции. Например:

1
2
3
4
void error(int i);
void (*p)(int);
p=&error;
(*p)(1);

Разыменование указателя при вызове не обязательно. Также не обязательно пользоваться & для получения адреса функции.

Предыдущий пример может быть записан как:

1
2
3
4
void error(int i);
void (*p)(int);
p=error;
p(1);

Указатели и структуры

В случае, если указатель используется для хранения адреса объекта типа структуры, до доступ к полям структуры может быть осуществлён двумя способами (строки 4 и 5):

1
2
3
4
5
struct vec2 { double x, y; };
vec2 dir;
vec2 *p_dir;
(*p_dir).x=0;
p_dir->x=0;

В строке 4 используется полная форма записи: разыменование указателя и обращение к члену x объекта dir. В строке используется сокращённая форма записи с использованием оператора -> который обеспечивает прямое обращение к члену структуры указываемого объекта. Таким образом, строки 4 и 5 дают один и тот же результат.

Использование указателей

Указатели позволяют максимально эффективно решить проблему возврата нескольких значений из функции, а также возврата больших объектов, копирование которых нежелательно.

В случае, если требуется вернуть несколько значений, можно воспользоваться следующим синтаксисом:

1
2
3
4
5
void GetMouseState(int *p_keys, int *p_x, int *p_y) {
  *p_keys  = …;
  *p_x = …;
  *p_y = …;
}

Поскольку функция GetMouseState получает копии адресов объектов, то при изменении их через указатели меняются сами объекты, а не их копии. Обратите внимание – копируются указатели, а не сами объекты! Формально здесь происходит то же самое, что и в случае со ссылками.

Вызов этой функции будет выглядеть следующим образом:

1
2
	
int keys,x,y;
GetMouseState(&keys,&x,&y);

В данном случае, в отличие от передачи аргумента по ссылке, клиентский код явно указывает, что он передаёт адрес объекта. Передавая адрес клиент подразумевает, что, скорее всего, адрес будет использован для изменения значения. Исходя из этого, я рекомендую для обеспечения работы с самим объектом, а не его копией, использовать константные ссылки, а для ситуации, когда вызываемая функция планирует изменять значение – использовать указатели. Во втором случае в клиентском коде явно видно, что берётся адрес объекта и следует ожидать его изменения.

Альтернативный вариант с использованием структуры в качестве возвращаемого значения

1
2
3
4
5
6
7
8
9
10
11
12
struct MouseState {
  int keys;
  int x,y;
};

MouseState GetMouseState() {
  MouseState mouse_state;
  mouse_state.keys  = …;
  mouse_state.x = …;
  mouse_state.y = …;
  return mouse_state;
}

не всегда удобен, и в общем случае требует копирования объекта структуры при возврате значения, что может быть неэффективно, а в ряде случаев и невозможно.

Но даже в этом случае можно использовать комбинированный приём:

1
2
3
4
5
6
7
8
9
10
struct MouseState {
  int keys;
  int x,y;
};

void GetMouseState(MouseState *p_mouse_state) {
  p_mouse_state->keys  = …;
  p_mouse_state->x = …;
  p_mouse_state->y = …;
}

В этом случае в функцию передаётся указатель на структуру, которая может быть заполнена в функции:

1
2
3
4
5
6
void DisplayCursorInfo() {
  MouseState mouse_state;
  GetMouseState(&mouse_state);
  cout << “Mouse cursor x=” << cursor.x << endl;
  cout << “Mouse cursor y=” << cursor.y << endl;
}

В данном случае объект структуры создаётся в клиентском коде и не требуется его копирование при возврате функции в противоположность возврату значения структуры.

Принципиальное отличие указателя от ссылки заключается в том, что указатель может иметь нулевое значение, то есть не указывать ни на какой объект. В то время как ссылка (при корректном использовании) всегда будет ссылаться на реальный объект.

Хорошим стилем является инициализация указателя, не указывающего ни на какой объект нулевым значением. Даже если это значение не планируется использовать реально – т.е. указатель планируется установить на требуемый объект, а затем использовать. В случае, если программистом будет допущена ошибка и указатель будет использован до того, как в него будет помещен адрес реального объекта, значительно проще будет обнаружить ошибку при обращении по нулевому адресу, чем по случайному адресу в памяти.

Рекомендация

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

Указатели стоит использовать только в строго определённых ситуациях:

  • В коде нижнего уровня, работающем непосредственно с оперативной памятью или внешними устройствами.
  • Для возврата из функций значений нескольких объектов или для возврата больших объектов как результатов функций.
  • При работе с контейнерами полиморфных объектов, которые будут обсуждаться позже.
Дата последнего обновления 2 апреля 2006