НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Что лепим морфу первым артом?
Всего ответов: 354
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

15. Перегруженные операторы и определенные пользователем преобразования (1)
15. Перегруженные операторы и определенные пользователем преобразования

В главе 15 мы рассмотрим два вида специальных функций: перегруженные операторы и определенные пользователем преобразования. Они дают возможность употреблять объекты классов в выражениях так же интуитивно, как и объекты встроенных типов. В этой главе мы сначала изложим общие концепции проектирования перегруженных операторов. Затем представим понятие друзей класса со специальными правами доступа и обсудим, зачем они применяются, обратив особое внимание на то, как реализуются некоторые перегруженные операторы: присваивание, взятие индекса, вызов, стрелка для доступа к члену класса, инкремент и декремент, а также специализированные для класса операторы new и delete. Другая категория специальных функций, которая рассматривается в этой главе, – это функции преобразования членов (конвертеры), составляющие набор стандартных преобразований для типа класса. Они неявно применяются компилятором, когда объекты классов используются в качестве фактических аргументов функции или операндов встроенных или перегруженных операторов. Завершается глава развернутым изложением правил разрешения перегрузки функций с учетом передачи объектов в качестве аргументов, функций-членов класса и перегруженных операторов.
15.1. Перегрузка операторов

В предыдущих главах мы уже показывали, что перегрузка операторов позволяет программисту вводить собственные версии предопределенных операторов (см. главу 4) для операндов типа классов. Например, в классе String из раздела 3.15 задано много перегруженных операторов. Ниже приведено его определение:

#include <iostream>
class String;
istream& operator>>( istream &, const String & );
ostream& operator<<( ostream &, const String & );
class String {
public:
     // набор перегруженных конструкторов
     // для автоматической инициализации
    String( const char* = 0 );
    String( const String & );

     // деструктор: автоматическое уничтожение
    ~String();

     // набор перегруженных операторов присваивания
    String& operator=( const String & );
    String& operator=( const char * );

     // перегруженный оператор взятия индекса
    char& operator[]( int );

     // набор перегруженных операторов равенства
     // str1 == str2;
    bool operator==( const char * );
    bool operator==( const String & );

     // функции доступа к членам
    int size() { return _size; };
    char * c_str() { return _string; }
private:
    int   _size;
    char *_string;
};

В классе String есть три набора перегруженных операторов. Первый – это набор операторов присваивания:

// набор перегруженных операторов присваивания
String& operator=( const String & );
String& operator=( const char * );

Сначала идет копирующий оператор присваивания. (Подробно они обсуждались в разделе 14.7.) Следующий оператор поддерживает присваивание C-строки символов объекту типа String:

String name;
name = "Sherlock";   // использование оператора operator=( char * )

(Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.)

Во втором наборе есть всего один оператор – взятия индекса:

// перегруженный оператор взятия индекса
char& operator[]( int );

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

if ( name[0] != 'S' )
   cout <<"увы, что-то не так\n";

(Детально этот оператор описывается в разделе 15.4.)

В третьем наборе определены перегруженные операторы равенства для объектов класса String. Программа может проверить равенство двух таких объектов или объекта и C-строки:

// набор перегруженных операторов равенства
// str1 == str2;
bool operator==( const char * );
bool operator==( const String & );

Перегруженные операторы позволяют использовать объекты типа класса с операторами, определенными в главе 4, и манипулировать ими так же интуитивно, как объектами встроенных типов. Например, желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat(), а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Зачастую имя проще запомнить, если определить перегруженный оператор. К примеру, вместо concat() мы назвали бы новую операцию operator+=(). Такой оператор используется следующим образом:

#include "String.h"
int main() {
   String name1 "Sherlock";
   String name2 "Holmes";

   name1 += " ";
   name1 += name2;

   if (! ( name1 == "Sherlock Holmes" ) )
      cout < < "конкатенация не сработала\n";
}

Перегруженный оператор объявляется в теле класса точно так же, как обычная функция-член, только его имя состоит из ключевого слова operator, за которым следует один из множества предопределенных в языке C++ операторов (см. табл. 15.1). Так можно объявить operator+=() в классе String:

