Б. Страуструп - Язык программирования С++ (1119446), страница 54
Текст из файла (страница 54)
конструкцию выражение(список-выражений), можно рассматривать как бинарнуюоперацию, в которой выражение является левым операндом, а список-выражений - правым. Операциювызова можно перегружать как и другие операции. В функции 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;// массив assocint 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() активноиспользуется для выделения подстрок и индексации многомерных массивов.194Бьерн Страуструп.Язык программирования С++Функция operator() должна быть функцией-членом.7.9 Косвенное обращениеОперацию косвенного обращения к члену -> можно определить как унарную постфиксную операцию.Это значит, если есть классclass Ptr {// ...X* operator->();};объекты класса Ptr могут использоваться для доступа к членам класса X также, как для этой целииспользуются указатели:void f(Ptr p){p->m = 7;}// (p.operator->())->m = 7Превращение объекта p в указатель p.operator->() никак не зависит от члена m, на который онуказывает.
Именно по этой причине operator->() является унарной постфиксной операцией. Однако, мыне вводим новых синтаксических обозначений, так что имя члена по-прежнему должно идти после -> :void g(Ptr p){X* q1 = p->;X* q2 = p.operator->();}// синтаксическая ошибка// нормальноПерегрузка операции -> прежде всего используется для создания "хитрых указателей", т.е. объектов,которые помимо использования как указатели позволяют проводить некоторые операции при каждомобращении к указуемому объекту с их помощью.
Например, можно определить класс RecPtr дляорганизации доступа к объектам класса Rec, хранимым на диске. Параметром конструктора RecPtrявляется имя, которое будет использоваться для поиска объекта на диске. При обращении к объекту спомощью функции RecPtr::operator->() он переписывается в основную память, а в конце работыдеструктор RecPtr записывает измененный объект обратно на диск.class RecPtr {Rec* in_core_address;const char* identifier;// ...public:RecPtr(const char* p): identifier(p) { in_core_address = 0; }~RecPtr(){ write_to_disc(in_core_address,identifier); }Rec* operator->();};Rec* RecPtr::operator->(){if (in_core_address == 0)in_core_address = read_from_disc(identifier);return in_core_address;}Использовать это можно так:main(int argc, const char* argv){for (int i = argc; i; i--) {RecPtr p(argv[i]);195Бьерн Страуструп.Язык программирования С++p->update();}}На самом деле, тип RecPtr должен определяться как шаблон типа (см.
$$8), а тип структуры Recordбудет его параметром. Кроме того, настоящая программа будет содержать обработку ошибок ивзаимодействие с диском будет организовано не столь примитивно.Для обычных указателей операция -> эквивалентна операциям, использующим * и []. Так, если описаноY* p;то выполняется соотношениеp->m == (*p).m == p[0].mКак всегда, для определенных пользователем операций такие соотношения не гарантируются. Там, гдевсе-таки такая эквивалентность требуется, ее можно обеспечить:class X {Y* p;public:Y* operator->() { return p; }Y& operator*() { return *p; }Y& operator[](int i) { return p[i]; }};Если в вашем классе определено более одной подобной операции, разумно будет обеспечитьэквивалентность, точно так же, как разумно предусмотреть для простой переменной x некоторогокласса, в котором есть операции ++, += = и +, чтобы операции ++x и x+=1 были эквивалентны x=x+1.Перегрузка -> как и перегрузка [] может играть важную роль для целого класса настоящих программ, ане является просто экспериментом ради любопытства.
Дело в том, что в программировании понятиекосвенности является ключевым, а перегрузка -> дает ясный, прямой эффективный способпредставления этого понятия в программе. Есть другая точка зрения на операцию ->, как на средствозадать в С++ ограниченный, но полезный вариант понятия делегирования (см. $$12.2.8 и 13.9).7.10 Инкремент и декрементЕсли мы додумались до "хитрых указателей", то логично попробовать переопределить операцииинкремента ++ и декремента -- , чтобы получить для классов те возможности, которые эти операциидают для встроенных типов. Такая задача особенно естественна и необходима, если ставится цельзаменить тип обычных указателей на тип "хитрых указателей", для которого семантика остаетсяпрежней, но появляются некоторые действия динамического контроля.
Пусть есть программа сраспространенной ошибкой:void f1(T a) // традиционное использование{T v[200];T* p = &v[10];p--;*p = a;// Приехали: `p' настроен вне массива,// и это не обнаружено++p;*p = a;// нормально}Естественно желание заменить указатель p на объект класса CheckedPtrToT, по которому косвенноеобращение возможно только при условии, что он действительно указывает на объект. Применятьинкремент и декремент к такому указателю будет можно только в том случае, что указатель настроен наобъект в границах массива и в результате этих операций получится объект в границах того же массива:class CheckedPtrToT {// ...196Бьерн Страуструп.Язык программирования С++};void f2(T a) // вариант с контролем{T v[200];CheckedPtrToT p(&v[0],v,200);p--;*p = a;++p;*p = a;// динамическая ошибка:// `p' вышел за границы массива// нормально}Инкремент и декремент являются единственными операциями в С++, которые можно использовать какпостфиксные и префиксные операции.
Следовательно, в определении класса CheckedPtrToT мыдолжны предусмотреть отдельные функции для префиксных и постфиксных операций инкремента идекремента:class CheckedPtrToT {T* p;T* array;int size;public:////CheckedPtrToT(T* p, T* a, int s);////CheckedPtrToT(T* p);T* operator++();//T* operator++(int);//T* operator--();//T* operator--(int);//T& operator*();//начальное значение `p'связываем с массивом `a' размера `s'начальное значение `p'связываем с одиночным объектомпрефикснаяпостфикснаяпрефикснаяпостфикснаяпрефиксная};Параметр типа int служит указанием, что функция будет вызываться для постфиксной операции.
Насамом деле этот параметр является искусственным и никогда не используется, а служит только дляразличия постфиксной и префиксной операции. Чтобы запомнить, какая версия функции operator++используется как префиксная операция, достаточно помнить, что префиксной является версия безискусственного параметра, что верно и для всех других унарных арифметических и логическихопераций.
Искусственный параметр используется только для "особых" постфиксных операций ++ и --.С помощью класса CheckedPtrToT пример можно записать так:void f3(T a) // вариант с контролем{T v[200];CheckedPtrToT p(&v[0],v,200);p.operator--(1);p.operator*() = a;p.operator++();p.operator*() = a;// динамическая ошибка:// `p' вышел за границы массива// нормально}В упражнении $$7.14 [19] предлагается завершить определение класса CheckedPtrToT, а другимупражнением ($$9.10[2]) является преобразование его в шаблон типа, в котором для сообщений одинамических ошибках используются особые ситуации.