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

Chương 4: Kế thừa và Đa hình

4.1. Mối quan hệ đặc biệt hóa, tổng quát hóa

Hai lớp đối tượng được gọi là quan hệ đặc biệt hóa – tổng quát hóa với nhau khi, lớp đối tượng này là trường hợp đặc biệt của lớp đối tượng kia và lớp đối tượng kia là trường hợp tổng quát của lớp đối tượng này.

Trong hình vẽ trên: lớp đối tượng TamGiacCan là trường hợp đặc biệt của lớp đối tượng TamGiac và lớp đối tượng TamGiac là trường hợp tổng quát của lớp đối tượng TamGiacCan.

Cây kế thừa

Cây kế thừa là một cây đa nhánh thể hiện mối quan hệ đặc biệt hóa - tổng quát hóa giữa các lớp trong hệ thống, trong chương trình.

4.2. Kế thừa

Kế thừa là một đặc điểm 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. Nó cho phép một lớp có thể được thừa hưởng các thuộc tính, phương thức từ một lớp khác.

Lớp kế thừa từ một lớp khác thì được gọi là lớp con (child class, subclass) hay lớp dẫn xuất (derived class). Lớp được các lớp khác kế thừa được gọi là lớp cha (parent class, superclass) hay lớp cơ sở (base class).

Ví dụ thực tế

  • Bạn có một lớp đối tượng con người, có các thuộc tính như họ tên, ngày sinh, quê quán, ta khai báo thêm một lớp sinh viên kế thừa từ lớp con người.
  • Khi đó lớp sinh viên sẽ có các thuộc tính họ tên, ngày sinh, quê quán từ lớp con người mà không cần phải khai báo lại. Lớp con người được gọi là lớp cha và lớp sinh viên là lớp con
  • Ngoài các thuộc tính của lớp cha, lớp con còn có thể có thêm các thuộc tính, phương thức của riêng nó. Ở ví dụ trên thì lớp sinh viên có thể có thêm các thuộc tính như MSSV, tên trường, chuyên ngành …

Lợi ích của kế thừa

  • Kế thừa cho phép xây dựng lớp mới từ lớp đã có.
  • Kế thừa cho phép tổ chức các lớp chia sẻ mã chương trình chung, nhờ vậy có thể dễ dàng sửa chữa, nâng cấp hệ thống.
  • Định nghĩa sự tương thích giữa các lớp, nhờ đó ta có thể chuyển kiểu tự động.

4.3. Định nghĩa lớp cơ sở và lớp dẫn xuất trong C++

Bài toán quản lí cửa hàng sách

Lấy ví dụ về bài toán quản lí hóa đơn trong một hiệu sách, chủ cửa hàng muốn áp dụng các phương thức tính tiền khác nhau cho các loại sách khác nhau. Một vài quyển sách chỉ được bán ở một mức giá cố định, trong khi một vài quyển khác sẽ được giảm giá khi mua với số lượng lớn.

Để giải quyết vấn đề, ta sẽ tạo một lớp cơ sở có tên là Quote đại diện cho những quyển sách không được giảm giá và lớp BulkQuote được kế thừa từ lớp Quote, tượng trưng cho những loại sách sẽ được áp mã giảm giá khi mua trên một số lượng nhất định.

Hai lớp sẽ có chung các thuộc tính là mã số sách và giá tiền, cùng với một phương thức đặc biệt dùng để tính tiền. Trước tiên hãy cùng xem sơ qua định nghĩa của hai lớp này.

Định nghĩa lớp cơ sở

class Quote {  
private:
string bookNo; // mã số sách
protected:
double price; // giá của một quyển sách
public:
// phương thức thiết lập:
Quote();
Quote(const string& book, double salesPrice);
// phương thức truy vấn:
string getBookNo() { return bookNo; }
// phương thức tính tiền khi biết số lượng sách được mua:
virtual double NetPrice(int n) { return price * n; }
};

Điểm mới trong phần định nghĩa của lớp cơ sở Quote là sự xuất hiện của từ khóa virtual trước dòng khai báo của phương thức NetPrice và phạm vi truy xuất protected.

Chúng ta sẽ tìm hiểu kĩ hơn về virtual ở phần Đa hình, bây giờ bạn đọc chỉ cần biết rằng từ khóa virtual dùng để chỉ những phương thức mà lớp cha muốn lớp con có thể định nghĩa lại một phiên bản của riêng mình.

