Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Прочее

Указатели и ссылки в C++

В чём сходство и разница ссылок и указателей в C++? Рассмотрим пример, в котором функции возвращают сумму аргументов:

struct  STRUCT {int  member;};
int  ptr_fun (int*  op1, STRUCT*  op2) {
   return  *op1 + op2 -> member;
}

int  ref_fun (int&  op1, STRUCT&  op2) {
   return  op1 + op2 . member;
}
Функция «ptr_fun» использует только указатели, а «ref_fun» - только ссылки. С первого же взгляда заметно, что ссылки приятнее на глаз: они не требуют разыменования, да и «.» короче, чем «->». Почему со ссылками возможна точечная нотация, а с указателями – нет? Ссылки не могут ссылаться на объекты ссылочного типа: т.е. на указатели, массивы, и т.д. Ссылки всегда указывают на «конечный» объект. Причина в этом.

        Интересно, это небольшие плюшки бесплатны или за это надо платить дополнительным кодом при его генерации? Смотрим код функции на ассемблере «ptr_fun»:
	push      ebp
	mov       ebp,esp
	mov       eax,dword ptr [ebp+8]
	mov       eax,dword ptr [eax]
	mov       edx,dword ptr [ebp+12]
	add       eax,dword ptr [edx]
	pop       ebp
	ret
А вот во что компилируется «ref_fun»:
	push      ebp
	mov       ebp,esp
	mov       eax,dword ptr [ebp+8]
	mov       eax,dword ptr [eax]
	mov       edx,dword ptr [ebp+12]
	add       eax,dword ptr [edx]
	pop       ebp
	ret
Разницы нет вообще! Может, их вызов организован как-то по-другому? Смотрим вызов:
   int  a = ptr_fun (&X, &Y);
компилируется в
	lea       eax,dword ptr [ebp-8]
	push      eax
	lea       edx,dword ptr [ebp-4]
	push      edx
	call      @@ptr_fun$qpip6STRUCT
	add       esp,8
	mov       edi,eax
Вызов
   b = ref_fun (X, Y);
компилируется в то же самое (с поправкой на вызов разных функций):
	lea       eax,dword ptr [ebp-8]
	push      eax
	lea       edx,dword ptr [ebp-4]
	push      edx
	call      @@ref_fun$qrir6STRUCT
	add       esp,8
	mov       edi,eax
Оказывается, ссылки и указатели ничем не отличаются с точки зрения процессора. Мы платим за них одинаковую цену. Но если это так, то может быть они «взаимозаменяемы»? Попробуем в место вызова «ptr_fun» подсунуть адрес «ref_fun» и наоборот. Заметят ли это компьютер и ОС? Немного подправим вышеописанные функции, добавив в них вывод диагностики для «сигнализации», чтобы нас не обманули. Ну и вторая функция будет вычитать вместо сложения:
#include   

struct  STRUCT {int  member;};

int  ptr_fun (int*  op1, STRUCT*  op2) {
   cout << "Pointers\n";
   return  *op1 + op2 -> member;
}

int  ref_fun (int&  op1, STRUCT&  op2) {
   cout << "References\n";
   return  op1 - op2 . member;
}

typedef  int (*PF)(int*, STRUCT*);
typedef  int (*RF)(int&, STRUCT&);

main() {   
   int  X = 3;
   STRUCT  Y;
   Y . member = 2;
   int  a, b;
   a = ptr_fun (&X, &Y);  // вызываем как обычно
   cout << a << "\n";
   b = ref_fun (X, Y);  // вызываем как обычно
   cout << b << "\n";

   PF  pf = (PF) ref_fun;  // подмена
   RF  rf = (RF) ptr_fun;  // подмена

   a = (*pf)(&X, &Y);      // вызов ref_fun как ptr_fun
   cout << a << "\n";
   b = (*rf)(X, Y);        // вызов ptr_fun как ref_fun
   cout << b << "\n";

   return  0;
};
Запускаем и получаем на выходе:
Pointers
5
References:
1
References:
1
Pointers
5
Да, подмена прошла незамеченной. В чём же тогда разница между ними? Разницы на уровне процессора не существует, но она есть на уровне синтаксиса и семантики языка и компилятора.
Указатели и ссылки в C++
        Концепция указателей Си и семантика их работы позволяет обойти некоторые концептуальные недостатки языка. Мало того, что указатели могут быть инициализированы неправильным значением или не инициализированы вовсе. Значение указателя может быть подправлено на любое число:
   char* ptr = "abc";
   ptr -= 16;	// компилятор не проверяет правильность
   ptr += 64;	// и тут тоже
