ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В C++ (4-Е ИЗДАНИЕ) (часть 11) онлайн

Упражнения

Решения к упражнениям, помеченным знаком *, можно найти в приложении Ж.

*1. Пусть имеется та же издательская компания, которая описана в упражне-

                нии 1 главы 9, которая продает и книги, и аудио версии печатной продук-

                ции. Как и в том упражнении, создайте класс publication, хранящий название

                (фактически, строку) и цену (типа float) публикации. Создайте два порож-

                денных класса: book, в котором происходит изменение счетчика страниц (ти-

                па int), и tape, в котором происходит изменение счетчика записанных на

                кассету минут. Каждый из классов должен иметь метод getdata(), запраши-

                вающий информацию у пользователя, и putdata() для вывода данных на экран.

 2. Напишите main(), где создавался бы массив указателей на класс publication.

                Это очень похоже на то, что мы делали в текущей главе на примере

                VIRTPERS. В цикле запрашивайте у пользователя данные о конкретной кни-

                ге или кассете, используйте new для создания нового объекта book или

                tape. Сопоставляйте указатель в массиве с объектом. Когда пользователь

                закончит ввод исходных данных, выведите результат для всех введенных

                книг и кассет, используя цикл for и единственное выражение

                pubarr[j]->putdata(); для вывода данных о каждом объекте из массива

*3. В классе Distance, как показано в примерах FRENGL и FRISQ из этой главы,

создайте перегружаемую операцию умножения *, чтобы можно было умно-

жать два расстояния. Сделайте эту функцию дружественной, тогда мож-

но будет использовать выражение типа Wdist=7.5*dist2. Вам понадобится

конструктор с одним аргументом для перевода величин из формата чисел

с плавающей запятой в формат Distance. Напишите какой-либо main() на

свое усмотрение для того, чтобы несколькими способами проверить рабо-

ту этой перегружаемой операции.

 

*4.         Как уже говорилось, классы можно заставлять вести себя как массивы.

                Пример CLARRAY показывает один из способов создания такого класса.

 

Листинг 11.26. Программа CLARRAY

// clarray.cpp

// создает класс-массив

#include <iostream>

using namespace std;

///////////////////////////////////////////////////////////

class Array                  //моделирует обычный массив C++

  {

 

 

Листинг 11.26   (продолжение)

  private:

    int* ptr;                //указатель на содержимое Array

    int size;                //размер Array

  public:

    Array(int s)             //конструктор с одним аргументом

      {

      size = s;              //аргумент – размер Array

      ptr = new int[s];      //выделить память под Array

      }

    ~Array()                 //деструктор

      { delete[] ptr; }

    int& operator [] (int j) //перегружаемая операция

                             //списка индексов

      { return *(ptr+j); }

  };

///////////////////////////////////////////////////////////

int main()

{

  const int ASIZE = 10;      //размер массива

  Array arr(ASIZE);          //создать массив

 

  for(int j=0; j<ASIZE; j++) //заполнить его j^2

    arr[j] = j*j;

 

  for(j=0; j<ASIZE; j++)     //вывести его содержимое

    cout << arr[j] << ' ';

  cout << endl;

  return 0;

}

 

Результат работы программы:

                0 1 4 9 16 25 36 49 64 81

Взяв за основу приведенную программу, добавьте перегружаемое присваи-

вание и перегружаемый конструктор копирования к классу Array. Затем

добавьте к main() выражение Array arr2(arr1); и arr3=arr1; для проверки того,

что перегружаемые операции работают. Конструктор копирования должен

создать новый объект Array со своим собственным местом в памяти, вы-

деленным для хранения элементов массива. И конструктор копирования,

и оператор присваивания должны копировать содержимое старого объек-

та класса Array в новый. Что будет, если вы присвоите объект Array одного

размера объекту Array другого размера?

5. Взяв за основу программу из упражнения 1 этой главы, добавьте метод

типа bool, называющийся isOveersize(), к классам book и tape. Допустим,

