На этом шаге мы познакомимся с описанием и использованием указателей.
Если мы объявляем переменную базового типа (например, целочисленную), то в памяти под эту переменную выделяется место. Объем выделяемой памяти зависит от типа переменной. В эту область памяти записывается значение переменной, и в случае необходимости оно оттуда считывается. Когда мы выполняем операции по изменению значения переменной, новое значение заносится в выделенную под переменную область памяти. У этой области памяти есть адрес. Но обычно он нас мало интересует. Мы обращаемся к переменной по имени. Этого вполне достаточно, чтобы присвоить значение переменной, а также изменить или прочитать значение переменной. Другими словами, мы получаем доступ к области памяти не напрямую, а через имя переменной. Вместе с тем в языке 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 - значением указателю можно присвоить адрес переменной символьного типа.
Стоит заметить, что объявить указатель можно только для нессылочного типа. Проще говоря, мы не можем объявить указатель на объект класса. Объяснение в данном случае простое и связано с тем, что доступ к объекту мы получаем не напрямую, а через объектную переменную. Значение объектной переменной - не объект, а ссылка на объект (то есть фактически адрес объекта). Вместе с тем, как мы увидим далее, указатели могут применяться при работе с массивами и текстом.
Также стоит заметить, что в языке 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;
Console.WriteLine(*p);
На следующем шаге мы закончим изучение этого вопроса.