Phạm vi truy xuất protected trong kế thừa

Tương tự với cách mà người dùng sử dụng một lớp đối tượng, các lớp dẫn xuất có thể truy cập đến các thành viên public và không thể truy cập đến các thành viên private trong lớp cơ sở của nó. Tuy nhiên có những trường hợp mà ta muốn một vài thành viên của lớp cơ sở có thể được truy cập ở lớp dẫn xuất trong khi vẫn giới hạn quyền truy cập từ bên ngoài. Những thành viên như vậy sẽ được chỉ định là các thành viên protected.

Phạm vi truy xuất protected có thể được xem như là một sự kết hợp giữa publicprivate:

  • Giống với private, các thành viên protected không thể được truy cập từ bên ngoài.
  • Giống với public, các thành viên protected của lớp cơ sở có thể được truy cập bởi các lớp bạn và các lớp dẫn xuất của nó.

Trong ví dụ về lớp cơ sở Quote, thuộc tính price được chỉ định là protected nên các hàm thành viên của các lớp kế thừa từ Quote có thể truy cập trực tiếp vào nó trong khi người dùng thì không. Thuộc tính bookNo được chỉ định là private nên không thể được truy cập trực tiếp từ bên ngoài lớp, kể cả là các lớp con hay người dùng. Còn lại các thành viên public thì có thể được truy cập ở bất cứ đâu.

Định nghĩa lớp dẫn xuất

Cú pháp kế thừa

Trong C++, lớp dẫn xuất được khai báo theo cú pháp:

class B : <tkhóa dn xut> A { 
...
};

Ở đây ta hiểu rằng lớp B kế thừa từ lớp A, lớp A là lớp cơ sở và lớp B là lớp dẫn xuất từ A.

Từ khóa dẫn xuất có ba loại đó là private, publicprotected sẽ được giải thích ở phần Các kiểu kế thừa.

Định nghĩa lớp dẫn xuất

Trước hết ta hãy cùng xem qua định nghĩa của lớp BulkQuote:

// lớp BulkQuote kế thừa từ lớp Quote 
class BulkQuote : public Quote {
private:
double discount; // tỉ lệ phần trăm được giảm
int minQty; // số lượng mua tối thiểu để được áp mã
public:
   BulkQuote();
   BulkQuote(const string& book, double salesPrice, double  disc, int cnt);
   // phương thức NetPrice sẽ có một phiên bản khác
   double NetPrice(int n);
};

Lớp dẫn xuất sẽ được kế thừa các thành viên nằm trong lớp cơ sở (trừ phương thức thiết lập), thêm vào đó nó có thể định nghĩa thêm các thành viên mới của riêng mình hoặc định nghĩa một phiên bản khác cho vài phương thức ở lớp cơ sở.

  • Lớp BulkQuote trong ví dụ này đã được thừa hưởng từ lớp Quote 2 thuộc tính là bookNo, price cùng với phương thức getBookNo.
  • Lớp BulkQuote còn có thêm 2 thuộc tính mới là discount, minQty và 2 phương thức thiết lập của riêng mình, ngoài ra, nó sẽ tự định nghĩa lại hàm NetPrice ở lớp Quote.
  • Những thành viên đã được thừa hưởng thì không cần phải ghi lại trong phần thân của lớp dẫn xuất, ta chỉ cần viết khai báo và định nghĩa cho các thành viên mới cùng với các phương thức cần định nghĩa lại.

Lưu ý: Mặc dù lớp dẫn xuất không có quyền truy cập trực tiếp vào các thuộc tính private ở lớp cơ sở nhưng nó vẫn được thừa hưởng các thuộc tính đó. Trong ví dụ trên, lớp BulkQuote được kế thừa thuộc tính bookNo từ Quote, tuy nhiên thuộc tính này chỉ có thể được truy cập thông qua phương thức getBookNo.

4.4. Các kiểu kế thừa

Trong các phần trước, ta đã tìm hiểu cách một lớp đối tượng quản lí quyền truy cập vào các thành viên của mình thông qua phạm vi truy xuất được chỉ định bên trong lớp.