class String {
public:
   // набор перегруженных операторов +=
   String& operator+=( const String & );
   String& operator+=( const char * );
   // ...
private:
   // ...
};

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

#include <cstring>

inline String& String::operator+=( const String &rhs )
{
     // Если строка, на которую ссылается rhs, непуста
    if ( rhs._string )
    {
        String tmp( *this );

        // выделить область памяти, достаточную
        // для хранения конкатенированных строк
        _size += rhs._size;
        delete [] _string;
        _string = new char[ _size + 1 ];

        // сначала скопировать в выделенную область исходную строку
        // затем дописать в конец строку, на которую ссылается rhs
        strcpy( _string, tmp._string );
        strcpy( _string + tmp._size, rhs._string );
     }
    return *this;
}

inline String& String::operator+=( const char *s )
{
     // Если указатель s ненулевой
    if ( s )
    {
        String tmp( *this );

        // выделить область памяти, достаточную
        // для хранения конкатенированных строк
        _size += strlen( s );
        delete [] _string;
        _string = new char[ _size + 1 ];
        // сначала скопировать в выделенную область исходную строку
        // затем дописать в конец C-строку, на которую ссылается s
        strcpy( _string, tmp._string );
        strcpy( _string + tmp._size, s );
     }
    return *this;
}

15.1.1. Члены и не члены класса

Рассмотрим операторы равенства в нашем классе String более внимательно. Первый оператор позволяет устанавливать равенство двух объектов, а второй – объекта и C-строки:

#include "String.h"
int main() {
   String flower;
   // что-нибудь записать в переменную flower

   if ( flower == "lily" )  // правильно
      // ...
   else
   if ( "tulip" == flower )  // ошибка
      // ...
}

При первом использовании оператора равенства в main() вызывается перегруженный operator==(const char *) класса String. Однако на второй инструкции if компилятор выдает сообщение об ошибке. В чем дело?

Перегруженный оператор, являющийся членом некоторого класса, применяется только тогда, когда левым операндом служит объект этого класса. Поскольку во втором случае левый операнд не принадлежит к классу String, компилятор пытается найти такой встроенный оператор, для которого левым операндом может быть C-строка, а правым – объект класса String. Разумеется, его не существует, поэтому компилятор говорит об ошибке.

Но можно же создать объект класса String из C-строки с помощью конструктора класса. Почему компилятор не выполнит неявно такое преобразование:

if ( String( "tulip" ) == flower )  //правильно: вызывается оператор-член

Причина в его неэффективности. Перегруженные операторы не требуют, чтобы оба операнда имели один и тот же тип. К примеру, в классе Text определяются следующие операторы равенства:

class Text {
public:
   Text( const char * = 0 );
   Text( const Text & );

   // набор перегруженных операторов равенства
   bool operator==( const char * ) const;
   bool operator==( const String & ) const;
   bool operator==( const Text & ) const;

   // ...
};

и выражение в main() можно переписать так:

if ( Text( "tulip" ) == flower )  // вызывается Text::operator==()

Следовательно, чтобы найти подходящий для сравнения оператор равенства, компилятору придется просмотреть все определения классов в поисках конструктора, способного привести левый операнд к некоторому типу класса. Затем для каждого из таких типов нужно проверить все ассоциированные с ним перегруженные операторы равенства, чтобы понять, может ли хоть один из них выполнить сравнение. А после этого компилятор должен решить, какая из найденных комбинаций конструктора и оператора равенства (если таковые нашлись) лучше всего соответствует операнду в правой части! Если потребовать от компилятора выполнения всех этих действий, то время трансляции программ C++ резко возрастет. Вместо этого компилятор просматривает только перегруженные операторы, определенные как члены класса левого операнда (и его базовых классов, как мы покажем в главе 19).

Разрешается, однако, определять перегруженные операторы, не являющиеся членами класса. При анализе строки в main(), вызвавшей ошибку компиляции, подобные операторы принимались во внимание. Таким образом, сравнение, в котором C-строка стоит в левой части, можно сделать корректным, если заменить операторы равенства, являющиеся членами класса String, на операторы равенства, объявленные в области видимости пространства имен:

