Ооп. часть 5. наследование и ещё немного полиморфизма

Конструкторы перемещения

Конструктор перемещения — это специальная функция-член, которая перемещает владение данными существующего объекта в новую переменную без копирования исходных данных. Он принимает в качестве первого параметра ссылку rvalue, а все дополнительные параметры должны иметь значения по умолчанию. Конструкторы перемещения могут значительно повысить эффективность программы при передаче больших объектов.

Компилятор выбирает конструктор перемещения в определенных ситуациях, где объект инициализируется другим объектом того же типа, который будет уничтожен и больше не нуждается в его ресурсах. В следующем примере показан один случай, когда конструктор перемещения выбирается с помощью разрешения перегрузки. В конструкторе, который вызывает , возвращаемое значение является (значение срока действия). Она не назначается какой-либо переменной, поэтому она собирается выйти из области. Чтобы создать мотивацию для этого примера, пусть Box имеет большой вектор строк, который представляет его содержимое. Вместо копирования вектора и его строк конструктор Move «украсть» из поля «Box Expires», чтобы вектор теперь принадлежал новому объекту. Вызов является необходимым, поскольку оба класса и реализуют собственные конструкторы перемещения.

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

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

Дополнительные сведения о написании нетривиальных конструкторов перемещения см. в разделе конструкторы перемещения и операторы присваивания перемещения (C++).

Статическое или раннее связывание (static/early binding)

Давайте разберёмся, как происходит вызов обычных функций и методов классов. Вызов обычных функций и методов происходит через механизм, называемый статическим (статичным) связыванием (static binding) или ранним связыванием (early binding).

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

Когда мы запускаем сборку (building) программы, компилятор просматривает исходный код и превращает все операторы в команды процессора. Допустим, в коде встречается вызов какой-нибудь функции:

someFunction(arg); // some - какой-то

Если это обычная функция (не указатель на функцию), то при вызове используется механизм раннего связывания.

Во время компиляции для кода (определения) функции выделяется память, и назначаются адреса для каждого оператора. Первый адрес в определении (теле функции) является адресом функции. При вызове someFunction, процессор будет переходить на адрес функции и начнёт выполнять тело функции

Самое важное здесь то, что адрес функции назначается во время компиляции, и именно этот адрес используется при вызове функции. Это и есть раннее или статичное связывание

Т.е. имя функции крепко привязано к адресу функции.

Теперь взглянем на небольшой пример:

class Base
{
public:
  void Method ()
  {
    cout << "Базовый класс\n";
  }
};

class Derived : public Base
{};

// внутри main
Base b;
Derived d;
b.Method();
d.Method();

//-------- Вывод:
Базовый класс
Базовый класс

На экран будет выведено две строки Базовый класс. На этапе компиляции память выделяется для двух копий Method – для базового класса и для производного. Оба адреса привязываются к именам методов: Base::Method, Derived::Method. Т.е. когда в коде мы вызываем Method, то вызывается метод, соответствующий типу объекта. Чтобы увидеть, что для каждого объекта вызывается свой метод, давайте переопределим метод Derived::Method:

public:
  void Method ()
  {
    cout << "Производный класс\n";
  }

// внутри main
Base b;
Derived d;
b.Method();
d.Method();

//-------- Вывод:
Базовый класс
Производный класс

Здесь хорошо видно, что вызываются два разных метода. Теперь следующий пример. Определения классов оставим без изменений. Поработаем с указателями:

Замечание
В данном примере пока что не будем обращать внимание, зачем нужно в указатель на Base помещать объект Derived.

Base* b = new Derived;
Derived* d = new Derived;
b->Method();
d->Method();

//-------- Вывод:
Базовый класс
Производный класс

Самое важное здесь то, что компилятор спокойно “проглатывает” тот факт, что указатель на Base указывает на производный класс. Дело в том, что базовый и производный классы являются совместимыми по типу

Во время выполнения программы процессор видит, что b – это указатель на Base