Хорошо, что компилятор не пропустит явно абсурдные операции:
   ptr *= 2;	// ошибка при компиляции
Но ничто не мешает обойти такую «защиту»:
   ptr = (char*) (2*(int)ptr);  // то же, что и ptr *= 2;
        С изменением значения указателя связана ещё одна проблема. Когда указатель связан с динамически выделенным участком памяти, то при изменения указателя этот участок становится недоступным. Ведь уже никто «не помнит», где находится этот участок, поэтому его невозможно освободить.

        Вот почему к обычным указателям приклеилось название «диких указателей». На другой, «светлой» стороне (или «культурной»?) мы видим растущую популярность всякого рода «умных» указателей, которые призваны дать безопасные способы работы с памятью. Но это опять борьба с последствиями, а причина – то, как был задуман Си. Использование «правильных» средств не отменяет возможности использования опасных: последние по-прежнему законны.

        Ссылки более безопасны, нежели указатели. Они перед использованием должны быть инициализированы, что уже создаёт какие-то гарантии. Адрес, содержащийся в ссылке, нельзя увеличивать и уменьшать арифметичекими операциями, единственный способ его изменить – присвоить ссылке адрес другого существующего объекта. Ссылки вносят единообразие при обращении к членам объектов:
obj1 . member = x;   // obj1 – ссылка
obj2 . member = y;   // obj2 – собственно объект
Но если obj1– указатель, то вносится разнобой:
obj1 -> member = x;
obj2 . member = y; 
        Со ссылками труднее вытворять всяческие трюки, недаром тип «void*» существует, а тип «void&» – нет. Конечно, это ограничивает неограниченный полёт творческой мысли отдельных одарённых личностей. Но есть ли такие области в программировании, где бы ссылки оказались бессильны в решении задач?

Когда функциональность ссылок недостаточна?

        Иногда в системном программировании надо обратиться по абсолютному адресу. Если ссылками пользоваться «по-честному», без union, например, то они не позволят этого сделать.

        Однако, недостаточность возможностей ссылок этим не исчерпывается. Ссылки нужно инициализировать адресом существующего объекта. Это требование не всегда можно выдержать. Рассмотрим такую структуру данных, как список. Он состоит из элементов примерно такого типа:
struct element {
   <какой-то тип> value; // собственно элемент
   element* prev;        // указатель на предыдущий элемент
   element* next;        // указатель на следующий элемент
};
        Когда элемент списка первый в этом списке, то указатель «prev» имеет значение NULL. Последний же элемент имеет указатель «next» так же равным NULL. Конечно, можно завести отдельные поля для того, чтобы отличать первый и последний элементы от остальных, но это расточительно. Поэтому соответствующие указатели просто устанавливаются в NULL в качестве сигнала. А теперь вопрос: а можем ли мы в структуре «element» заменить указатели на ссылки?
struct element {
   <какой-то тип> value; // собственно элемент
   element& prev;        // указатель на предыдущий элемент
   element& next;        // указатель на следующий элемент
};
Увы, но...
  • Мы теперь не можем полям «prev» и «next» присваивать значение NULL.
  • Мы не можем изменять значение этих полей, ведь они инициализируются адресом единожды. А если мы в список вставляем новый элемент и удаляем старый – мы же должны поменять адреса у соседей, но как теперь это сделать?

