3.2. Указатель - StriderAJR/StudentCpp GitHub Wiki

Рассматриваемые темы:

Указатели

#pragma once

#include <iostream>

using namespace std;

namespace Pointers
{
   int main()
   {
      int* ptr1;                       // Это указатель
      float *ptr2;                     // И это указатель
      double *ptr3;                    // Это тоже указатель
      char* ptr4;                      // И даже это указатель
      int num1;                        // Это переменная типа данных int
      int* anotherPtr1;                // А это указатель!
      float num2;                      // Не указатель
      double num3;                     // Тоже не указатель
      char *num4;                      // А вот это указатель
      cout  << "sizeof(short) = "      << sizeof(short)     << endl
            << "sizeof(int) = "        << sizeof(int)       << endl      
            << "sizeof(long long) = "  << sizeof(long long) << endl  
            << "sizeof(char) = "       << sizeof(char)      << endl      
            << "sizeof(int*) = "       << sizeof(int)       << endl        
            << "sizeof(double*) = "    << sizeof(double*)   << endl 
            << "sizeof(char*) = "      << sizeof(char*)     << endl;    
         
      long long number = 65796;      
      long long* ptr = &number;
      
      cout << *ptr << endl;          // Выведется число 65796
      short* ptr_2 = (short*) ptr;   // Явно говорим, что нам нужен указатель short*, а не long long*
      cout << "ptr = " << ptr << " ptr_2 = " << ptr_2 << endl;
      cout << *ptr_2 << endl;        // Вывыведется 260 ^_^
      
      char* ptr_3 = (char*)ptr;
      cout  << "1 байт = " << *ptr_3                   << endl // Выведется какой-то непонятный символ...
            << "1 байт = " << (int) *ptr_3             << endl
            << "1 байт = " << int(*ptr_3)              << endl
            << "1 байт = " << static_cast<int>(*ptr_3) << endl
            << "2 байт = " << (int)*(ptr_3 + 1)        << endl  
            << "3 байт = " << (int)*(ptr_3 + 2)        << endl
            << "4 байт = " << (int)*(ptr_3 + 3)        << endl
            << "5 байт = " << (int)*(ptr_3 + 4)        << endl
            << "6 байт = " << (int)*(ptr_3 + 5)        << endl
            << "7 байт = " << (int)*(ptr_3 + 6)        << endl
            << "8 байт = " << (int)*(ptr_3 + 7)        << endl;
      
      for(int i = 0; i < sizeof(number); i++)
      {
         cout << (int)*((char*)ptr + i) << " ";
      }
      cout << endl;
      
      for (int i = 0; i < sizeof(number); i++)
      {
         cout << (int)*(ptr + i) << " ";
      }
      cout << endl;
      
      cout << "ptr = " << ptr << " (ptr+1) = " << ptr + 1 << endl;
      cout << "Разница = " << int(ptr + 1) - int(ptr) << endl;
      
      return 0;
   }
}

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

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

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

NB: данное высказывание работают не полностью в более высокоуровневых языка, таких как C# или Java, потому что ни работают в обертки собственной виртуальной машины и имеют ряд утилит для управления памятью кучи, например, "сборщик мусора". Для управления памятью в куче был введен тип данных - УКАЗАТЕЛЬ.

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

Объявляется указатель по следующему шаблону: ТИП_ДАННЫХ* ИМЯ_ПЕРЕМЕННОЙ;

NB: здесь тип_данных - это тип данных, которые хранятся по адресу в указателе.

int* ptr1;             // Это указатель
float *ptr2;           // И это указатель
double *ptr3;          // Это тоже указатель
char* ptr4;            // И даже это указатель

У всех этих переменных тип данных - указатель Не int, float, double или какой-то еще, а именно УКАЗАТЕЛЬ*.

int num1;              // Это переменная типа данных int
int* anotherPtr1;      // А это указатель!
float num2;            // Не указатель
double num3;           // Тоже не указатель
char *num4;            // А вот это указатель

NB: синтаксически компилятор позволяет объвлять указатель двумя способами:

int* ptr5;

или

int *ptr6; 

По нотации С++ правильно писать именно так.


Синтаксически разработчики компилятора сделали вот такую подлянку:

int* a1, a2;

Думаете здесь через запятую объявлено 2 переменных типа данных указатель? А вот ФИГ ВАМ! В случае выше объявлен ОДИН указатель и ОДНА переменная int Чтобы через запятую получить два указателя, нужно писать так:

 int* a3, *a4; 

Т.е. обязательно добавлять звездочку перед именем самой переменной. Без этого оператор запятая не сможет правильно разобрать конструкцию.

