Указатели в C/C++ для начинающих Что такое указатели? Указатели — это те же переменные. Разница в том, что вместо того, чтобы хранить определенные данные, они хранят адрес (указатель), где данные могут быть найдены. Концептуально это очень важно. Многие программы и идеи зависят от указателей, как от основы их архитектуры, например, связанные списки (linked lists). Введение Как объявить указатель? Собственно, так же, как и любую другую переменную, но с добавлением звездочки перед именем переменной. Так, например, следующий код создает два указателя, которые указывают на целое число. int *pNumberOne; int *pNumberTwo; Обратили внимание на префикс "p" в обоих именах переменных? Это принятый способ обозначить, что переменная является указателем. Так называемая венгерская нотация. Теперь давайте сделаем так, чтобы указатели на что-нибудь указывали: pNumberOne = &some_number; pNumberTwo = &some_other_number; Знак & (амперсанд) следует читать как "адрес переменной ..." и означает адрес переменной в памяти, который будет возвращен вместо значения самой переменной. Итак, в этом примере pNumberOne установлен и содержит адрес переменной some_number, также pNumberOne указывает на some_number. Таким образом, если мы хотим получить адрес переменной some_number, мы можем использовать pNumberOne. Если мы хотим получить значение переменной some_number через pNumberOne, нужно добавить звездочку (*) перед pNumberOne (*pNumberOne). Звездочка (*) разыменовывает (превращает в саму переменную) указатель и должна читаться как "место в памяти, которое указывается через ...", кроме объявлений, как в строке int *pNumber. Чему мы научились: Пример Фух! Многовато для начала, не правда ли? Я бы рекомендовал, если вы не понимаете то, что описано выше, перечитать ещё раз. Указатели - дело непростое и понимание их концепции может занять определенное время. Вот пример, который показывает идеи, изложенные выше. Пример написан на C, без дополнений C++. #include void main() { // объявляем переменные: int nNumber; int *pPointer; // инициализируем объявленные переменные: nNumber = 15; pPointer = &nNumber; // выводим значение переменной nNumber: printf("nNumber is equal to : %d\n", nNumber); // теперь изменяем nNumber через pPointer: *pPointer = 25; // убедимся что nNumber изменил свое значение в результате предыдущего действия, // выведя значение переменной ещё раз printf("nNumber is equal to : %d\n", nNumber); } Внимательно просмотрите код выше и скомпилируйте. Убедитесь в том, что вы понимаете, почему он работает. Потом, если вы готовы, читайте дальше! Ловушка! Попробуйте найти ошибку в этой программе: #include int *pPointer; void SomeFunction() { int nNumber; nNumber = 25; // делаем так, чтобы pPointer указывал на nNumber pPointer = &nNumber; } void main() { SomeFunction(); // сделайте чтобы pPointer указывал на что нибудь // почему это не работает? printf("Value of *pPointer: %d\n", *pPointer); } Программа сначала вызывает функцию SomeFunction, которая создает переменную nNumber, а потом инициализирует pPointer, чтобы он указывал на nNumber. Далее начинаются проблемы. Когда функция завершается, nNumber удаляется, поскольку это локальная переменная. Локальные переменные всегда удаляются, когда выполнение программы выходит из блока, где эти переменные были объявлены. Это означает, что когда функция SomeFunction завершается и выполнение возвращается в main(), переменная исчезает. Таким образом pPointer указывает на ту область памяти, где была переменная, но которая уже не принадлежит программе. Если вы не совсем поняли, о чем идет речь, ещё раз прочтите о локальных и глобальных переменных, а также об областях определения (scope). Эта концепция также очень важна. Итак, как проблема может быть решена? Ответ - использованием техники известной как динамическое выделение памяти. Обратите внимание, что в языке C такой техники нет, а пример ниже относится к C++. Динамическое выделение памяти Динамическое выделение памяти, возможно, является ключом к указателям. Она используется для того, чтобы выделить память без необходимости объявлять переменные, а затем создавать указатели, указывающие на эти переменные. Все же концепция может показаться сначала запутанной, но она очень проста. Код ниже демонстрирует, как выделить память для целого числа: int *pNumber; pNumber = new int; Первая строчка объявляет указатель pNumber. Вторая строчка выделяет память для целого числа (int) и указывает pNumber на эту область памяти. Вот ещё один пример, на этот раз с числом с двойной точностью (double). double *pDouble; pDouble = new double; Формула остается одной и той же, так что вам вряд ли удастся ошибиться. Но, что отличает динамическое выделение так это то что память которая была выделена не удаляется после завершения работы функции или текущего блока кода. Так что если вы перепишете предыдущий пример используя динамическое выделение - вы увидите что теперь он работает: #include int *pPointer; void SomeFunction() { // pPointer указывает на целое число pPointer = new int; *pPointer = 25; } void main() { SomeFunction(); //инициализируем указатель чтобы он на что то указывал printf("Value of *pPointer: %d\n", *pPointer); } Прочитайте и скомпилируйте приведеный пример. Убедитесь что вы понимаете как и почему он работает. Когда вызывается SomeFunction, она выделяет память и инициализирует pPointer чтобы он указывал на неё. В этот раз когда функция завершается выделенная память остается нетронутой и pPointer всё ещё указывает на неё. На этом всё про динамическое выделение памяти! Память приходит, память уходит С памятью всегда существуют сложности и в данном случае довольно серьезные, но эти сложности можно с легкостью обойти. Проблема заключается в том что не смотря на то что память которая была динамически выделена остается нетронутой она никогда не освобождается автоматически. Память будет оставаться выделенной до тех пор пока вы не скажете компьютеру что вам она больше не нужна. Проблема в том что если вы не скажете системе что память вам больше не нужна, она будет занимать место которое возможно необходимо другим приложениям либо частям вашего приложения. В частности это может привести к сбою системы по причине использования всей доступной памяти, поэтому это очень важно. Освобождение памяти когда она вам больше не нужна делается очень просто: delete pPointer; С этим всё. Вы должны быть осторожны поскольку нужно передавать правильный указатель, указатель который который указывает именно на ту область памяти которую вы выделяли а не на непонятный мусор. Попытка освободить память (при помощи delete) которая уже была освобождена опасно и может привести к сбою программы. По-этому следующий обновленный пример показывает как это делать правильно без траты памяти: #include int *pPointer; void SomeFunction() { // делаем так чтобы pPointer указывал на целое число pPointer = new int; *pPointer = 25; } void main() { SomeFunction(); // указываем pPointer на что нибудь printf("Value of *pPointer: %d\n", *pPointer); delete pPointer; } Разница в одну строку - это все, что сделано, но это важная строка. Если вы не удалите память, то получите так называемую "утечку памяти", когда память постепенно утекает и не может быть возвращена, пока приложение не закроется. Передача указателей в функции Возможность передавать указатели в функции очень полезна, и ее легко освоить. Если нам нужна программа, которая получает число и прибавляет к нему пять, мы можем написать что-то похожее на этот код: #include void AddFive(int Number) { Number = Number + 5; } void main() { int nMyNumber = 18; printf("My original number is %d\n", nMyNumber); AddFive(nMyNumber); printf("My new number is %d\n", nMyNumber); } Однако здесь проблема в том, что переменная Number, к которой мы обращаемся внутри функции - это копия переменной nMyNumber, передаваемой в функцию. Таким образом, строка Number = Number + 5 добавляет пять к копии переменной, оставляя оригинальную переменную в main() неизменной. Попробуйте запустить программу, чтобы убедиться в этом. Чтобы избавиться от этой проблемы, мы можем передавать в функцию указатель на место в памяти, где хранится число, но тогда мы должны поправить функцию, чтобы она принимала указатель вместо числа. Для этого изменим void AddFive(int Number) на AddFive(int* Number) добавлением звездочки. Здесь снова текст программы, с внесенными изменениями. Обратите внимание, чт мы должны убедиться, что передаем в функцию адрес nMyNumber вместо самого числа. Это сделано при помощи оператора &, который (как вы помните), читается как "получить адрес". #include void AddFive(int *Number) { *Number = *Number + 5; } void main() { int nMyNumber = 18; printf("My original number is %d\n", nMyNumber); AddFive(&nMyNumber); printf("My new number is %d\n", nMyNumber); } Попробуйте придумать свой собственный пример для демонстрации этих возможностей указателей. Обратили внимание на важность звездочки (*) перед Number в функции AddFive? Это необходимо для того чтобы указать компилятору что мы хотим добавить 5 к числу на которое указывает переменная Number, а не добавить 5 к самому указателю. Последнее замечание по поводу функций это то что вы можете возвращать из них указатели, это делается так: int *MyFunction(); В этом примере, MyFunction возвращает указатель на целое число (int). Указатели на классы Есть ещё парочка предостережений, одно из них это структуры и классы. Вы можете задать класс как показано ниже: class MyClass { public: int m_Number; char m_Character; }; Далее вы можете объявить переменную типа MyClass следующим образом: MyClass thing; Вы уже должны это знать. Если нет, перечитайте снова текст выше. Для того чтобы объявить указатель на MyClass, вы сделаете следующее: MyClass *thing; ... как вы и ожидали. Далее вы выделяете память и инициализируете указатель чтобы он указывал на неё: thing = new MyClass; Тут то и начинаются проблемы. Как вы будете дальше использовать этот указатель? Да, в обычной ситуации вы бы написали thing.m_Number, но вы не можете, потому что это не MyClass, а только указатель на него. Так, thing сам посебе не содержит переменной m_Number, это структура на которую он указывает которая содержит m_Number. Соответственно мы должны использовать другую систему доступа. Для этого точка (.) заменяется на черту со знаком больше (->). Следующий пример показывает как: class MyClass { public: int m_Number; char m_Character; }; void main() { MyClass *pPointer; pPointer = new MyClass; pPointer->m_Number = 10; pPointer->m_Character = 's'; delete pPointer; } Указатели на массивы Вы также можете создавать указатели которые указывают на массивы. Это делается так: int *pArray; pArray = new int[6]; Этот пример создает указатель pArray и направляется его на масив из шести элементов. Другой способ, без использования динамического выделения, выглядит так: int *pArray; int MyArray[6]; pArray = &MyArray[0]; Обратите внимание что вместо написания &MyArray[0], вы можете просто написать MyArray. Это, конечно же, применимо только к массивам по причине способа их реализации в языках C/C++. По общим правилам необходимо было бы написать pArray = &MyArray, но это неправильно. Если вы так напишите то получите указатель на указатель на массив (не опечатка), что определенно не то что вам нужно. Использование указателей на массивы Если у вас есть указатель на масив, как его использовать? Например у вас есть указатель на массив целых чисел (int[]). Указатель изначально будет указывать на первое значение в массиве как показывает следующий пример: #include void main() { int Array[3]; Array[0] = 10; Array[1] = 20; Array[2] = 30; int *pArray; pArray = &Array[0]; printf("pArray points to the value %d\n", *pArray); } Для того чтобы переместить указатель к следующему элементу массива, мы можем сделать pArray++. Мы также можем, как некоторые из вас могли уже догадатьтся, сделать pArray+2, что передвинет указатель сразу на 2 элемента. С чем нужно быть осторожным так это с верхней границей массива (в данном случае это 3 элемента), потому что компилятор не может проверить вышли ли вы за предел массива используя указатели. Вы легко можете получить полный сбой системы если будете не аккуратны. Вот ещё один пример, на этот раз показывающий три значения которые мы установили: #include void main() { int Array[3]; Array[0] = 10; Array[1] = 20; Array[2] = 30; int *pArray; pArray = &Array[0]; printf("pArray points to the value %d\n", *pArray); pArray++; printf("pArray points to the value %d\n", *pArray); pArray++; printf("pArray points to the value %d\n", *pArray); } Мы также можем двигать указатель в любую сторону, так pArray - 2 это 2 элемента от того места куда указывает указатель. Убедитесь что вы добавляете и вычитает значение указателя а не у значения на которое он указывает. Данный метод использования указателей и массивов более всего полезен при использовании циклов, таких как for или while. Заметим так же , что если мы имеем указатель на значение, например int* pNumberSet, мы можем обращаться к нему как к массиву. Вот пример, pNumberSet[0] равно *pNumberSet; а pNumberSet[1] равно *(pNumberSet + 1). Еще одно важное замечание по массивам состоит в том, что если мы выделяем память при помощи new, как в таком примере: int *pArray; pArray = new int[6]; ...то выделенная таким образом память должна освобождаться следующей коммандой: delete[] pArray; Обратим внимание на [] после delete. Это указание компилятору о том, что удаляется целый массив, а не отдельный элемент. Мы должны использовать этот метод всякий раз при удалении массивов, иначе у нас возникнет утечка памяти. Заключение Последнее замечание: мы не должны удалять память, которую не выделили ранее с использованием new, как в следующем примере: void main() { int number; int *pNumber = number; delete pNumber;// ошибка - память для *pNumber не была выделена при помощи new. } Общие вопросы и ЧаВо Вопрос: Почему я получаю ошибки "symbol undefined" при использовании new и delete? Ответ: вероятнее всего, это происходит потому, что ваш исходный код интерпретируется компилятором как написанный на языке C. Операторы new и delete являются новыми возможностями языка C++. Ситуацию обычно можно исправить, указав для исходных файлов расширение *.cpp вместо *.c Вопрос: в чем разница между new и malloc? Ответ: Оператор new сушествует только в языке C++ и является стандартным (за исключением особых функций в Windows) способом выделения памяти. Вы не должны использовать malloc в C++, разве что в случае крайней необходимости. Поскольку malloc не был разработан для объектно ориентированных возможностей языка C++, его использование для выделения памяти для классов приведет к тому что конструктор класса вызван не будет, как пример того какие проблемы могут возникнуть. В результате проблем возникших при использовании malloc и free, а также потому что они устарели для любого использования (в языке C++) в этой статье они не обсуждаются детально. Я не одобряю использование этих функций где бы то нибыло. Вопрос: можно ли использовать free и delete вместе? Ответ: Вы должны освобождать память тем же механизмом которым вы её выделяли. Например используйте free только в случае если память выделялась функцией malloc, delete используется только если память выделялась при помощи new, и так далее. Ссылки Ссылки определенно находятся вне темы этой статьи. Но поскольку меня очень часто спрашивали о ссылках люди которые читали эту статью я опишу их кратко. Ссылки очень схожи с указателями и в большенстве случаем они могут быть использованы как альтернатива указателям. Как вы помните я уже писал что амперсанд (&) читается при объявлении как "адрес переменной ...". В случае присутствия амперсанда в объявлении в том виде который показан ниже - его следует читать как "ссылка на переменную ...". int& Number = myOtherNumber; Number = 25; Ссылки это те же что и указатель на myOtherNumber, за исключение того что ссылка автоматически разименовывается. Таким образом ссылка ведет себя как переменная со значением а не как переменная с адресом (как указатель). Идентичный по смыслу код, используя указатели показан ниже: int* pNumber = &myOtherNumber; *pNumber = 25; Другое отличие между указателями и ссылками это то что вы не можете "сбросить" ссылку. Это означает что нельзя изменять значение адреса ссылки после того как она была инициализирована. Например, код ниже выведет "20": int myFirstNumber = 25; int mySecondNumber = 20; int &myReference = myFirstNumber; myReference = mySecondNumber; printf("%d", myFristNumber); При использовании в классе, значение ссылки должно быть установлено в конструкторе следующим образом: CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable) { // код конструктора } Заключение Тему указателей сложно освоить с первого раза, так что стоит просмотреть всё заново. Большинство людей не понимает всё с первого прочтения. Вот основные моменты снова: Указатели это переменные которые указывают на область памяти. Вы объявляете указатель добавляя звездочку (*) перед именем переменной (int * number). Вы можете получить адрес переменной добавив амперсанд (&) перед её именем, например pNumber = &my_number. Звездочка при объявлении переменной (например int * number), читается как "адрес памяти который указывается переменной ...". Амперсанд при объявлении переменной (например int &number), читается как "адрес переменной ..." Вы можете выделять память используя оператор new. Указатели ДОЛЖНЫ быть того же типа что и переменные на которые вы хотите чтобы они указывали. Например int *number не будет указывать на MyClass. Вы можете передавать указатели в функции. Вы должны удалит память которая была выделена при помощи оператора delete. Вы можете получить указатель на массив который уже существует следующим образом: &array[0];. Вы должны удалить массив память для которого была динамически выделена следующим образом: delete[], а не просто delete. Это не абсолютно полное руководство по работе с указателями. Есть еще много вещей, которые я мог бы раскрыть более детально, таких как указатели на указатели, и тем, которых я не коснулся вообще, например, функциональных указателей, которые слишком сложны для этой статьи. Так же есть вещи, которые используются слишком редко, чтобы сбивать начинающих с толку обилием деталей. Вот и все! Попробуйте запустить код представленый в этой статье и придумать ещё какие нибудь свои вариации и примеры. ------------------------------------------------------------------------------- http://translated.by/you/a-beginner-s-guide-to-pointers/into-ru/trans/ © Andrew Peace Original (English): A Beginner's Guide to Pointers (http://www.codeproject.com/KB/cpp/pointers.aspx) Translation: © Mr.ElectroNick, Someone.else, Grigoriy, m0nhawk, Dmitry-Leushin, ventalf, voxpuibr, Axx, dShadow. License: The Code Project Open License (CPOL) translated.by crowd