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&); на этот
раз инициализируемой является временная переменная, которая затем
- стр 188 -
присваивается s. Такие переменные, естественно, уничтожаются как
положено с помощью string::~string() при первой возможности.
6.7 Индексирование
Чтобы задать смысл индексов для объектов класса используется
функция 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
увеличивается:
- стр 189 -
#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();
}
- стр 190 -
6.8 Вызов Функции
Вызов функции, то есть запись выражение(список_выражений), можно
проинтерпретировать как бинарную операцию, и операцию вызова можно
перегружать так же, как и другие операции. Список параметров
функции operator() вычисляется и проверяется в соответствие с
обычнчми правилами передачи параметров. Перегружающая функция может
оказаться полезной главным образом для определения типов с
единственной операцией и для типов, у которых одна операция
настолько преобладает, что другие в большинстве ситуаций можно не
принимать во внимание.
Для типа ассоциативного массива assoc мы не определили итератор.
Это можно сделать, определив класс assoc_iterator, работа которого
состоит в том, чтобы в определенном порядке поставлять элементы из
assoc. Итератору нужен доступ к данным, которые хранятся в assoc,
поэтому он сделан другом:
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";
}
- стр 191 -
Итераторный тип вроде этого имеет преимущество перед набором
функций, которые выполняют ту же работу: у него есть собственные
закрытые данные для хранения хода итерации. К тому же обычно
существенно, чтобы одновременно могли работать много итераторов
этого типа.
Конечно, такое применение объектов для представления итераторов
никак особенно с перегрузкой операций не связано. Многие любят
использовать итераторы с такими операциями, как first(), next() и
last() (первый, следующий и последний).
6.9 Класс Строка
Вот довольно реалистичный пример класса string. В нем
производится учет ссылок на строку с целью минимизировать
копирование и в качестве констант применяются стандартные
символьные строки C++.
#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; }
};
Конструкторы и деструкторы просты (как обычно):
- стр 192 -
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;
}
Благоразумно обеспечить, чтобы присваивание объекта самому себе
работало правилньо:
- стр 193 -
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, которая завершает сохранение слов в строках, или не
встретит конец файла. После этого она печатает строки в обратном
порядке и завершается.
- стр 194 -
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];
}
6.10 Друзья и Члены
Теперь, наконец, можно обсудить, в каких случаях для доступа к
закрытой части определяемого пользователем типа использовать члены,
а в каких - друзей. Некоторые операции должны быть членами:
конструкторы, деструкторы и виртуальные функции (см. следующую
главу), но обчно это зависит от выбора.
Рассмотрим простой класс 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 (=, *=, ++
и т.д.), наиболее естественно определяются как члены.
И наоборот, если нужно иметь неявное преобразование для всех
операндов операции, то реализующая ее функция должна быть другом, а