Васильев А.Н. Основы программирования на C#. Васильев А. Н. Программирование
Скачать 5.54 Mb.
|
Properties, как показано на рис. 4.1. Глава Рис. 4.1. В контекстном меню проекта выбирается команда Должна открыться вкладка свойств проекта, как показано на рис. Там следует выбрать раздели установить флажок опции Allow unsafe code (см. рис. 4.2). После этого вкладку свойств проекта можно закрыть. Для компилирования и запуска программы на выполнение используем команду Start Without Debugging изменю рис. 4.3) или нажимаем комбинацию клавиш Указатели 187 Рис. 4.2. Настройка режима использования небезопасного кода на вкладке свойств проекта Рис. 4.3. Запуск программы на выполнение с помощью команды Start Without Debugging изменю Глава Результат выполнения программы представлен ниже: Результат выполнения программы (из листинга 4.1) Ɂɧɚɱɟɧɢɟ ɩɟɪɟɦɟɧɧɨɣ n=123 Ɂɧɚɱɟɧɢɟ ɜɵɪɚɠɟɧɢɹ *p=123 Ⱥɞɪɟɫ ɩɟɪɟɦɟɧɧɨɣ n: 2028064 Ⱥɞɪɟɫ ɜ ɭɤɚɡɚɬɟɥɟ p: 2028064 Ⱥɞɪɟɫ ɜ ɭɤɚɡɚɬɟɥɟ q: 2028064 Ⱥɞɪɟɫ ɜ ɭɤɚɡɚɬɟɥɟ s: 2028064 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ int: 65601 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ byte: 65 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ char: 'A' Ɂɧɚɱɟɧɢɟ ɩɟɪɟɦɟɧɧɨɣ n=65601 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ int: 65606 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ byte: 70 Ɂɧɚɱɟɧɢɟ ɬɢɩɚ char: 'F' Ɂɧɚɱɟɧɢɟ ɩɟɪɟɦɟɧɧɨɣ Главный метод программы описывается с ключевым словом unsafe. В теле метода объявляется переменная n типа int, а также указатель p назначение типа int. Командой p=&n указателю p значением присваивается адрес переменной n. Причем происходит это еще до того, как переменной присвоено значение. Но проблемы здесь нет, поскольку значение указателя p — это адрес переменной n. А адресу переменной появляется в результате объявления этой переменной. То есть, несмотря на то что значение переменной n еще не присвоено, адресу нее уже имеется. Значение переменной n присваивается не напрямую, а через указатель p . Для этого мы использовали команду *p=123. В результате в область памяти, выделенную под переменную n, записывается значение 123. Именно это значение мы получаем, когда проверяем значение переменной или значение выражения *p. Значение выражения *p — это число, Указатели 189 записанное по адресу, который является значением указателя p. Чтобы узнать этот адрес, мы используем выражение (uint)p, в котором значение указателя p явно приводится к формату целого неотрицательного числа типа uint. Само по себе значение адреса мало о чем говорит. Более того, он от запуска к запуску меняется, поскольку при разных запусках программы под переменную может выделяться область памяти в разных местах. Важно то, что если уж переменная получила адрес, то он будет неизменным до окончания выполнения программы. Кроме указателя p, в программе командами byte* q и char* s объявляются еще два указателя. Указатель q может ссылаться назначение типа byte , а указатель s может содержать в качестве значения адрес области памяти, в которую записано значение типа char. После этого командами и s=(char*)p указателям присваиваются значения. Несложно сообразить, что оба указателя q и s получают в качестве значения тот же адрес, что записан в указатель p. Убедиться в этом несложно достаточно сравнить значения выражений (uint)p, (uint)q и (uint)s. От запуска к запуску значения могут быть разными, номе- жду собой они всегда совпадают, поскольку речь идет об одном и том же адресе, записанном в указатели p, q и s. В чем же тогда разница между этими указателями А разница в том, какое количество однобайтовых ячеек попадает под контроль указателя. Адрес, записанный в указатель это адрес одной однобайтовой ячейки. Если мы получаем доступ к памяти через указатель p, то, поскольку он предназначен для работы с целыми числами, операции выполняются с четырьмя однобайтовыми ячейками той, чей адрес записан в указатель, и еще тремя соседними. Причем значение, которое записывается в память, и значение, считываемое из памяти, интерпретируются как целые числа типа int. Если доступ к памяти получаем с помощью указателя s, то операции выполняются с двумя ячейками памяти и значение в этих двух ячейках интерпретируется как символьное (значение типа char). Наконец, если мы получаем доступ к памяти с помощью указателя q, то операции выполняются только с одной ячейкой, а значение в этой ячейке интерпретируется как целочисленное значение типа Командой n=65601 переменной n присваивается новое значение. Число можно представить так 65 601 = 65 536 + 64 + 1 = 2 16 + 2 6 + 2 Поэтому в двоичном коде из 32 битов число 65 601 выглядит как 00000000 00000001 00000000 01000001. Первый, самый младший байт содержит код 01000001, второй байт нулевой (код 00000000), третий байт имеет код 00000001, и четвертый, самый старший байт Глава тоже нулевой (код 00000000). Значение выражения *p вычисляется по всем четырем байтами интерпретируется как число типа int. Поэтому мы ожидаемо получаем значение переменной При вычислении значения выражения *q используется только первый байт с кодом 01000001. Этот код интерпретируется как неотрицательное целочисленное значение. Если перевести данный двоичный код в десятичный, то получим число Наконец, значение выражения *s вычисляется по двум байтам (первому и второму. Получаем бинарный код 00000000 01000001, в котором второй байт нулевой (состоит из одних нулей. Поэтому формально бинарный код соответствует числу 65. Но поскольку s является указателем на символьное значение, то результат интерпретируется как символ. В кодовой таблице символов код 65 имеет буква ƍAƍ (английский алфавит). При выполнении команды *s= ƍFƍ в первый и второй байты записывается код символа ƍFƍ. Код этого символа равен 70. Число 70 в двоичном коде (из 16 битов) выглядит как 00000000 01000110. То есть второй байт как был нулевым, так нулевыми остался, а первый изменился была стал 01000110. Все четыре байта теперь имеют код 00000000 00000001 00000000 01000110. Этот код соответствует числу 65 606. Такое значение получаем при вычислении значения выражения, оно же новое значение переменной Когда вычисляется значение выражения *q, то используется только первый байт с кодом 01000110, означающим число 70. При проверке значения выражения *s естественным образом получаем значение Адресная арифметика Завтра вечером вылетаете за товаром в квадрат. Будьте внимательны, помните остро- жайшей секретности этой операции. из м/ф Приключения капитана Врунгеля» Выше мы видели, что адрес — это целое число. Мы можем представлять себе одномерную последовательность ячеек (хотя в действительности все намного сложнее — нов данном случае это неважно, каждая размером в 1 байт. У каждой ячейки есть адрес (целое число. Будем исходить Указатели 191 из того, что адрес увеличивается справа налево. Адреса соседних ячеек отличаются на 1. Если имеется некоторая ячейка с адресом, то соседняя ячейка справа имеет адрес на единицу меньше, а соседняя ячейка слева имеет адрес на единицу больше. Для записи значений может использоваться несколько ячеек (будем называть это блоком) — все зависит от типа значения. Например, значение типа double занимает 8 ячеек, значение типа int занимает 4 ячейки, значение типа char занимает 2 ячейки, а значение типа byte занимает 1 ячейку. То есть для записи значений разных типов используются блоки памяти разного размера. В указатель при этом записывается адрес первой ячейки в блоке. Поэтому если в памяти подряд размещено два значения типа byte, то адреса блоков, в которые записаны эти значения, будут отличаться на 1. Если рядом в памяти два блока с значениями, то адреса этих блоков отличаются на 4. Адреса соседних блоков отличаются на 8. Таким образом, дискретность изменения адресов соседних блоков памяти определяется типом значений, которые записаны в эти блоки НАЗ А МЕТКУ Здесь приводятся объемы памяти, которые должны выделяться для значений разных типов по стандарту языка C#. Узнать фактический объем памяти (в байтах, выделяемый компилятором под значение определенного типа, можно с помощью инструкции sizeof . Например, результатом выражения sizeof(int) является целое число, равное количеству байтов, выделяемых для значения типа int (должно быть равно 4). Значение выражения sizeof(char) — объем памяти (в байтах, выделяемый для значения типа char (по стандарту должно быть Считывание значения из области памяти и запись в область памяти значения с использованием указателей — далеко не единственные поддерживаемые в C# операции. Есть группа важных и полезных операций, которые могут выполняться с указателями НАЗ А МЕТКУ Далее под ячейкой будет подразумеваться однобайтовый блока под блоком в общем случае мы будем подразумевать группу из нескольких ячеек. Фраза указатель ссылается на ячейку понимается в том смысле, что указатель содержит адрес соответствующей ячейки в качестве значения Глава 4 192 • Указатели можно сравнивать с помощью операторов ==, !=, <, >, <= и >=. При этом сравниваются числовые значения адресов Можно вычислять разность указателей (одного типа. Результатом будет целое число, равное количеству блоков между ячейками, на которые ссылаются указатели. Другими словами, целое число определяет, насколько блоков памяти смещена одна ячейка по отношению к другой. Размер блока памяти (единица, в которой измеряется смещение) определяется типом базового значения, указанного при объявлении указателей. Допустим, p и q являются указателями назначения типа int, а разность указателей q-p равна 3. Это означает, что если взять ячейку с адресом из указателя p и выполнить смещение на 3 блока (в направлении увеличения адреса, то получим ячейку, адрес которой записан в указатель q. Поскольку один блок — это 4 однобайтовых блока, то разность q-p, равная 3, означает, что ячейка, на которую ссылается указатель q, смещена по отношению к ячейке, на которую ссылается указатель p, на 12 од- нобайтовых блоков. Соответственно, разность адресов в указателях q и p равна 12. Если бы указатели p и q были предназначены для работы с значениями, то речь бы шла о смещении на 3 блока, каждый из которых состоит из 2 ячеек (значение типа char записывается в 2 однобайтовых блока. Поэтому адрес ячейки, на которую ссылается указатель q, был бы больше адреса из указателя p на 6. Отрицательная разность указателей означает, что смещение выполняется уменьшением адреса К указателю можно прибавлять целое число, а также из указателя можно вычитать целое число. Результатом будет адрес ячейки, смещенной по отношению к адресу из исходного указателя на количество блоков, определяемых прибавляемым или вычитаемым числом. Размер блока определяется типом, указанным при объявлении указателя. При прибавлении положительного числа адрес увеличивается, а при вычитании положительного числа адрес уменьшается. Так, если p является указателем для значений, то результатом выражения является адрес ячейки, смещенной (с увеличением адреса) по отношению к ячейке с адресом из указателя p на 3 блока или 12 однобайтовых блоков. Результат выражения p-3 вычисляется аналогичным образом, но адрес при этом уменьшается Указатели можно индексировать после имени указателя в квадратных скобках указывается целочисленный индекс, который может быть в том числе и отрицательным. Результатом выражения Указатели 193 на основе проиндексированного указателя является значение, записанное в блоке, смещенном по отношению к ячейке, на которую ссылается указатель. Количество блоков для смещения определяется индексом. Если индекс положительный — адрес при смещении увеличивается, если индекс отрицательный — адрес при смещении уменьшается. Через выражение на основе проиндексированного указателя можно прочитать значение в блоке памяти и записать значение в этот блок памяти (присвоив выражению значение. Например, если p является указателем назначение, то выражение p[3] представляет собой значение в блоке, смещенном на 3 int- блока по отношению к ячейке с адресом из указателя Перечисленные выше правила имеют простые и очевидные последствия. Например, если указатели (одного типа) ссылаются на соседние блоки, то разность этих указателей равна 1 или -1, в зависимости оттого, как относительно друг друга расположены блоки. При этом разность адресов, записанных в указатели, отличается назначение, равное объему (в байтах) блока памяти, выделяемого для записи значения данного типа. Далее, если p — некоторый указатель, то p[0] — значение, записанное в блоке, на который ссылается p (то есть тоже, что и *p). Значениями выражений p+1 и p-1 являются адреса блоков, соседних с блоком, на который ссылается указатель p. Если k — целое число, то выражение p[k] эквивалентно выражению *(p+k), итак далее (таких интересных соотношений довольно много). Для большей наглядности рассмотрим небольшой пример, в котором используются правила адресной арифметики (а если более конкретно, то индексируются указатели. Обратимся к программе в листинге Листинг 4.2. Индексирование указателей System; class MiniArrayDemo{ // Ƚɥɚɜɧɵɣ ɦɟɬɨɞ: unsafe static void Main(){ // Ɉɛɴɹɜɥɟɧɢɟ ɱɢɫɥɨɜɨɣ ɩɟɪɟɦɟɧɧɨɣ: double miniarray; // Ɋɚɡɦɟɪ Ǝbyte-ɦɚɫɫɢɜɚƎ: int m=sizeof(double)/sizeof(byte); Глава 4 194 // ɍɤɚɡɚɬɟɥɶ ɧɟɨɩɪɟɞɟɥɟɧɧɨɝɨ ɬɢɩɚ: void* pnt; // Ɂɧɚɱɟɧɢɟ ɭɤɚɡɚɬɟɥɹ: pnt=&miniarray; // ɍɤɚɡɚɬɟɥɶ ɧɚ byte-ɡɧɚɱɟɧɢɟ: byte* p; // Ɂɧɚɱɟɧɢɟ ɭɤɚɡɚɬɟɥɹ: p=(byte*)pnt; // ɉɟɪɟɛɨɪ ɛɥɨɤɨɜ ɩɚɦɹɬɢ ɫ ɩɨɦɨɳɶɸ ɭɤɚɡɚɬɟɥɹ: for(int k=0;k ȼ ɛɥɨɤ ɩɚɦɹɬɢ ɡɚɩɢɫɵɜɚɟɬɫɹ ɡɧɚɱɟɧɢɟ: p[k]=(byte)(k+1); // Ɉɬɨɛɪɚɠɟɧɢɟ ɡɧɚɱɟɧɢɹ ɢɡ ɛɥɨɤɚ ɩɚɦɹɬɢ: Console.Write( Ǝ|Ǝ+p[k]); } Console.WriteLine( Ǝ|Ǝ); // ɇɨɜɨɟ ɡɧɚɱɟɧɢɟ ɭɤɚɡɚɬɟɥɹ: p=(byte*)pnt+m-1; // ɉɟɪɟɛɨɪ ɛɥɨɤɨɜ ɩɚɦɹɬɢ ɫ ɩɨɦɨɳɶɸ ɭɤɚɡɚɬɟɥɹ: for(int k=0;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 Указатели // ȼ ɛɥɨɤ ɩɚɦɹɬɢ ɡɚɩɢɫɵɜɚɟɬɫɹ ɡɧɚɱɟɧɢɟ: q[k]=(char)( ƍAƍ+k); // Ɉɬɨɛɪɚɠɟɧɢɟ ɡɧɚɱɟɧɢɹ ɢɡ ɛɥɨɤɚ ɩɚɦɹɬɢ: Console.Write( Ǝ|Ǝ+q[k]); } Console.WriteLine( Ǝ|Ǝ); // ɇɨɜɨɟ ɡɧɚɱɟɧɢɟ ɭɤɚɡɚɬɟɥɹ: q=(char*)pnt+n-1; // ɉɟɪɟɛɨɪ ɛɥɨɤɨɜ ɩɚɦɹɬɢ ɫ ɩɨɦɨɳɶɸ ɭɤɚɡɚɬɟɥɹ: for(int k=0;k ɂɫɩɨɥɶɡɨɜɚɧ ɨɬɪɢɰɚɬɟɥɶɧɵɣ ɢɧɞɟɤɫ: Console.Write( Ǝ|Ǝ+q[-k]); } Console.WriteLine( Ǝ|Ǝ); Результат выполнения программы следующий: Результат выполнения программы (из листинга В программе реализована очень простая идея. Объявляется переменная типа double. Под такую переменную выделяется 8 байтов. Сначала каждый такой байт интерпретируется как переменная типа byte занимает байт памяти. Получается 8 однобайтовых блоков, в каждый из которых записывается значение. Индексируя указатели, мы можем получать доступ к этим блокам как к элементам массива. Эту же область памяти размером в 8 байтов, выделенную под double- переменную, можно интерпретировать как 4 блока по 2 байта. В каждый из этих 4 блоков можно записать значение (под переменную типа Глава 4 196 char выделяется 2 байта. Получая доступ к значениям путем индексирования указателя, мы создаем иллюзию массива из 4 символьных элементов. Таким образом, одну и туже область памяти можем использовать как массив из 8 элементов или как массив из 4 элементов. Если более детально, тов программе объявляется переменная miniarray типа double. Значение целочисленной переменной m вычисляется выражением sizeof(double)/sizeof(byte). Это отношение объема памяти (в байтах, выделяемой для значений типа double (значение 8), и объема памяти, выделяемой для значений типа byte (значение 1). Также командой void* pnt мы объявляем указатель неопределенного типа. Операции адресной арифметики к такому указателю неприменимы. Фактически все, что можно с таким указателем сделать, записать в него адрес. Именно это мы делаем с помощью команды, которой указателю pnt в качестве значения присваивается адрес переменной miniarray. q ПОДРОБНОСТИ bОбласть памяти, выделенной под переменную miniarray , состоит из 8 блоков, размер каждого блока равен 1 байту. Адрес переменной miniarray — это адрес первого из 8 блоков. Именно адрес первого блока записывается в указатель Командой byte* p объявляется указатель p назначение типа byte. Значение указателю присваиваем командой p=(byte*)pnt. В этом случае, во-первых, адрес из указателя pnt копируется в указатель p. Во-вторых, пришлось использовать явное приведение типа. Без этого не обойтись, поскольку pnt является указателем назначение неопределенного типа, а указатель p объявлен для работы с значениями. В итоге указатель содержит адрес первого из 8 блоков, выделенных для переменной miniarray { i НАЗ А МЕТКУ Хотя указатели pnt и p содержат один и тот же адрес, между ними есть принципиальная разница. Поскольку тип данных, на которые может ссылаться указатель p , определен, ток указателю p могут применяться операции адресной арифметики Указатели 197 Для записи значений в однобайтовые блоки запускаем оператор цикла, в котором индексная переменная k пробегает значения от 0 до m-1 включительно (то есть перебираются все однобайтовые блоки. За каждый цикл при заданном индексе k сначала выполняется команда p[k]=(byte)(k+1) , которой в однобайтовый блок записывается значение, а затем это значение отображается в консольном окне с помощью команды Console.Write( Ǝ|Ǝ+p[k]). В обеих командах использована инструкция видав которой индексируется указатель p. Выражение представляет собой значение, записанное в блок памяти, смещенный на k позиций по отношению к блоку памяти, на который ссылается указатель p. Поскольку указатель p объявлен для работы со значениями типа byte, а под значение этого типа выделяется 1 байт, то при смещениях используются однобайтовые блоки ПОДРОБНО СТ ИВ команде p[k]=(byte)(k+1) использовано явное приведение к типу byte . Необходимость явного приведения типа обусловлена тем, что сумма k+1 представляет собой значение типа int , а выражение подразумевает работу со значениями типа При выполнении команды p=(byte*)pnt+m-1 указателю p присваивается новое значение. Это адрес последнего блока в области памяти, выделенной для переменной miniarray типа double. В соответствии с использованной командой значение указателя pnt приводится к типу byte* , а к полученному результату прибавляется число m-1 (точнее, сначала прибавляется значение переменной m, а затем вычитается число. Приведение к типу byte* необходимо для того, чтобы с указателем (адресом) можно было выполнять арифметические операции в том числе и прибавлять к указателю числа. Значением выражения (byte*)pnt+m-1 является адрес блока памяти, смещенного на m-1 позиций по отношению к блоку, на который ссылается указатель pnt. Поскольку в приведенной команде мы указатель pnt приводили к типу byte* , а под значение типа byte выделяется 1 байт памяти, то смещение на одну позицию означает смещение на один однобайтовый блок. Таким образом, результат выражения (byte*)pnt+m-1 — это адрес одно- байтового блока, смещенного по отношению к первому блоку (его адрес записан в указатель pnt) на m-1 позиций. Это последний блок в области памяти, выделенной под переменную miniarray типа double. Глава 4 |