Процессор не обращает внимание, что на самом деле этот указатель указывает на объект Derived. При вызове метода объекта b процессор переходит к адресу Base::Method

Чтобы объект b вызвал метод Derived::Method, нужно привести тип. Например, так:

static_cast<Derived*>(b)->Method();

//-------- Вывод:
Производный класс

Это примеры раннего связывания (статического).

Обратите внимание, что в этом примере мы помещали в указатель на Base объект Derived, а не наоборот:

Derived* d = new Base;
b->Method();
d->Method();

Этот случай нам не интересен. К тому же при компиляции возникнет ошибка – здесь нужно использовать static_cast или dynamic_cast. Практическое применение имеет только случай, когда Base* указывает на Derived. В этом случае появляется возможность использовать полиморфизм (polymorphism).

Что такое ООП

Раньше программистам приходилось весь функционал программы записывать в одном файле. Что в будущем неизбежно приводило к путанице из-за нескольких сотен и даже тысяч строк. А с приходом классов появилась возможность отделять любую часть программы в отдельный файл.

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

В ООП входит такие свойства:

Инкапсуляция — это возможность задавать разную область видимости определенной части класса .

Наследование — это свойство создавать новый класс на базе старого

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

Полиморфизм — возможность создать объекты с одинаковым интерфейсом, но с разной их реализацией. Например, есть три класса , и

У каждого из них есть метод , который вычисляет площадь фигуры. Но для каждого класса функция реализована по-разному.

SquarePlis() {

  square = a * a;  // для квадрата

  square = 1 / 2 * h * a;  // для треугольника

  square = 3.14 * r * r;  // для круга

}

1
2
3
4
5
6
7
8
9

SquarePlis(){

 
  square=a*a; // для квадрата

 
 square=12*h*a; // для треугольника

 
  square=3.14*r*r; // для круга

 
}

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

Переопределение методов в C#

Чтобы переопределить метод в классе-наследнике, этот метод определяется с в классе модификатором . В отличие от перегрузки, переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе. Например, рассмотрим следующие классы:

class Person
{
    public string Name { get; set; }
    public Person(string name)
    {
        Name = name;
    }
    public virtual void Display()
    {
        Console.WriteLine(Name);
    }
}
class Employee : Person
{
    public string Company { get; set; }
    public Employee(string name, string company) : base(name)
    {
        Company = company;
    }
}

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

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

    Person p1 = new Person("Вася");
    p1.Display(); // вызов метода Display из класса Person

    Employee e1 = new Employee("Билл", "Microsoft");
    e1.Display(); // вызов метода Display из класса Person

В консоли мы увидим следующее:

Вася

Билл

Если же нас не устраивает функционал метода в родителе, то мы можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором , который имеет то же самое имя и набор параметров:

class Employee : Person
{
    public string Company { get; set; }
    public Employee(string name, string company): base(name)
    {
        Company = company;
    }

    public override void Display()
    {
        Console.WriteLine($"{Name} работает в {Company}");
    }
}

Пример использования:

    Person p1 = new Person("Bill");
    p1.Display(); // вызов метода Display из класса Person

    Employee p2 = new Employee("Tom", "Microsoft");
    p2.Display(); // вызов метода Display из класса Employee

В консоли будут следующие строки:

Bill

Tom работает в Microsoft

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

При переопределении виртуальных методов в C# необходимо учитывать следующие ограничения:

  1. Виртуальный и переопределенный методы должны иметь один и тот же . Если виртуальный метод определен с помощью модификатора , то и переопредленный метод также должен иметь модификатор .
  2. Нельзя переопределить или объявить виртуальным статический метод.

Чистые виртуальные функции с определениями

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

#include <iostream>
#include <string>

class Animal // это абстрактный родительский класс
{
protected:
std::string m_name;

public:
Animal(std::string name)
: m_name(name)
{
}

std::string getName() { return m_name; }
virtual const char* speak() = 0; // присваивание значения «= 0» говорит о том, что эта функция является чистой виртуальной функцией
};

