Перегрузка операций
7.10 Инкремент и декремент
Если мы додумались до "хитрых указателей", то логично попробовать переопределить операции инкремента ++ и декремента --, чтобы получить для классов те возможности, которые эти операции дают для встроенных типов. Такая задача особенно естественна и необходима, если ставится цель заменить тип обычных указателей на тип "хитрых указателей", для которого семантика остается прежней, но появляются некоторые действия динамического контроля. Пусть есть программа с распространенной ошибкой:
void f1(T a) // традиционное использование { T v[200]; T* p = &v[0]; p--; *p = a; // Приехали: 'p' настроен вне массива, // и это не обнаружено ++p; *p = a; // нормально }
Естественно желание заменить указатель p на объект класса CheckedPtrToT, по которому косвенное обращение возможно только при условии, что он действительно указывает на объект. Применять инкремент и декремент к такому указателю будет можно только в том случае, что указатель настроен на объект в границах массива и в результате этих операций получится объект в границах того же массива:
class CheckedPtrToT { // ... }; void f2(T a) // вариант с контролем { T v[200]; CheckedPtrToT p(&v[0],v,200); p--; *p = a; // динамическая ошибка: // `p' вышел за границы массива ++p; *p = a; // нормально }
Инкремент и декремент являются единственными операциями в С++, которые можно использовать как постфиксные и префиксные операции. Следовательно, в определении класса CheckedPtrToT мы должны предусмотреть отдельные функции для префиксных и постфиксных операций инкремента и декремента:
class CheckedPtrToT { T* p; T* array; int size; public: // начальное значение `p' // связываем с массивом `a' размера `s' CheckedPtrToT(T* p, T* a, int s); // начальное значение `p' // связываем с одиночным объектом CheckedPtrToT(T* p); T* operator++(); // префиксная T* operator++(int); // постфиксная T* operator--(); // префиксная T* operator--(int); // постфиксная T& operator*(); // префиксная };
Параметр типа int служит указанием, что функция будет вызываться для постфиксной операции. На самом деле этот параметр является искусственным и никогда не используется, а служит только для различия постфиксной и префиксной операции. Чтобы запомнить, какая версия функции operator++ используется как префиксная операция, достаточно помнить, что префиксной является версия без искусственного параметра, что верно и для всех других унарных арифметических и логических операций. Искусственный параметр используется только для "особых" постфиксных операций ++ и --.
С помощью класса CheckedPtrToT пример можно записать так:
void f3(T a) // вариант с контролем { T v[200]; CheckedPtrToT p(&v[0],v,200); p.operator--(1); p.operator*() = a; // динамическая ошибка: // `p' вышел за границы массива p.operator++(); p.operator*() = a; // нормально }
В упражнении 7.14 [19] предлагается завершить определение класса CheckedPtrToT, а другим упражнением ( 9.10[2]) является преобразование его в шаблон типа, в котором для сообщений о динамических ошибках используются особые ситуации. Примеры использования операций ++ и -- для итераций можно найти в 8.8.
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; } }
Как и всегда операции присваивания похожи на конструкторы. В них нужно позаботиться об удалении первого операнда, задающего левую часть присваивания:
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; }
Операция вывода показывает как используется счетчик числа ссылок. Она сопровождает как эхо каждую введенную строку (ввод происходит с помощью операции <<, приведенной ниже):
ostream& operator<<(ostream& s, const string& x) { return s << x.p->s << " [" << x.p->n << "]\n"; }
Операция ввода происходит с помощью стандартной функции ввода символьной строки ( 10.3.1):
istream& operator>>(istream& s, string& x) { char buf[256]; s >> buf; // ненадежно: возможно переполнение buf // правильное решение см. в 10.3.1 x = buf; cout << "echo: " << x << '\n'; return s; }
Операция индексации нужна для доступа к отдельным символам. Индекс контролируется:
void error(const char* p) { cerr << p << '\n'; exit(1); } char& string::operator[](int i) { if (i<0 || strlen(p->s)<i) error("недопустимое значение индекса"); return p->s[i]; }
В основной программе просто даны несколько примеров применения строковых операций. Слова из входного потока читаются в строки, а затем строки печатаются. Это продолжается до тех пор, пока не будет обнаружена строка done, или закончатся строки для записи слов, или закончится входной поток. Затем печатаются все строки в обратном порядке и программа завершается.
int main() { string x[100]; int n; cout << " здесь начало \n"; for ( n = 0; cin>>x[n]; n++) { if (n==100) { error("слишком много слов"); return 99; } string y; cout << (y = x[n]); if (y == "done") break; } cout << "теперь мы идем по словам в обратном порядке \n"; for (int i=n-1; 0<=i; i--) cout << x[i]; return 0; }