Download as pptx, pdf, or txt
Download as pptx, pdf, or txt
You are on page 1of 70

Lập trình hướng

đối tượng trong


C++
Phần IV. Polymorphism – Đa
hình
Nhóm 9: - Nguyễn Đình Trường 20215769
- Bùi Linh Nghi 20206068
- Nguyễn Đức Trọng 20215766
- Vũ Trung Kiên 20215719
Nội dung
Nội dung

I_ Basic Definitions
// Các khái niệm cơ bản
Nội dung

II_ Virtual member


function
// Hàm ảo
Nội dung

III_ Redefining Virtual


Functions
// Xác định lại các hàm ảo
Nội dung

IV_ Early and Late


Binding
// Liên kết tĩnh và liên kết động
Nội dung

V_ Virtual Destructors
// Hàm hủy ảo
Nội dung

VI_ Abstract classes


& Interfaces
// Lớp trừu tượng và phương tiện
01 CÁC KHÁI NIỆM
CƠ BẢN
Các KN cơ
Đa hình (polymorphism) nghĩa là có nhiều
bản
hình thái khác nhau. Tiêu biểu là, đa hình xuất
hiện khi có một cấu trúc cấp bậc của các lớp và
chúng là liên quan với nhau bởi tính kế thừa.
Me:
Các KN cơ
What teachers see:
bản

What my mom see: What my brother see:

What my friends see:

Ví dụ: Một người cùng một lúc có thể có đặc điểm khác nhau.
Giống như một người vừa là học sinh, đồng thời là một người con
trai, một người bạn, một người anh. Vì vậy, cùng một người sở hữu
những hành vi khác nhau trong các tình huống khác nhau. Điều này
được gọi là đa hình.​
Ví dụ
#include <iostream>
class TamGiac: public Hinh{
using namespace std;
public:
TamGiac( int a=0, int b=0):Hinh(a, b) { }
int dt=0;
int dientich ()
class Hinh {
{
protected:
dt= (chieurong * chieucao / 2);
int chieurong, chieucao;
cout << "Dien tich cua lop TamGiac la: " << dt
public:
<< endl;
Hinh( int a=0, int b=0)
}
{
};
chieurong = a;
// ham main cua chuong trinh
chieucao = b;
int main( )
}
{
int dientich()
Hinh *hinh;
{
HinhChuNhat hcn(13,6);
cout << "Dien tich cua lop cha:" <<endl;
TamGiac tag(8, 9);
return 0;
}
// luu giu dia chi cua HinhChuNhat
};
hinh = &hcn;
class HinhChuNhat: public Hinh{
// goi dien tich cua hinh chu nhat.
public:
hinh->dientich();
HinhChuNhat( int a=0, int b=0):Hinh(a, b) { }
int dientich ()
// luu giu dia chi cua TamGiac
{
hinh = &tag;
dt= (chieurong * chieucao);
// goi dien tich cua tam giac.
cout << "Dien tich cua lop HinhChuNhat la: " << dt <<
hinh->dientich();
endl;
return 0;
}
}
};
HÀM ẢO
Virtual Member 02
Function
Hàm ảo

Khi khai báo hàm ảo với từ khóa virtual nghĩa là hàm này
sẽ được gọi theo loại đối tượng được trỏ (hoặc tham chiếu),
chứ không phải theo loại của con trỏ (hoặc tham chiếu). Và
điều này dẫn đến kết quả khác nhau:
- Nếu không khai báo hàm ảo virtual trình biên dịch sẽ
gọi hàm tại lớp cở sở
- Nếu dùng hàm ảo virtual trình biên dịch sẽ gọi hàm tại
lớp dẫn xuất
Hàm ảo

Và để khai báo một phương thức ảo, ta khai báo như


bình thường nhưng thêm từ khóa "virtual " phía trước.

Bây giờ, chúng ta sửa chương trình trên một chút và


đặt trước dòng int dientich() trong lớp Hinh với từ
khóa virtual, khi đó nó trông như sau:
Ví dụ
class Hinh {
protected:
int chieurong, chieucao;
public:
Hinh( int a=0, int b=0)
{
chieurong = a;
chieucao = b;
}
virtual int dientich()
{
cout << "Dien tich cua lop cha: " <<endl;
return 0;
}
};
03 XÁC ĐỊNH LẠI
HÀM ẢO
Redifining Virtual
Functions
Redefining virtual functions (xác định lại các hàm
ảo) là quá trình khi một lớp dẫn xuất triển khai lại một
hàm ảo đã được định nghĩa trong lớp cơ sở.
Khi một hàm ảo được xác định lại trong một lớp
dẫn xuất, hàm này sẽ thay thế (ghi đè) hàm ảo của
cùng tên trong lớp cơ sở khi được gọi trên một đối
tượng của lớp dẫn xuất.
Ta xét ví dụ sau
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
class Shape { // Xác định lại hàm ảo để tính diện tích hình vuông
public: double areaSquare (double side) const override {
// Hàm ảo để tính diện tích hình chữ nhật std::cout << "Calculating area of square..." <<
virtual double areaRectangle(double length, double width) std::endl;
const{ return side * side;
return length * width; }
} };
// Hàm ảo để tính diện tích hình vuông
virtual double areaSquare(double side) const{ int main() {
return side * side; Rectangle rectangle;
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
Lớp Shape là lớp cơ sở có hai hàm ảo areaRectangle()
class Shape { //vàXác định lại
areaSquare() để hàm ảo để
tính diện tíchtính diện
của hình chữtích
nhật vàhình
hình vuông
public: double areaSquare (double side) const override {
// Hàm ảo để tính diện tích hình chữ nhật vuông.
std::cout << "Calculating area of square..." <<
virtual double areaRectangle(double length, double width) Lớp Rectangle là lớp dẫn xuất từ Shape, nó xác định lại
std::endl;
const{ (override) hàm ảo* areaRectangle()
return side side; để tính diện tích của
return length * width; } hình chữ nhật. Trong ví dụ này, khi hàm areaRectangle()
} }; được gọi trên một đối tượng của lớp Rectangle, chương
// Hàm ảo để tính diện tích hình vuông
trình sẽ in ra dòng "Calculating area of rectangle..." trước
virtual double areaSquare(double side) const{ int main()
return side * side; khi tính {
diện tích của hình chữ nhật.
Rectangle rectangle;
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
class Shape
Lớp{ Square là lớp dẫn xuất từ Shape, nó xác // Xác định lại hàm ảo để tính diện tích hình vuông
public:
định lại (override) hàm ảo areaSquare() để tính diện double areaSquare (double side) const override {
// Hàm ảo để tính diện tích hình chữ nhật std::cout << "Calculating area of square..." <<
tích của hình vuông. Tương tự, khi hàm
virtual double areaRectangle(double length, double width) std::endl;
const{areaSquare() được gọi trên một đối tượng của lớp return side * side;
Square,
return chương
length trình sẽ in ra dòng "Calculating area
* width; }
} of square..." trước khi tính diện tích của hình vuông. };
// Hàm ảo để tính diện tích hình vuông
virtual double areaSquare(double side) const{ int main() {
return side * side; Rectangle rectangle;
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square : public Shape {
Kết quả khi chạy chương trình sẽ
// Lớp cơ sở
class Shape {
public:
// Xác định lại hàm ảo để tính diện tích hình vuông
public:
hiển thị diện tích của hình chữ
// Hàm ảo để tính diện tích hình chữ nhật
double areaSquare (double side) const override {
std::cout << "Calculating area of square..." <<
virtual double areaRectangle(double length, double width) std::endl;
const{nhật và hình vuông được tính toán return side * side;
return length * width; }
} cùng với thông điệp về việc tính };
// Hàm ảo để tính diện tích hình vuông
diện tích của mỗi hình.
virtual double areaSquare(double side) const{ int main() {
return side * side; Rectangle rectangle;
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
04 LIÊN KẾT TĨNH &
LIÊN KẾT ĐỘNG
Early & Late binding
Khi biên dịch chương trình, mọi biến và hàm
đều được nối đến địa chỉ trong bộ nhớ. Và quá
trình chuyển đổi định danh thành địa chỉ được gọi
là binding. Quá trình này xảy ra ở cả biến và hàm.
Đối với hàm, trình biên dịch sẽ khớp lệnh gọi
hàm với định nghĩa hàm phù hợp. Quá trình này
có thể được thực hiện trong thời gian biên dịch
hoặc thời gian chạy

Nếu trình biên dịch biết mọi thông tin cần biết
để gọi được biết ngay trong thời gian biên dịch,
tức là biết hàm nào sẽ được gọi ngay trong thời
gian biên dịch thì gọi là early binding
Còn nếu trình biên dịch không biết hàm nào được
gọi cho đến khi thời gian chạy thì gọi là late
binding, liên quan đến những hàm chỉ được thực
Liên kết tĩnh

Khi trình biên dịch xử lý một hàm trong chương trình, nó


thay thế hàm đó bằng những chỉ dẫn ngôn ngữ máy trỏ tới hàm
để máy tính có thể thực thi.

Do đó, khi máy xử lý tới hàm nào, nó sẽ trỏ tới địa


chỉ của hàm đó trong bộ nhớ.

Gọi hàm trực tiếp hoặc nạp chồng hàm là một ví dụ


Liên kết tĩnh Nạp chồng hàm:
#include<iostream>
using namespace std;

class Base {
public:
Gọi hàm trực void display() {
cout << "In Base class" << endl;
tiếp: <iostream>
#include }
};

using namespace std;


class Derived: public Base{
public:
void display(int x){
void display(){
cout << x; cout << "In Derived class" << endl;
} }
};
int main(){ int main(){

display(5); Base *base_pointer = new Derived;


return 0; base_pointer->display();
return 0;
} }

Output: 5 Output: In Base class


Liên kết động
Con trỏ hàm và hàm ảo với lớp là một ví dụ
Liên kết động
Con trỏ hàm:

#include <iostream>

using namespace std;

void display(int x){


cout << x;
}

int main(){

void (*ptrFunc)(int) =
display;
ptrFunc(5);
return 0;
}

Output: 5
Liên kết động
Con trỏ hàm:
Trong ví dụ này, trình biên dịch ko biết
#include <iostream> hàm nào đang được lưu trong con trỏ hàm
cho tới thời gian chạy, thế nên ko thể là liên
using namespace std; kết tĩnh (early binding)
Khi đó, trình biên dịch giữ nó lại đến khi
void display(int x){ tham chiếu của hàm xuất hiện, máy tính sẽ trỏ
cout << x; tới địa chỉ của con trỏ hàm, sau đó gọi hàm
} tới chương trình.

int main(){

void (*ptrFunc)(int) =
display;
ptrFunc(5);
return 0;
}

Output: 5
Sau đây là ví dụ Hàm ảo với lớp:
Liên kết động
#include<iostream>
using namespace std;

class Base {
public:
virtual void display() {
cout << "In Base class" << endl;
}
};

class Derived: public Base{


public:
void display(){

}
cout << "In Derived class" << endl; Output: In Derived class
};

int main(){

Base *base_pointer = new Derived;


base_pointer->display();
return 0;
}
Lợi ích của Performance boost
liên kết tĩnh Một trong những điểm mạnh chính của liên kết tĩnh là
tăng hiệu năng. Bởi các lệnh gọi phương thức được thực
thi tại thời điểm biên dịch, do đó thời gian thực thi khi
chạy sẽ trở nên nhanh hơn. Ko cần tìm kiếm phương thức
trong suốt thời gian thực thi sẽ giúp chúng ta tiết kiệm
thời gian chạy
Compile-Time Checking
Liên kết sớm cho chúng ta khả năng kiểm tra ngay
trong thời gian biên dịch. Nghĩa là các lỗi liên quan đến
gọi hàm có thể được phát hiện sớm hơn. Điều này giúp
nắm bắt và khắc phục các sự cố tiềm ẩn trước khi chúng
trở thành sự cố lớn khó giải quyết hơn
Code Predictability
Với liên kết tĩnh, hành vi của mã trở nên dễ dự đoán
hơn. Chúng ta sẽ bớt được sự mơ hồ về việc phương thức
hay hàm nào sẽ được gọi, từ đó dễ đọc hiểu và bảo trì
Hạn chế
của Lack Of Flexibility
liên kết Với liên kết tĩnh, tính linh hoạt sẽ bị giảm đi. Bởi mọi
thứ đều đã được quyết định tại thời gian biên dịch, nên
tĩnh việc thay đổi hành vi trong thời gian chạy sẽ trở nên khó
khăn. Điều này hạn chế khả năng thích ứng tình huống
mà cần các quyết định tại thời gian chạy.
Difficulty In Code Reusability
Khả năng sử dụng lại mã có thể bị cản trở khi mà
phương thức được thực thi được xác định trong thời gian
chạy.Việc tạo các thành phần chung có thể hoạt động với
nhiều loại kiểu dữ liệu hoặc kiểu đối tượng sẽ trở nên
phức tạp
Increased Dependency
Liên kết tĩnh tăng sự phụ thuộc vào độ chính xác của
thông tin được cấp phát tĩnh. Nếu xảy ra lỗi hoặc sơ suất
ngay tại thời điểm bắt đầu, có thể dẫn đến vấn đề nghiêm
trọng trong thời gian chạy và khó để debug
Enhanced Flexibility Lợi ích của
Đặc điểm nổi bật của việc liên kết động là tính linh hoạt
vô song của nó. Nó cho phép quyết định phương thức hoặc liên kết động
chức năng liên quan đến một đối tượng trong thời gian chạy,
tức là có thể phản hồi các sự kiện xảy ra ngày trong quá trình
chạy, ko cần phải viết những chương trình dự phòng, hỗ trợ
các thay đổi linh hoạt và thúc đẩy khả năng thích ứng trong
các ứng dụng.
Facilitates Polymorphism
Liên kết động đóng vai trò then chốt trong việc hỗ trợ tính
đa hình trong C++, cho phép các đối tượng thuộc các lớp
khác nhau được coi là đối tượng của một lớp chung. Điều
này đặc biệt hữu ích trong việc tạo ra các thành phần mã
generic và dễ tái sử dụng hơn
Enables Dynamic Linking
Điều này có nghĩa là liên kết đến các thư viện hoặc
phương thức trong thời gian chạy thay vì lúc biên dịch. Nó
có thể có ích khi làm việc với các plugin hoặc tiện ích mở
Performance Overheads (chi phí hiệu suất) Hạn chế của
Bởi vì quá trình phân giải phương thức diễn ra trong thời gian
chạy nên tiến trình có thể gây ra độ trễ, đặc biệt khi cần phải liên kết động
thực hiện các thao tác liên kết hoặc đóng lại thường xuyên.
Increased Complexity
Liên kết động có thể làm tăng thêm độ phức tạp cho mã. Việc
đảm bảo hành vi chính xác bằng độ phân giải động thường đòi
hỏi các biện pháp bảo vệ bổ sung, làm tăng gánh nặng bảo trì và
hiểu biết
Debugging Challenges
Khó gỡ lỗi hơn vì phương thức diễn ra trong thời gian chạy.
Các lỗi liên quan đến lệnh gọi phương thức có thể không rõ ràng
cho đến khi ứng dụng đang chạy, làm việc khắc phục sự cố trở
nên phức tạp hơn
Dependency On Runtime Information
Liên kết muộn mang lại sự phụ thuộc vào thông tin thời gian
chạy. Nếu có bất kỳ sự khác biệt hoặc thay đổi nào trong môi
trường thời gian chạy, điều đó có thể dẫn đến các hành vi không
mong muốn hoặc thậm chí là ứng dụng bị treo.
05
HÀM HỦY ẢO
VIRTUAL
DESTRUCTOR
Yo
ur
Destructor (hàm hủy) là một loại hàm F un
thành viên đặc biệt khác của class, được cti
o n
thực thi khi một đối tượng của class đó bị
hủy. Trong khi các hàm constructors (hàm
khởi tạo) được thiết kế để khởi tạo một
class, thì các hàm destructors (hàm hủy)
được thiết kế để hỗ trợ việc dọn dẹp bộ nhớ.
Function
Derived class: Lớp dẫn xuất – sinh ra bằng cách thừa kế lớp Cơ sở (Base
class)
Base class: Lớp cơ sở – bị thừa kế -> sinh ra Derived class
Khi gọi hàm Hủy (Destructor), hàm Hủy của Lớp Derived sẽ thực hiện
Cú pháp: Hàm hủy được viết bằng ký tự ‘~’ xuất
hiện đằng trước tên của lớp. Và khi chúng ta
muốn viết hàm hủy ảo, rất đơn giản chỉ cần
thêm từ khóa virtual trước dấu ngã.

// Chú ý: Hàm khởi tạo thì không thể là ảo được.


Việc khai báo một hàm khởi tạo là hàm ảo sẽ
gây ra lỗi cú pháp.
Nếu một đối tượng (hàm hủy không phải hàm ảo) bị
hủy một cách rõ ràng bởi việc sử dụng toán tử delete
cho con trỏ của lớp cơ sở của đối tượng đó, thì hàm
hủy của lớp cơ sở (phù hợp kiểu con trỏ) sẽ được gọi
trên đối tượng đó.

Có một giải pháp đơn giản cho vấn đề này – khai báo
một hàm hủy lớp cơ sở là hàm ảo (virtual). Nó sẽ làm
cho tất cả các hàm hủy ở lớp thừa kế sẽ là ảo ngay cả
khi nó không có cùng tên với hàm hủy lớp cơ sở. Bây
giờ, nếu đối tượng thừa kế bị hủy một cách rõ ràng
bằng cách gọi toán tử delete của lớp cơ sở cho đối
tượng ở lớp thừa kế, thì hàm hủy của lớp tương ứng
sẽ được gọi.
Ví dụ: Giả sử có lớp cha Base và một lớp con Derived được hiện thực như dư
đây.
Trường hợp 1: Phương thức lớp cha không đánh dấu Virtual
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;
}

Output: Destructor Base


Trường hợp 1: Phương thức lớp cha không đánh dấu Virtual

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ụ sau đây, tương tự như ví dụ trên


nhưng có sửa đổi ở lớp Derived
Trường hợp 1: Phương thức lớp cha không đánh dấu Virtual
class Base
{
public:
Với lớp con như trên, mặc dù Base() {};
định nghĩa phương thức để ~Base() { cout << "Destructor Base\n"; };
giải phóng m_array nhưng };
phương thức của lớp con
class Derived : public Base
không được gọi. Có nghĩa là {
chương tình đã rò rỉ 1024*4 private:
bytes bộ nhớ. 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;
}
Trường hợp 2: Phương thức lớp cha có đánh dấu Virtual
Trường hợp 2: Phương thức lớp cha có đánh dấu Virtual
class Base
{
Như vậy phương thức hủy public:
của lớp con được gọi trước Base() {};
sau đó mới gọi phương thức virtual ~Base() { cout << "Destructor Base\n"; };
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 đủ. class Derived : public Base
{
public:
Derived() {};
~Derived() { cout << "Destructor Derived\n"; }
};

int main()
{
Base *b = new Derived();
delete b;
return 0;
}

Destructor Derived
Output
Destructor Base
Tại Sao Bạn Nên Khai Báo Một Hàm Hủy Là Ảo
Giả sử có class Base và class Derived kế thừa từ Base
Ta định nghĩa con trỏ: Base * b = new Derived();
Lúc này để tạo ra Derived(), thì Base() phải được tạo ra trước. Khi chúng ta delete b, 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 Base, thì
chỉ Destructor của Base được gọi, đối tượng Derived() vẫn còn đó. Nói ngắn gọn đơn giản, hàm hủy
ảo giúp chúng ta hủy các tài nguyên theo thứ tự đúng, khi chúng ta muốn xóa con trỏ trỏ từ lớp
cha đến đối tượng lớp con. Tránh rò rỉ bộ nhớ.
Lời khuyên ở đây, bất cứ khi nào trong chương trình của chúng ta sử dụng đến class có tính đa
hình hoặc class đó có một hoặc nhiều hàm ảo, chúng ta nên viết kèm thêm một hàm hủy ảo cho lớp
cha.
LỚP TRỪU TƯỢNG
& PHƯƠNG TIỆN
Abstract classes 06
& Intefaces
Lớp trừu tượng
Class trong C++ là khái niệm được người dùng định nghĩa như một
kiểu dữ liệu đơn giản hay một cấu trúc dữ liệu, được khai báo bằng từ
khóa class, nó chứa các biến (còn gọi là thuộc tính) hay các hàm (còn
gọi là phương thức).
Các phần tử của class được quản lý bởi ba thuộc tính truy cập:
private, protected hoặc public (thuộc tính mặc định khi truy cập vào
một phần tử trong class là private). Các phần tử private không thể được
truy cập bên ngoài class mà chỉ có thể được truy cập thông qua các
phương thức của class chứa chúng. Ngược lại, các phần tử public có thể
được truy cập ở bất kỳ class nào.
Lớp trừu tượng

Lớp trừu tượng trong C++ là một lớp mà không thể tạo đối tượng từ
nó trực tiếp. Điều này thường được sử dụng để tạo ra một giao diện
chung cho các lớp con, mà sau đó các lớp con cụ thể sẽ triển khai các
phương thức ảo của nó. Lớp trừu tượng có thể chứa các phương thức
ảo, các phương thức không ảo, các biến dữ liệu và các hàm cũng như
các hằng số và phương thức static.
Để định nghĩa một lớp trừu tượng, bạn cần có ít
nhất một phương thức ảo thuần túy (pure virtual
method), được khai báo bằng cách sử dụng từ khóa
virtual và đặt dấu = 0 ở cuối. Một lớp trừu tượng
cũng có thể chứa các phương thức không ảo và dữ
liệu thành viên thông thường.

Lớp AbstractClass là một lớp trừu tượng.

Nó có ít nhất một phương thức ảo thuần túy là


abstractMethod().

Ngoài ra, nó có thể chứa các phương thức không ảo


và dữ liệu thành viên khác như bất kỳ lớp C++ thông
thường nào.
Điều quan trọng là bạn không thể tạo đối tượng trực
tiếp từ một lớp trừu tượng, nhưng bạn có thể tạo con
trỏ hoặc tham chiếu tới lớp trừu tượng.
Mục đích của lớp trừu tượng là phục vụ như một
bản thiết kế cho các lớp khác. Nó định nghĩa các
hành vi và thuộc tính chung mà các lớp con có thể
kế thừa và triển khai khi cần thiết. Các lớp trừu
tượng cũng có thể chứa các phương thức cụ thể, có
các triển khai thực hiện và có thể được các lớp con
kế thừa trực tiếp.

Để sử dụng một lớp trừu tượng, bạn thường tạo


một lớp con mở rộng lớp trừu tượng và cung cấp các
triển khai cho tất cả các phương thức trừu tượng của
nó. Điều này cho phép tái sử dụng mã và giúp thực
thi giao diện hoặc hợp đồng chung giữa các lớp liên
quan.
Để hiểu hơn, ta xét một ví dụ khác:
Ví dụ
#include <iostream>
// Lớp con thực hiện từ lớp trừu tượng
Trong
class Cat ví dụ này,
: public lớp Animal
Animal { là một lớp trừu
//Lớp trừu tượng tượng với một phương thức ảo makeSound().
public:
class Animal {
public: Lớp Dogkhai
//Triển và Cat là các
phương thứclớp
ảo con của Animal,
void makeSound() const override {
// Phương thức ảo thuần túy (Pure virtual mỗi lớp triển khai phương thức ảo
std::cout << "Meow!" << std::endl;
method) makeSound() của riêng mình. Khi bạn tạo đối
}
virtual void makeSound() const = 0;
}; tượng từ lớp Dog hoặc Cat, bạn có thể gọi
// Phương thức thực hiện công việc chung cho phương thức makeSound() của chúng, nhưng
int main(){
tất cả các đối tượng Animal không thể tạo đối tượng từ lớp Animal vì
nó là
// Khai báo một con chó và một con mèo
void move() const { lớp
Dogtrừu
dog;tượng.
std::cout << "This Animal moves." <<
Cat cat;
std::endl;
}
// Gọi phương thức chung
};
dog.move();
cat.move();
// Lớp con thực hiện từ lớp trừu tượng
class Dog : public Animal {
// Gọi phương thức ảo từ đối tượng lớp con
public:
dog.makeSound();
// Triển khai phương thức ảo
cat.makeSound();
void makeSound() const override {
std::cout << "Woof!" << std::endl;
return 0;
}
}
};
Phương tiện
Trong lập trình hướng đối tượng, một interface là
một tập hợp các phương thức trừu tượng (abstract
methods) mà một lớp có thể triển khai. Interface định
nghĩa các hành vi mà một đối tượng có thể thực hiện
mà không quan tâm đến cách thức cụ thể để thực hiện
chúng.

Trong C++, interface thường được thực hiện bằng


cách sử dụng lớp trừu tượng chỉ chứa các phương thức
trừu tượng mà không có bất kỳ phương thức không
trừu tượng nào. Tất cả các phương thức trong một
interface đều là phương thức ảo thuần túy (pure virtual
methods).
#include <iostream>
int main(){
//Interface // Khởi tạo một đói tượng Circle và
class Drawable { gọi các phương thức của Interface
public: Circle circle;
// Phương thức ảo thuần túy để vẽ circle.draw();
virtual void draw() const = 0; circle.erase();

// Phương thức ảo thuần túy để xóa return 0;


virtual void erase() const = 0; }

// Hàm hủy ảo
virtual ~Drawable() {}
};

// Lớp con triển khai interface


class Circle : public Drawable {
public:
// Triển khai phương thức vẽ
void draw() const override {
std::cout << "Drawing Circle." << std::endl;
}

// Triển khai phương thức xóa


void erase() const override {
std::couit << "Erasing Circle." << std::endl;
}
};
#include <iostream>

//Interface Drawable là interface với hai phương


class Drawable {
public:
thức ảo thuần túy draw() và erase().
// Phương thức ảo thuần túy để vẽ
virtual void draw() const = 0; Lớp Circle triển khai interface
// Phương thức ảo thuần túy để xóa Drawable.
virtual void erase() const = 0;
Khi một đối tượng Circle được tạo, bạn
// Hàm hủy ảo
virtual ~Drawable() {} có thể gọi các phương thức draw() và
}; erase() thông qua con trỏ hoặc tham chiếu
// Lớp con triển khai interface của interface Drawable.
class Circle : public Drawable {
public:
// Triển khai phương thức vẽ
void draw() const override {
std::cout << "Drawing Circle." << std::endl;
}

// Triển khai phương thức xóa


void erase() const override {
std::couit << "Erasing Circle." << std::endl;
}
};
#include <iostream>

//Interface
Đặc điểm của interface
class Drawable { + Chỉ chứa khai báo không chứa phần định nghĩa.
public:
// Phương thức ảo thuần túy để vẽ + Việc ghi đè một thành phần trong interface cũng
virtual void draw() const = 0;
không cần từ khóa override
// Phương thức ảo thuần túy để xóa
virtual void erase() const = 0; + Không thế khai báo phạm vi truy cập trong
Interface và các thành phần này mặc định là public
// Hàm hủy ảo
virtual ~Drawable() {} + Interface không chứa các thuộc tính (các biến) dù là
};
hằng số hay biến tĩnh
// Lớp con triển khai interface
class Circle : public Drawable { + Interface k có constructor và k có destructor
public:
// Triển khai phương thức vẽ
+ Các lớp có thể thực thi nhiều interface cung một lục
void draw() const override { ( Đây là đặc điểm được coi như là thay thế tính đa kế
std::cout << "Drawing Circle." << std::endl;
} thừa)
// Triển khai phương thức xóa + Một interface có thể kế thừa nhiều interface khác
void erase() const override {
std::couit << "Erasing Circle." << std::endl; nhưng không thể kế thừa bất kỳ lớp nào.
} Interface giúp tạo ra một giao diện chung cho các lớp,
};
từ đó tăng tính linh hoạt và tái sử dụng trong mã
Abstract Class vs.
Interfaces

Giống nhau
+ Đều được sử dụng để đạt được tính trừu tượng trong OOP
+ Đêù được sử dụng trong kế thừa
+ Đều có thể chứa cả phương thức và các trường dữ liệu (variables)
+ Đều không thể tạo đội tượng cụ thể với keyword new
Abstract class Interface
Keyword abstract Khai báo tường minh Mặc định

Các kiểu biến Có thể chứa mọi loại biến: final, non-final, Chỉ có thể chứa các biến static và final
static, non-static
Hằng số Có thể có hoặc không Mặc định các biến số trong interface là hằng
số

Khác nhau Loại phương thức Hỗ trợ tất cả các loại phương thức : Chỉ chứa các phương thức: abstract, default,
constructor, static, non-static, abstract static, private

Sự triển khai lẫn nhau Abstract class có thể cung cấp triển khai Interface không thê cung cấp triền khai cho
cho interface abtract class
Đa kế thừa Không hỗ trợ đa kế thừa với nhiều lớp khác Một interface có thể kế thừa nhiều interface
khác

Đa thực thi Abstract class có thể mở rộng nhiều lớp Interface chỉ có thể extend các interface
khác và thực hiện nhiều interface tùy ý khác

Access modifiers Hỗ trọ tất cả các loại access modifiers Mặc định các thành phần của interface là
(Quyền truy cập) public

Constructor Có thể chứa các constructor Không hỗ trọ


Mục đích Làm lớp cha chung của một nhóm các lớp Định ra hành động để có thể dùng chung
liên quan cho các lớp
THANKS FOR
LISTENING!
Do you have any questions?
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
class Shape { // Xác định lại hàm ảo để tính diện tích hình vuông
public: double areaSquare (double side) const override {
Vậy kết quả là gì?
// Hàm ảo để tính diện tích hình chữ nhật
virtual double areaRectangle(double length, double width)
std::cout << "Calculating area of square..." <<
std::endl;
const{ return side * side;
return length * width; }
} };
// Hàm ảo để tính diện tích hình vuông
virtual double areaSquare(double side) const{ int main() {
return side * side; Rectangle rectangle;
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
Ví dụ
#include <iostream> // Lớp dẫn xuất từ Shape: Hình vuông
class Square
Trong đoạn: public
code trướcShape {
đó, khi chúng ta sử dụng phạm vi giới
// Lớp cơ sở public:
hạn Shape::, nó không trả về diện tích không xác định là 0, mà là
class Shape { //hàm
Xác định
area() của lại
lớp cơhàm ảo để
sở Shape tính
không đượcdiện tích hình vuông
triển khai.
public: doubleTrongareaSquare (double
lớp Shape, area() side)
là một hàm thànhconst
viên ảo, override
đồng {
// Hàm ảo để tính diện tích hình chữ nhật std::cout
nghĩa <<Shape
với việc lớp "Calculating
là một lớp trừu area
tượng vàofchúng
square..."
ta <<
virtual double areaRectangle(double length, double width) std::endl;
không thể tạo một đối tượng của nó. Khi một lớp kế thừa từ
const{ return
Shape side
và không * khai
triển side;
lại hàm area(), lớp đó cũng trở thành
return length * width; } một lớp trừu tượng và không thể tạo một đối tượng của nó. Do
} }; đó, việc gọi area() trên các đối tượng của lớp dẫn xuất mà không
// Hàm ảo để tính diện tích hình vuông triển khai lại hàm này sẽ không trả về một giá trị cụ thể.
virtual double areaSquare(double side) const{ int main() { kết quả là một giá trị không xác định khi chúng ta gọi
Vì vậy,
return side * side; Rectangle rectangle;
area() trên các đối tượng rectangle và square.
} Square square;
}; double length = 4;
double width = 5;
// Lớp dẫn xuất từ Shape: Hình chữ nhật double side = 3;
class Rectangle : public Shape {
public: //Tính diện tích hình chữ nhật
// Xác định lại hàm ảo để tính diện tích hình chữ nhật std::cout << "Diện tích hình chữ nhật: " <<
double areaRectangle (double length, double width) const rectangle.areaRectangle(length, width) << std::endl;
override {
std::cout << "Calculating area of rectangle..." << //Tính diện tích vuông
std::endl; std::cout << "Diện tích hình vuông: " <<
return length * width; square.areaSquare(side) << std::endl;
} }
};
Ví dụ

Vậy phải làm thế nào để thu được kết quả chính
xác?
Hàm ảo và Ghi đè (Virtual Functions & Override)
#include <iostream> // Lớp kế thừa từ Rectangle: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
class Shape { // Hàm khởi tạo
public: Square (double side) : Rectangle(side, side){}
// Hàm thành viên ảo để tính diện tích
virtual double area() const = 0; // Định nghĩ lại hàm thành viên ảo area() để tính diện
}; tích hình vuông
double area() const override {
// Lớp kế thừa từ Shape: Hình chữ nhật return side * side; // hoặc return Rectangle::area();
class Rectangle : public Shape { }
protected: };
double length;
double width; int main(){
// Tạo một hình chữ nhật
public: Rectangle rectangle(4, 5);
// Hàm khởi tạo // Tính diện tích hình chữ nhật
Rectangle (double l, double w) : length(l), width(w){} std::cout << "Diện tích hình chữ nhật: " <<
rectangle.area() << std::endl;
// Định nghĩ lại hàm thành viên ảo area() để tính diện
tích hình chữ nhật // Tạo một hình chữ nhật
double area() const override { Square square(3);
return length * width; // Tính diện tích hình vuông
} std::cout << "Diện tích hình vuông: " << square.area()
}; << std::endl;

return 0;
}
Hàm ảo và Ghi đè (Virtual Functions & Override)
#include <iostream> // Lớp kế thừa từ Rectangle: Hình vuông
class Square : public Shape {
// Lớp cơ sở public:
class Shape { // Hàm khởi tạo
public: Square (double side) : Rectangle(side, side){}
// Hàm thành viên ảo để tính diện tích
virtual double area() const = 0; // Định nghĩ lại hàm thành viên ảo area() để tính diện
}; tích hình vuông
double area() const override {
// Lớp kế thừa từ Shape: Hình chữ nhật return side * side; // hoặc return Rectangle::area();
class Rectangle : public Shape { }
protected: };
double length;
double width; int main(){
// Tạo một hình chữ nhật
public: Rectangle rectangle(4, 5);
// Hàm khởi tạo // Tính diện tích hình chữ nhật
Rectangle (double l, double w) : length(l), width(w){} std::cout << "Diện tích hình chữ nhật: " <<
rectangle.area() << std::endl;
// Định nghĩ lại hàm thành viên ảo area() để tính diện
tích hình chữ nhật // Tạo một hình chữ nhật
double area() const override { Square square(3);
return length * width; // Tính diện tích hình vuông
} std::cout << "Diện tích hình vuông: " << square.area()
}; << std::endl;

return 0;
}
Ưu điểm của hàm virtual so vs việc ghi đè
• Tiện lợi cho quản lý mã nguồn lớn
• Khả năng mở rộng dễ dàng
• Tính đa hình linh hoạt
• Giúp tránh nhầm lẫn

Cũng có một số tình huống mà việc sử dụng ghi đè có thể được ưu


tiên:
• Hiệu suất
• Phức tạp ít hơn

You might also like