assoc. Итератору нужен доступ к даным, которые хранятся в assoc, поэтому он сделан другом:
class assoc (* friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int amp; operator[] (char*); *);
Итератор определяется как
class assoc_iterator(* assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc amp; s) (* cs = amp;s; i = 0; *) pair* operator()() (* return (i«cs-»free)? amp;cs-»vec[i++] : 0; *) *);
Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать опрацией (). По достижении конца массива он возвращает 0:
main() // считает вхождения каждого слова во вводе (* const MAX = 256; // больше самого большого слова char buf[MAX]; assoc vec(512); while (cin»»buf) vec[buf]++; assoc_iterator next(vec); pair* p; while ( p = next() ) cout «„ p-“name „„ ': ' «« p-“val «« « “; *)
0 Итераторный тип вроде этого имеет преимущество перед нбором функций, которые выполняют ту же работу: у него есть собственные закрытые данные для хранения хода итерации. К тму же обычно существенно, чтобы одновременно могли работать много итераторов этого типа.
Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).
6.9 Класс String
Вот довольно реалистичный пример класса строк string. В нем производится учет ссылок на строку с целью минимизировать копирование и в качестве констант применяются стандартные символьные строки С++.
#include «stream.h» #include «string.h»
class string (* struct srep (* char* s; // указатель на данные int n; // счетчик ссылок *); srep *p;
public: string(char *); // string x = «abc» string(); // string x; string(string amp;); // string x = string ... string amp; operator=(char *); string amp; operator=(string amp;); ~string(); char amp; operator[](int i);
friend ostream amp; operator«„(ostream amp;, string amp;); friend istream amp; operator“»(istream amp;, string amp;);
friend int operator==(string amp; x, char* s) (*return strcmp(x.p-»s, s) == 0; *)
friend int operator==(string amp; x, string amp; y) (*return strcmp(x.p-»s, y.p-»s) == 0; *)
friend int operator!=(string amp; x, char* s) (*return strcmp(x.p-»s, s) != 0; *)
friend int operator!=(string amp; x, string amp; y) (*return strcmp(x.p-»s, y.p-»s) != 0; *)
*);
Конструкторы и деструкторы просты (как обычно):
string::string() (* p = new srep; p-»s = 0; p-»n = 1; *)
string::string(char* s) (* p = new srep; p-»s = new char[ strlen(s)+1 ]; strcpy(p-»s, s); p-»n = 1; *)
string::string(string amp; x) (* x.p-»n++; p = x.p; *)
string::~string() (* if (–p-»n == 0) (* delete p-»s; delete p; *) *)
Как обычно, операции присваивания очень похожи на контрукторы. Они должны обрабатывать очистку своего первого (лвого) операнда:
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 «« «] “; *)
Операция ввода использует стандартную функцию ввода сивольной строки (#8.4.1).
istream amp; operator»»(istream amp; s, string amp; x) (* char buf[256]; s »» buf; x = buf; cout «„ 'echo: ' «« x «« « “; return s; *)
Для доступа к отдельным символам предоставлена операция индексирования. Осуществляется проверка индекса:
void error(char* p) (* cerr «„ p «« « “; 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 «„ „отсюда начнем “; for (n = 0; cin“»x[n]; n++) (* string y; if (n==100) error(«слишком много строк»); cout «„ (y = x[n]); if (y=='done') break; *) cout «« «отсюда мы пройдем обратно “;
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. Если только не применяется перегрузка, имена членов
обычно короче имен друзей.