bool operator==( const String &, const String & );
bool operator==( const String &, const char * );

Обратите внимание, что эти глобальные перегруженные операторы имеют на один параметр больше, чем операторы-члены. Если оператор является членом класса, то первым параметром неявно передается указатель this. То есть для операторов-членов выражение

flower == "lily"

переписывается компилятором в виде:

flower.operator==( "lily" )

и на левый операнд flower в определении перегруженного оператора-члена можно сослаться с помощью this. (Указатель this введен в разделе 13.4.) В случае глобального перегруженного оператора параметр, представляющий левый операнд, должен быть задан явно.

Тогда выражение

flower == "lily"

вызывает оператор

bool operator==( const String &, const char * );

Непонятно, какой оператор вызывается для второго случая использования оператора равенства:

"tulip" == flower

Мы ведь не определили такой перегруженный оператор:

bool operator==( const char *, const String & );

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

operator==( String("tulip"), flower );

и вызывает для выполнения сравнения следующий перегруженный оператор: bool operator==( const String &, const String & );
Но тогда зачем мы предоставили второй перегруженный оператор:

bool operator==( const String &, const char * );

Преобразование типа из C-строки в класс String может быть применено и к правому операнду. Функция main() будет компилироваться без ошибок, если просто определить в пространстве имен перегруженный оператор, принимающий два операнда String:

bool operator==( const String &, const String & );

Предоставлять ли только этот оператор или еще два:

bool operator==( const char *, const String & );
bool operator==( const String &, const char * );

зависит от того, насколько велики затраты на преобразование из C-строки в String во время выполнения, то есть от "стоимости” дополнительных вызовов конструктора в программах, пользующихся нашим классом String. Если оператор равенства будет часто использоваться для сравнения C-строк и объектов , то лучше предоставить все три варианта. (Мы вернемся к вопросу эффективности в разделе, посвященном друзьям.

Подробнее о приведении к типу класса с помощью конструкторов мы расскажем в разделе 15.9; в разделе 15.10 речь пойдет о разрешении перегрузки функций с помощью описанных преобразований, а в разделе 15.12 – о разрешении перегрузки операторов.)

Итак, на основе чего принимается решение, делать ли оператор членом класса или членом пространства имен? В некоторых случаях у программиста просто нет выбора:

    * если перегруженный оператор является членом класса, то он вызывается лишь при условии, что левым операндом служит член этого класса. Если же левый операнд имеет другой тип, оператор обязан быть членом пространства имен;
    * язык требует, чтобы операторы присваивания ("="), взятия индекса ("[]"), вызова ("()") и доступа к членам по стрелке ("->") были определены как члены класса. В противном случае выдается сообщение об ошибке компиляции:

// ошибка: должен быть членом класса
char& operator[]( String &, int ix );

(Подробнее оператор присваивания рассматривается в разделе 15.3, взятия индекса – в разделе 15.4, вызова – в разделе 15.5, а оператор доступа к члену по стрелке – в разделе 15.6.)

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

Прежде чем закончить этот подраздел, определим операторы равенства для класса String в пространстве имен:

bool operator==( const String &str1, const String &str2 )
{
   if ( str1.size() != str2.size() )
      return false;
   return strcmp( str1.c_str(), str2.c_str() ) ? false : true ;
}

inline bool operator==( const String &str, const char *s )
{
   return strcmp( str.c_str(), s ) ? false : true ;
}

15.1.2. Имена перегруженных операторов

Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).

Таблица 15.1. Перегружаемые операторы

+    -    *    /    %    ^    &    |    ~
!    ,    =    <    >    <=    >=    ++    --
<<    >>    ==    !=    &&    ||    +=    -=    /=
%=    ^=    &=    |=    *=    <=    >>=    []    ()
->    ->*    new    new[]    delete    delete[]

Проектировщик класса не вправе объявить перегруженным оператор с другим именем. Так, при попытке объявить оператор ** для возведения в степень компилятор выдаст сообщение об ошибке.

