PVOID - Дружественность и наследование

Этот раздел является переводом туториала C++ Language


 Дружественные функции


В принципе, закрытые и защищенные члены класса недоступны извне того же класса, в котором они объявлены. Однако, это правило не применяется к «друзьям».

 Друзья — это функции или классы, объявленные с использованием ключевого слова friend.

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

// дружественные функции

#include <iostream>
using namespace std;

class Rectangle {    
    int width, height;
    
  public:
    Rectangle() {}    
    Rectangle (int x, int y):
      width(x), height(y) {}      
    int area() {return width * height;}
    
    friend Rectangle duplicate (const Rectangle&);
};

Rectangle duplicate (const Rectangle& param)
{
  Rectangle res;
  res.width = param.width*2;
  res.height = param.height*2;
  return res;
}

int main () {
  Rectangle foo;
  Rectangle bar (2,3);
  foo = duplicate (bar);
  cout << foo.area() << '\n';
  return 0;
}
24

   Функция duplicate является дружественной классу Rectangle. Таким образом, функция duplicate имеет доступ к членам width и height (являющимися закрытыми) различных объектов типа Rectangle. Однако, ни в объявлении duplicate, ни в ее последующем использовании в main(), функция duplicate не считается членом класса Rectangle. Это не так. Она просто имеет доступ к закрытым и защищенным членам класса, не являясь членом класса.

Типичный пример использования дружественных функций — это операции, производимые между двумя различными классами, с получением доступа к закрытым и защищенным членам обоих.

Дружественные классы


Подобно дружественным функциям, дружественный класс — это класс, члены которого имеют доступ к закрытым или защищенным членам другого класса:

// дружественный класс
#include <iostream>
using namespace std;

class Square;

class Rectangle {
    int width, height;
  public:
    int area ()
      {return (width * height);}
    void convert (Square a);
};

class Square {
  friend class Rectangle;
  private:
    int side;
  public:
    Square (int a) : side(a) {}
};

void Rectangle::convert (Square a) {
  width = a.side;
  height = a.side;
}
  
int main () {
  Rectangle rect;
  Square sqr (4);
  rect.convert(sqr);
  cout << rect.area();
  return 0;
}
16

В этом примере класс Rectangle является дружественным классу Square, что позволяет методам Rectangle иметь доступ к закрытым и защищенным членам Square. В частности, Rectangle имеет доступ к полю Square::side, которая описывает сторону квадрата.

В этом примере есть еще кое-что новое: в начале программы находится пустое объявление класса Square. Это необходимо, так как класс Rectangle использует Square (как параметр метода convert), и Square использует Rectangle (объявляя его другом).

Дружественные отношения никогда не возникают без явного указания: В нашем примере класс Rectangle считается дружественным классу Square, но класс Square не считается дружественным классу Rectangle. Таким образом, методы Rectangle могут получить доступ к защищенным и закрытым членам Square, но не наоборот. Конечно, Square также может быть объявлен другом Rectangle, если необходимо предоставление такого доступа.

Другое свойство дружественный отношений это то, что они нетранзитивны: друг друга не считается другом без явного указания.

Наследование между классами


   Классы в C++ могут быть расширены путем создания новых классов, сохраняющих характеристики базового класса. Этот процесс, известный как наследование, включает в себя базовый класс и производный класс. Производный класс наследует члены базового класса, в дополнение к которым он может добавить свои собственные члены.

Например, давайте представим ряд классов для описания двух типов многоугольников: прямоугольник и треугольник. Эти два многоугольника имеют некоторые общие свойства, такие как значения, необходимые для вычисления их площадей: в обоих случаях их можно описать как высота и ширина (или основание). Это может быть представлено в мире классов классом Polygon, из которого мы получим два других: Rectangle и Triangle:

Класс Polygon будет содержать члены, которые являются общими для обоих типов многоугольников. В нашем случае: ширина и высота. Rectangle и Triangle будут производными классами, которые определяют особенности, которые отличают один тип многоугольника от другого.

Классы, производные от других, наследуют все доступные члены базового класса. Это означает, что если базовый класс включает член A и мы наследуем класс от него с другим членом, называемым B, производный класс будет содержать как член A, так и член B.

Отношение наследования двух классов объявляется в производном классе. Определение производного класса использует следующий синтаксис:

class derived_class_name: public base_class_name
{ /*...*/ };

Где derived_class_name это имя производного класса, а base_class_name это имя базового класса, на котором он основан. Спецификатор доступа public может быть заменен любым другим спецификатором доступа (protected или private). Этот спецификатор доступа ограничивает наибольший уровень доступа для членов, наследуемых от базового класса: Члены с большим уровнем доступа наследуются с этим уровнем, в то время как члены с равным или меньшим уровнем доступа сохраняют уровень ограничений в производном классе.

 // производные классы
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b;}
 };

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
 };

class Triangle: public Polygon {
  public:
    int area ()
      { return width * height / 2; }
  };
  