const char* Animal::speak() // несмотря на то, что вот здесь находится её определение
{
return «buzz»;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <iostream>
#include <string>
 

classAnimal// это абстрактный родительский класс

{

protected

std::stringm_name;

public

Animal(std::stringname)

m_name(name)

{

}

std::stringgetName(){returnm_name;}

virtualconstchar*speak()=;// присваивание значения «= 0» говорит о том, что эта функция является чистой виртуальной функцией

};

constchar*Animal::speak()// несмотря на то, что вот здесь находится её определение

{

return»buzz»;

}

В этом случае speak() по-прежнему считается чистой виртуальной функцией (хотя позже мы её определили), а Animal по-прежнему считается абстрактным родительским классом (и, следовательно, объекты этого класса не могут быть созданы). Любой класс, который наследует класс Animal, должен переопределить метод speak() или он также будет считаться абстрактным классом.

При определении чистой виртуальной функции, её тело (определение) должно быть записано отдельно (не встроено).

Это полезно, когда вы хотите, чтобы дочерние классы имели возможность переопределять виртуальную функцию или оставить её реализацию по умолчанию (которую предоставляет родительский класс). В случае, если дочерний класс доволен реализацией по умолчанию, он может просто вызвать её напрямую. Например:

#include
#include

class Animal // это абстрактный родительский класс
{
protected:
std::string m_name;

public:
Animal(std::string name)
: m_name(name)
{
}

std::string getName() { return m_name; }
virtual const char* speak() = 0; // обратите внимание, speak() является чистой виртуальной функцией
};

const char* Animal::speak()
{
return «buzz»; // реализация по умолчанию
}

class Dragonfly: public Animal
{

public:
Dragonfly(std::string name)
: Animal(name)
{
}

virtual const char* speak() // этот класс уже не является абстрактным, так как мы переопределили функцию speak()
{
return Animal::speak(); // используется реализация по умолчанию класса Animal
}
};

int main()
{
Dragonfly dfly(«Barbara»);
std::cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#include
#include

classAnimal// это абстрактный родительский класс

{

protected

std::stringm_name;

public

Animal(std::stringname)

m_name(name)

{

}

std::stringgetName(){returnm_name;}

virtualconstchar*speak()=;// обратите внимание, speak() является чистой виртуальной функцией

};

constchar*Animal::speak()

{

return»buzz»;// реализация по умолчанию

}

classDragonflypublicAnimal

{

public

Dragonfly(std::stringname)

Animal(name)

{

}

virtualconstchar*speak()// этот класс уже не является абстрактным, так как мы переопределили функцию speak()

{

returnAnimal::speak();// используется реализация по умолчанию класса Animal

}

};

intmain()

{

Dragonfly dfly(«Barbara»);

std::cout

Результат выполнения программы:

Неоднозначность имен

Множественное наследование предоставляет возможность наследования имен по нескольким путям. Имена членов класса в этих путях не обязательно должны быть уникальными. Эти конфликты имен называются неоднозначностями.

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

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

Рассмотрим предыдущий пример. Поскольку имя является членом обоих классов и , компилятор не может определить, какая переменная обозначает функцию, которую необходимо вызвать. Доступ к члену неоднозначен, если он может ссылаться на несколько функций, объектов, типов или перечислителей.

Компилятор определяет неоднозначности, выполняя тесты в указанном порядке.

  1. Если доступ к имени неоднозначен (как описано выше), создается сообщение об ошибке.

  2. Если перегруженные функции однозначны, они разрешаются.

  3. Если доступ к имени нарушает разрешение доступа к членам, создается сообщение об ошибке. (Дополнительные сведения см. в разделе Управление доступом к членам.)

Если выражение приводит к неоднозначности в результате наследования, его можно разрешить вручную, указав вместо данного имени имя класса. Чтобы выполнить компиляцию в предыдущем примере без неоднозначностей, можно использовать следующий код.

Примечание

Если объявлен , могут возникнуть ошибки, если сослаться на в области . Однако ошибка не выдается, если не внести неквалифицированную ссылку на в области .

Доминирование

Через граф наследования можно достичь несколько имен (функции, объекта или перечислителя). С невиртуальными базовыми классами такие случаи неоднозначны. Они также неоднозначны с виртуальными базовыми классами, если одно из имен не доминирует над другими.

То или иное имя доминирует над другим, если оно определено в обоих классах и один класс является производным от другого. Доминирующее имя — это имя в производном классе; оно используется тогда, когда в противном случае возникла бы неоднозначность, как показано в следующем примере.

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

Явные и неявные преобразования указателей и ссылок в типы класса могут приводить к неоднозначности. На следующем рисунке «Неоднозначное преобразование указателей в базовые классы» показано следующее:

Объявление объекта типа .

Результат применения оператора взятия адреса ( & ) к этому объекту

Обратите внимание, что оператор взятия адреса всегда возвращает базовый адрес объекта.

Результат явного преобразования указателя, полученного с помощью оператора взятия адреса, в тип базового класса. Обратите внимание, что приведение адреса объекта к типу не всегда предоставляет компилятору достаточно информации о том, какой из типов подобъектов типа следует выбрать; в данном случае существуют два подобъекта.

Неоднозначное преобразование указателей в базовые классы

Преобразование в тип (указатель на ) является неоднозначным, поскольку нет способа определить, какой подобъект типа является правильным

Обратите внимание, что неоднозначности можно избежать, явно указав используемый подобъект, как показано ниже:

Неоднозначности и виртуальные базовые классы

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

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


Виртуальное и невиртуальное наследование

На этом рисунке доступ к любому члену класса через невиртуальная базовые классы вызывает неоднозначность; у компилятора нет сведений, поясняющих, нужно ли использовать вложенный объект, связанный с , или вложенный объект, связанный с . Однако если задано как виртуальный базовый класс, вопросов о том, к какому из вложенных объектов осуществляется доступ, не возникает.

Виртуальные базовые классы

Чтобы использовать базовый класс совместно, просто вставьте ключевое слово в список наследования производного класса. Это создает так называемый виртуальный базовый класс, что означает, что существует только один базовый объект. Этот базовый объект используется всеми объектами в дереве наследования и создается только один раз. Вот пример (для простоты без конструкторов), показывающий, как использовать ключевое слово для создания общего базового класса:

Теперь, когда вы создаете объект класса , вы получите только одну копию в объекте , которая будет использоваться как частью , так и частью .

Однако это приводит к еще одной проблеме: если и используют общий базовый класс , кто несет ответственность за его создание? Как оказалось, ответ – . За создание отвечает конструктор . Следовательно, это единственный раз, когда классу разрешено напрямую вызывать конструктор, не являющийся непосредственно родительским:

На этот раз наш предыдущий пример:

дает результат:

Как видите, создается только один раз.

Есть несколько деталей, которые стоит упомянуть.

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

Во-вторых, обратите внимание, что у конструкторов и всё еще есть вызовы конструктора. При создании экземпляра эти вызовы конструктора просто игнорируются, потому что за создание отвечает , а не или

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

В-третьих, если класс наследует один или несколько классов, имеющих виртуальных родителей, за создание виртуального базового класса отвечает наиболее производный класс. В этом случае наследует и , оба из которых имеют виртуальный базовый класс . , самый производный класс, отвечает за создание

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

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

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

Пример использования классов

Давайте используем созданный класс на практике создав карточку об одном работнике, например Иване. Класс разместим в файле , который подключим к главному файлу таким образом .

// workers.h
#include <string>
class Worker {
public:
void discover_avarage_AP () {
double answer = 0;

for (int i = 0; i < 6; i++) {
answer += academic_performance;
}
set_avarage_AP(answer);
// вместо avarage_AP = answer / 6;
}

void set_avarage_AP (double score) {
avarage_AP = score / 6;
}
// здесь находятся set и get функции
double get_avarage_AP () {
return avarage_AP;
}
void set_name(string a) {
// считываем имя
name = a;
}
void set_academic_performance (vector v) {
// заполняем 6 месячныю успеваемость
for (int i = 0; i < 6; i++) { academic_performance = v;
}
}
string get_name () {
// выводим имя
return name;
}
// конец set и get функций

private:
// средняя успеваемость
int avarage_AP;
string name; // имя
// успеваемость за 6 месяцев
int academic_performance;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// workers.h
#include <string>

classWorker{

public

voiddiscover_avarage_AP(){

doubleanswer=;

for(inti=;i<6;i++){

answer+=academic_performancei;

}

set_avarage_AP(answer);

// вместо avarage_AP = answer / 6;

}

voidset_avarage_AP(doublescore){

avarage_AP=score6;

}

// здесь находятся set и get функции

doubleget_avarage_AP(){

returnavarage_AP;

}

voidset_name(stringa){

// считываем имя

name=a;

}

voidset_academic_performance(vectorv){

// заполняем 6 месячныю успеваемость

for(inti=;i<6;i++){academic_performancei=vi;

}

}

stringget_name(){

// выводим имя

returnname;

}

// конец set и get функций

private

// средняя успеваемость

intavarage_AP;

stringname;// имя

// успеваемость за 6 месяцев

intacademic_performance6;

};

В строках 19-34: находятся и функции для инициализации наших свойств. Вот какие именно:

  • — считывает имя работника.
  • — считывает успеваемость на работе за шесть месяцев.

Функции set имеют такое же название, только вместо get — set.

А вот как выглядит main.cpp

// main.cpp
#include <iostream>
#include <vector>
#include «workers.h»

using namespace std;

int main() {
Worker employee;

string name;
vector <int> average_balls;

cout << «Your name: «; cin >> name;
cout << «Your academic performance for 6 months: » << endl;

for (int i = 0; i < 6; i++) {
int one_score;
cout << i + 1 << «) «; cin >> one_score;
average_balls.push_back(one_score);
}

employee.set_name(name);
employee.set_academic_performance(average_balls);

employee.discover_avarage_AP();

cout << endl << employee.get_name() << endl;
cout << «Avarage academic performance: » << employee.get_avarage_AP() << endl;

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

// main.cpp
#include <iostream>
#include <vector>
#include «workers.h»
 

usingnamespacestd;

intmain(){

Worker employee;

stringname;

vector<int>average_balls;

cout<<«Your name: «;cin>>name;

cout<<«Your academic performance for 6 months: «<<endl;

for(inti=;i<6;i++){

intone_score;

cout<<i+1<<«) «;cin>>one_score;

average_balls.push_back(one_score);

}

employee.set_name(name);

employee.set_academic_performance(average_balls);

employee.discover_avarage_AP();

cout<<endl<<employee.get_name()<<endl;

cout<<«Avarage academic performance: «<<employee.get_avarage_AP()<<endl;

return;

}

Для создания объекта  мы указали класс .

  • В строках 14 — 21: считываем пользовательские данные.
  • В строках 23- 24: отсылаем полученные данные классу (функциями set).
  • Вычисляем среднюю успеваемость вызвав функцию в строке 26.
  • В строках 28 — 29: выводим все свойства: имя, фамилию, возраст, средний балл.

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

employee.cpp

Your name: Иван

Your academic performance for 6 months:
1) 3
2) 4
3) 5
4) 5
5) 3
6) 4