Следующие четыре оператора языка C++ не могут быть перегружены:

// неперегружаемые операторы
:: .* . ?:

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

// ошибка: нельзя переопределить встроенный оператор сложения int
int operator+( int, int );

Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов.

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

Предопределенные приоритеты операторов (см. раздел 4.13) изменить нельзя. Независимо от типа класса и реализации оператора в инструкции

x == y + z;

всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.

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

// некорректно: ! - это унарный оператор
bool operator!( const String &s1, const String &s2 )
{
   return ( strcmp( s1.c_str(), s2.c_str() ) != 0 );
}

Для встроенных типов четыре предопределенных оператора ("+", "-", "*" и "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.

Для всех перегруженных операторов, за исключением operator(), недопустимы аргументы по умолчанию.
15.1.3. Разработка перегруженных операторов

Операторы присваивания, взятия адреса и оператор "запятая” имеют предопределенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать. Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком. Выбор предоставляемых операторов зависит от ожидаемого использования класса.

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

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

    * isEmpty() становится оператором "ЛОГИЧЕСКОЕ НЕ”, operator!().
    * isEqual() становится оператором равенства, operator==().
    * copy() становится оператором присваивания, operator=().

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

Примером неправильного использования перегрузки операторов является определение operator+() как операции вычитания, что бессмысленно: не согласующаяся с интуицией семантика опасна.

Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций. Безупречно четкое и обоснованное объяснение того, что делает operator+(), вряд ли устроит пользователей класса String, полагающих, что он служит для конкатенации строк. Если семантика перегруженного оператора неочевидна, то лучше его не предоставлять.

Эквивалентность семантики составного оператора и соответствующей последовательности простых операторов для встроенных типов (например, эквивалентность оператора +, за которым следует =, и составного оператора +=) должна быть явно поддержана и для класса. Предположим, для String определены как operator+(), так и operator=() для поддержки операций конкатенации и почленного копирования:

String s1( "C");
String s2( "++" );
s1 = s1 + s2;    // s1 == "C++"

Но этого недостаточно для поддержки составного оператора присваивания

s1 += s2;

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

Упражнение 15.1

Почему при выполнении следующего сравнения не вызывается перегруженный оператор operator==(const String&, const String&):

"cobble" == "stone"

Упражнение 15.2

Напишите перегруженные операторы неравенства, которые могут быть использованы в таких сравнениях:

String != String
String != С-строка
C-строка != String

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

Упражнение 15.3

Выявите те функции-члены класса Screen, реализованного в главе 13 (разделы 13.3, 13.4 и 13.6), которые можно перегружать.

Упражнение 15.4

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

Упражнение 15.5

Реализуйте перегруженные операторы ввода и вывода для класса Screen из главы 13.
15.2. Друзья

Рассмотрим еще раз перегруженные операторы равенства для класса String, определенные в области видимости пространства имен. Оператор равенства для двух объектов String выглядит следующим образом:

bool operator==( const String &str1, const String &str2 )
{
   if ( str1.size() != str2.size() )
      return false;
   return strcmp( str1.c_str(), str2.c_str() ) ? false : true;
}

Сравните это определение с определением того же оператора как функции-члена:

bool String::operator==( const String &rhs ) const
{
   if ( _size != rhs._size )
      return false;
   return strcmp( _string, rhs._string ) ? false : true;
}

Нам пришлось модифицировать способ обращения к закрытым членам класса String. Поскольку новый оператор равенства – это глобальная функция, а не функция-член, у него нет доступа к закрытым членам класса String. Для получения размера объекта String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().

Альтернативной реализацией является объявление глобальных операторов равенства друзьями класса String. Если функция или оператор объявлены таким образом, им предоставляется доступ к неоткрытым членам.

Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения, то безразлично, в какой из секций – public, private или protected – они объявлены. В примере ниже мы решили поместить все подобные объявления сразу после заголовка класса:

class String {
   friend bool operator==( const String &, const String & );
   friend bool operator==( const char *, const String & );
   friend bool operator==( const String &, const char * );
public:
   // ... остальная часть класса String
};

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

// дружественные операторы напрямую обращаются к закрытым членам
// класса String
bool operator==( const String &str1, const String &str2 )
{
   if ( str1._size != str2._size )
      return false;
   return strcmp( str1._string, str2._string ) ? false : true;
}
inline bool operator==( const String &str, const char *s )
{
   return strcmp( str._string, s ) ? false : true;
}
// и т.д.

Можно возразить, что в данном случае прямой доступ к членам _size и _string необязателен, так как встроенные функции c_str() и size() столь же эффективны и при этом сохраняют инкапсуляцию, а значит, нет особой нужды объявлять операторы равенства для класса String его друзьями.

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

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

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

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

extern ostream& storeOn( ostream &, Screen & );
extern BitMap& storeOn( BitMap &, Screen & );
// ...

class Screen
{
   friend ostream& storeOn( ostream &, Screen & );
   friend BitMap& storeOn( BitMap &, Screen & );
   // ...
};

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

Объявление функции другом двух классов должно выглядеть так:

class Window;   // это всего лишь объявление
class Screen {
   friend bool is_equal( Screen &, Window & );
   // ...
};

class Window {
   friend bool is_equal( Screen &, Window & );
   // ...
};

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

class Window;
class Screen {
   // copy() - член класса Screen
   Screen& copy( Window & );
   // ...
};

class Window {
   // Screen::copy() - друг класса Window
   friend Screen& Screen::copy( Window & );
   // ...
};

Screen& Screen::copy( Window & ) { /* ... */ }

Функция-член одного класса не может быть объявлена другом второго, пока компилятор не увидел определения ее собственного класса. Это не всегда возможно. Предположим, что Screen должен объявить некоторые функции-члены Window своими друзьями, а Window – объявить таким же образом некоторые функции-члена Screen. В таком случае весь класс Window объявляется другом Screen:

class Window;
class Screen {
   friend class Window;
   // ...
};

К закрытым членам класса Screen теперь можно обращаться из любой функции-члена Window.

Упражнение 15.6

Реализуйте операторы ввода и вывода, определенные для класса Screen в упражнении 15.5, в виде друзей и модифицируйте их определения так, чтобы они напрямую обращались к закрытым членам. Какая реализация лучше? Объясните почему.
15.3. Оператор =

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

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

String car ("Volks");
car = "Studebaker";

мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе:

class String {
public:
   // оператор присваивания для char*
   String& operator=( const char * );
   // ...
private:
   int  _size;
   char *string;
};

Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится "пустым". В противном случае ему присваивается копия C-строки:

String& String::operator=( const char *sobj )
{
   // sobj - нулевой указатель
   if (! sobj ) {
      _size = 0;
      delete[] _string;
      _string = 0;
   }
   else {
      _size = strlen( sobj );
      delete[] _string;
      _string = new char[ _size + 1 ];
      strcpy( _string, sobj );
   }
   return *this;
}

_string ссылается на копию той C-строки, на которую указывает sobj. Почему на копию? Потому что непосредственно присвоить sobj члену _string нельзя:

_string = sobj; // ошибка: несоответствие типов

sobj – это указатель на const и, следовательно, не может быть присвоен указателю на "не-const" (см. раздел 3.5). Изменим определение оператора присваивания:

String& String::operator=( const *sobj ) { // ... }

Теперь _string прямо ссылается на C-строку, адресованную sobj. Однако при этом возникают другие проблемы. Напомним, что C-строка имеет тип const char*. Определение параметра как указателя на не-const делает присваивание невозможным:

car = "Studebaker";  // недопустимо с помощью operator=( char *) !

Итак, выбора нет. Чтобы присвоить C-строку объекту типа String, параметр должен иметь тип const char*.

Хранение в _string прямой ссылки на C-строку, адресуемую sobj, порождает и иные сложности. Мы не знаем, на что именно указывает sobj. Это может быть массив символов, который модифицируется способом, неизвестным объекту String. Например:

char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };
String trap = ia;   // trap._string ссылается на ia
ia[3] = 'g';  // а вот это нам не нужно:
              // модифицируется и ia, и trap._string

Если trap._string напрямую ссылался на ia, то объект trap демонстрировал бы своеобразное поведение: его значение может изменяться без вызова функций-членов класса String. Поэтому мы полагаем, что выделение области памяти для хранения копии значения C-строки менее опасно.

Обратите внимание, что в операторе присваивания используется delete. Член _string содержит ссылку на массив символов, расположенный в хипе. Чтобы предотвратить утечку, память, выделенная под старую строку, освобождается с помощью delete до выделения памяти под новую. Поскольку _string адресует массив символов, следует использовать версию delete для массивов (см. раздел 8.4).

И последнее замечание об операторе присваивания. Тип возвращаемого им значения – это ссылка на класс String. Почему именно ссылка? Дело в том, что для встроенных типов операторы присваивания можно сцеплять:

// сцепление операторов присваивания
int iobj, jobj;
iobj = jobj = 63;

Они ассоциируются справа налево, т.е. в предыдущем примере присваивания выполняются так:

iobj = (jobj = 63);

Это удобно и при работе с объектами класса String: поддерживается, к примеру, следующая конструкция:

String ver, noun;
verb = noun = "count";

При первом присваивании из этой цепочки вызывается определенный ранее оператор для const char*. Тип полученного результата должен быть таким, чтобы его можно было использовать как аргумент для копирующего оператора присваивания класса String. Поэтому, хотя параметр данного оператора имеет тип const char *, возвращается все же ссылка на String.

Операторы присваивания бывают перегруженными. Например, в нашем классе String есть такой набор:

// набор перегруженных операторов присваивания
String& operator=( const String & );
String& operator=( const char * );

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

Оператор взятия индекса operator[]() можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе 2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор взятия индекса обязан быть функцией-членом класса.

У пользователей String должна иметься возможность чтения и записи отдельных символов члена _string. Мы хотим поддержать следующий способ применения объектов данного класса:

String entry( "extravagant" );
String mycopy;

for ( int ix = 0; ix < entry.size(); ++ix )
   mycopy[ ix ] = entry[ ix ];

Оператор взятия индекса может появляться как слева, так и справа от оператора присваивания. Чтобы быть в левой части, он должен возвращать l-значение индексируемого элемента. Для этого мы возвращаем ссылку:

#include <cassert>

inine char&
String::operator[]( int elem ) const
{
   assert( elem >= 0 && elem < _size );
   return _string[ elem ];
}

В следующем фрагменте нулевому элементу массива color присваивается символ 'V':

String color( "violet" );
color[ 0 ] = 'V';

Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)
15.5. Оператор вызова функции

Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int можно определить класс absInt:

