На этом шаге мы рассмотрим еще один пример использования адресной арифметики.
Еще один пример, в котором иллюстрируются операции с указателями, представлен в примере ниже. В каком-то смысле он напоминает предыдущую программу (из предыдущего шага), но реализовано все немного иначе.
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; ,
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);
На следующем шаге мы рассмотрим указатели на экземпляр структуры.