Skip to content

Chương 6: Kế Thừa (Inheritance)¤


1. Quan hệ giữa các lớp đối tượng¤

Trong lập trình hướng đối tượng, các lớp không tồn tại độc lập mà có mối quan hệ với nhau. Có 4 loại quan hệ chính:


1.1 Quan hệ Một - Một (1-1)¤

Khái niệm: Một đối tượng thuộc lớp A quan hệ với đúng một đối tượng thuộc lớp B, và ngược lại.

Ví dụ thực tế:

  • LOPHOCGIAOVIEN (quan hệ Chủ nhiệm): Mỗi lớp học có đúng một giáo viên chủ nhiệm, mỗi giáo viên chủ nhiệm phụ trách đúng một lớp.
  • VOCHONG (quan hệ Hôn nhân)
  • COUNTRYCAPITAL (quan hệ Có thủ đô)
Text Only
ClassA ———————— ClassB

1.2 Quan hệ Một - Nhiều (1-n)¤

Khái niệm: Một đối tượng thuộc lớp A có quan hệ với nhiều đối tượng thuộc lớp B, nhưng mỗi đối tượng lớp B chỉ quan hệ với đúng một đối tượng lớp A.

Ví dụ thực tế:

  • LOPHOCHOCSINH: Một lớp học có nhiều học sinh, nhưng mỗi học sinh chỉ thuộc một lớp.
  • CONGTYNHANVIEN: Một công ty có nhiều nhân viên.
  • HOASITACPHAM: Một họa sĩ có nhiều tác phẩm.
Text Only
ClassA ———————<< ClassB

1.3 Quan hệ Nhiều - Nhiều (n-n)¤

Khái niệm: Một đối tượng lớp A có quan hệ với nhiều đối tượng lớp B, và ngược lại một đối tượng lớp B cũng có quan hệ với nhiều đối tượng lớp A.

Ví dụ thực tế:

  • NAMNU (quan hệ Yêu): Một người nam có thể yêu nhiều người nữ và ngược lại.
  • BACSIBENHNHAN (quan hệ Khám bệnh): Một bác sĩ khám nhiều bệnh nhân, một bệnh nhân khám nhiều bác sĩ.
Text Only
ClassA >>———————<< ClassB

1.4 Quan hệ Đặc biệt hóa – Tổng quát hóa¤

Khái niệm: Lớp A là trường hợp đặc biệt của lớp B, và lớp B là trường hợp tổng quát của lớp A. Đây là nền tảng của kế thừa.

Ví dụ:

  • TAMGIACCAN là trường hợp đặc biệt của TAMGIAC
  • SINHVIEN là trường hợp đặc biệt của NGUOI
graph TD
    Animal --> TerrestrialAnimal
    Animal --> AquaticAnimal
    TerrestrialAnimal --> Cat
    TerrestrialAnimal --> Horse
    TerrestrialAnimal --> Chicken
    AquaticAnimal --> Fish
    AquaticAnimal --> Octopus
    Polygon --> Triangle
    Polygon --> Rectangle
Hold "Alt" / "Option" to enable pan & zoom

2. Kế thừa (Inheritance)¤

2.1 Định nghĩa¤

Kế thừa là một cơ chế của ngôn ngữ lập trình dùng để biểu diễn mối quan hệ đặc biệt hóa – tổng quát hóa giữa các lớp. Nhờ kế thừa, ta có thể:

  • Xây dựng lớp mới dựa trên lớp đã có, tái sử dụng code.
  • Tổ chức các lớp thành sơ đồ phân cấp (cây kế thừa).
  • Dễ dàng sửa chữa, nâng cấp hệ thống — chỉ cần sửa ở lớp cha là tất cả lớp con được hưởng.
  • Trong C++, kế thừa còn cho phép chuyển kiểu tự động giữa lớp cha và lớp con.

2.2 Thuật ngữ¤

Thuật ngữ Ý nghĩa
Lớp cha / Lớp cơ sở (Base class / Superclass) Lớp được kế thừa
Lớp con / Lớp dẫn xuất (Derived class / Subclass) Lớp kế thừa từ lớp cha

2.3 Quan hệ "là một" (is-a)¤

Kế thừa thường được dùng để biểu diễn mối quan hệ "là một":

  • Một Sinh viên là một Người
  • Một Hình tròn là một Hình ellipse
  • Một Tam giác là một Đa giác