Иван
Avarage academic performance: 4

Process returned 0 (0x0) execution time : 0.010 s
Press any key to continue.

Основы наследования

В C++ новый тип может наследовать все поля и методы другого типа. Для этого достаточно указать структуру или класс в списке базовых типов. Такой приём используется в SFML при объявлении классов фигур:

Что означает public перед именем базового типа? Во-первых внешний код может передать RectangleShape в функцию, принимающую ссылку на Shape, то есть возможен так называемы upcast от более низкого (и более конкретного) типа RectangleShape к более высокому (и более абстрактному) типу Shape:

Во-вторых из-за public наследования все унаследованные поля и методы сохраняют свой уровень доступ: приватные остаются приватными, публичные остаются публичными. А если бы мы наследовали Shape с ключевым словом private, то уровень доступа стал бы ниже: все методы и поля стали бы приватными:

Объявление

Объявляется с использованием ключевого слова extends:

public class Vehicle {
    protected String licensePlate = null;

    public void setLicensePlate(String license) {
        this.licensePlate = license;
    }
}
public class Car extends Vehicle {
    int numberOfSeats = 0;

    public String getNumberOfSeats() {
        return this.numberOfSeats;
    }
}

Класс Car в этом примере расширяет класс Vehicle, то есть Car наследуется от Vehicle. Поскольку Car расширяет Vehicle, защищенное поле licensePlate из Vehicle наследуется Car. Когда licensePlate наследуется, оно становится доступным внутри экземпляра Car.