И вот из-за такой маленькой особенности, чтобы не забывать об этой синтаксической особенности исторически стали пристыковыввать звездочку к имени переменной. Вот так:

int *a6;

Но у многих начинающих С++ программистов из-за этого начинаются проблемы с идентификацией типа данных переменных

int *some_variable;

На вопрос "Какой это тип данных?" Часто дают ответ - "int естественно". Видимо символ * - это мухи натоптали на экране. Поэтому некоторые, чтобы не забывать об этом, стыкуют звездочку именно с типом данных

int* another_variable;

Лично мне нравится писать именно так. Никто вас за это не осудит, но по стандарту делается это наоборот. Кстати, а сколько памяти занимается указатель в памяти? Это же переменная и, значит, под нее выделяется память. Есть отличная команда sizeof, которая может получить размер переменной или типа данных

  cout << "sizeof(short) = " << sizeof(short) << endl
       << "sizeof(int) = " << sizeof(int) << endl      
       << "sizeof(long long) = " << sizeof(long long) << endl  
       << "sizeof(char) = " << sizeof(char) << endl      
       << "sizeof(int*) = " << sizeof(int) << endl        
       << "sizeof(double*) = " << sizeof(double*) << endl 
       << "sizeof(char*) = " << sizeof(char*) << endl;    

Запустите и проверьте, что вывдетеся на экран.

Такой маленький эксперимент показывает, что вне зависимости от тип данных, адрес которых хранит указатель, размер самого указателя не меняется. И это логично. Указатель - это просто адрес. А размерность шины адреса в вычислительной машине - фиксирована.

Если указатель - это адрес, то как сохранить адрес в указатель? Сам же он там не появится, мы должны этот адрес как-то узнать, а затем сохранить в указатель. Для работы с указателями есть 2 оператора:

  1. operator& - получить адрес переменной. Например,
 int a = 5;
 int* ptr_a = &a; 

В ptr_a сохранится адрес ячейки памяти, в которой хранится переменная a.

  1. operator*- получить данные из ячейки памяти по ее адресу.
 int b = *ptr_a;

В ptr_a хранится адрес ячейки памяти, в которой хранится число 5. С помощью оператора звездочка мы по адресу из указатель ptr_a получаем данные. В данном случае это число 5.

ТЕКСТ, КОТОРЫЙ НУЖНО ВНИМАТЕЛЬНО ОСОЗНАТЬ

int* ptr;
cout << *ptr;

В двух этих строчках звездочки - это РАЗНЫЕ вещи.

  • int* - здесь звездочка это синтаксическое обозначение типа данных указателя.
  • *ptr - здесь звездочка это оператор. Оператор всегда что-то делает.

У оператора есть над чем идет операция и оператор зачастую что-то возвращает как результат. А звездочка в синтаксисе объявления типа данных - ничего не делает. Запомните и осознайте это!

И все же, что действительно скрывается за конструкцией ТИП_ДАННЫХ* ИМЯ_ПЕРЕМЕННОЙ; Допустим, у нас есть переменная

 long long number = 65796;

и указатель

 long long* ptr = &number;

Тогда связь этих переменных в памяти будет такой:

Tabl14

Разберем, что на картинке. Сверху написаны адрес ячейки памяти и имя переменной этой ячейки. (Помните же, что переменная - это просто символьное имя ячейки памяти в помощь программисту)

0xFFF01 - это адрес ячейки переменной number, которая хранит число 65796

0xFFFC2 - это адрес ячейки переменной ptr, которая хранит число адр2 - адрес другой ячейки

Только имейте в виде, что 0xFFF01 и 0xFFF01 - число только для примера.

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

cout << "адр2 = " << ptr << endl;

Почему я не могу записать адреса явным числом? Потому что после каждого запуска программы эти адреса буду меняться. Но на самом деле даже эта визуализация упрощенная, потому что минимальная адресуемая ячейка памяти - 1 байт. Но указатель занимает 4 байта, а переменная long long занимает 8 байт А у нас на рисунке только одна ячейка, хотя должно быть 4 и 8 соответственно. Давайте для простоты ptr так и будем в упрощенном виде обозначать одной ячейкой, а вот number нарисуем, как оно действительно выглядит в памяти. Как разместить такое простенькое число 65796 аж в 8 ячейках памяти? Машина все числа хранит в двочином виде.

В бинарном представлении число 65796 выглядит так:

Tabl15

Это 8 байт. 1 байт = 8 бит. И у нас записано 8 раза по 8 бит.

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

Наше число number лежит (точнее только начинается) по адресу 0xFFF01.

Tabl16

