Шаг 206.
Язык программирования C#. Начала.
Указатели. Знакомство с указателями (окончание)

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

    Перед тем как рассмотреть первый пример, отметим некоторые особенности способа реализации программ, в которых используются указатели.

    Как мы увидим далее, получив доступ к области памяти, мы автоматически получаем доступ и к соседним ячейкам. Формально это означает, что мы получаем возможность выполнять операции с памятью напрямую. Такие операции считаются небезопасными, поскольку исполнительная система, под управлением которой выполняются программы, не может проконтролировать безопасность выполнения этих операций. Поэтому соответствующий код считается небезопасным - основной груз ответственности по реализации корректной и неконфликтной работы программы ложится на плечи программиста. В языке C# небезопасный код выделяется в отдельные блоки, которые помечаются ключевым словом unsafe. Например, если в главном методе программы используются указатели, то главный метод можно описать с ключевым словом unsafe (или пометить этим ключевым словом блок, в котором указатели задействованы). Также следует изменить настройки приложения, разрешив использование небезопасного кода. Для этого на вкладке свойств проекта (можно открыть с помощью команды Properties (Свойства) в контекстном меню проекта или в главном меню Project (Проект)) в разделе сборки (раздел Build (Построение)) следует установить флажок опции, разрешающей использование небезопасного кода (опция Allow unsafe code (Разрешить небезопасный код)).

    Теперь перейдем к рассмотрению примера, в котором используются указатели. Интересующая нас программа представлена в примере ниже.

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

namespace pr206_1
{
    // Класс с главным методом: 
    class Program
    {
        // Главный метод (описан с ключевым словом unsafe):
        unsafe static void Main()
        {
            // Объявление целочисленной переменной: 
            int n;
            // Объявление указателя на значение типа int: 
            int* p;
            // Присваивание указателю значения: 
            p = &n;
            // Через указатель присваивается значение переменной:
            *p = 123;
            // Отображение значения переменной:
            Console.WriteLine("Значение переменной n={0}", n);
            Console.WriteLine("Значение выражения *p={0}", *p);
            Console.WriteLine("Адрес переменной n: {0}", (uint)p);
            Console.WriteLine();
            // Объявление указателя на значение типа byte: 
            byte* q;
            // Объявление указателя на значение типа char: 
            char* s;
            // Присваивание указателей и явное приведение типа:
            q = (byte*)p;
            s = (char*)p;
            // Переменной присваивается новое значение: 
            n = 65601;
            // Проверка значений указателей:
            Console.WriteLine("Адрес в указателе p: {0}", (uint)p);
            Console.WriteLine("Адрес в указателе q: {0}", (uint)q);
            Console.WriteLine("Адрес в указателе s: {0}", (uint)s); 
            Console.WriteLine();
            // Отображение значения через указатель:
            Console.WriteLine("Значение типа int: {0}", *p);
            Console.WriteLine("Значение типа byte: {0}", *q);
            Console.WriteLine("Значение типа char: \'{0}\'", *s);
            Console.WriteLine("Значение переменной n={0}", n);
            Console.WriteLine();
            // Присваивание значения через указатель:
            *s = 'F';
            // Проверка значений:
            Console.WriteLine("Значение типа int: {0}", *p);
            Console.WriteLine("Значение типа byte: {0}", *q);
            Console.WriteLine("Значение типа char: \'{0}\'", *s);
            Console.WriteLine("Значение переменной n={0}", n);
            // Задержка:
            Console.ReadLine();
        }
    }
}
Архив проекта можно взять здесь.

    Для того чтобы скомпилировать эту программу, после того как создан проект и введен программный код, следует открыть окно свойств проекта. Например, в пункте меню Project (Проект) выбрать команду Properties (Свойства), как показано на рисунке 1.


Рис.1. В пункте меню Project (Проект) выбирается команда Properties (Свойства)

    Должна открыться вкладка свойств проекта, как показано на рисунке 2.


Рис.2. Настройка режима использования небезопасного кода на вкладке свойств проекта

    Там следует выбрать раздел Build (Построение) и установить флажок опции Allow unsafe code (Разрешить небезопасный код) (рисунок 2). После этого вкладку свойств проекта можно закрыть. Для компилирования и запуска программы на выполнение используем команду Start Without Debugging (Запуск без отладки) из меню Debug (Отладка) (рисунок 3) или нажимаем комбинацию клавиш Ctrl+F5.


Рис.3. Запуск программы на выполнение

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


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

    Главный метод программы описывается с ключевым словом unsafe. В теле метода объявляется переменная n типа int, а также указатель p на значение типа int. Командой

  p = &n; 