Являются ли ссылка указателем с урезанной функциональностью?

        В принципе, такое толкование сути ссылок недалеко от истины. Но... Учебники внушают мысль, что ссылка – это лишь синоним имени какого-то объекта и она не занимает места в памяти. Но можно ли верить учебникам, что этот сыр достанется совсем бесплатно? Я вас умоляю:
   printf("sizeof(int*) = %d\n", sizeof(int*));
   printf("sizeof(int&) = %d\n", sizeof(int&));
Этот код выдаёт совершенно одинаковую цену на сыр. Так что надо быть поосторожнее с утверждением «не занимает место в памяти». Рассмотрим ещё пример:
   int& func (int& k) { return k;}

   func(i) = func(i+1);
        Здесь мы видим чудо чудное и диво дивное, не вообразимое в Си, но возможное в C++. Функция употреблена в левой части выражения! Чудеса стали возможными благодаря ссылкам. Это как раз тот случай, когда функциональность ссылок превосходит функциональность указателей.

        Ну и нельзя обойти вниманием вопрос, зачем в C++ вообще появились ссылки. Они делают возможным перезагрузку операторов:
   T& operator+= (T&, T&);

Выводы

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

Последняя правка: 2017-02-16    18:40

ОценитеОценки посетителей
   ███████████████████████████ 5 (62.5%)
   ███████████ 2 (25%)
   ██████ 1 (12.5%)
   ▌ 0

Отзывы

     2016/01/21 21:31, rst256

Естественно что операции над ссылками и над указатели порождают идентичный код, ссылки это указатели с урезанным функционалом, введенные в язык отнюдь не с целью сделать его удобней/безопасней/круче, а по совсем иным причинам (см. статью о ссылках с++ на вики)...

Фирменный ход с++ - любой косяк/костыль выдавать за очень полезную фичу!
Здесь это тупейшая "защита от дурака", просто за счет урезания ненужного "особо одаренным" программистам функционала.

     2016/01/25 15:57, Автор сайта

Да, костылей хватает, но было бы неправильно говорить, что они есть только в C++. К примеру, функция malloc всегда возвращает результат типа void*. Тут хоть убейся, но без костыля в виде приведения типов ну никак не обойтись:
double* ptr = (double*) malloc (1);
В этом примере есть ещё изъян: нет никакой связи между размером запрашиваемой памяти и типом указателя в левой части. Мы точно видим, сколько нам надо памяти – sizeof(double), а malloc не видит, ей можно подсунуть всё, что угодно.

А вот C++:
double* ptr = new double;
И костыль для приведения типа не нужен, да и с размером памяти ошибиться труднее. Однако, ни Си, ни C++ не могут служить нам примером того, как в новом языке реализовать адресную арифметику, чтобы было и просто, и ясно, и безопасно.

     2016/04/12 23:42, rst256

К примеру, функция malloc всегда возвращает результат типа void*.

Все правильно, так и должно быть.
double* ptr = (double*) malloc (1);
Ну и зачем вам это "приведение"? К чему вы собрались приводить свеже-выделенный блок память?

Мы точно видим, сколько нам надо памяти, а malloc ...

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

А вот C++:
double* ptr = new double;
И костыль для приведения типа не нужен, ...

Придумать и решить несуществующую проблему, крестоносцы тут всегда были вне конкуренции.

     2016/04/12 23:55, rst256

Причиной введения ссылок в язык С++ в основном являлась необходимость перегрузки операторов, применяемых к объектам пользовательских типов (классов). Как упоминалось выше, передача по значению громоздких объектов в качестве операндов вызывала бы лишние затраты на их копирование. С другой стороны, передача операндов по адресу с использованием указателей приводит к необходимости взятия адресов операндов в выражениях.
Однако, выражение &y - &z уже имело определённый смысл в языке С.

Источник: https://ru.wikipedia.org/wiki/Ссылка_(C++)

     2016/04/13 08:00, rst256