Lưu ý quan trọng: Không phải mọi quan hệ đều nên dùng kế thừa. Chỉ dùng kế thừa khi thỏa mãn đúng quan hệ "là một".


3. Kế thừa đơn (Single Inheritance)¤

3.1 Cú pháp¤

C++
class LopCha {
    // Thành phần của lớp cơ sở
};

class LopCon : public/protected/private LopCha {
    // Thành phần bổ sung của lớp dẫn xuất
};

Có 3 kiểu kế thừa: public, protected, private — sẽ giải thích chi tiết ở phần phạm vi truy xuất.


3.2 Ví dụ: Người và Sinh Viên¤

Xét bài toán: Sinh viên là một Người, nhưng có thêm thông tin mã số.

C++
// Lớp cha
class Nguoi {
    char *HoTen;
    int NamSinh;
public:
    Nguoi() {}
    Nguoi(char *ht, int ns) : NamSinh(ns) {
        HoTen = strdup(ht);
    }
    ~Nguoi() { delete[] HoTen; }

    void An() const { cout << HoTen << " an 3 chen com\n"; }
    void Ngu() const { cout << HoTen << " ngu ngay 8 tieng\n"; }
    void Xuat() const;

    friend ostream& operator<<(ostream &os, Nguoi &p);
};
C++
// Lớp con kế thừa từ Nguoi
class SinhVien : public Nguoi {
    char *MaSo;
public:
    SinhVien() {}
    SinhVien(char *ht, char *ms, int ns) : Nguoi(ht, ns) {
        MaSo = strdup(ms);
    }
    ~SinhVien() { delete[] MaSo; }

    void Xuat() const; // Định nghĩa lại (override) phương thức Xuat
};
C++
// Triển khai
void Nguoi::Xuat() const {
    cout << "Nguoi, ho ten: " << HoTen;
    cout << " sinh " << NamSinh << endl;
}

void SinhVien::Xuat() const {
    cout << "Sinh vien, ma so: " << MaSo << endl;
    // Không thể in HoTen trực tiếp vì HoTen là private của Nguoi
}
C++
// Sử dụng
int main() {
    Nguoi p1("Le Van Nhan", 1980);
    SinhVien s1("Vo Vien Sinh", "200002541", 1984);

    // SinhVien kế thừa phương thức An() từ Nguoi
    p1.An();   // Le Van Nhan an 3 chen com
    s1.An();   // Vo Vien Sinh an 3 chen com  <-- kế thừa!

    // Xuat() được định nghĩa lại ở SinhVien
    p1.Xuat(); // Gọi Nguoi::Xuat()
    s1.Xuat(); // Gọi SinhVien::Xuat()

    // Gọi tường minh phương thức của lớp cha
    s1.Nguoi::Xuat(); // Gọi trực tiếp Nguoi::Xuat() qua đối tượng SinhVien
}

3.3 Những gì lớp con kế thừa¤

Khi SinhVien kế thừa Nguoi:

Về dữ liệu: Mỗi đối tượng SinhVien tự động có HoTenNamSinh (dù không khai báo lại).

Về thao tác: Lớp con kế thừa tất cả các phương thức của lớp cha — đây là tái sử dụng code. Khả năng này có thể truyền qua vô hạn mức (lớp cháu, lớp chắt,... đều được hưởng).

Ngoại lệ: Phương thức thiết lập (constructor) không được kế thừa. Lớp con phải tự định nghĩa constructor của mình.


3.4 Cây kế thừa nhiều mức¤

graph TD
    NGUOI --> GIAOVIEN
    NGUOI --> SINHVIEN
    SINHVIEN --> NAMSINH
    SINHVIEN --> NUSINH
Hold "Alt" / "Option" to enable pan & zoom

Ở đây, SinhVien vừa là lớp con của Nguoi, vừa là lớp cha của NamSinhNuSinh.


3.5 Ràng buộc ngữ nghĩa ở lớp con¤

Đôi khi lớp con kế thừa từ lớp cha nhưng cần ràng buộc thêm điều kiện cho dữ liệu.

Ví dụ: Số ảoSố phức nhưng phần thực luôn bằng 0.

