На этом шаге мы рассмотрим небольшой пример, иллюстрирующий использование адресной арифметики.
Для большей наглядности рассмотрим небольшой пример, в котором используются правила адресной арифметики (а если более конкретно, то индексируются указатели). Обратимся к программе в примере ниже.
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; ,
Командой
byte* p;
p = (byte*)pnt; .
Для записи значений в однобайтовые блоки запускаем цикл, в котором индексная переменная k пробегает значения от 0 до m-1 включительно (то есть перебираются все однобайтовые блоки). За каждую итерацию цикла при заданном индексе k сначала выполняется команда
p[k] = (byte)(k + 1); ,
Console.Write("|" + p[k]); .
p[k] = (byte)(k + 1);
При выполнении команды
p = (byte*)pnt + 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;
На следующем шаге мы закончим изучение этого вопроса.