Шаг 208.
Язык программирования C#. Начала.
Указатели. Адресная арифметика (продолжение)

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

    Для большей наглядности рассмотрим небольшой пример, в котором используются правила адресной арифметики (а если более конкретно, то индексируются указатели). Обратимся к программе в примере ниже.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace pr208_1
{
    class Program
    {
        // Главный метод: 
        unsafe static void Main()
        {
            // Объявление числовой переменной: 
            double miniarray;
            // Размер "byte-массива":
            int m = sizeof(double) / sizeof(byte);
            // Указатель неопределенного типа: 
            void* pnt;
            // Значение указателя: 
            pnt = &miniarray;
            // Указатель на byte-значение: 
            byte* p;
            // Значение указателя: 
            p = (byte*)pnt;
            // Перебор блоков памяти с помощью указателя: 
            for(int k = 0; k < m; k++) {
                // В блок памяти записывается значение: 
                p[k] = (byte)(k + 1);
                // Отображение значения из блока памяти: 
                Console.Write("|" + p[k]);
            }
            Console.WriteLine("|");
            // Новое значение указателя: 
            p = (byte*)pnt + m - 1;
            // Перебор блоков памяти с помощью указателя: 
            for(int k = 0; k < m; k++){
                // Использован отрицательный индекс: 
                Console.Write("|" + p[-k]);
            }
            Console.WriteLine("|");
            // Размер "char-массива":
            int n = sizeof(double) / sizeof(char);
            // Указатель на char-значение: 
            char* q;
            // Значение указателя: 
            q = (char*)pnt;
            // Перебор блоков памяти с помощью указателя: 
            for(int k = 0; k < n; k++){
                // В блок памяти записывается значение: 
                q[k] = (char)('A' + k);
                // Отображение значения из блока памяти:
                Console.Write("|" + q[k]);
            }
            Console.WriteLine("|");
            // Новое значение указателя: 
            q = (char*)pnt + n - 1;
            // Перебор блоков памяти с помощью указателя: 
            for(int k = 0; k < n; k++){
                // Использован отрицательный индекс:
                Console.Write("|" + q[-k]);
            }
            Console.WriteLine("|");
        }
    }
}
Архив проекта можно взять здесь.

    Результат выполнения программы следующий:


Рис.1. Результат выполнения программы

    В программе реализована очень простая идея. Объявляется переменная типа double. Под такую переменную выделяется 8 байтов. Сначала каждый такой байт интерпретируется как переменная типа byte (занимает 1 байт памяти). Получается 8 однобайтовых блоков, в каждый из которых записывается значение. Индексируя указатели, мы можем получать доступ к этим блокам как к элементам массива.

    Эту же область памяти размером в 8 байтов, выделенную под double-переменную, можно интерпретировать как 4 блока по 2 байта. В каждый из этих 4 блоков можно записать char-значение (под переменную типа char выделяется 2 байта). Получая доступ к char-значениям путем индексирования указателя, мы создаем иллюзию массива из 4 символьных элементов. Таким образом, одну и ту же область памяти можем использовать как byte-массив из 8 элементов или как char-массив из 4 элементов.

    Если более детально, то в программе объявляется переменная miniarray типа double. Значение целочисленной переменной m вычисляется выражением sizeof(double)/sizeof(byte). Это отношение объема памяти (в байтах), выделяемой для значений типа double (значение 8), и объема памяти, выделяемой для значений типа byte (значение 1). Также командой

  void* pnt;
мы объявляем указатель неопределенного типа. Операции адресной арифметики к такому указателю неприменимы. Фактически все, что можно с таким указателем сделать, записать в него адрес. Именно это мы делаем с помощью команды
  pnt = &miniarray;    , 
которой указателю pnt в качестве значения присваивается адрес переменной miniarray.


Область памяти, выделенной под переменную miniarray, состоит из 8 блоков, размер каждого блока равен 1 байту. Адрес переменной miniarray - это адрес первого из 8 блоков. Именно адрес первого блока записывается в указатель pnt.

    Командой

  byte* p;
объявляется указатель p на значение типа byte. Значение указателю присваиваем командой
  p = (byte*)pnt;                 . 
В этом случае, во-первых, адрес из указателя pnt копируется в указатель p. Во-вторых, пришлось использовать явное приведение типа. Без этого не обойтись, поскольку pnt является указателем на значение неопределенного типа, а указатель p объявлен для работы с byte-значениями. В итоге указатель p содержит адрес первого из 8 блоков, выделенных для переменной miniarray.


Хотя указатели pnt и p содержат один и тот же адрес, между ними есть принципиальная разница. Поскольку тип данных, на которые может ссылаться указатель p, определен, то к указателю p могут применяться операции адресной арифметики.

    Для записи значений в однобайтовые блоки запускаем цикл, в котором индексная переменная k пробегает значения от 0 до m-1 включительно (то есть перебираются все однобайтовые блоки). За каждую итерацию цикла при заданном индексе k сначала выполняется команда

  p[k] = (byte)(k + 1);               , 
которой в однобайтовый блок записывается значение, а затем это значение отображается в консольном окне с помощью команды
  Console.Write("|" + p[k]);        . 
