Бьерн Страуструп (947334), страница 42
Текст из файла (страница 42)
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 {
// ...
};
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;
}
7.12 Друзья и члены
В заключении можно обсудить, когда при обращении в закрытую часть
пользовательского типа стоит использовать функции-члены, а когда
функции-друзья. Некоторые функции, например конструкторы, деструкторы
и виртуальные функции ($$R.12), обязаны быть членами, но для других
есть возможность выбора. Поскольку, описывая функцию как член, мы
не вводим нового глобального имени, при отсутствии других доводов
следует использовать функции-члены.
Рассмотрим простой класс X:
class X {
// ...
X(int);
int m1();
int m2() const;
friend int f1(X&);
friend int f2(const X&);
friend int f3(X);
};
Вначале укажем, что члены X::m1() и X::m2() можно вызывать только
для объектов класса X. Преобразование X(int) не будет применяться
к объекту, для которого вызваны X::m1() или X::m2():
void g()
{
1.m1(); // ошибка: X(1).m1() не используется
1.m2(); // ошибка: X(1).m2() не используется
}
Глобальная функция f1() имеет то же свойство ($$4.6.3), поскольку
ее параметр - ссылка без спецификации const. С функциями f2() и
f3() ситуация иная:
void h()
{
f1(1); // ошибка: f1(X(1)) не используется
f2(1); // нормально: f2(X(1));
f3(1); // нормально: f3(X(1));
}
Следовательно операция, изменяющая состояние объекта класса,
должна быть членом или глобальной функцией с параметром-ссылкой
без спецификации const. Операции над основными типами, которые
требуют в качестве операндов адреса (=, *, ++ и т.д.),
для пользовательских типов естественно определять как члены.
Обратно, если требуется неявное преобразование типа для всех
операндов некоторой операции, то реализующая ее функция должна
быть не членом, а глобальной функцией и иметь параметр типа ссылки
со спецификацией const или нессылочный параметр. Так обычно обстоит
дело с функциями, реализующими операции, которые для основных
типов не требуют адресов в качестве операндов (+, -, || и т.д.).
Если операции преобразования типа не определены, то нет
неопровержимых доводов в пользу функции-члена перед функцией-другом
с параметром-ссылкой и наоборот. Бывает, что программисту просто
одна форма записи вызова нравится больше, чем другая.
Например, многим для обозначения функции обращения матрицы m больше
нравится запись inv(m), чем m.inv(). Конечно, если функция
inv() обращает саму матрицу m, а не возвращает новую, обратную m,
матрицу, то inv() должна быть членом.
При всех прочих равных условиях лучше все-таки остановиться
на функции-члене. Можно привести такие доводы. Нельзя гарантировать,
что когда-нибудь не будет определена операция обращения. Нельзя во
всех случаях гарантировать, что будущие изменения не повлекут за
собой изменения в состоянии объекта. Запись вызова функции-члена
ясно показывает программисту, что объект может быть изменен, тогда
как запись с параметром-ссылкой далеко не столь очевидна. Далее,
выражения допустимые в функции-члене могут быть существенно
короче эквивалентных выражений в глобальной функции. Глобальная
функция должна использовать явно заданные параметры, а в