Tính kế thừa trong C++ (Inheritance): lớp cha, lớp con và ghi đè hàm

Tính kế thừa trong C++ (Inheritance): lớp cha, lớp con và ghi đè hàm - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ học tính kế thừa trong C++ (inheritance): cách tạo lớp con từ lớp cha, gọi hàm khởi tạo của lớp cha, thứ tự khởi tạo và hủy đối tượng, phạm vi protected, các kiểu kế thừa và cách ghi đè hàm của lớp cha. Kế thừa giúp các class giống nhau dùng chung code, thay vì sao chép rồi sửa từng nơi.

Bài 18 trong Serie Lập trình C++ từ A tới Z

Tính kế thừa trong C++ là gì?

Kế thừa (inheritance) là cách tạo một class mới dựa trên class có sẵn. Class mới, gọi là lớp con (derived class), tự động có các thuộc tính và phương thức của class cũ, gọi là lớp cha (base class), và chỉ cần viết thêm phần riêng của nó.

Thư viện của chúng ta không chỉ có sách mà còn có tạp chí. Cả hai đều có mã và tên, đều cần in thông tin. Nhưng sách có tác giả, còn tạp chí có số kỳ. Nếu viết 2 class riêng thì phần mã, tên, in thông tin phải viết 2 lần, sửa một chỗ phải nhớ sửa chỗ kia.

Với kế thừa, phần chung đưa vào lớp cha TaiLieu, còn Sach và TapChi kế thừa từ nó:

Sơ đồ tính kế thừa trong C++: Sach và TapChi là lớp con kế thừa từ lớp cha TaiLieu

Trong sơ đồ, dấu - là thành viên private, dấu + là public. Kế thừa thể hiện quan hệ “là một” (is-a): sách là một tài liệu, tạp chí là một tài liệu. Đây là cách kiểm tra nhanh xem có nên dùng kế thừa hay không.

Cú pháp kế thừa trong C++

#include <iostream>
#include <string>

class TaiLieu
{
public:
    TaiLieu(int ma, const std::string& ten) : ma_(ma), ten_(ten) {}

    int ma() const { return ma_; }
    const std::string& ten() const { return ten_; }

    void inThongTin() const
    {
        std::cout << "[" << ma_ << "] " << ten_ << '\n';
    }

private:
    int ma_;
    std::string ten_;
};

class Sach : public TaiLieu
{
public:
    Sach(int ma, const std::string& ten, const std::string& tacGia)
        : TaiLieu(ma, ten), tacGia_(tacGia) {}

    const std::string& tacGia() const { return tacGia_; }

private:
    std::string tacGia_;
};

class TapChi : public TaiLieu
{
public:
    TapChi(int ma, const std::string& ten, int soKy)
        : TaiLieu(ma, ten), soKy_(soKy) {}

    int soKy() const { return soKy_; }

private:
    int soKy_;
};

int main()
{
    Sach s(1001, "Lap trinh C++", "Bjarne Stroustrup");
    TapChi t(2001, "Tap chi Dien tu", 125);

    s.inThongTin();                                   // ham ke thua tu TaiLieu
    std::cout << "  Tac gia: " << s.tacGia() << '\n';
    t.inThongTin();
    std::cout << "  So ky: " << t.soKy() << '\n';
    return 0;
}

Sach và TapChi gọi hàm inThongTin kế thừa từ TaiLieu và in thêm tác giả, số kỳ

  • class Sach : public TaiLieu: dấu : và tên lớp cha cho biết Sach kế thừa TaiLieu. Chữ public là kiểu kế thừa, giải thích ở dưới.
  • s.inThongTin(): Sach không viết hàm này, nhưng gọi được vì đã kế thừa từ TaiLieu.
  • Sach không truy cập trực tiếp được ma_ và ten_ vì chúng là private của lớp cha. Muốn đọc thì dùng getter ma(), ten() kế thừa được.

Gọi hàm khởi tạo của lớp cha

Trong hàm khởi tạo của Sach, phần : TaiLieu(ma, ten) gọi hàm khởi tạo của lớp cha để khởi tạo mã và tên. Lớp con không tự gán ma_, ten_ được, nên đây là cách duy nhất truyền dữ liệu cho phần lớp cha.

Quên gọi thì sao?

#include <string>

class TaiLieu
{
public:
    TaiLieu(int ma, const std::string& ten) : ma_(ma), ten_(ten) {}

private:
    int ma_;
    std::string ten_;
};

