Изменить стиль страницы

string amp; string::operator=(char* s) (* if (p-»n » 1) (* // разъединить себя p-»n–; p = new srep; *) else if (p-»n == 1) delete p-»s;

p-»s = new char[ strlen(s)+1 ]; strcpy(p-»s, s); p-»n = 1; return *this; *)

Благоразумно обеспечить, чтобы присваивание объекта смому себе работало правильно:

string amp; string::operator=(string amp; x) (* x.p-»n++; if (–p-»n == 0) (* delete p-»s; delete p; *) p = x.p; return *this; *)

Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции ««, которая определяется позднее):

ostream amp; operator«„(ostream amp; s, string amp; x) (* return s „„ x.p-“s „« « [“ «« x.p-“n «« «]\n“; *)

Операция ввода использует стандартную функцию ввода сивольной строки (#8.4.1).

istream amp; operator»»(istream amp; s, string amp; x) (* char buf[256]; s »» buf; x = buf; cout «„ "echo: " «« x «« «\n“; return s; *)

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

void error(char* p) (* cerr «„ p «« «\n“; exit(1); *)

char amp; string::operator[](int i) (* if (i«0 !! strlen(p-»s)«i) error(„индекс за границами“); return p-»s[i]; *)

Головная программа просто немного опробует действия над строками. Она читает слова со ввода в строки, а потом эти строки печатает. Она продолжает это делать до тех пор, пока не распознает строку done, которая завершает сохранение слов в строках, или пока не встретит конец файла. После этого она печатает строки в обратном порядке и завершается.

main() (* string x[100]; int n;

cout «„ „отсюда начнем\n“; for (n = 0; cin“»x[n]; n++) (* string y; if (n==100) error(«слишком много строк»); cout «„ (y = x[n]); if (y=="done") break; *) cout «« «отсюда мы пройдем обратно\n“;

for (int i=n-1; 0«=i; i–) cout «« x[i]; *)

6.10 Друзья и члены

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

Рассмотрим простой класс X:

class X (* // ... X(int); int m(); friend int f(X amp;); *);

Внешне не видно никаких причин делать f(X amp;) другом дполнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для «настоящего объекта», в то время как друг f() мжет вызываться для объекта, созданного с помощью неявного преобразования типа. Например:

void g() (* 1.m(); // ошибка f(1); // f(x(1)); *)

Поэтому операция, изменяющая состояние объекта, должна быть членом, а не другом. Для определяемых пользователем тпов операции, требующие в случае фундаментальных типов опранд lvalue (=, *=, ++, *= и т.д.), наиболее естественно оределяются как члены.

И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, !! и т.д.).

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

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

6.11 Предостережение

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

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

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

6.12 Упражнения

1. (*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию «добавить в конец» +=. Какие еще операции над string вы хотели бы иметь возможность осуществлять?

2. (*1.5) Задайте с помощью перегрузки () операцию выделния подстроки для класса строк.

3. (*3) Постройте класс string так, чтобы операция выделния подстроки могла использоваться в левой части присвивания. Напишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут быть разными.

4. (*2) Постройте класс string так, чтобы для присваивания, передачи параметров и т.п. он имел семантику по значнию, то есть, когда копируется строковое представление, а не просто управляющая структура данных класса sring.

5. (*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходимо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не бдет изменена. Не пытайтесь одновременно с этим иметь операцию выделения подстроки, которая может использваться в левой части.

6. (*4) Разработайте класс string с семантикой по значению, копированием с задержкой и операцией подстроки, которая может стоять в левой части.

7. (*2) Какие преобразования используются в каждом выражнии следующей программы:

struct X (* int i; X(int); operator+(int); *);

struct Y (* int i; Y(X); operator+(X); operator int(); *);

X operator* (X,Y); int f(X);

X x = 1; Y y = x; int i = 2;

main() (* i + 10; y + 10; y + 10 * y; x + y + i; x * x + i; f(7); f(y); y + y; 106 + y; *)

Определите X и Y так, чтобы они оба были целыми типами. Измените программу так, чтобы она работала и печатала значения всех допустимых выражений.

8. (*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().

9. (*1) Определите класс RINT, который ведет себя в точноти как int за исключением того, что единственные возмоные операции – это + (унарный и бинарный), – (унарный и бинарный), *, /, %. Подсказка: не определяйте INT::operator int().

10. (*3) Определите класс LINT, ведущий себя как RINT, за исключением того, что имеет точность не менее 64 бит.

11. (*4) Определите класс, который реализует арифметику с произвольной точностью. Подсказка: вам надо управлять памятью аналогично тому, как это делалось для класса string.