Skip to content

Лекция 06. Наследование.

chrislvt edited this page Sep 8, 2020 · 1 revision

Наследование

Базовый класс выделяют в следующих случаях:

  • Общая схема исполнения разных объектов.
  • В объектах один и тот же набор методов.
  • Имеются два разных класса с разными методами. Если у методов похожая реализация, то выделяем базовый класс.
  • У нас имеется два разных класса. Если в дальнейшем они будут участвовать вместе, лучше сделать для них базовый класс уже на этом этапе разработки. Это нужно сделать, чтобы в дальнейшем нам было легко модифицировать программу.

Разбиваем класс в следующих случаях:

  • Если один объект исполняет разные роли.
  • Два множества методов используются в разной манере.
  • Методы между собой никак не связаны .
  • Одна сущность, но используется в разных частях программы.

На классах возможно множественное наследование.

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

Язык С++ – это практически единственный язык, в котором присутствует множественное наследование.

Преимущества множественного наследования

Какие преимущества множественного наследования? Пойдем методом от противного - попробуем избавиться от множественного наследования, и представим, что было бы.

  1. Представим такую ситуацию: выстраиваем вертикальную иерархию, класс C наследуется от класса B, а B наследуется от класса A. В этом случае, в класс A мы должны вынести много того, что к понятию класса А не относится. Не совсем логично.

    В случае со множественным наследованием (рис. ниже), мы четко разделяем понятия A и B. Такой подход уменьшает иерархию.

  2. Второй момент (опять не используем множественное наследование). Мы можем не выносить что-то в базовый класс, а один из классов включить как подобъект, то есть не использовать наследование. Пусть С - производная от класса А и включает подобъект класса В. Тоже возникнет проблема - не будем иметь доступа к защищенным полям класса Б (нам придется делать это через методы класса Б) + придется протаскивать интерфейс для класса С класса Б.

Неявное преобразование

ВАЖНО! В языке С++ происходит неявное преобразование от указателя объекта производного класса к указателю на объект базового класса. То же самое касается ссылок.

Рассмотрим пример ниже:

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

Можно использовать 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.

Аналогично для этой схемы:

Подмена метода рассматривается в примере ниже.

Пример. Подмена метода f()

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. И они у нас не виртуальные. Виртуальный только интерфейс.

Про override

Указать 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;
};

Другом класса может быть функция, метод из класса или класс. Приоритет надо отдавать варианту, когда мы делаем какой то определённый метод класса другом. Таким образом, если класс С меняется, нам не нужно будет просматривать весь класс А, а только вносить изменения в конкретный метод. Дружба приводит к тому, что если вносить изменения, надо изменять написанный код. Надо проектировать таким образом, чтобы не вносить изменения в написанный код.

Clone this wiki locally