Функции друзья и перегрузка оператора

В листинге 15.1 представлен класс String, в котором перегружается operator+. В нем также объявляется конструктор, принимающий указатель на константную строку, поэтому объект класса String можно создавать из строки с концевым нулевым символом.

Примечание: Строки в С и C++ представляют собой массивы символов, заканчивающиеся концевым нулевым символом. Такая строка получается, например,в следующем выражении присвоения: myString[] = "Hello World".

Но чего невозможно сделать в классе String, так это получить новую строку в результате сложения объекта этого класса с массивом символов:

char cString[] = { "Hello"} ; String sString(" Worid");

String sStringTwo = cString + sString; //ошибка!

Строки нельзя использовать с перегруженной функции operator+. Как объяснялось на занятии 10, выражение cString + sString на самом деле вызывает функцию cString.operator+(sString). Поскольку функция operator+() не может вызываться для символьной строки, данная попытка приведет к ошибке компиляции.

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

Листинг 15.8. Функция-друг operator+

1: // Листинг 15.8. Операторы друзья

2:

3: #include <iostream.h>

4: #include <string.h>

5:

6: // Рудиментарный класс string

7: class String

8: {

9: public:

10: // constructors

11: String();

12: String(const char *const);

13: String(const String &);

14: ~String();

15:

16: // перегруженные операторы

17: char & operator[](int offset);

18: char operator[](int offset) const;

19: String operator+(const String&);

20: friend String operator+(const String&, const String&);

21: void operator+=(const String&);

22: String & operator= (const String &);

23:

24: // методы общего доступа

25: int GetLen()const { return itsLen; }

26: const char * GetString() const { return itsString; }

27:

28: private:

29: String (int); // закрытый конструктор

30: char * itsString;

31: unsigned short itsLen;

32: };

33:

34: // конструктор, заданный по умолчанию, создает строку длиной 0 байт

35: String::String()

36: {

37: itsString = new char[1];

38: itsString[0] = '\0';

39: itsLen=0;

40: // cout << "\tDefault string constructor\n";

41: // ConstructorCount++:

42: }

43:

44: // закрытый конструктор, используемый только

45: // методами класса для создания новой строки

46: // указанного размера, заполненной нулями.

47: String::String(int len)

48: {

49: itsString = new char[len+1];

50: for (int i = 0; i<=len; i++)

51: itsString[i] = '\0';

52: itsLen=len;

53: // cout << "\tString(int) constructor\n";

54: // ConstructorCount++;

55: }

56:

57: // Преобразует массив символов в строку

58: String::String(const char * const cString)

59: {

60: itsLen = strlen(cString);

61: itsString = new char[itsLen+1];

62: for (int i = 0; i<itsLen; i++)

63: itsString[i] = cString[i];

64: itsString[itsLen]='\0';

65: // cout << "\tString(char*) constructor\n";

66: // ConstructorCount++;

67: }

68:

69: // конструктор-копировщик

70: String::String (const String & rhs)

71: {

72: itsLen=rhs.GetLen();

73: itsString = new char[itsLen+1];

74: for (int i = 0; i<itsLen;i++)

75: itsString[i] = rhs[i];

76: itsString[itsLen] = '\0';

77: // cout << "\tString(String&) constructor\n";

78: // ConstructorCount++;

79: }

80:

81: // деструктор, освобождает занятую память

82: String::~String ()

83: {

84: delete [] itsString;

85: itsLen = 0;

86: // cout << "\tString destructor\n";

87: }

88:

89: // этот оператор освобождает память, а затем

90: // копирует строку и размер

91: String& String::operator=(const String & rhs)

92: {

93: if (this == &rhs)

94: return <<this;

95: delete [] itsString;

96: itsLen=rhs.GetLen();

97: itsString = new char[itsLen+1];

98: for (int i = 0; i<itsLen;i++)

99: itsString[i] = rhs[i];

100: itsString[itsLen] = 1\0';

101: return *this;

102: // cout << "\tString operator=\n";

103: }

104:

105: // неконстантный оператор индексирования,

106: // возвращает ссылку на символ, который можно

107: // изменить!

108: char & String::operator[](int offset)

109: {

110: if (offset > itsLen)

111: return itsString[itsLen-1];

112: else

113: return itsString[offset];

114: }

