Bài 35.1: Giới thiệu lập trình hướng đối tượng trong C++

Bài 35.1: Giới thiệu lập trình hướng đối tượng trong C++
Sau hành trình khám phá những kiến thức cơ bản về C++, đã đến lúc chúng ta bước vào một thế giới mới, một triết lý lập trình mạnh mẽ và phổ biến: Lập trình hướng đối tượng (Object-Oriented Programming - OOP). C++ là một ngôn ngữ sinh ra để hỗ trợ OOP, và việc nắm vững các khái niệm này sẽ mở ra cánh cửa để bạn xây dựng các ứng dụng lớn hơn, phức tạp hơn, và dễ quản lý hơn rất nhiều.
Bạn đã quen với việc viết code theo kiểu "lập trình thủ tục" (procedural programming), nơi bạn tập trung vào các bước thực hiện (các hàm, các câu lệnh) để giải quyết vấn đề. OOP thay đổi góc nhìn đó. Thay vì tập trung vào "làm thế nào", chúng ta tập trung vào các đối tượng trong thế giới thực và mối quan hệ giữa chúng. Hãy nghĩ về thế giới xung quanh bạn: có những chiếc ô tô, những con chó, những cái cây, những cuốn sách... Mỗi thứ đó là một đối tượng với những đặc điểm (màu sắc, kích thước, tên...) và những hành động có thể thực hiện (chạy, sủa, rụng lá, mở ra...).
OOP cố gắng mô hình hóa thế giới thực này vào code của chúng ta, giúp code trở nên trực quan, dễ hiểu, và quan trọng nhất là dễ bảo trì và mở rộng. C++ là một trong những ngôn ngữ tiêu biểu hỗ trợ đa mô hình lập trình, bao gồm cả OOP một cách mạnh mẽ.
Hãy cùng nhau khám phá những khái niệm cốt lõi làm nên sức mạnh của OOP trong C++!
1. Class (Lớp) và Object (Đối tượng)
Đây là hai khái niệm nền tảng của OOP.
- Class (Lớp): Hãy tưởng tượng Class như một bản thiết kế hoặc một khuôn mẫu để tạo ra các đối tượng. Nó định nghĩa các đặc điểm (gọi là thuộc tính hay data members) và các hành động (gọi là phương thức hay member functions) mà các đối tượng được tạo ra từ nó sẽ có. Một Class không chiếm bộ nhớ khi chương trình chạy cho đến khi bạn tạo ra một Object từ nó.
- Object (Đối tượng): Một Object là một thể hiện cụ thể được tạo ra từ một Class. Mỗi Object có giá trị riêng cho các thuộc tính được định nghĩa trong Class của nó, và có thể thực hiện các hành động (gọi các phương thức) được định nghĩa trong Class đó.
Hãy nghĩ đơn giản: Class "Ô tô" là bản thiết kế chung mô tả một chiếc ô tô sẽ có màu sắc, số bánh, nhãn hiệu (thuộc tính) và các hành động như chạy, phanh, bấm còi (phương thức). Object "Chiếc ô tô của tôi" là một thể hiện cụ thể từ bản thiết kế đó, có màu "đỏ", 4 bánh, nhãn hiệu "Toyota", và có thể thực hiện các hành động chạy, phanh, bấm còi.
Trong C++, chúng ta định nghĩa một Class sử dụng từ khóa class
:
#include <iostream>
#include <string>
// Định nghĩa một Class tên là Dog (Chó)
class Dog {
public: // Phần công khai (public), có thể truy cập từ bên ngoài class
// Thuộc tính (Data Members)
string name; // Tên của con chó
int age; // Tuổi của con chó
// Phương thức (Member Functions)
void bark() { // Hàm để con chó sủa
cout << name << " (" << age << " tuổi) says Woof! Woof!" << endl;
}
void eat(string food) { // Hàm để con chó ăn
cout << name << " is eating " << food << "." << endl;
}
}; // Kết thúc định nghĩa class bằng dấu chấm phẩy ;
int main() {
// Tạo một Object (thể hiện) tên là myDog từ Class Dog
Dog myDog;
// Truy cập và gán giá trị cho các thuộc tính của object
myDog.name = "Buddy";
myDog.age = 3;
// Gọi các phương thức (hành động) của object
myDog.bark();
myDog.eat("kibble");
// Tạo một object khác từ cùng Class Dog
Dog anotherDog;
anotherDog.name = "Max";
anotherDog.age = 5;
anotherDog.bark();
return 0;
}
Giải thích:
- Chúng ta định nghĩa
class Dog { ... };
. - Bên trong Class, chúng ta khai báo các thuộc tính (
name
,age
) và các phương thức (bark()
,eat()
). - Từ khóa
public:
chỉ ra rằng các thuộc tính và phương thức được khai báo sau nó có thể được truy cập từ bên ngoài Class. - Trong
main()
, chúng ta tạo ra các ObjectmyDog
vàanotherDog
từ ClassDog
. Mỗi Object này có bộ nhớ riêng để lưu trữname
vàage
của nó. - Chúng ta dùng toán tử dấu chấm (
.
) để truy cập các thuộc tính và gọi các phương thức của một Object cụ thể (ví dụ:myDog.name
,myDog.bark()
).
2. Encapsulation (Đóng gói)
Encapsulation là nguyên tắc đóng gói dữ liệu (thuộc tính) và các phương thức xử lý dữ liệu đó vào làm một đơn vị duy nhất (chính là Class). Đồng thời, nó cũng đề cập đến việc che giấu chi tiết triển khai (information hiding).
Thay vì cho phép truy cập trực tiếp vào tất cả các thuộc tính của Object từ bên ngoài, chúng ta thường đặt các thuộc tính ở chế độ riêng tư (private
) và chỉ cung cấp các phương thức công khai (public
) để truy cập hoặc sửa đổi chúng. Các phương thức này thường được gọi là getter (để lấy giá trị) và setter (để đặt giá trị).
Tại sao lại làm vậy?
- Kiểm soát truy cập: Chúng ta có thể đặt các quy tắc hoặc kiểm tra hợp lệ khi dữ liệu bị thay đổi thông qua setter (ví dụ: tuổi không được âm).
- Bảo vệ dữ liệu: Ngăn chặn việc thay đổi dữ liệu một cách tùy tiện từ bên ngoài, giúp dữ liệu luôn ở trạng thái hợp lệ.
- Linh hoạt: Nếu sau này bạn quyết định thay đổi cách lưu trữ một thuộc tính nào đó bên trong Class, bạn chỉ cần sửa code bên trong Class mà không ảnh hưởng đến code bên ngoài đang sử dụng getter/setter đó.
C++ cung cấp các mức độ truy cập (access specifiers):
public:
: Có thể truy cập từ bất kỳ đâu.private:
: Chỉ có thể truy cập từ bên trong cùng một Class.protected:
: Chỉ có thể truy cập từ bên trong cùng một Class hoặc các Class kế thừa từ nó (chúng ta sẽ nói về kế thừa sau).
Ví dụ về Encapsulation với Class Dog
:
#include <iostream>
#include <string>
class Dog {
private: // Các thuộc tính này chỉ có thể truy cập từ bên trong Class Dog
string name;
int age;
public: // Các phương thức này có thể truy cập từ bên ngoài Class
// Constructor - một phương thức đặc biệt được gọi khi tạo Object
// Chúng ta sẽ nói kỹ hơn sau, tạm hiểu là dùng để khởi tạo Object
Dog(string n, int a) {
// Có thể thực hiện kiểm tra hợp lệ ở đây
name = n;
if (a > 0) {
age = a;
} else {
age = 0; // Giá trị mặc định nếu tuổi không hợp lệ
cerr << "Lỗi: Tuổi không hợp lệ. Đặt tuổi là 0." << endl;
}
}
// Getter cho name
string getName() const { // const: phương thức này không thay đổi trạng thái object
return name;
}
// Setter cho name
void setName(string n) {
name = n;
}
// Getter cho age
int getAge() const {
return age;
}
// Setter cho age - có kiểm tra hợp lệ
void setAge(int a) {
if (a > 0) {
age = a;
} else {
cerr << "Lỗi: Tuổi không hợp lệ. Tuổi không thay đổi." << endl;
}
}
// Phương thức khác vẫn ở public
void bark() const {
cout << name << " (" << age << " tuổi) says Woof!" << endl;
}
};
int main() {
// Tạo object sử dụng constructor
Dog myDog("Buddy", 3);
// myDog.name = "New Name"; // Lỗi compile! Không thể truy cập private member trực tiếp
// Lấy tên thông qua getter
cout << "Tên của chó: " << myDog.getName() << endl;
// Thay đổi tên thông qua setter
myDog.setName("Buddy the Great");
cout << "Tên mới: " << myDog.getName() << endl;
// Thay đổi tuổi thông qua setter (hợp lệ)
myDog.setAge(4);
cout << "Tuổi mới: " << myDog.getAge() << endl;
// Thử thay đổi tuổi không hợp lệ
myDog.setAge(-2); // Sẽ in ra thông báo lỗi từ setter
myDog.bark();
return 0;
}
Giải thích:
- Các thuộc tính
name
vàage
được đặt làprivate
. Bạn sẽ gặp lỗi biên dịch nếu cố gắng truy cập chúng trực tiếp từmain()
. - Các phương thức
getName()
,setName()
,getAge()
,setAge()
, vàbark()
được đặt làpublic
. Chúng là giao diện công khai để tương tác với ObjectDog
. - Phương thức
setAge()
chứa logic kiểm traif (a > 0)
để đảm bảoage
luôn là số dương. Đây là cách Encapsulation giúp bảo vệ tính toàn vẹn của dữ liệu. - Constructor
Dog(string n, int a)
cũng là một phần của Encapsulation, đảm bảo rằng khi một ObjectDog
được tạo ra, nó được khởi tạo với các giá trị ban đầu đã qua kiểm tra.
3. Abstraction (Trừu tượng)
Abstraction là nguyên tắc che giấu những chi tiết phức tạp và chỉ phơi bày những tính năng thiết yếu cho người dùng. Nó tập trung vào điều gì một đối tượng làm được, hơn là làm thế nào nó làm điều đó.
Hãy quay lại ví dụ chiếc ô tô: Khi bạn lái xe, bạn sử dụng bàn đạp ga, bàn đạp phanh, vô lăng. Bạn không cần phải hiểu chi tiết cơ chế hoạt động của động cơ, hệ thống phanh thủy lực hay hệ thống lái. Giao diện (bàn đạp, vô lăng) đã trừu tượng hóa sự phức tạp bên dưới.
Trong OOP, Abstraction đạt được thông qua:
- Sử dụng Encapsulation: Ẩn đi các thuộc tính và phương thức nội bộ, chỉ công khai các phương thức cần thiết để tương tác.
- Thiết kế giao diện: Định nghĩa các phương thức công khai rõ ràng, mô tả hành động mà Object có thể thực hiện mà không tiết lộ cách thức thực hiện chi tiết.
- (Nâng cao) Abstract Classes và Interfaces: C++ có khái niệm về lớp trừu tượng và phương thức thuần ảo (
pure virtual functions
), cho phép định nghĩa một "hợp đồng" về các phương thức mà các lớp con phải triển khai.
Với ví dụ Class Dog
ở trên: Khi bạn gọi myDog.bark()
, bạn chỉ cần biết rằng con chó sẽ sủa. Bạn không cần quan tâm bên trong phương thức bark()
đó có những dòng code nào, nó tạo ra âm thanh bằng cách nào, hay nó sử dụng những thuộc tính nội bộ nào (ví dụ: có thể có một thuộc tính soundFrequency
ẩn bên trong). bark()
là một giao diện trừu tượng cho hành động sủa.
Để minh họa rõ hơn sự trừu tượng, hãy thêm một chút chi tiết nội bộ vào phương thức bark()
:
#include <iostream>
#include <string>
class Dog {
private:
string name;
int age;
// Các chi tiết triển khai nội bộ bị ẩn đi
double volume; // Độ to của tiếng sủa
// Một phương thức nội bộ phức tạp (ví dụ)
void generateSoundWaves() const {
cout << "Generating complex sound waves at volume " << volume << "... ";
// ... hàng trăm dòng code xử lý âm thanh phức tạp ...
cout << "Done!" << endl;
}
public:
Dog(string n, int a, double v = 1.0) : name(n), age(a), volume(v) {} // Constructor
string getName() const { return name; }
int getAge() const { return age; }
void setVolume(double v) { volume = v; }
// Phương thức công khai, là giao diện trừu tượng
void bark() const {
// Người dùng chỉ gọi bark(), không cần biết generateSoundWaves() hoạt động thế nào
generateSoundWaves(); // Gọi phương thức nội bộ
cout << name << " (" << age << " tuổi) says Woof! (at volume " << volume << ")" << endl;
}
};
int main() {
Dog myDog("Max", 5, 0.8);
// Người dùng tương tác thông qua giao diện công khai
myDog.bark(); // Họ chỉ cần biết myDog.bark() sẽ làm con chó sủa
// Họ không cần biết (và cũng không thể truy cập nếu nó là private)
// cách âm thanh được tạo ra bên trong generateSoundWaves()
return 0;
}
Giải thích:
- Phương thức
generateSoundWaves()
là chi tiết triển khai nội bộ (giả định là phức tạp). Nó được đặt làprivate
hoặc sử dụng bên trong các phương thức công khai. - Phương thức
bark()
là giao diện trừu tượng. Người dùng chỉ cần biết rằng gọibark()
sẽ làm con chó phát ra âm thanh sủa. Họ không cần biết (và cũng không nên biết) về sự tồn tại củagenerateSoundWaves()
hay cách nó hoạt động. - Abstraction giúp đơn giản hóa việc sử dụng Object và cho phép thay đổi chi tiết triển khai nội bộ mà không ảnh hưởng đến code sử dụng Object đó.
4. Inheritance (Kế thừa)
Inheritance là một trong những sức mạnh lớn nhất của OOP, cho phép bạn tạo ra một Class mới (lớp con hay lớp dẫn xuất - Derived Class) dựa trên một Class đã có (lớp cha hay lớp cơ sở - Base Class).
Khi một lớp con kế thừa từ lớp cha, nó sẽ tự động có được (kế thừa) các thuộc tính và phương thức công khai (public
) và được bảo vệ (protected
) của lớp cha. Lớp con có thể thêm các thuộc tính và phương thức mới của riêng mình, hoặc thay đổi (ghi đè - override) các phương thức của lớp cha để phù hợp với đặc điểm riêng.
Mối quan hệ giữa lớp cha và lớp con thường được mô tả là mối quan hệ "is-a" (là một). Ví dụ: "Chó là một Động vật", "Ô tô là một Phương tiện giao thông".
Kế thừa giúp:
- Tái sử dụng code: Không cần viết lại code cho các thuộc tính và phương thức chung.
- Xây dựng hệ thống phân cấp: Tổ chức các Class thành cấu trúc cây logic, phản ánh mối quan hệ trong thế giới thực.
- Hỗ trợ Đa hình: Đây là nền tảng cho Đa hình runtime (sẽ nói sau).
Trong C++, bạn sử dụng dấu hai chấm :
sau tên lớp con, theo sau là mức độ truy cập (thường là public
) và tên lớp cơ sở:
#include <iostream>
#include <string>
#include <vector> // Để minh họa đa hình sau
// Lớp cơ sở (Base Class)
class Animal {
private:
// Thuộc tính private, chỉ truy cập được trong lớp Animal
string species; // Loại động vật (ví dụ: "Dog", "Cat")
protected:
// Thuộc tính protected, truy cập được trong lớp Animal và các lớp kế thừa
string name; // Tên của động vật
public:
// Constructor của lớp cơ sở
Animal(string s, string n) : species(s), name(n) {
cout << "Đã tạo một Animal: " << name << " (" << species << ")" << endl;
}
// Phương thức công khai của lớp cơ sở
void eat() const {
cout << name << " the " << species << " is eating." << endl;
}
// Phương thức ảo (Virtual method) - Quan trọng cho Đa hình runtime
// Sẽ nói kỹ hơn ở phần Đa hình
virtual void makeSound() const {
cout << name << " the " << species << " makes a generic sound." << endl;
}
// Virtual Destructor - Quan trọng khi làm việc với đa hình và con trỏ
// Giúp giải phóng bộ nhớ đúng cách khi sử dụng con trỏ lớp cơ sở
virtual ~Animal() {
cout << "Đã hủy một Animal: " << name << " (" << species << ")" << endl;
}
};
// Lớp dẫn xuất (Derived Class) kế thừa từ Animal
class Dog : public Animal { // Kế thừa công khai (public inheritance)
private:
string breed; // Giống chó (thuộc tính riêng của Dog)
public:
// Constructor của lớp Dog
// Phải gọi constructor của lớp cơ sở Animal sử dụng cú pháp khởi tạo danh sách (initializer list)
Dog(string n, string b) : Animal("Dog", n), breed(b) {
cout << "Đã tạo một Dog: " << name << " (" << breed << ")" << endl;
}
// Phương thức riêng của lớp Dog
void bark() const {
// Có thể truy cập thuộc tính 'protected' name từ lớp cơ sở
cout << name << " the " << breed << " says Woof! Woof!" << endl;
}
// Ghi đè (Override) phương thức makeSound() từ lớp cơ sở
// Từ khóa 'override' (C++11 trở lên) giúp kiểm tra xem phương thức này
// có thực sự ghi đè một phương thức ảo trong lớp cha hay không.
void makeSound() const override {
cout << name << " the " << breed << " says Woof! (override)" << endl;
}
// Destructor của lớp Dog
~Dog() {
cout << "Đã hủy một Dog: " << name << " (" << breed << ")" << endl;
}
};
// Lớp dẫn xuất khác
class Cat : public Animal {
public:
Cat(string n) : Animal("Cat", n) {
cout << "Đã tạo một Cat: " << name << endl;
}
void meow() const {
cout << name << " the Cat says Meow!" << endl;
}
void makeSound() const override {
cout << name << " the Cat says Meow! (override)" << endl;
}
~Cat() {
cout << "Đã hủy một Cat: " << name << endl;
}
};
int main() {
// Tạo một object của lớp dẫn xuất Dog
Dog myDog("Buddy", "Golden Retriever");
// myDog kế thừa phương thức eat() từ lớp Animal
myDog.eat(); // Output: Buddy the Dog is eating.
// myDog có phương thức riêng bark()
myDog.bark(); // Output: Buddy the Golden Retriever says Woof! Woof!
// myDog gọi phương thức makeSound đã được ghi đè
myDog.makeSound(); // Output: Buddy the Golden Retriever says Woof! (override)
cout << "\n--- Minh hoa ke thua va IS-A ---\n";
// Có thể sử dụng con trỏ hoặc tham chiếu tới lớp cơ sở (Animal)
// để trỏ tới object của lớp dẫn xuất (Dog hoặc Cat)
Animal* animalPtr1 = &myDog; // Pointer tới Animal trỏ tới object Dog
Cat myCat("Whiskers");
Animal& animalRef1 = myCat; // Reference tới Animal tham chiếu tới object Cat
// Khi gọi phương thức THÔNG THƯỜNG qua con trỏ/tham chiếu lớp cơ sở,
// phương thức của lớp cơ sở được gọi (early binding / compile-time)
animalPtr1->eat(); // Vẫn gọi Animal::eat()
animalRef1.eat(); // Vẫn gọi Animal::eat()
// Khi gọi phương thức VIRTUAL qua con trỏ/tham chiếu lớp cơ sở,
// phương thức của lớp CỤ THỂ (lớp dẫn xuất) được gọi (late binding / runtime)
animalPtr1->makeSound(); // Gọi Dog::makeSound() nhờ virtual!
animalRef1.makeSound(); // Gọi Cat::makeSound() nhờ virtual!
// Ghi chú về memory management: Dùng con trỏ thô cần cẩn thận.
// Trong C++, thường dùng smart pointers (như unique_ptr, shared_ptr)
// để quản lý bộ nhớ tốt hơn, đặc biệt với đa hình.
// Destructor ảo là cần thiết để đảm bảo destructor của lớp dẫn xuất được gọi đúng.
return 0;
}
Giải thích:
- Chúng ta định nghĩa
class Animal
làm lớp cơ sở. Nó có thuộc tínhprotected name
và các phương thứceat()
vàmakeSound()
.makeSound()
được đánh dấu làvirtual
. class Dog : public Animal
định nghĩa lớpDog
kế thừa công khai từAnimal
.- Trong constructor của
Dog
, chúng ta phải gọi constructor của lớp cơ sởAnimal
bằng cú pháp: Animal("Dog", n)
. Dog
tự động có phương thứceat()
. Nó thêm thuộc tínhbreed
và phương thức riêngbark()
. Nó cũng ghi đè phương thứcmakeSound()
bằng cách định nghĩa lại nó với cùng chữ ký và thêm từ khóaoverride
.- Thuộc tính
protected name
có thể được truy cập trực tiếp trong các lớp dẫn xuất nhưDog
. - Phần
main()
minh họa cách tạo objectDog
và gọi các phương thức kế thừa (eat()
) và các phương thức riêng (bark()
), cũng như phương thức đã được ghi đè (makeSound()
). - Quan trọng nhất là phần minh họa sử dụng con trỏ/tham chiếu lớp cơ sở (
Animal* animalPtr1 = &myDog;
). Một con trỏ tớiAnimal
có thể trỏ tới một ObjectDog
(hoặcCat
), vì "Dog is-a Animal" và "Cat is-a Animal".
5. Polymorphism (Đa hình)
Polymorphism có nghĩa là "đa hình thái" hay "nhiều hình thức". Trong OOP, nó cho phép bạn xử lý các đối tượng thuộc các Class khác nhau (nhưng có chung lớp cơ sở) thông qua một giao diện chung (thường là con trỏ hoặc tham chiếu tới lớp cơ sở). Khi một phương thức được gọi thông qua giao diện chung này, chương trình sẽ thực thi phương thức phù hợp với kiểu dữ liệu thực tế của đối tượng tại thời điểm chạy chương trình.
C++ hỗ trợ hai loại đa hình chính:
- Compile-time Polymorphism (Đa hình lúc biên dịch): Đạt được thông qua Function Overloading (nhiều hàm cùng tên nhưng khác tham số) và Operator Overloading (định nghĩa lại toán tử). Trình biên dịch biết sẽ gọi hàm/toán tử nào dựa vào chữ ký (số lượng và kiểu tham số) lúc biên dịch.
- Runtime Polymorphism (Đa hình lúc chạy): Đạt được thông qua Virtual Functions và Base Class Pointers/References. Quyết định gọi phương thức nào được đưa ra lúc chương trình đang chạy, dựa vào kiểu dữ liệu thực tế của đối tượng mà con trỏ/tham chiếu đang trỏ tới.
Runtime Polymorphism là đặc trưng mạnh mẽ của OOP, cho phép viết code linh hoạt và mở rộng.
Để có Runtime Polymorphism trong C++, bạn cần:
- Một hệ thống Class có Kế thừa.
- Các phương thức trong lớp cơ sở cần được gọi một cách đa hình phải được đánh dấu bằng từ khóa
virtual
. - Các phương thức này được ghi đè trong lớp dẫn xuất.
- Bạn phải sử dụng con trỏ hoặc tham chiếu tới lớp cơ sở để trỏ tới các Object của lớp dẫn xuất.
Ví dụ tiếp nối từ Kế thừa, minh họa Đa hình Runtime:
#include <iostream>
#include <vector>
#include <memory> // Để sử dụng unique_ptr, giúp quản lý bộ nhớ tốt hơn
// Lớp cơ sở Animal (từ ví dụ Kế thừa, có virtual makeSound)
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
// Phương thức ảo (có thể được ghi đè ở lớp con)
virtual void makeSound() const {
cout << name << " makes a generic sound." << endl;
}
// Destructor ảo rất quan trọng khi dùng đa hình với con trỏ
virtual ~Animal() {
cout << "Destructing Animal: " << name << endl;
}
};
// Lớp dẫn xuất Dog (ghi đè makeSound)
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
// Ghi đè phương thức ảo makeSound
void makeSound() const override {
cout << name << " says Woof!" << endl;
}
~Dog() override { // Cũng nên dùng override cho destructor ảo
cout << "Destructing Dog: " << name << endl;
}
};
// Lớp dẫn xuất Cat (ghi đè makeSound)
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
// Ghi đè phương thức ảo makeSound
void makeSound() const override {
cout << name << " says Meow!" << endl;
}
~Cat() override {
cout << "Destructing Cat: " << name << endl;
}
};
int main() {
// Tạo các đối tượng cụ thể
Dog myDog("Buddy");
Cat myCat("Whiskers");
cout << "\n--- Minh hoa Da hinh Runtime ---\n";
// Sử dụng con trỏ lớp cơ sở để trỏ đến các đối tượng lớp dẫn xuất
Animal* animalPtr1 = &myDog; // Con trỏ Animal trỏ đến Dog
Animal* animalPtr2 = &myCat; // Con trỏ Animal trỏ đến Cat
// Gọi phương thức makeSound() thông qua con trỏ lớp cơ sở
// Nhờ từ khóa 'virtual' trong lớp Animal, C++ biết cần gọi
// phiên bản makeSound() của lớp thực tế mà con trỏ đang trỏ tới
animalPtr1->makeSound(); // Output: Buddy says Woof! (gọi Dog::makeSound)
animalPtr2->makeSound(); // Output: Whiskers says Meow! (gọi Cat::makeSound)
cout << "\n--- Minh hoa Da hinh voi Collection (vector) ---\n";
// Một ví dụ mạnh mẽ hơn về Đa hình: sử dụng một collection
// chứa các đối tượng thuộc các lớp dẫn xuất khác nhau thông qua con trỏ/tham chiếu lớp cơ sở.
// Sử dụng unique_ptr để tự động quản lý bộ nhớ
vector<unique_ptr<Animal>> farm;
farm.push_back(make_unique<Dog>("Rocky"));
farm.push_back(make_unique<Cat>("Mittens"));
farm.push_back(make_unique<Dog>("Spike"));
farm.push_back(make_unique<Cat>("Leo"));
// Lặp qua collection và gọi phương thức makeSound() cho từng đối tượng
// Mỗi đối tượng sẽ thực hiện hành động makeSound() của riêng loại của nó
// mà không cần biết cụ thể nó là Dog hay Cat
cout << "Tieng keu cua cac dong vat trong trang trai:" << endl;
for (const auto& animal : farm) {
animal->makeSound(); // Đa hình đang hoạt động ở đây!
}
// Khi vector farm kết thúc phạm vi, unique_ptr sẽ tự động gọi destructor
// và nhờ virtual destructor trong Animal, destructor đúng của Dog/Cat sẽ được gọi.
return 0;
}
Giải thích:
- Lớp
Animal
có phương thứcvirtual void makeSound()
. Các lớp conDog
vàCat
ghi đè phương thức này. - Trong
main()
, chúng ta tạo con trỏAnimal*
và trỏ chúng đến các đối tượngDog
vàCat
. - Khi gọi
animalPtr1->makeSound()
(con trỏ kiểuAnimal
trỏ tớiDog
), chương trình không gọiAnimal::makeSound()
. Nhờ từ khóavirtual
, nó nhìn vào kiểu dữ liệu thực tế của đối tượng được trỏ tới (làDog
) và gọiDog::makeSound()
. - Tương tự,
animalPtr2->makeSound()
gọiCat::makeSound()
. - Ví dụ với
vector<unique_ptr<Animal>>
minh họa cách bạn có thể lưu trữ các đối tượng thuộc các loại khác nhau (Dog
,Cat
) trong cùng một collection bằng cách sử dụng con trỏ (hoặc smart pointer nhưunique_ptr
) tới lớp cơ sở. Vòng lặp for có thể xử lý tất cả chúng một cách thống nhất thông qua giao diệnAnimal*
, và khi gọianimal->makeSound()
, hành vi cụ thể của từng loại động vật sẽ được thực thi nhờ Đa hình Runtime.
Comments