C++
class Complex {
    friend ostream& operator<<(ostream&, Complex);
    friend class Imag;
    double re, im;
public:
    Complex(double r = 0, double i = 0) : re(r), im(i) {}
    Complex operator+(Complex b);
    Complex operator-(Complex b);
    double Norm() const { return sqrt(re*re + im*im); }
};
C++
class Imag : public Complex {
public:
    // Constructor đảm bảo phần thực luôn = 0
    Imag(double i = 0) : Complex(0, i) {}

    // Khi gán từ Complex, chỉ lấy phần ảo, bỏ phần thực
    Imag(const Complex &c) : Complex(0, c.im) {}

    Imag& operator=(const Complex &c) {
        re = 0;   // Ràng buộc: phần thực luôn = 0
        im = c.im;
        return *this;
    }

    // Override Norm() vì số ảo có Norm riêng
    double Norm() const { return fabs(im); }
};
C++
int main() {
    Imag i = 1;            // i = (0 + 1i)
    Complex z1(1, 1);      // z1 = (1 + 1i)
    Complex z3 = z1 - i;   // z3 = (1 + 0i)

    i = Complex(5, 2);     // i = (0 + 2i)  <-- phần thực bị bỏ!
    Imag j = z1;           // j = (0 + 1i)  <-- chỉ lấy phần ảo

    cout << "z1 = " << z1 << "\n";
    cout << "i = " << i << "\n";
    cout << "j = " << j << "\n";
}

Điểm mấu chốt: Bất cứ thao tác nào có thể vi phạm ràng buộc (như phép gán) đều phải được định nghĩa lại ở lớp con.


4. Phạm vi truy xuất trong kế thừa¤

4.1 Hai chiều truy xuất¤

Có hai câu hỏi cần trả lời:

Chiều dọc: Hàm thành phần của lớp con có quyền truy xuất các thành phần của lớp cha không? → Do thuộc tính khai báo ở lớp cha quyết định (private, protected, public).

Chiều ngang: Sau khi kế thừa, thế giới bên ngoài có quyền truy xuất thành phần của lớp cha thông qua đối tượng lớp con không? → Do kiểu kế thừa (public, protected, private) quyết định.


4.2 Ba thuộc tính truy cập¤

private¤

  • Chỉ có hàm thành phần của chính lớp đóhàm bạn (friend) được truy xuất.
  • Lớp con không được phép truy xuất.

protected¤

  • Thế giới bên ngoài không được truy xuất (giống private).
  • Nhưng tất cả lớp con, cháu... đều được phép truy xuất (khác private).

public¤

  • Bất kỳ ai cũng được truy xuất.

Câu hỏi kiểm tra: Cho đoạn code sau, cho biết câu lệnh nào đúng, câu lệnh nào sai?

C++
class A {
private:
    int a;
    void f();
protected:
    int b;
    void g();
public:
    int c;
    void h();
};

int main() {
    A x;
    x.a = 10;  // SAI: a là private, không truy xuất từ ngoài
    x.f();     // SAI: f() là private
    x.b = 20;  // SAI: b là protected, không truy xuất từ ngoài
    x.g();     // SAI: g() là protected
    x.c = 30;  // ĐÚNG: c là public
    x.h();     // ĐÚNG: h() là public
}

4.3 Vấn đề với private và lớp con¤

C++
class Nguoi {
    char *HoTen;   // private -> lớp con KHÔNG được truy xuất
    int NamSinh;
public:
    // ...
};

class SinhVien : public Nguoi {
    char *MaSo;
public:
    void Xuat() const;
};

void SinhVien::Xuat() const {
    // LỖI! HoTen là private của Nguoi, SinhVien không được truy xuất
    cout << "Sinh vien, ma so: " << MaSo << ", ho ten: " << HoTen;
}

Cách khắc phục tạm thời — dùng friend:

C++
class Nguoi {
    friend class SinhVien; // Cho phép SinhVien truy xuất private
    char *HoTen;
    int NamSinh;
public:
    // ...
};

Nhược điểm: Mỗi khi thêm lớp con mới (NuSinh, NamSinh,...) lại phải sửa lớp cha để khai báo thêm friend. Điều này vi phạm tính đóng gói và gây khó bảo trì.

Cách khắc phục đúng đắn — dùng protected:

C++
class Nguoi {
protected:          // Lớp con được truy xuất, thế giới ngoài thì không
    char *HoTen;
    int NamSinh;
public:
    // ...
};