115:

116: // константный оператор индексирования,

117: // используется для константных объектов (см. конструктор-копировщик!)

118: char String::operator[](int offset) const

119: {

120: if (offset > itsLen)

121: return itsString[itsLen-1];

122: else

123: return itsString[offset];

124: }

125: // создает новый объект String, добавляя

126: // текущий обьект к rhs

127: String String::operator+(const String& rhs)

128: {

129: int totalLen = itsLen + rhs.GetLen();

130: String temp(totalLen);

131: int i, j;

132: for (i = 0; i<itsLen; i++)

133: temp[i] = itsString[i];

134: for (j = 0, i = itsLen; j<rhs.GetLen(); j++, i++)

135: temp[i] = rhs[j];

136: temp[totalLen]='\0';

137: return temp;

138: }

139:

140: // создает новый объект String

141: // из двух объектов класса String

142: String operator+(const String& lhs, const String& rhs)

143: {

144: int totalLen = lhs.GetLen() + rhs.GetLen();

145: String temp(totalLen);

146: int i, j;

147: for (i = 0; i<lhs.GetLen(); i++)

148: temp[i] = lhs[i];

149: for (j = 0, i = lhs.GetLen();; j<rhs.GetLen(); j++, i++)

150: temp[i] = rhs[j];

151: temp[totalLen]='\0';

152: return temp;

153: }

154:

155: int main()

156: {

157: String s1("String 0ne ");

158: String s2("String Two ");

159: char *c1 = { "C-String 0ne " } ;

160: String s3;

161: Stnng s4;

162: String s5;

163:

164: cout << "s1: " << s1.GetString() << endl;

165: cout << "s2: " << s2.GetString() << endl;

166: cout << "c1: " << c1 << endl;

167: s3 = s1 + s2;

168: cout << "s3: " << s3.GetString() << endl;

169: s4 = s1 + cl;

170: cout << "s4: " << s4.GetStnng() << endl;

171: s5 = c1 + s2;

172: cout << "s5: " << s5.GetString() << endl;

173: return 0;

174: }

Результат:

s1: String 0ne

s2: String Two

c1: C-String One

s3: String One String Two

s4: String One C-String One

s5: C-String One String Two

Анализ: Объявления всех методов класса String, за исключением operator+, остались такими же, как в листинге 15.1. В строке 20 листинга 15.8 перегружается новый operator+, который принимает две ссылки на константные строки и возвращает строку, полученную в результате конкатенации исходных строк. Эта функция объявлена как друг класса String.

Обратите внимание, что функция operator+ не является функцией-членом этого или любого другого класса. Она объявляется среди функций-членов класса string как друг, но не как член класса. Тем не менее это все же полноценное объявление функции, и нет необходимости еще раз объявлять в программе прототип этой функции.

Выполнение функции operator+ определяется в строках 142—153. Определение выполнения функции аналогично приведенному в версии программы, представленной в листинге 15.1, за тем исключением что функция принимает в качестве аргументов две строки, обращаясь к ними с помощью открытых методов доступа класса.

Перегруженный оператор применяется в строке 171, где выполняется конкатенация двух строк.

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

Для объявления функции как друга класса используется ключевое слово friend, за которым следует объявление функции Это не предоставляет функции доступ к указателю this, но обеспечивает доступ ко всем закрытым и защищенным данным и функциям-членам.

Пример:

class PartNode

{ // ...

// сделаем функцию-член другого класса другом этого класса

friend void PartsList::Insert(Part*)

// сделаем другом глобальную функцию

friend int SomeFunction();

// ...

};

Перегрузка оператора вывода

Настало время снабдить наш класс String возможностью использовать объект cout для вывода своих данных так же, как при выводе данных базовых типов. До сих пор для вывода значения переменной-члена приходилось использовать следующее выражение:

cout << theString.GetString();

Неплохо было бы иметь возможность написать просто

cout << theString;

Для этого необходимо перегрузить функцию operator<<(). Более подробно использование потоков iostreams для вывода данных обсуждается на занятии 16. А в листинге 15.9 объявляется перегрузка функции operator<< как друга.

Листинг 15.8. Перегрузка operator<<()

1: #include <iostream.h>

2: #include <string.h>

3:

4: class String

