Chuyển tới nội dung chính

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.

Một interface Hinh, nhiều cài đặt cụ thể C++
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"; }
19
20int main() {
21 HinhTron t(2);
22 HinhVuong v(3);
23 in(t);
24 in(v);
25}
Ngăn xếp stack
main()
#1HinhTron
r2
#2HinhVuong
a3
Bộ nhớ động heap
(trống)
Hinh là TRỪU TƯỢNG (có hàm ảo thuần) nên KHÔNG thể viết Hinh h;. Ta tạo các đối tượng CỤ THỂ: t (HinhTron r=2) và v (HinhVuong a=3).
1/5

Ý tưởng cốt lõi

  • virtual double dienTich() const = 0;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.

Quên override thì lỗi báo ở đâu

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ốngNên chọn
Có một hành vi mặc định hợp lý cho mọi lớp conlớ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ứclớp cơ sở thường
Lớp cha chỉ là khái niệm, ví dụ Hinh, ThanhToanlớ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 đặtlớp trừu tượng
Chỉ mô tả khả năng, không mô tả cấu tạo, không có dữ liệuinterface, toàn hàm ảo thuần
Cần lớp cha giữ dữ liệu dùng chunglớ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.

Cắt lát đối tượ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 rongcao, 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:

  1. Khai báo class HinhChuNhat : public Hinh.
  2. Thêm hai trường rong, cao và constructor để gán giá trị.
  3. Override dienTich() trả về rong * cao.
  4. Trong main, tạo đối tượng và gọi in.
#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()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:

  1. Thêm virtual double chuVi() const = 0; vào Hinh.
  2. Trong mỗi lớp con, override cả chuVi()dienTich().
  3. Hàm inDayDu nhận const 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 HinhTronHinhVuong. 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:

  1. Hàm tongDienTich nhận const vector<Hinh*>&, cộng dồn h->dienTich() cho mỗi phần tử.
  2. Trong main, tạo các hình bằng new rồi đẩy vào vector.
  3. In tổng, sau đó delete từ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:

  1. Khai báo hai lớp chỉ chứa hàm ảo thuần và hủy tử ảo.
  2. class ViDienTu : public ThanhToan, public GhiNhat với một trường so_du.
  3. Override cả thucHien lẫn nhatKy.
  4. Trong main, trỏ một ThanhToan* vào đối tượng ViDienTu rồ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

Kiểm tra nhanh: lớp trừu tượng và interface0/4 đúngchưa trả lời
  1. 1Ký hiệu = 0 sau một hàm ảo, ví dụ virtual double dienTich() const = 0; nghĩa là gì?
  2. 2Hinh là lớp trừu tượng. Dòng nào hợp lệ?
  3. 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?
  4. 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 = 0 khai 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.
Ghi nhớ

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".