Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Для новичков > [FAQ] Указатели и ссылки


Автор: bsa 6.9.2009, 13:29
Любой новичок, который решил изучить язык Си или С++, почти сразу же сталкивается с непониманием смысла "указателей" и "ссылок".

Указатель - это переменная или константа, содержащая адрес памяти, начиная с которого могут располагаться данные определенного типа (достаточно представить бумажку, на которой написан домашний адрес). Указатель не несет в себе информации о количестве данных (возьмите бумажку и попытайтесь по ней определить, сколько домов находится на той же улице). Можно получить адрес любой переменной (у всех домов есть адрес, не так ли?) и присвоить его подходящему указателю (номер телефона - на страницу телефонной книги, адрес - в адресной, email - в список контактов... - каждому свое место). Операция по получению доступа к данным, на которые указывает указатель, называется разыменованием. Пример:
Код
int *p; /* указатель на переменную типа int */
int x; /* переменная типа int */
p = &x; /* указателю p присваиваем адрес переменной x */
*p = 0; /* разыменование указателя p и присваивание переменной x значения 0 */
double r;
p = &r; /* ошибка компиляции - нельзя присваивать указателю на целое адрес вещественной переменной */

Есть понятие дикий или подвисший указатель - это указатель непонятно куда. Он получается при создании переменной-указателя или после освобождения области памяти, в которую он указывал. Данными, на которые он указывает, пользоваться нельзя. В противном случае поведение программы непредсказуемо.
Есть понятие нулевой указатель - это указатель в никуда. Обычно, он используется для информирования, что требуемая память не выделена, данные не найдены и пр. При попытке использовать данные, на которые он указывает, программа аварийно завершится (ошибка: "segmentation fault" или "access violation"). Пример:
Код
int *p; /* дикий указатель*/
p = NULL; /* нулевой указатель */
*p = 0; /* вызовет аварийное завершение программы */

Есть типизированные указатели (аналоги: адрес квартиры, кабинета, номер телефона) и нетипизированные (широта, долгота и высота над уровнем моря). Типизированные указатели используются в основном для работы с массивами однотипных данных (например, положить в почтовый ящик каждого жильца дома №10 листовку с рекламой). Поэтому для таких указателей определены операторы: сложения/вычитания целых чисел и вычитание указателей того же типа (квартира №10 находится через 6 квартир от квартиры №4: №10 - №4 = 6, №4 + 6 = №10...). Например, у нас есть массив вещественных чисел. А так же есть типизированный указатель, на первый элемент этого массива. Увеличение на 1 этого указателя приведет к тому, что он станет указывать на следующий элемент массива. Уменьшение - на предыдущий. А если взять два указателя, которые указывают на данные в одном массиве, то можно узнать, на какое количество элементов нужно переместить первый, чтобы получить второй - это делается вычитанием указателей (если взять указатели на разные массивы, то результатом будет какое-то относительно случайное число). Пример:
Код
int a[100]; /* массив на 100 целых чисел */
int *p1; /* указатель на целое */
int *p2; /* второй указатель на целое */
int *p3; /* еще один указатель на целое */
int d; /* некое целое число */
p1 = &a[10]; /* p1 присваиваем адрес 10-го элемента массива a */
p2 = &a[64];
a[10] = 1; /* разыменование - присваивание 10-му элементу массива a значения 1 */
p1[0] = 1; /* тоже самое */
*(p1+ 0) = 1; /* тоже самое */
p3 = p1 + 10; /* p3 теперь указывает на 20-й элемент массива, аналогично: p3 = &a[20] */
p3 = p3 - 1; /* p3 == &a[19] */
++p3; /* p3 == &a[20] */
d = p2 - p1; /* разность указателей: d == 64 - 10 == 54 */
p3 += d; /* p3 == &a[74] */

Нетипизированные указатели используются для работы с целыми областями памяти, когда тип лежащих там данных не имеет значения. Например, используется функцией копирования блока памяти (memcpy). Для подобных указателей не определены ни арифметические операции, ни операция разыменования (нельзя получить непосредственный доступ к данным). Пример:
Код
void *p; /* нетипизированный указатель */
int x;
int *pi; /* типизированный указатель */
pi = &x; /* pi - указатель на x */
p = pi; /* p указывает на область памяти, в которой хранится переменная x */
*p = 0; /* ошибка - разыменование нетипизированного указателя */
Стандарт языка С++ гарантирует, что любой указатель может быть присвоен нетипизированному без потери информации. Именно поэтому, если по какой-то причине, нужно конвертировать указатель на один тип в указатель на другой, то делать это надо в 2 стадии, сначала сконвертировать в нетипизированный, а затем "типизировать" его в требуемый.