В поле licensePlate на самом деле не ссылаются из класса Car в приведенном выше коде, но можно, если мы захотим:

public class Car extends Vehicle {
    int numberOfSeats = 0;

    public String getNumberOfSeats() {
        return this.numberOfSeats;
    }

    public String getLicensePlate() {
        return this.licensePlate;
    }
}

Ссылка происходит внутри метода getLicensePlate(). Во многих случаях имело бы смысл поместить этот метод в класс Vehicle, где находится поле licensePlate.

Основы полиморфизма: виртуальные методы и их перегрузка

SFML использует ещё одну идиому C++: виртуальные методы. Ключевые слова , , относятся именно к этой идиоме. Например, в SFML определяется класс Drawable, который обозначает “сущность, которую можно нарисовать”. Все рисуемые классы SFML, включая , , , прямо или косвенно наследуются от .

Зачем это надо? Дело в том, что метод класса принимает параметр типа . Тем не менее, этот метод успешно рисует любые типы объектов: спрайты, фигуры, тексты. Он не выполняет проверок — он просто настраивает состояние рисования (RenderStates) и вызывает метод у сущности, которая является .

Виртуальный метод вызывается косвенно: если класс , унаследованный от , переопределил метод, а потом был передан как параметр типа , то вызов метода draw всё равно приведёт к вызову переопределённого метода , а не метода ! С обычными (не виртуальными) методами такого не происходит: если бы мы убрали слово из объявления , то вызов метода у параметра типа всегда приводил бы к вызову , даже если реальный тип объекта, скрытого за этим параметром, совсем другой.

Другими словами, RenderWindow и RectangleShape не знают, что они работают друг с другом, но тем не менее каждый вызывает правильный метод другого класса!

Что может пойти не так?

Воображать:

  • имеет некоторую базовую особенность.

  • добавляет к нему какой-то классный массив данных (например)

  • добавляет к нему некоторую классную функцию, такую как шаблон наблюдателя (например, на ).

  • наследует от и , а значит, и от .

При нормальном наследовании изменение из неоднозначно, и это должно быть решено. Даже если это так , внутри есть два , так что вам лучше запомнить это и обновить их одновременно.

С виртуальным наследованием изменение из -это нормально… но… Допустим, у вас есть . Через его интерфейс вы подключили наблюдателя. И через его интерфейс вы обновляете прохладный массив, который имеет побочный эффект прямого изменения …

Поскольку изменение выполняется непосредственно (без использования метода виртуального доступа), наблюдатель «listening»- не будет вызван , потому что код, реализующий прослушивание, находится в , а об этом не знает…

Рейтинг
( Пока оценок нет )
Понравилась статья? Поделиться с друзьями:
Все про сервера
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: