Здесь водятся Драконы! - старинная карта
class complex { double re, im; public: complex(double r, double i) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator*(complex, complex); };
void f() { complex a = complex(1, 3.1); complex b = complex(1.2, 2); complex c = b; a = b+c; b = b+c*a; c = a*b+complex(1,2); }
Выполняются обычные правила
приоритетов, поэтому второй
оператор означает b=b+(c*a), а не b=(b+c)*a.
6.2.1 Бинарные и Унарные Операции | |
6.2.2 Предопределенные Значения Операций | |
6.2.3 Операции и Определяемые Пользователем Типы |
Можно описывать функции, определяющие значения следующих операций:
+ - * / % ^ & | ~ ! = <> += -= *= /= %= ^= &= |= <<>> >>= <<= ="=" !="<=">= && || ++ -- [] () new delete
Последние четыре - это
индексирование (#6.7),
вызов функции (#6.8),
выделение свободной памяти и
освобождение свободной памяти (#3.2.6). Изменить
приоритеты перечисленных операций
невозможно, как невозможно
изменить и синтаксис выражений.
Нельзя, например, определить
унарную операцию % или бинарную !.
Невозможно определить новые
лексические символы операций, но в
тех случаях, когда множество
операций недостаточно, вы можете
использовать запись вызова
функции. Используйте например, не **,
а pow(). Эти ограничения могут
показаться драконовскими, но более
гибкие правила могут очень легко
привести к неоднозначностям.
Например, на первый взгляд
определение операции **, означающей
возведение в степень, может
показаться очевидной и простой
задачей, но подумайте еще раз.
Должна ли ** связываться влево (как в
Фортране) или вправо (как в Алголе)?
Выражение a**p должно
интерпретироваться как a*(*p) или как
(a)**(p)?
Имя функции операции есть ключевое
слово operator (то есть, операция), за
которым следует сама операция,
например, operator<<. Функция операция описывается и может вызываться так же, как любая другая функция. Использование операции это лишь сокращенная запись явного вызова функции операции. Например:
void f(complex a, complex b) { complex c = a + b; // сокращенная запись complex d = operator+(a,b); // явный вызов }
При наличии предыдущего описания complex оба инициализатора являются синонимами.6.2.1 Бинарные и Унарные Операции
class X { // друзья friend X operator-(X); // унарный минус friend X operator-(X,X); // бинарный минус friend X operator-(); // ошибка: нет операндов friend X operator-(X,X,X); // ошибка: тернарная // члены (с неявным первым параметром: this) X* operator&(); // унарное & (взятие адреса) X operator&(X); // бинарное & (операция И) X operator&(X,X); // ошибка: тернарное };
Когда операции ++ и -- перегружены, префиксное использование и постфиксное различить невозможно.6.2.2 Предопределенные Значения Операций
Относительно смысла операций, определяемых пользователем, не делается никаких предположений. В частности, поскольку не предполагается, что перегруженное = реализует присваивание ее первому операнду, не делается никакой проверки, чтобы удостовериться, является ли этот операнд lvalue (#с.6).
Значения некоторых встроенный операций определены как равносильные определенным комбинациям других операций над теми же аргументами. Например, если a является int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения для определенных пользователем операций не выполняются, если только не случилось так, что пользователь сам определил их таким образом. Например, определение operator+=() для типа complex не может быть выведено из определений complex::operator+() и complex::operator=().
По историческому совпадению операции = и & имеют предопределенный смысл для объектов классов. Никакого элегантного способа "не определить" эти две операции не существует. Их можно, однако, сделать недееспособными для класса X. Можно, например, описать X::operator&(), не задав ее определения. Если где-либо будет браться адрес объекта класса X, то компоновщик обнаружит отсутствие определения*1. Или, другой способ, можно определить X::operator&() так, чтобы вызывала ошибку во время выполнения.6.2.3 Операции и Определяемые Пользователем Типы
Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (функциям, которые переопределяют операции new и delete, это делать необязательно). Это правило гарантирует, что пользователь не может изменить смысл никакого выражения, не включающего в себя определенного пользователем типа. В частности, невозможно определить функцию, которая действует исключительно на указатели.
Функция операция, первым параметром которой предполагается основной тип, не может быть функцией членом. Рассмотрим, например, сложение комплексной переменной aa с целым 2: aa+2, при подходящим образом описанной функции члене, может быть проинтерпретировано как aa.operator+(2), но с 2+aa это не может быть сделано, потому что нет такого класса int, для которого можно было бы определить + так, чтобы это означало 2.operator+(aa). Даже если бы такой тип был, то для того, чтобы обработать и 2+aa и aa+2, понадобилось бы две различных функции члена. Так как компилятор не знает смысла +, определенного пользователем, то не может предполагать, что он коммутативен, и интерпретировать 2+aa как aa+2. С этим примером могут легко справиться функции друзья.
Все функции операции по определению перегружены. Функция операция задает новый смысл операции в дополнение к встроенному определению, и может существовать несколько функций операций с одним и тем же именем, если в типах их параметров имеются отличия, различимые для компилятора, чтобы он мог различать их при обращении (см. #4.6.7).6.3 Определяемое Преобразование Типа
6.3.1 Конструкторы | |
6.3.2 Операции Преобразования | |
6.3.3 Неоднозначности |
Приведенная во введении реализация комплексных чисел слишком ограничена, чтобы она могла устроить кого-либо, поэтому ее нужно расширить. Это будет в основном повторением описанных выше методов. Например:
class complex { double re, im; public: complex(double r, double i) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator+(complex, double); friend complex operator+(double, complex); friend complex operator-(complex, complex); friend complex operator-(complex, double); friend complex operator-(double, complex); complex operator-() // унарный - friend complex operator*(complex, complex); friend complex operator*(complex, double); friend complex operator*(double, complex); // ... };
Теперь, имея описание complex, мы можем
написать:
void f() { complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5); a = -b-c; b = c*2.0*c; c = (d+e)*a; }
Но писать функцию для каждого
сочетания complex и double, как это
делалось выше для operator+(),
невыносимо нудно. Кроме того,
близкие к реальности средства
комплексной арифметики должны
предоставлять по меньшей мере
дюжину таких функций; посмотрите,
например, на тип complex.
class complex { // ... complex(double r) { re=r; im=0; } };
Конструктор, требующий только один
параметр, необязательно вызывать
явно:
complex z1 = complex(23); complex z2 = 23;
class complex { double re, im; public: complex(double r, double i = 0) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator*(complex, complex); };
a=operator*( b, complex( double(2), double(0) ) )
class tiny { char v; int assign(int i) { return v = (i&~63) ? (error("ошибка диапазона"),0) : i; } public: tiny(int i) { assign(i); } tiny(tiny& i) { v = t.v; } int operator=(tiny& i) { return v = t.v; } int operator=(int i) { return assign(i); } operator int() { return v; } }
void main() { tiny c1 = 2; tiny c2 = 62; tiny c3 = c2 - c1; // c3 = 60 tiny c4 = c3; // нет проверки диапазона (необязательна) int i = c1 + c2; // i = 64 c1 = c2 + 2 * c1; // ошибка диапазона: c1 = 0 (а не 66) c2 = c1 -i; // ошибка диапазона: c2 = 0 c3 = c2; // нет проверки диапазона (необязательна) }
Тип вектор из tiny может оказаться
более полезным, поскольку он
экономит пространство. Чтобы
сделать этот тип более удобным в
обращении, можно использовать
операцию индексирования.
Другое применение определяемых
операций преобразования - это типы,
которые предоставляют
нестандартные представления чисел
(арифметика по основанию 100,
арифметика с фиксированной точкой,
двоично-десятичное представление и
т.п.). При этом обычно
переопределяются такие операции,
как + и *.
Функции преобразования
оказываются особенно полезными для
работы со структурами данных, когда
чтение (реализованное посредством
операции преобразования)
тривиально, в то время как
присваивание и инициализация
заметно более сложны.
Типы istream и ostream опираются на
функцию преобразования, чтобы
сделать возможными такие
операторы, как while (cin>>x) cout<>x выше
возвращает istream&. Это значение
неявно преобразуется к значению,
которое указывает состояние cin, а
уже это значение может проверяться
оператором while (см. #8.4.2).
Однако определять преобразование
из оного типа в другой так, что при
этом теряется информация, обычно не
стоит.
class x { /* ... */ x(int); x(char*); }; class y { /* ... */ y(int); }; class z { /* ... */ z(x); }; overload f; x f(x); y f(y); z g(z); f(1); // недопустимо: неоднозначность f(x(1)) или f(y(1)) f(x(1)); f(y(1)); g("asdf"); // недопустимо: g(z(x("asdf"))) не пробуется g(z("asdf"));
class x { /* ... */ x(int); } overload h(double), h(x); h(1);
Вызов мог бы быть
проинтерпретирован или как h(double(1)),
или как h(x(1)), и был бы недопустим по
правилу единственности. Но первая
интерпретация использует только
стандартное преобразование и она
будет выбрана по правилам,
приведенным в #4.6.7.
Правила преобразования не являются
ни самыми простыми для реализации и
документации, ни наиболее общими из
тех, которые можно было бы
разработать. Возьмем требование
единственности преобразования.
Более общий подход разрешил бы
компилятору применять любое
преобразование, которое он сможет
найти; таким образом, не нужно было
бы рассматривать все возможные
преобразования перед тем, как
объявить выражение допустимым. К
сожалению, это означало бы, что
смысл программы зависит от того,
какое преобразование было найдено.
В результате смысл программы неким
образом зависел бы от порядка
описания преобразования. Поскольку
они часто находятся в разных
исходных файлах (написанных
разными людьми), смысл программы
будет зависеть от порядка
компоновки этих частей вместе. Есть
другой вариант - запретить все
неявные преобразования. Нет ничего
проще, но такое правило приведет
либо к неэлегантным
пользовательским интерфейсам, либо
к бурному росту перегруженных
функций, как это было в предыдущем
разделе с complex.
Самый общий подход учитывал бы всю
имеющуюся информацию о типах и
рассматривал бы все возможные
преобразования. Например, если
использовать предыдущее описание,
то можно было бы обработать aa=f(1),
так как тип aa определяет
единственность толкования. Если aa
является x, то единственное, дающее
в результате x, который требуется
присваиванием, - это f(x(1)), а если aa -
это y, то вместо этого будет
использоваться f(y(1)). Самый общий
подход справился бы и с g("asdf"),
поскольку единственной
интерпретацией этого может быть
g(z(x("asdf"))). Сложность этого
подхода в том, что он требует
расширенного анализа всего
выражения для того, чтобы
определить интерпретацию каждой
операции и вызова функции. Это
приведет к замедлению компиляции, а
также к вызывающим удивление
интерпретациям и сообщениям об
ошибках, если компилятор
рассмотрит преобразования,
определенные в библиотеках и т.п.
При таком подходе компилятор будет
принимать во внимание больше, чем,
как можно ожидать, знает пишущий
программу программист!
class matrix { double m[4][4]; public: matrix(); friend matrix operator+(matrix&, matrix&); friend matrix operator*(matrix&, matrix&); };
matrix operator+(matrix&, matrix&); { matrix sum; for (int i=0; i<4; i++) for (int j="0;" j<4; j++) sum.m[i][j]="arg1.m[i][j]" + arg2.m[i][j]; return sum; }Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказаться более эффективным:
class matrix { // ... friend matrix& operator+(matrix&, matrix&); friend matrix& operator*(matrix&, matrix&); };
Это является допустимым, но приводит к сложности с выделением памяти. Поскольку ссылка на результат будет передаваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто операция используется в выражении больше одного раза, результат не может быть и статической переменной. Как правило, его размещают в свободной памяти. Часто копирование возвращаемого значения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется.
Рассмотрим очень простой класс строк string:
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } };
Строка - это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:
void f() { string s1(10); string s2(20); s1 = s2; }
будет размещать два вектора символов, а присваивание s1=s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом определить присваивание объектов типа string:
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } void operator=(string&) }; void string::operator=(string& a) { if (this == &a) return; // остерегаться s=s; delete p; p=new char[size=a.size]; strcpy(p,a.p); }
Это определение string гарантирует, и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приведет к появлению той же проблемы в новом облике:
void f() { string s1(10); s2 = s1; }
Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определенная пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было неразумно так делать: указатель p будет содержать неопределенное и совершенно случайное значение. Часто операция присваивания полагается на то, что ее аргументы инициализированы. Для такой инициализации, как здесь, это не так по определению. Следовательно, нужно определить похожую, но другую, функцию, чтобы обрабатывать инициализацию:
struct string { char* p; int size; // размер вектора, на который указывает p string(int sz) { p = new char[size=sz]; } ~string() { delete p; } void operator=(string&) string(string&); }; void string::string(string& a) { p=new char[size=a.size]; strcpy(p,a.p); }
Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&). Нельзя не подчеркнуть еще раз, что присваивание и инициализация - разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:
class X { // ... X(something); // конструктор: создает объект X(&X); // конструктор: копирует в инициализации operator=(X&); // присваивание: чистит и копирует ~X(); // деструктор: чистит };
Есть еще два случая, когда объект копируется: как параметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого переменная - формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из функции, хотя это менее очевидно. В обоих случаях будет применен X(X&), если он определен:
string g(string arg) { return arg; } main() { string s = "asdf"; s = g(s); }
Ясно, что после вызова g() значение s обязано быть "asdf". Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&); на этот раз инициализируемой является временная переменная, которая затем присваивается s. Такие переменные, естественно, уничтожаются как положено с помощью string::~string() при первой возможности.
Чтобы задать смысл индексов для объектов класса используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перепишем пример из #2.3.10, где при написании небольшой программы для подсчета числа вхождений слов в файле применялся ассоциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:
struct pair { char* name; int val; }; class assoc { pair* vec; int max; int free; public: assoc(int); int& operator[](char*); void print_all(); };
В assoc хранится вектор пар pair длины max. Индекс первого неиспользованного элемента вектора находится в free. Конструктор выглядит так:
assoc::assoc(int s) { max = (s<16) ? s : 16; free="0;" vec="new" pair[max]; }При реализации применяется все тот же простой и неэффективный метод поиска, что использовался в #2.3.10. Однако при переполнении assoc увеличивается:
#include int assoc::operator[](char* p) /* работа с множеством пар "pair": поиск p, возврат ссылки на целую часть его "pair" делает новую "pair", если 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; iname = new char[strlen(p)+1]; strcpy(pp->name,p); pp->val = 0; // начальное значение: 0 return pp->val; }
Поскольку представление assoc скрыто, нам нужен способ его печати. В следующем разделе будет показано, как определить подходящий итератор, а здесь мы используем простую функцию печати:
vouid assoc::print_all() { for (int i = 0; i>buf) vec[buf]++; vec.print_all(); }
class assoc { friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int& operator[](char*); };
Итератор определяется как
class assoc_iterator{ assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc& s) { cs = &s i = 0; } pair* operator()() { return (ifree)? &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 << "\n"; }Итераторный тип вроде этого имеет преимущество перед набором функций, которые выполняют ту же работу: у него есть собственные закрытые данные для хранения хода итерации. К тому же обычно существенно, чтобы одновременно могли работать много итераторов этого типа. Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).
#include #include class string { struct srep { char* s; // указатель на данные int n; // счетчик ссылок }; srep *p; public: string(char *); // string x = "abc" string(); // string x; string(string &); // string x = string ... string& operator=(char *); string& operator=(string &); ~string(); char& operator[](int i); friend ostream& operator<<(ostream&, string&); friend istream& operator>>(istream&, string&); friend int operator==(string& x, char* s) {return strcmp(x.p->s, s) == 0; } friend int operator==(string& x, string& y) {return strcmp(x.p->s, y.p->s) == 0; } friend int operator!=(string& x, char* s) {return strcmp(x.p->s, s) != 0; } friend int operator!=(string& x, string& 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& x) { x.p->n++; p = x.p; } string::~string() { if (--p->n == 0) { delete p->s; delete p; } }
Как обычно, операции присваивания очень похожи на конструкторы. Они должны обрабатывать очистку своего первого (левого) операнда:
string& 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& string::operator=(string& x) { x.p->n++; if (--p->n == 0) { delete p->s; delete p; } p = x.p; return *this; }
Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции <<, которая определяется позднее):ostream& operator<<(ostream& s, string& x) { return s << x.p->s << " [" << x.p->n << "]\n"; }Операция ввода использует стандартную функцию ввода символьной строки (#8.4.1).
istream& operator>>(istream& s, string& x) { char buf[256]; s >> buf; x = buf; cout << "echo: " << x << "\n"; return s; }Для доступа к отдельным символам предоставлена операция индексирования. Осуществляется проверка индекса:
void error(char* p) { cerr << p << "\n"; exit(1); } char& string::operator[](int i) { if (i<0 || strlen(p->s)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]; }
Теперь, наконец, можно
обсудить, в каких случаях для
доступа к закрытой части
определяемого пользователем типа
использовать члены, а в каких -
друзей. Некоторые операции должны
быть членами: конструкторы,
деструкторы и виртуальные функции
(см. следующую главу),
но обычно это зависит от выбора.
Рассмотрим простой класс X:
class X { // ... X(int); int m(); friend int f(X&); };
Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:
void g() { 1.m(); // ошибка f(1); // f(x(1)); }
Поэтому операция, изменяющее состояние объекта, должно быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++ и т.д.), наиболее естественно определяются как члены. И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, || и т.д.). Если никакие преобразования типа не определены, то оказывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова другому. Например, оказывается, что большинство предпочитает для обращения матрицы m запись m.inv(). Конечно, если inv() действительно обращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть другом. При прочих равных условиях выбирайте, чтобы функция была членом: никто не знает, вдруг когда-нибудь кто-то определит операцию преобразования. Невозможно предсказать, потребуют ли будущие изменения изменить статус объекта. Синтаксис вызова функции члена ясно указывает пользователю, что объект можно изменить; ссылочный параметр является далеко не столь очевидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использовать неявный this. Если только не применяется перегрузка, имена членов обычно короче имен друзей.
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; }
[Назад] [Содержание] [Вперед]