Lớp trừu tượng và interface
Đôi khi một lớp cơ sở chỉ muốn quy định hợp đồng ("mọi hình đều phải tính được
diện tích") mà không có cách tính chung. Khi đó ta dùng hàm ảo thuần
(= 0): lớp trở thành trừu tượng, không tạo đối tượng trực tiếp được, và bắt
buộc lớp con phải cài đặt.
Đặt vấn đề
Giả sử bạn viết phần mềm thu học phí. Sinh viên trả bằng chuyển khoản, bằng ví điện tử, hoặc bằng tiền mặt. Ba cách khác nhau hoàn toàn ở phần thực hiện, nhưng với phần còn lại của chương trình chúng đều là "một cách thanh toán".
Nếu bạn cho lớp cơ sở ThanhToan một hàm thucHien() có thân mặc định, bạn sẽ vướng ngay câu
hỏi: thân hàm đó nên làm gì? Không có câu trả lời hợp lý, vì "thanh toán nói chung" không tồn
tại. Tệ hơn, ai đó thêm phương thức mới mà quên override thì chương trình vẫn biên dịch trơn
tru rồi âm thầm chạy thân hàm vô nghĩa, và lỗi chỉ lộ ra khi tiền của sinh viên đã bị treo.
Hàm ảo thuần giải quyết đúng chuyện đó: nó nói "mọi cách thanh toán đều phải làm được việc này, nhưng tôi không biết làm thế nào, lớp con phải tự lo". Trình biên dịch trở thành người gác cổng. Từ "trừu tượng" ở đây nghĩa là "chưa đủ cụ thể để tồn tại": bạn vẽ được hình tròn, hình vuông, chứ không vẽ được "một hình" chung chung.
Thử ngay: xem lớp trừu tượng chạy từng bước
Hãy bấm ▶ Chạy (hoặc kéo thanh trượt) trước khi đọc lý thuyết. Chưa cần hiểu ký hiệu
= 0, chỉ cần nhìn một chỗ: hàm in chỉ viết đúng một lần và chỉ biết tới Hinh, thế mà
mỗi lần gọi nó lại ra một cách tính diện tích khác. Các mục bên dưới sẽ mổ xẻ từng phần.
1class Hinh {2public:3 virtual double dienTich() const = 0;4 virtual ~Hinh() = default;5};6class HinhTron : public Hinh {7public:8 double r;9 HinhTron(double r) : r(r) {}10 double dienTich() const override { return 3.14*r*r; }11};12class HinhVuong : public Hinh {13public:14 double a;15 HinhVuong(double a) : a(a) {}16 double dienTich() const override { return a*a; }17};18void in(const Hinh& h) { cout << h.dienTich() << "\n"; }1920int main() {21 HinhTron t(2);22 HinhVuong v(3);23 in(t);24 in(v);25}
Ý tưởng cốt lõi
virtual double dienTich() const = 0;là hàm ảo thuần, chỉ có chữ ký, không có thân.- Lớp chứa hàm ảo thuần là lớp trừu tượng:
Hinh h;sẽ lỗi biên dịch. - Mọi lớp con phải override hàm đó thì mới tạo đối tượng được. Đây chính là interface.
Mã nguồn
#include <iostream>
using namespace std;
class Hinh { // interface
public:
virtual double dienTich() const = 0; // hàm ảo thuần
virtual ~Hinh() = default;
};
class HinhTron : public Hinh {
public:
double r;
HinhTron(double r) : r(r) {}
double dienTich() const override { return 3.14 * r * r; }
};
class HinhVuong : public Hinh {
public:
double a;
HinhVuong(double a) : a(a) {}
double dienTich() const override { return a * a; }
};
void in(const Hinh& h) { cout << h.dienTich() << "\n"; }
int main() {
HinhTron t(2);
HinhVuong v(3);
in(t); // 12.56
in(v); // 9
}
Hàm ảo thuần hoạt động ra sao
Cú pháp virtual double dienTich() const = 0; gồm ba phần. virtual nói rằng lời gọi sẽ được
điều phối động, tức là chọn phiên bản theo kiểu thực sự của đối tượng lúc chạy. Phần = 0
không có nghĩa "trả về 0", nó chỉ là ký hiệu quy ước để nói "hàm này không có thân ở lớp cha".
Phần const cho biết hàm không đổi trạng thái đối tượng, nhờ vậy gọi được trên const Hinh&.
Lớp có ít nhất một hàm ảo thuần chưa cài đặt là lớp trừu tượng, và trình biên dịch từ chối mọi cố gắng tạo đối tượng từ nó. Nhưng bạn vẫn được phép khai báo con trỏ và tham chiếu tới nó, đây chính là điểm làm nên giá trị của lớp trừu tượng:
Hinh h; // loi bien dich: Hinh la lop truu tuong
Hinh* p = new HinhTron(2); // hop le: con tro kieu cha, doi tuong kieu con
Con trỏ và tham chiếu không phải là đối tượng, chúng chỉ gọi tên một đối tượng đang tồn tại ở đâu đó. Vì đối tượng thật luôn thuộc một lớp con cụ thể, mọi hàm ảo thuần đều đã có cài đặt, nên lời gọi luôn tìm được đích đến. Lớp con kế thừa mà không override đủ hàm ảo thuần thì bản thân nó cũng vẫn là lớp trừu tượng.
Nếu bạn viết class HinhTamGiac : public Hinh {}; mà quên dienTich(), dòng khai báo lớp vẫn
biên dịch bình thường. Lỗi chỉ nổ ra ở chỗ bạn tạo đối tượng HinhTamGiac t;, với thông báo
kiểu "cannot declare variable to be of abstract type". Sinh viên hay lao vào sửa đúng dòng bị
báo lỗi. Nơi cần sửa là thân lớp, hãy thêm hàm ảo thuần còn thiếu vào đó.
Interface trong C++
Java và C# có từ khóa interface riêng. C++ không có, nhưng đạt cùng hiệu quả bằng một quy ước
giản dị: một lớp chỉ gồm các hàm ảo thuần và một hủy tử ảo, không có dữ liệu thành viên, chính
là một interface. Nó không mang trạng thái, không ràng buộc lớp con bố trí dữ liệu ra sao, chỉ
nói "ai kế thừa tôi thì phải làm được việc này". Đó là tinh thần của interface: mô tả khả năng,
không mô tả cấu tạo.
Lớp trừu tượng thì tổng quát hơn: nó được phép có dữ liệu, có constructor, và có cả hàm cài sẵn dùng chung. Ví dụ dưới đây vừa bắt lớp con cài phần riêng, vừa cho sẵn phần chung:
#include <iostream>
#include <string>
#include <utility>
using namespace std;
class NhanVien {
protected:
string ten;
double luong_co_ban;
public:
NhanVien(string ten, double luong_co_ban)
: ten(move(ten)), luong_co_ban(luong_co_ban) {}
virtual double thuong() const = 0; // each role computes its bonus differently
double tongLuong() const { // shared logic lives in the abstract base
return luong_co_ban + thuong(); // dynamic dispatch happens here
}
const string& layTen() const { return ten; }
virtual ~NhanVien() = default;
};
class LapTrinhVien : public NhanVien {
int so_du_an;
public:
LapTrinhVien(string ten, double luong, int so_du_an)
: NhanVien(move(ten), luong), so_du_an(so_du_an) {}
double thuong() const override { return so_du_an * 500000.0; }
};
class QuanLy : public NhanVien {
int so_nhan_vien;
public:
QuanLy(string ten, double luong, int so_nhan_vien)
: NhanVien(move(ten), luong), so_nhan_vien(so_nhan_vien) {}
double thuong() const override { return so_nhan_vien * 300000.0 + 2000000.0; }
};
int main() {
LapTrinhVien a("An", 15000000, 3);
QuanLy b("Binh", 20000000, 5);
const NhanVien* danh_sach[] = { &a, &b };
for (const NhanVien* nv : danh_sach)
cout << nv->layTen() << ": " << nv->tongLuong() << "\n";
}
Hãy để ý tongLuong(). Nó nằm ở lớp cha, viết một lần duy nhất, nhưng bên trong lại gọi
thuong() là hàm ảo thuần, nên khi chạy lời gọi nhảy xuống đúng lớp con. Mẫu này gọi là
template method: khung thuật toán do lớp cha giữ, phần thay đổi giao xuống lớp con. Quy tắc
"tổng lương bằng lương cơ bản cộng thưởng" nhờ vậy chỉ tồn tại ở một chỗ.
Đa kế thừa interface
Một lớp C++ có thể kế thừa nhiều lớp cha. Đa kế thừa từ các lớp có dữ liệu thường gây rắc rối (bài toán kim cương, dữ liệu bị nhân đôi), nhưng đa kế thừa từ các interface thì gần như luôn an toàn, vì interface không mang dữ liệu nên không có gì để nhân đôi. Ý tưởng: một đối tượng có thể mang nhiều khả năng độc lập nhau. Một nút bấm vừa vẽ được ra màn hình, vừa lưu được xuống tệp cấu hình.
#include <iostream>
#include <string>
#include <vector>
#include <utility>
using namespace std;
class VeDuoc { // capability 1: can be drawn
public:
virtual void ve() const = 0;
virtual ~VeDuoc() = default;
};
class LuuDuoc { // capability 2: can be serialized
public:
virtual string chuoiLuu() const = 0;
virtual ~LuuDuoc() = default;
};
class Nut : public VeDuoc, public LuuDuoc { // a button has both capabilities
string nhan;
public:
explicit Nut(string nhan) : nhan(move(nhan)) {}
void ve() const override { cout << "[ " << nhan << " ]\n"; }
string chuoiLuu() const override { return "Nut:" + nhan; }
};
void veTatCa(const vector<VeDuoc*>& ds) {
for (const VeDuoc* v : ds) v->ve(); // only the drawing capability is needed
}
int main() {
Nut ok("OK"), huy("Huy");
vector<VeDuoc*> man_hinh{ &ok, &huy };
veTatCa(man_hinh); // [ OK ] roi [ Huy ]
cout << ok.chuoiLuu() << "\n"; // saving uses the other interface
}
Hàm veTatCa chỉ nhận VeDuoc*, nó không biết và không cần biết rằng Nut còn lưu được xuống
tệp. Mỗi hàm chỉ đòi hỏi đúng khả năng nó cần. Nguyên tắc này gọi là phân tách interface: giữ các
interface nhỏ và tập trung, thay vì gộp mọi thứ vào một lớp cha khổng lồ mà lớp con nào cũng phải
cài đặt cả những hàm nó không dùng tới.
Khi nào dùng lớp trừu tượng, khi nào dùng lớp cơ sở thường
Không phải cứ có kế thừa là phải trừu tượng. Câu hỏi để tự quyết định rất đơn giản: lớp cha này có tồn tại một mình một cách có nghĩa hay không.
| Tình huống | Nên chọn |
|---|---|
| Có một hành vi mặc định hợp lý cho mọi lớp con | lớp cơ sở thường, hàm virtual có thân |
| Không có cách cài đặt chung nào hợp lý | lớp trừu tượng, hàm ảo thuần |
Lớp cha tự nó là một đối tượng có nghĩa, ví dụ NhanVien chính thức | lớp cơ sở thường |
Lớp cha chỉ là khái niệm, ví dụ Hinh, ThanhToan | lớp trừu tượng |
| Muốn trình biên dịch bắt lỗi khi lớp con quên cài đặt | lớp trừu tượng |
| Chỉ mô tả khả năng, không mô tả cấu tạo, không có dữ liệu | interface, toàn hàm ảo thuần |
| Cần lớp cha giữ dữ liệu dùng chung | lớp trừu tượng có dữ liệu, không dùng interface thuần |
Một dấu hiệu thực dụng: nếu bạn đang phải nghĩ ra một thân hàm mặc định vô nghĩa chỉ để mã
biên dịch được, ví dụ return 0; hay để thân hàm rỗng, thì gần như chắc chắn hàm đó phải là
hàm ảo thuần. Thân hàm giả tạo là một cái bẫy im lặng: nó biến lỗi quên override thành một kết
quả sai âm thầm thay vì một lỗi biên dịch rõ ràng.
Đa hình chỉ hoạt động qua con trỏ hoặc tham chiếu. Nếu truyền theo giá trị, ví dụ
void in(Hinh h) thay vì void in(const Hinh& h), phần dữ liệu của lớp con sẽ bị cắt bỏ, chỉ
còn phần lớp cha. Hiện tượng đó gọi là object slicing. Với lớp trừu tượng bạn được cứu một
nửa: trình biên dịch báo lỗi ngay vì không thể tạo đối tượng Hinh. Với lớp cơ sở thường thì
mã vẫn biên dịch và chạy sai. Quy tắc an toàn: đa hình thì luôn dùng const Hinh& hoặc Hinh*.
Hủy tử ảo là bắt buộc
Đây là lỗi kinh điển nhất khi làm việc với lớp trừu tượng, và cũng khó phát hiện nhất vì chương trình vẫn chạy, chỉ rò rỉ bộ nhớ âm thầm.
Khi bạn viết delete p với p kiểu Hinh*, trình biên dịch phải quyết định gọi hủy tử nào. Nếu
~Hinh() không ảo, nó gọi ~Hinh() rồi dừng, phần dữ liệu riêng của lớp con không bao giờ
được dọn, và theo chuẩn C++ đây là hành vi không xác định. Nếu ~Hinh() là ảo, lời gọi được điều
phối động: hủy tử lớp con chạy trước, rồi hủy tử lớp cha chạy tiếp theo tự động.
#include <iostream>
#include <string>
using namespace std;
class TaiNguyen {
public:
virtual void chay() const = 0;
virtual ~TaiNguyen() = default; // WITHOUT virtual, ~Tep below would never run
};
class Tep : public TaiNguyen {
string* ten; // resource owned by the derived class
public:
explicit Tep(const string& t) : ten(new string(t)) {}
void chay() const override { cout << "Mo tep " << *ten << "\n"; }
~Tep() override {
cout << "Dong tep " << *ten << "\n";
delete ten; // only runs if the base dtor is virtual
}
};
int main() {
TaiNguyen* p = new Tep("hocphi.csv");
p->chay();
delete p; // virtual dtor: ~Tep() runs first, then ~TaiNguyen()
}
Nếu bạn xóa từ khóa virtual ở ~TaiNguyen(), chương trình vẫn in "Mo tep hocphi.csv" như
thường, nhưng dòng "Dong tep" biến mất và chuỗi cấp bằng new không bao giờ được giải phóng.
Không có thông báo lỗi nào, chỉ có bộ nhớ rò rỉ dần theo mỗi lần chạy.
Quy tắc gọn cần thuộc: một lớp đã có hàm ảo thì phải có hủy tử ảo. Trong lớp trừu tượng, viết
virtual ~Ten() = default; là đủ và đúng. Tốt hơn nữa, hãy dùng std::vector<std::unique_ptr<Hinh>>
thay cho vector<Hinh*>: bạn khỏi phải delete bằng tay và không thể quên. Nhưng hủy tử ảo vẫn
bắt buộc, vì unique_ptr biết gọi delete đúng lúc mà vẫn phải dựa vào hủy tử ảo để dọn được
phần lớp con.
Bài tập thực hành
Bài tập 1: thêm lớp hình thứ ba
Dựa trên lớp trừu tượng Hinh ở trên, hãy thêm lớp HinhChuNhat có hai cạnh rong và cao, cài đặt dienTich() trả về rong * cao. Tạo một đối tượng HinhChuNhat rồi truyền cho hàm in để in diện tích.
Lời giải chi tiết
Lớp Hinh quy định hợp đồng: mọi hình phải có dienTich(). Để thêm một hình mới, ta chỉ cần kế thừa public Hinh và override đúng hàm đó. Không phải sửa gì ở hàm in, vì in chỉ làm việc với const Hinh& và dựa vào đa hình để gọi đúng phiên bản.
Các bước:
- Khai báo
class HinhChuNhat : public Hinh. - Thêm hai trường
rong,caovà constructor để gán giá trị. - Override
dienTich()trả vềrong * cao. - Trong
main, tạo đối tượng và gọiin.
#include <iostream>
using namespace std;
class Hinh {
public:
virtual double dienTich() const = 0;
virtual ~Hinh() = default;
};
class HinhChuNhat : public Hinh {
public:
double rong, cao;
HinhChuNhat(double rong, double cao) : rong(rong), cao(cao) {}
double dienTich() const override { return rong * cao; } // override the contract
};
void in(const Hinh& h) { cout << h.dienTich() << "\n"; }
int main() {
HinhChuNhat hcn(2, 5);
in(hcn); // 10
}
Ý chính: nhờ lớp trừu tượng, mỗi loại hình mới chỉ cần override dienTich() mà không đụng tới mã đã có.
Bài tập 2: interface với hai hàm ảo thuần
Mở rộng Hinh thành interface có hai hàm ảo thuần: chuVi() và dienTich(). Cài đặt cho HinhTron (chu vi 2 * 3.14 * r) và HinhVuong (chu vi 4 * a). Viết hàm inDayDu(const Hinh& h) in cả chu vi lẫn diện tích.
Lời giải chi tiết
Một interface có thể quy định nhiều hơn một hàm ảo thuần. Lúc này lớp con phải override tất cả các hàm ảo thuần thì mới hết trừu tượng và tạo được đối tượng. Nếu quên một hàm, lớp con vẫn là trừu tượng và sẽ lỗi biên dịch khi tạo đối tượng.
Các bước:
- Thêm
virtual double chuVi() const = 0;vàoHinh. - Trong mỗi lớp con, override cả
chuVi()vàdienTich(). - Hàm
inDayDunhậnconst Hinh&và gọi cả hai, mỗi lời gọi đều điều phối động.
#include <iostream>
using namespace std;
class Hinh {
public:
virtual double chuVi() const = 0; // pure virtual
virtual double dienTich() const = 0; // pure virtual
virtual ~Hinh() = default;
};
class HinhTron : public Hinh {
public:
double r;
HinhTron(double r) : r(r) {}
double chuVi() const override { return 2 * 3.14 * r; }
double dienTich() const override { return 3.14 * r * r; }
};
class HinhVuong : public Hinh {
public:
double a;
HinhVuong(double a) : a(a) {}
double chuVi() const override { return 4 * a; }
double dienTich() const override { return a * a; }
};
void inDayDu(const Hinh& h) {
cout << "chu vi = " << h.chuVi() << ", dien tich = " << h.dienTich() << "\n";
}
int main() {
HinhTron t(2);
HinhVuong v(3);
inDayDu(t); // chu vi = 12.56, dien tich = 12.56
inDayDu(v); // chu vi = 12, dien tich = 9
}
Ý chính: lớp con phải override mọi hàm ảo thuần của interface, nếu thiếu một hàm thì lớp con vẫn trừu tượng.
Bài tập 3: tính tổng diện tích qua vector con trỏ
Cho một vector<Hinh*> chứa hỗn hợp HinhTron và HinhVuong. Viết hàm tongDienTich(const vector<Hinh*>& ds) duyệt qua danh sách và trả về tổng diện tích. Nhớ giải phóng bộ nhớ sau khi dùng.
Lời giải chi tiết
Ta lưu con trỏ kiểu cha Hinh* để một danh sách có thể chứa nhiều loại hình khác nhau. Khi duyệt, mỗi lời gọi h->dienTich() điều phối động tới đúng lớp con. Vì cấp phát bằng new, ta phải delete từng phần tử; destructor ảo của Hinh đảm bảo phần lớp con được dọn đúng.
Các bước:
- Hàm
tongDienTichnhậnconst vector<Hinh*>&, cộng dồnh->dienTich()cho mỗi phần tử. - Trong
main, tạo các hình bằngnewrồi đẩy vào vector. - In tổng, sau đó
deletetừng con trỏ để tránh rò rỉ.
#include <iostream>
#include <vector>
using namespace std;
class Hinh {
public:
virtual double dienTich() const = 0;
virtual ~Hinh() = default;
};
class HinhTron : public Hinh {
public:
double r;
HinhTron(double r) : r(r) {}
double dienTich() const override { return 3.14 * r * r; }
};
class HinhVuong : public Hinh {
public:
double a;
HinhVuong(double a) : a(a) {}
double dienTich() const override { return a * a; }
};
double tongDienTich(const vector<Hinh*>& ds) {
double tong = 0;
for (const Hinh* h : ds) tong += h->dienTich(); // dynamic dispatch per element
return tong;
}
int main() {
vector<Hinh*> ds{ new HinhTron(2), new HinhVuong(3), new HinhTron(1) };
cout << tongDienTich(ds) << "\n"; // 12.56 + 9 + 3.14 = 24.7
for (Hinh* h : ds) delete h; // free memory, virtual dtor cleans up
}
Ý chính: chứa con trỏ kiểu cha trong một vector cho phép gom nhiều loại hình lại và xử lý đồng nhất qua đa hình.
Bài tập 4: một lớp mang hai interface
Xây interface ThanhToan với hàm ảo thuần bool thucHien(double so_tien), và interface
GhiNhat với hàm ảo thuần std::string nhatKy() const. Viết lớp ViDienTu cài đặt cả hai:
thucHien trả về true nếu số dư đủ (và trừ số dư), trả về false nếu không đủ; nhatKy
trả về chuỗi mô tả số dư còn lại. Trong main, gọi thucHien qua một con trỏ ThanhToan*.
Lời giải chi tiết
Mấu chốt: hai interface mô tả hai khả năng độc lập, nên ViDienTu kế thừa công khai cả hai.
Vì cả hai interface đều không có dữ liệu thành viên, đa kế thừa ở đây hoàn toàn an toàn, không
có bài toán kim cương. Mỗi interface phải có hủy tử ảo riêng, vì ta có thể hủy đối tượng qua
con trỏ của bất kỳ interface nào trong hai cái.
Các bước:
- Khai báo hai lớp chỉ chứa hàm ảo thuần và hủy tử ảo.
class ViDienTu : public ThanhToan, public GhiNhatvới một trườngso_du.- Override cả
thucHienlẫnnhatKy. - Trong
main, trỏ mộtThanhToan*vào đối tượngViDienTurồi gọi qua con trỏ đó.
#include <iostream>
#include <string>
using namespace std;
class ThanhToan { // capability: can pay
public:
virtual bool thucHien(double so_tien) = 0;
virtual ~ThanhToan() = default;
};
class GhiNhat { // capability: can report a log line
public:
virtual string nhatKy() const = 0;
virtual ~GhiNhat() = default;
};
class ViDienTu : public ThanhToan, public GhiNhat {
double so_du;
public:
explicit ViDienTu(double so_du) : so_du(so_du) {}
bool thucHien(double so_tien) override {
if (so_tien > so_du) return false; // not enough balance
so_du -= so_tien;
return true;
}
string nhatKy() const override {
return "So du con lai: " + to_string(so_du);
}
};
int main() {
ViDienTu vi(500000);
ThanhToan* tt = &vi; // use it through one interface
cout << (tt->thucHien(200000) ? "OK" : "That bai") << "\n"; // OK
cout << (tt->thucHien(999999) ? "OK" : "That bai") << "\n"; // That bai
cout << vi.nhatKy() << "\n"; // the other interface
}
Ý chính: một lớp có thể mang nhiều interface độc lập, và mỗi phần chương trình chỉ cần biết đúng interface nó dùng tới.
Tự kiểm tra
Vì sao destructor của Hinh nên là virtual?
Vì ta thường thao tác đối tượng con qua con trỏ/tham chiếu kiểu cha (Hinh*). Nếu
delete một Hinh* đang trỏ tới HinhTron mà destructor không ảo, chỉ
~Hinh() chạy, phần HinhTron không được dọn đúng → rò rỉ. Destructor ảo đảm bảo
gọi đúng destructor của lớp con.
Câu hỏi tự kiểm
- 1Ký hiệu = 0 sau một hàm ảo, ví dụ virtual double dienTich() const = 0; nghĩa là gì?
- 2Hinh là lớp trừu tượng. Dòng nào hợp lệ?
- 3Vì sao đa kế thừa nhiều interface (toàn hàm ảo thuần, không dữ liệu) lại an toàn, không sinh vấn đề kim cương?
- 4Viết class HinhTamGiac : public Hinh {}; mà quên cài dienTich() thì lỗi nổ ra ở đâu?
Tóm tắt
Những điều cần mang theo sau bài này:
- Hàm ảo thuần
= 0khai báo một hàm không có thân ở lớp cha, buộc lớp con phải cài đặt. - Lớp có hàm ảo thuần chưa cài đặt là lớp trừu tượng: không tạo đối tượng được, nhưng vẫn khai báo con trỏ và tham chiếu tới nó được, và đó chính là cách ta dùng nó.
- Interface trong C++ chỉ là quy ước: một lớp toàn hàm ảo thuần, không dữ liệu, có hủy tử ảo. Đa kế thừa nhiều interface là an toàn vì không có dữ liệu để nhân đôi.
- Lớp trừu tượng rộng hơn interface: được phép có dữ liệu và có hàm cài sẵn dùng chung, cho phép mẫu template method (khung ở cha, chi tiết ở con).
- Chọn lớp trừu tượng khi lớp cha chỉ là khái niệm và không có cài đặt mặc định hợp lý. Chọn lớp cơ sở thường khi lớp cha tự nó đã là một đối tượng có nghĩa.
- Hủy tử ảo là bắt buộc với mọi lớp có hàm ảo. Thiếu nó, hủy đối tượng con qua con trỏ cha là hành vi không xác định và thường dẫn tới rò rỉ bộ nhớ im lặng.
- Đa hình chỉ chạy qua con trỏ và tham chiếu. Truyền theo giá trị gây cắt lát đối tượng.
Hàm ảo thuần = 0 biến lớp thành trừu tượng (interface): chỉ định hợp đồng,
bắt lớp con cài đặt, và không tạo đối tượng trực tiếp. Đây là cách C++ diễn đạt "mọi
X đều phải làm được việc Y, nhưng mỗi loại làm theo cách riêng".