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

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

    Еще один пример, в котором иллюстрируются операции с указателями, представлен в примере ниже. В каком-то смысле он напоминает предыдущую программу (из предыдущего шага), но реализовано все немного иначе.

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

namespace pr209_1
{
    // Класс с главным методом: 
    class Program
    {
        // Главный метод: 
        unsafe static void Main()
        {
            // Объявление переменной типа double: 
            double val;
            // Целочисленная индексная переменная: 
            int k = 1;
            // Указатели на значения типа double: 
            double* start, end;
            // Значения указателей:
            start = &val;
            end = start + 1;
            // Отображение адресов:
            Console.WriteLine("Адрес start\t{0}", (uint)start);
            Console.WriteLine("Адрес end\t{0}", (uint)end);
            // Разность адресов:
            Console.WriteLine("Paзность адресов {0,6}", (uint)end - (uint)start);
            // Разность указателей:
            Console.WriteLine("Разность double-yкaзaтeлeй\t{0}", end - start); 
            Console.WriteLine("Разность int-указателей \t{0}", 
                     (int*)end - (int*)start); 
            Console.WriteLine("Разность char-yкaзaтeлeй\t{0}", 
                     (char*)end - (char*)start); 
            Console.WriteLine("Разность byte-yкaзaтeлeй\t{0}", 
                     (byte*)end - (byte*)start); 
            // Указатель на значение типа byte: 
            byte* p = (byte*)start;
            // Указатель на значение типа char: 
            char* q = (char*)start;
            // Указатель на значение типа int: 
            int* r = (int*)start;
            Console.WriteLine("Тип byte:");
            Console.WriteLine("Адрес\t3начение");
            // Заполнение блоков памяти значениями и отображение
            // значений из блоков памяти:
            while(p < end) {
                // Значение записывается в блок памяти:
                *p = (byte)k;
                // Отображение адреса и значения из блока памяти: 
                Console.WriteLine("{0}\t{1}", (uint)p, *p);
                // Увеличение значения указателя:
                p++;
                // Новое значение переменной: 
                k += 2;
            }
            Console.WriteLine("Тип char:");
            Console.WriteLine("Адрес\t3начение");
            // Заполнение блоков памяти значениями и отображение 
            // значений из блоков памяти: 
            for(k = 0; q + k < end; k++) {
                // Значение записывается в блок памяти:
                *(q + k) = (char)('A' + 2 * k);
                // Отображение адреса и значения из блока памяти: 
                Console.WriteLine("{0}\t{1}", (uint)(q + k), *(q + k));
            }
            Console.WriteLine("Тип int:");
            Console.WriteLine("Адрес\t3начение");
            // Заполнение блоков памяти значениями и отображение 
            // значений из блоков памяти: 
            for (k = 0; &r[k] < end; k++)
            {
                // Значение записывается в блок памяти: 
                r[k] = 5 * (k + 1);
                // Отображение адреса и значения из блока памяти: 
                Console.WriteLine("{0}\t{1}", (uint)&r[k], r[k]);
            }
        }
    }
}
Архив проекта можно взять здесь.

    Результат выполнения программы может быть таким, как показано ниже:


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

    В этой программе мы объявляем переменную val типа double, а также указатели start и end, предназначенные для работы со значениями типа double. Командой

  start = &val; 
указатель start получает в качестве значения адрес переменной val. Значение указателю end присваивается командой
  end = start + 1;         , 
в результате чего в указатель end записывается адрес блока, расположенного сразу за областью памяти, выделенной под переменную val.


