Ở bài này, chúng ta sẽ tìm hiểu hàm ảo (virtual function) trong C++. Bài này có các kiến thức liên quan đến con trỏ đối tượng. Các bạn có thể đọc lại bài Khai báo và sử dụng con trỏ đối tượng trong C++ để xem lại kiến thức đó.
1. Hàm ảo (virtual function) là gì?
Hàm ảo (virtual function) là một hàm thành viên trong lớp cơ sở mà lớp dẫn xuất khi kế thừa cần phải định nghĩa lại.
Hàm ảo được sử dụng trong lớp cơ sở khi cần đảm bảo hàm ảo đó sẽ được định nghĩa lại trong lớp dẫn xuất. Việc này rất cần thiết trong trường hợp con trỏ có kiểu là lớp cơ sở trỏ đến đối tượng của lớp dẫn xuất.
Để hiểu tại sao phải sử dụng hàm ảo, chúng ta cùng xem xét một ví dụ:
class Base{
public:
void print(){
cout<<"Base class";
}
};
class Derived : public Base{
public:
void print(){
cout<<"Derived class";
}
};
Lớp Derived kế thừa public từ lớp Base. Chúng ta tạo một con trỏ (pointer) có kiểu là lớp Base trỏ đến một đối tượng của lớp Derived. Sau đó, gọi hàm print()
thì bản chất là gọi hàm print()
của lớp Base. Mà chúng ta muốn là phải gọi đến hàm print()
của lớp Derived bởi con trỏ đang trỏ đến đối tượng của lớp Derived.
void main(){
Derived derived1;
Base* base1 = &derived1;
//calls function of Base class
base1->print();
system("pause");
}
Kết quả
Base class
Để tránh trường hợp này, chúng ta khai báo hàm print()
trong lớp Base là một hàm ảo. Lúc này, lớp dẫn xuất phải định nghĩa lại hàm ảo đó. Khi gọi hàm print()
thông qua con trỏ thì sẽ gọi đến hàm được định nghĩa lại trong lớp dẫn xuất.
Hàm ảo là một phần không thể thiếu để thể hiện tính đa hình trong kế thừa được hỗ trợ bởi nguồn ngữ C++.
Lưu ý: Con trỏ của lớp cơ sở có thể chứa địa chỉ của đối tượng thuộc lớp dẫn xuất, nhưng ngược lại thì không được.
2. Định nghĩa hàm ảo (virtual function)
Sử dụng từ khóa virtual để định nghĩa một hàm ảo trong lớp cơ sở. Lớp dẫn xuất kế thừa từ lớp cơ sở phải định nghĩa lại hàm ảo.
#include <iostream>
using namespace std;
class Base{
public:
virtual void print(){//virtual function
cout<<"Base class";
}
};
class Derived : public Base{
public:
void print(){
cout<<"Derived class";
}
};
void main(){
Derived derived1;
Base* base1 = &derived1;
//calls function of Derived class
base1->print();
system("pause");
}
Kết quả
Derived class
Hàm ảo chỉ khác hàm thành phần thông thường khi được gọi từ một con trỏ. Sử dụng hàm ảo khi muốn con trỏ đang trỏ tới đối tượng của lớp nào thì hàm thành phần của lớp đó sẽ được gọi mà không xem xét đến kiểu của con trỏ.
3. Ví dụ sử dụng hàm ảo
#include <iostream>
#include <string>
using namespace std;
class Animal{
private:
string type;
public:
// constructor to initialize type
Animal(){
type = "Animal";
}
// declare virtual function
virtual string getType(){
return type;
}
};
class Dog : public Animal{
private:
string type;
public:
// constructor to initialize type
Dog(){
type = "Dog";
}
string getType(){
return type;
}
};
class Cat : public Animal{
private:
string type;
public:
// constructor to initialize type
Cat(){
type = "Cat";
}
string getType(){
return type;
}
};
void print(Animal* ani) {
cout<<"Animal: "<<ani->getType()<< endl;
}
void main(){
Animal* animal1 = new Animal();
Animal* dog1 = new Dog();
Animal* cat1 = new Cat();
print(animal1);
print(dog1);
print(cat1);
system("pause");
}
Kết quả
Animal: Animal
Animal: Dog
Animal: Cat
Lớp Animal là lớp cơ sở của lớp Dog và Cat. Tạo các con trỏ animal1, dog1, cat1 có kiểu là Animal trỏ để các đối tượng của lớp Animal, Dog, Cat. Một hàm ảo getType()
được khai báo trong lớp cơ sở Animal. Các lớp dẫn xuất Dog và Cat định nghĩa lại hàm này.
Khi hàm print(animal1)
được gọi, con trỏ trỏ đến một đối tượng của lớp Animal. Hàm ảo getType()
của lớp Animal sẽ thực thi.
Khi hàm print(dog1)
được gọi, con trỏ trỏ đến một đối tượng của lớp Dog. Hàm ảo getType()
được định nghĩa là trong lớp Dog được gọi.
Khi hàm print(cat1)
được gọi, con trỏ trỏ đến một đối tượng của lớp Cat. Hàm ảo getType()
được định nghĩa là trong lớp Cat được gọi.