Лекции.ИНФО


Глава 12: О классах подробнее



В этой главе мы продолжим рассмотрение классов, начатое в главе 11. Здесь вы познакомитесь с "дружественными" функциями, перегрузкой конструкторов, а также с возможностью передачи и возвращения объектов функциями. Кроме того, вы узнаете о существовании специального типа конструктора, именуемого конструктором копии, который используется в случае, когда возникает необходимость в создании копии объекта. Завершает главу описание ключевого слова this.

Функции-"друзья"

В C++ существует возможность разрешить доступ к закрытым членам класса функциям, которые не являются членами этого класса. Для этого достаточно объявить эти функции "дружественными" (или "друзьями") по отношению к рассматриваемому классу. Чтобы сделать функцию "другом" класса, включите ее прототип в public-раздел объявления класса и предварите его ключевым словом friend. Например, в этом фрагменте кода функция frnd() объявляется "другом" класса cl.

class cl {

  // . . .

 public:

  friend void frnd(cl ob);

  // . . .

};

Ключевое слово friend предоставляет функции, которая не является членом класса, доступ к его закрытым членам.

Как видите, ключевое слово friend предваряет остальную часть прототипа функции. Функция может быть "другом" нескольких классов.

Рассмотрим короткий пример, в котором функция-"друг" используется для доступа к закрытым членам класса myclass.

// Демонстрация использования функции-"друга".

#include <iostream>

using namespace std;

class myclass {

  int a, b;

 public:

  myclass(int i, int j) { a=i; b=j; }

  friend int sum(myclass x); // Функция sum() - "друг" класса myclass.

};

// Обратите внимание на то, что функция sum() не является членом ни одного класса

Int sum(myclass х)

{

 /* Поскольку функция sum() — "друг" класса myclass, она имеет право на прямой доступ к его членам данных а и b. */

 return x.a + x.b;

}

Int main ()

{

 myclass n (3, 4);

 cout << sum(n);

 return 0;

}

В этом примере функция sum() не является членом класса myclass. Тем не менее она имеет полный доступ к private-членам класса myclass. В частности, она может непосредственно использовать значения х.а и х.b. Обратите также внимание на то, что функция sum() вызывается обычным образом, т.е. без привязки к объекту (и без использования оператора "точка"). Поскольку она не функция-член, то при вызове ее не нужно квалифицировать с указанием имени объекта. (Точнее, при ее вызове нельзя задавать имя объекта.) Обычно функции-"другу" в качестве параметра передается один или несколько объектов класса, для которого она является "другом", как в случае функции sum().

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

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

// Использование функции-"друга".

#include <iostream>

using namespace std;

const int IDLE=0;

const int INUSE=1;

Class С2; // опережающее объявление

class C1 {

  int status; // IDLE если сообщение неактивно, INUSE если сообщение выведено на экран.

  // ...

 public:

  void set_status(int state);

  friend int idle(C1 a, C2 b);

};

class C2 {

  int status; // IDLE если сообщение неактивно, INUSE если сообщение выведено на экран.

  // ...

 public:

  void set_status(int state);

  friend int idle(C1 a, C2 b);

};

void C1::set_status(int state)

{

 status = state;

}

void C2::set_status(int state)

{

 status = state;

}

// Функция idle() - "друг" для классов C1 и C2.

int idle(C1 a, C2 b)

{

 if(a.status || b.status) return 0;

 else return 1;

}

Int main()

{

 C1 x;

 C2 y;

 x.set_status(IDLE);

 у.set_status(IDLE);

 if(idle(x, y)) cout << "Экран свободен.";

 else cout << "Отображается сообщение.";

 x.set_status(INUSE);

 if(idle(x, y)) cout << "Экран свободен.";

 else cout << "Отображается сообщение.";

 return 0;

}

При выполнении программа генерирует такие результаты.

Экран свободен.

Отображается сообщение.

Поскольку функция idle() является "другом" как для класса С1, так и для класса С2, она имеет доступ к закрытому члену status, определенному в обоих классах. Таким образом, состояние объекта каждого класса одновременно можно проверить всего одним обращением к функции idle().

Опережающее объявление предназначено для объявления имени классового типа до определения самого класса.

Обратите внимание на то, что в этой программе используется опережающее объявление (также именуемое опережающей ссылкой) для класса С2. Его необходимость обусловлена тем, что объявление функции idle() в классе С1 использует ссылку на класс С2 до его объявления. Чтобы создать опережающее объявление для класса, достаточно использовать формат, представленный в этой программе.

