5.4.1 Друзья | |
5.4.2 Уточнение Имени Члена | |
5.4.3 Вложенные Классы | |
5.4.4 Статические Члены | |
5.4.5 Указатели на Члены | |
5.4.6 Структуры и Объединения |
В это разделе описываются еще некоторые особенности, касающиеся
классов. Показано, как предоставить функции не члену доступ к
закрытым членам. Описывается, как разрешать конфликты имен членов,
как можно делать вложенные описания классов, и как избежать
нежелательной вложенности. Обсуждается также, как объекты класса
могут совместно использовать члены данные, и как использовать
указатели на члены. Наконец, приводится пример, показывающий, как
построить дискриминирующее (экономное) объединение.
Предположим, вы определили два класса, vector и matrix (вектор и
матрица). Каждый скрывает свое представление и предоставляет полный
набор действий для манипуляции объектами его типа. Теперь определим
функцию, умножающую матрицу на вектор. Для простоты допустим, что
в векторе четыре элемента, которые индексируются 0...3, и что
матрица состоит из четырех векторов, индексированных 0...3.
Допустим также, что доступ к элементам вектора осуществляется через
функцию elem(), которая осуществляет проверку индекса, и что в
matrix имеется аналогичная функция. Один подход состоит в
определении глобальной функции multiply() (перемножить) примерно
следующим образом:
5.4.1 Друзья
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, то мы
сможем обойтись без проверки индексов при обращении к элементу
матрицы. Однако членом двух классов функция быть не может. Нам
нужно средство языка, предоставляющее функции право доступа к
закрытой части класса. Функция не член, получившая право доступа к
закрытой части класса, называется другом класса (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. Достоинства функций друзей и членов
будут обсуждаться позже.
Функция член одного класса может быть другом другого. Например:
class x { // ... void f(); }; class y { // ... friend void x::f(); };
class x { friend class y; // ... };
Иногда полезно делать явное различие между именами членов класса и прочими именами. Для этого используется операция :: разрешения области видимости:
class x { int m; public: int readm() { return x::m; } void setm(int m) { x::m = m; } };
class my_file { // ... public: int open(char*, char*); }; int my_file::open(char* name, char* spec) { // ... if (::open(name,flag)) { // использовать open() из UNIX(2) // ... } // ... }
Описание класса может быть вложенным. Например:
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);} // ... };
class set { struct setmem { int mem; setmem* next; setmem(int m, setmem* n) }; // ... }; setmem::setmem(int m, setmem* n) { mem=m, next=n} setmem m1(1,0);
class setmem { friend class set; // доступ только с помощью членов set int mem; setmem* next; setmem(int m, setmem* n) { mem=m; next=n; } }; class set { setmem* first; public: set() { first=0; } insert(int m) { first = new setmem(m,first);} // ... };
Класс - это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных, членов этого класса. Однако некоторые типы наиболее элегантно реализуются, если все объекты этого типа могут совместно использовать (разделять) некоторые данные. Предпочтительно, чтобы такие разделяемые данные были описаны как часть класса. Например, для управления задачами в операционной системе или в ее модели часто бывает полезен список всех задач:
class task { // ... task* next; static task* task_chain; void shedule(int); void wait(event); // ... };
task::task_chain
Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, которые приводились в #4.6.9 относительно указателей на функции, в равной степени применимы и к функциям членам. Однако, на настоящее время в языке имеется дефект: невозможно описать выражением тип указателя, который получается в результате этой операции. Поэтому в текущей реализации приходится жульничать, используя трюки. Что касается примера, который приводится ниже, то не гарантируется, что он будет работать. Используемый трюк надо локализовать, чтобы программу можно было преобразовать с использованием соответствующей языковой конструкции, когда появится такая возможность. Этот трюк использует тот факт, что в текущей реализации this реализуется как первый (скрытый) параметр функции члена:
#include struct cl { char* val; void print(int x) { cout << val << x << "\n"; }; cl(char* v) { val = v; } }; // ``фальшивый'' тип для функций членов: typedef void (*proc)(void*, int); main() { cl z1("z1 "); cl z2("z2 "); proc pf1 = proc(&z1.print); proc pf2 = proc(&z2.print); z1.print(1); (*pf1)(&z1,2); z2.print(3); (*pf2)(&z2,4); }
По определению struct - это просто класс, все члены которого общие, то есть
struct s { ... есть просто сокращенная запись class s { public: ...
union tok_val { char* p; // строка char v[8]; // идентификатор (максимум 8 char) long i; // целые значения double d; // значения с плавающей точкой };
void strange(int i) { tok_val x; if (i) x.p = "2"; else x.d = 2; sqrt(x.d); // ошибка если i != 0 }
tok_val curr_val = 12; // ошибка: int присваивается tok_val'у
union tok_val { char* p; // строка char v[8]; // идентификатор (максимум 8 char) long i; // целые значения double d; // значения с плавающей точкой tok_val(char*); // должна выбрать между p и v tok_val(int ii) { i = ii; } tok_val() { d = dd; } };
void f() { tok_val a = 10; // a.i = 10 tok_val b = 10.0; // b.d = 10.0 }
tok_val::tok_val(char* pp) { if (strlen(pp) <= 8) strncpy(v,pp,8); // короткая строка else p = pp; // длинная строка }
class tok_val { char tag; union { char* p; char v[8]; long i; double d; }; int check(char t, char* s) { if (tag!=t) { error(s); return 0; } return 1; } public: tok_val(char* pp); tok_val(long ii) { i=ii; tag='i'; } tok_val(double dd) { d=dd; tag='d'; } long& ival() { check('i',"ival"); return i; } double& fval() { check('d',"fval"); return d; } char*& sval() { check('s',"sval"); return p; } char* id() { check('n',"id"); return v; } };
tok_val::tok_val(char* pp) { if (strlen(pp) <= 8) { // короткая строка tag = 'n' strncpy(v,pp,8); // скопировать 8 символов } else { // длинная строка tag = 's' p = pp; // просто сохранить указатель } }
void f() { tok_val t1("short"); // короткая, присвоить v tok_val t2("long string"); // длинная строка, присвоить p char s[8]; strncpy(s,t1.id(),8); // ok strncpy(s,t2.id(),8); // проверка check() не пройдет }
Если у класса есть конструктор, то он вызывается всегда, когда
создается объект класса. Если у класса есть деструктор, то он
вызывается всегда, когда объект класса уничтожается. Объекты могут
создаваться как:
Если x и y - объекты класса cl, то x=y в стандартном случае
означает побитовое копирование y в x (см. #2.3.8). Такая
интерпретация присваивания может привести к изумляющему (и обычно
нежелательному) результату, если оно применяется к объектам класса,
для которого определены конструктор и деструктор. Например:
Рассмотрим следующее:
[1] Автоматический объект: создается каждый раз, когда его
описание встречается при выполнении программы, и уничтожается
каждый раз при выходе из блока, в котором оно появилось;
[2] Статический объект: создается один раз, при запуске
программы, и уничтожается один раз, при ее завершении;
[3] Объект в свободной памяти: создается с помощью операции new
и уничтожается с помощью операции delete;
[4] Объект член: как объект другого класса или как элемент
вектора.
Объект также может быть сконструирован с помощью явного применения
конструктора в выражении (см. #6.4), в этом случае он является
автоматическим объектом. В следующих подразделах предполагается,
что объекты принадлежат классу, имеющему конструктор и деструктор.
Примером может служит класс table из #5.3.
5.5.1 Предостережение
class char_stack {
int size;
char* top;
char* s;
public:
char_stack(int sz) { top=s=new char[size=sz]; }
~char_stack() { delete s; } // деструктор
void push(char c) { *top++ = c; }
char pop() { return *--top; }
};
void h()
{
char_stack s1(100);
char_stack s2 = s1; // неприятность
char_stack s3(99);
s3 = s2; // неприятность
}
Здесь конструктор char_stack::char_stack() вызывается дважды: для
s1 и для s3. Для s2 он не вызывается, поскольку эта переменная
инициализируется присваиванием. Однако деструктор
char_stack::~char_stack() вызывается трижды: для s1, s2 и s3! Кроме
того, по умолчанию действует интерпретация присваивания как
побитовое копирование, поэтому в конце h() каждый из s1, s2 и s3
будет содержать указатель на вектор символов, размещенный в
свободной памяти при создании s1. Не останется никакого указателя
на вектор символов, выделенный при создании s3. Таких отклонений
можно избежать: см. Главу 6.
5.5.2 Статическая Память
table tbl1(100);
void f() {
static table tbl2(200);
}
main()
{
f();
}
Здесь конструктор table::table(), определенный в #5.3.1 , будет вызываться дважды: один раз для tbl1 и один раз для tbl2. Деструктор table::~table() также будет вызван дважды: для уничтожения tbl1 и tbl2 после выхода из main(). Конструкторы для глобальных статических объектов в файле выполняются в том порядке, в котором встречаются описания; деструкторы вызываются в обратном
порядке. Не определено, вызывается ли конструктор для локального
статического объекта, если функция, в которой этот объект описан,
не вызывается. Если конструктор для локального статического объекта
вызывается, то он вызывается после того, как вызваны конструкторы
для лексически предшествующих ему глобальных статических объектов.
Параметры конструкторов для статических объектов должны быть
константными выражениями:
void g(int a) { static table t(a); // ошибка }
Рассмотрим:
main() { table* p = new table(100); table* q = new table(200); delete p; delete p; // возможно, ошибка }
Рассмотрим
class classdef { table members; int no_of_members; // ... classdef(int size); ~classdef(); };
classdef::classdef(int size) : members(size) { no_of_members = size; // ... }
class classdef { table members; table friends; int no_of_members; // ... classdef(int size); ~classdef(); };
classdef::classdef(int size) : friends(size), members(size) { no_of_members = size; // ... }
classdef::classdef(int size) : friends(size=size/2), members(size); // дурной стиль { no_of_members = size; // ... }
classdef::classdef(int size) : members(size) { no_of_members = size; // ... }
class classdef { table* members; table* friends; int no_of_members; // ... classdef(int size); ~classdef(); }; classdef::classdef(int size) { members = new table(size); friends = new table; // размер таблицы по умолчанию no_of_members = size; // ... }
classdef::~classdef() { // ... delete members; delete friends; }
Чтобы описать вектор объектов класса, имеющего конструктор, этот класс должен иметь конструктор, который может вызываться без списка параметров. Нельзя использовать даже параметры по умолчанию. Например:
table tblvec[10];
class table { // ... void init(int sz); // как старый конструктор public: table(int sz) // как раньше, но без по умолчанию { init(sz); } table() // по умолчанию { init(15); } }
void f() { table* t1 = new table; table* t2 = new table[10]; delete t1; // одна таблица delete t2; // неприятность: 10 таблиц }
void g(int sz) { table* t1 = new table; table* t2 = new table[sz]; delete t1; delete[] t2; }
Когда вы используете много небольших объектов, размещаемых в
свободной памяти, то вы можете обнаружить, что ваша программа
тратит много времени выделяя и освобождая память под эти объекты.
Первое решение - это обеспечить более хороший распределитель памяти
общего назначения, второе для разработчика классов состоит в том,
чтобы взять под контроль управление свободной памятью для объектов
некоторого класса с помощью подходящих конструкторов и
деструкторов.
Рассмотрим класс name, который использовался в примерах table.
Его можно было бы определить так:
struct name { char* string; name* next; double value; name(char*, double, name*); ~name(); };
const NALL = 128; name* nfree;
name::name(char* s, double v, name* n) { register name* p = nfree; // сначала выделить if (p) nfree = p->next; else { // выделить и сцепить name* q = (name*)new char[ nall*sizeof(name) ]; for (p=nfree=&q[nall-1]; qnext = p-1; (p+1)->next = 0; } this = p; // затем инициализировать string = s; value = v; next = n; }
name* q = new name[nall];
name::~name() { next = nfree; nfree = this; this = 0; }
Когда в конструкторе производится присваивание указателю this, значение this до этого присваивания не определено. Таким образом, ссылка на член до этого присваивания не определена и скорее всего приведет к катастрофе. Имеющийся компилятор не пытается убедиться в том, что присваивание указателю this происходит на всех траекториях выполнения:
mytype::mytype(int i) { if (i) this = mytype_alloc(); // присваивание членам };
mytype::mytype(int i) { if (this == 0) this = mytype_alloc(); // присваивание членам };
Когда пользователь берет управление распределением и освобождением памяти, он может конструировать объекты, размер которых во время компиляции недетерминирован. В предыдущих примерах вмещающие (или контейнерные - перев.) классы vector, stack, intset и table реализовывались как структуры доступа фиксированного размера, содержание указатели на реальную память. Это подразумевает, что для создания таких объектов в свободной памяти необходимо две операции по выделению памяти, и что любое обращение к хранимой информации будет содержать дополнительную косвенную адресацию. Например:
class char_stack { int size; char* top; char* s; public: char_stack(int sz) { top=s=new char[size=sz]; } ~char_stack() { delete s; } // деструктор void push(char c) { *top++ = c; } char pop() { return *--top; } };
class char_stack { int size; char* top; char s[1]; public: char_stack(int sz); void push(char c) { *top++ = c; } char pop() { return *--top; } }; char_stack::char_stack(int sz) { if (this) error("стек не в свободной памяти"); if (sz < 1) error("размер стека < 1"); this = (char_stack*) new char[sizeof(char_stack)+sz-1]; size = sz; top = s; }
class expr { // ... public: expr(char*); int eval(); void print(); }
expr x("123/4+123*4-3"); cout << "x = " << x.eval() << "\n"; x.print();
#include main() { cout << "Hello, world\n"; }
Initialize Hello, world Clean up
*1 Иногда называется также квалификацией. (прим. перев.)
*2 Более поздние версии C++ поддерживают понятие указатель на
член: cl::* означает "указатель на член класса cl". Например:
typedef void (cl::*PROC)(int);
PROC pf1 = &cl::print; // приведение к типу ненужно
proc pf2 = &cl::print;
Для вызовов через указатель на функцию член используются операции .
и ->. Например:
(z1.*pf1)(2);
((&z2)->*pf2)(4);
(прим. автора)