5: {

6: public:

7: // конструкторы

8: String();

9: String(const char *const);

10: String(const String &);

11: ~String();

12:

13: // перегруженные операторы

14: char & operator[](int offset);

15: char operator[](int offset) const;

16: String operator+(const String&);

17: void operator+=(const String&);

18: String & operator= (const String &);

19: friend ostream& operator<<

20: (ostream& theStream,String& theString);

21: // Общие методы доступа

22: int GetLen()const { return itsLen; }

23: const char * GetString() const { return itsString; }

24:

25: private:

26: String (int); // закрытый конструктор

27: char * itsString;

28: unsigned short itsLen;

29: };

30:

31:

32: // конструктор, заданный no умолчанию, создает строку длиной 0 байт

33: String::String()

34: {

35: itsString = new char[1];

36: itsString[0] = '\0' ;

37: itsLen=0;

38: // cout << "\tDefault string constructor\n";

39: // ConstructorCount++;

40: }

41:

42: // закрытый конструктор, используемый только

43: // методами класса для создания новой строки

44: // указанного размера, заполненной значениями NULL.

45: String::String(int len)

46: {

47: itsString = new char[k:.H];

48: for (int i = 0; i<=len; i++)

49: itsString[i] = '\0';

50: itsLen=len;

51: // cout << "\tString(int) constructor\n";

52: // ConstructorCount++;

53: }

54:

55: // Преобразует массив символов в строку

56: String::String(const char * const cString)

57: {

58: itsLen = strlen(cString);

59: itsString = new char[itsLen+1];

60: for (int i = 0; i<itsLen; i++)

61: itsString[i] = cString[i];

62: itsString[itsLen]='\0';

63: // cout << "\tString(char*) constructor\n";

64: // ConstructorCount++;

65: }

66:

67: // конструктор-копировщик

68: String::String (const String & rhs)

69: {

70: itsLen=rhs.GetLen();

71: itsString = new char[itsLen+1];

72: for (int i = 0; i<itsLen;i++)

73: itsString[i] = rhs[i];

74: itsString[itsLen] = '\0';

75: // cout << "\tString(String&) constructor\n";

76: // ConstructorCount++;

77: }

78:

79: // деструктор освобождает занятую память

80: String::~String ()

81: {

82: delete [] itsString;

83: itsLen = 0;

84: // cout << "\tString destructor\n";

85: }

86:

87: // оператор равенства освобождает память, а затем

88: // копирует строку и размер

89: String& String::operator=(const String & rhs)

90: {

91: if (this == &rhs)

92: return *this;

93: delete [] itsString;

94: itsLen=rhs.GetLen();

95: itsString = new char[itsLen+1];

96: for (int i = 0; i<itsLen;i++)

97: itsString[i] = rhs[i];

98: itsString[itsLen] = '\0';

99: return *this;

100: // cout << "\tString operator=\n";

101: }

102:

103: // неконстантный оператор индексирования,

104: // возвращает ссылку на символ, который можно

105: // изменить!

106: char & String::operator[](int offset)

107: {

108: if (offset > itsLen)

109: return itsString[itsLen-1];

110: else

111: return itsString[offset];

112: }

113:

114: // константный оператор индексирования,

115: // используется для константных объектов (см. конструктор-копировщик!)

116: char String::operator[](int offset) const

117: {

118: if (offset > itsLen)

119: return itsString[itsLen-1];

120: else

121: return itsString[offset];

122: }

123:

124: // создает новую строку, добавляя текущую

125: // строку к rhs

126: String String::operator+(const String& rhs)

127: {

12S: int totalLen = itsLen + rhs.GetLen();

129: String temp(totalLen);

130: int i, j;

131: for (i = 0; i<itsLen; i++)

132: temp[i] = itsString[i];

133: for (j = 0; j<rhs.GetLen(); j++, i++)

134: temp[i] = rhs[];

135: temp[totalLen]='\0';

136: return temp;

137: }

138:

139: // изменяет текущую строку, ничего не возвращая

140: void String::operator+=(const String& rhs)

141: {

142: unsigned short rhsLen = rhs.GetLen();

143: unsigned short totalLen = itsLen + rhsLen;

144: String temp(totalLen);

145: int i, j;

146: for (i = 0; i<itsLen; i++)

147: temp[i] = itsString[i];

148: for (j = 0, i = 0; j<rhs.GetLen(); j++, i++)

149: temp[i] = rhs[i-itsLen];

150: temp[totalLen]='\0' ;

151: *this = temp;

152: }