class SinhVien : public Nguoi {
protected:
    char *MaSo;
public:
    void Xuat() const;
};

void SinhVien::Xuat() const {
    // OK! HoTen là protected, SinhVien được phép truy xuất
    cout << "Sinh vien, ma so: " << MaSo << ", ho ten: " << HoTen;
}

Nhờ protected, khi thêm NuSinh kế thừa SinhVien, lớp NuSinh cũng truy xuất được HoTen, NamSinh, MaSokhông cần sửa lớp cha:

C++
class NuSinh : public SinhVien {
public:
    NuSinh(char *ht, char *ms, int ns) : SinhVien(ht, ms, ns) {}

    void An() const {
        // Truy xuất HoTen (Nguoi::protected) và MaSo (SinhVien::protected)
        cout << HoTen << " ma so " << MaSo << " an 2 to pho";
    }
};

Nguyên tắc thực hành: Thông thường dùng protected cho thành phần dữ liệupublic cho phương thức. Tránh dùng friend để cho phép lớp con truy xuất — hãy dùng protected thay thế.


4.4 Ba kiểu kế thừa và ảnh hưởng đến phạm vi¤

Bảng tổng hợp đầy đủ:

Thuộc tính trong lớp cha Kế thừa public Kế thừa protected Kế thừa private
public public trong lớp con protected trong lớp con private trong lớp con
protected protected trong lớp con protected trong lớp con private trong lớp con
private Ẩn (không truy xuất trực tiếp) Ẩn (không truy xuất trực tiếp) Ẩn (không truy xuất trực tiếp)
graph LR
    subgraph "Kế thừa public"
        A1[public] --> B1[public]
        A2[protected] --> B2[protected]
        A3[private] --> B3[ẩn]
    end
    subgraph "Kế thừa protected"
        C1[public] --> D1[protected]
        C2[protected] --> D2[protected]
        C3[private] --> D3[ẩn]
    end
    subgraph "Kế thừa private"
        E1[public] --> F1[private]
        E2[protected] --> F2[private]
        E3[private] --> F3[ẩn]
    end
Hold "Alt" / "Option" to enable pan & zoom

Giải thích thực tế: - public inheritance (thường dùng nhất): Giữ nguyên phạm vi truy xuất. Dùng khi muốn thể hiện quan hệ "là một". - protected inheritance: Hạ tất cả public xuống protected. Lớp cháu vẫn truy xuất được, nhưng bên ngoài thì không. - private inheritance: Mọi thứ đều trở thành private. Lớp cháu không kế thừa được gì từ lớp ông nội trở lên.


4.5 Ví dụ minh họa kiểu kế thừa¤

Ví dụ 1 — Kế thừa public:

C++
class mother {
protected:
    int x, y;
public:
    void set(int a, int b);
private:
    int z;
};

class daughter : public mother {
private:
    double a;
public:
    void foo();
};

void daughter::foo() {
    x = y = 20;   // OK: x, y là protected trong mother -> được truy xuất ở lớp con
    set(5, 10);   // OK: set() là public -> được truy xuất
    cout << a;    // OK: a là thành phần riêng của daughter
    z = 100;      // LỖI! z là private của mother -> lớp con không truy xuất được
}

Ví dụ 2 — Kế thừa private:

C++
class son : private mother {
private:
    double b;
public:
    void foo();
};

void son::foo() {
    x = y = 20;   // OK: x, y là protected trong mother -> trong hàm thành phần của son vẫn truy xuất được
    set(5, 10);   // OK: tương tự
    cout << b;    // OK: b là thành phần riêng của son
    z = 100;      // LỖI! z là private của mother
}

int main() {
    son s;
    s.set(1, 2);  // LỖI! Kế thừa private -> set() trở thành private trong son
                  // Bên ngoài không truy xuất được
}

4.6 Truy cập phương thức khi có override¤

C++
class Point {
protected:
    int x, y;
public:
    void set(int a, int b) { x = a; y = b; }
    void foo();
    void print();
};

class Circle : public Point {
private:
    double r;
public:
    // Override set() với thêm tham số r
    void set(int a, int b, double c) {
        Point::set(a, b); // Gọi tường minh phương thức lớp cha
        r = c;
    }
    void print() { /* ... */ }
};

