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

<!--StartFragment -->
  • Друзья

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

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

  • Статические Члены

  • Указатели на Члены

  • Структуры и Объединения


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

Друзья

Предположим, вы определили два класса, 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, то мы сможем обойтись без проверки индексов при обращении к элементу матрицы. Однако членом двух классов функция быть не может. Нам нужно средство языка, предоставляющее функции право доступа к закрытой части класса. Функция не член, получившая право доступа к закрытой части класса, называется другом класса (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;
// ...
};


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

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

Иногда полезно делать явное различие между именами членов класса и прочими именами. Для этого используется операция :: разрешения области видимости:
 

 


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 как имена функций членов, не теряя при этом возможности обращаться к той версии функции, которая не является членом.

Например:
 

 


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);

Такая запись, как set::setmem::setmem(), не является ни необходимой, ни допустимой. Единственный способ скрыть имя класса - это сделать это с помощью метода файлы-как-модули. Большую часть нетривиальных классов лучше описывать раздельно:

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_chain (цепочка задач) как static обеспечивает, что он будет всего лишь один, а не по одной копии на каждый объект task. Он все равно остается в области видимости класса task, и "извне" доступ к нему можно получить, только если он был описан как public. В этом случае его имя должно уточняться именем его класса:

task::task_chain

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

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

Именованное объединение определяется как struct, в которой все члены имеют один и тот же адрес. Если известно, что в каждый момент времени нужно только одно значение из структуры, то объединение может сэкономить пространство. Например, можно определить объединение для хранения лексических символов C компилятора:
 

 


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
}

Когда это невозможно (для таких типов, как char* и char[8], int и char, и т.п.), нужный член может быть найден только посредством анализа инициализатора в ходе выполнения или с помощью задания дополнительного параметра.

Например:

tok_val::tok_val(char* pp)
{
if (strlen(pp) <= 8)
strncpy(v,pp,8); // короткая строка
else
p = pp; // длинная строка
}

Таких ситуаций вообще-то лучше избегать.

Использование конструкторов не предохраняет от такого случайного неправильного употребления tok_val, когда сначала присваивается значение одного типа, а потом рассматривается как другой тип. Эта проблема решается встраиванием объединения в класс, который отслеживает, какого типа значение помещается:
 

 


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; }
};


Конструктор, получающий строковый параметр, использует для копирования коротких строк strncpy(). strncpy() похожа на strcpy(), но получает третий параметр, который указывает, сколько символов должно копироваться:

tok_val::tok_val(char* pp)
{
if (strlen(pp) <= 8) { // короткая строка
tag = "N"
strncpy(v,pp,8); // скопировать 8 символов
}
else { // длинная строка
tag = "S"
p = pp; // просто сохранить указатель
}
}

Тип tok_val можно использовать так:

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() не пройдет
}



Опубликовал admin
23 Мар, Вторник 2004г.



Alcatraz игровые автоматы играть онлайн
Игровые автоматы играть
imperator.casino
Охлаждающая попона hurtta
Попоны для собак на Ozon
mysuperdog.ru
Программирование для чайников.