Указатели для начинающих (Pointers)
ОГЛАВЛЕНИЕ
Подготовка
Каким способом определить указатель? Таким же, как и любую другую переменную, за исключением того, что необходимо добавить символ звездочки перед именем. К примеру, следующий код создает два указателя, оба из них указывают на значение типа integer:
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. Символ * разыменовывает указатель и читается как "распределенная память, на которую ссылается", и декларируется в виде int *pNumber.
Пример
Указатели - это сложная тема, и вам стоит четко и точно понимать их, потому что понадобится какое-то время, прежде чем вы сможете ими легко управлять. Вот пример, который демонстрирует приведенные выше принципы. Он написан на языке C, без расширений C++.
#include <stdio.h>
void main()
{
// объявление переменных:
int nNumber;
int *pPointer;
// теперь мы придаем им значения:
nNumber = 15;
pPointer = &nNumber;
// выводим значение nNumber:
printf("nNumber равно : %d\n", nNumber);
// теперь мы изменяем значение nNumber посредством pPointer:
*pPointer = 25;
// докажем, что nNumber изменилось в результате указанного
// выше кода и выведем снова значение:
printf("nNumber равно : %d\n", nNumber);
}
Изучите и скомпилируйте указанный код. Убедитесь в том, что вы понимаете как он работает и после продолжайте читать статью.
Ловушка
Прочтите следующий код и постарайтесь найти погрешность:
#include <stdio.h>
int *pPointer;
void SomeFunction();
{
int nNumber;
nNumber = 25;
// pPointer с указанием на nNumber:
pPointer = &nNumber;
}
void main()
{
SomeFunction(); // заставляем pPointer указывать на что-нибудь
// почему это не сработало ?
printf("Значение *pPointer: %d\n", *pPointer);
}
Данная программа сначала вызывает функцию SomeFunction , которая создает переменную под названием nNumber и затем создает указатель на нее pPointer. Тем не менее, далее начинаются проблемы. При выходе из функции nNumber удаляется потому, что она является локальной переменной. Локальные переменные всегда удаляются при выходе из блока, где они были определены. Это означает, что когда SomeFunction возвращается в main(), переменная будет удалена. Итак, pPointer указывает на место, где была переменная, но которая уже не принадлежит данной программе. Если вам это непонятно, то вам стоит изучить тему локальных и глобальных переменных, а также области их действий - данный принцип также очень важен.
Итак, как можно решить эту проблему? Ответом будет использование метода под названием динамическое распределение. Помните, что оно отличается в C и в C++. Поскольку большинство разработчиков используют C++, то мы будем использовать его в последующих примерах.
Динамическое распределение
Динамическое распределение наверняка является ключевой темой по отношению к указателям. Оно используется для того, чтобы распределить память без необходимости в определении переменных и затем заставить указатели указывать на них. Хотя данный принцип может показаться запутывающим, на самом деле он прост. Следующий код распределяет память под тип integer:
int *pNumber;
pNumber = new int;
Первая строка объявляет указатель pNumber. Вторая строка распределяет память для типа integer и затем заставляет pNumber указывать на новую память. Вот еще один пример с использованием типа double:
double *pDouble;
pDouble = new double;
Формула одинакова во всех случаях, поэтому тут ошибиться сложно. Динамическое распределение отличается тем, что память не удаляется при возврате функции или когда выполнение выходит за рамки текущего блока. Если мы перепишем указанный выше пример с использованием динамического распределения, то теперь все будет работать как нужно:
#include <stdio.h>
int *pPointer;
void SomeFunction()
{
// заставляем pPointer указывать на новое значение типа integer
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction(); // заставляем pPointer указывать на что-нибудь
printf("Значение *pPointer: %d\n", *pPointer);
}
Изучите и скомпилируйте указанный выше код. Убедитесь в том, что вы понимаете, как он работает. Когда вызывается SomeFunction, она распределяет некоторую память и заставляет pPointer указывать на нее. На этот раз, когда функция возвращается, память остается действительной, поэтому pPointer все еще указывает на что-то пригодное. И это все, что касается динамического распределения! Вам стоит понять и разобраться в этом, и только тогда продолжать читать данную статью, где мы расскажем про другую ошибку в данном коде.
Распределение и удаление памяти
Есть некоторые вещи, которые имеют серьезные последствия, хотя их же можно с легкостью исправить. Проблема заключается в том, что хотя память, которую вы динамически распределили, сохраняет свои значения, она на самом деле не будет автоматически удалена. То есть память будет занята до того, как вы скажете компьютеру о том, что вам она больше не нужна. Проблемы появляются в том случае, если вы не оповещаете компьютер о том, что она вам не нужна, и место, которое могло быть использовано другими программами или частями вашего приложения, будет занято. Со временем это приведет к системным сбоям, когда вся память будет исчерпана, поэтому все это очень важно. Освобождение памяти не сложная процедура:
delete pPointer;
И это все. Тем не менее, вам стоит быть осторожным и удостовериться, что вы передали верный указатель, то есть указатель, который на самом деле указывает на распределенную память, а не на какой-то мусор. Попытка удалить ( delete ) память, которая уже была освобождена опасна и может привести к отказу системы. Вот пример, который не приводит к утере памяти:
#include <stdio.h>
int *pPointer;
void SomeFunction()
{
//заставляем pPointer указывать на новое значение типа integer
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction();// заставляем pPointer указывать на что-нибудь
printf("Значение of *pPointer: %d\n", *pPointer);
delete pPointer;
}
Различие всего лишь в одной, но очень важной строке. Если вы не освободите память, то у вас будет так называемая "утечка памяти", когда память потихоньку утекает и не может быть использована, пока приложение не будет закрыто.
Передача указателей и функции
Возможность передачи указателей функциям может быть очень полезной, а также очень простой. Если необходимо сделать программу, которая добавляет пять к числу, то мы можем написать что-то похожее на следующий код:
#include <stdio.h>
void AddFive(int Number)
{
Number = Number + 5;
}
void main()
{
int nMyNumber = 18;
printf("Оригинальное число равно %d\n", nMyNumber);
AddFive(nMyNumber);
printf("Оригинальное число равно %d\n", nMyNumber);
}
Тем не менее, проблема заключается в том, что Number , к которому ссылаются в AddFive является копией переменной nMyNumber , переданной в функцию, а не самой переменной. Тем самым строка Number = Number + 5 добавляет пять к копии этой переменной, при этом оригинальная переменная в main() не будет изменена. Попробуйте запустить программу и убедитесь в этом сами.
Чтобы обойти данную проблему, мы можем передать в функцию указатель того места, где данное число содержится, тем самым она будет ожидать указатель на число, а не число. Для этого мы изменим void AddFive(int Number) на void AddFive(int* Number), добавляя звездочку. Вот данная программа с внесенными изменениями. Заметьте, что мы должны убедиться, что передаем адрес nMyNumber вместо самого числа. Это выполняется посредством добавления символа & , который (как вы уже знаете) читается как "по адресу".
#include <stdio.h>
void AddFive(int* Number)
{
*Number = *Number + 5;
}
void main()
{
int nMyNumber = 18;
printf("Оригинальное число равно %d\n", nMyNumber);
AddFive(&nMyNumber);
printf("Оригинальное число равно %d\n", nMyNumber);
}
Попробуйте придумать свой пример использования передачи указателей в функцию. Заметьте важность использования символа * до Number в функции AddFive. Это необходимо для указания компилятору, что мы хотим добавить пять к числу, на которое указывает переменная Number, а не добавлять пять к самому указателю. Наконец, последнее, что стоит упомянуть о функциях, так это то, что вы можете возвращать указатели из них примерно так:
int * MyFunction();
В данном примере MyFunction возвращает указатель на integer.
Указатели на классы
Существует парочка других препятствий с указателями, одним из которых является структура классов. Вы можете указать класс следующим образом:
class MyClass
{
public:
int m_Number;
char m_Character;
};
Затем вы можете определить переменную типа MyClass:
MyClass thing;
Все это вы уже знаете. А если нет, то вам стоит немного почитать про массивы. Чтобы определить указатель на MyClass вам необходимо использовать:
MyClass *thing;
...как вы и прочитали в начале статьи. Затем вы можете распределить некоторую память и заставить данный указатель ссылаться на память:
thing = new MyClass;
Тут вы встретите следующую проблему - как же использовать указатель? Вы могли бы написать thing.m_Number, но в случае с указателями thing не является 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 <stdio.h>
void main()
{
int Array[3];
Array[0] = 10;
Array[1] = 20;
Array[2] = 30;
int *pArray;
pArray = &Array[0];
printf("pArray указывает на значение %d\n", *pArray);
}
Для того, чтобы указатель перешел на следующее значение в массиве, мы можем применить pArray++. Мы также можем, как вы наверняка поняли, написать pArray + 2, что передвинет указатель на два элемента. Вам стоит быть осторожным, чтобы не перейти за границу массива (в данном случае это 3-е значение), поскольку компилятор не может проверить вышли ли вы за границы в случае, если вы используете указатели. Вы можете привести систему к полному отказу. Данный пример демонстрирует все три значения, которые мы устанавливали:
#include <stdio.h>
void main()
{
int Array[3];
Array[0] = 10;
Array[1] = 20;
Array[2] = 30;
int *pArray;
pArray = &Array[0];
printf("pArray указывает на значение %d\n", *pArray);
pArray++;
printf("pArray указывает на значение %d\n", *pArray);
pArray++;
printf("pArray указывает на значение %d\n", *pArray);
}
Вы также можете вычитать значения, тем самым pArray - 2 приведет вас на 2 элемента назад относительно той позиции, куда указывает текущее значение pArray. Тем не менее, удостоверьтесь, что вы добавляете и вычитаете из указателя, а не из его значения. Данная манипуляция, использующая указатели и массивы, наиболее полезна при использовании циклов, таких как while.
Заметьте, что если у вас есть указатель на значение, то есть int* pNumberSet, то вы можете обращаться к нему как к массиву. К примеру, pNumberSet[0]эквивалентно *pNumberSet; аналогично pNumberSet[1] эквивалентно *(pNumberSet + 1).
Последнее, о чем мы вас хотим предупредить относительно массивов, заключается в том, что если вы распределяете памятть для массива используя new, как это показано в следующем примере:
int *pArray;
pArray = new int[6];
...то он должен быть удален посредством следующей команды:
delete[] pArray;
Заметьте квадратные скобки [] после delete. Это указывает компилятору на то, что удаляется весь массив, а не один его элемент. Вы должны использовать данный метод в случаях, когда вы используете массивы, в противном случае у вас будет наблюдаться утечка памяти.
Послесловие
Последняя заметка - вы должны удалять память, которую вы не распределяли, следующим образом:
void main()
{
int number;
int *pNumber = number;
delete pNumber; // неверно - *pNumber не был расположен посредством команды new.
}
Частые вопросы и ответы
В: Почему появляется ошибка "symbol undefined" при использовании new и delete?
О: Это, скорее всего, вызвано тем, что исходный файл считывается компилятором как простой C-файл. Команды new и delete являются новой возможностью C++. Чтобы ошибка исчезла вам просто необходимо изменить расширение файла на *.cpp.
В: В чем различия между new и malloc?
О: new - это ключевое слово, которое существует только в C++ и теперь является стандартом для распределения памяти. Вам не рекоммендуется использовать malloc в пределах приложений C C++ если у вас для этого нет особых причин. Поскольку malloc не был разработан для объектно-ориентированных функций C++, использование данной команды для распределения памяти классов предотвратит вызов конструктора классов в качестве одной из возможных проблем. В данной статье команды malloc и free не обсуждались по причине того, что они могут вызвать проблемы и являются устаревшими, а также мы рекомендуем не прибегать к их использованию.
В: Можно ли использовать 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)
{
// код конструктора располагается здесь
}
Вывод
Данная тема очень сложна для понимания, потому вам стоит уделить ей немало внимания. Многие люди не понимают ее с первого раза, поэтому мы ещё раз приведем основные идеи:
1. Указатели - это переменные, которые указывают на область в памяти. Указатели объявляются посредством символа (*) перед названием переменной (int *number).
2. Вы можете получить адрес любой переменной добавив амперсанд (&) перед ней, то есть pNumber = &my_number.
3. Символ звездочки, если он не поставлен в объявлении (к примеру так int *number), должен читаться как "область памяти, на которую указывает".
4. Амперсанд, если не указан в определении (к примеру так int &number), читается как "по адресу".
5. Вы можете распределять память посредством ключевого слова new.
6. Указатели должны быть такого же типа, как и переменные, на которые они будут ссылаться; поэтому, int *number не будет указывать на MyClass.
7. Указатели можно передавать функциям.
8. Память необходимо освобождать используя ключевое слово delete.
9. Вы можете создать указатель на массив, который уже существует, посредством написания &array[0];.
10. Необходимо удалить массив, который был динамически распределен, посредством оператора delete[], а не просто delete.
Данная статья не является полноценным гидом по указателям - существуют другие вещи, которые также можно было обсудить в деталях, такие, например, как указатели на указатели, или те вещи, которые не стоит упоминать вообще, такие как указатели функций, что слишком сложно для начинающих. Новичкам мы рекомендуем попробовать приведенные примеры и набраться опыта в использовании указателей.
Автор: Andrew Peace