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

Пример со счетчиком

В качестве примера мы создадим класс, объекты которого могут быть полезны практически для любой программы. Счетчик — это средство, предназначенное для хранения количественной меры какой-либо изменяющейся величины. Счет- чик может хранить число обращений к файлу, число раз, которое пользователь нажал клавишу Enter, или количество клиентов банка. Как правило, при наступ- лении соответствующего события счетчик увеличивается на единицу (инкремен- тируется). Обращение к счетчику происходит, как правило, для того, чтобы уз- нать текущее значение той величины, для измерения которой он предназначен.

Допустим, что счетчик, который мы сейчас создадим, будет важной частью нашей программы, и многие из ее функций будут использовать значение этого счетчика. В процедурных языках, таких, как C, счетчик, скорее всего, был бы представлен в виде глобальной переменной. Но, как мы уже говорили в главе 1, использование глобальных переменных усложняет разработку программы и не- безопасно с точки зрения несанкционированного доступа со стороны функций. Наш следующий пример, COUNTER, использует такой счетчик, значение которого может быть изменено только с помощью его собственных методов.

// counter.cpp

// счетчик в качестве объекта

#include <iostream>

using namespace std;

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

class Counter

{

  private:

    unsigned int count;         // значение счетчика

  public:

    Counter() : count(0)        // конструктор

      { /* пустое тело */ }

    void inc_count()            // инкрементирование счетчика

      { count++; }

    int get_count()             // получение значения счетчика

      { return count; }

};

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

int main()

{

  Counter c1, c2;               // определение с инициализацией

  cout <<"\nc1=" << c1.get_count(); // вывод

  cout <<"\nc2=" << c2.get_count();

  c1.inc_count();               // инкрементирование c1

  c2.inc_count();               // инкрементирование c2

  c2.inc_count();               // инкрементирование c2

  cout <<"\nc1=" << c1.get_count(); // вывод

  cout <<"\nc2=" << c2.get_count();

  cout << endl;

  return 0;

}

Класс Counter имеет единственное поле count типа unsigned int, поскольку значение счетчика не может быть отрицательным, и три метода: конструктор Counter(), который мы рассмотрим чуть позже, inc_count(), инкрементирующий поле count, и get_count(), возвращающий текущее значение счетчика.

Автоматическая инициализация

Когда создается объект типа Counter, нам хотелось бы, чтобы его поле count было инициализировано нулевым значением, поскольку большинство счетчиков на- чинают отсчет именно с нуля. Мы могли бы провести инициализацию с помо- щью вызова функции set_count() с аргументом, равным нулю, или создать специ- альный метод zero_count(), обнуляющий значение функции. Недостаток такого подхода заключается в том, что эти функции необходимо вызывать явно каждый раз при создании объекта типа Counter:

Counter c1;        // при определении объекта

c1.zero_count();             // это необходимое действие

Подобные действия легко могут привести к неправильной работе всей про- граммы, поскольку программисту для этого достаточно забыть проинициализи- ровать хотя бы одну переменную после ее создания. Если в программе создается множество таких переменных, гораздо проще и надежнее было бы инициализи- ровать их автоматически при создании. В нашем примере конструктор Counter() выполняет эти действия. Конструктор вызывается автоматически при создании каждого из объектов. Таким образом, в функции main() оператор

Counter c1, c2;

создает два объекта типа Counter. При создании каждого из них вызывается конст- руктор Counter(), присваивающий полю counter нулевое значение. Таким образом, кроме создания переменных, данный оператор еще присваивает их полям нулевое значение.

Имя конструктора

У конструкторов есть несколько особенностей, отличающих их от других мето- дов класса. Во-первых, имя конструктора в точности совпадает с именем класса (в нашем примере таким именем является Counter). Таким образом, компилятор отличает конструкторы от других методов класса. Во-вторых, у конструкторов не существует возвращаемого значения. Это объясняется тем, что конструктор автоматически вызывается системой, и, следовательно, не существует вызывающей программы или функции, которой конструктор мог бы возвратить значение. Следо- вательно, указание возвращаемого значения для конструктора не имеет смысла. Отсутствие типа возвращаемого значения у конструкторов является вторым при- знаком, по которому компилятор может отличить их от других методов класса.

Список инициализации

Одной из наиболее часто возлагаемых на конструктор задач является инициа- лизация полей объекта класса. Для каждого объекта класса Counter конструктор выполняет инициализацию поля count нулем. Вы, вероятно, ожидали, что это

действие будет произведено в теле конструктора приблизительно следующим образом:

Counter()

                { count = 0; }

Такая форма записи не рекомендуется, несмотря на то, что она не содержит ошибок. Инициализация в нашем примере происходит следующим образом:

Counter() : count(0) {  }

Инициализация расположена между прототипом метода и телом функции и предварена двоеточием. Инициализирующее значение помещено в скобках пос- ле имени поля.

Если необходимо инициализировать сразу несколько полей класса, то значе- ния разделяются запятыми, и в результате образуется список инициализации:

SomeClass() : m1(7), m2(33), m3(4) {  }

Причины, по которым инициализация не проводится в теле конструктора, достаточно сложны. Инициализация полей с помощью списка инициализа- ции происходит до начала исполнения тела конструктора, что в некоторых ситуациях бывает важно. Так, например, список инициализации — это единст- венный способ задать начальные значения констант и ссылок. В теле конструк- тора, как правило, производятся более сложные действия, чем обычная инициа- лизация.

Результаты работы программы со счетчиком

В функции main() рассматриваемой нами программы создаются два объекта класса Counter с именами c1 и c2. Затем на экран выводятся значения полей каж- дого из объектов, которые, согласно нашей задумке, должны быть инициализи- рованы нулевыми значениями. Далее значение счетчика c1 инкрементируется один раз, а значение счетчика c2 — два раза, и программа вновь заставляет объек- ты вывести значения своих полей на экран (что является в данном случае вполне корректным). Результат работы программы выглядит следующим образом:

c1=0 c2=0 c1=1 c2=2

Для того чтобы убедиться в том, что конструктор функционирует именно так, как мы описали выше, заставим его печатать сообщение во время выполнения:

Counter() : count(O)

{ cout << "Конструктор\n"; }

Теперь результат работы программы будет выглядеть следующим образом:

Конструктор Конструктор

c1=0

c2=0 c1=1 c2=2

Как можно видеть, конструктор исполняется дважды: первый раз — для пе- ременной c1, второй раз — для переменной c2, во время выполнения оператора

Counter c1, c2; в функции main().

Конструкторы и собственные типы данных

Разработчики компиляторов для языков C, VB или C++ должны позаботиться о том, чтобы для любой переменной стандартного типа, которую программист оп- ределяет в своей программе, вызывался необходимый конструктор. Например, если в программе встречается определение переменной типа int, где-то должен существовать конструктор, который выделит для этой переменной четыре байта памяти. Таким образом, научившись создавать свои собственные конструкторы, мы можем выполнять задачи, с которыми сталкиваются разработчики компиля- торов. Мы сделали еще один шаг на пути к созданию собственных типов данных, в чем мы скоро убедимся.

 

12