C++ - Страустрап Бьярн
Шрифт:
Интервал:
Закладка:
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 amp;) *);
void string::operator=(string amp; a) (* if (this == amp;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 amp;); string(string amp;); *);
void string::string(string amp; a) (* p=new char[size=a.size]; strcpy(p,a.p); *)
Для типа X инициализацию тем же типом X обрабатывает конструктор X(X amp;). Нельзя не подчеркнуть еще раз, что присвивание и инициализация – разные действия. Это особенно сщественно при описании деструктора. Если класс X имеет контруктор X(X amp;), выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комлект функций, чтобы полностью избежать побитового копирования объектов:
class X (* // ... X(something); // конструктор: создает объект X( amp;X); // конструктор: копирует в инициализации operator=(X amp;); // присваивание: чистит и копирует ~X(); // деструктор: чистит *);
Есть еще два случая, когда объект копируется: как парметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого пременная – формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из фунции, хотя это менее очевидно. В обоих случаях будет применен X(X amp;), если он определен:
string g(string arg) (* return arg; *)
main() (* string s = «asdf»; s = g(s);
*) Ясно, что после вызова g() значение s обязано быть «asdf». Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string amp;). Для взятия копии этого значения из g() требуется еще один вызов string(string amp;); на этот раз инициализируемой является врменная переменная, которая затем присваивается 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 amp; 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 «string.h»
int assoc::operator[](char* p) /* работа с множеством пар «pair»: поиск p, возврат ссылки на целую часть его «pair» делает новую «pair», если p не встречалось */ (* register pair* pp;
for (pp= amp;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; i«max; i++) nvec[i] = vec[i]; delete vec; vec = nvec; max = 2*max; *)
pp = amp;vec[free++]; pp-»name = 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«free; i++) cout „« vec[i].name «« ": " «« vec[i].val «« «n“; *)
Мы можем, наконец, написать простую главную программу:
main() // считает вхождения каждого слова во вводе (* const MAX = 256; // больше самого большого слова char buf[MAX]; assoc vec(512); while (cin»»buf) vec[buf]++; vec.print_all(); *)
6.8 Вызов функции
Вызов функции, то есть запись выражение(список_выражний), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие оперции. Список параметров функции operator() вычисляется и прверяется в соответствие с обычными правилами передачи парметров. Перегружающая функция может оказаться полезной главным образом для определения типов с единственной операцей и для типов, у которых одна операция настолько преобладет, что другие в большинстве ситуаций можно не принимать во внимание.
Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к даным, которые хранятся в assoc, поэтому он сделан другом:
class assoc (* friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int amp; operator[](char*); *);
Итератор определяется как
class assoc_iterator(* assoc* cs; // текущий массив assoc int i; // текущий индекс public: assoc_iterator(assoc amp; s) (* cs = amp;s; i = 0; *) pair* operator()() (* return (i«cs-»free)? amp;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“; *)
0 Итераторный тип вроде этого имеет преимущество перед нбором функций, которые выполняют ту же работу: у него есть собственные закрытые данные для хранения хода итерации. К тму же обычно существенно, чтобы одновременно могли работать много итераторов этого типа.
Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).