"Друг" одного класса может быть членом другого класса. Перепишем предыдущую программу так, чтобы функция idle() стала членом класса С1. Обратите внимание на использование оператора разрешения области видимости (или оператора разрешения контекста) при объявлении функции idle() в качестве "друга" класса С2.

/* Функция может быть членом одного класса и одновременно "другом" другого.

*/

#include <iostream>

using namespace std;

const int IDLE=0;

const int INUSE=1;

Class C2; // опережающее объявление

class C1 {

  int status; // IDLE, если сообщение неактивно, INUSE, если сообщение выведено на экран.

  // ...

 public:

  void set_status(int state);

  int idle(C2 b); // теперь это член класса C1

};

class C2 {

  int status; // IDLE, если сообщение неактивно, INUSE, если сообщение выведено на экран.

  // . . .

 public:

  void set_status(int state);

  friend int C1::idle(C2 b); // функция-"друг"

};

void C1::set_status(int state)

{

 status = state;

}

void C2::set_status(int state)

{

 status = state;

}

// Функция idle() -- член класса С1 и "друг" класса С2.

int C1::idle(С2 b)

{

 if(status || b.status) return 0;

 else return 1;

}

Int main()

{

 C1 x;

 C2 y;

 x.set_status(IDLE);

 y.set_status(IDLE);

 if(x.idle(y)) cout << "Экран свободен.";

 else cout << "Отображается сообщение.";

 x.set_status(INUSE);

 if(x.idle(y)) cout << "Экран свободен.";

 else cout << "Отображается сообщение.";

 return 0;

}

Поскольку функция idle() является членом класса C1, она имеет прямой доступ к переменной status объектов типа С1. Следовательно, в качестве параметра необходимо передавать функции idle() только объекты типа С2.

Перегрузка конструкторов

Несмотря на выполнение конструкторами уникальных действий, они не сильно отличаются от функций других типов и также могут подвергаться перегрузке. Чтобы перегрузить конструктор класса, достаточно объявить его во всех нужных форматах и определить каждое действие, связанное с соответствующим форматом. Например, в следующей программе объявляется класс timer, который действует как вычитающий таймер. При создании объекта типа timer таймеру присваивается некоторое начальное значение времени. При вызове функции run() таймер выполняет счет в обратном порядке до нуля, а затем подает звуковой сигнал. В этом примере конструктор перегружается трижды, предоставляя тем самым возможность задавать время как в секундах (причем либо числом, либо строкой), так и в минутах и секундах (с помощью двух целочисленных значений). В этой программе используется стандартная библиотечная функция clock(), которая возвращает количество сигналов, принятых от системных часов с момента начала выполнения программы. Вот как выглядит прототип этой функции:

clock_t clock();

Тип clock_t представляет собой разновидность длинного целочисленного типа. Операция деления значения, возвращаемого функцией clock(), на значение CLOCKS_PER_SEC позволяет преобразовать результат в секунды. Как прототип для функции clock(), так и определение константы CLOCKS_PER_SEC принадлежат заголовку <ctime>.

// Использование перегруженных конструкторов.

#include <iostream>

#include <cstdlib>

#include <ctime>

using namespace std;

class timer{

  int seconds;

 public:

  // секунды, задаваемые в виде строки

  timer(char *t) { seconds = atoi (t); }

  // секунды, задаваемые в виде целого числа

  timer(int t) { seconds = t; }

  // время, задаваемое в минутах и секундах

  timer(int min, int sec) { seconds = min*60 + sec; }

  void run();

};

Void timer::run()

{

 clock_t t1;

 t1 = clock();

 while( (clock()/CLOCKS_PER_SEC - t1/CLOCKS_PER_SEC)<seconds);

 cout << ""; // звуковой сигнал

}

Int main()

{

 timer a (10), b("20"), c(1, 10);

 a.run(); // отсчет 10 секунд

 b.run(); // отсчет 20 секунд

 c.run(); // отсчет 1 минуты и 10 секунд

 return 0;

}

При создании в функции main() объектов а, b и с класса timer им присваиваются начальные значения тремя различными способами, поддерживаемыми перегруженными функциями конструкторов. В каждом случае вызывается конструктор, который соответствует заданному списку параметров и потому надлежащим образом инициализирует "свой" объект.

На примере предыдущей программы вы, возможно, не оценили значимость перегрузки функций конструктора, поскольку здесь можно было обойтись единым способом задания временного интервала. Но если бы вы создавали библиотеку классов на заказ, то вам стоило бы предусмотреть набор конструкторов, охватывающий самый широкий спектр различных форматов инициализации, тем самым обеспечив других программистов наиболее подходящими для их программ форматами. Кроме того, как будет показано ниже, в C++ существует атрибут, который делает перегруженные конструкторы особенно ценным средством инициализации объектов.

