Ở bài này, chúng ta sẽ tìm hiểu virtual trong C++. Từ khoá virtual có một số đặc tính khá thú vị mà mình muốn chia sẻ với các bạn qua bài viết ngày hôm nay. Hãy cùng Techacademy tìm hiểu nhé.
1. Virtual Trong C++ Là Gì ? Tác Dụng Của Virtual Trong C++
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
2. Pure Virtual Function Trong C++ Là Gì
Hàm thuần ảo (pure virtual function) được sử dụng khi:
- Không cần sử dụng hàm này trong lớp cơ sở, chỉ phục vụ cho lớp dẫn xuất
- Lớp dẫn xuất bắt buộc phải định nghĩa lại hàm thuần ảo
Ví dụ, chúng ta có 1 lớp cơ sở là Shape. Các lớp dẫn xuất của lớp Shape là Triangle, Square và Circle. Chúng ta muốn tính diện tích của các hình này.
Chúng ta có thể tạo ra một hàm thuần ảo có tên là calculateArea() trong lớp Shape. Các lớp Triangle, Square và Circle phải định nghĩa lại hàm calculateArea() với công thức tính diện tích khác nhau cho mỗi hình.
Hàm thuần ảo không có thân hàm và bắt buộc phải kết thúc với “= 0”.
class Shape{ public: // creating a pure virtual function virtual void calculateArea() = 0; };
Lưu ý: Cú pháp “= 0” không phải là gán hàm thuần ảo có giá trị bằng 0. Nó chỉ là cú pháp cho biết đó là hàm thuần ảo (pure virtual function).
3. Virtual Destructor Trong C++
Trong một lớp thì Destructor có thể được đánh dấu làm hàm ảo còn Constructor thì không được đánh dấu là hàm ảo.
virtual Product(); // illegal virtual ~Product(); // legal
Xét một vài ví dụ để làm rõ Virtual Destructor. Giả sử có lớp cha Base và một lớp con Derived được hiện thực như dưới đây.
Trường hợp 1: Phương thức lớp cha không đánh dấu Virtual
Phương thức hủy lớp cha không được đánh dấu là hàm ảo:
class Base { public: Base() {}; ~Base() { cout << "Destructor Base\n"; }; }; class Derived : public Base { public: Derived() {}; ~Derived() { cout << "Destructor Derived\n"; } }; int main() { Base *b = new Derived(); delete b; return 0; }
Sau khi Build và Debug, chỉ có dòng “Destructor Base” được xuất ra, có nghĩa là chỉ phương thức lớp cha được gọi nhưng phương thức của lớp con không được gọi. Dẫn đến có thể gây nên thiếu sót như không thu hồi bộ nhớ các cấp phát động của lớp cha hoặc các thủ tục cần thực hiện trước khi đối tượng được thu hồi.
Xét tiếp ví dụ dưới đây, tương tự như ví dụ trên nhưng có sửa đổi ở lớp Derived
class Base { public: Base() {}; ~Base() { cout << "Destructor Base\n"; }; }; class Derived : public Base { private: int* m_array; public: Derived() { this->m_array = new int[1024]; }; ~Derived() { cout << "Destructor Derived\n"; delete this->m_array; } }; int main() { Base *b = new Derived(); delete b; return 0; }
Với lớp con như trên, mặc dù định nghĩa phương thức để giải phóng m_array nhưng phương thức của lớp con không được gọi. Có nghĩa là chương tình đã rò rỉ 1024*4 bytes bộ nhớ.
Trường hợp 2: Phương thức lớp cha có đánh dấu Virtual
Phương thức lớp cha được đánh dấu là phương thức ảo:
class Base { public: Base() {}; virtual ~Base() { cout << "Destructor Base\n"; }; }; class Derived : public Base { public: Derived() {}; ~Derived() { cout << "Destructor Derived\n"; } }; int main() { Base *b = new Derived(); delete b; return 0; }
Bulid và Debug thì chương trình xuất ra hai dòng là
Destructor Derived Destructor Base
Như vậy phương thức hủy của lớp con được gọi trước sau đó mới gọi phương thức hủy lớp cha và các thủ tục cần thiết trước khi hủy các đối tượng đã được thực hiện đầy đủ.
4. Nhược Điểm Của Các Hàm Ảo
Vì hầu hết thời gian bạn sẽ muốn các hàm của mình là ảo, tại sao không làm cho tất cả các hàm trở nên ảo? Câu trả lời là bởi vì nó không hiệu quả – việc giải quyết một cuộc gọi hàm ảo mất nhiều thời gian hơn là giải quyết một cuộc gọi thông thường. Hơn nữa, trình biên dịch cũng phải cấp phát một con trỏ phụ cho mỗi đối tượng lớp có một hoặc nhiều hàm ảo.
5. Tại Sao Không Nên Gọi Các Hàm Ảo Từ Các Hàm Tạo Hoặc Hàm Hủy
Ở đây, Bạn không nên gọi các hàm ảo từ các hàm tạo hoặc hàm hủy. Tại sao?
Hãy nhớ rằng khi một lớp Derived được tạo, phần Base được xây dựng trước. Nếu bạn đã gọi một hàm ảo từ hàm tạo cơ sở và phần lớp Derived thậm chí chưa được tạo, thì nó không thể gọi hàm của Derived vì không có đối tượng Derived được khởi tạo để gọi hàm. Trong C ++, nó sẽ gọi hàm trong class Base thay thế.
Một vấn đề tương tự tồn tại cho hàm huỷ. Nếu bạn gọi một hàm ảo trong hàm hủy của lớp Cơ sở, nó sẽ luôn gọi hàm của lớp Cơ sở, bởi vì phần Derived của lớp đã bị hủy.
Quy tắc: Không bao giờ gọi các hàm ảo từ các hàm tạo hoặc hàm hủy.
6. Tại Sao Bạn Nên Khai Báo Một Hàm Hủy Là Ảo
Giả sử có class Parent và class Child kế thừa từ Parent.
Ta định nghĩa con trỏ: Parent * p = new Child();
Lúc này để tạo ra Child(), thì Parent() phải được tạo ra trước. Khi chúng ta delete p, thì cả 2 đối tượng này cũng phải được gọi Destructor. Vậy nếu không khai báo virtual cho hàm Destructor của Parent, thì chỉ Destructor của Parent được gọi, đối tượng Child() vẫn còn đó.
0 / 5 - (0 Đánh Giá)