В обеих командах использована инструкция вида p[k], в которой индексируется указатель p. Выражение p[k] представляет собой значение, записанное в блок памяти, смещенный на k позиций по отношению к блоку памяти, на который ссылается указатель p. Поскольку указатель p объявлен для работы со значениями типа byte, а под значение этого типа выделяется 1 байт, то при смещениях используются однобайтовые блоки.


В команде
  p[k] = (byte)(k + 1);
использовано явное приведение к типу byte. Необходимость явного приведения типа обусловлена тем, что сумма k+1 представляет собой значение типа int, а выражение p[k] подразумевает работу со значениями типа byte.

    При выполнении команды

  p = (byte*)pnt + m - 1;
указателю p присваивается новое значение. Это адрес последнего блока в области памяти, выделенной для переменной miniarray типа double. В соответствии с использованной командой значение указателя pnt приводится к типу byte*, а к полученному результату прибавляется число m-1 (точнее, сначала прибавляется значение переменной m, а затем вычитается число 1). Приведение к типу byte* необходимо для того, чтобы с указателем (адресом) можно было выполнять арифметические операции (в том числе и прибавлять к указателю числа). Значением выражения (byte*)pnt+m-1 является адрес блока памяти, смещенного на m-1 позиций по отношению к блоку, на который ссылается указатель pnt. Поскольку в приведенной команде мы указатель pnt приводили к типу byte*, а под значение типа byte выделяется 1 байт памяти, то смещение на одну позицию означает смещение на один однобайтовый блок. Таким образом, результат выражения (byte*)pnt+m-1 - это адрес однобайтового блока, смещенного по отношению к первому блоку (его адрес записан в указатель pnt) на m-1 позиций. Это последний блок в области памяти, выделенной под переменную miniarray типа double.


Точнее, значение выражения (byte*)pnt+m-1 вычисляется следующим образом: от блока, на который ссылается указатель pnt, выполняется m смещений с увеличением адреса, а потом 1 смещение с уменьшением адреса, и адрес полученного блока возвращается в качестве результата. Можно было воспользоваться командой (byte*)pnt+(m-1). В таком случае сразу выполнялось бы m-1 смещений с увеличением адреса.

    После присваивания нового значения указателю p еще раз выполняется конструкция цикла, в которой диапазон изменения индексной переменной k такой же, как и в предыдущем случае. За каждую итерацию цикла при заданном значении индекса k отображается значение выражения p[-k]. Особенность данного выражения в том, что индекс отрицательный. Поскольку речь идет об индексировании указателя, то отрицательный индекс означает, что смещение выполняется уменьшением адреса. Другими словами, значение выражения p[-k] - это значение в блоке памяти, который смещен на k позиций (так, что адрес уменьшается) по отношению к блоку памяти, на который ссылается указатель p. Поскольку p ссылается на последний блок области памяти, выделенной для переменной miniarray, то проиндексированный отрицательным индексом указатель дает значение соответствующего предыдущего блока. То есть выражение p[0] возвращает значение последнего блока, выражение p[-1] возвращает значение предпоследнего блока, выражение p[-2] позволяет узнать значение блока перед предпоследним, и так далее. В итоге получается, что в консоли отображаются значения, записанные в однобайтовые блоки, но отображаются они в обратном порядке (от последнего блока к начальному).

    Значение целочисленной переменной n вычисляется на основе выражения sizeof(double)/sizeof(char) как отношение объема памяти, выделяемой для double-значений (8 байтов), к объему памяти, выделяемой для char-значений (2 байта). В итоге переменная n получает значение 4. В данном случае мы интерпретируем память, выделенную под переменную miniarray, как 4 последовательно размещенных блока, каждый по 2 байта. Для получения доступа к этим двухбайтовым блокам объявляется указатель q на значение типа char. Значение указателю q присваивается командой

  q = (char*)pnt;     . 
Это адрес первого блока в области памяти, выделенной под переменную miniarray. Но, в отличие от случая с указателем p, здесь указатель q предназначен для работы с символьными значениями. Поэтому базовые блоки, на которые выполняется смещение, двухбайтовые (то есть состоят из двух однобайтовых блоков), а значения в этих двухбайтовых блоках интерпретируются как символьные. Сначала в операторе цикла мы заносим с помощью команды
  q[k] = (char)('A' + k);
в эти блоки символы, начиная с 'A' (и заканчивая 'D'), и отображаем их в консольном окне. А затем "перебрасываем" указатель q на последний двухбайтовый блок (команда
  q = (char*)pnt + n - 1;
) и с помощью еще одного цикла, индексируя указатель q отрицательными индексами (выражение q[-k]), отображаем символьные значения двухбайтовых блоков в обратном порядке.


Результатом выражения (char*)pnt+n-1 является адрес последнего двухбайтового блока в области памяти, выделенной под переменную miniarray. Указатель pnt приводится к типу char*, поэтому все смещения при выполнении операций адресной арифметики выполняются на уровне блоков размером в 2 байта (такого размера блок выделяется под значение типа char). Значение выражения (char*)pnt+n-1 - это адрес блока, смещенного n-1 раз (с увеличением адреса) по отношению к блоку, на который ссылается указатель pnt. Смещение на одну позицию означает смещение на два однобайтовых блока.

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




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