Динамическая инициализация

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

int n = strlen(str);

double arc = sin(theta);

float d = 1.02 * count / deltax;

Применение динамической инициализации к конструкторам

Подобно простым переменным, объекты можно инициализировать динамически при их создании. Это средство позволяет создавать объект нужного типа с использованием информации, которая становится известной только во время выполнения программы. Чтобы показать, как работает механизм динамической инициализации, модифицируем программу реализации таймера, приведенную в предыдущем разделе.

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

Например, в следующей версии программы таймера для создания двух объектов b и с используется динамическая инициализация.

// Демонстрация динамической инициализации.

#include <iostream>

#include <cstdlib>

#include <ctime>

using namespace std;

class timer{

  int seconds;

 public:

  // секунды, задаваемые в виде строки

  timer(char *t) { seconds = atoi(t); }

  // секунды, задаваемые в виде целого числа

  timer(int t) { seconds = t; }

  // время, задаваемое в минутах и секундах

  timer(int min, int sec) { seconds = min*60 + sec; }

  void run();

};

Void timer::run()

{

 clock_t t1;

 t1 = clock();

 while((clock()/CLOCKS_PER_SEC - t1/CLOCKS_PER_SEC)<seconds);

 cout << ""; // звуковой сигнал

}

Int main()

{

 timer a(10);

 a.run();

 cout << "Введите количество секунд: ";

 char str[80];

 cin >> str;

 timer b(str); // инициализация в динамике

 b.run();

 cout << "Введите минуты и секунды: ";

 int min, sec;

 cin >> min >> sec;

 timer с(min, sec); // инициализация в динамике

 c.run();

 return 0;

}

Как видите, объект a создается с использованием целочисленной константы. Однако основой для создания объектов b и c служит информация, вводимая пользователем. Поскольку для объекта b пользователь вводит строку, имеет смысл перегрузить конструктор timer() для приема строк. Объект c также создается во время выполнения программы с использованием данных, вводимых пользователем. Поскольку в этом случае время вводится в виде минут и секунд, для построения объекта c логично использовать формат конструктора, принимающего два аргумента. Трудно не согласиться с тем, что наличие множества форматов инициализации избавляет программиста от выполнения дополнительных преобразований при инициализации объектов.

Механизм перегрузки конструкторов способствует понижению уровня сложности программ, позволяя создавать объекты наиболее естественным для их применения образом. Поскольку существует три наиболее распространенных способа передачи объекту значений временных интервалов, имеет смысл позаботиться о том, чтобы конструктор timer() был перегружен для реализации каждого из этих способов. При этом перегрузка конструктора timer() для приема значения, выраженного в днях или наносекундах, вряд ли себя оправдает. Загромождение кода конструкторами для обработки редко возникающих ситуаций оказывает, как правило, дестабилизирующее влияние на программу.

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

Присваивание объектов

Если два объекта имеют одинаковый тип (т.е. оба они — объекты одного класса), то один объект можно присвоить другому. Для присваивания недостаточно, чтобы два класса были физически подобны; имена классов, объекты которых участвуют в операции присваивания, должны совпадать. Если один объект присваивается другому, то по умолчанию данные первого объекта поразрядно копируются во второй. Присваивание объектов демонстрируется в следующей программе.

// Демонстрация присваивания объектов.

#include <iostream>

using namespace std;

class myclass {

  int a, b;

 public:

  void setab(int i, int j) { a = i, b = j; }

  void showab();

};

Void myclass::showab()

{

 cout << "а равно " << a << '';

 cout << "b равно " << b << '';

}

Int main()

{

 myclass ob1, ob2;

 ob1.setab(10, 20);

 ob2.setab(0, 0);

 cout << "Объект ob1 до присваивания: ";

 ob1.showab();

 cout << "Объект ob2 до присваивания: ";

 ob2.showab();

 cout << ' ';

 ob2 = ob1; // Присваиваем объект ob1 объекту ob2.

 cout << "Объект ob1 после присваивания: ";

 ob1.showab();

 cout << "Объект ob2 после присваивания: ";

 ob2.showab();

 return 0;

}









Читайте также:

Последнее изменение этой страницы: 2016-03-17; Просмотров: 98;


lektsia.info 2017 год. Все права принадлежат их авторам! Главная