Trong bài học trước – Cơ bản về kế thừa trong C++, bạn đã học được rằng mọi class đều có thể kế thừa các biến thành viên và hàm thành viên từ các class khác. Trong bài này, chúng ta sẽ đi sâu hơn về trình tự/thứ tự khởi tạo diễn ra khi một subclass(hay là class derived – class dẫn xuất từ class khác) được thể hiện (instantiated).

Đầu tiên, hãy cùng xem qua một số class mới được sử dụng để minh họa những điểm kiến thức quan trọng:

/**
* 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/
*/

class Base
{
public:
    int m_id;
 
    Base(int id=0)
        : m_id(id)
    {
    }
 
    int getId() const { return m_id; }
};
 
class Derived: public Base
{
public:
    double m_cost;
 
    Derived(double cost=0.0)
        : m_cost(cost)
    {
    }
 
    double getCost() const { return m_cost; }
};

Trong ví dụ này, subclass được dẫn xuất/kế thừa từ class Base.

Bởi vì Derived kế thừa các hàm và biến từ Base, nên bạn có thể giả định rằng các thành viên của Base được sao chép vào Derived. Tuy nhiên, điều này là không đúng. Thay vào đó, chúng ta có thể xem Derived là một class gồm hai phần: Một phần là Derived, một phần là Base.

Đến nay, bạn đã thấy rất nhiều ví dụ về những gì sẽ xảy ra khi chúng ta thể hiện (instantiate) một class bình thường (không kế thừa):

int main()
{
    Base base;
 
    return 0;
}

Base là một class không dẫn xuất (non-derived class) bởi vì nó không kế thừa từ bất kỳ class nào khác. C++ cấp phát bộ nhớ cho Base, sau đó gọi tới hàm constructor mặc định của Base để thực hiện việc khởi tạo.

Chúng ta cùng xem những gì sẽ xảy ra khi thể hiện một class được dẫn xuất:

int main()
{
    Derived derived;
 
    return 0;
}

Nếu tự mình làm thử điều này, bạn sẽ không nhận thấy bất kỳ sự khác biệt nào so với ví dụ trước, khi chúng ta thể hiện class Base (đây là một non-derived class). Nhưng thật ra, có tồn tại một số khác biệt nhỏ. Như đã đề cập ở trên, class Derived thực sự gồm hai phần: Một phần Base, và một phần Derived. Khi C++ xây dựng các đối tượng của class Derived, sẽ có hai giai đoạn được thực hiện. Đầu tiên, class cơ sở ở cấp cao nhất (the most-base class) nằm ở đỉnh của cây kế thừa sẽ được xây dựng trước. Sau đó từng class con sẽ được xây dựng theo thứ tự, cho đến khi lớp con ở cấp thấp nhất, nằm dưới cùng của cây kế thừa được xây dựng xong.

Vì vậy, khi khởi tạo một thể hiện của class Derived, trước tiên, phần Base của Derived sẽ được xây dựng trước (sử dụng hàm constructor mặc định của class Base). Một khi phần Base được xử lý xong, phần Derived sẽ được xây dựng (sử dụng hàm constructor mặc định của class Derived). Lúc này, không còn bất kỳ subclass classes nào nữa, vì vậy chúng ta đã hoàn thành việc khởi tạo.

Có thể dễ dàng minh họa quá trình 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>
 
class Base
{
public:
    int m_id;
 
    Base(int id=0)
        : m_id(id)
    {
        std::cout << "Base\n";
    }
 
    int getId() const { return m_id; }
};
 
class Derived: public Base
{
public:
    double m_cost;
 
    Derived(double cost=0.0)
        : m_cost(cost)
    {
        std::cout << "Derived\n";
    }
 
    double getCost() const { return m_cost; }
};
 
int main()
{
    std::cout << "Instantiating Base\n";
    Base cBase;
 
    std::cout << "Instantiating Derived\n";
    Derived cDerived;
 
    return 0;
}

Đoạn chương trình trên sẽ in ra kết quả sau:

Instantiating Base
Base
Instantiating Derived
Base
Derived

Như bạn có thể thấy, khi xây dựng class Derived, phần Base của class Derived sẽ được xây dựng trước. Điều này là hợp lý vì về mặt logic, một lớp con không thể tồn tại mà không có một lớp cha. Nó cũng giúp cho code đỡ bị lỗi hơn: child class (lớp con) thường sử dụng các biến và hàm từ parent class (lớp cha), nhưng parent class thường không biết gì về child class. Việc khởi tạo parent class trước tiên đảm bảo rằng những biến đó đã được khởi tạo xong trước thời điểm derived class được tạo ra, do đó chúng đã sẵn sàng để có thể được sử dụng bởi derived class.

1. Trình tự khởi tạo của các chuỗi kế thừa

Đôi khi, một class sẽ được kế thừa từ nhiều class khác, và các class này lại kế thừa từ những class khác nữa. Ví dụ:

/**
* 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/
*/

class A
{
public:
    A()
    {
        std::cout << "A\n";
    }
};
 
class B: public A
{
public:
    B()
    {
        std::cout << "B\n";
    }
};
 
class C: public B
{
public:
    C()
    {
        std::cout << "C\n";
    }
};
 
class D: public C
{
public:
    D()
    {
        std::cout << "D\n";
    }
};

Hãy nhớ rằng, C++ luôn luôn xây dựng base class ở cấp cao nhất trước. Sau đó nó mới đi dọc theo cây kế thừa theo thứ tự lần lượt và xây dựng từng devired class nối tiếp nhau.

Dưới đây là một đoạn chương trình ngắn minh họa thứ tự xây dựng các lớp dọc theo chuỗi kế thừa

int main()
{
    std::cout << "Constructing A: \n";
    A cA;
 
    std::cout << "Constructing B: \n";
    B cB;
 
    std::cout << "Constructing C: \n";
    C cC;
 
    std::cout << "Constructing D: \n";
    D cD;
}

Kết quả in ra là:

Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D

2. Kết luận

C++ sẽ xây dựng các subclass (lớp con/dẫn xuất) theo các giai đoạn, khởi đầu với base class ở cấp cao nhất (the most-base class, nằm ở đỉnh của cây kế thừa), và hoàn thành với child class ở cấp thấp nhất (the most-child class, nằm ở dưới cùng của cây kế thừa). Khi mỗi class được xây dựng, hàm constructor thích hợp từ class đó sẽ được gọi để khởi tạo phần đó của class.

Đến đây, có lẽ bạn đã nhận ra, các class ví dụ trong bài này đều sử dụng các hàm constructor mặc định của base class (để giúp ví dụ đơn giản hơn). Trong bài học tiếp theo, chúng ta sẽ xem xét kỹ hơn về vai trò của các hàm constructor trong quá trình xây dựng các subclass

class (bao gồm cả cách để chọn rõ ràng hàm constructor nào của base class mà bạn muốn subclass sử dụng).

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