153:

154: // int String::ConstructorCount =

155: ostream& operator<< ( ostream& theStream,String& theString)

156: {

157: theStream << theString.itsString; 158: return theStream;

159: }

160:

161: int main()

162: {

163: String theString("Hello world.");

164: cout << theString;

165: return 0;

166: }

Результат:

Hello world.

Анализ: В строке 19 operator<< объявляется как функция-друг, которая принимает ссылки на ostream и String и возвращает ссылку на ostream. Обратите внимание, что она не является функцией-членом класса String. Поскольку эта функция возвращает ссылку на ostream, можно конкатенировать вызовы operator<< следующим образом:

cout << "myAge: " << itsAge << " years. ";

Выполнение этой функции-друга представлено строками 155—159. Основное назначение функции состоит в том, чтобы скрыть детали процедуры передачи строки в iostream. Больше ничего и не требуется. Более подробно о функции ввода и перегрузке operator>> вы узнаете на следующем занятии.

Резюме

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

Вы также узнали, как объявлять классы и функции-друзьями другого класса. Объявление функции друга позволяет перегрузить оператор ввода таким образом, что появляется возможность использовать объект cout в пользовательском классе точно так же, как в стандартных встроенных классах.

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

Вопросы и ответы

Почему так важно разбираться в особенностях отношений между классами при выборе различных подходов установки взаимосвязей между ними?

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

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

Современное программирование — это разрешение противоречий между достижением максимальной точности моделирования событий и предупреждением чрезвычайного усложнения программ. Поэтому чем больше объектов программы будут использоваться как "черные ящики", тем меньше всевозможных параметров нужно отслеживать при отладке или модернизации программы. Методы вложенных классов скрыты от пользователей, что нельзя сказать о закрытом наследовании.

Почему бы не описать все классы, объекты которых используются в других классах, друзьями этих классов?

Объявление одного класса другом какого-либо иного открывает закрытые методы и данные класса, что снижает инкапсуляцию класса. Лучше всего держать как можно больше членов одного класса закрытыми от всех остальных классов.

Если функция перегружается, нужно ли описывать каждый вариант этой функции другом класса?

Да. Если вы перегружаете функцию и хотите представить все варианты этой функции друзьями другого класса, то в описании класса каждый вариант функции должен сопровождаться ключевым словом friend.

Коллоквиум

В этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из вопросов, предложенных ниже.

Контрольные вопросы

1. Как объявить класс, являющийся частным проявлением другого класса?

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

3. В чем состоят различия между вложением и делегированием?

4. В чем состоят различия между делегированием и выполнением класса в пределах другого класса?

5. Что такое функция-друг?

6. Что такое класс-друг?

7. Если класс Dog является другом Boy, то можно ли сказать, что Boy — друг Dog?

8. Если класс Dog является другом Boy, а Terrier произведен от Dog, является ли Terrier другом Boy?

9. Если класс Dog является другом Boy, а Boy — другом House, можно ли считать Dog другом House?

10. Где необходимо размещать объявление функции-друга?

Упражнения

1. Объявите класс Animal, который содержит переменную-член, являющуюся объектом класса String.

2. Объявите класс BoundedArray, являющийся массивом.

3. Объявите класс Set, выполняемый в пределах массива BoundedArray.

4. Измените листинг 15.1 таким образом, чтобы класс String включал перегруженный оператор вывода (>>).

5. Жучки: найдите ошибки в этом коде:

1: #include <iostream.h>

2:

3: class Animal;

4:

5: void setValue(Animal& , int);

6:

7:

8: class Animal

9: {

10: public:

11: int GetWeight()const { return itsWeight; }

12: int GetAge() const { return itsAge; }

13: private:

14: int itsWeight;

15: int itsAge;

16: };

17:

18: void setValue(Animal& theAnimal, int theWeight)

19: {

20: friend class Animal;

21: theAnimal.itsWeight = theWeight;

22: }

23:

24: int main()

25: {

26: Animal peppy;

27: setValue(peppy,5);

28: }

6. Исправьте листинг, приведенный в упражнении 5, и откомпилируйте его.