Можно создать указатель на любой тип. Для этого необходимо при его объявлении поставить звездочку перед именем переменной:
Код
int x;
int *px; /* указатель на int */
struct MyStruct;
struct MyStruct *ps; /* указатель на структуру MyStruct (для создания указателя достаточно объявления структуры!) */
int* *px; /* указатель на int* = указатель на указатель на int */
int a[10]; /* массив из 10 int */
int* ap[10]; /* массив из 10 указателей на int */
int (*pa)[10]; /* указатель на массив из 10 int */
int (*func)(void); /* указатель на функцию вида int myFunc(void) */
Немного остановимся на указателе на указатель. Многие не понимают, зачем это вообще нужно. Есть очень простой пример, иллюстрирующий их незаменимость. Когда функция принимает какие-то данные по значению, то создается их копия, и после выхода из функции, данные, ей переданные, останутся неизменными. Чтобы данные функция могла менять - ей нужно передать указатель на них. А что если функция должна менять именно указатель (например, функция добавления данных в массив с возможностью его расширения)? В этом случае и используется указатель на указатель.
Если нужно сделать typedef, то тут все очень просто - просто вместо имени переменной указываете название типа:
Код
typedef int *MyPointer;
typedef int (*MyArray)[10];
typedef int (*MyFunc)(void);
Только стоит иметь в виду, что:
Код
typedef char * string;
const string s;
s = "xxx";
Вызовет ошибку компиляции, в отличие от:
Код
const char *s;
s = "xxx";
Причина в том, что компилятор раскладывает string так:
Код
char * const s; //const string s;
Так как const это модификатор относящийся к переменной, а не к типу. На эти грабли наступил, например, разработчик библиотеки http://openil.sf.net.

Стандартные библиотеки языков Си и С++ поддерживают перевод значений указателей в строковый вид, удобный для восприятия человеком. В принципе, это не особо нужно, разве что для отладки. Со времен языка Си указатели на char используются в качестве указателей на строки, именно поэтому стандартным поведением оператора вывода в поток (С++, std::ostream) на подобный указатель является отображение строки.  Чтобы это избежать, указатель нужно привести к указателю на void: static_cast<const void*>(p)

Преобразование указателей (и не только) в С и С++ немного отличается. В первом существует только один способ: Type *x = (Type*)y; Более того, преобразование любого указателя к указателю на void и обратно происходит молча - без лишних телодвижений. В С++ немного по другому - существует аж 4 вида преобразований (плюс, сохранен вариант из С): static_cast (преобразование совместимых типов), dynamic_cast (преобразование указателя на базовый класс к указателю на потомок), const_cast (снятие модификатора const с указателя) и reinterpret_cast (любое преобразование без проверки корректности). Эти преобразования расположены в порядке убывания предпочтительности использования. Последний из них аналогичен варианту из языка С. К тому же автоматическое преобразования указателя на void в указатель на другой тип отменено. Для этого необходимо использовать: Type *x = static_cast<Type*>(y);

Ссылка была введена в С++ для упрощения передачи сложных структур в функции, чтобы избежать их копирования, которое сильно ухудшит скоростные показатели программы. Чтобы представить себе как она работает достаточно одного слова - псевдоним. Т.е. если у вас есть переменная x, то вы можете сделать ей псевдоним с именем y. Если вы измените y, то изменится и x, а если измените x, то аналогично изменится и y. Но нельзя сменить объект, с которым ассоциирована ссылка. Т.е. если вы сделали ссылку, как псевдоним для x, то вы не сможете уже сделать ее псевдонимом для z. Операция взятия адреса от ссылки приведет к тому, что будет возвращен адрес ассоциированного с ней объекта.
Пример использования ссылок:
Код
int x;
int &y = x; /* ссылка на x */
y = 0; /* теперь x == 0 */
++x; /* теперь y == 1 */
--y; /* теперь x == 0 */
int z = 10;
y = z; /* теперь x = 10 */
int *p;
p = &y; /* p == &x */
double &r; /* ошибка - нельзя объявлять ссылки без указания "цели" */
double &r = x; /* ошибка - нельзя ссылаться на неподходящий тип */
...
int func(double &r) /* правильно */
{
...
}
...
double d;
func(d); /* правильно */
func(x); /* ошибка, нельзя ссылаться на неподходящий тип */
...
int func2(const double &r)
{
...
}
...
func2(x); /* правильно, так как ссылка идет на константный объект, который будет создан путем конвертации int в double */
Скорость работы со ссылкой не ниже, чем с указателем (так как на машинном уровне реализация похожая, а зачастую вообще одинаковая - машинный код совпадает).

http://forum.vingrad.ru/index.php?show_type=forum&showtopic=269794&kw=faq-c++

Автор: EgoBrain 22.9.2009, 13:05
Можно продолжение по данной теме? Не затронуты такие аспекты как: указатель на указатель, передача в функцию указателя/указателя на указатель (пример был лишь с ссылкой), и чем отличается ссылка от указателя на уровне компилятора а не на логическом (если можно так выразиться).
Вот это, только Вашими словами:
Цитата(maxim1000 @ 22.9.2005,  00:56)
ссылка ближе к указателю, чем к пременной
в общем-то именно так она и реализована на уровне компилятора
просто везде, где она используется, компилятор дорисовывает звездочку smile
Код

int x;
int &y=x;
y++;
x=y;

при компиляции будет сначала переделано в такое:
Код

int x;
int *z=&x;
(*z)++;
x=(*z);

поэтому, когда мы пишем
Код

y=x;

на самом деле получается
Код

(*z)=x;

получается, что изменить значение адреса невозможно

единственное, чем отличается ссылка от указателя - инициализация:
Код

int &y=x;

означает занесение адреса x в y
и это навсегда (пока не выйдем из этого блока)...

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

Автор: zim22 22.9.2009, 13:24
Цитата(EgoBrain @  22.9.2009,  13:05 Найти цитируемый пост)
И еще хотелось бы доп. объяснений по всяким синтаксическим хитростям типа (*z)++ и другое изащренное использование скобок и звездочек.

все подобные хитрости очень легко "парсятся" человеком, если знать(посмотреть) таблицу С++ операторов, а именно их приоритет и ассоциативность (лево/право)

Автор: bsa 22.9.2009, 13:54
Цитата(EgoBrain @ 22.9.2009,  13:05)
Не затронуты такие аспекты как: указатель на указатель, передача в функцию указателя/указателя на указатель (пример был лишь с ссылкой), и чем отличается ссылка от указателя на уровне компилятора а не на логическом (если можно так выразиться).

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

Автор: zim22 22.9.2009, 14:08
Цитата(bsa @  22.9.2009,  13:54 Найти цитируемый пост)
Но если будет народ настаивать, то напишу.

я против. новичку это ни к чему.

Автор: ller 22.9.2009, 23:17
zim22, а не совсем новичкам? Тем кто уже общие аспекты знает, я думаю такая информация пригодится. Только указать  smile пионерам вход воспрещен )))

Автор: EgoBrain 24.9.2009, 00:53
Если кто-то посчитает для себя эту информацию лишней, то не будет ее читать. Что касается глубины изучения на первых этапах, то я скажу, что на самом деле изучая такую точную математическую науку как программирования очень сложно балансировать между конкретикой и абстракционизмом, иной раз приходится сильно углублятся чтобы что-то понять, просто вот мне например зачастую очень сложно изучать на абстрактном уровне, так как на этом остаются вопросы.

Автор: wrathchildtoo 24.9.2009, 14:44
Цитата(bsa @  22.9.2009,  13:54 Найти цитируемый пост)
А вот на счет необходимости рассмотрения ссылки с точки зрения компилятора я не уверен - зачем новичку забивать голову лишним? Но если будет народ настаивать, то напишу.

Было бы интересно узнать. Ну или хотя бы киньте ссылочку где почитать.

Автор: bsa 24.9.2009, 21:47
Цитата(wrathchildtoo @  24.9.2009,  14:44 Найти цитируемый пост)
Было бы интересно узнать. Ну или хотя бы киньте ссылочку где почитать.

Почитать можно в книжках по С++ или в http://lmgtfy.com/?q=различия+указатель+ссылка.

Автор: EgoBrain 25.9.2009, 00:45
Цитата(wrathchildtoo @ 24.9.2009,  14:44)
Цитата(bsa @  22.9.2009,  13:54 Найти цитируемый пост)
А вот на счет необходимости рассмотрения ссылки с точки зрения компилятора я не уверен - зачем новичку забивать голову лишним? Но если будет народ настаивать, то напишу.