Đến với kế thừa trong C++, các từ khóa dẫn xuất còn được sử dụng trong khai báo lớp con để có thể thay đổi mức độ truy cập của các thành viên ở lớp cha khi chúng được kế thừa xuống lớp con. Tức là kiểm soát quyền truy cập mà người dùng của lớp dẫn xuất có đối với các thành viên được kế thừa từ lớp cơ sở.

Thành phần private ở lớp cha thì không truy xuất được ở lớp con.

Kế thừa public

Lớp con kế thừa public từ lớp cha thì các thành phần protected của lớp cha trở thành protected của lớp con, các thành phần public của lớp cha trở thành public của lớp con.

Kế thừa protected

Lớp con kế thừa protected từ lớp cha thì các thành phần protectedpublic của lớp cha trở thành protected của lớp con.

Kế thừa private

Lớp con kế thừa private từ lớp cha thì các thành phần protectedpublic của lớp cha trở thành private của lớp con.

Ta có bảng quy tắc kế thừa như sau:

Phạm vi truy cập lớp cha \ Từ khóa dẫn xuấtprivateprotectedpublic
privateXXX
protectedprivateprotectedprotected
publicprivateprotectedpublic

Lưu ý: Kiểu kế thừa không ảnh hưởng đến cách mà lớp con truy cập đến các thành phần ở lớp cha (phần này được đảm nhiệm bởi các phạm vi truy xuất do lớp cha chỉ định) mà chỉ ảnh hưởng đến việc các đối tượng bên ngoài có thể truy cập những gì từ lớp con đó.

4.5. Phương thức thiết lập trong kế thừa

Mặc dù một đối tượng của lớp con được thừa hưởng các thuộc tính từ lớp cha, nó không nên trực tiếp khởi tạo dữ liệu cho các thuộc tính đó (trong vài trường hợp thì là không thể luôn).

  • Ở các ví dụ trên, lớp BulkQuote kế thừa 2 thuộc tính là bookNoprice từ Quote, tuy nhiên bookNo là thành phần private của Quote, kể cả lớp con cũng không truy cập được thuộc tính này, dẫn đến việc BulkQuote không thể trực tiếp gán dữ liệu cho bookNo.
  • Cho dù các thuộc tính này đều là protected và lớp con có thể truy cập được đi chăng nữa, lớp con cũng không nên khởi tạo dữ liệu trực tiếp cho chúng (bởi vì lớp cha có cung cấp các phương thức để tương tác với các thuộc tính của nó, lớp con nên tôn trọng và sử dụng lại các phương thức này).

Tóm lại, lớp con nên sử dụng phương thức thiết lập của lớp cha để khởi tạo dữ liệu cho các thuộc tính chung mà nó được thừa kế. Cách sử dụng như sau (lấy ví dụ là phương thức thiết lập cho lớp BulkQuote):

//Phương thức thiết lập của Quote 
Quote::Quote(string id, double price) : bookNo(id), price(price) {}

//Phương thức thiết lập của Bulk_quote
BulkQuote::BulkQuote(string id, double price, double disc, int n) : Quote(id,price), discount(disc), minQty(n) {}

Một điểm mới trong ví dụ trên chính là việc sử dụng danh sách khởi tạo trong phương thức thiết lập (constructor initializer list). Có một vài điểm khác biệt giữa việc sử dụng danh sách khởi tạo và việc thực hiện phép gán (operator=) trong thân phương thức thiết lập ở ví dụ của các chương trước, tuy nhiên để tránh loãng nội dung thì kiến thức này sẽ không nói ở đây ☹.

Trở lại vấn đề chính, với cách định nghĩa như trên, tạo 1 đối tượng derived thuộc lớp BulkQuote, khi một phương thức thiết lập của BulkQuote được gọi với 4 đối số đầu vào:

BulkQuote derived("893-523-52-2211-3", 150000, 0.2, 3); 

2 đối số đầu tiên sẽ được dùng để gọi phương thức thiết lập của Quote, sau khi phương thức này thực hiện xong nhiệm vụ và các thuộc tính chung đã được khởi tạo, hai đối số còn lại sẽ lần lượt được dùng để khởi tạo cho giá trị cho 2 thuộc tính riêng của BulkQuote.

Constructor mặc định