class absInt {
public:
   int operator()( int val ) {
      int result = val < 0 ? -val : val;
      return result;
   }
};

Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением.

#include <vector>
#include <algoritm>

int main() {
   int ia[] = { -0, 1, -1, -2, 3, 5, -5, 8 };
   vector<int > ivec( ia, ia+8 );

   // заменить каждый элемент его абсолютным значением
   transform( ivec.begin(), ivec.end(), ivec.begin(), absInt() );

   // ...
}

Первый и второй аргументы transform() ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.

Четвертый аргумент – это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:

typedef vector<int >::iterator iter_type;
// конкретизация transform()
// операция absInt применяется к элементу вектора int
iter_type transform( iter_type iter, iter_type last,
                   iter_type result, absInt func )
{
   while ( iter != last )
      *result++ = func( *iter++ );  // вызывается absInt::operator()

   return iter;
}

func – это объект класса, который предоставляет операцию absInt, заменяющую число типа int его абсолютным значением. Он используется для вызова перегруженного оператора operator() класса absInt. Этому оператору передается аргумент *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.
15.6. Оператор "стрелка"

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

Допустим, мы хотим определить тип класса для представления указателя на объект Screen (см. главу 13):

class ScreenPtr {
   // ...
private:
   Screen *ptr;
};

Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию (детально конструкторы рассматривались в разделе 14.2):