Ну ок. И причем тут указатели и вся эта абра-кадабра?

 cout << *ptr << endl;          // Выведется число 65796

А теперь немного магии

short* ptr_2 = (short*) ptr;    // Явно говорим, что нам нужен указатель short*, а не long long*

Но ptr_2 хранит тот же адрес, что и ptr

   cout << "ptr = " << ptr << " ptr_2 = " << ptr_2 << endl;
   cout << *ptr_2 << endl;      // Вывыведется 260 ^_^

Что же тут произошло. short это тип данных, длина которого всего 2 байта. Он просто не может вместить в себя больше данных. У нас есть указатель short* ptr_2, который хранит тот же адрес, что и ptr, т.е. это адрес одной и той же области в памяти. Но когда мы делаем cout << ptr_2; Программа видит тип short* - значит, мы ждем увидеть short. Начиная с адреса ptr_2 извлекается первые 2 байта и они преобразуются в short число

Tabl16

Т.е. извлечется число 00000001 00000100 а это как раз таки 260.

МАГИЯ!

Продолжим заниматься магией. А что если мы хотим посмотреть чему равен каждый конкретный байт числа? Во-первых, нам понадобится выбрать тип указателя, чтобы размер типа данных был 1 байт. Для этого подойдет char*

char* ptr_3 = (char*)ptr;

Итак мы хотим посмотреть значение первого байта числа. Еще раз смотрим на схемку

Tabl16

Самый первый байт лежит по адресу самого указателя. Т.е. нам нужно вывести значение по адресу самого указателя.

  cout << "1 байт = " << *ptr_3 << endl; // Выведется какой-то непонятный символ...

Что не так? После разыменовывания указателя типа char* мы получим тип данных char - символ Вот нам и выводят на экран символ с кодом 4. Ну извините, что он такой некрасивый и непонятный. Если нам хочется увидеть все-таки число, то нужно явно сказать программе, что мы хотим число. Это называется ПРИВЕДЕНИЕМ ТИПОВ Можно привести тип так:

   cout << "1 байт = " << (int) *ptr_3 << endl;

или так:

   cout << "1 байт = " << int(*ptr_3) << endl;

или для совсем продвинутых:

  cout << "1 байт = " << static_cast<int>(*ptr_3) << endl;

А как же достучаться до следующего байта? Второго байта. На схемке видно, что следующий байт располагается по адресу на 1 больше, чем адрес начала, т.е. это (ptr_3 + 1)

  cout << "2 байт = " << (int)*(ptr_3 + 1) << endl;

Далее по аналогии для всех остальных байт

   cout << "3 байт = " << (int)*(ptr_3 + 2) << endl;
   cout << "4 байт = " << (int)*(ptr_3 + 3) << endl;
   cout << "5 байт = " << (int)*(ptr_3 + 4) << endl;
   cout << "6 байт = " << (int)*(ptr_3 + 5) << endl;
   cout << "7 байт = " << (int)*(ptr_3 + 6) << endl;
   cout << "8 байт = " << (int)*(ptr_3 + 7) << endl;

Ну а если у вас с информатикой (или логикой) совсем хорошо, то помня о начале индексации с нуля, можно было бы догадаться, как оформить все то же самое через цикл

  for(int i = 0; i < sizeof(number); i++)
   {
      cout << (int)*((char*)ptr + i) << " ";
   }
   cout << endl;

А вот так не получится

   for (int i = 0; i < sizeof(number); i++)
   {
      cout << (int)*(ptr + i) << " ";
   }
   cout << endl;

Казалось бы, какая разница писать ((char*)ptr + i) или (ptr + i)

и там, и там мы увеличиваем адрес на 1! Почему не работает?! Увеличиваем на 1, да. Только

(char*)ptr и ptr - разные указатели

ptr - это указатель на long long т.е. он заточен на работу с данными длиной 8 байт (char*)ptr - с помощью приведения типов превращен в указатель char* и работает с данными длиной 1 байт. Уже догадались?

((char*)ptr + 1) - прыгнет на 1 байт, потому что его данные длиной 1 байт, но (ptr + 1) - воспринимает все данные длинной 8 байт и прыгнет он... Да, на 8 байт сразу! Если ptr = 0xFFF01, то (ptr+1) = 0xFFF09

Механизм, когда указатель перемещается на нужное кол-во байт в зависимости от типа данных, хранимых по адресу, называется МАСШТАБИРОВАНИЕ*

cout << "ptr = " << ptr << " (ptr+1) = " << ptr + 1 << endl;
cout << "Разница = " << int(ptr + 1) - int(ptr) << endl;

That's all, folks!

⚠️ **GitHub.com Fallback** ⚠️