Khi một đối tượng của lớp con được khởi tạo mặc định, thứ tự như trên cũng được thực hiện, tức là constructor mặc định của lớp cha được gọi trước để khởi tạo dữ liệu mặc định cho các thuộc tính chung, sau đó constructor của lớp con mới bắt đầu thực hiện việc gán dữ liệu cho các thuộc tính riêng.

Quote::Quote() { 
cout << “Ham khoi tao cua lop co so!<< endl;
}

BulkQuote::BulkQuote() {
cout << “Ham khoi tao cua lop dan xuat!<< endl;
}

int main() {
BulkQuote obj;
return 0;
}

Kết quả: Hàm khởi tạo ở lớp cơ sở được gọi đến trước rồi mới tới hàm khởi tạo ở lớp dẫn xuất.

Ham khoi tao cua lop co so!
Ham khoi tao cua lop dan xuat!

4.6. Phép gán và con trỏ trong kế thừa

Thông thường, một biến con trỏ chỉ có thể giữ địa chỉ của các đối tượng có cùng kiểu với nó, tuy nhiên trong kế thừa có một ngoại lệ: một con trỏ đối tượng thuộc lớp cơ sở có thể giữ địa chỉ của một đối tượng thuộc lớp dẫn xuất.

Dẫu vậy, một con trỏ đối tượng thuộc lớp dẫn xuất lại không thể giữ địa chỉ của một đối tượng thuộc lớp cơ sở, như ví dụ dưới đây:

Quote base; 
BulkQuote derived;
Quote* basePtr = &derived; // Đúng
BulkQuote* derivedPtr = &base; // Sai

Việc có thể gán địa chỉ của một đối tượng thuộc lớp dẫn xuất cho một biến con trỏ đối tượng thuộc lớp cơ sở dẫn đến một kết quả quan trọng: Khi sử dụng một con trỏ đối tượng thuộc lớp cơ sở, chúng ta không biết chắc được đối tượng mà con trỏ đó sẽ giữ có kiểu dữ liệu gì, nó có thể là một đối tượng thuộc lớp cơ sở hoặc lớp dẫn xuất. Điều này góp phần tạo nên tính đa hình trong OOP sẽ được tìm hiểu ở chương tới.

Phép gán trực tiếp giữa các đối tượng

Ta cũng có thể gán trực tiếp một đối tượng thuộc lớp con cho một đối tượng thuộc lớp cha. Ngược lại, không thể gán một đối tượng của lớp cha cho một đối tượng thuộc lớp con.

Tuy nhiên có một vài điểm cần lưu ý: Khi dùng một biến có kiểu lớp con khởi tạo cho một đối tượng thuộc lớp cha, chương trình sẽ chỉ sao chép những thuộc tính chung giữa 2 lớp mà không sao chép các thuộc tính riêng của lớp con. Đơn giản là vì lớp cha thì không thể biết được các lớp con của nó có các thuộc tính mới nào, nó chỉ biết được những thuộc tính đã được định nghĩa sẵn bên trong mình mà thôi.

Quote item1; 
BulkQuote item2("13hd", 50000, 0.2, 3);
item1 = item2;

Sau dòng lệnh trên, item1 lúc này chỉ chứa 2 thuộc tính là bookNo = 13hdprice = 50000, 2 thuộc tính discount = 0.2minQty = 3 trong item2 đã bị lược bỏ.

4.7. Phương thức ảo (Virtual function) và Đa hình (Polymorphism)

Đa hình

Lớp dẫn xuất được kế thừa các phương thức đã có ở lớp cơ sở, tuy nhiên hành vi của chúng có thể được tinh chỉnh để tương thích hơn với lớp dẫn xuất. Để làm vậy, lớp dẫn xuất cần phải định nghĩa lại một phiên bản khác cho các phương thức đó.

Trong lớp cơ sở, ta thêm từ khóa virtual vào trước phần khai báo của những phương thức mà các lớp dẫn xuất có thể ghi đè lại (override), những phương thức này sẽ được gọi là phương thức ảo.

  • Khi gọi các phương thức ảo thông qua một con trỏ đối tượng, lời gọi sẽ được thực hiện theo cơ chế đa hình, cho phép xác định đúng hành vi (phương thức) sẽ được thực thi.
  • Tùy thuộc vào kiểu dữ liệu của đối tượng mà con trỏ giữ địa chỉ, phiên bản của phương thức ảo nằm trong lớp cơ sở hoặc lớp dẫn xuất sẽ được thực hiện (nhớ lại rằng một con trỏ thuộc lớp cơ sở có thể giữ địa chỉ của một đối tượng thuộc lớp dẫn xuất).

Ngoại trừ các phương thức tĩnhphương thức thiết lập, các phương thức khác đều có thể được khai báo là phương thức ảo. Những phương thức đã được khai báo là virtual trong lớp cơ sở thì những phương thức cùng têncùng danh sách tham số đầu vào trong lớp dẫn xuất cũng sẽ là phương thức ảo.

Trong bài toán quản lí cửa hàng sách, ta đã khai báo phương thức NetPrice trong Quote là phương thức ảo, lớp BulkQuote muốn định nghĩa một phiên bản khác của NetPrice nên sẽ ghi lại dòng khai báo của của nó (lưu ý rằng tên hàm, kiểu dữ liệu trả về, danh sách tham số đầu vào phải giống hệt nhau):

class Quote {  
// ...
public:
virtual double NetPrice(int n) { return price * n; }
};

class BulkQuote : public Quote {
// ...
public:
double NetPrice(int n);
}

Phương thức ảo có thể được định nghĩa bên trong hoặc bên ngoài lớp, khi định nghĩa bên ngoài thì cũng không cần phải ghi lại từ khóa virtual. Ta sẽ định nghĩa một phiên bản của NetPrice cho lớp BulkQuote như sau:

double BulkQuote::NetPrice(int n) { 
// nếu số lượng mua lớn hơn minQty thì sẽ áp dụng giảm giá
if (n > minQty) {
return n * (1 - discount) * price;
}
return n * price;
}

Để hiểu rõ hơn cơ chế hoạt động của đa hình, hãy cùng xem qua ví dụ dưới đây:


Quote base("978-179-64-4473-5", 150000); // base là đối tượng thuộc lớp cơ sở Quote
BulkQuote derived("978-179-64-4473-5", 150000, 0.2, 3); // derived là đối tượng thuộc lớp dẫn xuất BulkQuote
Quote* basePtr; // basePtr là con trỏ thuộc lớp cơ sở Quote
basePtr = &base; // basePtr giữ địa chỉ của base
cout << "Gia cua don hang 1: ";
cout << basePtr->NetPrice(5) << endl; // phương thức NetPrice của Quote được thực hiện
basePtr = &derived; // basePtr giữ địa chỉ của derived
cout << "Gia cua don hang 2: ";
cout << basePtr->NetPrice(5) << endl; // phương thức NetPrice của BulkQuote được thực hiện
  • Bởi vì basePtr là con trỏ kiểu QuoteNetPrice là phương thức ảo, phiên bản của NetPrice được gọi sẽ phụ thuộc vào kiểu dữ liệu của đối tượng mà basePtr giữ địa chỉ.
  • Tại dòng số 10, basePtr đang giữ địa chỉ đối tượng base thuộc lớp Quote nên phiên bản NetPrice của lớp Quote được thực hiện.
  • Còn ở dòng số 15, basePtr lúc này giữ địa chỉ của đối tượng derived thuộc lớp BulkQuote, do đó hàm NetPrice được định nghĩa bởi lớp BulkQuote được gọi.
  • Khi chạy chương trình sẽ thấy hai kết quả in ra khác nhau, chứng tỏ hai phiên bản khác nhau của NetPrice đã được thực hiện:
Gia cua don hang 1: 750000
Gia cua don hang 2: 600000
  • Trong ví dụ trên, ta thấy thông qua cùng một giao diện là phương thức NetPrice (dòng số 10 và dòng số 15), ta vẫn có thể thực hiện các hành động khác nhau (thanh toán khi có giảm giá và không có giảm giá) tùy thuộc vào ngữ cảnh. Đây chính là sự thể hiện của tính đa hình trong OOP.

Danh sách con trỏ đa hình

Trong đề thi OOP, tính đa hình được yêu cầu bởi tạo một danh sách quản lý nhiều loại đối tượng có thuộc tính chung.

Ví dụ một ngôi nhà có 2 loại vật nuôi là MèoChó. 2 lớp Chó và Mèo kế thừa từ lớp Vật nuôi.

