Moștenirea
Moștenirea (în engleză: inheritance) este unul dintre conceptele cheie ale programării orientate pe obiecte și permite unui clasă derivată să moștenească funcționalitatea unei clase de bază.
De ce avem nevoie de moștenire?
Să analizăm o situație simplă. Presupunem că avem două clase: una care reprezintă o persoană și alta un angajat al unei companii:
class Person
{
public:
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
std::string name;
unsigned age;
};
class Employee
{
public:
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
std::string name;
unsigned age;
std::string company;
};
Adică putem spune că un angajat ESTE o persoană. Deoarece un angajat are practic toate atributele unei persoane (nume, vârstă) și adaugă câteva proprii (companie). De aceea, în acest caz este mai bine să folosim mecanismul de moștenire. Moștenim clasa Employee din clasa Person:
class Person
{
public:
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
std::string name; // nume
unsigned age; // vârstă
};
class Employee : public Person
{
public:
std::string company; // companie
};
Pentru a stabili relația de moștenire, după numele clasei se pune două puncte, apoi specificatorul de acces și numele clasei din care dorim să moștenim. În acest context, clasa Person se mai numește clasă de bază (sau superclasă, clasă-părinte), iar Employee – clasă derivată (subclasă, clasă moștenitoare).
Specificatorul de acces permite să indicăm la ce membri ai clasei de bază va avea acces clasa derivată. În acest caz se folosește public:
public:
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
std::string name; // nume
unsigned age; // vârstă
care permite utilizarea în clasa derivată a tuturor membrilor publici ai clasei de bază. Dacă nu folosim specificatorul de acces, clasa Employee nu va ști nimic despre variabilele name și age și funcția print.
După stabilirea moștenirii putem elimina din clasa Employee acele variabile care deja sunt definite în clasa Person. Să folosim ambele clase:
#include <iostream>
class Person
{
public:
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
std::string name; // nume
unsigned age; // vârstă
};
class Employee : public Person
{
public:
std::string company; // companie
};
int main()
{
Person tom;
tom.name = "Tom";
tom.age = 23;
tom.print(); // Name: Tom Age: 23
Employee bob;
bob.name = "Bob";
bob.age = 31;
bob.company = "Microsoft";
bob.print(); // Name: Bob Age: 31
}
Astfel, printr-o variabilă de tipul clasei Employee putem accesa toți membrii publici ai clasei Person.
Constructori
Acum să facem toate variabilele private și să adăugăm constructori pentru a le inițializa. Este important de reținut că constructorii nu se moștenesc. Dacă clasa de bază are doar constructori cu parametri, clasa derivată trebuie să apeleze explicit unul dintre aceștia în constructorul său:
#include <iostream>
class Person
{
public:
Person(std::string name, unsigned age)
{
this->name = name;
this->age = age;
}
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
private:
std::string name;
unsigned age;
};
class Employee: public Person
{
public:
Employee(std::string name, unsigned age, std::string company): Person(name, age)
{
this->company = company;
}
private:
std::string company;
};
int main()
{
Person person {"Tom", 38};
person.print(); // Name: Tom Age: 38
Employee employee {"Bob", 42, "Microsoft"};
employee.print(); // Name: Bob Age: 42
}
După lista parametrilor constructorului clasei derivate, urmează, după două puncte, apelul constructorului clasei de bază, căruia i se transmit valorile parametrilor n și a.
Employee(std::string name, unsigned age, std::string company): Person(name, age)
{
this->company = company;
}
Dacă nu am apela constructorul clasei de bază, aceasta ar fi o eroare.
Ieșirea în consolă a programului:
Name: Tom Age: 38
Name: Bob Age: 42
Astfel, în linia:
Employee employee {"Bob", 42, "Microsoft"};
mai întâi va fi apelat constructorul clasei de bază Person, căruia i se vor transmite valorile "Bob" și 42. Astfel vor fi stabilite numele și vârsta. Apoi va fi executat constructorul propriu al clasei Employee, care va stabili compania.
De asemenea, am fi putut defini constructorul Employee astfel, folosind liste de inițializare:
Employee(std::string name, unsigned age, std::string company): Person(name, age), company(company)
{
}
Conectarea constructorului clasei de bază
În exemplele de mai sus, constructorul Employee diferă de cel al lui Person doar printr-un parametru – company. Toți ceilalți parametri din Employee sunt transmiși către Person. Totuși, dacă am avea o corespondență completă a parametrilor între cele două clase, atunci nu ar mai fi necesar să definim un constructor separat pentru Employee, ci am putea conecta constructorul clasei de bază:
#include <iostream>
class Person
{
public:
Person(std::string name, unsigned age)
{
this->name = name;
this->age = age;
}
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
private:
std::string name; // nume
unsigned age; // vârstă
};
class Employee: public Person
{
public:
using Person::Person; // conectăm constructorul clasei de bază
};
int main()
{
Person person {"Tom", 38};
person.print(); // Name: Tom Age: 38
Employee employee {"Bob", 42};
employee.print(); // Name: Bob Age: 42
}
Aici, în clasa Employee, conectăm constructorul clasei de bază cu ajutorul cuvântului cheie using:
using Person::Person;
Astfel, clasa Employee va avea de fapt același constructor ca Person, cu aceiași doi parametri. Și acest constructor îl putem apela pentru a crea un obiect Employee:
Employee employee {"Bob", 42};
Definirea constructorilor de copiere
Când definim un constructor de copiere într-o clasă derivată, trebuie să apelăm în el constructorul de copiere al clasei de bază. De exemplu, să adăugăm constructori de copiere în clasele Person și Employee:
#include <iostream>
class Person
{
public:
// constructorul de copiere al clasei Person
Person(const Person& person)
{
name = person.name;
age = person.age;
}
Person(std::string name, unsigned age)
{
this->name = name;
this->age = age;
}
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
private:
std::string name;
unsigned age;
};
class Employee: public Person
{
public:
Employee(std::string name, unsigned age, std::string company): Person(name, age)
{
this->company = company;
}
// constructorul de copiere al clasei Employee
// apelăm constructorul de copiere al clasei de bază
Employee(const Employee& employee): Person(employee)
{
company = employee.company;
}
private:
std::string company;
};
int main()
{
Employee tom{"Tom", 38, "Google"};
Employee tomas{tom}; // apelăm constructorul de copiere
tomas.print(); // Name: Tom Age: 38
}
În constructorul de copiere al clasei derivate Employee, apelăm constructorul de copiere al clasei de bază Person:
Employee(const Employee& employee): Person(employee)
{
company = employee.company;
}
În acest caz, constructorului de copiere Person i se transmite obiectul employee, în care vor fi stabilite variabilele name și age. În propriul constructor al clasei Employee este setată doar variabila company.
Moștenirea destructorilor
Distrugerea unui obiect al clasei derivate poate implica atât destructorul propriu al clasei derivate, cât și pe cel al clasei de bază. De exemplu, să definim în ambele clase destructorii:
#include <iostream>
class Person
{
public:
Person(std::string name, unsigned age)
{
this->name = name;
this->age = age;
std::cout << "Person created" << std::endl;
}
~Person()
{
std::cout << "Person deleted" << std::endl;
}
void print() const
{
std::cout << "Name: " << name << "\tAge: " << age << std::endl;
}
private:
std::string name;
unsigned age;
};
class Employee: public Person
{
public:
Employee(std::string name, unsigned age, std::string company): Person(name, age)
{
this->company = company;
std::cout << "Employee created" << std::endl;
}
~Employee()
{
std::cout << "Employee deleted" << std::endl;
}
private:
std::string company;
};
int main()
{
Employee tom{"Tom", 38, "Google"};
tom.print();
}
În ambele clase, destructorul doar afișează un mesaj. În funcția main este creat un obiect Employee, dar la terminarea programului se va apela atât destructorul clasei derivate, cât și al celei de bază:
Person created
Employee created
Name: Tom Age: 38
Employee deleted
Person deleted
După cum vedem în consola de ieșire, la crearea obiectului Employee este apelat mai întâi constructorul clasei de bază Person, apoi cel propriu al lui Employee. La ștergerea obiectului Employee, procesul se desfășoară invers – mai întâi se apelează destructorul clasei derivate, apoi cel al clasei de bază. Astfel, dacă în destructorul clasei de bază se eliberează memorie, acest lucru va fi efectuat în mod obligatoriu la ștergerea obiectului derivat.
Interzicerea moștenirii
Uneori, moștenirea dintr-o clasă nu este dorită. Cu ajutorul specificatorului final, putem interzice moștenirea:
class Person final
{
};
După aceasta, nu vom mai putea moșteni alte clase din Person. De exemplu, dacă încercăm să scriem următorul cod, vom întâlni o eroare:
class Employee : public Person
{
};