Смекни!
smekni.com

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

размер этих объектов меняется, то файлы, в которых класс

используется, нужно компилировать заново. Можно написать такую

программу (и она уже написана), которая определяет множество

(минимальное) файлов, которое необходимо компилировать заново после

изменения описания класса, но пока что широкого распространения она

не получила.

Почему, можете вы спросить, C++ разработан так, что после

изменения закрытой части необходима новая компиляция пользователей

класса? И действительно, почему вообще закрытая часть должна быть

представлена в описании класса? Другими словами, раз пользователям

класса не разрешается обращаться к закрытым членам, почему их

описания должны приводиться в заголовочных файлах, которые, как

предполагается, пользователь читает? Ответ - эффективность. Во

многих системах и процесс компиляции, и последовательность

операций, реализующих вызов функции, проще, когда размер

автоматических объектов (объектов в стеке) известен во время

компиляции.

Этой сложности можно избежать, представив каждый объект класса

как указатель на "настоящий" объект. Так как все эти указатели

будут иметь одинаковый размер, а размещение "настоящих" объектов

можно определить в файле, где доступна закрытая часть, то это может

решить проблему. Однако решение подразумевает дополнительные ссылки

по памяти при обращении к членам класса, а также, что еще хуже,

каждый вызов функции с автоматическим объектом класса включает по

меньшей мере один вызов программ выделения и освобождения свободной

памяти. Это сделало бы также невозможным реализацию inline-функций

- стр 153 -

членов, которые обращаются к данным закрытой части. Более того,

такое изменение сделает невозможным совместную компоновку C и C++

программ (поскольку C компилятор обрабатывает struct не так, как

это будет делать C++ компилятор). Для C++ это было сочтено

неприемлемым.

5.3.2 Законченный Класс

Программирование без скрытия данных (с применением структур)

требует меньшей продуманности, чем программирование со скрытием

данных (с использованием классов). Структуру можно определить не

слишком задумываясь о том, как ее предполагается использовать. А

когда определяется класс, все внимание сосредотачивается на

обеспечении нового типа полным множеством операций; это важное

смещение акцента. Время, потраченное на разработку нового типа,

обычно многократно окупается при разработке и тестировании

программы.

Вот пример законченного типа intset, который реализует понятие

"множество целых":

