Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 34 из 43)

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 << "&bsol;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 << "]&bsol;n";

}

Операция ввода использует стандартную функцию ввода символьной

строки (#8.4.1).

istream& operator>>(istream& s, string& x)

{

char buf[256];

s >> buf;

x = buf;

cout << "echo: " << x << "&bsol;n";

return s;

}

Для доступа к отдельным символам предоставлена операция

индексирования. Осуществляется проверка индекса:

void error(char* p)

{

cerr << p << "&bsol;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 << "отсюда начнем&bsol;n";

for (n = 0; cin>>x[n]; n++) {

string y;

if (n==100) error("слишком много строк");

cout << (y = x[n]);

if (y=="done") break;

}

cout << "отсюда мы пройдем обратно&bsol;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 (=, *=, ++

и т.д.), наиболее естественно определяются как члены.

И наоборот, если нужно иметь неявное преобразование для всех

операндов операции, то реализующая ее функция должна быть другом, а