Если необходимо обеспечить динамическое связывание при передаче параметров в функцию, то такой параметр должен представлять ссылку или указатель на объект базового типа:
#include <iostream>
#include <string>
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>
#include <string>
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>
#include <string>
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>
#include <string>
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(); // а так можно