Начнём, как обычно, издалека. Когда операционная система запускает программу которую вы написали, все данные относящиеся к программе помещаются в оперативную память. Как вы, наверное, помните (из урока про типы данных), память в компьютере состоит из байтов. Байты в памяти нумерются, т.е. у каждого байта есть порядковый номер - его адрес. С помощью адреса процессор может обратиться к любому байту в памяти. Адреса хранятся в шестнадцатиричном формате. Обычно мы используем десятичную систему. В компьютерах удобнее всего использовать двоичную (числа формируются всего из двух цифр: 0 и 1), или степень двойки (16 = 24). При этом в шестнадцатиричной системе 16 цифр: 0, 1, ... 9, A, B, C, D, E, F (где цифры от a до f соответствуют десятичным 10-15). Адрес выглядит примерно так: 0x0012fc2c, что равно числу 1244204 в десятичной системе счисления. 0x (ноль икс) перед адресом говорит, что число шестнадцатиричное.

Ну так вот, у каждого байта есть свой адрес. Когда ваша программа попадает в оперативную память, всем данным присваиваются адреса, чтобы процессор мог к ним обращаться. Каким данным присваиваются адреса: переменным, функциям, структурным переменным, объектам классов.

При этом данные хранятся в двух разных участках памяти: стеке и куче:

код на языке c++
int a = 0;

int main()
{
  int b = 0;
  return 0;
}

a хранится в куче (heap), b хранится в стеке (stack). Все глобальные переменные попадают в кучу, все локальные переменные попадают в стек. что такое стек, вы можете узнать в соответствующем уроке раздела Алгоритмы. Куча же - это область памяти где данные никак не организованы.

Теперь, рассмотрим простое объявление переменной:

int a;
a - идентификатор с помощью которого мы можем обращаться к какой-то области памяти. При этом данная переменная занимает четыре байта. Обращение к переменной происходит по первому байту.

При создании переменной a, ей присваивается адрес 0x0012fc28 (адрес взят для примера). Процессор обращается к этой переменной с помощью этого адреса. Сама же переменная занимает в памяти следующие байты: 0x0012fc28, 0x0012fc29, 0x0012fc2a, 0x0012fc2b (обратите внимание на последние цифры в адресах). Где первый байт - адрес переменной.

Теперь мы плавно переходим к рассмотрению указателей.

Следует заметить, что указатели есть не во всех языках программирования.

Указатель (pointer) - это переменная, значением которой является адрес.

Чтобы получить адрес обычной переменной можно воспользоваться следующим синтаксисом:

cout << &a;
На экран будет выведен адрес переменной a.

Указатель можно создать следующим образом:

int a = 0;
int* ptr;

ptr = &a;

cout << ptr;
Здесь & - операция получения адреса. Не путайте использование & для получения адреса и для передачи в функцию значения по ссылке.

В данном примере мы создали указатель ptr на тип int и присвоили этому указателю адрес, где хранится переменная a.

Когда вы создаёте указатель, всегда инициализируйте его каким-нибудь значением. По умолчанию в указателе может храниться любой адрес, и если вы попытаетесь изменить значение в этом адресе, то вполне вероятно возникновение ошибки при выполнении программы.

Через указатель можно изменять значение хранящееся по адресу. Для этого указатель нужно разыменовать:

код на языке c++
int a = 0;
int* ptr = a;

*ptr = 10; //  то же самое что и: a = 10;

cout << a; 

вывод на экран: 10
Здесь мы воспользовались указателем, чтобы изменить значение переменной a. В данном случае * - операция разыменования. Не путайте с объявлением указателя!

При разыменовании указателя мы получаем доступ к значению адреса в памяти, на который указывает указатель.

Указатель на тип void

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

Указатели на разные типы не могут использоваться друг с другом:

код на языке c++
int a = 0;
float b = 1;

int* ptr_a = &a;
float* ptr_b = &b;
// ptr_a = ptr_b; // Так нельзя!!!

Чтобы обойти это ограничение можно воспользоваться указателем на void:

код на языке c++
int a = 0;
float b = 1;
char c = 2;

int* ptr_a = &a;   // Указатель  на тип int
float* ptr_b = &b; // Указатель  на тип float
void* ptr_c = &c;  // указатель  на void

// ptr_a = ptr_b; // Так нельзя!!!

ptr_c = a; // Оба варианта
ptr_c = b; // корректны

