Васильев А.Н. Основы программирования на C#. Васильев А. Н. Программирование
Скачать 5.54 Mb.
|
Задания для самостоятельной работы Предлагаю вам взять несколько журналов в пользу детей Германии. По полтиннику штука. из к/ф Собачье сердце Напишите программу, в которой объявляется перечисление для представления дней недели. Предложите методы (или добавьте в главный метод блоки кода, позволяющие по числовому значению определить день недели (с учетом периодической повторяемости дней, а также позволяющие по двум значениям из перечисления определить минимальное количество дней между соответствующими днями недели Напишите программу, в которой объявляется структура с целочисленным, текстовыми символьным полями. Предложите такие версии конструктора стремя аргументами (целое число, текст, символ, с двумя аргументами (целое число и текст) и с одним аргументом (текст. В структуре должен быть метод, при вызове которого отображаются значения полей экземпляра структуры Глава 3 176 3. Напишите программу, содержащую структуру с полем, которое является символьным массивом. Предложите две версии конструктора с текстовым аргументом (символьный массив формируется на основе текста) и с двумя аргументами (целое число и символ. В последнем случае целочисленный аргумент определяет размер массива, а символьный аргумент определяет значение, которое присваивается всем элементам массива. Опишите в структуре индексатор, который позволял бы считывать значение символа из массива и присваивать значение элементу массива. Предложите метод, который при вызове менял бы порядок символов в массиве на противоположный. Переопределите метод ToString() таким образом, чтобы он возвращал текстовую строку, содержащую символы из массива и числовые коды этих символов Напишите программу, содержащую структуру с целочисленным массивом. Опишите конструктор с одним аргументом, определяющим размер массива. Массив должен заполняться случайными числами. В структуре должны быть методы, возвращающие результатом наибольший элемент в массиве, наименьший элемент в массиве, а также метод, возвращающий среднее значение элементов в массиве (сумма элементов массива, деленная на количество элементов в массиве Напишите программу, содержащую структуру стремя закрытыми целочисленными полями. Предложите версии конструктора с одним, двумя и тремя целочисленными аргументами. В структуре должно быть два целочисленных свойства. Одно свойство результатом возвращает наименьшее из значений полей экземпляра структуры. Другое свойство возвращает наибольшее из значений полей экземпляра структуры. Предложите вариант обработки ситуации, когда первому или второму свойству присваивается значение Напишите программу, в которой есть структура с двумя целочисленными полями. Опишите в структуре операторные методы для выполнения операций сложения, вычитания и умножения двух экземпляров структуры. Во всех случаях результатом должен быть новый экземпляр структуры, значения полей которого вычисляются соответственно как сумма, разность или произведение соответствующих полей экземпля- ров-операндов. Предложите два операторных метода для унарных операторов, которые возвращали бы результатом наибольшее и наименьшее из значений полей экземпляра структуры Перечисления и структуры Напишите программу, в которой есть структура с символьным полем. Также в программе должен быть статический метод, аргументом которому передается текстовое значение. Результатом метод возвращает массив из экземпляров структуры. Размер массива определятся длиной текста. Значения символьных полей экземпляров структуры в массиве задаются символами из текста. Предложите метод, которому в качестве аргумента передается массив из экземпляров структуры, а результатом метод возвращает текстовую строку, состоящую из символьных значений полей экземпляров в массиве Напишите программу, в которой объявлены две структуры. У одной структуры есть текстовое поле, ау другой структуры есть символьное поле. Впервой структуре (с текстовым полем) должен быть метод с целочисленным аргументом, который результатом возвращает экземпляр второй структуры (с символьным полем. Значение символьного поля экземпляра-результата определяется как символ из текста (поле экземпляра, из которого вызывается метод) с индексом, определяемым аргументом метода Напишите программу, в которой есть структура с двумя целочисленными полями. Предложите статический метод, аргументом которому передается целочисленный массив. Результатом метод возвращает экземпляр структуры, первое поле которого равно значению максимального или минимального) элемента в массиве, а второе поле экземпляра равно индексу этого элемента в массиве Напишите программу, в которой объявлены две структуры. У первой структуры есть целочисленное поле, ау второй структуры есть два целочисленных поля. Предложите операторный метод, с помощью которого сумма двух экземпляров первой структуры возвращала бы результатом экземпляр второй структуры (поля экземпляра-результата — это поля суммируемых экземпляров. Во второй структуре опишите метод, который результатом возвращает массив из двух экземпляров первой структуры. В этом случае экземпляр с двумя полями, из которого вызывается метод, разбивается на два экземпляра, у каждого из которых по одному полю Глава 4 УКАЗАТЕЛИ Вот, доктор, что происходит, когда исследователь вместо того, чтобы идти параллельно и ощупью с природой, форсирует вопрос и приоткрывает завесу. из к/ф Собачье сердце» В этой главе речь пойдет о достаточно интересном механизме языка C# — мы будем обсуждать указатели и все, что сними связано. В частности, мы узнаем что представляют собой указатели и как они используются почему код, в котором используются указатели, является небезопасным какие основные операции можно выполнять с указателями как создаются указатели на структуры как указатели связаны с массивами что связывает указатели с текстовыми строками что такое многоуровневая адресация и как она используется. Как всегда, будет много примеров, с помощью которых мы проиллюстрируем практические приемы применения указателей. Знакомство с указателями Пусть меня ждут в пирамиде Хеопса уйму- мии, й коридор налево. из м/ф Приключения капитана Врунгеля» Если мы объявляем переменную базового типа (например, целочисленную, тов памяти под эту переменную выделяется место. Объем Указатели 179 выделяемой памяти зависит от типа переменной. В эту область памяти записывается значение переменной, ив случае необходимости оно оттуда считывается. Когда мы выполняем операции по изменению значения переменной, новое значение заносится в выделенную под переменную область памяти. У этой области памяти есть адрес. Но обычно он нас мало интересует. Мы обращаемся к переменной по имени. Этого вполне достаточно, чтобы присвоить значение переменной, а также изменить или прочитать значение переменной. Другими словами, мы получаем доступ к области памяти не напрямую, а через имя переменной. Вместе стем в языке C# существует механизм, позволяющий обращаться к памяти напрямую, минуя переменную, под которую эта память выделена. Делается это с помощью указателей. Общая идея состоит в том, чтобы узнать (получить) адрес, по которому записана переменная, и затем обращаться к соответствующей области памяти не через имя переменной, а по адресу области памяти. Адрес переменной записывается как значение в другую переменную, которая называется указателем. Таким образом, указатель — это переменная, значением которой является адрес другой переменной НАЗ А МЕТКУ Технически адрес переменной — это некоторое целое число. Гипотетически мы могли бы запоминать адреса как целые числа. Но это бесперспективный подход, поскольку нам адрес как таковой не очень-то и нужен. Нам нужен адрес как средство доступа к области памяти. Поэтому адреса запоминаются в специальных переменных, которые называются указателями. Значения указателей адреса) обрабатываются по специальным правилам (называются адресной арифметикой. Запоминая адреса в специальных переменных, мы как бы даем знать, что это именно адреса, а необычные целые числа, и что обрабатывать данные значения следует как адреса, а не как целые числа. При объявлении указателя важно отобразить два момента. Во-первых, необходимо показать, что речь идет об объявлении указателя. Во-вто- рых, нужно явно указать, какого типа значение может быть записано в область памяти, адрес которой будет содержать указатель. Почему это важно Дело в том, что память разбивается на блоки, размер каждого блока равен 1 байту (это 8 битов. Каждый такой однобайтовый блок имеет адрес. Теперь представим, что объявляется целочисленная Глава переменная типа int. Для такой переменной выделяется 32 бита, что составляет 4 байта. Таким образом, для целочисленной переменной выделяется однобайтовых блока. Если бы переменная была типа char, то для нее выделялось бы 16 битов, или 2 байта — то есть 2 однобайто- вых блока. А для переменной типа byte выделяется всего 1 однобайто- вый блок (8 битов. Если под переменную выделяется несколько одно- байтовых блоков, то адресом области памяти, в которую записывается значение переменной, является адрес первого однобайтового блока. Получается, что по одному лишь адресу мы можем определить место, начиная с которого в памяти записывается значение переменной. Нонам еще нужно знать размер области памяти, которую занимает переменная. Этот размер можно определить по типу переменной. Поэтому при объявлении указателей важно также определить, с переменными какого типа предполагается работать. С учетом двух упомянутых выше обстоятельств объявление указателя происходит следующим образом. Указывается идентификатор типа, соответствующий типу переменной, адрес которой может быть записан в указатель, и символ звездочка *. После этой конструкции указывается имя указателя. Например, если мы хотим объявить указатель на целочисленное значение (то есть указатель, значением которого может быть адрес переменной типа int), то можем использовать следующую инструкцию В данном случае объявляется указатель p, которому в качестве значения могут присваиваться адреса переменных типа int. Мы в таком случае будем говорить, что p является указателем на целочисленное значение типа Если мы хотим объявить два указателя q и r назначение типа double указатели, которым в качестве значений можно присваивать адреса переменных типа double), мы могли бы воспользоваться такой командой Корректным является и такое объявление указателя В данном случае объявляется символьный указатель s — значением указателю можно присвоить адрес переменной символьного типа Указатели 181 Стоит заметить, что объявить указатель можно только для нессылоч- ного типа. Проще говоря, мы не можем объявить указатель на объект класса. Объяснение в данном случае простое и связано стем, что доступ к объекту мы получаем не напрямую, а через объектную переменную. Значение объектной переменной — не объекта ссылка на объект (то есть фактически адрес объекта. Вместе стем, как мы увидим далее, указатели могут применяться при работе с массивами и текстом НАЗ А МЕТКУ При объявлении указателей такие выражения, как int* или то есть выражение вида ɬɢɩ* ), с некоторой натяжкой можно интерпретировать как тип указателя. Во всяком случае, такой подход помогает во многих нетривиальных случаях интуитивно находить правильные ответы и решения. Также стоит заметить, что в языке C++ объявление double* q,r означает, что объявляется указатель q назначение типа double и переменная (обычная, не указатель) r типа double . То есть в языке C++ звездочка * в объявлении указателя применяется только к первой переменной. В языке C# это не так командой double* q,r объявляется два указателя ( q и r ) назначение типа В правиле, согласно которому при объявлении указателя нужно идентифицировать тип значения, на которое может ссылаться указатель, имеется исключение мы можем объявить указатель назначение неопределенного типа. В таком случаев качестве идентификатора типа используется ключевое слово void. Вот пример объявления такого указателя Данной командой объявлен указатель pnt, и при этом не определен тип значения, на которое может ссылаться указатель (тип переменной, адрес которой может быть присвоен указателю. Такой указатель можно использовать только для запоминания адреса. Никакие иные операции с таким указателем выполнить не удастся. Оно и понятно. Ведь если неизвестно, какого типа переменная записана в области памяти сданным адресом, то и неизвестно, какой объем памяти она занимает. А если объем доступной памяти неизвестен, то и операции с ней не выполняются Глава Итак, мы выяснили, как указатель объявляется. Но что же с ним делать Есть две базовые операции, с которых мы и начнем наше знакомство с использованием указателей. Операции такие Получение адреса переменной Получение доступа к значению, записанному по адресу. Для получения адреса переменной используют инструкцию & (ампер- санд). Если ее указать перед именем переменной (получается выражение вида & ɩɟɪɟɦɟɧɧɚɹ), то значением такого выражения будет адрес области памяти, в которую записано значение переменной. Например, имеется целочисленная переменная n типа int и указатель p назначение типа int, объявленные следующим образом n; // ɉɟɪɟɦɟɧɧɚɹ int* p; // Тогда значением выражения &n является адрес переменной n. Поскольку переменная n объявлена с типом int и указатель p также объявлен с идентификатором int, то адрес переменной n может быть присвоен указателю p. Корректной является следующая команда: p=&n; Обратная операция к получению адреса переменной — получение доступа к значению, записанному по адресу. Для этого перед указателем, содержащим соответствующий адрес, нужно указать звездочку * (получается выражение вида * ɭɤɚɡɚɬɟɥɶ). Скажем, выражение *p — это значение, записанное по адресу, содержащемуся в указателе p. Причем с помощью выражения вида * ɭɤɚɡɚɬɟɥɶ можно не только прочитать значение, записанное по адресу, но и записать поэтому адресу новое значение. В частности, мы могли бы воспользоваться такими командами Первой из представленных двух команд (выражение *p=123) по адресу из указателя p записывается целочисленное значение 123. Если значение указателю p присваивается командой p=&n, то речь фактически идет о переменной n. То есть командой *p=123 значение Указатели 183 присваивается переменной n. При выполнении команды Console. WriteLine(*p) в консольном окне отображается значение, записанное по адресу из указателя p. Несложно догадаться, что речь идет о значении переменной p. q ПОДРОБНОСТИ bУказатель позволяет получить доступ к области памяти, выделенной под переменную, минуя непосредственное обращение к этой переменной. На каком-то этапе мы узнаем адрес области памяти, в которой хранится значение переменной, и с помощью указателя получаем доступ к этой области памяти. С помощью указателя мы можем прочитать значение, записанное в память, а также мы можем записать туда новое значение. И здесь критически важно знать, какой объем памяти следует использовать при чтении/запи- си значений. Также нужно знать, как интерпретировать прочитанное значение (скажем, как целочисленное неотрицательное значение, целочисленное значение со знакомили символ. Все это определяется по идентификатору типа, указанному при объявлении указателя. Этот тип должен совпадать с типом переменной, адрес которой заносится в указатель. В случае необходимости можно использовать явное приведение типа. Такие ситуации рассматриваются немного позже. Перед тем как рассмотреть первый пример, отметим некоторые особенности способа реализации программ, в которых используются ука- затели. Как мы увидим далее, получив доступ к области памяти, мы автоматически получаем доступ и к соседним ячейкам. Формально это означает, что мы получаем возможность выполнять операции с памятью напрямую. Такие операции считаются небезопасными, поскольку исполнительная система, под управлением которой выполняются программы, не может проконтролировать безопасность выполнения этих операций. Поэтому соответствующий код считается небезопасным — основной груз ответственности по реализации корректной и неконфликтной работы программы ложится на плечи программиста. В языке C# небезопасный код выделяется в отдельные блоки, которые помечаются ключевым словом unsafe . Например, если в главном методе программы используются указатели, то главный метод можно описать с ключевым словом unsafe или пометить этим ключевым словом блок, в котором указатели задействованы. Также следует изменить настройки приложения, разрешив Глава использование небезопасного кода. Для этого на вкладке свойств проекта (можно открыть с помощью команды Properties в контекстном меню проекта или в главном меню Project) в разделе сборки (раздел Build) следует установить флажок опции, разрешающей использование небезопасного кода (опция Allow unsafe Теперь перейдем к рассмотрению примера, в котором используются указатели. Интересующая нас программа представлена в листинге Листинг 4.1. Знакомство с указателями System; // Ʉɥɚɫɫ ɫ ɝɥɚɜɧɵɦ ɦɟɬɨɞɨɦ: class PointersDemo{ // Ƚɥɚɜɧɵɣ ɦɟɬɨɞ (ɨɩɢɫɚɧ ɫ ɤɥɸɱɟɜɵɦ ɫɥɨɜɨɦ 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); Для того чтобы скомпилировать эту программу, после того как создан проект и введен программный код, следует открыть окно свойств проекта. Например, в контекстном меню проекта выбрать команду |