Ta cần một danh sách để quản lý VatNuoi. Để VatNuoi thể hiện cơ chế Đa hình là Meo hoặc Cho, ta phải sử dụng các con trỏ VatNuoi*, do phương thức ảo và đa hình trong C++ chỉ hoạt động thông qua con trỏ.


Xem thêm các thao tác xử lý tại Dạng đề 3.

4.8. Lớp cơ sở trừu tượng (Abstract base class)

Khái niệm

Lớp cơ sở trừu tượng là một loại lớp đặc biệt vì ta không thể khởi tạo các đối tượng của lớp này.

Mục đính nó tồn tại là chỉ để làm lớp cơ sở cho các lớp dẫn xuất khác kế thừa lên, cung cấp một khuôn mẫu, giao diện chung cho các lớp con đó. Gọi là lớp cơ sở trừu tượng vì nó đại diện cho một khái niệm chung chung, trừu tượng và các lớp con của nó sẽ là các phiên bản cụ thể, rõ ràng hơn. Ta sẽ làm rõ vấn đề hơn qua các ví dụ sau.

Cách định nghĩa một lớp cơ sở trừu tượng

Để một lớp cơ sở là trừu tượng, nó phải có ít nhất một phương thức thuần ảo.

Khác với một phương thức ảo bình thường, phương thức thuần ảo không được định nghĩa. Ta chỉ định một phương thức ảo là thuần ảo bằng cách viết = 0 sau dòng khai báo của nó.

Lấy ví dụ về lớp cơ sở Shape, đại diện chung cho các đối tượng là hình vẽ, các lớp con của nó sẽ là Circle, Square,… Các lớp trong cây kế thừa này sẽ có một phương thức ảo là Draw dùng để vẽ hình.

  • Bởi vì Shape chỉ là một khái niệm trừu tượng về hình vẽ, việc gọi phương thức Draw trên một đối tượng Shape là không hợp lí (vì có biết vẽ cái gì đâu :v) nên ta sẽ cho Shape là một lớp cơ sở trừu tượng và Draw là một phương thức thuần ảo của Shape:
class Shape { 
public:
// phương thức thuần ảo:
virtual void Draw() = 0;
// phương thức Draw của Shape không có hành động cụ thể
};
  • Thật ra hàm Draw có thể được định nghĩa là không làm gì bằng cách chỉ thực hiện câu lệnh return bên trong thân hàm, khi đó Shape không phải lớp cơ sở trừu tượng và ta có thể tạo các đối tượng của Shape và gọi phương thức Draw trên chúng.

  • Tuy nhiên, ghi nhớ rằng một trong các mục đính của việc tạo lớp cơ sở trừu tượng là để ngăn không cho người dùng tạo ra các đối tượng của lớp này và thực hiện các hành động vô nghĩa.

  • Các lớp kế thừa từ Shape phải định nghĩa phương thức Draw, nếu không các lớp đó cũng sẽ là lớp trừu tượng. Do đó việc để một phương thức là thuần ảo còn có tác dụng là bắt buộc các lớp con phải ghi đè lại một phương thức nào đó:

class Square : public Shape { 
private:
int edgeLength;
public:
void Draw() {
// định nghĩa phương thức Draw cho lớp Square
}
};

4.9. Phương thức phá hủy trong kế thừa

Như đã nói ở phần phương thức thiết lập trong kế thừa, khi một đối tượng được khởi tạo, các thuộc tính chung sẽ được khởi tạo trước rồi mới đến các thuộc tính riêng.

Phương thức phá hủy thì ngược lại, khi một destructor của lớp con được gọi, nó sẽ thu hồi các tài nguyên đã cấp phát cho các thuộc tính riêng trước, rồi sau đó destructor của lớp cha mới được gọi để dọn dẹp các thuộc tính chung.

Ví dụ:

class Quote {  
// ...
public:
~Quote() {
cout << “Ham huy cua lop co so!<< endl;
}
};

class BulkQuote : public Quote {
// ...
public:
~BulkQuote() {
cout << “Ham huy cua lop dan xuat!<< endl;
}
};

int main() {
BulkQuote obj;
return 0;
}

Kết quả: Hàm hủy của lớp dẫn xuất được gọi đến trước hàm hủy của lớp cơ sở.

Ham huy cua lop dan xuat!
Ham huy cua lop co so!