книга, в которой больше 800 страниц, или кассета со временем проигры-

вания более 90 минут, будут считаться объектами с превышением разме-

ра. К этой функции можно обращаться из main(), а результат ее работы

выводить в виде строки «Превышение размера!» для соответствующих

книг и кассет. Допустим, объекты классов book и tape должны быть до-

ступны через указатели на них, хранящиеся в массиве типа publication.

 

Что в этом случае вам нужно добавить в базовый класс publication? Вы

можете привести примеры компонентов этого базового класса?

6.            Возьмите за основу программу из упражнения 8 главы 8, где было пере-

гружено пять арифметических операций для работы с денежным форма-

том. Добавьте два оператора, которые не были перегружены в том упраж-

нении:

long double * bMoney //умножать число на деньги

long double / bMoney //делить число на деньги

Эти операции требуют наличия дружественных функций, так как справа

от оператора находится объект, а слева — обычное число. Убедитесь, что

main() позволяет пользователю ввести две денежные строки и число с пла-

вающей запятой, а затем корректно выполняет все семь арифметических

действий с соответствующими нарами значений.

7.            Как и в предыдущем упражнении, возьмите за основу программу из упраж-

нения 8 главы 8. На этот раз от вас требуется добавить функцию, округ-

ляющую значение bMoney до ближайшего доллара:

mo2 = round(mo1);

Как известно, значения, не превышающие $0.49, округляются вниз, а чис-

ла от $0.50 и более округляются вверх. Можно использовать библиотеч-

ную функцию modfl(). Она разбивает переменную типа long double на це-

лую и дробную части. Если дробная часть меньше 0.50, функция просто

возвращает целую часть числа. В противном случае возвращается увели-

ченная на 1 целая часть. В main() проверьте работоспособность функции

путем передачи в нее последовательно значений, одни из которых меньше

$0.49, другие - больше $0.50.

8.            Помните программу PARSE из главы 10? Попробуйте доработать ее, чтобы

она могла вычислять значения математических выражений с рациональны-

ми числами, например типа float, а не только с одноразрядными числами:

3.14159 / 2.0 + 75.25 * 3.333 + 6.02

Во-первых, нужно развить стек до такой степени, чтобы он мог хранить и

операторы (типа char), и числа (типа float). Но как, спрашивается, можно

хранить в стеке значения двух разных типов? Ведь стек — это, по сути де-

ла, массив. Надо еще учесть, что типы char и float даже не совпадают по

размеру! Даже указатели на разные типы данных (char* и float*) компиля-

тор не позволит хранить в одном массиве, несмотря на то, что они одина-

кового размера. Единственный способ хранить в массиве два разных типа

указателей — сделать эти типы наследниками одного и того же базового

класса. При этом базовому классу даже нет нужды иметь какие-то собст-

венные данные, это может быть абстрактный класс, из которого никакие

объекты создаваться не будут.

Конструкторы могут хранить значения в порожденных классах обычным

способом, но должна иметься специальная чистая виртуальная функция

 

для того, чтобы извлечь эти значения. Представляем возможный сцена-

рий работы над этим вопросом:

  class Token                      // Абстрактный базовый класс

    {

    public:

      virtual float getNumber()=0; // чистая виртуальная

                                   // функция

      virtual char getOperator()=0;

    };

  class Operator : public Token

    {

    private:

      char oper;             // Операторы +, –, *, /

    public:

      Operator(char);        // конструктор устанавливает значение

      char getOperator();    // получить значение

      float getNumber();     // просто некая функция

    };

  class Number : public Token

    {

    private:

      float fnum;            // число

    public:

      Number(float);         // конструктор устанавливает значение

      float getNumber();     // получить значение

      char getOperator();    // просто некая функция

    };

Token* atoken[100];    //содержит типы Operator* и Number*

 

Виртуальные функции базового класса должны быть реализованы во всех

порожденных классах, в противном случае классы становятся абстракт-

ными. Таким образом, классу Operand нужна функция getNumber(), несмот-