class Sach : public TaiLieu
{
public:
    Sach(const std::string& tacGia) : tacGia_(tacGia) {}   // quen goi TaiLieu(...)

private:
    std::string tacGia_;
};

int main()
{
    Sach s("Bjarne Stroustrup");
    return 0;
}

g++ báo lỗi no matching function for call to TaiLieu::TaiLieu() khi lớp con quên gọi constructor lớp cha

Không gọi hàm khởi tạo lớp cha thì C++ tự gọi hàm khởi tạo không tham số TaiLieu(). Nhưng TaiLieu chỉ có hàm khởi tạo 2 tham số, nên g++ báo no matching function for call to 'TaiLieu::TaiLieu()'. Thấy lỗi này trong lớp con thì gần như chắc chắn là quên gọi hàm khởi tạo của lớp cha.

Thứ tự khởi tạo và hủy

Một đối tượng lớp con chứa bên trong nó phần lớp cha. Thứ tự tạo và hủy như sau:

#include <iostream>

class TaiLieu
{
public:
    TaiLieu()  { std::cout << "  Khoi tao TaiLieu\n"; }
    ~TaiLieu() { std::cout << "  Huy TaiLieu\n"; }
};

class Sach : public TaiLieu
{
public:
    Sach()  { std::cout << "  Khoi tao Sach\n"; }
    ~Sach() { std::cout << "  Huy Sach\n"; }
};

int main()
{
    std::cout << "Tao doi tuong:\n";
    {
        Sach s;
        std::cout << "Het khoi lenh:\n";
    }
    return 0;
}

Thứ tự khởi tạo từ lớp cha xuống lớp con, hủy theo thứ tự ngược lại

Khởi tạo đi từ lớp cha xuống lớp con, còn hủy thì ngược lại. Giống như xây nhà: đổ móng (lớp cha) trước rồi mới xây tầng trên (lớp con), còn phá nhà thì dỡ tầng trên trước. Hàm ~Sach() có dấu ngã là hàm hủy (destructor), chạy khi đối tượng bị hủy, chi tiết có trong bài constructor và destructor.

Phạm vi protected

private thì lớp con cũng không đụng được. Khi muốn lớp con dùng được mà bên ngoài vẫn không truy cập được, dùng protected:

#include <iostream>
#include <string>

class TaiLieu
{
public:
    TaiLieu(const std::string& ten, int soLuong) : soLuong_(soLuong), ten_(ten) {}
    int soLuong() const { return soLuong_; }

protected:
    int soLuong_;          // lop con duoc dung, ben ngoai thi khong

private:
    std::string ten_;
};

class SachThamKhao : public TaiLieu
{
public:
    SachThamKhao(const std::string& ten, int soLuong) : TaiLieu(ten, soLuong) {}

    // sach tham khao chi doc tai cho: moi lan doc giam tam 1 cuon tren ke
    void layRaDoc()
    {
        if (soLuong_ > 0)
            soLuong_--;
    }
};

int main()
{
    SachThamKhao tuDien("Tu dien Anh Viet", 2);
    tuDien.layRaDoc();
    std::cout << "Con tren ke: " << tuDien.soLuong() << '\n';
    // tuDien.soLuong_ = 10;   // loi: soLuong_ la protected
    return 0;
}

Lớp con SachThamKhao sửa thuộc tính protected soLuong_ của lớp cha

SachThamKhao::layRaDoc() sửa trực tiếp soLuong_ được vì nó là protected. Còn từ main thì vẫn bị chặn như private.

Lưu ý: protected cũng là một lỗ hổng đóng gói nhỏ: mọi lớp con, kể cả do người khác viết sau này, đều sửa được thuộc tính đó mà không qua kiểm tra. Ưu tiên để thuộc tính private và cho lớp con dùng phương thức protected có kiểm tra, chỉ để thuộc tính protected khi thật sự cần.

Các kiểu kế thừa: public, protected, private

Chữ đứng trước tên lớp cha quyết định các thành viên của lớp cha có phạm vi gì trong lớp con:

Thành viên lớp cha Kế thừa public Kế thừa protected Kế thừa private
public public protected private
protected protected protected private
private Không truy cập được Không truy cập được Không truy cập được

