ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В C++ (4-Е ИЗДАНИЕ) (часть 10) онлайн
Упражнения
Решения к упражнениям, помеченным знаком *, можно найти в приложении Ж.
*1. Напишите программу, которая принимает группу чисел от пользователя и помещает их в массив типа float. После того как числа будут помещены в массив, программа должна подсчитать их среднее арифметическое и вывес- ти результат на дисплей. Используйте указатели везде, где только возможно.
*2. Используйте класс String из примера NEWSTR этой главы. Добавьте к нему метод upit(), который будет переводить символы строки в верхний регистр. Вы можете использовать библиотечную функцию toupper(), которая прини- мает отдельный символ в качестве аргумента и возвращает символ, пере- веденный в верхний регистр (если это необходимо). Эта функция исполь-
зует заголовочный файл Cctype. Добавьте в функцию main() необходимые строки для тестирования метода upit().
*3. Используйте массив указателей на строки, представляющие собой назва- ния дней недели, из примера PTROSTR этой главы. Напишите функции для сортировки этих строк в алфавитном порядке, используя в качестве осно- вы функции bsort() и order() из программы PTRSORT этой главы. Сортиро- вать необходимо указатели на строки, а не сами строки.
*4. Добавьте деструктор в программу LINKLIST. Он должен удалять все эле- менты списка при удалении объекта класса linklist. Элементы должны удаляться по очереди, в соответствии с их расположением в списке. Про- тестируйте деструктор путем вывода сообщения об удалении каждого из элементов списка; удалено должно быть также количество элементов, ка- кое было положено в список (деструктор вызывается автоматически для каждого существующего объекта).
5. Предположим, что в функции main() определены три локальных массива одинакового размера и типа (скажем, float). Первые два уже инициализи- рованы значениями. Напишите функцию addarrays(), которая принимает в качестве аргументов адреса грех массивов, складывает соответствующие элементы двух массивов и помещает результат в третий массив. Четвертым аргументом этой функции может быть размерность массивов. На всем протяжении программы используйте указатели.
6. Создайте свою версию библиотечной функции strcmp(s1, s2), которая срав- нивает две строки и возвращает -1, если s1 идет первой по алфавиту, 0, если в s1 и s2 одинаковые значения, и 1, если s2 идет первой по алфавиту. Назовите вашу функцию compstr(). Она должна принимать в качестве ар- гументов два указателя на строки char*, сравнивать эти строки посим- вольно и возвращать число int. Напишите функцию main() для проверки работы вашей функции с разными строками. Используйте указатели во всех возможных ситуациях.
7. Модифицируйте класс person из программы PERSORT этой главы так, что- бы он включал в себя не только имя человека, но и сведения о его зарпла- те в виде поля salary типа float. Вам будет необходимо изменить методы setName() и printName() на setData() и printData(), включив в них возмож- ность ввода и вывода значения salary, как это можно сделать с именем. Вам также понадобится метод getSalary(). Используя указатели, напишите функцию salsort(), которая сортирует указатели массива persPtr по значе- ниям зарплаты. Попробуйте вместить всю сортировку в функцию salsort(), не вызывая других функций, как это сделано в программе PERSORT. При этом не забывайте, что операция -> имеет больший приоритет, чем опера- ция *, и вам нужно будет написать
if ( (*(pp+j))->getSalary() > (*(pp+k))->getSalary() ) { /* меняем указатели местами */ }
8. Исправьте функцию additem() из программы LINKLIST так, чтобы она до- бавляла новый элемент в конец списка, а не в начало. Это будет означать,
что первый вставленный элемент будет выведен первым и результат рабо- ты программы будет следующим:
25 36 49 64
Для того чтобы добавить элемент, вам необходимо будет пройти по цепи до конца списка, а затем изменить указатель последнего элемента так, что- бы он указывал на новый элемент.
9. Допустим, что нам нужно сохранить 100 целых чисел так, чтобы иметь к ним легкий доступ. Допустим, что при этом у нас есть проблема: память нашего компьютера так фрагментирована, что может хранить массив, наи- большее количество элементов в котором равно десяти (такие проблемы действительно появляются, хотя обычно это происходит с объектами, зани- мающими большое количество памяти). Вы можете решить эту проблему, определив 10 разных массивов по 10 элементов в каждом и массив из 10 указателей на эти массивы. Массивы будут иметь имена а0, a1, а2 и т. д. Адрес каждого массива будет сохранен в массиве указателей типа int*, ко- торый называется ар. Вы сможете получить доступ к отдельному целому используя выражение ap[j] [к], где j является номером элемента массива указателей, а к — номером элемента в массиве, на который этот указатель указывает. Это похоже на двумерный массив, но в действительности яв- ляется группой одномерных массивов.
Заполните группу массивов тестовыми данными (скажем, номерами 0, 10, 20 и т. д.), а затем выведите их, чтобы убедиться, что все работает пра- вильно.
10. Описанный в упражнении 9 подход нерационален, так как каждый из 10 массивов объявляется отдельно, с использованием отдельного имени, и каждый адрес получают отдельно. Вы можете упростить программу, ис- пользуя операцию new, которая позволит вам выделить память для масси- вов в цикле и одновременно связать с ними указатели:
for ( j = 0; j < NUMARRAYS; j++ ) // создаем NUMARRAYS массивов *( ар + j ) = new int [ MAXSIZE ]; //no MAXSIZE целых чисел в каждом
Перепишите программу упражнения 9, используя этот подход. Доступ к отдельному элементу массивов вы сможете получить, используя то же выражение, что и в упражнении 9, или аналогичное выражение с указате- лями: *(*(ap+j)+k).
11. Создайте класс, который позволит вам использовать 10 отдельных масси- вов из упражнения 10 как один одномерный массив, допуская примене- ние операций массива. То есть мы можем получить доступ к элементам массива, записав в функции main() выражение типа a[j], а методы класса могут получить доступ к полям класса, используя двухшаговый подход. Перегрузим операцию [ ] (см. главу 9 «Наследование»), чтобы получить нужный нам результат. Заполним массив данными и выведем их. Хотя
для интерфейса класса использованы операции индексации массива, вам следует использовать указатели внутри методов класса.
12. Указатели сложны, поэтому давайте посмотрим, сможем ли мы сделать работу с ними более понятной (или, возможно, более непонятной), ис- пользуя их симуляцию в классе.
Для разъяснения действия наших доморощенных указателей мы смодели- руем память компьютера с помощью массивов. Так как доступ к массивам всем понятен, то вы сможете увидеть, что реально происходит, когда мы используем для доступа к памяти указатели.
Мы будем использовать один массив типа char для хранения всех типов переменных. Именно так устроена память компьютера: массив байтов (тип char имеет тот же размер), каждый из которых имеет адрес (или, в терми- нах массива, индекс). Однако C++ не позволит нам хранить данные типа float или int в массиве типа char обычным путем (мы можем использовать объединения, но это другая история). Поэтому мы создадим симулятор памяти, используя отдельный массив для каждого типа данных, которые мы хотим сохранить. В этом упражнении мы ограничимся одним типом float, и нам понадобится массив для него. Назовем этот массив fmemory. Однако значения указателей (адреса) тоже хранятся в памяти, и нам пона- добится еще один массив для их хранения. Так как в качестве модели адре- сов мы используем индексы массива, то нам потребуется массив типа int, назовем его pmemory, для хранения этих индексов.
Индекс массива fmemory (назовем его fmem_top) показывает на следу- ющее по очереди доступное место, где можно сохранить значение типа float. У нас есть еще похожий индекс массива pmemory (назовем его pmem_ top). Не волнуйтесь о том, что наша «память» может закончиться. Мы предполагаем, что эти массивы достаточно большие, чтобы хранить все, что мы захотим, и нам не надо заботиться об управлении памятью.
Создадим класс Float, который мы будем использовать для моделирова- ния чисел типа float, которые будет храниться в fmemory вместо настоя- щей памяти. Класс Float содержит поле, значением которого является ин- декс массива fmemory, хранящего значения типа float. Назовем это поле addr. В классе также должны быть два метода. Первый — это конструктор, имеющий один аргумент типа float для инициализации значения. Конст- руктор помещает значение аргумента в элемент массива fmemory, на который указывает указатель fmem_top, а затем записывает значение fmem_top в мас- сив addr. Это похоже на то, как компоновщик и компилятор хранят обыч- ные переменные в памяти. Второй метод является перегружаемой опера- цией &. Он просто возвращает значение указателя (индекса типа int) в addr.
Создадим второй класс ptrFloat. Объект этого класса содержит адрес (ин- декс) в pmemory. Метод класса инициализирует этот «указатель» значени- ем типа int. Второй метод перегружает операцию * (операция разыменова- ния). Его действия более сложны. Он получает адрес из массива pmemory, в котором хранятся адреса. Затем полученный адрес используется как ин-
декс массива fmemory для получения значения типа float, которое распола- галось но нужному нам адресу.
float& ptrFloat::operator* ( ) {
return fmemory [ pmemory [ addr ] ];
}
Таким образом мы моделируем действия операции разыменования (*). Заметим, что вам нужно возвращаться из этой функции по ссылке, чтобы можно было использовать операцию * слева от знака равно. Классы Float и ptrFloat похожи, но класс Float хранит данные типа float в массиве, представляющем собой память, а класс ptrFloat хранит поля типа int (являющиеся у нас указателями, но на самом деле индексами массива) в другом массиве, который тоже представляет собой память. Это типичное использование этих классов в функции main();
Float var1 = 1.234; // определяем и инициализируем
Float var2 = 5.678; // две вещественные переменные
ptrFloat ptr1 = &var1; // определяем и инициализируем
ptrFloat ptr2 = &var2; // два указателя
cout << " *ptr1 = " << *ptr1; // получаем значения переменных cout << " *ptr2 = " << *ptr2; // и выводим на экран
*ptr1 = 7.123; // присваиваем новые значения
*ptr2 = 8.456; // переменным, адресованным через указатели
cout << " *ptr1 = " << *ptr1; // снова получаем значения cout << " *ptr2 = " << *ptr2; // и выводим на экран
Заметим, что за исключением других имен типов переменных, это выгля- дит так же, как действия с настоящими переменными. Далее результат ра- боты программы:
*ptr1 = 1.234 *ptr2 = 5.678
*ptr1 = 7.123 *ptr2 = 8.456
Такой путь реализации указателей может показаться очень сложным, но здесь показана их внутренняя работа и работа операции адреса. Мы рас- смотрели природу указателей в различных ракурсах.
12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
