Опубликован: 10.10.2006 | Уровень: специалист | Доступ: платный
Лекция 5:

Классы

5.4 Еще о классах

В этом разделе описаны дополнительные свойства класса. Описан способ обеспечить доступ к частным членам в функциях, не являющихся членами ( \S 5.4.1). Описано, как разрешить коллизии имен членов ( \S 5.4.2) и как сделать описания классов вложенными ( \S 5.4.3), но при этом избежать нежелательной вложенности ( \S 5.4.4). Вводится понятие статических членов (static), которые используются для представления операций и данных, относящихся к самому классу, а не к отдельным его объектам ( \S 5.4.5). Раздел завершается примером, показывающим, как можно построить дискриминирующее (надежное) объединение ( \S 5.4.6).

5.4.1 Друзья

Пусть определены два класса: vector (вектор) и matrix (матрица). Каждый из них скрывает свое представление, но дает полный набор операций для работы с объектами его типа. Допустим, надо определить функцию, умножающую матрицу на вектор. Для простоты предположим, что вектор имеет четыре элемента с индексами от 0 до 3, а в матрице четыре вектора тоже с индексами от 0 до 3. Доступ к элементам вектора обеспечивается функцией elem(), и аналогичная функция есть для матрицы. Можно определить глобальную функцию multiply (умножить) следующим образом:

vector multiply(const matrix& m, const 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) раз. Если в elem() проводится настоящий контроль границ массива, то на такой контроль будет потрачено значительно больше времени, чем на выполнение самой функции, и в результате она окажется непригодной для пользователей. С другой стороны, если elem() есть некий специальный вариант доступа без контроля, то тем самым мы засоряем интерфейс с вектором и матрицей особой функцией доступа, которая нужна только для обхода контроля.

Если можно было бы сделать multiply членом обоих классов vector и matrix, мы могли бы обойтись без контроля индекса при обращении к элементу матрицы, но в то же время не вводить специальной функции elem(). Однако, функция не может быть членом двух классов. Надо иметь в языке возможность предоставлять функции, не являющейся членом, право доступа к частным членам класса. Функция - не член класса, - имеющая доступ к его закрытой части, называется другом этого класса. Функция может стать другом класса, если в его описании она описана как friend (друг). Например:

class matrix;

 class vector {
   float v[4];
   // ...
   friend vector multiply(const matrix&, const vector&);
 };

 class matrix {
   vector v[4];
   // ...
   friend vector multiply(const matrix&, const vector&);
 };

Функция-друг не имеет никаких особенностей, за исключением права доступа к закрытой части класса. В частности, в такой функции нельзя использовать указатель this, если только она действительно не является членом класса. Описание friend является настоящим описанием. Оно вводит имя функции в область видимости класса, в котором она была описана, и при этом происходят обычные проверки на наличие других описаний такого же имени в этой области видимости. Описание friend может находится как в общей, так и в частной частях класса, это не имеет значения.

Теперь можно написать функцию multiply, используя элементы вектора и матрицы непосредственно:

vector multiply(const matrix& m, const 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;
}

Отметим, что подобно функции-члену дружественная функция явно описывается в описании класса, с которым дружит. Поэтому она является неотъемлемой частью интерфейса класса наравне с функцией-членом.

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

class x {
   // ...
   void f();
};

class y {
   // ...
   friend void x::f();
};

Вполне возможно, что все функции одного класса являются друзьями другого класса. Для этого есть краткая форма записи:

class x {
  friend class y;
  // ...
};

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

5.4.2 Уточнение имени члена

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

class X {
 int m;
  public:
 int readm() const { return m; }
 void setm(int m)  { X::m = m; }
 };

В функции X::setm() параметр m скрывает член m, поэтому к члену можно обращаться, только используя уточненное имя X::m. Правый операнд операции :: должен быть именем класса.

Начинающееся с :: имя должно быть глобальным именем. Это особенно полезно при использовании таких распространенных имен как read, put, open, которыми можно обозначать функции-члены, не теряя возможности обозначать ими же функции, не являющиеся членами.

Например:

class my_file {
// ...
 public:
int open(const char*, const char*);
 };

 int my_file::open(const char* name, const 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); }
   // ...
};

Доступность вложенного класса ограничивается областью видимости лексически объемлющего класса:

setmem m1(1,0);  // ошибка: 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 X {
 struct M1 { int m; };
public:
 struct M2 { int m; };

 M1 f(M2);
};

void f()
{
 M1 a;      // ошибка: имя 'M1' вне области видимости
 M2 b;      // ошибка: имя 'M2' вне области видимости
 X::M1 c;   // ошибка: X::M1 частный член
 X::M2 d;   // нормально
}

Отметим, что контроль доступа происходит и для имен вложенных классов.

В функции-члене область видимости класса начинается после уточнения X:: и простирается до конца описания функции. Например:

M1 X::f(M2 a)    // ошибка: имя `M1' вне области видимости
   { /* ... */ }

      X::M1 X::f(M2 a)  // нормально
   { /* ... */ }

      X::M1 X::f(X::M2 a) // нормально, но третье уточнение X:: излишне
   { /* ... */ }
Равиль Ярупов
Равиль Ярупов
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

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

как я получу диплом о профессиональной переподготовке?

Евгений Чаленко
Евгений Чаленко
Россия, Новокузнецк, НФИ КемГУ
Антон Свитенков
Антон Свитенков
Беларусь, Речица