Особенности динамического связывания

Последнее обновление: 16.03.2023

Динамическое связывание при передаче параметров

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

#include <iostream>
 
class Person
{
public:
    Person(std::string name): name{name} { }
    virtual void print() const  // виртуальная функция
    {
        std::cout << name << std::endl;
    }
    std::string getName() const {return name;}
private:
    std::string name;
};
class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company}{ }
    void print() const override // функция переопределена
    {
        std::cout << getName() << " (" << company << ")" << std::endl;
    }
private:
    std::string company;
};

void printPerson(const Person& person) 
{
    person.print();
}
 
int main()
{
    Person tom {"Tom"};
    Employee bob {"Bob", "Microsoft"};
    printPerson(tom);   // Tom
    printPerson(bob);   // Bob (Microsoft)
}

В данном случае функция printPerson в качестве параметра принимает константную ссылку на объект типа Person, коим в реальности также может быть объект Employee. Поэтому при вызове функции print программа будет динамически решать, какую именно реализацию функции вызвать.

Динамическое связывание и коллекции

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

#include <iostream>
 
class Person
{
public:
    Person(std::string name): name{name} { }
    virtual void print() const  // виртуальная функция
    {
        std::cout << name << std::endl;
    }
    std::string getName() const {return name;}
private:
    std::string name;
};
class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company}{ }
    void print() const override // функция переопределена
    {
        std::cout << getName() << " (" << company << ")" << std::endl;
    }
private:
    std::string company;
};

void printPerson(const Person& person) 
{
    person.print();
}
 
int main()
{
    Person tom {"Tom"};
    Employee bob {"Bob", "Microsoft"};
    Employee sam {"Sam", "Google"};
    Person people[]{tom, bob, sam};
    for(const auto& person: people)
    {
        person.print();
    }
}

Здесь массив people хранит объекты Person, в качестве которых также могут выступать объекты Employee. Однако при такой организации каждый объект Employee, который помещается в массив, преобразуется в объект Person. В итоге при переборе такого массива вызывается функция print из класса Person:

Tom
Bob
Sam

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

int main()
{
    Person tom {"Tom"};
    Employee bob {"Bob", "Microsoft"};
    Employee sam {"Sam", "Google"};
    Person* people[]{&tom, &bob, &sam}; // массив указателей
    for(const auto& person: people)
    {
        person->print();
    }
}

Здесь массив хранит адреса всех объектов, соотвественно получим совсем другой вывод:

Tom
Bob (Microsoft)
Sam (Google)

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

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

#include <iostream>
#include <memory>

class Person
{
public:
    Person(std::string name): name{name} { }
     ~Person()
    {
        std::cout << "Person " << name << " deleted" << std::endl;
    }
    virtual void print() const  // виртуальная функция
    {
        std::cout << name << std::endl;
    }
    std::string getName() const {return name;}
private:
    std::string name;
};
class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company}{ }
    ~Employee()
    {
        std::cout << "Employee " << getName() << " deleted" << std::endl;
    }
    void print() const override // функция переопределена
    {
        std::cout << getName() << " (" << company << ")" << std::endl;
    }
private:
    std::string company;
};
void printPerson(const Person& person)
{
    person.print();
}
 
int main()
{
    std::unique_ptr<Person> sam { std::make_unique<Employee>("Sam", "Google") };
    sam->print();
}

Здесь переменная sam представляет smart-указатель std::unique_ptr на объект Person, который автоматически выделяет память для одного объекта Employee. Поскольку объект Employee - это одновременно объект Person, то никакой проблемы в данном случае не будет.

Для обоих классов определены деструкторы, который просто выводят строку на консоль. То есть мы ожидаем, что после завершения функции main объект указателя sam будет удален, и будут выполняться деструкторы классов Employee и Person (ведь у нас объект Employee). Но что нам покажется в реальности консоль:

Sam (Google)
Person Sam deleted

А консоль нам показывает, что для объекта по указателю sam вызывается только деструктор класса Person, хотя объект то у нас Employee. Это может иметь неприятные последствия, особенно, если в конструкторе Employee выделяем память, а в деструткоре Employee освобождаем. Чтобы все-таки деструктор Employee вызывался, нам надо определить деструктор базового класса как виртуальный. Итак, изменим код деструктора класса Person, добавив перед ним слово virtual:

virtual ~Person()
{
    std::cout << "Person " << name << " deleted" << std::endl;
}

Весь остальной код остается прежним. И теперь мы получим другой консольный вывод:

Sam (Google)
Employee Sam deleted
Person Sam deleted

Таким образом, теперь вызывается деструктор обоих классов.

Переопределение спецификатора доступа

Стоит отметить, что виртуальные функции позволяют нам обойти ограничения на доступ к функциям. Например, сделаем функцию print в классе Employee приватной:

#include <iostream>
 
class Person
{
public:
    Person(std::string name): name{name}
    { }
    virtual void print() const  // виртуальная функция
    {
        std::cout << "Name: " << name << std::endl;
    }
private:
    std::string name;
};
class Employee: public Person
{
public:
    Employee(std::string name, std::string company): Person{name}, company{company}
    { }
private:
    void print() const override // функция переопределена
    {
        Person::print();
        std::cout << "Works in " << company << std::endl;
    }
    std::string company;
};
 
int main()
{
    Employee bob {"Bob", "Microsoft"};
    Person* person {&bob};
    //bob.print();            // так нельзя - функция приватная
    person->print();        // а так можно
}

Поскольку теперь функция print в Employee приватная, мы не можем вне класса вызвать эту функцию напрямую для объекта Employee:

Employee bob {"Bob", "Microsoft"};
bob.print();	// так нельзя - функция приватная

Зато можем вызвать эту реализацию через указатель на тип Person:

Person* person {&bob};
person->print();        // а так можно
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850