ря на то, что она фиктивная. Классу Number нужна функция getOperand(),

несмотря на то, что она тоже фиктивная.

Поработайте над этим каркасом, сделайте его реально работающей про-

граммой, добавив класс Stack, содержащий объекты класса Token, и функ-

цию main(), в которой бы заносились в стек и извлекались из него разные

арифметические операторы и числа в формате с плавающей запятой.

9. Внесем некоторое разнообразие в пример HORSE из главы 10, создав класс

для внесения в него лошадей экстракласса. Предположим, что любая ло-

шадь, которая на скачках к середине дистанции находится впереди всех,

становится практически непобедимой. Относительно класса лошадей соз-

дадим порожденный класс comhorse (для конкурентоспособной лошади).

Перегрузим функцию horse_tick() в этом классе таким образом, чтобы каж-

дая лошадь могла проверять, является ли она ведущей и нет ли поблизо-

сти соперников (скажем, ближе, чем на 0,1 форлонг (1/80 часть мили или

20,1 м.)). Если есть, то ей следует немного ускориться. Может быть, не

настолько, чтобы побеждать на всех скачках, но в достаточной мере для

того, чтобы оставаться конкурентоспособной.

Как каждая лошадь узнает, где находятся остальные? Моделирующий ее

объект должен иметь доступ к области памяти, в которой хранятся дан-

 

                ные о соперниках. В программе HORSE это hArray. Будьте внимательны: вы

                создаете класс для передовой лошади, он должен быть наследником клас-

                са всех лошадей. Поэтому классу comhorse потребуется перегрузить hArray.

                Вам может потребоваться создать еще один производный класс, comtrack,

                для отслеживания позиции лошади.

                Можно непрерывно проверять, лидирует ли ваша лошадь, и если она впе-

                реди всех, но лишь ненамного, следует ее немного ускорить.

10.                         Упражнение 4 в главе 10 включало в себя добавление к классу linklist пе-

регружаемого деструктора. Допустим, мы заполняем объект этого класса

данными, а затем присваиваем один класс целиком другому, используя

стандартный оператор присваивания:

                list2 = list1;

Допустим, что впоследствии мы удалим объект класса list1. Можем ли мы

все еще использовать list2 для доступа к введенным данным? Увы, нет,

так как при удалении list1 все его ссылки были удалены. Единственное,

что было известно объекту linklist про удаленный объект, это указатель на

него. Но его удалили, указатель в list2 стал недееспособным, и все попыт-

ки получить доступ к данным приведут к получению мусора вместо дан-

ных, а в худшем случае — к зависанию программы.

Один из способов избежать этих проблем — перегрузить оператор при-

сваивания, чтобы он вместе с объектом копировал бы все его ссылки. Но

тогда придется пройти по всей цепочке, поочередно копируя все ссылки.

Как отмечалось ранее, следует также перегружать конструктор копирова-

ния. Чтобы была возможность удалять объекты linklist в main(), можно

создавать их с помощью указателя и new. В таком случае проще будет

проверять работу новых операций. Не переживайте, если обнаружите, что

в процессе копирования порядок следования данных изменился.

Понятно, что копирование всех данных не является самым эффективным

решением проблемы с точки зрения экономии памяти. Сравните этот под-

ход с представленным в примере STRIMEM (глава 10), где использовался

только один набор данных для всех объектов, и хранилась информация о

том, сколько объектов указывали на эти данные.

11.          Выполните изменения в соответствии с упражнением 7, применив их к

программе PARSE главы 10. То есть заставьте программу анализировать

выражения, содержащие числа в формате с плавающей запятой. Совмес-

тите классы, предложенные в упражнении 7, с алгоритмами из PARSE. Вам

придется работать с указателями на символы вместо работы с самими

символами. Это потребует выражений, подобных следующим:

Number* ptrN = new Number(ans);

s.push(ptrN);

and

Operator* ptr0 = new Operator(ch);

s.push(ptr0);

 

35