-
Notifications
You must be signed in to change notification settings - Fork 0
Лекция 06. Наследование.
- Общая схема исполнения разных объектов.
- В объектах один и тот же набор методов.
- Имеются два разных класса с разными методами. Если у методов похожая реализация, то выделяем базовый класс.
- У нас имеется два разных класса. Если в дальнейшем они будут участвовать вместе, лучше сделать для них базовый класс уже на этом этапе разработки. Это нужно сделать, чтобы в дальнейшем нам было легко модифицировать программу.
- Если один объект исполняет разные роли.
- Два множества методов используются в разной манере.
- Методы между собой никак не связаны .
- Одна сущность, но используется в разных частях программы.
На классах возможно множественное наследование.
Язык С++ – это практически единственный язык, в котором присутствует множественное наследование.
Какие преимущества множественного наследования? Пойдем методом от противного - попробуем избавиться от множественного наследования, и представим, что было бы.
-
Представим такую ситуацию: выстраиваем вертикальную иерархию, класс C наследуется от класса B, а B наследуется от класса A. В этом случае, в класс A мы должны вынести много того, что к понятию класса А не относится. Не совсем логично.
В случае со множественным наследованием (рис. ниже), мы четко разделяем понятия A и B. Такой подход уменьшает иерархию.
-
Второй момент (опять не используем множественное наследование). Мы можем не выносить что-то в базовый класс, а один из классов включить как подобъект, то есть не использовать наследование. Пусть С - производная от класса А и включает подобъект класса В. Тоже возникнет проблема - не будем иметь доступа к защищенным полям класса Б (нам придется делать это через методы класса Б) + придется протаскивать интерфейс для класса С класса Б.
ВАЖНО! В языке С++ происходит неявное преобразование от указателя объекта производного класса к указателю на объект базового класса. То же самое касается ссылок.
Рассмотрим пример ниже:
A *p = new C; // Неявное преобразование (пример с указателями)
C obj;
A& alias = obj; // Неявное преобразование (пример со ссылкой)
Непосредственная база для какого-либо класса является прямой базой. Может входить в производный класс только один раз. Прямой базовый класс явно перечисляется в заголовке при объявлении производного класса.
Косвенная база - прямая база прямой базы. Может входить в базовый класс сколько угодно раз.
Может возникнуть ситуация, когда в наш класс косвенная база входит два раза:
В большинстве случаев нам необходимо, чтобы базовый класс входил в производный только один раз. Рассмотрим пример ниже.
В данном примере два раза отрабатывает конструктор класса А.
class A
{
public:
A (char* s)
{
cout << "Creature A" << s << ";" << endl;
}
};
class B : public A
{
public:
B() : A (" from B")
{
cout << "Creature B;" << endl;
}
};
class C : public B, public A // В класс C подобъект класса А будет входить два раза.
{
public:
C() : A(" from C")
{
cout << "Creature C;" << endl;
}
};
void main()
{
C obj;
}
Вызовется конструктор класса С. Из С вызовется конструктор класса B (так как класс B наследуется раньше класса A). Вызовется конструктор класса A. Создастся объект класса А. Создастся объект класса B. Из С вызовется конструктор класса А. Создастся объект класса А. Создастся объект класса С.
На экран в результате работы программы будет выведено следующее:
Creature A from B;
Creature B;
Creature A from C;
Creature C;
Проблема решается с помощью виртуального наследования.
В большинстве случаев необходимо, чтобы базовый класс входил в производный только один раз. Для этого используется виртуальное наследование.
ВАЖНО! При виртуальном наследовании меняется порядок создания объекта: если в списке наследования есть виртуальное наследование (виртуальные базы), они отрабатывают в первую очередь слева направо, а потом всё остальные базы.
Чтобы сделать родительский (базовый) класс общим, используется ключевое слово virtual в строке объявления дочернего класса.
Исправим предыдущий пример, добавив виртуальное наследование. Теперь, когда для класса С будет создаваться объект класса А, будет включаться механизм виртуальности. Когда будет создаваться подобъект класса С, для него не будет создан объект класса А.
class A
{
public:
A(char* s)
{
cout << "Creature A" << s << ";" << endl;
}
};
class B : virtual public A
{
public:
B() : A(" from B")
{
cout << "Creature B;" << endl;
}
};
class C : public B, virtual public A
{
public:
C() : A(" from C")
{
cout << "Creature C;" << endl;
}
};
void main()
{
C obj;
}
На экран в результате работы программы будет выведено следующее:
Creature A from C;
Creature B;
Creature C;
Рассмотрим следующий пример:
class A {};
class B : virtual public A{}; // Здесь virtual наследование
class C : public A {}; // Здесь не virtual наследование
class D : public B, public C{};
Порядок создания объекта класса D: сначала вызывается конструктор класса B. Для него вызывается конструктор класса А, будет выполняться механизм виртуальности. Создастся подобъект класса А, отработает конструктор класса B. Для C уже не будет выполняться A.
Но если поменять порядок наследования для класса D:
class D : public C, public B{};
Смена последовательности наследование приводит к тому, что класс А будет включен два раза, что не должно происходить при включении механизма виртуальности.
ВАЖНО! Используя множественное наследование, виртуально надо стараться виртуально наследоваться по всем ветвям, чтобы не зависеть от порядка наследования.
Ниже представлена правильная версия:
class A {};
class B : virtual public A{}; // Здесь virtual наследование
class C : virtual public A {}; // Тут теперь тоже virtual наследование
class D : public B, public C{};
В общем случае, диаграмма наследования представляет собой направленный ациклический граф.
ВАЖНО! Метод, находящийся в наге дальше, подменяет другие методы, находящиеся выше.
Рассмотрим нижеприведенную схему. В классе А есть перегруженный метод f: f() и f(int). В производном классе есть всего один метод f().
В данном случае в классе B подменяется метод f(). Для объектов класса B метод f(int) недоступен. Это сделано, чтобы при проектировании было корректное наследование.
ВАЖНО! Если мы подменяем один перегруженный метод, мы обязаны подменить все остальные. Если мы этого не сделаем, они будут перекрыты.
Можно использовать using (но это плохо). В данном примере мы хоть и описали в классе B только один перегруженный метод, но с помощью using мы можем использовать f(int). Таким образом, объект класса B может использовать f() своего класса B и f(int) базового класса A.
class A
{
public:
void f() { cout<<"Executing f() from A;"<<endl; }
void f(int i) { cout<<"Executing f(int) from A;"<<endl; }
};
class B : virtual public A
{
public:
void f() { cout<<"Executing f from B;"<<endl; }
using A::f; // плохо!!!
};
class C : virtual public A
{
};
class D : virtual public C, virtual public B
{
};
void main()
{
D obj;
obj.f(); // вызвается метод f() класса B
obj.f(1); // Вызывается метод f(int) класса A
}
Программа выведет на экран:
Executing f from B;
Executing f(int) from A;
Какие схемы возможны при множественном наследовании?
В данном случае для объектов класса D метод f() класса B подменят метод f() класса A.
Аналогично для этой схемы:
Подмена метода рассматривается в примере ниже.
class A
{
public:
void f() { cout<<"Executing f from A;"<<endl; }
};
class B : virtual public A
{
public:
void f() { cout<<"Executing f from B;"<<endl; }
};
class C : public B, virtual public A
{
};
void main()
{
C obj;
obj.f();
}
В данном примере метод f() класса В доминирует для класса С над методами класса А.
Программа выведет на экран:
Executing f from B;
В современных языках от множественного наследования отказались. В объектно-ориентированном проектировании множественное наследование не рассматривается.
Рассмотрим следующую схему:
Предположим, есть объект класса А с методом draw(), который умеет себя нарисовать. Производные от него классы - B и C, тоже имеют метод draw() и тоже могут себя нарисовать, а так же они могут нарисовать подобъект базового класса. То есть, при рисовании объектов класса B (аналогично для C) вызывается метод draw() класса А.
Когда мы создаем объект класса D, в котором мы должны отрисовать объект класса B и объект класса C, draw() класса A вызывается два раза. Это называется проблема множественного вызова базового класса.
class A
{
public:
void f() { cout<<"Executing f from A;"<<endl; }
};
class B : virtual public A
{
public:
void f()
{
A::f();
cout<<"Executing f from B;"<<endl;
}
};
class C : virtual public A
{
public:
void f()
{
A::f();
cout<<"Executing f from C;"<<endl;
}
};
class D : virtual public C, virtual public B
{
public:
void f()
{
C::f();
B::f();
cout<<"Executing f from D;"<<endl;
}
};
void main()
{
D obj;
obj.f();
}
Метод f() класса А срабатывает дважды.
Программа выведет на экран:
Executing f from A;
Executing f from C;
Executing f from A;
Executing f from B;
Executing f from D;
Красивых способов борьбы с этой чумой, к сожалению, нет.
Идея: разделить метод на две части. Часть, которая относится непосредственно к самому классу, и часть, которая относится ко всему объекту.
В классе А мы разделили f() на две части, причем, то что относится к самому классу A - его собственное, мы делаем его защищенным, доступным только для методов класса А и производных классов. В производных классах мы тоже разделяем - метод, относящийся непосредственно к самому классу и ко всему объекту.
class A
{
protected:
void _f() { cout<<"Executing f from A;"<<endl; }
public:
void f() { this->_f(); }
};
class B : virtual public A
{
protected:
void _f() { cout<<"Executing f from B;"<<endl; }
public:
void f()
{
A::_f();
this->_f();
}
};
class C : virtual public A
{
protected:
void _f() { cout<<"Executing f from C;"<<endl; }
public:
void f()
{
A::_f();
this->_f();
}
};
class D : virtual public C, virtual public B
{
protected:
void _f() { cout<<"Executing f from D;"<<endl; }
public:
void f()
{
A::_f(); C::_f(); B::_f();
this->_f();
}
};
void main()
{
D obj;
obj.f();
}
Программа выведет на экран:
Executing f from A;
Executing f from C;
Executing f from B;
Executing f from D;
Решение не самое красивое, но других, к сожалению, нет.
Следующая проблема, которая возникает при множественном наследовании – это неоднозначность при множественном наследовании.
class A
{
public:
int a;
int (*b)(); // Если что, это указатель на функцию :)
int f();
int f(int);
int g();
};
class B
{
int a;
int b;
public:
int f();
int g;
int h();
int h(int);
};
class C: public A, public B {};
class D
{
public:
static void fun(C& obj)
{
obj.a = 1; // Error!!!
obj.b(); // Error!!!
obj.f(); // Error!!!
obj.f(1); // Error!!!
obj.g = 1; // Error!!!
obj.h(); obj.h(1); // Только для тех методов, которые идут по одной ветви - всё корректно.
}
};
void main()
{
C obj;
D::fun(obj);
}
Есть два класса – класс А и класс В. Класс С – производная от классов А и В. В классе С мы получаем доступ к членам объекта класса C. Здесь играет следующее правило проверки на неоднозначность: проверка на неоднозначность происходит до проверки на перегрузку, на тип и до проверки на уровень доступа.
Решение проблемы (тоже некрасивое решение): Объединяем два класса, и в производном классе полностью подменяем то, что находится в базовых классах. Получается громоздкий производный класс (ведь мы всё, что относится к базовым классам - подменяем). Когда мы объединяем два класса одним классом, есть еще один недостаток.(см. пример ниже).
Также есть еще один недостаток – когда мы программируем, для объекта какого-либо класса мы должны выделить цель - для чего мы создаем это понятие. Объекты данного класса должны выполнять определенную задачу, и она должна быть только одна. Правило: У одного объекта не должно быть несколько обязанностей. Когда мы объединяем два понятия, это чаще всего приводит к тому, что новое сформированное понятие имеет несколько обязанностей. Такого быть не должно.
Если один объект выступает в разных ролях, мы не должны объединять интерфейс, должны его разносить.
Исключением является ситуация, когда мы объединяем два разных понятия, формируя интерфейс одной обязанности. В этом случае используется следующая схема. У нас есть два класса, и мы формируем новое понятие, используя интерфейс только одного класса. В данном случае идёт наследование только по схеме public только от класса B, от класса A по схеме private. Таким образом, для объектов класса C интерфейс класса A невидим.
class A
{
public:
void f1() { cout<<"Executing f1 from A;"<<endl; }
void f2() { cout<<"Executing f2 from A;"<<endl; }
};
class B
{
public:
void f1() { cout<<"Executing f1 from B;"<<endl; }
void f3() { cout<<"Executing f3 from B;"<<endl; }
};
class C : private A, public B {};
class D
{
public:
void g1(A& obj)
{
obj.f1();
obj.f2();
}
void g2(B& obj)
{
obj.f1();
obj.f3();
}
};
void main()
{
C obj;
D d;
// obj.f1(); Error!!! Неоднозначность
// d.g1(obj); Error!!! Нет приведения к базовому классу при наследовании по схеме private
d.g2(obj);
}
Но здесь здесь есть проблема – проверка на неоднозначность происходит до проверки на схему наследования. Поэтому метод f() для объектов класса C мы вызвать не сможем - это неоднозначность, хотя наследуем по разной схеме.
Что нужно сделать: Нужно в классе С подменить те методы, которые идут по ветви public.
При такой схеме (когда в одном случае мы поддерживаем только один интерфейс) множественное наследование можно использовать.
ВАЖНО! Если в наге нет общей базы (общая база задает интерфейсные методы для производных классов), то подмена должны осуществляться только по одной ветке.
class A
{
public:
void f1() { cout<<"Executing f1 from A;"<<endl; }
void f2() { cout<<"Executing f2 from A;"<<endl; }
};
class B
{
public:
void f1() { cout<<"Executing f1 from B;"<<endl; }
void f3() { cout<<"Executing f3 from B;"<<endl; }
};
class C : public A, public B {};
class D
{
public:
void g1(A& obj) // Для класса D в метод g1() мы передаем класс C, и
{ // на объект класса C ставится ссылка класса А.
obj.f1(); // Соответственно, мы можем вызывать только те методы, которые относятся к классу А.
obj.f2();
}
void g2(B& obj)
{
obj.f1();
obj.f3();
}
};
void main()
{
C obj;
D d;
d.g1(obj); // Мы передаем объект класса C, а не класса A!
d.g2(obj);
}
Вспомним, что при использовании указателя, ссылки на базовый класс - производный класс приводятся к указателю на базовый. Ссылку на базовый мы можем инициализировать объектом на производный. В данном примере это и происходит.
Для класса D в метод g1() мы передаем класс С, и на объект класса С ставится ссылка класса А, соответственно, мы можем вызывать только те методы, которые относятся к классу А. Но объект у нас класса С, не А. В данном примере все нормально - мы не переопределяли никаких методов.
Но представьте, если в классе С мы переопределили метод f1() или f2(). При такой ситуации будут вызываться методы класса А, а нам бы хотелось, чтобы вызывались методы класса С (мы же передаем туда объект класса С).
Когда создаются объекты, нужно создавать в памяти специальные таблицы, которые для данного объекта содержат адреса методов, которые надо вызывать. Соответственно, объект должен иметь указатель на эту таблицу. Понятно, что всегда это использовать неудобно. Это приводит к тому, что занимается память, место (в памяти хранятся эти таблицы), усложняется доступ к методу. Мы сначала должны получить доступ через указатель на таблицу, в таблице мы должны найти нужный нам метод, получить адрес и по этому адресу вызвать метод. Это долгий процесс, тратится очень много времени - на вызов такого метода.
Поэтому разработчик языка Страуструп решил, что этот механизм должен включать сам программист, если ему это нужно. Для этого используется виртуальное наследование, а эти таблицы называются виртуальными таблицами. Если мы в класс добавляем хотя бы один виртуальный метод, то в объект этого класса добавляется еще указатель на виртуальную таблицу. Такие классы называются полиморфными - те, которые содержат виртуальные методы.
Вот как раз пример.
В этом примере, для работы с ссылкой или с указателем на класс А, если ссылка указывает на объект класса инициализированного классом B или указатель указывает на объект класса B, будет вызываться метод производного класса B, который подменяет базовый.
class A
{
public:
virtual void f() { cout<<"Executing f from A;"<<endl; }
};
class B : public A
{
public:
virtual void f() override { cout<<"Executing f from B;"<<endl; } // Перегрузка метода f()
};
class C
{
public:
static void g(A& obj) { obj.f(); } // Вызывается метод f() класса B.
};
void main()
{
B obj;
C::g(obj); // Ссылка класса A на объект класса C
}
Это приводит к тому, что требуется больше памяти, появляется указатель, виртуальная таблица и увеличивается время, так как сначала нужно перейти на таблицу по указателю, там осуществить поиск и потом по указателю вызвать метод. Что это нам дает?
Когда мы разрабатываем программу, основной подход - рекурсивный дизайн. Все изменения мы делаем за счет наследования, не изменяя базовое понятие, делая над ним надстройку. Идея какая: мы использовали одно понятие, потом мы начинаем использовать другое понятие. Мы должны в коде легко подменить одно понятие на другое.
Используя виртуальный подход, работая с ссылкой или указателем на базовый класс, мы легко можем подменить объект. Это основной механизм работы - работа с указателем или ссылкой на базовый класс.
ВАЖНО! Базовый класс задает интерфейс, который в производных мы не должны не сужать не расширять, мы должны использовать этот интерфейс (для того, чтобы можно было подменить один объект другим).
Базовый класс задает интерфейс для производных, но возникает проблема: мы не знаем, как реализовать методы для класса. Можно писать пустое тело для метода, но тогда возникает следующий момент: объекты этого класса мы сможем создавать, но зачем оно нам надо?
Поэтому разработчик языка добавил следующий синтаксис:
class A
{
public:
virtual void f() = Ø; // Определяем метод как чисто виртуальный.
};
Мы можем в базовом классе определить метод как чисто виртуальный, присвоив ноль. Такой метод не надо реализовывать. Класс становится абстрактным. Объекты абстрактного класса создавать нельзя. Абстрактный класс – класс, который содержит хотя бы один чисто виртуальный метод. Если производные классы не будут подменять чисто виртуальный метод, они будут тоже абстрактными.
class A // Абстрактный класс
{
public:
virtual void f() = 0; // Чисто виртуальный метод.
};
class B : public A
{
public:
virtual void f() override { cout<<"Executing f from B;"<<endl; }
};
class C
{
public:
static void g(A& obj) { obj.f(); }
};
void main()
{
B obj;
C::g(obj);
}
Предположим, у нас есть класс B, производный от класса A.
А *p = new В; // Создание объекта класса B. Мы вызываем конструктор класса B для создания конкретного объекта.
.
.
.
delete p;
Класс А – абстрактный, класс В – не абстрактный. Мы можем работать с классом В, вызывая метод. Вступает правило: для класса А мы все методы, которые могут быть подменены в классе В, должны определить с модификатором virtual, чтобы один объект можно было подменить другим.
Мы работаем с указателем на А. Мы подменили один объект другим, но правую часть мы вызываем конкретно. Напрашивается виртуальный конструктор, но конструктор - это не метод объекта, а метод класса, конструктор не может быть виртуальным. Получается проблема с подменой (спойлер: решается с помощью порождающих паттернов).
Возникает еще одна проблема – вызывается деструктор, а деструктор должен вызываться для объекта класса В. Но деструктор вызывается для объекта, поэтому деструктор может быть виртуальным. Соответственно, когда мы определяем какой-либо класс, в любом случае для базового полиморфного класса мы должны определить деструктор. Если мы не можем его определить, то мы делаем деструктор чисто виртуальным.
class A
{
public:
virtual void f() = 0;
virtual ~A() = 0;
};
Но возникает проблема – у нас создается объект какого-то производного класса, по цепочке отрабатывает конструктор, в обратном порядке отрабатывают деструкторы. А мы удалили этот деструктор! Поэтому реализовать этот деструктор мы обязаны. Реализовать как пустой.
class A
{
public:
virtual void f() = 0;
virtual ~A() = 0;
};
A::~A() {}
Задав виртуальный деструктор, каждый производный класс определяет для себя этот деструктор.
Итоги: если у нас полиморфный базовый класс, мы должны подменяемые методы определить, как виртуальные, а так же должны объявить и определить виртуальный деструктор. В дальнейшем мы будем говорить, что базовые классы ВСЕГДА должны быть абстрактными. Как раз механизм с виртуальным деструктором дает нам возможность формировать такой базовый класс. Если мы определили чисто виртуальный деструктор в классе, то этот класс тоже является абстрактным, хотя, казалось бы, есть его реализация.
class A // Абстрактый класс, несмотря на наличие реализации деструктора
{
public:
virtual ~A() = 0; // Чисто виртуальный деструктор
};
A::~A() {} // Реализация деструктора
class B : public A
{
public:
virtual ~B() { cout<<"Class B destructor called;"<<endl; }
};
void main()
{
A* pobj = new B();
delete pobj;
}
С виртуальными методами возникает проблема – виртуальные методы нельзя вызывать в конструкторах и деструкторах.
class A
{
public:
virtual ~A() { cout<<"Class A destructor called;"<<endl; }
virtual void f() { cout<<"Executing f from A;"<<endl; }
};
class B : public A
{
public:
B() { this->f(); }
virtual ~B()
{
cout<<"Class B destructor called;"<<endl;
this->f();
}
void g() { this->f(); }
};
class C : public B
{
public:
virtual ~C() { cout<<"Class C destructor called;"<<endl; }
virtual void f() override { cout<<"Executing f from C;"<<endl; }
};
void main()
{
C obj;
obj.g();
}
Есть класс А, имеющий виртуальный деструктор и виртуальный метод f(). Класс В – конструктор, в котором мы вызываем метод f() и деструктор, в котором мы тоже вызываем метод f(). Метод g() тоже написан специально. У нас есть класс С, который подменяет метод f базового класса.
Создается объект класса C. Прежде чем создастся объект класса C, сначала отрабатывает конструктор класса B, а перед этим A! Но в конструкторе класса B мы вызываем метод f(), но объекта класса C еще нет, поэтому вызовется метод класса A(). Проблема - объекта еще нет. С деструктором наоборот - объекта уже нет, и тоже вызовется метод f().
При вызове метода g() для объекта C будет вызываться для объекта C через указатель this - это указатель на объект класса C.
Правило: мы не должны вызывать виртуальные методы в конструкторах и деструкторах.
Тогда как быть: Мы строим иерархию. Ту часть, которую мы бы хотели вызвать, или, возможно, мы вызовем для объекта, мы выносим в методы protected или private. И они у нас не виртуальные. Виртуальный только интерфейс.
Указать virtual достаточно только в базовом классе, в производных можно не писать. Но глядя на производные классы, без virtual мы не поймем, виртуальный метод или нет. Для лучшего чтения класса, рекомендуется писать слово virtual.
Чтобы понимать, что есть этот метод в базовом классе, как виртуальный, начиная с 11 версии добавлен модификатор override, который говорит о том, что этот метод подменяет метод базового класса.
В современных объектно-ориентированных языка понятие дружбы нет, так как дружба делает зависимыми два класса.
Что такое дружба? Мы можем дать возможность объектам одного класса дать доступ ко всем членам другого класса, прописав его как друг этого класса (как Америка дружит с Грузией). У нас даже наследники не имеют доступа ко всем членам, а друг имеет!
Дружба в программировании - это плохо. В жизни - хорошо. Если можно обойтись без дружбы - надо обходиться без дружбы.
Дружбы не наследуется и дружба не транзитивна (друг моего друга мне не друг).
Есть класс А. Производный от него класс В. У нас есть друг – класс С. Все методы этого класса будут иметь доступ ко всем членам класса А.
class C; // forward объявление
class A
{
private:
void f1() { cout<<"Executing f1;"<<endl; }
friend C; // Мы говорим, что у нас есть друг - класс C. Все методы класса C будут иметь доступ ко всем членам
// класса A
};
class B : public A
{
private:
void f2() { cout<<"Executing f2;"<<endl; }
};
class C
{
public:
static void g1(A& obj) { obj.f1(); } // Метод, который принимает ссылку на объект класса А
// В нём мы имеем доступ к этому методу
static void g2(B& obj) // Получаем ссылку на объект класса B - производный от класса A
{
obj.f1(); // f1() мы можем вызвать - мы имеем доступ ко всем членам класса
// obj.f2(); // Error!!! Имеет доступ только к членам A
}
};
class D : public C
{
public:
// static void g2(A& obj) ( obj.f1(); } // Error!!! Дружба не наследуется
};
void main()
{
A aobj;
C::g1(aobj);
B bobj;
C::g1(bobj);
C::g2(bobj);
}
Что касается виртуальных методов - если мы работаем с ними, возникает подмена. Виртуальный метод protected - лучше так не делать. Виртуальные методы лучше делать только с уровнем доступа public.
class C; // forward объявление
class A
{
protected:
virtual void f() { cout<<"Executing f from A;"<<endl; }
friend C;
};
class B : public A
{
protected:
virtual void f() override { cout<<"Executing f from B;"<<endl; }
};
class C
{
public:
static void g(A& obj) { obj.f(); } // Друг принимает ссылку на объект класса А, а мы передаем объект класса B.
// Будет вызываться метод, который подменяет производный.
};
void main()
{
B bobj;
C::g(bobj);
}
Дружба - это не очень хорошо. Мы вынуждены изменять класс, так как не можем от него отнаследоваться. И будем вынуждены для друга посмотреть все методы, как они работают с объектами этих классов, разобраться, внести в них изменения. Это плохо. Мы можем ограничить дружбу, дать доступ какому либо методу.
class A
{
friend void f(C* pc);
friend void A::f(); // Наш выбор!
friend class B;
};
Другом класса может быть функция, метод из класса или класс. Приоритет надо отдавать варианту, когда мы делаем какой то определённый метод класса другом. Таким образом, если класс С меняется, нам не нужно будет просматривать весь класс А, а только вносить изменения в конкретный метод. Дружба приводит к тому, что если вносить изменения, надо изменять написанный код. Надо проектировать таким образом, чтобы не вносить изменения в написанный код.