int main() {
    Circle C;
    C.set(10, 10, 100); // Gọi Circle::set() - 3 tham số
    C.foo();            // Gọi Point::foo() - kế thừa từ Point
    C.print();          // Gọi Circle::print() - đã override

    Point A;
    A.set(30, 50);      // Gọi Point::set() - 2 tham số
    A.print();          // Gọi Point::print()
}

5. Phương thức thiết lập và hủy bỏ trong kế thừa¤

5.1 Phương thức thiết lập (Constructor)¤

Constructor không được kế thừa. Khi tạo một đối tượng lớp con, constructor của lớp cha luôn được gọi trước.

Ví dụ 1 — Không truyền tham số cho lớp cha:

C++
class A {
public:
    A() { cout << "A:default" << endl; }
    A(int a) { cout << "A:parameter" << endl; }
};

class B : public A {
public:
    B(int a) {
        cout << "B" << endl;
        // Không chỉ định rõ -> tự động gọi A() (constructor mặc định)
    }
};

B test(1);
// Output:
// A:default   <- gọi A() trước
// B           <- sau đó mới khởi tạo B

Ví dụ 2 — Truyền tham số cho constructor lớp cha:

C++
class C : public A {
public:
    C(int a) : A(a) { // Chỉ định rõ: gọi A(int a)
        cout << "C" << endl;
    }
};

C test(1);
// Output:
// A:parameter  <- gọi A(int) trước
// C            <- sau đó mới khởi tạo C

Lưu ý quan trọng: Nếu lớp cha chỉ có constructor có tham số (không có constructor mặc định), thì lớp con bắt buộc phải có constructor và truyền tham số lên lớp cha qua danh sách khởi tạo (: LopCha(tham_so)).


5.2 Phương thức hủy bỏ (Destructor)¤

Khi một đối tượng bị hủy: 1. Destructor của lớp con được gọi trước. 2. Sau đó destructor của lớp cha được gọi tự động.

Vì vậy, lớp con chỉ cần dọn dẹp phần dữ liệu riêng của mình, không được và không cần dọn dẹp dữ liệu của lớp cha.

C++
class SinhVien : public Nguoi {
    char *MaSo;
public:
    SinhVien(char *ht, char *ms, int ns) : Nguoi(ht, ns) {
        MaSo = strdup(ms);
    }

    // Copy constructor - phải gọi copy constructor của lớp cha
    SinhVien(const SinhVien &s) : Nguoi(s) {
        MaSo = strdup(s.MaSo);
    }

    ~SinhVien() {
        delete[] MaSo;  // Chỉ xóa MaSo - phần riêng của SinhVien
        // HoTen sẽ được ~Nguoi() xóa tự động
    }
};

5.3 Con trỏ và kế thừa¤

C++
Nguoi *p;
SinhVien s("Nguyen Van A", "SV001", 2000);

p = &s;      // OK: Con trỏ lớp cha có thể trỏ đến đối tượng lớp con

SinhVien *sv;
Nguoi ng("Tran B", 1980);
sv = &ng;    // LỖI: Con trỏ lớp con KHÔNG thể trỏ đến đối tượng lớp cha

// Ép kiểu (nguy hiểm, cần cẩn thận):
sv = (SinhVien*) &ng;  // Có thể biên dịch nhưng nguy hiểm về mặt ngữ nghĩa

6. Đa kế thừa (Multiple Inheritance)¤

6.1 Khái niệm¤

Đa kế thừa cho phép một lớp kế thừa từ nhiều lớp cơ sở cùng lúc.

C++
class A : public B, private C {
    // ...
};

Tất cả đặc điểm của kế thừa đơn vẫn áp dụng.


6.2 Vấn đề xung đột tên¤

Khi hai lớp cơ sở có thành phần trùng tên, lớp con sẽ gặp lỗi mơ hồ (ambiguity):

C++
class BASE_A {
public:
    int a;
    int f() { return 0; }
    int g() { return 0; }
    int h() { return 0; }
};

class BASE_B {
public:
    int a;       // Trùng tên với BASE_A::a
    int f() { return 0; } // Trùng tên với BASE_A::f
    int g() { return 0; } // Trùng tên với BASE_A::g
};

class ClassC : public BASE_A, public BASE_B {
    // ...
};