Trên thực tế, gần như luôn dùng kế thừa public, vì nó giữ đúng quan hệ “là một”: sách là một tài liệu, nên ai dùng được tài liệu thì cũng dùng được sách. Lưu ý với class, nếu quên ghi chữ public thì mặc định là kế thừa private, các hàm public của lớp cha bị giấu hết khỏi bên ngoài.

Ghi đè hàm của lớp cha

Lớp con có thể viết lại một hàm cùng tên với lớp cha để làm việc theo cách riêng. Ví dụ sách in thông tin thì thêm tên tác giả:

#include <iostream>
#include <string>

class TaiLieu
{
public:
    TaiLieu(int ma, const std::string& ten) : ma_(ma), ten_(ten) {}

    void inThongTin() const
    {
        std::cout << "[" << ma_ << "] " << ten_ << '\n';
    }

private:
    int ma_;
    std::string ten_;
};

class Sach : public TaiLieu
{
public:
    Sach(int ma, const std::string& ten, const std::string& tacGia)
        : TaiLieu(ma, ten), tacGia_(tacGia) {}

    void inThongTin() const                  // ghi de ham cua lop cha
    {
        TaiLieu::inThongTin();                // goi lai phien ban cua lop cha
        std::cout << "  Tac gia: " << tacGia_ << '\n';
    }

private:
    std::string tacGia_;
};

int main()
{
    Sach s(1001, "Lap trinh C++", "Bjarne Stroustrup");
    s.inThongTin();
    return 0;
}

Lớp Sach ghi đè inThongTin, gọi lại phiên bản lớp cha rồi in thêm tác giả

TaiLieu::inThongTin() gọi lại phiên bản của lớp cha để in mã và tên, sau đó lớp con in thêm phần của mình. Nhờ vậy không phải viết lại phần in mã, tên.

Cách ghi đè ở đây chỉ chạy đúng khi gọi trực tiếp trên đối tượng Sach. Khi để sách và tạp chí chung một danh sách kiểu TaiLieu rồi gọi inThongTin(), các bạn sẽ thấy nó luôn gọi bản của lớp cha. Muốn mỗi loại tự gọi đúng bản của mình thì cần hàm ảo virtual, nội dung chính của bài tính đa hình trong C++.

Khi nào không nên dùng kế thừa?

Kế thừa chỉ hợp với quan hệ “là một”. Nếu quan hệ là “có một” (has-a) thì dùng thành phần (composition): đặt đối tượng này làm thuộc tính của class kia.

  • Thư viện có nhiều sách: class ThuVien có thuộc tính là danh sách Sach, chứ không kế thừa Sach.
  • Phiếu mượn có một độc giả và một cuốn sách: PhieuMuon chứa DocGia và Sach.

Kế thừa sai quan hệ sẽ khiến lớp con có những phương thức vô nghĩa, ví dụ ThuVien kế thừa Sach thì thư viện cũng có hàm tacGia().

Bài tập

  1. Viết lớp cha NguoiDung (họ tên, số điện thoại) và 2 lớp con DocGia (số sách đang mượn) và ThuThu (ca làm việc). Tạo đối tượng và in thông tin.
  2. Thêm hàm khởi tạo và hàm hủy có in ra màn hình cho 3 class ở bài 1, quan sát thứ tự gọi.
  3. Bỏ chữ public trong class Sach : public TaiLieu rồi gọi s.inThongTin() từ main. g++ báo lỗi gì?
  4. Giữa các cặp sau, cặp nào nên dùng kế thừa, cặp nào nên dùng thành phần: Xe và Động cơ, Ô tô và Xe, Thư viện và Kệ sách, Sách điện tử và Sách.

Lời giải sẽ có trong bài tổng hợp bài tập OOP C++ nhé.

Kết

Tính kế thừa trong C++ giúp đưa phần chung của nhiều class lên lớp cha, lớp con chỉ viết phần riêng. Ba điểm cần nhớ: luôn gọi hàm khởi tạo lớp cha trong danh sách khởi tạo, khởi tạo đi từ cha xuống con và hủy theo chiều ngược lại, và chỉ dùng kế thừa cho quan hệ “là một”. Các bạn đọc thêm về khái niệm này ở trang Inheritance trên Wikipedia.

Bài trước là tính đóng gói trong C++. Bài tiếp theo chúng ta sẽ tìm hiểu trường hợp đặc biệt khi một class kế thừa từ nhiều lớp cha: đa kế thừa trong C++. Nếu thấy bài viết hữu ích, hãy chia sẻ cho bạn bè nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *