Любой новичок, который решил изучить язык Си или С++, почти сразу же сталкивается с непониманием смысла "указателей" и "ссылок".
Указатель - это переменная или константа, содержащая адрес памяти, начиная с которого могут располагаться данные определенного типа (достаточно представить бумажку, на которой написан домашний адрес). Указатель не несет в себе информации о количестве данных (возьмите бумажку и попытайтесь по ней определить, сколько домов находится на той же улице). Можно получить адрес любой переменной (у всех домов есть адрес, не так ли?) и присвоить его подходящему указателю (номер телефона - на страницу телефонной книги, адрес - в адресной, 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++ |