int main() {
    ClassC C;
    C.f();   // LỖI: Mơ hồ - BASE_A::f() hay BASE_B::f()?
    C.a = 1; // LỖI: Mơ hồ - BASE_A::a hay BASE_B::a?
    C.g();   // LỖI: Mơ hồ
    C.h();   // OK: Chỉ có BASE_A có h()
}

Cách khắc phục — dùng toán tử phạm vi :::

C++
C.BASE_A::f();  // Gọi rõ ràng f() của BASE_A
C.BASE_B::f();  // Gọi rõ ràng f() của BASE_B
C.BASE_A::a = 1;

6.3 Vấn đề kế thừa hình thoi (Diamond Problem)¤

Khi lớp D kế thừa từ cả BC, mà cả BC cùng kế thừa từ A:

graph TD
    A --> B
    A --> C
    B --> D
    C --> D
Hold "Alt" / "Option" to enable pan & zoom

Lúc này D sẽ có hai bản sao của các thành phần từ A. Giải pháp trong C++ là dùng virtual inheritance (kế thừa ảo) — nội dung nâng cao.


7. Bài tập và lời giải¤

Bài tập 1: Cây kế thừa các loại xe¤

Đề: Vẽ cây kế thừa cho các lớp: XE, XEHAIBÁNH, XEBABÁNH, XEBONBÁNH, XEĐẠP, XEGẮNMÁY, XEHƠI, XETẢI NHẸ, XELAM, XEXICHLO.

Lời giải:

graph TD
    XE --> XEHAIBÁNH
    XE --> XEBABÁNH
    XE --> XEBONBÁNH
    XEHAIBÁNH --> XEĐẠP
    XEHAIBÁNH --> XEGẮNMÁY
    XEBABÁNH --> XELAM
    XEBABÁNH --> XEXICHLO
    XEBONBÁNH --> XEHƠI
    XEBONBÁNH --> XETẢINHẸ
Hold "Alt" / "Option" to enable pan & zoom

Bài tập 2: Quản lý sinh viên cao đẳng và đại học¤

Đề: Xây dựng chương trình C++ quản lý sinh viên 2 hệ với điều kiện tốt nghiệp khác nhau.

Phân tích thiết kế:

graph TD
    SinhVien --> SVCaoDang
    SinhVien --> SVDaiHoc
Hold "Alt" / "Option" to enable pan & zoom

SinhVien (lớp cha) chứa thông tin chung: mã số, họ tên, địa chỉ, tổng tín chỉ, điểm TB.

SVCaoDang thêm: điểm thi tốt nghiệp. Tốt nghiệp khi: tín chỉ ≥ 120, điểm TB ≥ 5, điểm thi TN ≥ 5.

SVDaiHoc thêm: tên luận văn, điểm luận văn. Tốt nghiệp khi: tín chỉ ≥ 170, điểm TB ≥ 5, điểm LV ≥ 5.

C++
#include <iostream>
#include <cstring>
using namespace std;

class SinhVien {
protected:
    char *MaSo;
    char *HoTen;
    char *DiaChi;
    int TongTinChi;
    double DiemTB;

public:
    SinhVien(const char *ms, const char *ht, const char *dc,
             int tc, double dtb) {
        MaSo    = strdup(ms);
        HoTen   = strdup(ht);
        DiaChi  = strdup(dc);
        TongTinChi = tc;
        DiemTB  = dtb;
    }

    virtual ~SinhVien() {
        delete[] MaSo;
        delete[] HoTen;
        delete[] DiaChi;
    }

    virtual bool DuDieuKienTotNghiep() const = 0; // Hàm thuần ảo
    virtual void Xuat() const {
        cout << "Ma so: " << MaSo << ", Ho ten: " << HoTen << endl;
        cout << "Dia chi: " << DiaChi << endl;
        cout << "Tong tin chi: " << TongTinChi
             << ", Diem TB: " << DiemTB << endl;
    }
};

class SVCaoDang : public SinhVien {
    double DiemThiTN;
public:
    SVCaoDang(const char *ms, const char *ht, const char *dc,
              int tc, double dtb, double dttn)
        : SinhVien(ms, ht, dc, tc, dtb), DiemThiTN(dttn) {}

    bool DuDieuKienTotNghiep() const override {
        return (TongTinChi >= 120 && DiemTB >= 5.0 && DiemThiTN >= 5.0);
    }

