straustrup2 (852740), страница 52
Текст из файла (страница 52)
Решитьвозникающую здесь проблему предлагается в упражнении $$7.14 [20]. Дальнейшее развитие понятиеассоциативного массива получит в $$8.8.Функция operator[]() должна быть членом класса. Отсюда следует, что эквивалентность x[y] == y[x]может не выполняться, если x объект класса. Обычные отношения эквивалентности, справедливые дляопераций со встроенными типами, могут не выполняться для пользовательских типов ($$7.2.2, см.также $$7.9).193Бьерн Страуструп.Язык программирования С++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;// массив 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]) является преобразование его в шаблон типа, в котором для сообщений одинамических ошибках используются особые ситуации.
Примеры использования операций ++ и -- дляитераций можно найти в $$8.8.197Бьерн Страуструп.Язык программирования С++7.11 Строковый классТеперь можно привести более осмысленный вариант класса string. В нем подсчитывается число ссылокна строку, чтобы минимизировать копирование, и используются как константы стандартные строки C++.#include <iostream.h>#include <string.h>class string {struct srep {char* s;// указатель на строкуint n;// счетчик числа ссылокsrep() { n = 1; }};srep *p;public:string(const char *);// string x = "abc"string();// string x;string(const string &);// string x = string ...string& operator=(const char *);string& operator=(const string &);~string();char& operator[](int i);friend ostream& operator<<(ostream&, const string&);friend istream& operator>>(istream&, string&);friend int operator==(const string &x, const char *s){ return strcmp(x.p->s,s) == 0; }friend int operator==(const string &x, const string &y){ return strcmp(x.p->s,y.p->s) == 0; }friend int operator!=(const string &x, const char *s){ return strcmp(x.p->s,s) != 0; }friend int operator!=(const string &x, const string &y){ return strcmp(x.p->s,y.p->s) != 0; }};Конструкторы и деструкторы тривиальны:string::string(){p = new srep;p->s = 0;}string::string(const string& x){x.p->n++;p = x.p;}string::string(const char* s){p = new srep;p->s = new char[ strlen(s)+1 ];strcpy(p->s, s);}string::~string(){if (--p->n == 0) {delete[] p->s;delete p;}198Бьерн Страуструп.Язык программирования С++}Как и всегда операции присваивания похожи на конструкторы.
В них нужно позаботиться об удалениипервого операнда, задающего левую часть присваивания:string& string::operator=(const char* s){if (p->n > 1) {// отсоединяемся от старой строкиp->n--;p = new srep;}else// освобождаем строку со старым значениемdelete[] p->s;p->s = new char[ strlen(s)+1 ];strcpy(p->s, s);return *this;}string& string::operator=(const string& x){x.p->n++;// защита от случая ``st = st''if (--p->n == 0) {delete[] p->s;delete p}p = x.p;return *this;}Операция вывода показывает как используется счетчик числа ссылок.