Было бы интересно узнать. Ну или хотя бы киньте ссылочку где почитать.

Читайте закрепленную тему на этом же форуме! http://forum.vingrad.ru/articles/topic-60932.html

Добавлено через 1 минуту и 15 секунд
Просто я еще хотел услышать на этот счет мнение 
bsa

Автор: bsa 25.9.2009, 13:24
EgoBrain, немного подправил про ссылки. Более глубоко рассказывать тут желания у меня нет.

Автор: EgoBrain 27.9.2009, 03:11
Цитата(zim22 @ 22.9.2009,  13:24)
все подобные хитрости очень легко "парсятся" человеком, если знать(посмотреть) таблицу С++ операторов, а именно их приоритет и ассоциативность (лево/право)

Очень интересно, дайте таблицу на русском.

Автор: zim22 27.9.2009, 08:07
Цитата(EgoBrain @  27.9.2009,  03:11 Найти цитируемый пост)
Очень интересно, дайте таблицу на русском.

http://www.cplusplus.com/doc/tutorial/operators/

Автор: bsa 27.9.2009, 17:11

M
bsa
zim22EgoBrain, завязываем с оффтопиком.

Автор: unicuum 1.12.2009, 12:12
Цитата(bsa @  6.9.2009,  13:29 Найти цитируемый пост)

Можно создать указатель на любой тип. Для этого необходимо при его объявлении поставить звездочку перед именем переменной:

Вопрос в том, ставим ли мы звёздочку перед именем переменной или после указываемого типа. Я вот лично столкнулся с тем, что значки указателя (звёздочка) и ссылки (амперсанд) красивее выглядят присоединённых к типу, то есть не так:
Код

int *p;
а вот так
Код

int* p;

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

Автор: bsa 1.12.2009, 12:21
unicuum, не вижу смысла обсуждать подобные вещи. Это все из разряда, где ставить const - до названия типа или после, где ставить открывающую фигурную скобку, какой размер отступа нужно использовать... На вкус и цвет, как говорится...


M
bsa
предлагаю закончить с обсуждением предпочтений, слабо касающихся темы

Автор: baldina 1.12.2009, 12:58
bsa
Цитата

Скорость работы со ссылкой не ниже, чем с указателем. А зачастую выше, так как компилятор легко может ее оптимизировать. Пример:

Из примера не понятно, что там компилятор сможет "легко оптимизировать". и как это "легко" соотносится с 
Цитата

если оптимизация вообще не удалась

боюсь, для объяснения этого придется-таки поговорить о путях реализации

Автор: bsa 1.12.2009, 16:49
baldina, спасибо.
Подправил информацию про ссылки.

Автор: Tobuk 1.12.2009, 21:15
Так и не понял из темы зачем же нужны ссылки? Все же прекрасно получаеться с указателями.
Зачем использовать ссылки урезая тем самым себе возможности?

Автор: mes 1.12.2009, 22:14
Цитата(Tobuk @  1.12.2009,  20:15 Найти цитируемый пост)
Зачем использовать ссылки урезая тем самым себе возможности?

я бы перефразировал.. Ссылки ввели наряду с указателями, чтоб расширить возможности.
smile

Автор: unicuum 2.12.2009, 01:47
Цитата(Tobuk @  1.12.2009,  21:15 Найти цитируемый пост)
Зачем использовать ссылки урезая тем самым себе возможности?

Цитата(mes @  1.12.2009,  22:14 Найти цитируемый пост)
я бы перефразировал.. Ссылки ввели наряду с указателями, чтоб расширить возможности

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

Автор: mes 2.12.2009, 10:35
Цитата(unicuum @  2.12.2009,  00:47 Найти цитируемый пост)
от, имеем два противоположных мнения, пока что без намёка, что урезаем и что расширяем. 

Почему без намека.. Tobuk, предположил что если ссылки нельзя "переставить", а также "хранить в них нуль", то они являются урезаными указателями.
Однако тут забылось, что вышеуказанные два фактора нужны далеко не всегда, и во многих случаях будут заставлять программиста отвлекаться на лишние детали,
чтоб гарантировать безопасность кода.

Автор: unicuum 2.12.2009, 10:58
Вот так сразу вспоминается только то, что ссылки в операторах используют, для возвращения *this, и потом на основе полученного строят выражения. А что ещё?

