Hiện tại, chúng ta đã nói về kế thừa là gì theo một ý nghĩa khá trừu tượng, hãy cùng tìm hiểu về cách thức nó được sử dụng trong C++.

Kế thừa trong C++ diễn ra giữa các lớp (class). Trong một mối quan hệ kế thừa (quan hệ is-a, tức là “là một”), class mà đang được kế thừa được gọi là parent class (lớp cha), base class (lớp cơ sở), hoặc super class; còn class mà đang thực hiện việc kế thừa được gọi là child class (lớp con), derived class (lớp được dẫn xuất), hoặc subclass.

Trong biểu đồ trên, Fruit là parent class/super class (lớp cha), còn Apple và Banana là các subclass (lớp con).

Trong biểu đồ này, Triangle vừa là con (của Shape) và vừa là cha (của Right Triangle).

Một child class – lớp con sẽ kế thừa các hành vi (các hàm thành viên) và các thuộc tính (các biến thành viên) từ parent class – lớp cha (tùy thuộc vào một số hạn chế truy cập mà chúng ta sẽ nói tới trong một bài học tương lai).

Các biến và hàm này sẽ trở thành các thành viên của subclass (lớp con/lớp được dẫn xuất từ lớp cha).

Bên cạnh các biến thành viên và hàm thành viên được kế thừa từ super class thì các subclass (lớp con) hiển nhiên cũng có thể có được các biến và hàm thành viên của riêng chúng. Một lát nữa chúng ta sẽ cùng xem ví dụ về điều này.

1. Person class

Sau đây là một class đơn giản dùng để đại diện cho một người chung chung:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <string>
 
class Person
{
// In this example, we're making our members public for simplicity
public: 
    std::string m_name;
    int m_age;
 
    Person(std::string name = "", int age = 0)
        : m_name(name), m_age(age)
    {
    }
 
    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
 
};

Bởi vì class Person này được thiết kế để đại diện cho một người chung chung, nên nó cũng chỉ bao gồm các thành viên mang tính chung chung đối với mọi kiểu người. Mỗi người (không phân biệt giới tính, nghề nghiệp, v.v…) đều có tên, tuổi, vì vậy các thuộc tính này đã được khai báo trong class Person.

Lưu ý rằng, trong ví dụ này, chúng ta đã thiết lập mức độ kiểm soát truy cập cho tất cả các biến và hàm là công khai (public). Điều này hoàn toàn là vì mục đích giữ cho các ví dụ này được đơn giản mà vẫn thể hiện được những kiến thức quan trọng của bài. Còn thông thường thì chúng ta sẽ đặt hầu hết các biến ở chế độ private (riêng tư). Về các mức độ kiểm soát truy cập và cách mà chúng tương tác với tính kế thừa trong C++ sẽ được đề cập tới trong phần sau của chương này.

2. BaseballPlayer class

Giả sử, chúng ta muốn viết một chương trình để lưu trữ và theo dõi thông tin về một số cầu thủ bóng chày. Class được xây dựng để mô hình hóa các cầu thủ bóng chày sẽ cần phải chứa những thông tin chuyên biệt về họ – Ví dụ, chúng ta có thể sẽ muốn lưu trữ batting average (hệ số đánh banh trung bình) của các cầu thủ, và số lần home runs (những cú chạy về nhà) mà họ đã đạt được.

Dưới đây là class BaseballPlayer chưa hoàn thiện của chúng ta:

class BaseballPlayer
{
// In this example, we're making our members public for simplicity
public:
    double m_battingAverage;
    int m_homeRuns;
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage(battingAverage), m_homeRuns(homeRuns)
    {
    }
};

Bây giờ, chúng ta lại muốn theo dõi thêm cả tên (name) và tuổi (age) của các cầu thủ bóng chày, tuy nhiên những biến thuộc tính về tên và tuổi này đã được khai báo trong class Person.

Chúng ta có ba lựa chọn về cách để thêm thuộc tính tên (name) và tuổi (age) cho class BaseballPlayer:

1. Khai báo thêm biến name và age trực tiếp làm biến thành viên của class BaseballPlayer. Đây có lẽ là lựa chọn tồi tệ nhất, vì chúng ta sẽ lặp lại code đã tồn tại bên trong class Person. Bất cứ cập nhật nào diễn ra trên class Person cũng sẽ cần phải được thực hiện trên class BaseballPlayer, điều này không ổn một chút nào.

2. Khai báo thêm một biến thành viên kiểu Person bên trong class BaseballPlayer nhằm thực hiện kết hợp đối tượng (object composition). Nhưng chúng ta phải tự hỏi lại bản thân mình rằng, liệu việc “một BaseballPlayer – cầu thủ bóng chày có một Person – người” có hợp lý hay không? Và chắc chắn rồi, điều đó không hợp lý một chút nào, vì vậy cách này sẽ khiến mô hình phân cấp các đối tượng của chúng ta bị sai.

3. Làm cho class BaseballPlayer kế thừa thuộc tính name và age từ class Person. Hãy nhớ rằng sự thừa kế đại diện cho một mối quan hệ is-a (là một). Một BaseballPlayer – cầu thủ bóng chày có phải là một Person – người không? Chắc chắn là có rồi. Vì vậy, triển khai tính kế thừa là một lựa chọn tốt ở đây.

3. Để BaseballPlayer trở thành một derived class – lớp con

Cú pháp để làm cho class BaseballPlayer kế thừa class Person khá đơngiản. Sau cú pháp khai báo “class BaseballPlayer”, chúng ta sử dụng thêm từ khóa “public”, và tên của class mà chúng ta muốn được kế thừa. Làm như cách vừa rồi, được gọi là kế thừa công khai (public inheritance). Chúng ta sẽ nói thêm về ý nghĩa của kế thừa công khai trong một bài học tương lai.

// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage;
    int m_homeRuns;
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage(battingAverage), m_homeRuns(homeRuns)
    {
    }
};

Khi được mô tả bằng biểu đồ, sự kế thừa chúng ta vừa triển khai sẽ trông như sau:

Khi BaseballPlayer kế thừa từ Person, BaseballPlayer sẽ thu thập được các hàm thành viên và biến thành viên tử Person. Ngoài ra, BaseballPlayer còn tự định nghĩa thêm hai biến thành viên của riêng mình: m_battingAverage và m_homeRuns. Điều này là hợp lý vì các thuộc tính này đều là những đặc trưng của một đối tượng BaseballPlayer (cầu thủ bóng chày), chứ không phải là đặc trưng của một người chung chung.

Do đó, các đối tượng BaseballPlayer sẽ có 4 biến thành viên, bao gồm: m_battingAverage và m_homeRuns của chính bản thân nó, và m_name cùng với m_age kế thừa được từ class Person.

Rất dễ để có thể chứng minh được điều này:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
#include <string>
 
class Person
{
public:
    std::string m_name;
    int m_age;
 
    Person(std::string name = "", int age = 0)
        : m_name(name), m_age(age)
    {
    }
 
    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
 
};
 
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
    double m_battingAverage;
    int m_homeRuns;
 
    BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
       : m_battingAverage(battingAverage), m_homeRuns(homeRuns)
    {
    }
};
 
int main()
{
    // Create a new BaseballPlayer object
    BaseballPlayer joe;
    // Assign it a name (we can do this directly because m_name is public)
    joe.m_name = "Joe";
    // Print out the name
    std::cout << joe.getName() << '\n'; // use the getName() function we've acquired from the Person base class
 
    return 0;
}

Đoạn chương trình trên sẽ in ra giá trị:

Joe

Đoạn code trên có thể được biên dịch và chạy bởi vì joe là một BaseballPlayer, và tất cả các đối tượng BaseballPlayer đều có một biến thành viên m_name và một hàm thành viên getName() kế thừa được từ class Person.

4. Thêm một ví dụ về subclass – lớp con

Chúng ta sẽ viết thêm một class khác cũng kế thừa class Person. Lần này, chúng ta sẽ viết class Employee. Một employee (nhân viên) “is a – là một” person (người), vì vậy sử dụng kế thừa ở đây là hoàn toàn hợp lý

// Employee publicly inherits from Person
class Employee: public Person
{
public:
    double m_hourlySalary;
    long m_employeeID;
 
    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary(hourlySalary), m_employeeID(employeeID)
    {
    }
 
    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};

Employee sẽ kế thừa m_name và m_age từ Person (cũng như hai hàm truy cập), và bản thân nó thì có thêm 2 biến thành viên và 1 hàm thành viên của riêng mình. Lưu ý rằng hàm printNameAndSalary() sẽ sử dụng các biến từ cả class mà nó thuộc về (Employee::m_hourlySalary) và cả các biến từ parent class – lớp cha của nó (Person::m_name).

Ta có thể lập được mô hình phân cấp sau:

Lưu ý rằng Employee và BaseballPlayer không có mối quan hệ trực tiếp nào. mặc dù cả hai đều kế thừa từ class Person.

Dưới đây là ví dụ đầy đủ sử dụng class Employee:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
#include <string>
 
class Person
{
public:
    std::string m_name;
    int m_age;
 
    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
 
    Person(std::string name = "", int age = 0)
        : m_name(name), m_age(age)
    {
    }
};
 
// Employee publicly inherits from Person
class Employee: public Person
{
public:
    double m_hourlySalary;
    long m_employeeID;
 
    Employee(double hourlySalary = 0.0, long employeeID = 0)
        : m_hourlySalary(hourlySalary), m_employeeID(employeeID)
    {
    }
 
    void printNameAndSalary() const
    {
        std::cout << m_name << ": " << m_hourlySalary << '\n';
    }
};
 
int main()
{
    Employee frank(20.25, 12345);
    frank.m_name = "Frank"; // we can do this because m_name is public
 
    frank.printNameAndSalary();
    
    return 0;
}

Kết quả là:

Frank: 20.25

5. Chuỗi kế thừa

Chúng ta hoàn toàn có thể kế thừa từ một class, mà bản thân class đó cũng đang kế thừa từ một class khác. Không có gì đáng chú ý hoặc đặc biệt khi làm như vậy – mọi thứ vẫn được tiến hành như trong các ví dụ phía trên.

Ví dụ, chúng ta sẽ viết một class Supervisor. Một Supervisor (giám đốc) là một Employee (công nhân), và Employee thì là một Person (người). Chúng ta đã viết class Employee, vậy thì hãy sử dụng nó làm base class (lớp cha, lớp cơ sở) để cài đặt class Supervisor:

class Supervisor: public Employee
{
public:
    // This Supervisor can oversee a max of 5 employees
    long m_overseesIDs[5];
 
    Supervisor()
    {
    }
 
};

Lúc này, mô hình phân cấp các class của chúng ta trông sẽ như sau:

Tất cả các đối tượng Supervisor đều sẽ kế thừa các hàm và biến từ cả class Employee và class Person, và có thêm biến thành viên của riêng nó là m_overseesIDs.
Thông qua việc xây dựng những chuỗi kế thừa như vậy, chúng ta có thể tạo ra một tập hợp các class có thể tái sử dụng, mang tính tổng quát cao (những class nằm ở phía trên) và dần dần trở nên cụ thể và chuyên biệt hơn ở mỗi cấp độ kế thừa phía dưới.

6. Tại sao tính kế thừa lại hữu ích?

Kế thừa từ một base class (lớp cơ sở) có nghĩa là chúng ta sẽ không cần phải định nghĩa lại những thông tin đã có ở base class, trong các subclass (lớp dẫn xuất/lớp con) của chúng ta. Chúng ta sẽ tự động nhận được các hàm thành viên và biến thành viên của base class thông qua kế thừa, và sau đó chỉ cần bổ sung thêm các hàm thành viên hoặc biến thành viên nếu muốn. Điều này không chỉ tiết kiệm công sức, mà còn có ý nghĩa là nếu chúng ta thực hiện bất kỳ cập nhật hoặc sửa đổi nào trên base class (ví dụ: Khai báo thêm hàm, hoặc fix bug), thì tất cả các subclass của chúng ta cũng sẽ tự động kế thừa những thay đổi này!

Ví dụ, nếu bạn khai báo thêm một hàm mới cho class Person, thì cả class Employee và class Supervisor đều sẽ tự động có được quyền truy cập vào hàm đó. Nếu chúng ta khai báo thêm một biến mới trong class Employee thì class Supervisor cũng sẽ có được quyền truy cập vào biến đó. Điều này cho phép chúng ta xây dựng được các class mới theo một cách dễ dàng, trực quan, và ít cần bảo trì.

7. Tổng kết

Tính kế thừa cho phép chúng ta tái sử dụng các class bằng cách để các class khác kế thừa các thành viên (biến thành viên, hàm thành viên) từ các class nào đó. Trong các bài học tương lai, chúng ta sẽ tiếp tục tìm hiểu về cách thức hoạt động của kế thừa trong lập trình hướng đối tượng.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!