class ScreenPtr {
public:
   ScreenPtr( const Screen &s ) : ptr( &s ) { }
   // ...
};

В любом определении объекта класса ScreenPtr должен присутствовать инициализатор – объект класса Screen, на который будет ссылаться объект ScreenPtr:

ScreenPtr p1;   // ошибка: у класса ScreenPtr нет конструктора по умолчанию

Screen myScreen( 4, 4 );
ScreenPtr ps( myScreen );  // правильно

Чтобы класс ScreenPtr вел себя как встроенный указатель, необходимо определить некоторые перегруженные операторы – разыменования (*) и "стрелку” для доступа к членам:

// перегруженные операторы для поддержки поведения указателя
class ScreenPtr {
public:
   Screen& operator*()  { return *ptr; }
   Screen* operator->() { return ptr; }
   // ...
};

Оператор доступа к членам унарный, поэтому параметры ему не передаются. При использовании в составе выражения его результат зависит только от типа левого операнда. Например, в инструкции point->action(); исследуется тип point. Если это указатель на некоторый тип класса, то применяется семантика встроенного оператора доступа к члену. Если же это объект или ссылка на объект, то проверяется, есть ли в этом классе перегруженный оператор доступа. Когда перегруженный оператор "стрелка" определен, он вызывается для объекта point, иначе инструкция неверна, поскольку для обращения к членам самого объекта (в том числе по ссылке) следует использовать оператор "точка". Перегруженный оператор "стрелка" должен возвращать либо указатель на тип класса, либо объект класса, в котором он определен. Если возвращается указатель, то к нему применяется семантика встроенного оператора "стрелка". В противном случае процесс продолжается рекурсивно, пока не будет получен указатель или определена ошибка. Например, так можно воспользоваться объектом ps класса ScreenPtr для доступа к членам Screen: ps->move( 2, 3 ); Поскольку слева от оператора "стрелка" находится объект типа ScreenPtr, то употребляется перегруженный оператор этого класса, который возвращает указатель на объект Screen. Затем к полученному значению применяется встроенный оператор "стрелка" для вызова функции-члена move(). Ниже приводится небольшая программа для тестирования класса ScreenPtr. Объект типа ScreenPtr используется точно так же, как любой объект типа Screen*: #include <iostream> #include <string> #include "Screen.h" void printScreen( const ScreenPtr &ps ) { cout << "Screen Object ( " << ps->height() << ", " << ps->width() << " )\n\n"; for ( int ix = 1; ix <= ps->height(); ++ix ) { for ( int iy = 1; iy <= ps->width(); ++iy ) cout <<ps->get( ix, iy ); cout << "\n"; } } int main() { Screen sobj( 2, 5 ); string init( "HelloWorld" ); ScreenPtr ps( sobj ); // Установить содержимое экрана string::size_type initpos = 0; for ( int ix = 1; ix <= ps->height(); ++ix ) for ( int iy = 1; iy <= ps->width(); ++iy ) { ps->move( ix, iy ); ps->set( init[ initpos++ ] ); } // Вывести содержимое экрана printScreen( ps ); return 0; }