Указатели на void особенно полезны при использовании с классами. В DirectX очень многие объекты создаются как указатели на void.

Указатели-константы

Мы уже не раз встречались с указателями-константами. Это такие указатели, значение которых не может быть изменено. Т.е. не могут быть изменены адреса. Значения же хранящиеся в адресах могут изменяться.

Более известное имя указателей-констант - массивы.

Хотя надо заметить, что данный вид указателей используется не только с массивами:

код на языке c++
int a = 1;
int b = 5;
int* ptr_a = &a;
int* const ptr_b = &b;
*ptr_b = 2;
// ptr_b = ptr_a; // так  не получится. ptr_b - константа
*ptr_b = 3;

Передача аргумента в функцию по указателю

Мы уже умеем передавать аргументы в функции двумя способами: по значению и по ссылке. Третий способ - передача по указателю (pass-by-pointer).

код на языке c++
int main()
{
  int a = 10;
  pass_by_pointer(&a); // Передача адреса переменной
  cout << a; // 5
  return 0;
}

void pass_by_pointer (void* ptr)
{
*ptr = 5;
}

При передаче по указателю мы передаём адрес. Внутри функции параметр ptr может непосредственно влиять на содержимое внешней переменной a.

Операция new

В программе pseudo_game мы использовали заданный массив. Мы не могли сделать что-нибудь подобное:

cin >> s; // количество строк
cin >> c; // количество столбцов

char map[s][c];[/b]
Компилятор должен знать заранее (до начала выполнения программы) сколько памяти выделить на массив. То же самое и с классами: все объекты классов должны быть созданы до начала выполнения программы. Вы не можете динамически создать объект.

Операция new позволяет обойти это ограничение. (В примере используется класс tank из программы pseudo_game):

код на языке c++
/*
Здесь представлены сразу два примера:
создание указателя  на tank
и
выделение памяти  для хранения всех клеток игрового поля
*/

tank* t34 = new tank; // выделение памяти  под объект tank
                      // Здесь t34 - указатель  на тип tank

int s,c; // количество строк  и столбцов
cin >> s;
cin >> c;

int symbols = s*c; // общее количество клеток

char* map; // указатель
map = new char [symbols]; // выделение памяти  на все клетки

Здесь t34 и map - указатели.

В данном примере мы вынуждены использовать одномерный массив для представления двумерного. Как в данном случае получить доступ к произовльному элементу? Вот как выглядит инициализация клетки (5,6):

*(map+5*c+6+1) = 'Т';
Думаю, данный пример нуждается в пояснениях. Запись (5,6) - координата клетки, куда нам нужно что-нибудь поместить. 5 - шестая строка. Напомню, что отсчёт ведётся с нуля. 6 - седьмой столбец. В одномерном массиве данный адрес нужно считать так: в строке - c элементов (пусть пользователь ввёл 10). Сначала нужно посчитать все клетки в предыдущих строках (пять строк (нулевая, первая, вторая, третья, четвёртая) по 10 элементов) - 5*с. Затем к этому количеству нужно прибавить ещё 6+1 (дополнительная клетка нужна, так как клетки отсчитываются от нуля).

Получившееся значение (57) мы прибавляем к map. А map - указатель на char. И вот этот вот 57-й адрес от начала массива мы разыменовываем и присваиваем ему 'T'. Как просто!!! :)

На самом деле - действительно просто. Нужно только привыкнуть. Многие функции в наших программах придётся переписывать, но указатели позволяют сделать программу более гибкой.

Запомните: когда мы прибавляем какое-то значение к указателю, значение указателя увеличивается не на количество байтов, а на количество элементов того типа, на который указывает указатель. Вот почему так важно при объявлении указателя писать тип.

Кстати, такой вид двухмерных массивов очень популярен. Когда видеокарта выводит на экран пиксели, все вычисления производятся именно с одномерным массивом.

Операция delete

Операция delete используется для освобождения памяти после того как она стала не нужна:

delete [] map;

delete t34;
Лучше всего освобождать память всегда когда вы её выделяете. Особенно когда память выделялось в какой-нибудь из функций.

Доступ к объектам

Последнее на что хочу обратить внимание. Доступ к членам класса объекта созданного с помощью указателей отличается от того, что мы видели раньше.

t34->fuel = 100;
То есть вместо точки используются символы ->.

Статья взята с shatalov.su