На этом шаге мы рассмотрим еще один пример использования адресной арифметики.
Еще один пример, в котором иллюстрируются операции с указателями, представлен в примере ниже. В каком-то смысле он напоминает предыдущую программу (из предыдущего шага), но реализовано все немного иначе.
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;
end = start + 1; ,
Область памяти, выделенная под переменную val, состоит из 8 однобайтовых блоков. В указатель start записывается адрес первого из этих 8 блоков. При прибавлении числа 1 к указателю
start (в команде
end = start + 1;
Значения адресов, которые вычисляются выражениями (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;
Для заполнения области памяти byte-значениями запускаем цикл while, в котором проверяется условие p<end (адрес в указателе p меньше адреса в указателе end). За каждую итерацию цикла командой
*p = (byte)k;
p++;
k += 2;
В следующей уонструкции цикла for переменная k принимает начальное значение 0. За каждую итерацию цикла значение переменной k увеличивается на 1. Цикл выполняется, пока истинно условие q+k<end. Значение выражения q+k - это указатель на блок памяти (предназначенный для записи char-значения), смещенный на k позиций по отношению к блоку, на который ссылается указатель q. Условие состоит в том, что адрес этого блока должен быть меньше адреса из указателя end.
В теле цикла командой
*(q + k) = (char)('A' + 2 * k);
В еще одной конструкции цикла for проверяется условие &r[k]<end. Значение выражения r[k] - это значение в блоке, смещенном на k позиций по отношению к блоку, на который ссылается указатель r. В данном случае смещение выполняется на блоки, в которые можно записать int-значения. Выражение &r[k] - это адрес блока, в который записано значение r[k]. Этот адрес должен быть меньше адреса из указателя end.
В теле цикла командой
r[k] = 5 * (k + 1);
При отображении адресов, если используется указатель на значение типа byte, дискретность изменения адреса равна 1. При отображении адресов с помощью указателей на значения типа char дискретность
изменения адреса равна 2. При использовании указателя на значение типа int дискретность изменения адреса равна 4. Фактически дискретность изменения адреса определяется количеством однобайтовых блоков,
используемых для записи значения соответствующего типа.
На следующем шаге мы рассмотрим указатели на экземпляр структуры.