7. Жучки: найдите ошибки в этом коде:

1: #include<iostream.h>

2:

3: class Animal;

4:

5: void setValue(Animal& , int);

6: void setValue(Animal&. ,int,int);

7:

8: class Animal

9: {

10: friend void setValue(Animal& ,int);

11: private:

12: int itsWeight;

13: int itsAge;

14: };

15:

16: void setValue(Animal& theAnimal, int theWeight)

17: {

18: theAnimal.itsWeight = theWeight;

19: }

20:

21:

22: void setValue(Animal& theAnimal, int theWeight, int theAge)

23: {

24: theAnimal.itsWeight = theWeight;

25: theAnimal.itsAge = theAge;

26: }

27:

28: int main()

29: {

30: Animal peppy;

31: setValue(peppy,5);

32: setValue(peppy, 7, 9);

33: }

8. Исправьте листинг, приведенный в упражнении 7, и откомпилируйте его.

День 16-й. Потоки

Ранее для вывода на экран и считывания с клавиатуры мы использовали объекты cout и cin, не понимая до конца принципов их работы. Сегодня вы узнаете:

• Что такое потоки ввода-вывода и как они используются

• Как с помощью потоков управлять вводом и выводом данных

• Как с помощью потоков записывать информацию в файл и затем считывать ее

Знакомство с потоками

Язык программирования C++ специально не определяет, каким образом данные выводятся на экран или в файл либо как они считываются программой. Тем не менее эти особенности являются важной частью работы программиста, поэтому стандартная библиотека C++ включает библиотеку iostream, упрощающую ввод-вывод (I/O).

Благодаря выделению операций ввода-вывода в отдельную библиотеку упрощается создание аппаратно независимого языка разработки программ для разных платформ. Это позволяет создать программу на C++ для компьютеров PC, а затем откомпилировать ее для рабочей станции Sun. Разработчики снабдили компилятор библиотеками для всех случаев. Так, по крайней мере, должно быть теоретически.

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

Инкапсуляция

Классы iostream рассматривают информацию, выводимую программой на экран, как побитовый поток данных. Если данные выводятся в файл или на экран, то источник потока, как правило, содержится в программе. Если же поток направлен в противоположную сторону, данные могут поступать с клавиатуры или файла на диске. В этом случае они заносятся в переменные.

Одна из основных целей использования потоков состоит в инкапсуляции процедуры обмена данными с диском или дисплеем компьютера. Сама программа работает только с потоками, которые реализуют эти процессы. Схематически эта идея проиллюстрирована на рис. 16.1.

Рис. 16.1. Инкапсуляция с помощью потоков

Буферизация

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

Суть идеи проиллюстрирована на примере знакомого со школьной скамьи бака с в.одой (рис. 16.2). Вода заливается сверху, и бак постепенно наполняется, поскольку нижний вентиль закрыт.

Когда вода (данные) достигает верха, нижний вентиль автоматически открывается и вся вода выливается (рис. 16.3).

Как только бак опустеет, нижний вентиль закрывается, а верхний открывается вновь, и вода снова поступает в бак (рис. 16.4).

В некоторых случаях необходимо, чтобы вода сразу же выливалась из бака, не дожидаясь его наполнения. В программировании такая ситуация называется очисткой буфера (рис. 16.5).

Рис. 16.2. Буфер наполняется данными, как закрытый бак — водой

Рис. 16.3. Открывается сливной вентиль, и вода (данные) сливается из бака

Рис. 16.4. Повторное наполнение бака

Рис. 16.5. Очистка буфера подобна экстренному сливу воды

Потоки и буферы

В C++ применяется объектно-ориентированный подход к реализации обмена данными с буферизированными потоками.

• Класс streambuf управляет буфером, поэтому его функции предоставляют возможность наполнять, опорожнять и очищать буфер, а также выполнять с ним другие операции.

• Класс ios является базовым для классов потоков ввода-вывода. В качестве переменной-члена класса ios выступает объект streambuf.

• Классы istream и ostream являются производными от класса ios и отвечают соответственно за потоковый ввод и вывод данных.

• Класс iosteam является производным от классов istream и ostream и обеспечивает методы ввода-вывода для печати на экран.

• Классы fstream используются для ввода-вывода из файлов.

Наши рекомендации