int main () {
  Rectangle rect;
  Triangle trgl;
  rect.set_values (4,5);
  trgl.set_values (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}
20
10

   Объекты классов Rectangle и Triangle содержат члены, унаследованные от Polygon. Это: widthheight и set_values.

Модификатор доступа protected используется в классе Polygon аналогично private. Единственное различие заключается в наследовании: когда класс наследуется от другого, члены производного класса имеют доступ к защищенным членам, наследуемым от базового класса, но не к закрытым.

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

 Доступ  public  protected  private
 Члены того же класса  да  да  да
 Члены производного класса  да  да  нет
 Не члены  да  нет  нет

 

Где "не члены" представляет собой любой доступ извне класса, например, из main(), из другого класса или функции.

В примере выше члены, наследуемые Rectangle и Triangle, имеют такой же уровень доступа, как и в базовом классе Polygon:

Polygon::width           // защищенный доступ
Rectangle::width         // защищенный доступ

Polygon::set_values()    // открытый доступ
Rectangle::set_values()  // открытый доступ

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

class Rectangle: public Polygon { /* ... */ }

   Это ключевое слово public после двоеточия (:) означает, что наивысший уровень доступа, который члены наследуют от класса, имя которого следует за ним (в данном случае Polygon), будет доступен в производном классе (в данном случае Rectangle). Так как public является наивысшим уровнем доступа, производный класс будет наследовать все члены с таким же уровнем доступа, который они имеют в базовом классе.

В случае protected, все открытые члены базового класса наследуются как защищенные в производном классе. Если задан уровень доступа private, все члены базового класса наследуются как private.

Например, если Daughter это класс, производный от Mother, который определен как:

class Daughter: protected Mother;

тогда protected устанавливается как ограничитель уровня доступа для членов Daughter, которые наследуются от Mother. Все члены, которые имеют модификатор доступа public в классе Mother станут защищенными в Daughter. Конечно, это не запрещает объявлять собственные открытые члены в классе Daughter. Это ограничение уровня доступа устанавливается только для членов, наследуемых от Mother.

Если для наследования не указан уровень доступа, компилятор устанавливает private для классов, объявленных ключевым словом class, и public для тех, что объявлены как struct.

На самом деле, большинство случаев наследования в C++ использует наследование public. Если необходимы другие уровни доступа для базовых классов, они обычно могут быть представлены как поля класса.

Что наследуется от базового класса?


   В принципе, производный класс без ограничения доступа (public) наследует доступ ко всем членам базового класса, за исключением:

  • конструкторов и деструктора;
  • оператора присваивания (operator=);
  • друзей;
  • закрытых членов;

   Если не указано иное, конструкторы производного класса вызывают конструктор по умолчанию базового класса (т.е. конструктор без аргументов).

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

 derived_constructor_name (parameters) : base_constructor_name (parameters) {...}

Например:

// конструкторы и производные классы
#include <iostream>
using namespace std;

class Mother {
  public:
    Mother ()
      { cout << "Mother: no parameters\n"; }
    Mother (int a)
      { cout << "Mother: int parameter\n"; }
};

class Daughter : public Mother {
  public:
    Daughter (int a)
      { cout << "Daughter: int parameter\n\n"; }
};

class Son : public Mother {
  public:
    Son (int a) : Mother (a)
      { cout << "Son: int parameter\n\n"; }
};

int main () {
  Daughter kelly(0);
  Son bud(0);
  
  return 0;
}
Mother: no parameters
Daughter: int parameter

Mother: int parameter
Son: int parameter

 

   Отметим различия между вызовами конструктора Mother при создании нового объекта Daughter и объекта Son. Это различие существует по причине различных объявлений конструкторов классов Daughter и Son.

Daughter (int a)          // ничего не указано: вызывается конструктор по умолчанию
Son (int a) : Mother (a)  // указан конструктор: вызывается указанный конструктор

 Множественное наследование


   Класс может наследовать более одного класса простым перечислением нескольких базовых классов, разделенных запятыми, в списке базовых классов (т.е. после двоеточия). Например, если программа имеет определенный класс Output для вывода на экран, и мы хоте ли бы, чтобы наши классы Rectangle и Triangle также наследовали и его члены в дополнение к членам Polygon, мы можем написать:

class Rectangle: public Polygon, public Output;
class Triangle: public Polygon, public Output;

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

// множественное наследование
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    Polygon (int a, int b) : width(a), height(b) {}
};

class Output {
  public:
    static void print (int i);
};

void Output::print (int i) {
  cout << i << '\n';
}

class Rectangle: public Polygon, public Output {
  public:
    Rectangle (int a, int b) : Polygon(a,b) {}
    int area ()
      { return width*height; }
};

class Triangle: public Polygon, public Output {
  public:
    Triangle (int a, int b) : Polygon(a,b) {}
    int area ()
      { return width*height/2; }
};
  
int main () {
  Rectangle rect (4,5);
  Triangle trgl (4,5);
  rect.print (rect.area());
  Triangle::print (trgl.area());
  return 0;
}
20
10