class intset {

int cursize, maxsize;

int *x;

public:

intset(int m, int n); // самое большее, m int'ов в 1..n

~intset();

int member(int t); // является ли t элементом?

void insert(int t); // добавить "t" в множество

void iterate(int& i) { i = 0; }

int ok(int& i) { return i

void error(char* s)

{

cerr << "set: " << s << "&bsol;n";

exit(1);

}

Класс intset используется в main(), которая предполагает два

целых параметра. Первый параметр задает число случайных чисел,

которые нужно сгенерировать. Второй параметр указывает диапазон, в

котором должны лежать случайные целые:

- стр 154 -

main(int argc, char* argv[])

{

if (argc != 3) error("ожидается два параметра");

int count = 0;

int m = atoi(argv[1]); // число элементов множества

int n = atoi(argv[2]); // в диапазоне 1..n

intset s(m,n);

while (count maxsize) error("слищком много элементов");

int i = cursize-1;

x[i] = t;

while (i>0 && x[i-1]>x[i]) {

int t = x[i]; // переставить x[i] и [i-1]

x[i] = x[i-1];

x[i-1] = t;

i--;

}

}

Для нахождения членов используется просто двоичный поиск:

int intset::member(int t) // двоичный поиск

{

int l = 0;

int u = cursize-1;

while (l <= u) {

int m = (l+u)/2;

if (t < x[m])

u = m-1;

else if (t > x[m])

l = m+1;

else

return 1; // найдено

}

return 0; // не найдено

}

И, наконец, нам нужно обеспечить множество операций, чтобы

пользователь мог осуществлять цикл по множеству в некотором

порядке, поскольку представление intset от пользователя скрыто.

Множество внутренней упорядоченности не имеет, поэтому мы не можем

просто дать возможность обращаться к вектору (завтра я, наверное,

реализую intset по-другому, в виде связанного списка).

Дается три функции: iterate() для инициализации итерации, ok()

для проверки, есть ли следующий элемент, и next() для того, чтобы

взять следующий элемент:

class intset {

// ...

void iterate(int& i) { i = 0; }

int ok(int& i) { return iiterate(var);

while (set->ok(var)) cout << set->next(var) << "&bsol;n";

}

Другой способ задать итератор приводится в #6.8.

5.4 Друзья и Объединения

В это разделе описываются еще некоторые особенности, касающиеся

классов. Показано, как предоставить функции не члену доступ к

закрытым членам. Описывается, как разрешать конфликты имен членов,

как можно делать вложенные описания классов, и как избежать

нежелательной вложенности. Обсуждается также, как объекты класса

могут совместно использовать члены данные, и как использовать

указатели на члены. Наконец, приводится пример, показывающий, как

построить дискриминирующее (экономное) объединение.

5.4.1 Друзья

Предположим, вы определили два класса, vector и matrix (вектор и

матрица). Каждый скрывает свое представление и предоставляет полный

набор действий для манипуляции объектами его типа. Теперь определим

функцию, умножающую матрицу на вектор. Для простоты допустим, что

в векторе четыре элемента, которые индексируются 0...3, и что

матрица состоит из четырех векторов, индексированных 0...3.

Допустим также, что доступ к элементам вектора осуществляется через

функцию elem(), которая осуществляет проверку индекса, и что в

matrix имеется аналогичная функция. Один подход состоит в

определении глобальной функции multiply() (перемножить) примерно

следующим образом:

vector multiply(matrix& m, vector& v);

{

vector r;

for (int i = 0; i<3; i++) { // r[i] = m[i] * v;

r.elem(i) = 0;

for (int j = 0; j<3; j++)

r.elem(i) += m.elem(i,j) * v.elem(j);

}

return r;

}

Это своего рода "естественный" способ, но он очень неэффективен.

При каждом обращении к multiply() elem() будет вызываться 4*(1+4*3)

раза.

Теперь, если мы сделаем multiply() членом класса vector, мы

сможем обойтись без проверки индексов при обращении к элементу

вектора, а если мы сделаем multiply() членом класса matrix, то мы

- стр 157 -

сможем обойтись без проверки индексов при обращении к элементу

матрицы. Однако членом двух классов функция быть не может. Нам

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

закрытой части класса. Функция не член, получившая право доступа к

закрытой части класса, называется другом класса (friend). Функция

становится другом класса после описания как friend. Например:

class matrix;

class vector {

float v[4];

// ...

friend vector multiply(matrix&, vector&);

};

class matrix {

vector v[4];

// ...

friend vector multiply(matrix&, vector&);

};

Функция друг не имеет никаких особенностей, помимо права доступа к

закрытой части класса. В частности, friend функция не имеет

указателя this (если только она не является полноправным членом

функцией). Описание friend - настоящее описание. Оно вводит имя

функции в самой внешней области видимости программы и

сопоставляется с другими описаниями этого имени. Описание друга

может располагаться или в закрытой, или в открытой части описания

класса; где именно, значения не имеет.

Теперь можно написать функцию умножения, которая использует

элементы векторов и матрицы непосредственно:

vector multiply(matrix& m, vector& v);

{

vector r;

for (int i = 0; i<3; i++) { // r[i] = m[i] * v;

r.v[i] = 0;

for (int j = 0; j<3; j++)

r.v[i] += m.v[i][j] * v.v[j];

}

return r;

}

Есть способы преодолеть эту конкретную проблему эффективности не

используя аппарат friend (можно было бы определить операцию

векторного умножения и определить multiply() с ее помощью). Однако

существует много задач, которые проще всего решаются, если есть

возможность предоставить доступ к закрытой части класса функции,

которая не является членом этого класса. В Главе 6 есть много

примеров применения friend. Достоинства функций друзей и членов

будут обсуждаться позже.

Функция член одного класса может быть другом другого. Например:

- стр 158 -

class x {

// ...

void f();

};

class y {

// ...

friend void x::f();

};

Нет ничего необычного в том, что все функции члены одного класса

являются друзьями другого. Для этого есть даже более краткая

запись:

class x {

friend class y;

// ...

};

Такое описание friend делает все функции члены класса y друзьями x.

5.4.2 Уточнение* Имени Члена

Иногда полезно делать явное различие между именами членов класса

и прочими именами. Для этого используется операция :: разрешения

области видимости:

class x {

int m;

public:

int readm() { return x::m; }

void setm(int m) { x::m = m; }

};

В x::setm() имя параметра m прячет член m, поэтому единственный

способ сослаться на член - это использовать его уточненное имя

x::m. Операнд в левой части :: должен быть именем класса.

Имя с префиксом :: (просто) должно быть глобальным именем. Это

особенно полезно для того, чтобы можно было использовать часто

употребимые имена вроде read, put и open как имена функций членов,

не теряя при этом возможности обращаться к той версии функции,

которая не является членом. Например:

____________________

* Иногда называется также квалификацией. (прим. перев.)

- стр 159 -

class my_file {

// ...

public:

int open(char*, char*);

};

int my_file::open(char* name, char* spec)

{

// ...

if (::open(name,flag)) { // использовать open() из UNIX(2)

// ...

}

// ...

}

5.4.3 Вложенные Классы

Описание класса может быть вложенным. Например:

class set {

struct setmem {

int mem;

setmem* next;

setmem(int m, setmem* n) { mem=m; next=n; }

};

setmem* first;

public:

set() { first=0; }

insert(int m) { first = new setmem(m,first);}

// ...

};

Если только вложенный класс не является очень простым, в таком