Когда компилятор обрабатывает оператор определения переменной, например int i=10;, он выделяет память в соответствии с типом (int) и инициализирует её указанным значением (10). Все обращения к переменной по её имени (i) заменяются компилятором на адрес области памяти, в которой хранится значение переменной. Программист может определить собственные переменные для хранения адресов областей памяти. Такие переменные называются указателями.
Приведём примеры использования указателей:
- доступ к элементам массива;
- передача аргументов в функцию, от которой требуется изменить эти аргументы;
- передача в функцию массивов и строковых переменных;
- динамическое выделение памяти;
- создание сложных структур данных (связные списки, бинарные деревья и т.д.).
Идея указателей заключается в следующем. Каждый байт компьютера имеет адрес. Таким образом, каждая переменная и каждая функция программы начинается с какого-либо конкретного адреса. Для получения адреса переменной можно использовать операцию получения адреса &. Необходимо понимать, что адреса переменных – это не то же самое, что их значение. Также необходимо помнить, что операция получения адреса переменной (&var1) и операция ссылки в определении функции (int& var1) – это две разные операции.
Переменная, содержащая в себе значение адреса, называется указателем. Рассмотрим следующий пример.
int main() {
int var1 = 11; // две переменные
int var2 = 22;
cout << &var1 << endl // покажем адреса переменных (увидим: 7208448 7208444)
<< &var2 << endl << endl;
int* ptr; // это переменная-указатель на целое
ptr = &var1; // присвоим ей значение адреса var1
cout << ptr << endl; // и покажем на экране (увидим: 7208448)
ptr = &var2; // теперь значение адреса var2
cout << ptr << endl; // и покажем на экране (увидим: 7208444)
getch(); return 0; }
В данной программе, в строке int* ptr; определена переменная ptr как указатель на int, т.е. эта переменная может содержать в себе адрес переменной типа int. Итак, указатели предназначены для хранения адресов областей памяти. Как видно из данного примера, указатель может хранить адрес переменной соответствующего типа.
Вместо вывода на дисплей адресов, хранящихся в переменной ptr, можно вывести значения, хранящиеся по адресу, на который указывает ptr. Для этого достаточно изменить строку, например, cout << ptr << endl; на cout << *ptr << endl; //увидим 11, если ptr указывает на var1. Выражение *ptr позволяет получить значения переменных var1 и var2 и называется операцией разыменовывания (разадресации), которая означает: взять значение переменной, на которую указывает указатель.
Указатели можно использовать не только для получения значения переменной, на которую он указывает, но и для выполнения действий с этими переменными. Рассмотрим следующий пример.
int main ( ) {
int var1, var2; // две переменные
int* ptr; // указатель на целое
ptr = &var1; // пусть ptr указывает на var1
*ptr = 37; // то же самое, что var1 = 37;
var2 = *ptr; // то же самое, что var2 = var1;
cout << var2 << endl; // убедимся, что var2 равно 37
getch(); return 0;
}
Необходимо понимать, что звёздочка, используемая в операции разыменовывания, - это не то же самое, что звёздочка, используемая при объявлении указателя. Доступ к значению переменной, хранящейся по адресу, с использованием операции разыменовывания называется непрямым доступом. Пока в приводимых примерах мы не обнаружили преимущества использования указателей для доступа к переменным, поскольку мы всегда можем применить прямой доступ. Важность указателей становится очевидной в том случае, когда мы не имеем прямого доступа к переменной.
Необходимо запомнить следующее правило: адрес, который помещается в указатель, должен быть одинакового с ним типа. Например, мы не можем присвоить указателю на int адрес переменной типа float.
С указателями можно выполнять следующие операции: разадресация; присваивание; сложение с константой; вычитание; инкрементирование; декрементирование; сравнение; приведение типов. Например, выражение (*р)++ инкрементирует значение, на которое ссылается указатель. Суммирование двух указателей не допускается.