Перегрузка операций
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;
}