    void Xuat() const override {
        cout << "=== SINH VIEN CAO DANG ===" << endl;
        SinhVien::Xuat();
        cout << "Diem thi tot nghiep: " << DiemThiTN << endl;
        cout << "Ket qua: "
             << (DuDieuKienTotNghiep() ? "Du dieu kien tot nghiep"
                                       : "Chua du dieu kien") << endl;
    }
};

class SVDaiHoc : public SinhVien {
    char *TenLuanVan;
    double DiemLuanVan;
public:
    SVDaiHoc(const char *ms, const char *ht, const char *dc,
             int tc, double dtb, const char *tlv, double dlv)
        : SinhVien(ms, ht, dc, tc, dtb), DiemLuanVan(dlv) {
        TenLuanVan = strdup(tlv);
    }

    ~SVDaiHoc() { delete[] TenLuanVan; }

    bool DuDieuKienTotNghiep() const override {
        return (TongTinChi >= 170 && DiemTB >= 5.0 && DiemLuanVan >= 5.0);
    }

    void Xuat() const override {
        cout << "=== SINH VIEN DAI HOC ===" << endl;
        SinhVien::Xuat();
        cout << "Ten luan van: " << TenLuanVan << endl;
        cout << "Diem luan van: " << DiemLuanVan << endl;
        cout << "Ket qua: "
             << (DuDieuKienTotNghiep() ? "Du dieu kien tot nghiep"
                                       : "Chua du dieu kien") << endl;
    }
};

int main() {
    SVCaoDang svcd("CD001", "Nguyen Van A", "TP.HCM", 125, 6.5, 7.0);
    SVDaiHoc  svdh("DH001", "Tran Thi B",  "Ha Noi", 175, 7.2,
                   "He thong quan ly sinh vien", 8.0);

    svcd.Xuat();
    cout << endl;
    svdh.Xuat();

    return 0;
}

Bài tập 3: Quản lý nhân viên công ty phần mềm¤

Đề: Lập trình viên và Kiểm chứng viên có lương tính khác nhau.

Thiết kế lớp:

graph TD
    NhanVien --> LapTrinhVien
    NhanVien --> KiemChungVien
Hold "Alt" / "Option" to enable pan & zoom
C++
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

class NhanVien {
protected:
    char *MaNV, *HoTen, *Email, *SoDienThoai;
    int Tuoi;
    double LuongCoBan;

public:
    NhanVien(const char *ma, const char *ht, int tuoi,
             const char *sdt, const char *email, double lcb) {
        MaNV       = strdup(ma);
        HoTen      = strdup(ht);
        SoDienThoai= strdup(sdt);
        Email      = strdup(email);
        Tuoi       = tuoi;
        LuongCoBan = lcb;
    }

    virtual ~NhanVien() {
        delete[] MaNV; delete[] HoTen;
        delete[] Email; delete[] SoDienThoai;
    }

    virtual double TinhLuong() const = 0;

    virtual void Xuat() const {
        cout << "Ma NV: " << MaNV << " | Ho ten: " << HoTen
             << " | Tuoi: " << Tuoi << endl;
        cout << "SDT: " << SoDienThoai << " | Email: " << Email << endl;
        cout << "Luong co ban: " << LuongCoBan
             << " | Luong thang nay: " << TinhLuong() << endl;
    }
};

class LapTrinhVien : public NhanVien {
    int SoGioOvertime;
public:
    LapTrinhVien(const char *ma, const char *ht, int tuoi,
                 const char *sdt, const char *email,
                 double lcb, int overtime)
        : NhanVien(ma, ht, tuoi, sdt, email, lcb),
          SoGioOvertime(overtime) {}

    double TinhLuong() const override {
        return LuongCoBan + SoGioOvertime * 200000.0;
    }

    void Xuat() const override {
        cout << "--- LAP TRINH VIEN ---" << endl;
        NhanVien::Xuat();
        cout << "So gio overtime: " << SoGioOvertime << endl;
    }
};

class KiemChungVien : public NhanVien {
    int SoLoiPhatHien;
public:
    KiemChungVien(const char *ma, const char *ht, int tuoi,
                  const char *sdt, const char *email,
                  double lcb, int soLoi)
        : NhanVien(ma, ht, tuoi, sdt, email, lcb),
          SoLoiPhatHien(soLoi) {}

    double TinhLuong() const override {
        return LuongCoBan + SoLoiPhatHien * 50000.0;
    }

    void Xuat() const override {
        cout << "--- KIEM CHUNG VIEN ---" << endl;
        NhanVien::Xuat();
        cout << "So loi phat hien: " << SoLoiPhatHien << endl;
    }
};

int main() {
    vector<NhanVien*> danhSach;
    danhSach.push_back(
        new LapTrinhVien("NV001","Nguyen Van A",25,"0901","a@co.vn",10000000,20));
    danhSach.push_back(
        new KiemChungVien("NV002","Tran Thi B",30,"0902","b@co.vn",8000000,50));
    danhSach.push_back(
        new LapTrinhVien("NV003","Le Van C",28,"0903","c@co.vn",12000000,5));

    // Tính lương trung bình
    double tongLuong = 0;
    for (auto nv : danhSach) tongLuong += nv->TinhLuong();
    double luongTB = tongLuong / danhSach.size();

    cout << "=== DANH SACH NHAN VIEN ===" << endl;
    for (auto nv : danhSach) { nv->Xuat(); cout << endl; }

    cout << "=== NHAN VIEN CO LUONG THAP HON TRUNG BINH ("
         << luongTB << ") ===" << endl;
    for (auto nv : danhSach)
        if (nv->TinhLuong() < luongTB) nv->Xuat();

    for (auto nv : danhSach) delete nv;
    return 0;
}

Bài tập 4 (Bài tập đầu chương): Quản lý sinh viên đại học CNTT¤

Điều kiện tốt nghiệp: Tín chỉ ≥ 140, Điểm TB ≥ 5, Điểm luận văn ≥ 5.

C++
class SinhVienDHCNTT {
protected:
    char *MaSo, *HoTen, *DiaChi;
    int TongTinChi;
    double DiemTB;
    char *TenLuanVan;
    double DiemLuanVan;

public:
    SinhVienDHCNTT(const char *ms, const char *ht, const char *dc,
                   int tc, double dtb, const char *tlv, double dlv) {
        MaSo       = strdup(ms);
        HoTen      = strdup(ht);
        DiaChi     = strdup(dc);
        TongTinChi = tc;
        DiemTB     = dtb;
        TenLuanVan = strdup(tlv);
        DiemLuanVan= dlv;
    }

    ~SinhVienDHCNTT() {
        delete[] MaSo; delete[] HoTen;
        delete[] DiaChi; delete[] TenLuanVan;
    }

    bool DuDieuKienTotNghiep() const {
        return (TongTinChi >= 140 && DiemTB >= 5.0 && DiemLuanVan >= 5.0);
    }

    void Xuat() const {
        cout << "Ma so: " << MaSo << " | Ho ten: " << HoTen << endl;
        cout << "Dia chi: " << DiaChi << endl;
        cout << "Tong TC: " << TongTinChi << " | Diem TB: " << DiemTB << endl;
        cout << "Luan van: " << TenLuanVan
             << " | Diem LV: " << DiemLuanVan << endl;
        cout << "Tot nghiep: "
             << (DuDieuKienTotNghiep() ? "Co" : "Chua") << endl;
    }
};

Tổng kết¤

graph TD
    A[Quan hệ giữa các lớp] --> B[1-1]
    A --> C[1-n]
    A --> D[n-n]
    A --> E[Đặc biệt hóa / Tổng quát hóa]
    E --> F[Kế thừa]
    F --> G[Kế thừa đơn]
    F --> H[Đa kế thừa]
    G --> I[Phạm vi truy xuất]
    I --> J[private / protected / public]
    I --> K[Kiểu kế thừa: public / protected / private]
Hold "Alt" / "Option" to enable pan & zoom
Chủ đề Điểm cốt lõi
Quan hệ "là một" Nền tảng để áp dụng kế thừa đúng cách
protected Cho phép lớp con truy xuất, che giấu với bên ngoài
Constructor Không kế thừa; lớp con phải tự định nghĩa và gọi constructor cha
Destructor Tự động gọi destructor cha sau destructor con
Override Lớp con định nghĩa lại phương thức của lớp cha
Đa kế thừa Cẩn thận với xung đột tên và diamond problem