Разумеется, подобные манипуляции с указателями на объекты классов не так эффективны, как работа со встроенными указателями. Поэтому интеллектуальный указатель должен предоставлять дополнительную функциональность, важную для приложения, чтобы оправдать сложность своего использования.
15.7. Операторы инкремента и декремента

Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и декремент (--). Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.

Сначала мы определим новый член size, который содержит либо нуль (это говорит о том, что объект ScreenPtr указывает на единственный объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:

class ScreenPtr {
public:
   // ...
private:
   int size;         // размер массива: 0, если единственный объект
   int offset;       // смещение ptr от начала массива
   Screen *ptr;
};

Модифицируем конструктор класса ScreenPtr с учетом его новой функциональности и дополнительных членов,. Пользователь нашего класса должен передать конструктору дополнительный аргумент, если создаваемый объект указывает на массив:

class ScreenPtr {
public:
    ScreenPtr( Screen &s , int arraySize = 0 )
              : ptr( &s ), size ( arraySize ), offset( 0 ) { }
private:
   int size;
   int offset;
   Screen *ptr;
};

С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr можно определять следующим образом:

Screen myScreen( 4, 4 );
ScreenPtr pobj( myScreen );  // правильно: указывает на один объект
const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
ScreenPtr parr( *parray, arrSize );  // правильно: указывает на массив

Теперь мы готовы определить в ScreenPtr перегруженные операторы инкремента и декремента. Однако они бывают двух видов: префиксные и постфиксные. К счастью, можно определить оба варианта. Для префиксного оператора объявление не содержит ничего неожиданного:

class ScreenPtr {
public:
   Screen& operator++();
   Screen& operator--();
   // ...
};

Такие операторы определяются как унарные операторные функции. Использовать префиксный оператор инкремента можно, к примеру, следующим образом: const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr( *parray, arrSize ); for ( int ix = 0; ix < arrSize; ++ix, ++parr ) // эквивалентно parr.operator++() } printScreen( parr );

Определения этих перегруженных операторов приведены ниже:

Screen& ScreenPtr::operator++()
{
   if ( size == 0 ) {
      cerr <<"не могу инкрементировать указатель для одного объекта\n";
      return *ptr;
   }
   if ( offset >= size - 1 ) {
      cerr << "уже в конце массива\n";
      return *ptr;
   }

   ++offset;
   return *++ptr;
}

Screen& ScreenPtr::operator--()
{
   if ( size == 0 ) {
      cerr << "не могу декрементировать указатель для одного объекта\n";
      return *ptr;
   }
   if ( offset <= 0 ) {
      cerr << "уже в начале массива\n";
      return *ptr;
   }

   --offset;
   return *--ptr;
}

Чтобы отличить префиксные операторы от постфиксных, в объявлениях последних имеется дополнительный параметр типа int. В следующем фрагменте объявлены префиксные и постфиксные варианты операторов инкремента и декремента для класса ScreenPtr:

class ScreenPtr {
public:
   Screen& operator++();    // префиксные операторы
   Screen& operator--();
   Screen& operator++(int); // постфиксные операторы
   Screen& operator--(int);
   // ...
};

Ниже приведена возможная реализация постфиксных операторов:

Screen& ScreenPtr::operator++(int)
{
   if ( size == 0 ) {
      cerr << "не могу инкрементировать указатель для одного объекта\n";
      return *ptr;
   }
   if ( offset == size ) {
      cerr << "уже на один элемент дальше конца массива\n";
      return *ptr;
   }

   ++offset;
   return *ptr++;
}

Screen& ScreenPtr::operator--(int)
{
   if ( size == 0 ) {
      cerr << "не могу декрементировать указатель для одного объекта\n";
      return *ptr;
   }
   if ( offset == -1 ) {
      cerr <<"уже на один элемент раньше начала массива\n";
      return *ptr;
   }

   --offset;
   return *ptr--;
}

Обратите внимание, что давать название второму параметру нет необходимости, поскольку внутри определения оператора он не употребляется. Компилятор сам подставляет для него значение по умолчанию, которое можно игнорировать. Вот пример использования постфиксного оператора:

const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
ScreenPtr parr( *parray, arrSize );
for ( int ix = 0; ix < arrSize; ++ix)
    printScreen( parr++ );

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

parr.operator++(1024);   // вызов постфиксного operator++

Перегруженные операторы инкремента и декремента разрешается объявлять как дружественные функции. Изменим соответствующим образом определение класса ScreenPtr:

class ScreenPtr {
   // объявления не членов
   friend Screen& operator++( Screen & );    // префиксные операторы
   friend Screen& operator--( Screen & );
   friend Screen& operator++( Screen &, int); // постфиксные операторы
   friend Screen& operator--( Screen &, int);
public:
   // определения членов
};

Упражнение 15.7

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

Упражнение 15.8

С помощью ScreenPtr можно представить указатель на массив объектов класса Screen. Модифицируйте перегруженные operator*() и operator >() (см. раздел 15.6) так, чтобы указатель ни при каком условии не адресовал элемент перед началом или за концом массива. Совет: в этих операторах следует воспользоваться новыми членами size и offset.
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1230 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp