Вся Вводная правда об указателях. Часть 1:

09:39 2015-11-26 19

Рейтинг 2/5, всего 1 голосов

С самого начала моей карьеры, как программиста, я постоянно встречаю людей(лично или в сети), которые при слове «указатели» впадают в состояние уныния, или наоборот, крайнего возбуждения, находясь в котором, они начинают бранить C++ и указатели на чём свет стоит. И это можно встретить как у начинающих программистов, так и у тех, кто уже довольно давно и долго программирует на языках типа Java и C#. Вообще, у всех, кто «ненавидит» C++(и C), на мой взгляд, есть всегда 2 довода: шаблоны и указатели, исключая шаблоны из уравнения, мы остаёмся с этим «страшными» указателями. Я никогда не понимал почему люди так боятся указателей, ведь это так просто! Но видимо не всем в своё время их объяснили в той мере, чтобы человек понял, потому что я далёк от мысли, что указателей боятся те люди, которые их понимают. Устав от предыдущих статей, посвящённых низкому уровню многопоточной разработки, а также имея давнее желание иметь материал, который можно было бы представлять всем тем, кто не понимает указатели, я и решил написать этот пост, который можно отнести к категории «для самых маленьких». Если вы неплохо ориентируетесь в указателях, то настоящая статья не откроет вам ничего нового. Что такое указатель? Чтобы понять что такое указатель, предлагаю рассмотреть следующую аналогию из материального мира: представим, что у нас есть горизонтальный шкаф, который состоит из некоторого количества выдвижных ящиков: Одним из интересных свойств этого шкафа является то, что каждый выдвижной ящик является кубом, размером 1*1*1 дц3. Примем этот размер за единицу, и в дальнейшем всё будем измерять в ящиках. Таким образом мы ввели размерность — ящик. Кроме этого свойства, есть и другое — мы имеем перед собой шкаф-трансформер, т.е. если у нас есть какая-то вещь, которая не вмещается в один ящик, мы просто вынимаем несколько ящиков, группируем их(неважно как, пусть будет магия) и, положив вещь в ящик, вставляем новый, большой ящик обратно в шкаф: Ах да, у нас есть специальные очки, в которых мы смотрим на шкаф, что делает его прозрачным для нас(другие не могут видеть внутренности, пока не откроют ящик!): Как вы можете видеть, в нашем шкафу поместилась книга Страуструпа, которая заняла один ящик — размер книги один ящик, а также четырёхтомник Кнута, размером в 4 ящика. Т.к. человек привык к определённому порядку, то ему как-то надо запоминать где у него что лежит(у него нет наших очков!), поэтому он прибегает к простейшему способу — он нумерует ящики слева-направо, начиная с единицы. Так, книга Страуструпа лежит в 1-м ящике, а четырёхтомник Кнута лежит в 4-м ящике и занимает 4 ящика. Поэтому, положив в наш шкаф ещё и книгу Майерса, сразу после Кнута, она будет лежать в (4+4) — в 8-м ящике. Т.е. Кнут занимает 4, 5, 6 и 7 ящики, пусть они и выглядят как один большой ящик. Пока всё нормально, наш подопытный запомнил(записал), где у него что лежит и когда надо он достаёт нужную книгу. Но тут кому-то(пусть будет дворецкий) вдруг пришло в голову, что книгу Страуструпа стоит переложить в ящик номер 2, и он, замыслив этот план, его исполняет. Но дворецкий понимает, что хозяин привык к тому, что книга лежит в другом месте, и что хозяин рискует не обнаружить её в дальнейшем. Поэтому дворецкий кладёт листок бумаги, на котором указывает, что книга лежит в ящике номер 2. Таким образом мы получаем следующую картину: Но дальше хуже, пришла теперь экономка(а она что в хозяйском шкафу забыла?), которая решила, что Страуструпу лучше лежать в 3-м ящике, но она не знает, что во второй ящик книгу переложил дворецкий и не подозревает о наличии указателя в первом ящике. Поэтому она кладёт книгу в третий ящик и оставляет указатель во втором: Теперь, когда хозяин решит почитать книгу по C++, ему сначала придется пройти занимательный квест: Открыть первый ящик, где он ожидает увидеть книгу, найти там записку-указатель на 2-й ящик. Открыть второй ящик, также обнаружить там указатель, но уже на 3-й ящик. Открыть третий ящик и, наконец, начать чтение. Так вот к чему я всё это тут рассказываю? А вот к чему: пусть шкаф, который мы видели ранее будет оперативной памятью, ящик, являющийся минимальной ячейкой шкафа, — ячейкой памяти, размером в один байт. Теперь всё становится ближе к нашей программистской вселенной и разговор пойдёт более предметный. Итак, продолжая преобразовывать нашу аналогию во что-то существенное, мы теперь должны преобразовать наши книги в «программистские» аналоги. Так, книга Старуструпа размером в один ящик, будет переменной размером в один байт, в C++ для этого служит тип char. Он вмещает как раз один байт данных. Книга Кнута, размером 4 ящика, становится переменной размером 4 байта, для большинства архитектур нам подойдёт тип int из C++. Дадим имена нашим переменным: char straustrup;
int knuth;
Таким образом, мы имеем следующую картину памяти:

Мы как-бы дали имена некоторой части памяти, т.е. возвращаясь к нашей аналогии, хозяин «запомнил» где у него лежит Страуструп, а где лежит Кнут, но он их ещё не положил туда! Правда, в отличии от шкафа, каждый ящик которого может быть либо пуст, либо полон, — все ячейки памяти всегда что-то содержат. Это могут быть старые данные, это могут быть нули — не важно, важно то, что даже если мы ничего не положили туда, мы всегда можем что-то оттуда достать. Правда мы не знаем что это будет. Пока отложим это знание, т.к. на данный момент нам это не нужно, это будет нужно позже.
Теперь положим в наши «ящики» какие-то данные(книги в ящики):
char straustrup = ‘S’;
int knuth = 100500;
Память стала выглядеть вот так:

Теперь мы знаем, что лежит в наших именованных частях памяти. Таким образом, впоследствии мы можем заглянуть в наши «ящики» и гарантировано найти там конкретные «книги». Т.е. наш хозяин точно знает, что обратившись к straustrup он увидит ‘S’. Итак, мы уже прошли всю нашу ситуацию, вплоть до дворецкого. Здесь ситуация становится интереснее: давайте «переложим», наше значение ‘S’ из первой ячейки во вторую. Сделали. Теперь нам как-то нужно указать хозяину, что ‘S’ находится в другом месте(напоминаю, у хозяина есть только переменная типа char с именем straustrup). Можно попробовать положить в straustrup адрес второй ячейки, давайте это сделаем:
char straustrup = 2;
Но это означает, что если позднее хозяин посмотрит на то, что содержит участок памяти с именем straustrup, то он увидит число 2. Как он может понять, что это адрес другой ячейки? Никак, ведь в отличии от нашей истории со шкафом он не увидит, что 2 написано на клочке бумаги, в котором поясняется, что это указатель на другой ящик. А это значит, что идя в ногу с аналогией, мы должны сообщить хозяину, что в ячейки памяти с именем straustrup лежит указатель на другую ячейку, а не сама книга. Для этих целей в C++ есть специальная нотация:
char* straustrup = 2;

Если вы попробуете скомпилировать вышенаписанный код, то вам это не удастся, но это на данный момент не важно. Главное понять суть.

Код выше, по-русски, можно написать так: объявляем переменную типа «указатель на переменную типа char», даём переменной имя straustrup и присваиваем ей значение 2. Теперь, если хозяин решит прочитать значение переменной straustrup, то он получит всё ту же двойку(возвращаясь к нашей аналогии, он увидит листок с цифрой 2), но теперь он видит, что тип переменной это не просто хранилище значений, а указатель(видит, что на бумаге нарисован указатель). Поэтому он понимает, что для получения значения, на которое указывает указатель ему нужно «перейти по указателю», т.е. посмотреть в ту ячейку, адрес которой содержится в переменной типа char*. В C++ эта операция называется разыменованием(dereferencing) и записывается следующим образом:
*straustrup
Вышеозначенная строчка выполняет следующее:

Извлекает адрес, который лежит в переменной straustrup
«Проходит» по данному адресу
Возвращает нам ту часть памяти(ячейку) на которую указывает straustrup

Что мы можем делать с *straustrup? Мы можем использовать это выражение для получения значения, которое лежит по адресу(в нашем случае адрес это 2) или же мы может положить что-то новое по этому адресу. Таким образом, данное выражение даёт нам анонимный доступ на чтение и запись к некоторой ячейке. Почему анонимный? Потому что мы не именовали эту ячейку ранее(к примеру, char bigValue), а просто использовали результат разыменовывания указателя. Если записать предыдущие разглагольствования кодом, то получится следующее:
// Создадим указатель, который указывает на 222-ю ячейку
char* pointerToData = 222;
// Поместим 33 в 222-ю ячейку
*pointerToData = 33;
// В 222 ячейке гарантировано лежит 33
assert(*pointerToData == 33)
Мы ещё не завершили нашу аналогию(помните экономку?), поэтому давайте добавим ещё указателей! Итак, наш «Страуструп» перемещён в ячейку с номером три, а в ячейку номер 2 помещён указатель на него. Как нам описать это в C++? Очень просто, но сначала давайте попробуем по старинке:
char* straustrup = 2;
Хозяин, пройдя по указателю(*straustrup) получит значение 3(экономка постаралась), но это не то значение, что он ожидает и он(как и в первый раз) не знает, что 3 это тоже указатель, а не значение которое он хочет получить. Мы должны явно сказать ему, что 3 нужно интерпретировать как указатель. Как это сделать? Давайте разбираться. В прошлый раз мы по-русски записали наше определение выше как: объявляем переменную типа «указатель на переменную типа char», даём переменной имя straustrup и присваиваем ей значение 2. Тут всё понятно, но ситуация у нас изменилась и что-то нужно поправить. А изменилось у нас то, что наш указатель указывает на ячейку, которая содержит другой указатель. Т.е. наш тип будет теперь «указатель на переменную типа указатель на char», другими словами — двойной указатель. С++ является довольно последовательным языком, поэтому синтаксис очевиден:
char** straustrup = 2;
Теперь всё, что нужно хозяину, это два раза разыменовать указатель и он получит искомое:
**straustrup;
Как это работает? Да очень просто, давайте разобьём это выражение на шаги:
char** straustrup = 2;
// Т.к. straustrup указывает на char*, объявим переменную
char* straustrup2 = *straustrup;
// Теперь получим искомое
char desiredStraustrup = *straustrup2;
Разумеется, **straustrup написать проще и быстрее, но вы должны понимать, что здесь не происходит никакой магии — простое «скакание» по указателям. Те же слова, только кодом:
// Создадим указатель, который указывает на 222-ю ячейку
char** pointerToPointer = 222;
// Поместим в 222-ю адрес 333-й ячейки
*pointerToPointer = 333;
// Поместим 11 в 333-ю ячейку
**pointerToData = 11;
// В 333-й ячейке гарантировано лежит 11
assert(**pointerToData == 11)
Графически это будет выглядеть так:

Главное. что нужно вынести из этого параграфа, это то, что памяти совершенно фиолетово, что находится в её ячейках, только назначая тип какой-либо ячейке памяти, вы указываете как нужно интерпретировать те данные, что лежат в ячейке. Таким образом, указатель является простым механизмом, который предназначен для того, чтобы донести до программиста, что содержимое этой переменной не интересно само по себе, но интересно то, что лежит по адресу, содержащемуся в переменной указателя. Тип того, что лежит по указателю является частью типа указателя и поэтому программист можно чётко определить, какого размера(сколько ячеек) и какого типа данные лежит по этому адресу.

Указатель — это переменная, которая содержит адрес ячейки памяти.

На что он указывает?
Разобравшись с тем, что указатель это всего лишь переменная хранящая адрес, нужно разобраться на что же он обычно указывает. В предыдущем параграфе мы явно назначали адреса ячеек, но тот код не мог быть скомпилирован ни одним компилятором, который придерживается стандарта? Почему? Потому что мы переменной типа «указатель на char», пытались присвоить значение типа int. Как вы наверное знаете, C++ является строго-типизированным языком, а это значит, что при любом присваивании проверяется может ли переменная одного типа быть присвоена другой. Разумеется, в переменную типа указатель, можно положить лишь другой указатель. Так что же нам делать, как положить адрес ячейки в указатель? Нужно использовать явное приведение типов, чтобы «заткнуть» компилятор:
int* pointerToInt = reinterpret_cast(10);
Для разнообразия, я использовал тип «указатель на int». Но куда указывает наш указатель? «На 10-ю ячейку, вестимо!» — скажет внимательный читатель. Да, это правда, но что находится в этой десятой ячейке? Разве мы положили туда что-то? Нам кто-то разрешал трогать десятую ячейку? На эти вопросы можно два раза ответить «нет» — мы ничего туда не клали, и никто нам позволения не давал туда лезть. С другой стороны, я уже упоминал, что память не ящик, и там всегда лежит что-то, даже если мы туда ничего не клали. Более того, почему это мы должны у кого-то спрашивать разрешения?
Давайте разбираться, что же говорит нам наш простенький кусок кода. Говорит он следующее: мы имеем указатель(pointerToInt), который указывает на 10-ю ячейку памяти, т.е. на 10-й байт памяти. Что ещё можно сказать? При разыменовании мы получим анонимный доступ к участку памяти размеров в 4 байта(здесь и далее мы считаем sizeof(int)==4). Т.к. мы ничего не положили в нашу анонимную переменную типа int, давайте просто прочитаем память — вдруг там что-то интересное лежит?!
std::cout


Террористы ИГИЛ взяли на себя ответственность за взрыв в Манчестере
Террористы ИГИЛ взяли на себя ответственность за взрыв в Манчестере
07:22 2017-05-24 7

В ИГИЛ рассказали, как устроили теракт на концерте в Манчестере
В ИГИЛ рассказали, как устроили теракт на концерте в Манчестере
20:17 2017-05-23 17

В ИГИЛ рассказали детали об организации теракта на стадионе Манчестера
В ИГИЛ рассказали детали об организации теракта на стадионе Манчестера
18:23 2017-05-23 18

«Исламское государство» взяло ответственность за теракт в Манчестере
16:19 2017-05-23 15

Офицер из Новосибирска погиб в Сирии
07:22 2017-05-23 20

Названы темы неожиданных майских переговоров Путина и Макрона
20:15 2017-05-22 9

В Сирии погиб еще один путинский «ихтамнет»
16:15 2017-05-22 27

Все сирийские повстанцы покинули город Хомс
08:17 2017-05-22 12

«Мумия»: финальный удлиненный трейлер выдал «козыри» фильма
22:20 2017-05-21 14

Вася Обломов высмеял российское телевидение в новом клипе
21:22 2017-05-21 30