Шаг 205.
Язык программирования C#. Начала.
Указатели. Знакомство с указателями

    На этом шаге мы познакомимся с описанием и использованием указателей.

    Если мы объявляем переменную базового типа (например, целочисленную), то в памяти под эту переменную выделяется место. Объем выделяемой памяти зависит от типа переменной. В эту область памяти записывается значение переменной, и в случае необходимости оно оттуда считывается. Когда мы выполняем операции по изменению значения переменной, новое значение заносится в выделенную под переменную область памяти. У этой области памяти есть адрес. Но обычно он нас мало интересует. Мы обращаемся к переменной по имени. Этого вполне достаточно, чтобы присвоить значение переменной, а также изменить или прочитать значение переменной. Другими словами, мы получаем доступ к области памяти не напрямую, а через имя переменной. Вместе с тем в языке C# существует механизм, позволяющий обращаться к памяти напрямую, минуя переменную, под которую эта память выделена. Делается это с помощью указателей.

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


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

    При объявлении указателя важно отобразить два момента. Во-первых, необходимо показать, что речь идет об объявлении указателя. Во-вторых, нужно явно указать, какого типа значение может быть записано в область памяти, адрес которой будет содержать указатель. Почему это важно? Дело в том, что память разбивается на блоки, размер каждого блока равен 1 байту (это 8 битов). Каждый такой однобайтовый блок имеет адрес. Теперь представим, что объявляется целочисленная переменная типа int. Для такой переменной выделяется 32 бита, что составляет 4 байта. Таким образом, для целочисленной переменной выделяется 4 однобайтовых блока. Если бы переменная была типа char, то для нее выделялось бы 16 битов, или 2 байта - то есть 2 однобайтовых блока. А для переменной типа byte выделяется всего 1 однобайтовый блок (8 битов). Если под переменную выделяется несколько однобайтовых блоков, то адресом области памяти, в которую записывается значение переменной, является адрес первого однобайтового блока. Получается, что по одному лишь адресу мы можем определить место, начиная с которого в памяти записывается значение переменной. Но нам еще нужно знать размер области памяти, которую занимает переменная. Этот размер можно определить по типу переменной. Поэтому при объявлении указателей важно также определить, с переменными какого типа предполагается работать.

    С учетом двух упомянутых выше обстоятельств объявление указателя происходит следующим образом. Указывается идентификатор типа, соответствующий типу переменной, адрес которой может быть записан в указатель, и символ звездочка *. После этой конструкции указывается имя указателя. Например, если мы хотим объявить указатель на целочисленное значение (то есть указатель, значением которого может быть адрес переменной типа int), то можем использовать следующую инструкцию:

  int* p;

    В данном случае объявляется указатель р, которому в качестве значения могут присваиваться адреса переменных типа int. Мы в таком случае будем говорить, что p является указателем на целочисленное значение типа int.

    Если мы хотим объявить два указателя q и r на значение типа double (указатели, которым в качестве значений можно присваивать адреса переменных типа double), мы могли бы воспользоваться такой командой:

  double* q, r;
Корректным является и такое объявление указателя:
  char* s;

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

    Стоит заметить, что объявить указатель можно только для нессылочного типа. Проще говоря, мы не можем объявить указатель на объект класса. Объяснение в данном случае простое и связано с тем, что доступ к объекту мы получаем не напрямую, а через объектную переменную. Значение объектной переменной - не объект, а ссылка на объект (то есть фактически адрес объекта). Вместе с тем, как мы увидим далее, указатели могут применяться при работе с массивами и текстом.


При объявлении указателей такие выражения, как int* или double* (то есть выражение вида тип*), с некоторой натяжкой можно интерпретировать как "тип указателя". Во всяком случае, такой подход помогает во многих нетривиальных случаях интуитивно находить правильные ответы и решения.

    Также стоит заметить, что в языке C++ объявление double* q, r означает, что объявляется указатель q на значение типа double и переменная (обычная, не указатель) r типа double. То есть в языке C++ звездочка * в объявлении указателя "применяется" только к первой переменной. В языке C# это не так: командой double* q, r объявляется два указателя (q и r) на значение типа double.


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

  void* pnt;

    Данной командой объявлен указатель pnt, и при этом не определен тип значения, на которое может ссылаться указатель (тип переменной, адрес которой может быть присвоен указателю). Такой указатель можно использовать только для запоминания адреса. Никакие иные операции с таким указателем выполнить не удастся. Оно и понятно. Ведь если неизвестно, какого типа переменная записана в области памяти с данным адресом, то и неизвестно, какой объем памяти она занимает. А если объем доступной памяти неизвестен, то и операции с ней не выполняются.

    Итак, мы выяснили, как указатель объявляется. Но что же с ним делать? Есть две базовые операции, с которых мы и начнем наше знакомство с использованием указателей. Операции такие:

    Для получения адреса переменной используют инструкцию & (амперсанд). Если ее указать перед именем переменной (получается выражение вида &переменная), то значением такого выражения будет адрес области памяти, в которую записано значение переменной. Например, имеется целочисленная переменная n типа int и указатель p на значение типа int, объявленные следующим образом:

  int n;  // Переменная 
  int* p; // Указатель

    Тогда значением выражения &n является адрес переменной n. Поскольку переменная n объявлена с типом int и указатель p также объявлен с идентификатором int, то адрес переменной n может быть присвоен указателю р. Корректной является следующая команда:

  p = &n;

    Обратная операция к получению адреса переменной - получение доступа к значению, записанному по адресу. Для этого перед указателем, содержащим соответствующий адрес, нужно указать звездочку * (получается выражение вида *указатель). Скажем, выражение - это значение, записанное по адресу, содержащемуся в указателе p. Причем с помощью выражения вида *указатель можно не только прочитать значение, записанное по адресу, но и записать по этому адресу новое значение. В частности, мы могли бы воспользоваться такими командами:

  *р = 123; // Присваивание значения
  Console.WriteLine(*p); // Считывание значения

    Первой из представленных двух команд (выражение *р=123) по адресу из указателя p записывается целочисленное значение 123. Если значение указателю p присваивается командой p=&n, то речь фактически идет о переменной n. То есть командой

  *p = 123; 
значение присваивается переменной n. При выполнении команды
  Console.WriteLine(*p); 
в консольном окне отображается значение, записанное по адресу из указателя р. Несложно догадаться, что речь идет о значении переменной p.


Указатель позволяет получить доступ к области памяти, выделенной под переменную, минуя непосредственное обращение к этой переменной. На каком-то этапе мы узнаем адрес области памяти, в которой хранится значение переменной, и с помощью указателя получаем доступ к этой области памяти. С помощью указателя мы можем прочитать значение, записанное в память, а также мы можем записать туда новое значение. И здесь критически важно знать, какой объем памяти следует использовать при чтении/записи значений. Также нужно знать, как интерпретировать прочитанное значение (скажем, как целочисленное неотрицательное значение, целочисленное значение со знаком или символ). Все это определяется по идентификатору типа, указанному при объявлении указателя. Этот тип должен совпадать с типом переменной, адрес которой заносится в указатель. В случае необходимости можно использовать явное приведение типа. Такие ситуации рассматриваются немного позже.

    На следующем шаге мы закончим изучение этого вопроса.




Предыдущий шаг Содержание Следующий шаг