C++ — страница 34 из 57

ию, это означало бы, что смысл программы зависит от того, какое преобразование было найдено. В резултате смысл программы неким образом зависел бы от порядка опсания преобразований. Поскольку они часто находятся в разных исходных файлах (написанных разными людьми), смысл программы будет зависеть от порядка компоновки этих частей вместе. Есть другой вариант – запретить все неявные преобразования. Нет ничего проще, но такое правило приведет либо к неэлегантным пользовательским интерфейсам, либо к бурному росту перегрженных функций, как это было в предыдущем разделе с complex.

Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Напрмер, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность толкования. Если aa является x, то единственное, дающее в рзультате x, который требуется присваиванием, – это f(x(1)), а если aa – это y, то вместоэтого будет использоваться f(y(1)). Самый общий подход справился бы и с g(«asdf»), поскольку единственной интерпретацией этого может быть g(z(x(«asdf»))). Сложность этого подхода в том, что он требует расширенного нализа всего выражения для того, чтобы определить интерпретцию каждой операции и вызова функции. Это приведет к замедлнию компиляции, а также к вызывающим удивление интерпретацим и сообщениям об ошибках, если компилятор рассмотрит преоразования, определенные в библиотеках и т.п. При таком подхде компилятор будет принимать во внимание больше, чем, как можно ожидать, знает пишущий программу программист!

6.4 Константы

Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e являются константами типа double. Вместо них, однако, часто можно использовать константы осноных типов, если их реализация обеспечивается с помощью фунций членов. Общий аппарат для этого дают конструкторы, полчающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в «comlpex.h» есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам функций приведут две операции *, а операция + и конструктор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.

6.5 Большие Объекты

При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копрование каждого double заметны, но с ними вполне можно примриться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:

class matrix (* double m[4][4]; public: matrix(); friend matrix operator+(matrix amp;, matrix amp;); friend matrix operator*(matrix amp;, matrix amp;); *);

Ссылки позволяют использовать выражения, содержащие обычные арифметические операции над большими объектами, без ненужного копирования. Указатели применять нельзя, потому что невозможно для применения к указателю смысл операции переоределить невозможно. Операцию плюс можно определить так:

matrix operator+(matrix amp;, matrix amp;); (* matrix sum; for (int i=0; i«4; i++) for (int j=0; j«4; j++) sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j]; return sum; *)

Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказатся более эффективным:

class matrix (* // ... friend matrix amp; operator+(matrix amp;, matrix amp;);

friend matrix amp; operator*(matrix amp;, matrix amp;); *);

Это является допустимым, но приводит к сложности с выдлением памяти. Поскольку ссылка на результат будет передваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто оперция используется в выражении больше одного раза, результат не может быть и статической переменной. Как правило, его размщают в свободной памяти. Часто копирование возвращаемого знчения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется.

6.6 Присваивание и Инициализация

Рассмотрим очень простой класс строк string:

struct string (* char* p; int size; // размер вектора, на который указывает p

string(int sz) (* p = new char[size=sz]; *) ~string() (* delete p; *) *);

Строка – это структура данных, состоящая из вектора сиволов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:

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 скрыто, нам нужен способ его печати. В следующем разделе будет показано, как опредлить подходящий итератор, а здесь мы используем простую фунцию печати: