Từ khoá virtual trong C++
C++
30
White

Võ Phi Hùng viết ngày 24/09/2015

Từ khoá virtual có một số đặc tính khá thú vị mà mình muốn chia sẻ với các đồng chí ngày hôm nay. Bài viết có vẻ hơi dài hơn mức cần thiết nhưng đừng ngại, tại mình copy paste code hơi nhiều thôi.

Tác dụng của từ khoá virtual, tại sao cần có virtual function.

Một số tài liệu có viết công dụng của Virtual Function như sau:

"Virtual Function là để khai báo một function ở class cha (base class) mà sau đó các class kế thừa (derived class) có thể override function đó"

Nhưng chờ đã, có gì không ổn ở chỗ này, nếu chỉ là để override thôi thì mình hoàn toàn có thể khai báo function ở base class mà không cần virtual thì vẫn được cơ mà. Vậy ko lẽ đồng chí Virtual Function này vô dụng? Để làm rõ vấn đề cũng như hạn chế buồn ngủ vì phải đọc quá nhiều chữ, chúng ta thử xét ví dụ nhỏ sau:

class Buffalo {
public:
    void  action(){printf("I'm eating grass\n");};
}; 

class YoungBuffalo : public Buffalo {
    void action(){printf("I'm typing keyboard\n");};
};

int main()
{
  Buffalo *elon = new Buffalo();
  YoungBuffalo *andy = new YoungBuffalo();

  elon->action();
  andy->action();
}

Output sẽ ra như thế này:

I'm eating grass
I'm typing keyboard

Nếu chỉ xét đến đây thì cậu virtual chắc sẽ hơi buồn vì mọi chuyện có vẻ vẫn ổn mà không cần đến sự có mặt của nó. Vì vậy chúng ta thử xét tiếp 1 ví dụ khác để làm chỗ cho virtual toả sáng một chút.

class Buffalo {
public:
    void  action(){printf("I'm eating grass\n");};
}; 

class YoungBuffalo : public Buffalo {
public:
    void action(){printf("I'm typing keyboard\n");};
};

int main()
{
  Buffalo *elon = new Buffalo();
  Buffalo *andy = new YoungBuffalo(); // khác với lúc nãy là YoungBuffalo *andy = new YoungBuffalo();

  elon->action();
  andy->action();
}

Lần này output sẽ là như thế này:

I'm eating grass
I'm eating grass

Đến đây thì chắc không cần phải quá tinh mắt bạn cũng đã nhận ra vấn đề rồi đúng không. Mặc dù andy được tạo ra từ constructor của class YoungBuffalo thế nhưng nó hành xử lại như thể nó là một Buffalo. Thế nhưng ví dụ này trông hơi bị thiếu thông minh vì chả mấy ai khai báo Buffalo *andy = new YoungBuffalo(); như này để tự làm khó mình cả. Mình sẽ xét một ví dụ thực tế hơn chút nữa.

class Buffalo {
public:
    void  action(){printf("I'm eating grass\n");};
}; 

class YoungBuffalo : public Buffalo {
public:
    void action(){printf("I'm typing keyboard\n");};
};

void takeAnBuffalo(Buffalo* buffalo){
    buffalo->action();
}

int main()
{
  Buffalo *elon = new Buffalo();
  Buffalo *andy = new YoungBuffalo(); 
  takeAnBuffalo(elon);
  takeAnBuffalo(andy);
}

Output sẽ vẫn lại là:

I'm eating grass
I'm eating grass

Lúc này thì vấn đề thực sự đã rõ rồi, vì vậy chúng ta sẽ fix với vấn đề này với virtual như sau:

class Buffalo {
public:
    virtual void  action(){printf("I'm eating grass\n");}; // thêm virtual vào chỗ này
}; 

class YoungBuffalo : public Buffalo {
public:
    void action(){printf("I'm typing keyboard\n");};
};

void takeAnBuffalo(Buffalo* buffalo){
    buffalo->action();
}

int main()
{
  Buffalo *elon = new Buffalo();
  Buffalo *andy = new YoungBuffalo();
  takeAnBuffalo(elon);
  takeAnBuffalo(andy);
}

Output:

I'm eating grass
I'm typing keyboard

Hoàn hảo! Lần này thì ai đã làm việc của người đó, Buffalo đã ăn cỏ, và YoungBuffalo đã gõ bàn phím. Ta sẽ đi đến mục tiếp theo.

Virtual Destructor

Virtual destructor cũng giống như các hàm virtual bình thường và được khai báo bằng cách thêm từ khoá virtual đằng trước hàm destructor. Nhưng trước khi học cách sử dụng virtual destructor, chúng ta sẽ nhìn qua một ví dụ mà không có virtual destructor.

class Animal {
public:
    ~Animal(){printf("This is Animal's destructor\n");};
}; 

class Cat : public Animal {
public:
    ~Cat(){printf("This is Cat's destructor\n");};
};


int main()
{
    Animal* tom = new Cat();
    delete tom;
}

Output:

This is Animal's destructor

Chúng ta có thể thấy ngay là destructor của class Cat đã không được gọi, mặc dù tom được khởi tạo bằng constructor của class Cat. Điều này thật sự rất nguy hiểm, vì destructor của class Cat không được gọi nên các đối tượng riêng của lớp đó cũng không được giải phóng và vì thế object tom chỉ được giải phóng 1 phần tài nguyên, điều này gây ra rò rỉ bộ nhớ (hiện tượng bộ nhớ đã được cấp phát không thu hồi lại được). Để khắc phục chúng ta thêm từ khoá virtual vào trước destructor của base class.

class Animal {
public:
    virtual ~Animal(){printf("This is Animal's destructor\n");};
}; 

class Cat : public Animal {
public:
    ~Cat(){printf("This is Cat's destructor\n");};
};


int main()
{
    Animal* tom = new Cat();
    delete tom;
}

Output:

This is Cat's destructor
This is Animal's destructor

Virtual destructor là một thứ rất quan trọng khi bạn làm việc với C++, nếu bạn có ý định cho phép kế thừa class mà bạn đang viết thì bạn bắt buộc phải thêm virtual destructor cho class đó, ngược lại thì bạn đang ngầm ám chỉ rằng class của bạn không cho phép kế thừa. Điều này tương đương với từ khoá final trong Java. Nếu bạn thấy một class không có virtual destructor, đơn giản là đừng có kế thừa nó, vì nó đi không đúng với ý định của người viết ra class, và có thể gây ra thiệt hại hệ thống nếu bạn cố tình bỏ qua.

Tham khảo

Why do we need Virtual Methods in C++?
When to use virtual destructors?

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Võ Phi Hùng

6 bài viết.
33 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
13 6
Bài này được mình dịch lại từ bài gốc ở đây. http://www.joelonsoftware.com/articles/fog0000000069.html Của tác giả Joel Spolsky thuộc công ty Netsc...
Võ Phi Hùng viết gần 3 năm trước
13 6
White
9 2
Khi làm quen với C hoặc C++ chắc bạn sẽ rất quen với mấy dòng kiểu như sau ở đầu và cuối mỗi file .h c++ ifndef _file_name_h define _file_name_h ...
Võ Phi Hùng viết gần 3 năm trước
9 2
White
8 2
Khái niệm macro và ví dụ Macro một cái tên nghe khá hổ báo, được dùng để chỉ những hàm được viết ở phần Preprocessor, thay vì đặt nó vào trong phầ...
Võ Phi Hùng viết gần 3 năm trước
8 2
Bài viết liên quan
Male avatar
0 0
Constructor của lớp cha luôn được gọi trước constructor của lớp con. class Foo { public: Foo() { cout << "Base class initializing" << endl; ...
baoquocphan viết 1 năm trước
0 0
White
10 1
Chức năng pattern matching trong C++ thấy nói sẽ kill (Link). Cách viết C++ do vậy sẽ thay đổi tương đối. Tự nhiên tôi thấy phải nhìn lại visitor ...
cpplover viết 3 năm trước
10 1
White
18 0
Nhân tiện vừa đọc bài viết liên quan tới OpenCV trên Kipalog, nên em xin giới thiệu về giải thuật sinh ảnh mosaic từ một ảnh gốc. Không hiểu sinh ả...
Cùi Bắp viết hơn 2 năm trước
18 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
6 bài viết.
33 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!