Gratt
Модостроитель
- Регистрация
- 14 Ноя 2014
- Сообщения
- 3.301
- Благодарности
- 4.636
- Баллы
- 625
Вообще решил написать этот тред в попытке облегчить понимание указателей на начальном этапе знакомства с плюсами. По началу будет не очень просто, зато потом жить без указателей не сможете .
Поэтому попробую объяснить максимально примитивным языком. Освоение указателей на C/C++ равносильно взятию художником кисточки и красок. Только с этого момента программист начинает творить . Не совру, если скажу, что весь движок состоит из указателей. И от их понимания зависит, сможет ли программист реализовать сложную программу под платформу ZenGin, да и вообще в целом.
Структура памяти
Для начала смоделируем, как будут выглядеть наши данные в памяти. Определим несколько переменных:
В памяти у нас создадутся 3 ячейки, которые будут хранить значения этих переменных - a, b и c соответственно.
Из рисунка ниже видно, что идущие друг за другом значения имеют адреса (формата 0xXXXXXXXX). Каждый объект лежит строго в своем адресе.
Почему же адреса идут не [00, 01, 02], а [00, 04 08]? Все очень просто. Каждая из этих ячеек имеет размер хранения данных.
Размер ячейки определяется размером типа данных. Например любое число int имеет размер 4 байта, отсюда и объясняется такой интервал между ячейками.
Известно, что чем больше тип, тем больше информации он может вместить.
Формат указателя
И так, что такое указатель... Еще раз вернемся к переменным a, b, и c. Их имена - это по сути метки, которые скрывают за собой адрес ячейки с данными.
А указатель - это ячейка, хранящая адрес другой ячейки.
Добавим в наш код указатель на переменную b:
И тогда структура программы будет выглядеть так:
Заметим, что значение ячейки указателя повторяет адрес переменной b. Это значит, что указатель на переменную b построен корректно.
Также важно, что тип указателя должен совпадать с типом данных в ячейке памяти + '*', на которую он ссылается. В нашем случае это указатель int* на переменную int.
Построение указателей
Теперь о том, как обращаться к данным по казателю. Для этого представим, что символ '*' - это некий уровень адресации (как иерархия папочек в проводнике).
Чем больше '*' в указателе, тем выше его уровень адресации. Например создадим указатель на указатель (далее двойной указатель):
Структура такой программы:
Как я уже сказал выше, тип указателя строится как тип данных на который он ссылается + '*', для p это читается как
Благодаря указателю pp мы ссылаемся одновременно на p и b и можем в любой момент времени читать/изменять их данные.
Теперь непосредственно к обращению к данным. Для того, чтобы извлечь информацию, на которую ссылается указатель, необходимо провести обратную операцию, поставив звездочку с другой стороны от объекта.
Визуализация:
Примеры операций
Ну и как это работает на практике. Определим переменную и указатель на нее. Попытаемся изменить значение переменной через указатель.
Видим, что через указатель мы можем изменять значение переменной a, а также в любой момент времени считывать ее значения.
Попробуем еще раз. Создадим два указателя на один и тот же объект.
Результат эквивалентен предыдущему. Оба указателя способствуют изменению значений переменной a.
И напоследок реализуем изменение значения адреса указателя через двойной указатель.
Поэтому попробую объяснить максимально примитивным языком. Освоение указателей на C/C++ равносильно взятию художником кисточки и красок. Только с этого момента программист начинает творить . Не совру, если скажу, что весь движок состоит из указателей. И от их понимания зависит, сможет ли программист реализовать сложную программу под платформу ZenGin, да и вообще в целом.
Структура памяти
Для начала смоделируем, как будут выглядеть наши данные в памяти. Определим несколько переменных:
C++:
int a = 5;
int b = 10;
int c = 15;
В памяти у нас создадутся 3 ячейки, которые будут хранить значения этих переменных - a, b и c соответственно.
Из рисунка ниже видно, что идущие друг за другом значения имеют адреса (формата 0xXXXXXXXX). Каждый объект лежит строго в своем адресе.
Почему же адреса идут не [00, 01, 02], а [00, 04 08]? Все очень просто. Каждая из этих ячеек имеет размер хранения данных.
Размер ячейки определяется размером типа данных. Например любое число int имеет размер 4 байта, отсюда и объясняется такой интервал между ячейками.
Известно, что чем больше тип, тем больше информации он может вместить.
Формат указателя
И так, что такое указатель... Еще раз вернемся к переменным a, b, и c. Их имена - это по сути метки, которые скрывают за собой адрес ячейки с данными.
А указатель - это ячейка, хранящая адрес другой ячейки.
Добавим в наш код указатель на переменную b:
C++:
int a = 5;
int b = 10;
int c = 15;
// Указатель определяется символом '*' после типа.
// Символ'&' перез объектом берет адрес, в котором он лежит. Или, говоря грубо, строит на него указатель.
// Этот указатель мы записываем в p.
int* p = &b;
И тогда структура программы будет выглядеть так:
Заметим, что значение ячейки указателя повторяет адрес переменной b. Это значит, что указатель на переменную b построен корректно.
Также важно, что тип указателя должен совпадать с типом данных в ячейке памяти + '*', на которую он ссылается. В нашем случае это указатель int* на переменную int.
Построение указателей
Теперь о том, как обращаться к данным по казателю. Для этого представим, что символ '*' - это некий уровень адресации (как иерархия папочек в проводнике).
Чем больше '*' в указателе, тем выше его уровень адресации. Например создадим указатель на указатель (далее двойной указатель):
C++:
int a = 5;
int b = 10;
int c = 15;
int* p = &b;
int** pp = &p;
Структура такой программы:
Как я уже сказал выше, тип указателя строится как тип данных на который он ссылается + '*', для p это читается как
int + *
, а для pp - int* + *
.Благодаря указателю pp мы ссылаемся одновременно на p и b и можем в любой момент времени читать/изменять их данные.
Теперь непосредственно к обращению к данным. Для того, чтобы извлечь информацию, на которую ссылается указатель, необходимо провести обратную операцию, поставив звездочку с другой стороны от объекта.
C++:
// pp -> возвращает адрес p - 0x0000000C
// *pp -> возвращает адрес b - 0x00000004
// **pp -> возвращает значение b - 10
// &pp -> возвращает адрес на pp, то бишь указатель int***,
// например:
int* p = &b;
int** pp = &p;
int*** ppp = &pp;
Визуализация:
Примеры операций
Ну и как это работает на практике. Определим переменную и указатель на нее. Попытаемся изменить значение переменной через указатель.
C++:
int a = 100;
// Выводим на экран значение переменной a
cmd << "1. " << a << endl;
int* p = &a;
// Выводим на экран значение в заданном адресе
cmd << "2. " << *p << endl;
*p = 200;
// Выводим на экран значение переменной a !!!
cmd << "3. " << a << endl;
// Результаты выводов
// 1. 100
// 2. 100
// 3. 200
Видим, что через указатель мы можем изменять значение переменной a, а также в любой момент времени считывать ее значения.
Попробуем еще раз. Создадим два указателя на один и тот же объект.
C++:
int a = 100;
int* p1 = &a;
int* p2 = &a;
cmd << "1. " << a << endl;
*p1 += 20;
cmd << "2. " << a << endl;
*p2 += 30;
cmd << "3. " << a << endl;
// Результаты вывода:
// 1. 100
// 2. 120
// 3. 150
Результат эквивалентен предыдущему. Оба указателя способствуют изменению значений переменной a.
И напоследок реализуем изменение значения адреса указателя через двойной указатель.
C++:
int a = 100;
int b = 50;
// Указатель на 'а'
int* p = &a;
// Указатель на 'p'
int** pp = &p;
// Выводим значение переменной 'а' через указатель 'p'
cmd << "1. " << *p << endl;
// Изменяем значение адреса указателя p через указатель на него.
// Другими словами теперь 'p' будет ссылаться не на 'а', а на 'b'
*pp = &b;
// Выводим значение переменной 'b' через указатель 'p'
cmd << "2. " << *p << endl;
// Проверяем целостность переменных 'a' и 'b'
cmd << "3. a = " << a << ", b = " << b << endl;
// Результаты вывода:
// 1. 100
// 2. 50
// 3. a = 100, b = 50