указателю p значением присваивается адрес переменной n. Причем происходит это еще до того, как переменной n присвоено значение. Но проблемы здесь нет, поскольку значение указателя p - это адрес переменной n. А адрес у переменной появляется в результате объявления этой переменной. То есть, несмотря на то что значение переменной n еще не присвоено, адрес у нее уже имеется.

    Значение переменной n присваивается не напрямую, а через указатель p. Для этого мы использовали команду

  *p = 123;              . 
В результате в область памяти, выделенную под переменную n, записывается значение 123. Именно это значение мы получаем, когда проверяем значение переменной n или значение выражения *p. Значение выражения *p - это число, записанное по адресу, который является значением указателя р. Чтобы узнать этот адрес, мы используем выражение (uint)p, в котором значение указателя р явно приводится к формату целого неотрицательного числа типа uint. Само по себе значение адреса мало о чем говорит. Более того, он от запуска к запуску меняется, поскольку при разных запусках программы под переменную может выделяться область памяти в разных местах. Важно то, что если уж переменная получила адрес, то он будет неизменным до окончания выполнения программы.

    Кроме указателя р, в программе командами

  byte* q;
и
  char* s; 
объявляются еще два указателя. Указатель q может ссылаться на значение типа byte, а указатель s может содержать в качестве значения адрес области памяти, в которую записано значение типа char. После этого командами
  q = (byte*)p;
и
  s = (char*)p;
указателям присваиваются значения. Несложно сообразить, что оба указателя q и s получают в качестве значения тот же адрес, что записан в указатель р. Убедиться в этом несложно - достаточно сравнить значения выражений (uint)p, (uint)q и (uint)s. От запуска к запуску значения могут быть разными, но между собой они всегда совпадают, поскольку речь идет об одном и том же адресе, записанном в указатели p, q и s. В чем же тогда разница между этими указателями? А разница в том, какое количество однобайтовых ячеек "попадает под контроль" указателя. Адрес, записанный в указатель, - это адрес одной однобайтовой ячейки. Если мы получаем доступ к памяти через указатель p, то, поскольку он предназначен для работы с целыми числами, операции выполняются с четырьмя однобайтовыми ячейками: той, чей адрес записан в указатель, и еще тремя соседними. Причем значение, которое записывается в память, и значение, считываемое из памяти, интерпретируются как целые числа типа int. Если доступ к памяти получаем с помощью указателя s, то операции выполняются с двумя ячейками памяти и значение в этих двух ячейках интерпретируется как символьное (значение типа char). Наконец, если мы получаем доступ к памяти с помощью указателя q, то операции выполняются только с одной ячейкой, а значение в этой ячейке интерпретируется как целочисленное значение типа byte.

    Командой

  n = 65601; 
переменной n присваивается новое значение. Число 65601 можно представить так:
  65 601 = 65 536 + 64 + 1 = 216 + 26 + 20. 
Поэтому в двоичном коде из 32 битов число 65 601 выглядит как
  00000000 00000001 00000000 01000001. 
Первый, самый младший байт содержит код 01000001, второй байт нулевой (код 00000000), третий байт имеет код 00000001, и четвертый, самый старший байт тоже нулевой (код 00000000). Значение выражения вычисляется по всем четырем байтам и интерпретируется как число типа int. Поэтому мы ожидаемо получаем значение переменной n.

    При вычислении значения выражения *q используется только первый байт с кодом 01000001. Этот код интерпретируется как неотрицательное целочисленное значение. Если перевести данный двоичный код в десятичный, то получим число 65.

    Наконец, значение выражения *s вычисляется по двум байтам (первому и второму). Получаем бинарный код 00000000 01000001, в котором второй байт нулевой (состоит из одних нулей). Поэтому формально бинарный код соответствует числу 65. Но поскольку s является указателем на символьное значение, то результат интерпретируется как символ. В кодовой таблице символов код 65 имеет буква 'A' (английский алфавит).

    При выполнении команды

  *s = 'F';
в первый и второй байты записывается код символа 'F'. Код этого символа равен 70. Число 70 в двоичном коде (из 16 битов) выглядит как 00000000 01000110. То есть второй байт как был нулевым, так нулевым и остался, а первый изменился: был 01000001, а стал 01000110. Все четыре байта теперь имеют код 00000000 00000001 00000000 01000110. Этот код соответствует числу 65 606. Такое значение получаем при вычислении значения выражения , оно же новое значение переменной n.

    Когда вычисляется значение выражения *q, то используется только первый байт с кодом 01000110, означающим число 70. При проверке значения выражения *s естественным образом получаем значение 'F'.

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




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