Перегрузка операций
7.7 Индексация
Операторная функция operator[] задает для объектов классов интерпретацию индексации. Второй параметр этой функций (индекс) может иметь произвольный тип. Это позволяет, например, определять ассоциативные массивы. В качестве примера можно переписать определение из 2.3.10, где ассоциативный массив использовался в небольшой программе, подсчитывающей число вхождений слов в файле.
Там для этого использовалась функция. Мы определим настоящий тип ассоциативного массива:
class assoc { struct pair { char* name; int val; }; pair* vec; int max; int free; assoc(const assoc&); // предотвращает копирование assoc& operator=(const assoc&); // предотвращает копирование public: assoc(int); int& operator[](const char*); void print_all(); };
В объекте assoc хранится вектор из структур pair размером max.
В переменной free хранится индекс первого свободного элемента вектора.
Чтобы предотвратить копирование объектов assoc, конструктор копирования и операция присваивания описаны как частные. Конструктор выглядит так:
assoc::assoc(int s) { max = (s<16) ? 16 : s; free = 0; vec = new pair[max]; }
В реализации используется все тот же неэффективный алгоритм поиска, что и в 2.3.10. Но теперь, если вектор переполняется, объект assoc увеличивается:
#include <string.h> int& assoc::operator[](const char* p) /* работает с множеством пар (структур pair): проводит поиск p, возвращает ссылку на целое значение из найденной пары, создает новую пару, если p не найдено */ { register pair* pp; for (pp=&vec[free-1]; vec<=pp; pp-- ) if (strcmp(p,pp->name) == 0) return pp->val; if (free == max) { //переполнение: вектор увеличивается pair* nvec = new pair[max*2]; for (int i=0; i<max; i++) nvec[i] = vec[i]; delete vec; vec = nvec; max = 2*max; } pp = &vec[free++]; pp->name = new char[strlen(p)+1]; strcpy(pp->name,p); pp->val = 0; // начальное значение = 0 return pp->val; }
Поскольку представление объекта assoc скрыто от пользователя, нужно иметь возможность напечатать его каким-то образом. В следующем разделе будет показано как определить настоящий итератор для такого объекта.
Здесь же мы ограничимся простой функцией печати:
void assoc::print_all() { for (int i = 0; i<free; i++) cout << vec[i].name << ": " << vec[i].val << '\n'; }
Наконец, можно написать тривиальную программу:
main() // подсчет числа вхождений во входной // поток каждого слова { const MAX = 256; // больше длины самого длинного слова char buf[MAX]; assoc vec(512); while (cin>>buf) vec[buf]++; vec.print_all(); }
Опытные программисты могут заметить, что второй комментарий можно легко опровергнуть. Решить возникающую здесь проблему предлагается в упражнении 7.14 [20]. Дальнейшее развитие понятие ассоциативного массива получит в 8.8.
Функция operator[]() должна быть членом класса. Отсюда следует, что эквивалентность x[y] == y[x] может не выполняться, если x объект класса. Обычные отношения эквивалентности, справедливые для операций со встроенными типами, могут не выполняться для пользовательских типов ( 7.2.2, см. также 7.9).
7.8 Вызов функции
Вызов функции, т.е. конструкцию выражение(список-выражений), можно рассматривать как бинарную операцию, в которой выражение является левым операндом, а список-выражений - правым. Операцию вызова можно перегружать как и другие операции. В функции operator ()() список фактических параметров вычисляется и проверяется по типам согласно обычным правилам передачи параметров. Перегрузка операции вызова имеет смысл прежде всего для типов, с которыми возможна только одна операция, а также для тех типов, одна из операций над которыми имеет настолько важное значение, что все остальные в большинстве случаев можно не учитывать.
Мы не дали определения итератора для ассоциативного массива типа assoc. Для этой цели можно определить специальный класс assoc_iterator, задача которого выдавать элементы из assoc в некотором порядке. В итераторе необходимо иметь доступ к данным, хранимым в assoc, поэтому он должен быть описан как friend:
class assoc { friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int& operator[](const char*); };
Итератор можно определить так:
class assoc_iterator { const assoc* cs; // массив assoc int i; // текущий индекс public: assoc_iterator(const assoc& s) { cs = &s; i = 0; } pair* operator()() { return (i<cs->free)? &cs->vec[i++] : 0; } };
Массив assoc объекта assoc_iterator нужно инициализировать, и при каждом обращении к нему с помощью операторной функции () будет возвращаться указатель на новую пару (структура 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(vec) ) cout << p->name << ": " << p->val << '\n'; }
Итератор подобного вида имеет преимущество перед набором функций, решающим ту же задачу: итератор может иметь собственные частные данные, в которых можно хранить информацию о ходе итерации. Обычно важно и то, что можно одновременно запустить сразу несколько итераторов одного типа.
Конечно, использование объектов для представления итераторов непосредственно никак не связано с перегрузкой операций. Одни предпочитают использовать тип итератора с такими операциями, как first(), next() и last(), другим больше нравится перегрузка операции ++, которая позволяет получить итератор, используемый как указатель (см. 8.8). Кроме того, операторная функция operator() активно используется для выделения подстрок и индексации многомерных массивов.