Требуется обновление браузера.

Си++: указатель или ссылка


Просмотров: 224
10 ноября 2019 года

Проблематика


Чем отличается ссылка от указателя? Когда использовать то или другое? На первый взгляд, оба инструмента делают одно и то же, приводя к избыточности и без того сложного языка.

"Масла в огонь" подливает амперсанд, который уже встречался и в роли битового И (бинарный
a&b
), и в роли логического И (бинарный
a&&b
), и в роли взятия адреса (унарный префиксный
&a
). Впрочем, последнее кажется логичным: ссылка ведь использует адрес объекта на который ссылается... но ведь для хранения адреса используется указатель?

Погружение


На уровне языка ассемблера (чтобы понять, причём здесь ассемблер, уместно будет прочитать предыдущую заметку), принципиальной разницы в представлении и оперировании указателями и ссылками - нет. Дьюхэрст, правда, утверждает, что в некоторых случаях память под ссылки не выделяется вовсе, но мне спровоцировать подобное не удалось. Могу предположить, что эффект определяется интеллектуальностью транслятора и оптимизацией конкретных ситуаций. В любом случае, это лишь исключение.

Рассмотрим следующий листинг:
Простой пример работы с указателем и ссылкой на языке Си++
123456789101112131415
#include <iostream>

using namespace std;

int main()
{
	int foo = 32;
	int &bar = foo;
	int *ptr = &foo;

	bar = 64;
	(*ptr) = 255;

	return 0;
}

Если вы пользуетесь IDE C::B, то дизассемблер будет представлен во всей красе синтаксиса AT&T. Данное обстоятельство сперва несколько запутывает (после милого сердцу синтаксиса intel), но, со временем, привыкаешь, тем более, что анализировать придётся не самый сложный код.

Вот так выглядит листинг дизассемблера со вставками исходников Си++:

Разберём фрагменты подробнее. Так как стековые push/pop тут не используются, не будем задумываться над ориентацией стека, так же, для простоты, будем считать, что esp (регистр вершины стека) равен нулю:


На вершине стека будет хранится адрес возврата - чтобы не засорять иллюстрацию ненужными подробностями, не будем записывать там какую-либо константу.

Создание автоматической переменной
12
7  	    	int foo = 32;
0x00401342	movl   $0x20,0x4(%esp)
Записать значение 20 (представление в 16 с/с эквивалентно 32 в 10 с/с) по адресу, получаемому при сложении числа, хранящегося в регистре esp и числа 4.

Начиная с 0xc - ссылка на foo
123
8  		int &bar = foo;
0x0040134A	lea    0x4(%esp),%eax
0x0040134E	mov    %eax,0xc(%esp)
Вычислить адрес (сложить значение из регистра esp с числом 4) и записать его в регистр eax.
Сохранить значение, хранящееся в регистре eax, по адресу, получаемому при сложении числа, хранящегося в регистре esp и числа 0xc (то есть 12 в 10 с/с).

Начиная с 0x8 - указатель на foo
123
9  		int *ptr = &foo;
0x00401352	lea    0x4(%esp),%eax
0x00401356	mov    %eax,0x8(%esp)
Вычислить адрес (сложить значение из регистра esp с числом 4) и записать его в регистр eax.
Сохранить значение, хранящееся в регистре eax, по адресу, получаемому при сложении числа, хранящегося в регистре esp и числа 0x8.

Запись значения через ссылку
123
11 		bar = 64;
0x0040135A	mov    0xc(%esp),%eax
0x0040135E	movl   $0x40,(%eax)
Сохранить значение, хранящееся по адресу, получаемому сложением содержимого регистра esp и числа 0xc, в регистр eax.
Сохранить значение 0x40 (то есть 64 в 10 с/с) по адресу, записанному в регистре eax.

Запись значения через указатель
123
12 		(*ptr) = 255;
0x00401364	mov    0x8(%esp),%eax
0x00401368	movl   $0xff,(%eax)
Сохранить значение, хранящееся по адресу, получаемому сложением содержимого регистра esp и числа 0x8, в регистр eax.
Сохранить значение 0xff (то есть 255 в 10 с/с) по адресу, записанному в регистре eax.

Таким образом, на языке ассемблера, код для ссылки и указателя полностью аналогичен. С другой стороны, на уровне Си++, ссылка представляет собой менее гибкий, и от того более безопасный интерфейс доступа к адресу, используемому для косвенной адресации. Синтаксис Си++ даже не позволяет взять адрес ссылки (переменной, хранящей адрес), так как ссылка реализована как синоним объекта, на который она ссылается. Таким образом, не прибегая к трюкачеству со стеком (см.) невозможно испортить значение ссылки и "отвязать" её, в то время как для указателя это штатная функциональность.

Выводы


Исходя из сказанного выше, можно рекомендовать использование ссылки везде, где её функциональности достаточно: создание псевдонима для переменной, передача параметра по ссылке, возвращение l-value. Всё то же самое можно переписать и при помощи указателей, наполнив код кучей разыменований и превратив его в настоящий снегопад (* * * * *). Если Вы создаёте функцию с формальными параметрами из имеющегося фрагмента кода, всё что понадобится - указать нужным аргументам ссылочный тип, если же реализовывать данный рефакторинг при помощи указателей - понадобится просмотреть каждую строчку на предмет необходимости разыменования.

В случае, если возможностей ссылки не хватает - надо задуматься над использованием указателя.

Ситуация, в некотором смысле, напоминает дилемму "стек или куча": и в ней можно всегда отдавать предпочтение более функциональной куче, но такой код не выглядит обоснованным и безопасным (опрятным).

Запись опубликована в категориях:

Scrupulosus Алгоритмы и аспекты  
 

Комментарии

Инкогнито
  Загружаем captcha