На этом шаге мы рассмотрим небольшой пример, иллюстрирующий использование адресной арифметики.
Для большей наглядности рассмотрим небольшой пример, в котором используются правила адресной арифметики (а если более конкретно, то индексируются указатели). Обратимся к программе в примере ниже.
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; ,
Область памяти, выделенной под переменную miniarray, состоит из 8 блоков, размер каждого блока равен 1 байту. Адрес переменной miniarray - это адрес первого из 8 блоков. Именно
адрес первого блока записывается в указатель pnt.
Командой
byte* p;
p = (byte*)pnt; .
Хотя указатели pnt и p содержат один и тот же адрес, между ними есть принципиальная разница. Поскольку тип данных, на которые может ссылаться указатель p, определен,
то к указателю p могут применяться операции адресной арифметики.
Для записи значений в однобайтовые блоки запускаем цикл, в котором индексная переменная k пробегает значения от 0 до m-1 включительно (то есть перебираются все однобайтовые блоки). За каждую итерацию цикла при заданном индексе k сначала выполняется команда
p[k] = (byte)(k + 1); ,
Console.Write("|" + p[k]); .
В команде
p[k] = (byte)(k + 1);
При выполнении команды
p = (byte*)pnt + m - 1;
Точнее, значение выражения (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; .
q[k] = (char)('A' + k);
q = (char*)pnt + n - 1;
Результатом выражения (char*)pnt+n-1 является адрес последнего двухбайтового блока в области памяти, выделенной под переменную miniarray. Указатель pnt приводится к типу
char*, поэтому все смещения при выполнении операций адресной арифметики выполняются на уровне блоков размером в 2 байта (такого размера блок выделяется под значение типа char).
Значение выражения (char*)pnt+n-1 - это адрес блока, смещенного n-1 раз (с увеличением адреса) по отношению к блоку, на который ссылается указатель pnt. Смещение на одну
позицию означает смещение на два однобайтовых блока.
На следующем шаге мы закончим изучение этого вопроса.