Мне кажется, или лепить бессмысленный тайп-кастинг к маллоку вас заставляет давно устаревший компилятор TCC? Ни один из современных компиляторов Си подобной фигней не страдает, "Under the C standard, the cast is redundant."(https://en.wikipedia.org/wiki/C_dynamic_memory_allocation#cite_note-4) уж не обессудьте, что без перевода, в русской версии статьи этого нет. Так какие же это костыли, если приведение там не требуется? Самое смешное, что на Си легко получить то, что вы хотите банальным макросом:
#ifdef ___CHECK_MALLOC_RESULTS

inline void *___check_malloc_results(size_t size){
void * ptr = malloc(size);
if (ptr == NULL){
fprintf(stderr,"malloc(%zu) failed in file %s at line # %d",
size, __FILE__, __LINE__);
exit(EXIT_FAILURE);
}
return ptr;
}

#define _NEW(T, N, ...) (T *)___check_malloc_results(sizeof(T)*(N));

#else

#define _NEW(T, N, ...) (T *)malloc(sizeof(T)*(N));

#endif

#define NEW(...) _NEW(__VA_ARGS__, 1)


#define _INIT(V, N, ...) V = NEW(typeof(*(V)), N)
#define INIT(...) _INIT(__VA_ARGS__, 1)
Пример использования:
	double * d = NEW(double);
double * d_x_6 = NEW(double, 6);
double * d2, *d3;
INIT(d2);
INIT(d3, 10);
d_x_6[5]=.5;
Один в один с++, я бы даже отказался от повторного указания типа в операторе new, макросы такое позволяют легко, нужно помнить и чтить принцип DRY!

     2016/04/15 12:58, Автор сайта

К чему вы собрались приводить свеже-выделенный блок память? ...
Придумать и решить несуществующую проблему...
лепить бессмысленный тайп-кастинг к маллоку вас заставляет давно устаревший компилятор TCC?...
Так какие же это костыли, если приведение там не требуется?...

Вы будете долго смеяться, но очень давно приучил себя указатели типа void* приводить к указателям на другие типы.
 void* vp;
int* ip;
vp = ip; // вполне законно
ip = vp; // ошибка при компиляции
ip = (int*) vp; // теперь правильно, необходимо приведение
Тупо заставил себя всегда делать приведение («Надо или не надо – не хочу думать, зато никогда не ругается при компиляции, а лишний код не генерится»), а теперь стандарт «для себя» машинально переношу на стандарт языка. Компиляторы в этом не виноваты, виноват я. Признаю ошибку.

Но строгая типизация всё-таки необходима, она помогает бороться с ошибками. Насаждать строгую типизацию и давать лазейки для её обхода в виде void* как-то не по фэншую.

     2016/08/07 04:29, rst256

Некоторая небезопасность указателей (указатели со значением NULL) должна быть ликвидирована обязательностью («зашитой» в синтаксис языка) проверок на NULL.

Ви наверное шутите? NULL одно из самых безопасных значений указателя, пожалуй даже самое безопасное из всех! Какое тогда по вашему значение будет указывать на то, что он пустой? Раз NULL нельзя, то, наверное, будет какой-то иной способ отмечать сие, пусть и с незначительными потерями производительности. Это не важно, важно то что пустой указатель, у которого не выставлено значение NULL, это НЕБЕЗОПАСНО. Тем более, что это значение скорее всего окажется внутри доступного программе сегмента. Всегда есть вероятность, что по указателю обратятся, и лучше если в тот момент его значение будет либо валидным, либо NULL. Ошибка сегментации всегда приятнее, чем разрушение данных.

Здесь мы видим чудо чудное и диво дивное, не вообразимое в Си, но возможное в C++. Функция употреблена в левой части выражения!

Вы издеваетесь? Я даже не припоминаю? было ли когда нибудь в Cи такое недоступно. Вся разница в том, что Си требует выполнить разадресацию явно. Вот пример: лямбда-функция в левой части выражения, обычные функции, разумеется, работают также.
*({ char* self (int i) { return &(sss[i]); } &self; })(10)='@';
P.S. А вот это как раз тот реальный случай, когда функциональность указателей превосходит функциональность ссылок:
((((mode *)(list = (char *) ((((int)list + (__alignof__(mode)<=4?3:7)) &
(__alignof__(mode)<=4?-4:-8))+sizeof(mode))))[-1]
Но это, конечно, же не для прикладного программирования, хотя va_arg там иногда используют.

     2017/09/21 19:37, Comdiv

int& func (int& k) { return k;}

func(i) = func(i+1);

Хотите сказать, что такой код у Вас компилируется?

     2017/09/30 23:57, Автор сайта

Код
#include <stdio.h>
int& func (int& k) { return k;}
main() {
int i = 99;
func(i) = func(i+1);
printf("i = %i\n", i);
func(i) = 999;
printf("i = %i\n", i);
}
не только компилируется, но и выполняется и выводит "100", а потом "999". А почему Вы этим интересуетесь?

     2018/05/23 23:06, Александр Коновалов aka Маздайщик

Некоторая небезопасность указателей (указатели со значением NULL) должна быть ликвидирована обязательностью («зашитой» в синтаксис языка) проверок на NULL.

Кстати, да. Во многих прикладных языках без адресной арифметики вроде C# или Java ссылочные переменные могут хранить или действительную ссылку на объект, или NULL (причём «неинициализированного» значения нет, подразумевается инициализация нулём). И этот факт некоторых теоретиков языков программирования напрягает (где-то читал на Хабре). Дескать, правильнее проектировать язык, в котором объектные ссылки по умолчанию всегда хранят действительный указатель, а для «обнуляемых» указателей нужно явно использовать тип вроде Nullable<T> с методами вроде bool has_value() и T get(). Последний для нуля выбрасывает исключение.

Так, кстати, сделано и в Rust’е.

     2018/05/24 15:27, Автор сайта

В Rust’е нет исключений, поинтересуйтесь на досуге этим. Там другие механизмы. А вот языки, которые делают вывод типов (ML был первым), интересны тем, что объект инициализируется не только типом, но и значением (как бы побочный эффект от вывода типов). Т.е. в ML и его последователях типа Haskell нет такого понятия, как «неинициализированное значение». Правда, есть значения типа «ошибка» (NULL для указателей — это и есть «ошибка»), но в Haskell это отслеживается монадой Maybe и родственными.

     2018/05/24 23:51, Александр Коновалов aka Маздайщик

Спасибо, я в курсе, что в Rust’е нет исключений. Там другой, более интересный механизм, который складывается из монады Error (не помню, как она в Расте точно называется), макросов и приведения типов. Про Rust я вспомнил в связи с тем, что в нём указатели (вне unsafe-кода) всегда ссылаются на актуальный объект, а для временно отсутствующего объекта применяется аналог монады Maybe. Я мог бы вспомнить Haskell, но он из «второй категории» по Вашей терминологии. А Раст позиционируется как из «первой».

В C# есть тип Nullable<T>, но он может добавлять значение NULL только типам значений. А мы рассуждаем об обратном: об отбирании значения NULL от объектных ссылок.

За все языки семейства ML не скажу, но OCaml позволяет создавать мутабельные поля в классах и имеет встроенный (в стандартной библиотеке, доступен по умолчанию) тип данных ref — изменяемую ячейку. Но в обоих случаях, как я понимаю, создать такие объекты, не проинициализировав их начальным значением, нельзя. Чисто синтаксически.

     2018/05/26 01:01, Автор сайта

Этот механизм в Rust не производит впечатления завершённости. Можно проверять значения на NULL, но можно и не проверять. Т.е. нет обязательности такой проверки. Нехорошо иметь такое в хорошем языке. Ну и плюс там используются макросы. А что такое макросы? Это надстройка над языком, которая говорит, что сам язык несовершенен, но за счёт костылей мы его маленечко ремонтируем. Если же язык хорош, то и макросы ему не нужны.

     2018/05/27 16:21, Александр Коновалов aka Маздайщик

Освежил в памяти обработку ошибок в Rust (https://habr.com/post/270371/ , Вам эта статья известна).

Как я понимаю, проверка указателя (завёрнутого в Option или Result) выполняется всегда. Её можно выполнять явно при помощи match, можно использовать комбинаторы разнообразные, можно использовать макрос try!, и можно использовать метод unwrap или expect, которые являются своего рода assert’ами, вызывающими panic. В любом случае разыменования нуля не происходит.

Под «необязательностью проверки» Вы имели ввиду unwrap()?

Или речь идёт об unsafe-блоках, где можно использовать нулевые указатели? Ну так на то они и unsafe.

     2018/05/27 23:42, Автор сайта

Надо почитать эту статью. Такое впечатление, что она прошла мимо меня.

     2018/05/27 23:47, Александр Коновалов aka Маздайщик

В этой статье есть несколько Ваших комментариев от 2015 года.

Я и сам её давно читал. Оттуда узнал, что в Расте есть аналог монад Хаскеля Maybe и Error. И что красиво обработка ошибок сделана через макрос try! и приведение типов (типаж From). Но вот название макроса и названия типов Option и Result (соответственно) я уже забыл. Сегодня специально искал эту статью и немножко завис на Хабре.

     2018/05/29 21:52, Автор сайта

Чем хорош склероз — тем, что каждый день новости :) Я уж и забыл про эту статью. Вроде бы те неясности, ради которых задавал вопросы, прояснились и уложились в голове должным образом. Надо найти время и внимательно перечитать эту статью.

Написать отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Устарел ли текст как форма представления программы

Русский язык и программирование

Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

Некоторые «вкусности» Алгол-68

«Двухмерный» синтаксис Python

Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

Должна ли программа быть удобочитаемой?

Стиль языка программирования

Тексто-графическое представление программы

●  Разделители

●  Строки программы

●  Слева направо или справа налево?

Комментарии

●  Длинные комментарии

●  Короткие комментарии

●  Комментарии автоматической генерации документации

●  Нерабочий код

Нужны ли беззнаковые целые?

Шестнадцатиричные и двоичные константы

Условные операторы

Переключатель

Циклы

●  Продолжение цикла и выход из него

Некошерный «goto»

Операции присвоения и проверки на равенство. Возможно ли однаковое обозначение?

Так ли нужны операции «&&», «||» и «^^»?

Постфиксные инкремент и декремент

Почему в PHP для конкатенации строк используется «.»?

Указатели и ссылки в C++

Использование памяти

Почему динамическое распределение памяти – это плохо

Как обеспечить возврат функциями объектов переменной длины?

●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●  Массивы переменной длины в C/C++

●  Размещение объектов в стеке, традиционный подход

●  Размещение объектов переменной длины с использованием множества стеков

●  Размещение объектов переменной длины с использованием двух стеков

●  Реализация двухстековой модели размещения данных

●  Двухстековая модель: тесты на скорость

●  Размещение объектов переменной длины с использованием одного стека

Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

Безопасность и размещение объектов переменной длины в стеке

Массивы, структуры, типы, классы переменной длины

О хранении данных в стеке, вместо заключения

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Прочее

Последние комментарии

2018/06/14 00:37, rst256
Лень — двигатель прогресса

2018/05/31 18:52, rst256
Программирование без программистов — это медицина без врачей

2018/05/31 17:57, rst256
Циклы

2018/05/31 17:50, Comdiv
Разбор цепочек знаков операций

2018/05/31 17:42, Comdiv
Как отличить унарный минус от бинарного

2018/05/30 18:57, Александр Коновалов aka Маздайщик
Раскрутка компилятора

2018/05/29 21:52, Автор сайта
Указатели и ссылки в C++

2018/05/28 20:29, Александр Коновалов aka Маздайщик
Анонс будущих статей