Область памяти, выделенная под переменную val, состоит из 8 однобайтовых блоков. В указатель start записывается адрес первого из этих 8 блоков. При прибавлении числа 1 к указателю start (в команде
  end = start + 1;
) выполняется смещение на один блок размера, равного объему памяти, выделяемой под double-значение (8 байтов). В результате в указатель end записывается адрес блока, расположенного за 8 однобайтовыми блоками, выделенными под переменную val.

    Значения адресов, которые вычисляются выражениями (uint)start и (uint)end, отображаются в консольном окне. В принципе, сами адреса от запуска к запуску меняются. Но разница между значениями адресов (вычисляется как разность (uint)end-(uint)start) всегда одна и та же и равна 8. Причина в том, что адрес приписывается каждому однобайтовому блоку. Адреса соседних однобайтовых блоков отличаются на 1. Под значение типа double выделяется 8 однобайтовых блоков, отсюда и результат. Но вот если мы вычислим разность указателей end-start, то получим значение 1. Указатели end и start объявлены для работы с double-значениями. Разность этих указателей - это количество значений типа double, которые можно записать между соответствующими адресами. В данном случае между адресами (с учетом начального блока, на который ссылается указатель start) находится 8 однобайтовых значений. В эту область можно записать одно значение типа double.

    При вычислении выражения (int*)end-(int*)start мы получаем значение 2. Здесь, как и при вычислении выражения end-start, вычисляется разность указателей. Но эти указатели предварительно приведены к типу int*. То есть мы имеем дело с указателями, предназначенными для работы с целочисленными значениями типа int. Поэтому значением выражения (int*)end-(int*)start является целое число, равное количеству ячеек для значений типа int, которые помещаются в области памяти между адресами из указателей end и start. Как мы уже знаем, там 8 однобайтовых ячеек, а для записи значения типа int нужно 4 байта. Получается, что в данную область памяти можно записать два значения типа int.

    Аналогично при вычислении выражения (char*)end-(char*)start в результате получаем число 4. Это количество char-блоков, которые помещаются в области памяти, выделенной под переменную val. Ну и несложно догадаться, почему результатом выражения (byte*)end-(byte*)start является число 8.

    Командами

  byte* p = (byte*)start;
  char* q = (char*)start;
  и 
  int* r = (int*)start;
объявляются указатели p, q и r на значения типа byte, char и int соответственно. В каждый из указателей копируется адрес, записанный в указатель start. Поскольку указатели разного типа, то используется явное приведение типов. Далее действуем так: с помощью каждого из указателей заполняем значениями область памяти, выделенную под переменную val, и отображаем эти значения в консольном окне. Технология решения задачи каждый раз разная.

    Для заполнения области памяти byte-значениями запускаем цикл while, в котором проверяется условие p<end (адрес в указателе p меньше адреса в указателе end). За каждую итерацию цикла командой

  *p = (byte)k;
в однобайтовую ячейку, на которую ссылается указатель p, записывается значение целочисленной переменной k (начальное значение 1), а затем отображается адрес ячейки (инструкция (uint)p) и значение в этой ячейке (инструкция *p). Командой
  p++;
увеличивается значение указателя (в результате указатель будет указывать на соседнюю ячейку), а командой
  k += 2; 
текущее значение переменной k увеличивается на 2.

    В следующей уонструкции цикла for переменная k принимает начальное значение 0. За каждую итерацию цикла значение переменной k увеличивается на 1. Цикл выполняется, пока истинно условие q+k<end. Значение выражения q+k - это указатель на блок памяти (предназначенный для записи char-значения), смещенный на k позиций по отношению к блоку, на который ссылается указатель q. Условие состоит в том, что адрес этого блока должен быть меньше адреса из указателя end.

    В теле цикла командой

  *(q + k) = (char)('A' + 2 * k);
в блок памяти, на который ссылается указатель q+k, записывается символьное значение, которое получается прибавлением к коду символа 'A' значения 2*k с последующим преобразованием к типу char (получаем буквы через одну, начиная с буквы 'A'). Также отображается адрес блока (инструкция (uint)(q+k)) и значение в этом блоке (инструкция *(q+k)).

    В еще одной конструкции цикла for проверяется условие &r[k]<end. Значение выражения r[k] - это значение в блоке, смещенном на k позиций по отношению к блоку, на который ссылается указатель r. В данном случае смещение выполняется на блоки, в которые можно записать int-значения. Выражение &r[k] - это адрес блока, в который записано значение r[k]. Этот адрес должен быть меньше адреса из указателя end.

    В теле цикла командой

  r[k] = 5 * (k + 1);
в соответствующий блок записывается значение (числа 5 и 10, а больше не помещается). Адрес блока (инструкция (uint)&r[k]) и значение из блока (инструкция r[k]) отображаются в консольном окне.


При отображении адресов, если используется указатель на значение типа byte, дискретность изменения адреса равна 1. При отображении адресов с помощью указателей на значения типа char дискретность изменения адреса равна 2. При использовании указателя на значение типа int дискретность изменения адреса равна 4. Фактически дискретность изменения адреса определяется количеством однобайтовых блоков, используемых для записи значения соответствующего типа.

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




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