1. Hàm huỷ ảo

Mặc dù C ++ cung cấp một hàm hủy mặc định cho các lớp của bạn nếu bạn không tự cung cấp cho lớp đó, đôi khi bạn muốn cung cấp hàm hủy của riêng bạn (đặc biệt nếu lớp cần cấp phát bộ nhớ). Bạn phải luôn biến các hàm hủy của mình thành ảo nếu bạn xử lý thừa kế. Hãy xem xét ví dụ sau:

/**
* 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:
    ~Base() // note: not virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};
 
class Derived: public Base
{
private:
    int* m_array;
 
public:
    Derived(int length)
    {
        m_array = new int[length];
    }
 
    ~Derived() // note: not virtual (your compiler may warn you about this)
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};
 
int main()
{
    Derived *derived { new Derived(5) };
    Base *base { derived };
 
    delete base;
 
    return 0;
}

Lưu ý: Nếu bạn biên dịch ví dụ trên, trình biên dịch của bạn có thể cảnh báo bạn về hàm hủy không ảo (có chủ ý cho ví dụ này). Bạn có thể cần phải tắt cờ trình biên dịch để các cảnh báo được bỏ qua.

Bởi vì base là một con trỏ cơ sở, khi base bị xóa, chương trình sẽ xem liệu hàm hủy của base có phải là ảo hay không. Nếu không phải thì nó giả sử nó gọi hàm hủy. Chúng ta có thể thấy điều này trong thực tế là ví dụ trên in:

Calling ~Base()

Tuy nhiên, chúng ta thực sự muốn hàm delete gọi hàm hủy của Derive (lần lượt sẽ gọi hàm hủy cơ sở), nếu không m_array sẽ không bị xóa. Chúng ta thực hiện điều này bằng cách làm cho hàm huỷ của Base là hàm ảo:

/**
* 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:
    virtual ~Base() // note: virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};
 
class Derived: public Base
{
private:
    int* m_array;
 
public:
    Derived(int length)
    {
        m_array = new int[length];
    }
 
    virtual ~Derived() // note: virtual
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};
 
int main()
{
    Derived *derived { new Derived(5) };
    Base *base { derived };
 
    delete base;
 
    return 0;
}

Bây giờ chương trình này tạo ra kết quả sau:

Calling ~Derived()
Calling ~Base()

Quy tắc: Bất cứ khi nào bạn đang xử lý thừa kế, bạn nên biến bất kỳ hàm hủy thành ảo.

Như với các hàm thành viên ảo thông thường, nếu một hàm của lớp cơ sở là ảo, tất cả các phần ghi đè dẫn xuất sẽ được coi là ảo bất kể chúng có được chỉ định như vậy hay không. Không cần thiết phải tạo một hàm hủy của lớp dẫn xuất chỉ để đánh dấu nó là ảo.

2. Hàm gán ảo

Có thể làm cho toán tử gán thành ảo. Tuy nhiên, không giống như trường hợp hàm hủy khi hàm ảo luôn là một ý tưởng hay, ảo hóa của toán tử gán thực sự rối và đi vào một số chủ đề nâng cao ngoài phạm vi của hướng dẫn này. Do đó bạn nên hạn chế dùng hàm gán ảo.

3. Cách bỏ qua hàm ảo

Rất hiếm khi bạn có thể muốn bỏ qua việc ảo hóa một hàm. Ví dụ, hãy xem xét code sau đâ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/
*/

class Base
{
public:
    virtual const char* getName() { return "Base"; }
};
 
class Derived: public Base
{
public:
    virtual const char* getName() { return "Derived"; }
};

Có thể có trường hợp bạn muốn một con trỏ lớp Base đến một đối tượng Derived để gọi Base :: getName() thay vì Derived :: getName(). Để làm như vậy, chỉ cần sử dụng toán tử phân giải phạm vi:

#include <iostream>
int main()
{
    Derived derived;
    Base &base { derived };
    // Calls Base::GetName() instead of the virtualized Derived::GetName()
    std::cout << base.Base::getName() << '\n';
 
    return 0;
}

Có lẽ bạn đã sử dụng điều này rất thường xuyên, nhưng thật tốt khi dùng nó ít nhất có thể.

4. Chúng ta có nên làm cho tất cả các hàm huỷ trở là hàm ảo?

Đây là một câu hỏi phổ biến của các lập trình viên mới. Như đã lưu ý trong ví dụ trên, nếu hàm hủy của lớp cơ sở không được đánh dấu là ảo, thì chương trình có nguy cơ bị rò rỉ bộ nhớ nếu sau đó lập trình viên xóa một con trỏ lớp cơ sở đang trỏ đến một đối tượng dẫn xuất. Một cách để tránh điều này là đánh dấu tất cả các hàm hủy của bạn là ảo. Nhưng bạn có nên làm vậy?

Thật dễ dàng để nói có, do đó, sau đó bạn có thể sử dụng bất kỳ lớp nào làm lớp cơ sở – nhưng có đổi lại chương tình của bạn sẽ tốn hiệu năng để làm như vậy. Vì vậy, bạn phải cân bằng chi phí đó, cũng như ý định của bạn.

Bây giờ, trình biên dịch sẽ khuyến khích chúng ta như sau:

Nếu bạn dự định lớp của bạn được kế thừa từ đó, hãy chắc chắn rằng hàm hủy của bạn là ảo.
Nếu bạn không có ý định lớp của bạn được kế thừa, hãy đánh dấu lớp của bạn là final. Điều này sẽ ngăn các lớp khác kế thừa nó ngay từ đầu, mà không áp đặt bất kỳ hạn chế sử dụng nào khác đối với chính lớp đó.

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