Автор: mes 2.12.2009, 11:42
Цитата(unicuum @  2.12.2009,  09:58 Найти цитируемый пост)
что ссылки в операторах используют, для возвращения *this, и потом на основе полученного строят выражения. А что ещё?

Важно не то, где используются, а какую семантику они несут.

как например отнесетесь к такому коду ?
Код

class { void f() { if (!this) return; ...} };



Автор: Tobuk 2.12.2009, 21:21
Вот пример:
Код

void foo(int &hello);
void foo(int hello);
void foo(int *hello);

int i = 5;

foo(i);
foo(i);
foo(&i);


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

>Ссылки ввели наряду с указателями, чтоб расширить возможности.

Что-что?
Ссылки появились только в С++, а указатели были "открыты" еще задолго до C.
Не вижу никакой выгоды от использования ссылок.
Хотя я пишу на C и их не использую :-\

Автор: mes 2.12.2009, 22:24
Цитата(Tobuk @  2.12.2009,  20:21 Найти цитируемый пост)

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


В других "безопасных" языках (тот же паскаль) тоже не заметно, но никого не смущает..
Кстати в них тоже "ссылочная" а не "указательная" передача параметров.


Цитата(Tobuk @  2.12.2009,  20:21 Найти цитируемый пост)

Что-что?
Ссылки появились только в С++, а указатели были "открыты" еще задолго до C.

1. "наряду" это не "одновременно", а ближе к "в дополнение, на равных условиях"
2. ну и "ссылочность" как таковая изобретена задолго до C++ и даже до Си.. просто раньше называлось не передача по ссылке, а передача по имени.

Цитата(Tobuk @  2.12.2009,  20:21 Найти цитируемый пост)
Не вижу никакой выгоды от использования ссылок.

Ну а мне было бы мучительно без них ..
Преимущества (основные в семантическом плане) 100 раз обсуждались - поиск поможет 
smile

Автор: bsa 3.12.2009, 00:48
Tobuk, ссылки нужны, чтобы гарантировать единообразие передачи параметров. Например, есть функция, которая принимает объекты типа std::string. Так как операция их копирования довольно длительной может быть, то было принято решение использовать ссылку. В результате, пользователь функции может вызвать ее с параметром типа std::string или типа const char *... Попробуй тоже самое сделать с указателями - ничего не получится. Более того, выглядеть будет как-то стремно, я уж не говорю о том, что код будет потенциально более опасен. Впрочем, Сишнику этого не понять.

Автор: baldina 3.12.2009, 11:44
Цитата

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

неправда
Код

void foo(/* by ref */ int& hello);
void foo(/* by value */ int const& hello);

Автор: djamshud 3.12.2009, 12:04
>void foo(/* by value */ int const& hello);

Ну щас.. Это тоже передача по ссылке. А чел кстати говорил про неясность в вызывающей функции:
call(somevar);
не ясно, изменит ли (гипотетически) call значение somevar или нет. Поэтому всегла, когда предполагается изменение передаваемых параметров, использую указатель. А ссылки рулят в случае ссылаемости на константу. Объекты передаются без копирования, а выглядит это, как обыный вызов.

Автор: baldina 3.12.2009, 12:24
технически - по ссылке, но семантически - по значению, т.к. значение аргумента изменить невозможно.
Цитата

не ясно, изменит ли (гипотетически) call значение somevar или нет

есть простой способ избежать неясностей - всегда использовать функциональный стиль, т.е. все результаты передаются через возвращаемое значение, аргументы не изменяются.
Цитата

call(somevar);

это могучий пример, но в реальном коде проблема носит скорее гипотетический характер: обычно тот, кто пишет/анализирует код, бывает в курсе, что и как делает call()  smile 

Автор: mes 3.12.2009, 21:07
Цитата(djamshud @  3.12.2009,  11:04 Найти цитируемый пост)
чел кстати говорил про неясность в вызывающей функции:
call(somevar);

вроде уже писал, но шас не нашел :
1. если человек знает сигнатуру функции то проблем нет, а если не знает, то что он вообще туда передает ?!
2. ну и названия функций следует подбирать так, чтоб было ясно, что может произойти внутри с аргументами.


Автор: bsa 4.12.2009, 01:22

M
bsa
Давайте всё-таки вернёмся к теме обсуждения.

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)