Bài giảng Lập trình hướng đối tượng Phần 2

pdf 166 trang Gia Huy 17/05/2022 1990
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Lập trình hướng đối tượng Phần 2", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfbai_giang_lap_trinh_huong_doi_tuong_phan_2.pdf

Nội dung text: Bài giảng Lập trình hướng đối tượng Phần 2

  1. Lập trình h•ớng đối t•ợng Ch•ơng 3: Dẫn xuất và thừa kế Nội dung của ch•ơng tập trung trình bày các vấn đề sau: Khái niệm lớp cơ sở, lớp dẫn xuất. Cách xây dựng lớp dẫn xuất. Các kiểu thừa kế. Phạm vi truy xuất đến các thành phần của lớp cơ sở. Hàm tạo, hàm huỷ, toán tử gán với tính thừa kế Thừa kế nhiều mức và sự trùng tên. Lớp cơ sở ảo. Ph•ơng thức tĩnh, ph•ơng thức ảo. 3.1. Sự dẫn xuất và tính thừa kế 3.1.1. Lớp cơ sở và lớp dẫn xuất Lập trình h•ớng đối t•ợng có hai đặc tr•ng cơ bản: Đóng gói dữ liệu, đ•ợc thể hiện bằng cách dùng khái niệm lớp để biểu diễn đối t•ợng với các thuộc tính private, chỉ cho phép bên ngoài truy nhập vào thông qua các ph•ơng thức. Dùng lại mã, thể hiện bằng việc thừa kế giữa các lớp. Việc thừa kế cho phép các lớp thừa kế (gọi là lớp dẫn xuất) sử dụng lại các ph•ơng thức đã đ•ợc định nghĩa trong các lớp gốc (gọi là lớp cơ sở). Một lớp có thể đ•ợc dẫn xuất từ nhiều lớp cơ sở, một lớp cơ sở cũng có thể là lớp cơ sở của nhiều lớp dẫn xuất. 3.1.2. Cách xây dựng lớp dẫn xuất Cú pháp khai báo một lớp thừa kế từ một lớp khác nh• sau: class : { // Khai báo các thành phần của lớp }; Trong đó: Trang - 88 -
  2. Lập trình h•ớng đối t•ợng Tên lớp dẫn xuất: là tên lớp đ•ợc cho thừa kế từ lớp khác. Tên lớp này tuân thủ theo quy tắc đặt tên biến trong C++. Tên lớp cở sở: là tên lớp đã đ•ợc định nghĩa tr•ớc đó để cho lớp khác thừa kế. Tên lớp này cũng tuân thủ theo quy tắc đặt tên biến của C++. Từ khóa dẫn xuất: là từ khóa quy định tính chất của sự thừa kế. Có ba từ khóa dẫn xuất là private, protected và public. Ví dụ 3.1: class Bus: public Car { // Khai báo các thành phần }; là khai báo một lớp Bus (xe buýt) thừa kế từ lớp Car (xe ô tô) với tính chất thừa kế là public. 3.1.3. Các kiểu thừa kế Sự thừa kế cho phép trong lớp dẫn xuất có thể sử dụng lại một số mã nguồn của các ph•ơng thức và thuộc tính đã đ•ợc định nghĩa trong lớp cơ sở. Nghĩa là lớp dẫn xuất có thể truy nhập trực tiếp đến một số thành phần của lớp cơ sở. Tuy nhiên, phạm vi truy nhập từ lớp dẫn xuất đến lớp cơ sở không phải bao giờ cũng giống nhau: chúng đ•ợc quy định bởi các từ khóa dẫn xuất private, protected và public. Thừa kế private Theo kiểu thừa kế này: Các thành phần private của lớp cơ sở thì không thể truy nhập đ•ợc từ lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành các thành phần private của lớp dẫn xuất. Các thành phần public của lớp cơ sở cũng trở thành các thành phần private của lớp dẫn xuất. Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất đ•ợc tuân thủ nh• quy tắc phạm vi lớp thông th•ờng. Thừa kế protected Theo kiểu thừa kế này: Trang - 89 -
  3. Lập trình h•ớng đối t•ợng Các thành phần private của lớp cơ sở thì không thể truy nhập đ•ợc từ lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành các thành phần protected của lớp dẫn xuất. Các thành phần public của lớp cơ sở cũng trở thành các thành phần protected của lớp dẫn xuất. Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất đ•ợc tuân thủ nh• quy tắc phạm vi lớp thông th•ờng. Thừa kế public Theo kiểu thừa kế này: Các thành phần private của lớp cơ sở thì không thể truy nhập đ•ợc từ lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành các thành phần protected của lớp dẫn xuất. Các thành phần public của lớp cơ sở vẫn là các thành phần public của lớp dẫn xuất. Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất đ•ợc tuân thủ nh• quy tắc phạm vi lớp thông th•ờng. 3.1.4. Thừa kế các thành phần dữ liệu Các thuộc tính của lớp cơ sở đ•ợc thừa kế trong lớp dẫn xuất. Nh• vậy, tập thuộc tính trong lớp dẫn xuất sẽ bao gồm: các thuộc tính mới khai báo trong lớp dẫn xuất và các thuộc tính mà lớp dẫn xuất đ•ợc thừa kế từ các lớp cơ sở có liên quan. Tuy vậy, trong các ph•ơng thức của lớp dẫn xuất không đ•ợc phép truy nhập vào các thuộc tính private của lớp cơ sở. Tên của thuộc tính trong lớp dẫn xuất và trong lớp cơ sở có thể đặt trùng nhau. Ví dụ 3.2: class Sinhvien { private: char *MaSV; char *TenSV; char *Diachi; char *Gioitinh; Trang - 90 -
  4. Lập trình h•ớng đối t•ợng float DiemToanCC; float DiemVatly; float DiemAnhvan; public: void nhap(); void hienthi(); }; class SinhvienCNTT:Sinhvien { private: float DiemLTC; float DiemCSDL; public: void nhap(); void hienthi(); int Xet_hocbong(); }; Khi đó lớp SinhvienCNTT ngoài các thuộc tính DiemLTC, DiemCSDL còn có các thuộc tính: MaSV, TenSV, Diachi, Gioitinh, DiemToanCC, DiemVatly, DiemAnhvan đ•ợc thừa kế từ lớp Sinhvien. 3.1.5. Thừa kế ph•ơng thức Trừ: Hàm tạo, hàm huỷ và toán tử gán, các ph•ơng thức (public) khác của lớp cơ sở đ•ợc thừa kế trong lớp dẫn xuất. Tên các ph•ơng thức của lớp dẫn xuất và lớp cơ sở có thể đặt trùng nhau. Ví dụ 3.3: #include #include #include #include #include #define MAX_TEN 50 #define MAX_MASO 5 #define MUC_CO_BAN 830000 Trang - 91 -
  5. Lập trình h•ớng đối t•ợng class Nguoi { protected: char HoTen[MAX_TEN]; char MaSo[MAX_MASO]; float Luong; public: Nguoi(); void Xuat(); void Nhap(); virtual void TinhLuong()=0; }; Nguoi::Nguoi() { strcpy(HoTen,""); strcpy(MaSo,""); Luong=0; } void Nguoi::Xuat() { cout >MaSo; cin.ignore(); cout<<"Ho va ten:"; cin.getline(HoTen,MAX_TEN); } class BienChe: public Nguoi Trang - 92 -
  6. Lập trình h•ớng đối t•ợng { protected: float HeSoLuong; float HeSoPhuCap; public: BienChe(); void TinhLuong(); void Nhap(); }; BienChe::BienChe() { HeSoLuong=HeSoPhuCap=0; } void BienChe::Nhap() { Nguoi::Nhap(); cout >HeSoLuong; cout >HeSoPhuCap; } void BienChe::TinhLuong() { Luong=MUC_CO_BAN*(1.0+HeSoLuong+HeSoPhuCap); } class HopDong : public Nguoi { protected: float TienCong; float NgayCong; float HeSoVuotGio; public: HopDong(); Trang - 93 -
  7. Lập trình h•ớng đối t•ợng void TinhLuong(); void Nhap(); }; HopDong::HopDong() { TienCong=NgayCong=HeSoVuotGio=0; } void HopDong::Nhap() { Nguoi::Nhap(); cout >TienCong; cout >NgayCong; cout >HeSoVuotGio; } void HopDong::TinhLuong() { Luong=TienCong*NgayCong*(1+HeSoVuotGio); } int main() { Nguoi *Ng[100]; int N=0; char Chon,Loai; clrscr(); do { cout >Loai; Loai=toupper(Loai); if (Loai=='B') Trang - 94 -
  8. Lập trình h•ớng đối t•ợng Ng[N]=new BienChe; else Ng[N]=new HopDong; Ng[N++]->Nhap(); cout >Chon; Chon=toupper(Chon); if ((N==100)||(Chon=='K')) break; }while (1); for(int I=0;I TinhLuong(); Ng[I]->Xuat(); } return 0; } 3.2. Hàm tạo và hàm huỷ đối với tính thừa kế 3.2.1. Xây dựng hàm tạo của lớp dẫn xuất Khi khai báo một đối t•ợng có kiểu lớp đ•ợc dẫn xuất từ một lớp cơ sở khác. Ch•ơng trình sẽ tự động gọi tới hàm tạo của lớp dẫn xuất. Tuy nhiên, thứ tự đ•ợc gọi sẽ bắt đầu từ hàm tạo t•ơng ứng của lớp cơ sở, sau đó đến hàm tạo của lớp dẫn xuất. Do đó, thông th•ờng, trong hàm tạo của lớp dẫn xuất phải có hàm tạo của lớp cơ sở. Cú pháp khai báo hàm tạo nh• sau: ([ ]): ([ ]) { // Khởi tạo các thuộc tính mới bổ sung của lớp dẫn xuất }; Vì tên hàm tạo là trùng với tên lớp, nên có thể viết lại thành: ([ ]): Trang - 95 -
  9. Lập trình h•ớng đối t•ợng ([ ]) { // Khởi tạo các thuộc tính mới bổ sung của lớp dẫn xuất }; Ví dụ 3.4: Bus():Car() { // Khởi tạo các thuộc tính mới bổ sung của lớp Bus } là một định nghĩa hàm tạo của lớp Bus thừa kế từ lớp Car. Định nghĩa này đ•ợc thực hiện trong phạm vi khai báo lớp Bus. Đây là một hàm tạo không tham số, nó gọi tới hàm tạo không tham số của lớp Car. L•u ý: Nếu định nghĩa hàm tạo bên ngoài phạm vi lớp thì phải thêm tên lớp dẫn xuất và toán tử phạm vi “::” tr•ớc tên hàm tạo. Giữa tên hàm tạo của lớp dẫn xuất và hàm tạo của lớp cơ sở, chỉ có một dấu hai chấm “:”, nếu là hai dấu “::” thì trở thành toán tử phạm vi lớp. Nếu không chỉ rõ hàm tạo của lớp cơ sở sau dấu hai chấm “:” ch•ơng trình sẽ tự động gọi hàm tạo ngầm định hoặc hàm tạo không có tham số của lớp cơ sở nếu hàm đó đ•ợc định nghĩa t•ờng minh trong lớp cơ sở. Ví dụ, định nghĩa hàm tạo: Bus():Car() { // Khởi tạo các thuộc tính mới bổ sung của lớp Bus }; Có thể thay bằng: Bus() { //Gọi hàm tạo không tham số của lớp Car // Khởi tạo các thuộc tính mới bổ sung của lớp Bus }; Ví dụ sau định nghĩa lớp Car có 3 thuộc tính với hai hàm tạo, sau đó định nghĩa lớp Bus có thêm thuộc tính label là số hiệu của tuyến xe buýt. Lớp Bus sẽ đ•ợc cài đặt hai hàm tạo t•ờng minh, gọi đến hai hàm tạo t•ơng ứng của lớp Car. Trang - 96 -
  10. Lập trình h•ớng đối t•ợng Ví dụ 3.5: #include /* Định nghĩa lớp Car */ class Car { int speed; // Tốc độ char mark[20]; // Nhãn hiệu float price; // Giá xe public: Car(); // Hàm tạo không tham số Car(int, char[], float); // Hàm tạo có tham số }; Car::Car() // Hàm tạo không tham số { speed = 0; strcpy(mark, “”); price = 0; } // Hàm tạo có tham số Car::Car(int speedIn, char markIn[], float priceIn) { speed = speedIn; strcpy(mark, markIn); price = priceIn; } /* Định nghĩa lớp Bus thừa kế từ lớp Car */ class Bus: public Car { int label; // Số hiệu tuyến xe public: Bus(); // Hàm tạo không tham số Bus(int, char[], float, int); // Hàm tạo có tham số }; Trang - 97 -
  11. Lập trình h•ớng đối t•ợng Bus::Bus():Car() // Hàm tạo không tham số { label = 0; } // Hàm tạo có tham số Bus::Bus(int sIn, char mIn[], float pIn, int lIn):Car(sIn, mIn, pIn) { label = lIn; } Trong hàm tạo của lớp Bus, muốn khởi tạo các thuộc tính của lớp Car, ta phải khởi tạo gián tiếp thông qua hàm tạo của lớp Car mà không thể gán giá trị trực tiếp cho các thuộc tính speed, mark và price. Lí do là các thuộc tính này có tính chất private, nên lớp dẫn xuất không thể truy nhập trực tiếp đến chúng. 3.2.2. Hàm huỷ của lớp dẫn xuất Khi một đối t•ợng của lớp dẫn xuất bị giải phóng khỏi bộ nhớ, thứ tự gọi các hàm hủy ng•ợc với thứ tự gọi hàm tạo: gọi hàm hủy của lớp dẫn xuất tr•ớc khi gọi hàm hủy của lớp cơ sở. Vì mỗi lớp chỉ có nhiều nhất là một hàm hủy, nên ta không cần phải chỉ ra hàm hủy nào của lớp cơ sở sẽ đ•ợc gọi sau khi hủy bỏ đối t•ợng của lớp dẫn xuất. Do vậy, hàm hủy trong lớp dẫn xuất đ•ợc khai báo và định nghĩa hoàn toàn giống với các lớp thông th•ờng: ::~ ([ ]) { // giải phóng phần bộ nhớ cấp phát cho các thuộc tính bổ sung } L•u ý: Hàm hủy của lớp dẫn xuất chỉ giải phóng phần bộ nhớ đ•ợc cấp phát động cho các thuộc tính mới bổ sung trong lớp dẫn xuất (nếu có), mà không đ•ợc giải phóng bộ nhớ đ•ợc cấp cho các thuộc tính trong lớp cơ sở (phần này là do hàm hủy của lớp cơ sở đảm nhiệm). Không phải gọi t•ờng minh hàm hủy của lớp cơ sở trong hàm hủy của lớp dẫn xuất. Ngay cả khi lớp dẫn xuất không định nghĩa t•ờng minh hàm hủy (do không cần thiết) mà lớp cơ sở lại có định nghĩa t•ờng minh. Ch•ơng trình vẫn gọi hàm Trang - 98 -
  12. Lập trình h•ớng đối t•ợng hủy ngầm định của lớp dẫn xuất, sau đó vẫn gọi hàm hủy t•ờng minh của lớp cơ sở. Ví dụ sau cài đặt lớp Bus thừa kế từ lớp Car: lớp Car có một thuộc tính có dạng con trỏ nên cần giải phóng bằng hàm hủy t•ờng minh. Lớp Bus có thêm một thuộc tính có dạng con trỏ là danh sách các đ•ờng phố mà xe buýt đi qua (mảng động các chuỗi kí tự *char[]) nên cũng cần giải phóng bằng hàm hủy t•ờng minh. Ví dụ 3.6: #include /* Định nghĩa lớp Car */ class Car { char *mark; // Nhãn hiệu xe public: ~Car(); // Hủy bỏ t•ờng minh }; Car::~Car(){ // Hủy bỏ t•ờng minh delete [] mark; } /* Định nghĩa lớp Bus thừa kế từ lớp Car */ class Bus: public Car { char *voyage[]; // Hành trình tuyến xe public: ~Bus(); // Hủy bỏ t•ờng minh }; Bus::~Bus(){ // Hủy bỏ t•ờng minh delete [] voyage; } Trong hàm hủy của lớp Bus, ta chỉ đ•ợc giải phóng vùng nhớ đ•ợc cấp phát cho thuộc tính voyage (hành trình của xe buýt), là thuộc tính đ•ợc bổ sung thêm của lớp Bus mà không đ•ợc giải phóng vùng nhớ cấp phát cho thuộc tính mark Trang - 99 -
  13. Lập trình h•ớng đối t•ợng (nhãn hiệu xe), việc này là thuộc trách nhiệm của hàm hủy của lớp Car vì thuộc tính mark đ•ợc khai báo trong lớp Car. 3.3. Phạm vi truy xuất đến các thành phần của lớp cơ sở Ta xét phạm vi truy xuất theo hai loại: Phạm vi truy xuất từ các hàm bạn, lớp bạn của lớp dẫn xuất Phạm vi truy xuất từ các đối t•ợng có kiểu lớp dẫn xuất Truy xuất từ các hàm bạn và lớp bạn của lớp dẫn xuất Với dẫn xuất private, hàm bạn có thể truy xuất đ•ợc các thành phần protected và public của lớp cơ sở vì chúng trở thành các thành phần private của lớp dẫn xuất, có thể truy xuất đ•ợc từ hàm bạn. Với dẫn xuất protected, hàm bạn cũng có thể truy xuất đ•ợc các thành phần protected và public của lớp cơ sở vì chúng trở thành các thành phần protected của lớp dẫn xuất, có thể truy xuất đ•ợc từ hàm bạn. Với dẫn xuất public, hàm bạn cũng có thể truy xuất đ•ợc các thành phần protected và public của lớp cơ sở vì chúng trở thành các thành phần protected và public của lớp dẫn xuất, có thể truy xuất đ•ợc từ hàm bạn. Đối với cả ba loại dẫn xuất, hàm bạn đều không truy xuất đ•ợc các thành phần private của lớp cơ sở, vì các thành phần này cũng không truy xuất đ•ợc từ lớp dẫn xuất. Truy xuất từ các đối t•ợng tạo bởi lớp dẫn xuất Với dẫn xuất private, đối t•ợng của lớp dẫn xuất không truy xuất đ•ợc bất cứ thành phần nào của lớp cơ sở vì chúng trở thành các thành phần private của lớp dẫn xuất, không truy nhập đ•ợc từ bên ngoài. Với dẫn xuất protected, đối t•ợng của lớp dẫn xuất không truy xuất đ•ợc bất cứ thành phần nào của lớp cơ sở vì chúng trở thành các thành phần protected của lớp dẫn xuất, không truy xuất đ•ợc từ bên ngoài. Với dẫn xuất public, đối t•ợng của lớp dẫn xuất có thể truy xuất đ•ợc các thành phần public của lớp cơ sở vì chúng trở thành các thành phần public của lớp dẫn xuất, có thể truy nhập đ•ợc từ bên ngoài. Việc gọi đến các thành phần của lớp cơ sở cũng t•ơng tự nh• gọi các thành phần lớp thông th•ờng: Đối với biến đối t•ợng thông th•ờng: Trang - 100 -
  14. Lập trình h•ớng đối t•ợng . ([Các tham số]); Đối với con trỏ đối t•ợng: -> ([Các tham số]); L•u ý: Cách gọi hàm thành phần này đ•ợc áp dụng khi trong lớp dẫn xuất, ta không định nghĩa lại các hàm thành phần của lớp cơ sở. Ví dụ sau minh họa việc sử dụng các thành phần lớp cơ sở từ đối t•ợng lớp dẫn xuất: lớpBus thừa kế từ lớp Car. Lớp Bus có định nghĩa bổ sung một số ph•ơng thức và thuộc tính mới. Khi đó, đối t•ợng của lớp Bus có thể gọi các hàm public của lớp Bus cũng nh• của lớp Car. Ví dụ 3.7: #include #include #include /* Định nghĩa lớp Car */ class Car { private: int speed; // Tốc độ char mark[20]; // Nhãn hiệu float price; // Giá xe public: void setSpeed(int); // Gán tốc độ cho xe int getSpeed(); // Đọc tốc độ xe void setMark(char); // Gán nhãn cho xe char[] getMark(); // Đọc nhãn xe void setPrice(float); // Gán giá cho xe float getPrice(); // Đọc giá xe // Khởi tạo thông tin về xe Car(int speedIn=0, char markIn[]=””, float priceIn=0); void show(); // Giới thiệu xe }; /* Khai báo ph•ơng thức bên ngoài lớp */ Trang - 101 -
  15. Lập trình h•ớng đối t•ợng Car::Car(int speedIn, char markIn[], float priceIn) { speed = speedIn; strcpy(mark, markIn); price = priceIn; } void Car::setSpeed(int speedIn) // Gán tốc độ cho xe { speed = speedIn; } int Car::getSpeed() // Đọc tốc độ xe { return speed; } void Car::setMark(char markIn) // Gán nhãn cho xe { strcpy(mark, markIn); } char[] Car::getMark()// Đọc nhãn xe { return mark; } void Car::setPrice(float priceIn) // Gán giá cho xe { price = priceIn; } float Car::getPrice()// Đọc giá xe { return price; } void Car::show() // Ph•ơng thức giới thiệu xe { cout << “This is a ” << mark << “ having a speed of ” Trang - 102 -
  16. Lập trình h•ớng đối t•ợng << speed << “km/h and its price is $” << price << endl; return; } /* Định nghĩa lớp Bus thừa kế từ lớp Car */ class Bus: public Car { int label; // Số hiệu tuyến xe public: Bus(int sIn=0, char mIn[]=””, float pIn=0, int lIn=0); void setLabel(int); // Gán số hiệu tuyến xe int getLabel(); // Đọc số hiệu tuyến xe }; Bus::Bus(int sIn, char mIn[], float pIn, int lIn):Car(sIn, mIn, pIn) { label = lIn; } void Bus::setLabel(int labelIn) // Gán số hiệu tuyến xe { label = labelIn; } int Bus::getLabel() // Đọc số hiệu tuyến xe { return label; } // Ch•ơng trình chính void main() { clrscr(); Bus myBus; // Biến đối t•ợng của lớp Bus int speedIn, labelIn; float priceIn; char markIn[20]; // Nhập giá trị cho các thuộc tính Trang - 103 -
  17. Lập trình h•ớng đối t•ợng cout > speedIn; cout > markIn; cout > priceIn; cout > labelIn; myBus.setSpeed(speedIn); // Ph•ơng thức của lớp Car myBus.setMark(markIn); // Ph•ơng thức của lớp Car myBus.setPrice(priceIn); // Ph•ơng thức của lớp Car myBus.setLabel(labelIn); // Ph•ơng thức của lớp Bus myBus.show(); // Ph•ơng thức của lớp Car return; } Trong ch•ơng trình trên, đối t•ợng myBus có kiểu lớp Bus, là lớp dẫn xuất của lớp cơ sở Car, có thể sử dụng các ph•ơng thức của lớp Car và lớp Bus một cách bình đẳng. Khi đó, lệnh myBus.show() sẽ gọi đến ph•ơng thức show() của lớp Car. 3.4. Thừa kế nhiều mức và sự trùng tên 3.4.1. Sơ đồ xây dựng các lớp dẫn xuất theo nhiều mức Một lớp sau khi khai báo có thể dùng làm lớp cơ sở cho các lớp dẫn xuất, đến l•ợt mình các lớp dẫn xuất lại có thể là lớp cơ sở cho các lớp dẫn xuất khác. Sự tiếp tục theo cách trên là không hạn chế. Hiện t•ợng này đ•ợc gọi là sự thừa kế nhiều mức. Một lớp có thể đ•ợc dẫn xuất từ nhiều lớp cơ sở. Để dễ hình dung ta có thể đ•a ra sơ đồ minh hoạ việc xây dựng các lớp dẫn xuất theo nhiều mức nh• sau: Trang - 104 -
  18. Lập trình h•ớng đối t•ợng A B C D E F G H Sơ đồ 3.1 Trong sơ đồ 3.1: Lớp A, lớp B là lớp cơ sở của lớp D Lớp A, lớp B, lớp D là lớp cơ sở của lớp F, lớp G Lớp B, lớp C là lớp cơ sở của lớp E Lớp B, lớp C, lớp E là lớp cơ sở của lớp G, lớp H 3.4.2. Sự thừa kế nhiều mức Theo tính thừa kế: lớp dẫn xuất đ•ợc thừa kế tất cả các thành phần (thuộc tính và ph•ơng thức) của lớp cơ sở, kể cả các thành phần mà lớp cơ sở đ•ợc thừa kế. Hay có thể nói lớp dẫn xuất đ•ợc thừa kế tất cả các thành phần của các lớp cơ sở có liên quan (các lớp tiền bối). Trong sơ đồ dẫn xuất 3.1: Lớp D đ•ợc thừa kế tất cả các thành phần của lớp A, B Lớp F, G đ•ợc thừa kế tất cả các thành phần của lớp D, A, B Lớp E đ•ợc thừa kế tất cả các thành phần của lớp B, lớp C Lớp H, lớp G đ•ợc thừa kế tất cả các thành phần của lớp B, lớp C, lớp E 3.4.3. Sự trùng tên Nguyên tắc đặt tên khi xây dựng các lớp đối t•ợng nh• sau: Tên các lớp không đ•ợc trùng nhau Tên các thuộc tính đ•ợc khai báo trong cùng một lớp không đ•ợc trùng nhau Tên các thuộc tính, ph•ơng thức của các lớp khác nhau có thể đặt trùng nhau Nguyên tắc thứ ba, và sự dẫn xuất nhiều mức sẽ dẫn đến hiện t•ợng trong một lớp sẽ có nhiều thành phần trùng tên nhau (do một số thành phần đ•ợc thừa kế). Ví dụ 3.8 : Trang - 105 -
  19. Lập trình h•ớng đối t•ợng class A { protected : int x, y ; }; class B { public : int x, z ; }; class C : public A, public B { private : int y, z, t ; }; Trong ví dụ này lớp C sẽ có các thuộc tính có tên là: y, z, t, x. Trong đó, hai thuộc tính có tên x, một đ•ợc thừa kế từ A, một đ•ợc thừa kế từ B; hai thuộc tính có tên là y, một là thuộc riêng của C, một đ•ợc thừa kế từ A; hai thuộc tính có tên là z. 3.4.4. Sử dụng các thành phần trong lớp dẫn xuất Trong sơ dẫn xuất nhiều mức có thể xuất hiện hiện t•ợng trùng tên. Để phân biệt các thành phần trùng tên này, ta cần sử dụng tên_lớp và toán tử phạm vi ( :: ) tr•ớc tên thành phần. Trong ví dụ 3.8, trong ph•ơng thức của lớp C để truy nhập vào thuộc tính x ta cần viết: A::x để truy nhập vào thuộc tính x đ•ợc thừa kế từ A hoặc B::x để truy nhập vào thuộc tính x đ•ợc thừa kế từ B. Trong tr•ờng hợp ta không sử dụng tên lớp và toán tử phạm tr•ớc tên thành phần, ch•ơng trình dịch sẽ phải tự xác định để biết thành phần đó thuộc lớp nào theo quy tắc: Tr•ớc tiên nó xem thành phần đang xét có trùng tên với thành phần nào của lớp dẫn xuất hay không. Nếu không trùng thì tiếp tục xét đến các lớp cơ sở có quan hệ gần nhất với lớp dẫn xuất trong sơ đồ thừa kế (lớp “cha” -> lớp “ông”-> ). Nếu thành phần đang xét xuất hiện trong hai lớp cơ sở đồng cấp thì Trang - 106 -
  20. Lập trình h•ớng đối t•ợng ch•ơng trình dịch không thể xác định đ•ợc thành phần này thuộc lớp nào và phải đ•a ra thông báo lỗi. Ví dụ thuộc tính x trong ví dụ 3.8. 3.5. Lớp cơ sở ảo 3.5.1. Một số lớp cơ sở xuất hiện nhiều lần trong lớp dẫn xuất A B C D E F G H Sơ đồ 3.2 Trong sơ đồ dẫn xuất trên ta thấy lớp cơ sở B đ•ợc đề cập hai lần trong lớp dẫn xuất G thông qua hai lớp cơ sở trung gian D và E. Khi đó, trong lớp G nếu ta truy nhập vào một thành phần của lớp B thì ch•ơng trình dịch sẽ không thể phân biệt đ•ợc thành phần đó đ•ợc thừa kế thông qua lớp D hay lớp E và đ•a ra thông báo Member is ambigous 3.5.2. Các lớp cơ sở ảo Giải pháp cho vấn đề một lớp cơ sở xuất hiện nhiều lần trong lớp dẫn xuất ta sử dụng lớp cơ sở ảo. Các lớp cơ sở ảo sẽ đ•ợc kết hợp để tạo một lớp cơ sở duy nhất cho bất kỳ lớp nào dẫn xuất từ chúng. Trong sơ đồ dẫn xuất 3.2 ta cần khai báo lớp B là lớp cơ sở ảo. Để một lớp trở thành lớp cơ sở ảo, ta chỉ cần thêm từ khoá virtual tr•ớc tên lớp cơ sở khi xây dựng lớp dẫn xuất. Ví dụ 3.9: class B { //Khai báo các thành phần của lớp B; }; class D : virtual public B { //Khai báo các thành phần của lớp D Trang - 107 -
  21. Lập trình h•ớng đối t•ợng }; class E: virtual B { //Khai báo các thành phần của lớp E }; 3.6. Toán tử gán của lớp dẫn xuất 3.6.1. Khi nào cần xây dựng toán tử gán Khi lớp dẫn xuất có các thuộc tính kiểu con trỏ hoặc tham chiếu (kể cả các thuộc tính đ•ợc thừa kế) thì không đ•ợc sử dụng toán tử gán mặc định mà cần xây dựng toán tử gán t•ờng minh cho lớp dẫn xuất. 3.6.2. Cách xây dựng toán tử gán cho lớp dẫn xuất Để xây dựng toán tử gán cho lớp dẫn xuất ta thực hiện các b•ớc sau: B1. Xây dựng toán tử gán cho các lớp cơ sở và các lớp thành phần B2. Xây dựng toán tử gán cho lớp dẫn xuất trong đó có gọi toán tử gán của lớp cơ sở và toán tử gán của các lớp thành phần. Để sử dụng toán tử gán của lớp cơ sở cần ép kiểu theo cú pháp: Tên_lớp_cơ_sở(*this)= Tên_lớp_cơ_sở::operator=(Tham số) Ví dụ 3.10: D &operator=(D &h) { B(*this) = B::operator=(h); //các lệnh gán giá trị cho các thuộc tính của lớp D } 3.7. Hàm tạo sao chép của lớp dẫn xuất 3.7.1. Khi nào cần xây dựng hàm tạo sao chép Khi lớp dẫn xuất có các thuộc tính kiểu con trỏ hoặc tham chiếu (kể cả các thuộc tính đ•ợc thừa kế) thì không đ•ợc sử dụng hàm tạo sao chép mặc định mà cần xây dựng hàm tạo sao chép t•ờng minh cho lớp dẫn xuất. Trang - 108 -
  22. Lập trình h•ớng đối t•ợng 3.7.2. Cách xây dựng hàm tạo sao chép cho lớp dẫn xuất Để xây dựng hàm tạo sao chép cho lớp dẫn xuất ta thực hiện các b•ớc sau: B1. Xây dựng hàm tạo cho các lớp cơ sở và các lớp thành phần B2. Xây dựng hàm tạo sao chép cho lớp dẫn xuất trong đó có gọi hàm tạo sao chép của lớp cơ sở và hàm tạo sao chép của các lớp thành phần. Ví dụ 3.11: #include #include class point { float x,y; public: point() {x = 0; y = 0;} point(float ox, float oy) {x = ox; y = oy; } point(point &p) {x = p.x; y = p.y;} void display() { cout<<"Goi ham point::display() \n"; cout<<"Toa do :"<<x<<" "<<y<<endl; } void move(float dx, float dy) { x += dx; y += dy; } }; /*lớp coloredpoint dẫn xuất từ lớp point*/ class coloredpoint : public point { unsigned int color; public: coloredpoint():point() { Trang - 109 -
  23. Lập trình h•ớng đối t•ợng color =0; } coloredpoint(float ox, float oy, unsigned int c):point(ox,oy) { color = c; } coloredpoint(coloredpoint &b):point((point &)b) { color = b.color; } void display() { cout<<"Ham coloredpoint::display()\n"; point::display(); cout<<"Mau "<<color<<endl; } }; void main() { clrscr(); coloredpoint m(2,3,5); cout<<"Diem m \n"; m.display(); cout<<"coloredpoint p =m;\n"; coloredpoint p =m; cout<<"Diem p \n"; p.display(); getch(); } 3.8. Ph•ơng thức tĩnh Ta xét ví dụ sau: Ví dụ 3.12 : Trang - 110 -
  24. Lập trình h•ớng đối t•ợng #include #include class Car { private: //Khai báo các thuộc tính của lớp Car public: void show() { cout show(); getch(); } Chạy ch•ơng trình trên cho kết quả là: Day la lop Bus Day la lop Car. Trang - 111 -
  25. Lập trình h•ớng đối t•ợng Nh• vậy, khi gọi ph•ơng thức show từ đối t•ợng của lớp Bus (myBus) thì ch•ơng trình thực hiện ph•ơng thức show đ•ợc khai báo trong lớp Bus. Còn khi gọi ph•ơng thức show từ con trỏ ptrCar là con trỏ kiểu lớp Car thì ch•ơng trình lại thực hiện ph•ơng thức show() của lớp Car, mà không gọi tới ph•ơng thức show() của lớp Bus mặc dù con trỏ ptrCar đang trỏ tới đối t•ợng myBus của lớp Bus vì ph•ơng thức show() là một ph•ơng thức tĩnh. Đây chính là nh•ợc điểm của ph•ơng thức tĩnh. Nh• vậy, ta có thể đ•a ra quy tắc gọi các ph•ơng thức tĩnh nh• sau: Nếu lời gọi ph•ơng thức tĩnh xuất phát từ một đối t•ợng của lớp nào thì ph•ơng thức của lớp đó sẽ đ•ợc gọi Nếu lời gọi ph•ơng thức tĩnh xuất phát từ một con trỏ kiểu lớp nào thì ph•ơng thức của lớp đó sẽ đ•ợc gọi mà không phụ thuộc vào con trỏ đó đang chứa địa chỉ của đối t•ợng của lớp nào. 3.9. Sự hạn chế của ph•ơng thức tĩnh Sự thừa kế trong C++ cho phép có sự t•ơng ứng giữa lớp cơ sở và các lớp dẫn xuất trong sơ đồ thừa kế: Một con trỏ có kiểu lớp cơ sở luôn có thể trỏ đến địa chỉ của một đối t•ợng của lớp dẫn xuất. Tuy nhiên, khi thực hiện lời gọi một ph•ơng thức tĩnh của lớp từ một con trỏ, trình biên dịch sẽ quan tâm đến kiểu của con trỏ chứ không phải đối t•ợng mà con trỏ đang trỏ tới: ph•ơng thức của lớp mà con trỏ có kiểu đ•ợc gọi chứ không phải ph•ơng thức của đối t•ợng mà con trỏ đang trỏ tới đ•ợc gọi. Điều này đ•ợc thể hiện trong ví dụ 3.12. Đây chính là nh•ợc điểm của ph•ơng thức tĩnh. Để giải quyết vấn đề này, C++ đ•a ra một khái niệm là ph•ơng thức ảo. Bằng cách sử dụng ph•ơng thức ảo. Khi gọi một ph•ơng thức từ một con trỏ đối t•ợng, trình biên dịch sẽ xác định kiểu của đối t•ợng mà con trỏ đang trỏ đến, sau đó nó sẽ gọi ph•ơng thức t•ơng ứng với đối t•ợng mà con trỏ đang trỏ tới. 3.10. Ph•ơng thức ảo và t•ơng ứng bội 3.10.1. Cách định nghĩa ph•ơng thức ảo Ph•ơng thức ảo (còn gọi là ph•ơng thức trừu t•ợng) đ•ợc khai báo với từ khoá virtual: Trang - 112 -
  26. Lập trình h•ớng đối t•ợng Nếu khai báo trong phạm vi lớp: virtual ([ ]); Nếu định nghĩa ngoài phạm vi lớp: virtual :: ([ ]) { } Ví dụ 3.13: class Car { public: virtual void show(); }; là khai báo ph•ơng thức ảo show() của lớp Car: ph•ơng thức không có tham số và không cần giá trị trả về (void). L•u ý: Từ khoá virtual có thể đặt tr•ớc hay sau kiểu trả về của ph•ơng thức. Với cùng một ph•ơng thức đ•ợc khai báo ở lớp cơ sở lẫn lớp dẫn xuất, chỉ cần dùng từ khoá virtual ở một trong hai lần định nghĩa ph•ơng thức đó là đủ: hoặc ở lớp cơ sở, hoặc ở lớp dẫn xuất. Trong tr•ờng hợp cây thừa kế có nhiều mức, cũng chỉ cần khai báo ph•ơng thức là ảo (virtual) ở một mức bất kì. Khi đó, tất cả các ph•ơng thức trùng tên với ph•ơng thức đó ở tất cả các mức đều đ•ợc coi là ph•ơng thức ảo. Đôi khi không cần thiết phải định nghĩa chồng (trong lớp dẫn xuất) một ph•ơng thức đã đ•ợc khai báo là ph•ơng thức ảo trong lớp cơ sở. 3.10.2. Quy tắc gọi ph•ơng thức ảo Một khi ph•ơng thức đ•ợc khai báo là ảo thì khi một con trỏ gọi đến ph•ơng thức đó, ch•ơng trình sẽ thực hiện ph•ơng thức t•ơng ứng với đối t•ợng mà con trỏ đang trỏ tới, thay vì thực hiện ph•ơng thức của lớp cùng kiểu với con trỏ. Đây đ•ợc gọi là hiện t•ợng đa hình (t•ơng ứng bội) trong C++. Ch•ơng trình sau minh hoạ việc sử dụng ph•ơng thức trừu t•ợng: lớp Bus thừa kế từ lớp Car, hai lớp này cùng định nghĩa ph•ơng thức trừu t•ợng show(). Khi ta dùng một con trỏ có kiểu lớp Car trỏ vào địa chỉ của một đối t•ợng kiểu Car, nó Trang - 113 -
  27. Lập trình h•ớng đối t•ợng sẽ gọi ph•ơng thức show() của lớp Car. Khi ta dùng cũng con trỏ đó, trỏ vào địa chỉ của một đối t•ợng kiểu Bus, nó sẽ gọi ph•ơng thức show() của lớp Bus. Ví dụ 3.14: #include #include #include /* Định nghĩa lớp Car */ class Car { private: int speed; // Tốc độ char mark[20]; // Nhãn hiệu float price; // Giá xe public: int getSpeed(){return speed;};// Đọc tốc độ xe char[] getMark(){return mark;};// Đọc nhãn xe float getPrice(){return price;};// Đọc giá xe // Khởi tạo thông tin về xe Car(int speedIn=0, char markIn[]=””, float priceIn=0); virtual void show(); // Giới thiệu xe, ph•ơng thức ảo }; /* Khai báo ph•ơng thức bên ngoài lớp */ Car::Car(int speedIn, char markIn[], float priceIn) { speed = speedIn; strcpy(mark, markIn); price = priceIn; } // Ph•ơng thức ảo giới thiệu xe virtual void Car::show() { cout << “This is a ” << mark << “ having a speed of ” << speed << “km/h and its price is $” << price << endl; Trang - 114 -
  28. Lập trình h•ớng đối t•ợng return; } /* Định nghĩa lớp Bus thừa kế từ lớp Car */ class Bus: public Car { int label; // Số hiệu tuyến xe public: Bus(int sIn=0, char mIn[]=””, float pIn=0, int lIn=0); void show(); // Giới thiệu xe }; Bus::Bus(int sIn, char mIn[], float pIn, int lIn):Car(sIn, mIn, pIn) { label = lIn; } // Định nghĩa chồng ph•ơng thức ảo void Bus::show(){ // Giới thiệu xe bus cout show(); // Ph•ơng thức của lớp Car ptrCar = &myBus; // Trỏ đến đối t•ợng lớp Bus ptrCar->show(); // Ph•ơng thức của lớp Bus return; } Trang - 115 -
  29. Lập trình h•ớng đối t•ợng Khi thực hiện ch•ơng trình ta thu đ•ợc kết quả nh• sau: This is a Ford having a speed of 100km/h and its price is $3000 This is a bus of type Mercedes, on the line 27, having a speed of 150km/h and its price is $5000 Dòng thứ nhất là kết quả khi con trỏ ptrCar trỏ đến địa chỉ của đối t•ợng myCar, thuộc lớp Car nên sẽ gọi ph•ơng thức show() của lớp Car với các dữ liệu của đối t•ợng myCar: (100, Ford, 3000). Dòng thứ hai t•ơng ứng là kết quả khi con trỏ ptrCar trỏ đến địa chỉ của đối t•ợng myBus,thuộc lớp Bus nên sẽ gọi ph•ơng thức show() của lớp Bus, cùng với các tham số của đối t•ợng myBus: (150, Mercedes, 5000, 27). L•u ý: Trong tr•ờng hợp ở lớp dẫn xuất không định nghĩa lại ph•ơng thức ảo, thì ch•ơng trình sẽ gọi ph•ơng thức của lớp cơ sở, nh•ng với dữ liệu của lớp dẫn xuất. trong ch•ơng trình trên, lớp Bus không định nghĩa chồng ph•ơng thức ảo show() thì kết quả hiển thị sẽ là hai dòng thông báo giống nhau, chỉ khác nhau ở dữ liệu của hai đối t•ợng khác nhau: This is a Ford having a speed of 100km/h and its price is $3000 This is a Mercedes having a speed of 150km/h and its price is $5000 3.10.3. T•ơng ứng bội Một khi ph•ơng thức đ•ợc khai báo là ph•ơng thức ảo thì khi một con trỏ gọi đến ph•ơng thức đó, ch•ơng trình sẽ thực hiện ph•ơng thức t•ơng ứng với đối t•ợng mà con trỏ đang trỏ tới, thay vì thực hiện ph•ơng thức của lớp cùng kiểu với con trỏ. Đây đ•ợc gọi là hiện t•ợng đa hình (t•ơng ứng bội) trong C++. Có thể sử dụng t•ơng ứng bội để tổ chức thực hiện các thuật toán khác nhau trên cùng một bài toán nh• sau : Lớp cơ sở trừu t•ợng sẽ chứa chứa dữ liệu bài toán và một ph•ơng thức ảo Mỗi lớp dẫn xuất ứng với một thuật toán cụ thể. Sử dụng một mảng con trỏ của lớp cơ sở và gán cho mỗi phần tử mảng địa chỉ của một đối t•ợng của lớp dẫn xuất. Sau đó, dùng các phần tử mảng để gọi tới các ph•ơng thức ảo Xét bài toán tìm kiếm: Cho một dãy gồm n khoá K1, K2, , Kn với Ki ≠ Kj nếu i ≠ j đã đ•ợc sắp xếp theo thứ tự tăng dần. Viết ch•ơng trình thực hiện tìm Trang - 116 -
  30. Lập trình h•ớng đối t•ợng kiếm khoá có giá trị bằng X cho tr•ớc theo các ph•ơng pháp tìm kiếm tuần tự và tìm kiếm nhị phân. Nếu tìm thấy thì cho biết vị trí của khoá trong dãy ng•ợc lại trả về giá trị 0. Ví dụ 3.15: #include #include #include class search { protected: int *k; public: virtual int find(int *a,int n,int x) { k = a; return 0; } search() { k=NULL; } ~search() { delete k; } }; class sequential_search:public search { public: virtual int find(int *a, int n, int x) { int i; i=1; Trang - 117 -
  31. Lập trình h•ớng đối t•ợng while((i x) r=m-1; else if(a[m] >n; a = new int[n]; if(a==NULL) { cout<<"Loi cap phat bo nho"; Trang - 118 -
  32. Lập trình h•ớng đối t•ợng exit(1); } else for(int i=1;i >a[i]; } cout >x; cout find(a,n,x) find(a,n,x)<<" trong day"; getch(); } 3.10.4. Liên kết động Để quản lý một dãy các đối t•ợng của một lớp nào đó, ta có thể dùng cấu trúc mảng, tức là khai báo mảng đối t•ợng. Tuy nhiên, với cách tổ chức l•u trữ này có nh•ợc điểm là hiệu quả sử dụng bộ nhớ không cao (nh• chúng ta đã phân tích ở một số môn học tr•ớc, chẳng hạn môn Cấu trúc dữ liệu và giải thuật). Một giải pháp khác cho vấn đề này là sử dụng liên kết động. Tức là, sử dụng danh sách móc nối, có thể nối đơn hoặc nối kép. Tr•ớc hết ta định nghĩa lớp quản lý đối t•ợng, sau đó ta định nghĩa lớp (chẳng hạn tên lớp là node) tổ chức cấu trúc của một nút, đối với danh sách nối đơn, một nút sẽ gồm hai tr•ờng: tr•ờng info có Trang - 119 -
  33. Lập trình h•ớng đối t•ợng kiểu đối t•ợng, tr•ờng link là con trỏ đối t•ợng chứa địa chỉ nút tiếp theo trong danh sách. Đối với danh sách nối kép một nút có cấu trúc gồm 3 tr•ờng t•ơng ứng với 3 thuộc tính: lptr, rptr, info. Ví dụ 3.16: Sử dụng danh sách nối đơn quản lý các đối t•ợng của lớp ts- thí sinh. #include #include #include #include class ts { char sbd[5]; char ten[30]; unsigned int diem; public: friend ostream &operator >(istream &is,ts &t) { cin.ignore(1); cout >t.diem; is.ignore(); return is; Trang - 120 -
  34. Lập trình h•ớng đối t•ợng } friend fstream &operator >(fstream &fs,ts &t) { fs.getline(t.sbd,5); fs.getline(t.ten,30); fs>>t.diem; fs.ignore(); return fs; } char *getsbd(); char *getten() { return ten; } unsigned int getdiem() { return diem; } }; char *ts::getsbd() { return sbd; } class node { public: Trang - 121 -
  35. Lập trình h•ớng đối t•ợng ts info; node *link; }; class list { node *home; public: list() { home=NULL; } void create() { node *p, *end; ts x; char ans; cout >x; add(x); cin.ignore(1); cout >ans; } while((ans!='k')&&(ans!='K')); } void display() { node *p; p=home; while(p!=NULL) { cout info; Trang - 122 -
  36. Lập trình h•ớng đối t•ợng p=p->link; } } unsigned int count() { node *p; unsigned int dem=0; p=home; while(p!=NULL) { dem++; p=p->link; } return dem; } void add(ts x) { node *p, *end; p=new node; p->info=x; p->link=NULL; if(home==NULL) home=p; else { end=home; while(end->link!=NULL) end=end->link; end->link=p; } } void remove(node *p) { node *q; if(home==NULL) return; Trang - 123 -
  37. Lập trình h•ớng đối t•ợng else { if(p==home) if(home->link==NULL) home=NULL; else { home=home->link; p->link=NULL; } else { q=home; while(q->link!=p) q=q->link; q->link=p->link; } delete p; } } node *find_add(char *x) { node *p; p=home; while((p!=NULL)&&(strcmp((p->info).sbd.getsbd(),x)!=0) p=p->link; return p; } void find_sbd(char *x) { node *p; p=home; while((p!=NULL)&&(strcmp((p->info).getsbd(),x)!=0)) p=p->link; if(p!=NULL) cout info; else cout<<"Khong tim thay thi sinh co so bao danh tren"; } Trang - 124 -
  38. Lập trình h•ớng đối t•ợng void find_ten(char *x) { node *p; p=home; while((p!=NULL)&&(strcmp((p->info).getten(),x)!=0)) p=p->link; if(p!=NULL) cout info; else cout info).getdiem()>diemmax) diemmax=(p->info).getdiem(); p=p->link; } p=home; while(p!=NULL) { if((p->info).getdiem()==diemmax) cout info; p=p->link; } } void find_min() { node *p; unsigned int diemmin=65535; p=home; while(p!=NULL) { if((p->info).getdiem() info).getdiem(); Trang - 125 -
  39. Lập trình h•ớng đối t•ợng p=p->link; } p=home; while(p!=NULL) { if((p->info).getdiem()==diemmin) cout info; p=p->link; } } list xettuyen_diem(unsigned int diemchuan) { node *p; list kq; p=home; cout info).getdiem()>=diemchuan) kq.add(p->info); p=p->link; } return kq; } void sort_asc() { node *p,*q; ts tg; p=home; while(p->link!=NULL) { q=p->link; while(q!=NULL) { if((p->info).getdiem()>(q->info).getdiem()) Trang - 126 -
  40. Lập trình h•ớng đối t•ợng { tg= p->info; p->info=q->info; q->info=tg; } q=q->link; } p=p->link; } } void remove(char *x); }; void list::remove(char *x) { node *p,*q; p=home; while(p!=NULL) { q=p; while((q!=NULL)&&(strcmp((q->info).getsbd(),x)!=0)) q=q->link; if(q!=NULL) { p=q->link; remove(q); } else p=NULL; } } void main() { list l,tt,kq; ts x; unsigned int diemchuan,soluong; char bd[5],ht[30]; char lc; Trang - 127 -
  41. Lập trình h•ớng đối t•ợng int chon; do { clrscr(); cout >chon; switch (chon) { case 1: l.create(); break; case 2: cout<<"\nDanh sach thi sinh:\n"; l.display(); getch(); break; case 3: do { clrscr(); cout<<"\na. Tim theo so bao danh"; cout<<"\nb. Tim theo ten"; cout<<"\nc. Thi sinh co diem cao nhat"; cout<<"\nd. Thi sinh co diem thap nhat"; cout<<"\ne. Quay lai(nhan r):"; cout<<"\n Ban chon:"; Trang - 128 -
  42. Lập trình h•ớng đối t•ợng cin>>lc; switch(lc) { case 'a': cin.ignore(1); cout >x; l.add(x); break; case 5: cout<<"Nhap so bao dand cua thi sinh can loai bo\n"; cin.ignore(1); Trang - 129 -
  43. Lập trình h•ớng đối t•ợng cin>>bd; l.remove(bd); break; case 6: l.sort_asc(); break; case 7: cout >diemchuan; kq=l.xettuyen_diem(diemchuan); kq.display(); getch(); } } while (chon!=8); } 3.10.5. Quy tắc gán địa chỉ đối t•ợng cho con trỏ lớp cơ sở Con trỏ đối t•ợng của lớp cơ sở không những có thể chứa địa chỉ của đối t•ợng thuộc lớp đó, nó còn có thể chứa địa chỉ của đối t•ợng của lớp dẫn xuất. Trong ví dụ 3.15, các con trỏ se[0], se[1] không những có thể chứa địa chỉ đối t•ợng của lớp search (hiển nhiên), còn có thể chứa địa chỉ đối t•ợng của lớp dẫn xuất sequential_search đ•ợc thể hiện qua các câu lệnh: search *se[2];//khai báo mảng con trỏ lớp cơ sở search sequential_search s_search;//khai báo đối t•ợng của lớp sequential_ search se[0] = &s_search;//gán địa chỉ cho con trỏ lớp cơ sở và có thể chứa địa chỉ đối t•ợng lớp dẫn xuất binary_search, thể hiện qua các câu lệnh: search *se[2];//khai báo mảng con trỏ lớp cơ sở search binary_search b_search;//khai báo đối t•ợng của lớp dẫn xuất binary_search se[1] = &b_search;//gán địa chỉ cho con trỏ lớp cơ sở 3.11. Lớp cơ sở trừu t•ợng Một lớp cơ sở trừu t•ợng là một lớp chỉ đ•ợc dùng làm cơ sở cho các lớp khác. Không có đối t•ợng nào của lớp cơ sở trừu t•ợng đ•ợc tạo ra cả, bởi vì nó chỉ Trang - 130 -
  44. Lập trình h•ớng đối t•ợng đ•ợc dùng để định nghĩa một số khái niệm tổng quát, chung cho các lớp khác. Chẳng hạn xét sơ đồ thừa kế sau Engine Car Public Transports Sơ đồ 3.3 Trong sơ đồ này lớp Engine là lớp cơ sở trừu tr•ợng đ•ợc dùng để xây dựng lớp Car và lớp PublicTransport. Lớp cơ sở trừu t•ợng đặc biệt áp dụng cho các lớp có các ph•ơng thức ảo thuần tuý. Ph•ơng thức ảo thuần tuý là ph•ơng thức ảo mà nội dung không có gì. Cách định nghĩa ph•ơng thức ảo thuần tuý nh• sau : virtual void tên_ph•ơng_thức() = 0 ; Ví dụ 3.17 : #include #include #include /* Định nghĩa lớp Engine */ class Engine { int power; // Công suất public: Engine(){power = 0;}; Engine(int pIn){power = pIn;}; virtual void show()=0; // ph•ơng thức ảo thuần tuý float getPower(){return power;}; }; /* Định nghĩa lớp Car dẫn xuất từ lớp cơ sở trừu t•ợng Engine*/ class Car: public Engine { int speed; // Tốc độ Trang - 131 -
  45. Lập trình h•ớng đối t•ợng char mark[20]; // Nhãn hiệu float price; // Giá xe public: Car(); Car(int, int, char[], float); void show(); // Giới thiệu float getSpeed(){return speed;}; char *getMark(){return mark;}; float getPrice(){return price;}; }; Car::Car(): Engine()// Khởi tạo không tham số { speed = 0; strcpy(mark,"Oto"); price = 0; } Car::Car(int pwIn, int sIn, char mIn[], float prIn): Engine(pwIn) { speed = sIn; strcpy(mark, mIn); price = prIn; } void Car::show() { cout<<"This is a:" << mark; cout<<"\nHaving a speed of:" <<speed<<"km/h"; cout<<"\nIts power is:"<<getPower()<<"KWh"; cout<<"\nAnd Price is:" <<price <<"$"<<endl; return; } class PublicTransport: public Engine { float ticket; Trang - 132 -
  46. Lập trình h•ớng đối t•ợng public: PublicTransport(); PublicTransport(int, float); void show(); float getTicket(){return ticket;}; }; PublicTransport::PublicTransport(): Engine() { ticket = 0; } PublicTransport::PublicTransport(int pwIn, float tIn): Engine(pwIn) { ticket = tIn; } void PublicTransport::show() { cout<<"This is a public transport having a ticket of:"<<ticket<<"$"; cout<<"\nAnd its power is:"<< getPower()<< "KWh" << endl; return; } void main() { clrscr(); Car myCar(250, 100, "Mercedes", 1.5); myCar.show(); PublicTransport myTrans(250, 0.2); myTrans.show(); getch(); } Ch•ơng trình sẽ in ra thông báo nh• sau: This is a: Mercedes Having a speed of: 100km/h Its power is: 250KWh Trang - 133 -
  47. Lập trình h•ớng đối t•ợng And price is: 1.5$ This is a public transport having a ticket of: 0.2$ And its power is: 250KWh Trang - 134 -
  48. Lập trình h•ớng đối t•ợng Câu hỏi và bài tập 1. Trong các khai báo sau, khai báo nào là đúng cú pháp khai báo lớp dẫn xuất: a. class A: public class B{ }; b. class A: public B{ }; c. class A: class B{ }; d. class A:: public B{ }; 2. Trong các kiểu dẫn xuất sau, kiểu dẫn xuất nào từ các ph•ơng thức lớp dẫn xuất, không thể truy nhập đến các thành phần private của lớp cơ sở: a. private b. protected c. public d. Cả ba kiểu trên 3. Trong các kiểu dẫn xuất sau, kiểu dẫn xuất nào từ đối t•ợng của lớp dẫn xuất, có thể truy nhập đến các thành phần của lớp cơ sở: a. private b. protected c. public d. Cả ba kiểu trên 4. A là lớp dẫn xuất public từ lớp cơ sở B. Giả sử có các kiểu khai báo: A myA, *ptrA; B myB, *ptrB; khi đó, các lệnh nào sau đây là không có lỗi: a. myA = myB; b. myB = myA; c. ptrA = &myB; d. ptrB = &myA; e. ptrA = ptrB; f. ptrB = ptrA; 5. A là lớp dẫn xuất public từ lớp cơ sở B. Giả sử có các kiểu khai báo và nguyên mẫu hàm: A myA; B myB; Trang - 135 -
  49. Lập trình h•ớng đối t•ợng void show(A, B); khi đó, các lời gọi nào sau đây là không có lỗi: a. show(myA, myA); b. show(myA, myB); c. show(myB, myA); d. show(myB, myB); 6. A là lớp dẫn xuất public từ lớp cơ sở B. Giả sử B có một hàm tạo: B(int, float); khi đó, khai báo hàm tạo nào sau đây của lớp A là chấp nhận đ•ợc: a. A::A(){ }; b. A::A(): B(){ }; c. A::A(int x, float y): B(){ }; d. A::A(int x, float y): B(x, y){ }; 7. A là lớp dẫn xuất public từ lớp cơ sở B. Giả sử B có hai hàm tạo: B(); B(int, float); khi đó, những khai báo hàm tạo nào sau đây của lớp A là chấp nhận đ•ợc: a. A::A(){ }; b. A::A(): B(){ }; c. A::A(int x, float y): B(){ }; d. A::A(int x, float y): B(x, y){ }; 8. A là lớp dẫn xuất public từ lớp cơ sở B. Giả sử B có hàm huỷ t•ờng minh: ~B(); khi đó, những khai báo hàm huỷ nào sau đây của lớp A là chấp nhận đ•ợc: a. A::~A(){ }; b. A::~A(): ~B(){ }; c. A::~A(int x){ }; d. A::~A(int x): ~B(){ }; 9. Giả sử B là một lớp đ•ợc khai báo: class B { int x; public: int getx(); Trang - 136 -
  50. Lập trình h•ớng đối t•ợng }; và A là một lớp dẫn xuất từ lớp B theo kiểu private: class A: private B { }; khi đó, nếu myA là một đối t•ợng lớp A, lệnh nào sau đây là chấp chận đ•ợc: a. myA.x; b. myA.getx(); c. Cả hai lệnh trên. d. Không lệnh nào cả. 10. Giả sử B là một lớp đ•ợc khai báo: class B { int x; public: int getx(); }; và A là một lớp dẫn xuất từ lớp B theo kiểu protected: class A: protected B { }; khi đó, nếu myA là một đối t•ợng lớp A, lệnh nào sau đây là chấp chận đ•ợc: a. myA.x; b. myA.getx(); c. Cả hai lệnh trên. d. Không lệnh nào cả. 11. Giả sử B là một lớp đ•ợc khai báo: class B { int x; public: int getx(); }; và A là một lớp dẫn xuất từ lớp B theo kiểu public: class A: public B Trang - 137 -
  51. Lập trình h•ớng đối t•ợng { }; khi đó, nếu myA là một đối t•ợng lớp A, lệnh nào sau đây là chấp chận đ•ợc: a. myA.x; b. myA.getx(); c. Cả hai lệnh trên. d. Không lệnh nào cả. 12. Giả sử B là một lớp đ•ợc khai báo: class B { public: void show(); }; và A là một lớp dẫn xuất từ lớp B theo kiểu public, có định nghĩa chồng hàm show(): class A: public B { public: void show(); }; khi đó, nếu myA là một đối t•ợng lớp A, muốn thực hiện ph•ơng thức show() của lớp B thì lệnh nào sau đây là chấp chận đ•ợc: a. myA.show(); b. myA.B::show(); c. B::myA.show(); d. A::B::show(); 13. Muốn khai báo một lớp A thừa kế từ hai lớp cơ sở B và C, những lệnh nào là đúng: a. class A: B, C{ }; b. class A: public B, C{ }; c. class A: public B, protected C{ }; d. class A: public B, public C{ }; 14. B là một lớp có hai hàm tạo: B(); B(int); Trang - 138 -
  52. Lập trình h•ớng đối t•ợng C cũng là một lớp có hai hàm khởi tạo: C(); C(int, int); và A là một lớp thừa kế từ B và C: class A: public B, public C{ }; khi đó, hàm tạo nào sau đây của lớp A là chấp nhận đ•ợc: a. A::A(){ }; b. A::A():B(),C(){ }; c. A::A(int x, int y): C(x, y){ }; d. A::A(int x, int y, int z): B(x), C(y, z){ }; e. A::A(int x, int y, int z): C(x, y), B(z){ }; 15. Muốn khai báo lớp A thừa kế từ lớp cơ sở ảo B, những khai báo nào sau đây là đúng: a. virtual class A: public B{ }; b. class virtual A: public B{ }; c. class A: virtual public B{ }; d. class A: public virtual B{ }; 16. Lớp A là một lớp dẫn xuất, đ•ợc thừa kế từ lớp cơ sở B. Hai lớp này đều định nghĩa ph•ơng thức show(). Muốn ph•ơng thức này trở thành ph•ơng thức ảo thì những khai báo nào sau đây là đúng: a. void A::show(){ } và void B::show(){ } b. virtual void A::show(){ } và void B::show(){ } c. void A::show(){ } và virtual void B::show(){ } d. virtual void A::show(){ } và virtual void B::show(){ } 17. Khai báo lớp ng•ời (Human) bao gồm các thuộc tính sau: Tên ng•ời (name) Tuổi của ng•ời đó (age) Giới tính của ng•ời đó (sex) Sau đó khai báo lớp Cá nhân (Person) thừa kế từ lớp Human vừa đ•ợc định nghĩa ở trên. 18. Bổ sung các ph•ơng thức truy nhập các thuộc tính của lớp Human, các ph•ơng thức này có kiểu public. Trang - 139 -
  53. Lập trình h•ớng đối t•ợng 19. Bổ sung thêm các thuộc tính của lớp Person: địa chỉ và số điện thoại. Thêm các ph•ơng thức truy nhập các thuộc tính này trong lớp Person. 20. Xây dựng hai hàm tạo cho lớp Human: một hàm không tham số, một hàm với đủ ba tham số t•ơng ứng với ba thuộc tính của nó. Sau đó, xây dựng hai hàm tạo cho lớp Person có sử dụng các hàm tạo của lớp Human: một hàm không tham số, một hàm đủ năm tham số (ứng với hai thuộc tính của lớp Person và ba thuộc tính của lớp Human). 21. Xây dựng một hàm main, trong đó có yêu cầu nhập các thuộc tính để tạo một đối t•ợng có kiểu Human và một đối t•ợng có kiểu Person, thông qua các hàm set thuộc tính đã xây dựng. 22. Xây dựng ph•ơng thức show() cho hai lớp Human và Person. Thay đổi hàm main: dùng một đối t•ợng có kiểu lớp Person, gọi hàm show() của lớp Person, sau đó lại gọi ph•ơng thức show() của lớp Human từ chính đối t•ợng đó. 23. Khai báo thêm một lớp ng•ời lao động (Worker), thừa kế từ lớp Human, có thêm thuộc tính là số giờ làm việc trong một tháng (hour) và tiền l•ơng của ng•ời đó (salary). Sau đó, khai báo thêm một lớp nhân viên (Employee) thừa kế đồng thời từ hai lớp: Person và Worker. Lớp Employee có bổ sung thêm một thuộc tính là chức vụ (position). 24. Chuyển lớp Human thành lớp cơ sở trừu t•ợng của hai lớp Person và Worker. Xây dựng thêm hai ph•ơng thức show() của lớp Worker và lớp Employee. Trong hàm main, khai báo một đối t•ợng lớp Employee, sau đó gọi đến cácph•ơng thức show() của các lớp Employee, Person, Worrker và Human. 25. Chuyển ph•ơng thức show() trong các lớp trên thành ph•ơng thức ảo. Trong hàm main, khai báo một con trỏ kiểu Human, sau đó, cho nó trỏ đến lần l•ợt các đối t•ợng của các lớp Human, Person, Worker và Employee, mỗi lần đều gọi ph•ơng thức show() để hiển thị thông báo ra màn hình. Trang - 140 -
  54. Lập trình h•ớng đối t•ợng Ch•ơng 4: Khuôn hình Nội dung của ch•ơng tập trung trình bày các vấn đề sau: Khuôn hình hàm: Khái niệm, khai báo khuôn hình hàm, khai báo chồng khuôn hình hàm, các tham số trong khuôn hình hàm, sử dụng khuôn hình hàm. Khuôn hình lớp: Khái niệm, khai báo khuôn hình lớp, các tham số trong khuôn hình lớp, sử dụng khuôn hình lớp. 4.1. Khuôn hình hàm 4.1.1. Khái niệm khuôn hình hàm Ta đã biết định nghĩa chồng hàm cho phép dùng một tên duy nhất cho nhiều hàm thực hiện các công việc khác nhau. Khái niệm khuôn hình hàm cũng cho phép sử dụng cùng một tên duy nhất để thực hiện các công việc khác nhau, tuy nhiên so với định nghĩa chồng hàm, nó có phần mạnh hơn và chặt chẽ hơn; mạnh hơn vì chỉ cần viết định nghĩa khuôn hình hàm một lần, rồi sau đó ch•ơng trình biên dịch làm cho nó thích ứng với các kiểu dữ liệu khác nhau; chặt chẽ hơn bởi vì dựa theo khuôn hình hàm, tất cả các hàm thể hiện đ•ợc sinh ra bởi trình biên dịch sẽ t•ơng ứng với cùng một định nghĩa và nh• vậy sẽ có cùng một giải thuật. 4.1.2. Tạo một khuôn hình hàm Giả thiết rằng ta cần viết một hàm min đ•a ra giá trị nhỏ nhất trong hai giá trị có cùng kiểu. Ta có thể viết một định nghĩa nh• thế đối với kiểu int nh• sau: int min (int a, int b) { if (a < b) return a; else return b; } Giả sử, ta lại phải viết định nghĩa hàm min() cho kiểu float float min(float a, float b) { if (a < b) return a; else b; } Trang - 141 -
  55. Lập trình h•ớng đối t•ợng Nếu tiếp tục nh• vậy, sẽ có khuynh h•ớng phải viết rất nhiều định nghĩa hàm hoàn toàn t•ơng tự nhau cho các kiểu double, char, char *, C++ cho phép giải quyết đơn giản vấn đề trên bằng cách định nghĩa một khuôn hình hàm duy nhất theo cách nh• sau: Ví dụ 4.1: #include //tạo một khuôn hình hàm template T min(T a, T b) { if (a T min (T a, T b) trong đó, template xác định rằng đó là một khuôn hình với một tham số kiểu T; Phần còn lại T min(T a, T b) nói rằng, min() là một hàm với hai tham số hình thức kiểu T và có giá trị trả về cũng là kiểu T. 4.1.3. Sử dụng khuôn hình hàm Khuôn hình hàm cho kiểu dữ liệu cơ sở Để sử dụng khuôn hình hàm min() vừa tạo ra, chỉ cần sử dụng hàm min() trong những điều kiện phù hợp (ở đây có nghĩa là hai tham số của hàm có cùng kiểu dữ liệu). Nh• vậy, nếu trong một ch•ơng trình có hai tham số nguyên n và p, với lời gọi min(n,p) ch•ơng trình biên dịch sẽ tự động sản sinh ra hàm min() (ta gọi là một hàm thể hiện) t•ơng ứng với hai tham số kiểu nguyên int. Nếu chúng ta gọi min() với hai tham số kiểu float, ch•ơng trình biên dịch cũng sẽ tự động sản sinh một hàm thể hiện min khác t•ơng ứng với các tham số kiểu float và cứ thế Sau đây là một ví dụ hoàn chỉnh: Trang - 142 -
  56. Lập trình h•ớng đối t•ợng Ví dụ 4.2: #include #include //tạo một khuôn hình hàm template T min(T a, T b) { if ( a < b) return a; else return b; } //ví dụ sử dụng khuôn hình hàm min void main() { clrscr(); int n = 41, p = 12; float x = 12.5, y= 3.25; cout<<"min (n, p) = "<<min (n, p)<<"\n";//int min(int, int) cout<<"min (x, y) = "<<min (x, y)<<"\n";//float min(float, float) getch(); } chạy ch•ơng trình ta thu đ•ợc kết quả: min(n, p) = 12 min(x, y) = 3.25 Nếu muốn sử dụng khuôn hình hàm min cho kiểu char *, ta viết lại hàm main nh• sau: void main() { clrscr(); char * adr1 = "DHBK"; char * adr2 = "CDSD"; cout << "min (adr1, adr2) ="<<min (adr1, adr2); getch(); } chạy ch•ơng trình ta thu đ•ợc kết quả: Trang - 143 -
  57. Lập trình h•ớng đối t•ợng min (adr1, adr2) = DHBK Ta hy vọng hàm min() trả về xâu "CDSD". Thực tế, với biểu thức min(adr1, adr2), ch•ơng trình biên dịch đã sinh ra hàm thể hiện sau đây: char * min(char * a, char * b) { if (a #include template T min( T a, T b) { if (a < b) return a; else return b; } class phanso { int ts,ms; public: phanso(int t = 0, int m = 1) { ts = t, ms = m;} void display() { cout <<ts<<"/"<<ms<<"\n"; } friend int operator < (phanso , phanso); }; int operator < (phanso a, phanso b) { Trang - 144 -
  58. Lập trình h•ớng đối t•ợng if (a.ts*b.ms int min(T a, U b, U c) { } Các tham số này có thể để ở bất kỳ đâu trong định nghĩa của khuôn hình hàm, nghĩa là: Trong dòng tiêu đề, trong các khai báo các biến cục bộ, trong các chỉ thị thực hiện. Trong mọi tr•ờng hợp, mỗi tham số kiểu phải xuất hiện ít nhất một lần trong khai báo danh sách các tham số hình thức của khuôn hình hàm. Khuôn hình hàm sau đây thực hiện trao đổi nội dung của hai biến. C++ quy định phải có một sự t•ơng ứng chính xác giữa kiểu của tham số hình thức và kiểu tham số thực sự đ•ợc truyền cho hàm. Ví dụ 4.4: Xây dựng khuôn hình hàm min, tìm giá trị nhỏ nhất của 3 đối a, b, c. Trong đó b, c là hai đối có cùng kiểu, hàm trả về một giá trị kiểu int. #include #include template int min(T a, U b, U c) Trang - 145 -
  59. Lập trình h•ớng đối t•ợng { int m = int(a) ; if (m > b) m = int(b) ; if (m > c) m = int(c); } void main() { clrscr(); int n= 10, p = 2, q = 3; float x =2.5, y = 5.0; cout T min(T a, T b) { if (a < b) return a; else return b; } Với các khai báo: int n; char c; Câu hỏi đặt ra là: ch•ơng trình dịch sẽ làm gì khi gặp lời gọi kiểu nh• là min(n,c)? Câu trả lời dựa trên hai nguyên tắc sau đây: C++ quy định phải có một sự t•ơng ứng chính xác giữa kiểu của tham số hình thức và kiểu tham số thực sự đ•ợc truyền cho hàm, tức là ta chỉ có thể sử dụng khuôn hình hàm min() trong các lời gọi với hai tham số có cùng kiểu. Lời gọi min(n, c) không đ•ợc chấp nhận và sẽ gây ra lỗi biên dịch. Trang - 146 -
  60. Lập trình h•ớng đối t•ợng C++ thậm chí còn không cho phép các chuyển kiểu thông th•ờng nh• là: T thành const T hay T[] thành T*, những tr•ờng hợp hoàn toàn đ•ợc phép trong định nghĩa chồng hàm. Giả sử ta khai báo các biến sau: int n; char c; unsigned int q; const int r = 10; int t[10]; int *adi; Khi đó các lời gọi hàm min sau đều không hợp lệ do không có sự t•ơng ứng chính xác về kiểu giữa tham số hình thức và tham số thực sự của hàm min min (n, c); //lỗi min (n, q); //lỗi min (n, r);//lỗi min (t, adi); //lỗi 4.1.6. Khởi tạo các biến có kiểu dữ liệu chuẩn Trong khuôn hình hàm, tham số kiểu có thể t•ơng ứng với một tham số thực sự có kiểu dữ liệu chuẩn, cũng có thể t•ơng ứng với tham số thực sự là một đối t•ợng có kiểu dữ liệu lớp nào đó. Khi tham số kiểu t•ơng ứng với một đối t•ợng ta cần phải khai báo bên trong khuôn hình hàm một đối t•ợng và truyền một hay nhiều tham số cho hàm thiết lập của lớp. Ví dụ 4.5: template void fct(T a) { T x(3); } Khi sử dụng hàm fct() cho một kiểu dữ liệu lớp, mọi việc đều tốt đẹp. Ng•ợc lại, nếu chúng ta cố gắng áp dụng cho một kiểu dữ liệu chuẩn, chẳng hạn nh• int, khi đó ch•ơng trình dịch sản sinh ra hàm sau đây: void fct(int a) { int x(3); } Trang - 147 -
  61. Lập trình h•ớng đối t•ợng Để cho chỉ thị int x(3); không gây ra lỗi, C++ đã ngầm hiểu câu lệnh đó nh• là phép khởi tạo biến x với giá trị 3, nghĩa là: int x = 3; Một cách t•ơng tự: double x(3.5); char c('e'); 4.1.7. Các hạn chế của khuôn hình hàm Về nguyên tắc, khi định nghĩa một khuôn hình hàm, một tham số kiểu có thể t•ơng ứng với bất kỳ kiểu dữ liệu nào, cho dù đó là một kiểu chuẩn hay một kiểu lớp do ng•ời dùng định nghĩa. Do vậy, không thể hạn chế việc thể hiện đối với một số kiểu dữ liệu cụ thể nào đó. Chẳng hạn, nếu một khuôn hình hàm có dòng đầu tiên: template void fct(T) chúng ta có thể gọi fct() với một tham số có kiểu bất kỳ: int, float, int *,int , t * (t là một kiểu dữ liệu nào đấy). Tuy nhiên, chính định nghĩa bên trong khuôn hình hàm lại chứa một số yếu tố có thể làm cho việc sản sinh hàm thể hiện không đúng nh• mong muốn. Ta gọi đó là các hạn chế của các khuôn hình hàm. Đầu tiên, chúng ta có thể cho rằng một tham số kiểu có thể t•ơng ứng với một con trỏ. Do đó, với dòng tiêu đề: template void fct(T *) ta chỉ có thể gọi fct() với một con trỏ đến một kiểu nào đó: int*, int , t *, t . Trong các tr•ờng hợp khác, sẽ gây ra các lỗi biên dịch. Ngoài ra, trong định nghĩa của một khuôn hình hàm, có thể có các câu lệnh không thích hợp đối với một số kiểu dữ liệu nhất định. Chẳng hạn, khuôn hình hàm: template T min(T a, T b) { if (a void fct(T) { Trang - 148 -
  62. Lập trình h•ớng đối t•ợng T x(2, 5); /*đối t•ợng cục bộ đ•ợc khởi tạo bằng một hàm tạo với hai tham số*/ } không thể áp dụng cho các kiểu dữ liệu lớp không có hàm tạo với hai tham số. Tóm lại, mặc dù không tồn tại một cơ chế hình thức để hạn chế khả năng áp dụng của các khuôn hình hàm, nh•ng bên trong mỗi một khuôn hình hàm đều có chứa những nhân tố để ng•ời ta có thể biết đ•ợc khuôn hình hàm đó có thể đ•ợc áp dụng đến mức nào. 4.1.8. Các tham số biểu thức của một khuôn hình hàm Trong khai báo của một khuôn hình hàm có thể khai báo các tham số hình thức với kiểu xác định. Ta gọi chúng là các tham số biểu thức. Ch•ơng trình sau đây khai báo một khuôn hình hàm cho phép đếm số l•ợng các phần tử nul (0 đối với các giá trị số hoặc NULL nếu là con trỏ) trong một mảng với kiểu bất kỳ và với kích th•ớc nào đó: Ví dụ 4.6: #include #include template int compte(T * tab, int n) { int i, nz = 0; for (i=0; i<n; i++) if (!tab[i]) nz++; return nz; } void main() { clrscr(); int t[5] = {5, 2, 0, 2, 0}; char c[6] = { 0, 12, 0, 0, 0}; cout<<" compte (t) = "<<compte(t, 5)<<"\n"; cout<<" compte (c) = "<<compte(c,6)<<"\n"; getch(); Trang - 149 -
  63. Lập trình h•ớng đối t•ợng } chạy ch•ơng trình ta thu đ•ợc kết quả : compte (t) = 2 compte (c) = 4 Ta có thể nói rằng khuôn hình hàm compte định nghĩa một họ các hàm compte trong đó kiểu của tham số đầu tiên là tuỳ ý (đ•ợc xác định bởi lời gọi), còn kiểu của tham số thứ hai đã xác định (kiểu int). 4.1.9. Định nghĩa chồng các khuôn hình hàm Giống nh• việc định nghĩa chồng các hàm thông th•ờng, C++ cho phép định nghĩa chồng các khuôn hình hàm, tức là có thể định nghĩa một hay nhiều khuôn hình hàm có cùng tên nh•ng với các tham số khác nhau. Điều đó sẽ tạo ra nhiều họ các hàm (mỗi khuôn hình hàm t•ơng ứng với một họ các hàm). Ví dụ có ba họ hàm min: Họ thứ nhất bao gồm các hàm tìm giá trị nhỏ nhất trong hai giá trị, Họ thứ hai tìm số nhỏ nhất trong ba số, Họ thứ ba tìm số nhỏ nhất trong một mảng. Ví dụ 4.7: #include #include //khuôn hình 1 template T min(T a, T b) { if (a T min(T a, T b, T c) { return min (min (a, b), c); } //khuôn hình 3 template T min (T *t, int n) Trang - 150 -
  64. Lập trình h•ớng đối t•ợng { T res = t[0]; for(int i = 1; i t[i]) res = t[i]; return res; } void main() { clrscr(); int n = 12, p = 15, q = 2; float x = 3.5, y = 4.25, z = 0.25; int t[6] = {2, 3, 4,-1, 21}; char c[4] = {'w', 'q', 'a', 'Q'}; cout<<“min(n,p) = ”<<min(n,p)<<"\n"; cout<<“min(n,p,q)=”<<min(n,p,q)<<"\n"; cout<<“min(x,y) = ”<<min(x,y)<<"\n"; cout<<“min(x,y,z) = ”<<min(x,y,z)<<"\n"; cout<<“min(t,6) = ”<<min(t,6)<<"\n"; cout<<“min(c,4) = ”<<min(c,4)<<"\n"; getch(); } Chạy ch•ơng trình ta thu đ•ợc kết quả: min(n,p) = 12 min(n,p,q) = 2 min(x,y) = 3.5 min(x,y,z) = 0.25 min(t,6) = -1 min(c,4) = Q Nhận xét Cũng giống nh• định nghĩa chồng các hàm, việc định nghĩa chồng các khuôn hình hàm có thể gây ra sự nhập nhằng trong việc sản sinh các hàm thể hiện. Chẳng hạn với bốn họ hàm sau đây: Trang - 151 -
  65. Lập trình h•ớng đối t•ợng template T fct(T, T) { } template T fct(T *, T) { } template T fct(T, T*) { } template T fct(T *, T*) { } xét các câu lệnh: int x; int y; và lời gọi fct(&x, &y) có thể t•ơng ứng với khuôn hình hàm 1 hay khuôn hình hàm 4. Ta có thể định nghĩa một hay nhiều khuôn hình cùng tên, mỗi khuôn hình có các tham số kiểu cũng nh• là các tham số biểu thức riêng. Hơn nữa, có thể cung cấp các hàm thông th•ờng với cùng tên với một khuôn hình hàm; trong tr•ờng hợp này ta nói đó là sự cụ thể hoá một hàm thể hiện. Trong tr•ờng hợp tổng quát khi có đồng thời cả hàm định nghĩa chồng và khuôn hình hàm, ch•ơng trình dịch lựa chọn hàm t•ơng ứng với một lời gọi hàm dựa trên các nguyên tắc sau đây: Đầu tiên, kiểm tra tất cả các hàm thông th•ờng cùng tên và chú ý đến sự t•ơng ứng chính xác; nếu chỉ có một hàm phù hợp, hàm đó đ•ợc chọn; còn nếu có nhiều hàm cùng thoả mãn (có sự nhập nhằng) sẽ tạo ra một lỗi biên dịch và quá trình tìm kiếm bị gián đoạn. Nếu không có hàm thông th•ờng nào t•ơng ứng chính xác với lời gọi, khi đó ta kiểm tra tất cả các khuôn hình hàm có cùng tên với lời gọi; nếu chỉ có một t•ơng ứng chính xác đ•ợc tìm thấy, hàm thể hiện t•ơng ứng đ•ợc sản sinh và vấn đề đ•ợc giải quyết; còn nếu có nhiều hơn một khuôn hình hàm( có sự nhập nhằng) điều đó sẽ gây ra lỗi biên dịch và quá trình tìm kiếm bị ngắt. 4.2. Khuôn hình lớp 4.2.1. Khái niệm khuôn hình lớp Bên cạnh khái niệm khuôn hình hàm, C++ còn cho phép định nghĩa khuôn hình lớp. Cũng giống nh• khuôn hình hàm, ở đây ta chỉ cần viết định nghĩa các Trang - 152 -
  66. Lập trình h•ớng đối t•ợng khuôn hình lớp một lần rồi sau đó có thể áp dụng chúng với các kiểu dữ liệu khác nhau để đ•ợc các lớp thể hiện khác nhau. 4.2.2. Tạo một khuôn hình lớp Xét ví dụ 4.8: #include #include class stack { private: unsigned int max; unsigned int top; int *a; public: stack(); ~stack(); void push(int x); int full(); int empty(); int pop(); }; stack::stack() { top=0; cout >max; a = new int[max]; } stack::~stack() { top = 0; max = 0; delete a; } int stack::empty() Trang - 153 -
  67. Lập trình h•ớng đối t•ợng { if(top =max) return 1; else return 0; } void stack::push(int x) { if(full()) { cout<<"stack day"; return; } top=top+1; a[top]=x; } int stack::pop() { if(empty()) { cout<<"Stack rong"; return 0; } top=top-1; return a[top+1]; } void main() { stack s; int n; Trang - 154 -
  68. Lập trình h•ớng đối t•ợng cout >n; while(n>0) { s.push(n % 2); n = n / 2; } while(!s.empty()) cout class tên_lớp { //khai báo các thuộc tính có kiểu: T1, T2, , Tn //khai báo các ph•ơng thức của lớp }; Cũng giống nh• các khuôn hình hàm, cú pháp template xác định rằng đó là một khuôn hình trong đó có các tham số kiểu T1, T2, , Tn . Khi các ph•ơng thức đ•ợc khai bên trong định nghĩa ta sẽ viết nh• bình th•ơng, còn đối với các ph•ơng thức viết ngoài định nghĩa lớp ta phải nhắc lại các tham số kiểu của khuôn hình lớp, có nghĩa là phải nhắc lại template <class Trang - 155 -
  69. Lập trình h•ớng đối t•ợng T1, class T2, , class Tn>tr•ớc định nghĩa hàm, còn tên của khuôn hình lớp đ•ợc viết nh• là tên_lớp Bây giờ ta sẽ xây dựng khuôn hình lớp stack để có thể làm việc với các kiểu phần tử khác nhau (Trong một stack thì các phần tử có cùng một kiểu dữ liệu) Ví dụ 4.9: #include #include template class stack { private: unsigned int max; unsigned int top; t *a; public: stack(); ~stack(); void push(t x); int full(); int empty(); t pop(); }; template stack ::stack() { top=0; cout >max; a = new t[max]; } template stack ::~stack() { top=0; max=0; delete a; Trang - 156 -
  70. Lập trình h•ớng đối t•ợng } template int stack ::empty() { if(top int stack ::full() { if(top>=max) return 1; else return 0; } template void stack ::push(t x) { if(full()) { cout t stack ::pop() { if(empty()) { cout<<"Stack rong"; return 0; } top=top-1; return a[top+1]; } void main() { Trang - 157 -
  71. Lập trình h•ớng đối t•ợng stack s;//sử dụng khuôn hình lớp stack int n; cout >n; while(n>0) { s.push(n % 2); n = n / 2; } while(!s.empty()) cout Tên_đối_t•ợng; ứng với mỗi kiểu dữ liệu cụ thể của một tham số kiểu cho ta một thể hiện của khuôn hình lớp. Ví dụ ta có thể có các kiểu thể hiện khác nhau của khuôn hình lớp stack khi khai báo các đối t•ợng nh• sau: stack s1; stack s2; stack s3; 4.2.4. Các tham số trong khuôn hình lớp Hoμ n toμ n giống nh− khuôn hình hμ m, các khuôn hình lớp có thể có các tham số kiểu vμ tham số biểu thức. Tuy có nhiều điểm giống nhau giữa khuôn hình hμ m vμ khuôn hình lớp, nh− ng các rμ ng buộc đối với các kiểu tham số lại không nh− nhau. Xét ví dụ 4.10: template //danh sách ba tham số kiểu class try { T x; Trang - 158 -
  72. Lập trình h•ớng đối t•ợng U t[5]; V fm1 (int, U); }; Để sản sinh ra một lớp thể hiện ta liệt kê đằng sau tên khuôn hình lớp các tham số thực (lμ tên các kiểu dữ liệu) với số l− ợng bằng với số các tham số trong danh sách (template ) của khuôn hình lớp. Ví dụ để sinh ra các lớp thể hiện của khuôn hình lớp try ta viết nh• sau: try // lớp thể hiện với ba tham số int, float, int hoặc try // lớp thể hiện với ba tham số int, int *, double hay try // lớp thể hiện với ba tham số char *, int, obj Trong dòng cuối ta cuối giả định obj lμ một kiểu dữ liệu đã đ− ợc định nghĩa tr− ớc đó. Thậm chí có thể sử dụng các lớp thể hiện để lμ m tham số thực cho các lớp thể hiện khác, chẳng hạn: try , double> try ,point , char *> Ta thấy rằng, vấn đề t− ơng ứng chính xác đ− ợc nói tới trong các khuôn hình hμ m không còn hiệu lực với các khuôn hình lớp. Với các khuôn hình hμ m, việc sản sinh một thể hiện không chỉ dựa vμ o danh sách các tham số có trong template mμ còn dựa vμ o danh sách các tham số hình thức trong tiêu đề của hμ m. Một tham số hình thức của một khuôn hình hμ m có thể có kiểu, lμ một lớp thể hiện nμ o đó, chẳng hạn: template void fct(point ) { } Việc khởi tạo mới các kiểu dữ liệu mới vẫn áp dụng đ− ợc trong các khuôn hình lớp. Một khuôn hình lớp có thể có các thμ nh phần(dữ liệu hoặc hμ m) static. Trong tr− ờng hợp nμ y, cần phải biết rằng, mỗi thể hiện của lớp có một tập hợp các thμ nh phần static của riêng mình: Trang - 159 -
  73. Lập trình h•ớng đối t•ợng 4.2.5. Các tham số biểu thức trong khuôn hình lớp Một khuôn hình lớp có thể chứa các tham số biểu thức. So với khuôn hình hμ m, khái niệm tham số biểu thức trong khuôn hình lớp có một số điểm khác biệt: tham số thực tế t− ơng ứng với tham số biểu thức phải lμ một hằng số. Giả sử rằng ta muốn định nghĩa một lớp array để thao tác trên các mảng một chiều chứa các đối t− ợng có kiểu bất kỳ. Một cách tự nhiên ta nghĩ ngay đến việc tạo một khuôn hình lớp với một tham số kiểu. Đồng thời còn có thể dùng một tham số thứ hai để xác định số thμ nh phần của mảng. Trong tr− ờng hợp nμ y, định nghĩa của khuôn hình lớp có dạng nh− sau: template class array { T tab[n]; public: }; Danh sách các tham số (template ) chứa hai tham số với đặc điểm khác nhau hoμ n toμ n: một tham số kiểu đ− ợc xác đinh bởi từ khoá class, một tham số biểu thức kiểu int>. Chúng ta sẽ phải chỉ rõ giá trị của chúng trong khai báo các lớp thể hiện. Chẳng hạn, lớp thể hiện: array t− ơng ứng với khai báo nh− sau: class array { int tab[4]; public: }; Sau đây lμ một ví dụ hoμ n chỉnh: Ví dụ 4.11: #include #include template class array Trang - 160 -
  74. Lập trình h•ớng đối t•ợng { T tab[n]; public: array() { cout ti; for(int i = 0; i < 4; i++) ti[i] = i; cout<<"ti: "; for(i = 0; i < 4; i++) cout <<ti[i]<<" "; cout<<"\n"; Trang - 161 -
  75. Lập trình h•ớng đối t•ợng array tp; for(i = 0; i < 3; i++) tp[i].display(); getch(); } Chạy ch•ơng trình trên ta thu đ•ợc kết quả nh• sau: Tao mang mot chieu ti: 0 1 2 3 Tao diem 1 1 Tao diem 1 1 Tao diem 1 1 Tao mang mot chieu Toa do: 1 1 Toa do: 1 1 Toa do: 1 1 4.2.6. Tổng quát về khuôn hình lớp Ta có thể khai báo một số tuỳ ý các tham số biểu thức trong danh sách các tham số của khuôn hình hàm. Các tham số này có thể xuất hiện ở bất kỳ nơi nào trong định nghĩa của khuôn hình lớp. Khi sản sinh một lớp có các tham số biểu thức, các tham số thực tế t•ơng ứng phải là các biểu thức hằng phù hợp với kiểu dữ liệu đã khai báo trong danh sách các tham số hình thức của khuôn hình lớp. 4.2.7. Cụ thể hoá khuôn hình lớp Khả năng cụ thể hoá khuôn hình lớp có đôi chút khác biệt so với khuôn hình hàm. Khuôn hình lớp định nghĩa họ các lớp trong đó mỗi lớp chứa đồng thời định nghĩa của chính nó và các hàm thành phần. Nh• vậy, tất cả các hàm thành phần cùng tên sẽ đ•ợc thực hiện theo cùng một giải thuật. Nếu ta muốn cho một hàm thành phần thích ứng với một tình huống cụ thể cụ thể nào đó, có thể viết một định nghĩa khác cho nó. Sau đây là một ví dụ cải tiến khuôn hình lớp point. ở đây chúng ta đã cụ thể hoá hàm hiển thị display() cho tr•ờng hợp kiểu dữ liệu char: Ví dụ 4.12 Trang - 162 -
  76. Lập trình h•ớng đối t•ợng #include #include //tạo một khuôn hình lớp template class point { T x, y; public: point(T abs = 0, T ord = 0) { x = abs; y = ord; } void display(); }; template void point ::display() { cout ::display() { cout ai(3,5); ai.display(); point ac('d','y'); ac.display(); point ad(3.5, 2.3); ad.display(); getch(); } Trang - 163 -
  77. Lập trình h•ớng đối t•ợng Toa do: 3 5 Toa do: 100 121 (100 là mã ASCII của ký tự d, 121 là mã ASCII của ký tự y) Toa do: 3.5 2.3 Ta chú ý dòng tiêu đề trong khai báo một thành phần đ•ợc cụ thể hoá: void point ::display() Khai báo này nhắc ch•ơng trình dịch sử dùng hàm này thay thế hàm display() của khuôn hình lớp point (trong tr•ờng hợp giá trị thực tế cho tham số kiểu là char). Nhận xét: Có thể cụ thể hoá giá trị của tất cả các tham số. Xét khuôn hình lớp sau đây: template class array { T tab[n]; public: table() {cout ::array( ) { } Có thể cụ thể hoá một hàm thành phần hay một lớp. Trong ví dụ 4.12 đã cụ thể hoá một hàm thành phần của khuôn hình lớp. Nói chung có thể: cụ thể hoá một hay nhiều hàm thành phần, hoặc không cần thay đổi định nghĩa của bản thân lớp (thực tế cần phải làm nh• vậy) mà cụ thể hoá bản thân lớp bằng cách đ•a thêm định nghĩa. Khả năng thứ hai này có dẫn tới việc phải cụ thể hoá một số hàm thành phần. Chẳng hạn, sau khi định nghĩa khuôn hình template class point, ta có thể định nghĩa một phiên bản cụ thể cho kiểu dữ liệu point thích hợp với thể hiện point . Ta làm nh• sau: class point { //định nghĩa mới }; Trang - 164 -
  78. Lập trình h•ớng đối t•ợng 4.2.8. Sự giống nhau của các lớp thể hiện Xét câu lệnh gán giữa hai đối t•ợng. Nh• chúng ta đã biết, chỉ có thể thực hiện đ•ợc phép gán khi hai đối t•ợng có cùng kiểu (với tr•ờng hợp thừa kế vấn đề đặc biệt hơn một chút nh•ng thực chất chỉ là chuyển kiểu ngầm định đối với biểu thức vế phải của lệnh gán). Tr•ớc đây, ta biết rằng hai đối t•ợng có cùng kiểu nếu chúng đ•ợc khai báo với cùng một tên lớp. Trong tr•ờng hợp khuôn hình lớp, nên hiểu sự cùng kiểu ở đây nh• thế nào? Thực tế, hai lớp thể hiện t•ơng ứng với cùng một kiểu nếu các tham số kiểu t•ơng ứng nhau một cách chính xác và các tham số biểu thức có cùng giá trị. Nh• vậy (giả thiết rằng chúng ta đã định nghĩa khuôn hình lớp array trong ví dụ 4.11) với các khai báo: array t1; array t2; ta không có quyền viết: t2 = t1; //không t•ơng thích giữa hai tham số đầu Cũng vậy, với các khai báo: array ta; array tb; cũng không đ•ợc quyền viết ta = tb;//giá trị thực của hai tham số sau khác nhau. trong khi đó, với các khai báo: array ta; array tb; thì câu lệnh gán: ta = tb; là hoàn toàn hợp lệ Những qui tắc chặt chẽ trên nhằm làm cho phép gán ngầm định đ•ợc thực hiện chính xác. Tr•ờng hợp có các định nghĩa chồng toán tử gán, có thể không nhất thiết phải tuân theo qui tắc đã nêu trên. Chẳng hạn hoàn toàn có thể thực hiện đ•ợc phép gán: t2 = t1; nếu ta có định nghĩa hai lớp thể hiện array và array và trong lớp thứ hai có định nghĩa chồng phép gán bằng (=). Trang - 165 -
  79. Lập trình h•ớng đối t•ợng 4.2.9. Các lớp thể hiện và các lớp bạn bè Các khuôn hình lớp cũng cho phép khai báo bạn bè. Bên trong một khuôn hình lớp, ta có thể thực hiện ba kiểu khai báo bạn bè nh• sau: Khai báo các lớp bạn hoặc các hàm bạn thông th•ờng Giả sử A là một lớp thông th•ờng và fct() là một hàm thông th•ờng. Ta có thể khai báo A là lớp bạn và fct() là hàm bạn của tất cả các lớp thể hiện của khuôn hình lớp try nh• sau: template class try { int x; public: friend class A; friend int fct(float); }; Khai báo bạn bè của một thể hiện của khuôn hình hàm, khuôn hình lớp Giả sử chúng ta có khuôn hình lớp và khuôn hình hàm sau: template class point { }; template int fct (T) { }; Tiếp theo ta định nghĩa hai khuôn hình lớp nh• sau: template class try1 { int x; public: friend class point ; friend int fct(double); }; Khai báo trên đã xác định hai thể hiện rất cụ thể của khuôn hình hàm fct và khuôn hình lớp point là bạn của khuôn hình lớp try1. T•ơng tự khuôn hình lớp try1, ta định nghĩa khuôn hình lớp try2 template class try2 { int x; public: friend class point ; Trang - 166 -
  80. Lập trình h•ớng đối t•ợng friend int fct(U); }; So với try1, trong try2 ta không xác định rõ các thể hiện của fct() và point là bạn của một thể hiện của try2. Các thể hiện này sẽ đ•ợc cụ thể tại thời điểm chúng ta tạo ra một lớp thể hiện của try2. Ví dụ, với lớp thể hiện try2 ta có lớp thể hiện bạn là point và hàm thể hiện bạn là fct Khai báo bạn bè của khuôn hình hàm, khuôn hình lớp Xét ví dụ sau đây: template class try3 { int x; public: template friend class point ; template friend int fct(X); }; Lần này, tất cả các thể hiện của khuôn hình lớp point đều là bạn của các thể hiện nào của khuôn hình lớp try3. T•ơng tự nh• vậy tất cả các thể hiện của khuôn hình hàm fct() đều là bạn của các thể hiện của khuôn hình lớp try3. Ví dụ 4.13: Viết ch•ơng trình xây dựng khuôn hình lớp danh sách nối đơn với hàm tạo, hàm huỷ và các ph•ơng thức dùng để: Tạo lập danh sách, hiển thị danh sách, bổ sung phần tử, loại bỏ phần tử, sắp xếp các phần tử, tìm kiếm phần tử. Nhập vào một dãy số nguyên và thực hiện các thao tác trên danh sách nối đơn l•u trữ dãy số nguyên trên #include #include template class node { public: t info; node *link; public: Trang - 167 -
  81. Lập trình h•ớng đối t•ợng }; template class list { s *home; public: list() { home=NULL; } void display() { s *p; p=home; while(p!=NULL) { cout info; p=p->link; } } unsigned int count() { s *p; unsigned int dem=0; p=home; while(p!=NULL) { dem++; p=p->link; } return dem; } void add(t x) Trang - 168 -
  82. Lập trình h•ớng đối t•ợng { node *p, *end; p=new node; p->info=x; p->link=NULL; if(home==NULL) home=p; else { end=home; while(end->link!=NULL) end=end->link; end->link=p; } } void add(int x, unsigned int k) { node *p, *q; unsigned int dem=0; if((k count()+1)) { cout info=x; p->link=NULL; if(k==1) { p->link=home; home=p; } else Trang - 169 -
  83. Lập trình h•ớng đối t•ợng { q=home;dem=1; while(dem link; dem++; } p->link=q->link; q->link=p; } } } void remove(node *p) { node *q; if(home==NULL) return; else { if(p==home) if(home->link==NULL) home=NULL; else { home=home->link; p->link=NULL; } else { q=home; while(q->link!=p) q=q->link; q->link=p->link; } delete p; } Trang - 170 -
  84. Lập trình h•ớng đối t•ợng } void remove(int x) { node *p,*q; p=home; while(p!=NULL) { q=p; while((q!=NULL)&&(q->info!=x)) q=q->link; if(q!=NULL) { p=q->link; remove(q); } else p=NULL; } } void remove(unsigned int k) { node *p, *q; unsigned int dem=0; if((k count())) { cout link; dem++; Trang - 171 -
  85. Lập trình h•ớng đối t•ợng } remove(q); } } }; void main() { list l; int x; unsigned int k; clrscr(); l.create(); cout >x; // cout >k; l.add(x); cout >x; l.remove(x); cout<<"danh sach sau khi loai bo:"; l.display(); getch(); } Trang - 172 -
  86. Lập trình h•ớng đối t•ợng Câu hỏi và bài tập 1. Cho một dãy gồm n khoá K1, K2, , Kn với Ki ≠ Kj nếu i ≠ j. Viết ch•ơng trình xây dựng khuôn hình lớp để cài đặt các giải thuật sắp xếp: Lựa chọn, thêm dần, nổi bọt, phân đoạn, vun đống và cài đặt các giải thuật tìm kiếm: tuần tự, nhị phân và cây nhị phân tìm kiếm. 2. Viết ch•ơng trình xây dựng khuôn hình lớp tập hợp với tập hợp với các ph•ơng thức: Nhập các phần tử của một tập hợp từ bàn phím, hiển thị các phần tử của tập hợp ra màn hình, kiểm tra một phần tử có thuộc tập hợp không?; các ph•ơng thức toán tử dùng để: Tính hợp của hai tập hợp, tính giao của hai tập hợp, tính hiệu hai tập hợp. Nhập vào từ bàn phím các phần tử của hai tập hợp, tính hợp, giao, hiệu hai tập hợp trên và hiển thị ra màn hình các phần tử của tập hợp kết quả tìm đ•ợc. 3. Viết ch•ơng trình xây dựng khuôn hình lớp danh sách nối kép với hàm tạo, hàm huỷ và các ph•ơng thức dùng để: Tạo lập danh sách, hiển thị danh sách, bổ sung phần tử, loại bỏ phần tử, sắp xếp các phần tử, tìm kiếm phần tử. Nhập vào một danh sách bệnh nhân, sử dụng danh sách nối kép l•u trữ danh sách bệnh nhân và sắp xếp thứ tự khám bệnh theo nguyên tắc ai đến tr•ớc thì đ•ợc khám tr•ớc. 4. Viết ch•ơng trình xây dựng khuôn hình lớp cây nhị phân tìm kiếm với hàm tạo, hàm huỷ và các ph•ơng thức dùng để: Tạo cây, hiển thị thông tin đ•ợc l•u trữ tại các nút trên cây, bổ sung nút mới, loại bỏ nút, Nhập vào một danh sách từ tiếng Anh và một nghĩa tiếng Việt t•ơng ứng, hãy tạo cây nhị phân tìm kiếm dựa trên dãy từ tiếng Anh vừa nhập và thực hiện các thao tác tìm kiếm, bổ sung, loại bỏ trên cây nhị phân trên. Trang - 173 -
  87. Lập trình h•ớng đối t•ợng Ch•ơng 5: Các dòng xuất nhập Nội dung của ch•ơng tập trung trình bày các vấn đề sau: Các lớp dòng tin (stream) Dòng cin và toán tử nhập Dòng cout và toán tử xuất Các ph•ơng thức, cờ định dạng Các thao tác với tệp tin 5.1. Các lớp stream C++ sử dụng khái niệm dòng tin (stream) và đ•a ra các lớp dòng tin để tổ chức việc xuất nhập dữ liệu. Dòng tin có thể xem nh• một dẫy các byte. Thao tác nhập là lấy (đọc) các byte từ dòng tin (khi đó gọi là dòng nhập - input) vào bộ nhớ. Thao tác xuất là đ•a các byte từ bộ nhớ ra dòng tin (khi đó gọi là dòng xuất - output). Các thao tác này là độc lập thiết bị. Để thực hiện việc nhập, xuất lên một thiết bị cụ thể, chúng ta chỉ cần gắn dòng tin với thiết bị này. Có 4 lớp quan trọng đ•ợc khai báo trong th• viện iostream.h là: + Lớp cơ sở: ios + Hai lớp: istream và ostream đ•ợc dẫn xuất từ lớp ios + Lớp: iostream đ•ợc dẫn xuất từ lớp istream và ostream. Sơ đồ dẫn xuất giữa các lớp nh• sau: ios istream ostream iostream Sơ đồ 5.1 Trang - 174 -
  88. Lập trình h•ớng đối t•ợng 5.2. Dòng cin và toán tử nhập Dòng cin là một đối t•ợng kiểu istream, là dòng vào chuẩn gắn với bàn phím. Các thao tác nhập trên dòng cin đồng nghĩa với nhập dữ liệu từ bàn phím. cin và toán tử nhập (>>) Cách dùng toán tử nhập để đọc dữ liệu từ dòng cin nh• sau: cin>>tham_số_1>>tham_số_2>> >>tham_số_n; Trong đó: các tham số có thể là biến; phần tử mảng nguyên, thực; ký tự; con trỏ ký tự Cú pháp trên dùng để nhập dữ liệu dạng số, ký tự, xâu ký tự không chứa khoảng trắng(dấu cách). Cách thức nhập nh• sau: Bỏ qua các ký tự trắng (dấu cách, dấu tab, dấu chuyển dòng) đứng tr•ớc nếu có và sau đó đọc vào các ký tự t•ơng ứng với kiểu yêu cầu (đ•ợc chỉ ra bởi kiểu của biến). Việc nhập kết thúc khi gặp ký tự trắng hoặc ký tự không đúng định dạng của dữ liệu cần nhập. cin và các ph•ơng thức nhập ký tự, chuỗi ký tự Toán tử nhập (>>) chỉ tiện lợi khi nhập dữ liệu dạng số, để nhập dữ liệu kiểu ký tự hoặc xâu ký tự ta có thể sử dụng một số ph•ơng thức sau: Cú pháp: int cin.get(); Chức năng: Nhập một ký tự (bao gồm cả khoảng trắng) Cú pháp: istream cin.get(char &ch); Chức năng: Nhập một ký tự và đặt vào biến kiểu char đ•ợc tham chiếu bởi ch Cú pháp: cin.get(char *str, int n, char delim = '\n'); Chức năng: Nhập một dãy ký tự (kể cả dấu cách và đ•a vào biến do str trỏ tới. Quá trình đọc kết thúc khi gặp ký tự giới hạn ('\n') hoặc đã nhận đủ n-1 ký tự Cú pháp: cin.getline(char *str, int n, char delim = '\n'); Chức năng: Nhập một dãy ký tự (kể cả dấu cách và đ•a vào biến do str trỏ tới. Quá trình đọc kết thúc khi gặp ký tự giới hạn ('\n') hoặc đã nhận đủ n-1 ký tự, sau đó loại mã của phím Enter ra khỏi dòng nhập. Cú pháp: Trang - 175 -
  89. Lập trình h•ớng đối t•ợng cin.ignore(n); Chức năng: Loại bỏ n ký tự trên dòng nhập. Th•ờng dùng để loại bỏ các ký tự không cần thiết trên dòng nhập tr•ớc khi thực hiện việc nhập dữ liệu đặc biệt là khi nhập dữ liệu dạng ký tự hoặc xâu ký tự. Có thể định nghĩa chồng toán tử nhập (>>) trên dữ liệu kiểu đối t•ợng của lớp. Để thực hiện việc này ta phải xây dựng hàm toán tử nhập là hàm bạn của lớp đó. Giả sử ta khai báo một lớp ts- thí sinh, gồm 3 thuộc tính: sbd- số báo danh, ten- họ và tên, diem- tổng điểm thi của thí sinh khi đó ta có thể định nghĩa chồng toán tử nhập để nhập thông tin về một thí sinh nh• sau: Ví dụ 5.1: class ts { char sbd[5]; char ten[30]; unsigned int diem; public: friend istream &operator>>(istream &is, ts &t) { cout >t.diem; is.ignore(); return is; } }; 5.3. Dòng cout và toán tử xuất Dòng cout là một đối t•ợng kiểu ostream, đó là dòng xuất chuẩn gắn với màn hình. Cách dùng toán tử xuất để xuất dữ liệu từ bộ nhớ ra dòng cout nh• sau: Trang - 176 -
  90. Lập trình h•ớng đối t•ợng Cú pháp: cout #include #include struct ts { char sbd[5]; char ten[30]; unsigned int diem; public: friend ostream &operator >(istream &is, ts &t) { cout<<"So bao danh:"; is.getline(t.sbd,5);//ph•ơng thức dùng để nhập xâu ký tự cout<<"Ho va ten:"; is.getline(t.ten,30); cout<<"Tong diem:"; Trang - 177 -
  91. Lập trình h•ớng đối t•ợng is>>t.diem; is.ignore(); return is; } }; void main() { ts ds[100]; int n,i; cout >n; cin.ignore(1);//xoá bộ nhớ đệm bàn phím for(i=1;i >ds[i]; } cout<<"\nDanh sach thi sinh\n"; for(i=1;i<=n;i++) cout<<ds[i]<<"\n"; getch(); } 5.4. Các ph•ơng thức định dạng 5.4.1. Nội dung định dạng giá trị xuất Nội dung định dạng là xác định các thông số: - Độ rộng quy định - Độ chính xác - Ký tự độn - Và các thông số khác Độ rộng thực tế của giá trị xuất: Trang - 178 -
  92. Lập trình h•ớng đối t•ợng C++ sẽ biến đổi giá trị cần xuất thành một chuỗi ký tự rồi đ•a chuỗi này ra màn hình. Ta sẽ gọi số ký tự của chuỗi này là độ rộng thực tế của giá trị xuất. Ví dụ với các câu lệnh: int n = 4567, m = -23 ; float x = -3.1416 ; char ht[] = “Tran Van Thong” ; thì độ rộng thực tế của n là 4, của m là 3, của x là 7, của ht là 14. Độ rộng quy định: Là số vị trí tối thiểu trên màn hình dành để in giá trị. Theo mặc định, độ rộng quy định bằng 0. Chúng ta có thể dùng ph•ơng thức cout.width() để thiết lập rộng này. Ví dụ câu lệnh: cout.width(8); sẽ thiết lập độ rộng quy định là 8. Mối quan hệ giữa độ rộng thực tế và độ rộng quy định: - Nếu độ rộng thực tế lớn hơn hoặc bằng độ rộng quy định thì số vị trí trên màn hình chứa giá trị xuất sẽ bằng độ rộng thực tế. - Nếu độ rộng thực tế nhỏ hơn độ rộng quy định thì số vị trí trên màn hình chứa giá trị xuất sẽ bằng độ rộng quy định. Khi đó sẽ có một số vị trí d• thừa. Các vị trí d• thừa sẽ đ•ợc độn (lấp đầy) bằng ký tự độn. Xác định ký tự độn: Ký tự độn mặc định là dấu cách (khoảng trống). Tuy nhiên, có thể dùng ph•ơng thức cout.fill() để chọn một ký tự độn khác. Ví dụ với các câu lệnh sau: int n=123; // Độ rộng thực tế là 3 cout.fill(„*‟); // Ký tự độn là * cout.width(5); // Độ rộng quy định là 5 cout << n ; thì kết quả in ra là: 123 Độ chính xác: Trang - 179 -
  93. Lập trình h•ớng đối t•ợng Là số vị trí dành cho phần thập phân (khi in số thực). Độ chính xác mặc định là 6. Tuy nhiên có thể dùng ph•ơng thức cout.precision() để chọn độ chính xác. Ví dụ với các câu lệnh: float x = 34.455 ; // Độ rộng thực tế 6 cout.precision(2) ; // Độ chính xác 2 cout.width(8); // Độ rộng quy •ớc 8 cout.fill(„0‟) ; // Ký tự độn là số 0 cout << x ; thì kết quả in ra là: 0034.46 5.4.2. Các ph•ơng thức định dạng Cú pháp: int cout.width() Chức năng: Cho biết độ rộng quy định hiện tại. Cú pháp: int cout.width(int n); Chức năng:Thiết lập độ rộng quy định mới là n và trả về độ rộng quy định tr•ớc đó. Chú ý: Độ rộng quy định n chỉ có tác dụng cho một giá trị xuất. Sau đó, C++ lại áp dụng độ rộng quy định bằng 0. Ví dụ với các câu lệnh: int m =1234, n = 56; cout << “\nAB” cout.width(6); cout << m ; cout << n ; thì kết quả in ra là: AB 123456 (giữa B và số 1 có 2 dấu cách). Cú pháp: int cout.precision(); Trang - 180 -
  94. Lập trình h•ớng đối t•ợng Chức năng: Cho biết độ chính xác hiện tại (đang áp dụng để xuất các giá trị thức). Cú pháp: int cout.precision(int n); Chức năng: Thiết lập độ chính xác sẽ áp dụng là n chữ số phần thập phân và cho biết độ chính xác tr•ớc đó. Độ chính xác đ•ợc thiết lập sẽ có hiệu lực cho tới khi gặp một câu lệnh thiết lập độ chính xác mới. Cú pháp: char cout.fill(); Chức năng: Cho biết ký tự độn hiện tại đang đ•ợc áp dụng. Cú pháp: char cout.fill(char ch); Chức năng: Quy định ký tự độn mới sẽ đ•ợc dùng là ch và cho biết ký tự độn đang dùng tr•ớc đó. Ký tự độn đ•ợc thiết lập sẽ có hiệu lực cho tới khi gặp một câu lệnh chọn ký tự độn mới. Ví dụ 5.3: #include #include void main() { clrscr(); float x=-3.1551, y=-23.45421; cout.precision(2); cout.fill('*'); cout << "\n" ; cout.width(8); cout << x; cout << "\n" ; cout.width(8); cout << y; getch(); } Trang - 181 -
  95. Lập trình h•ớng đối t•ợng Sau khi thực hiện, ch•ơng trình in ra màn hình 2 dòng sau: -3.16 -23.45 5.5. Cờ định dạng 5.5.1. Khái niệm chung về cờ Mỗi cờ chứa trong một bit. Cờ có 2 trạng thái: Bật (on) - có giá trị 1 Tắt (off) - có giá trị 0 Các cờ có thể chứa trong một biến kiểu long. Trong tệp đã định nghĩa các cờ sau: ios::left ios::right ios::internal ios::dec ios::oct ios::hex ios::fixed ios::scientific ios::showpos ios::uppercase ios::showpoint ios::showbase 5.5.2. Công dụng của các cờ Có thể chia các cờ thành các nhóm: Nhóm 1: gồm các cờ định vị (căn lề) ios::left, ios::right, ios::internal Cờ ios::left: Khi bật cờ ios:left thì giá trị in ra nằm bên trái vùng quy định, các ký tự độn nằm sau, ví dụ: 35 -89 Cờ ios::right: Khi bật cờ ios:right thì giá trị in ra nằm bên phải vùng quy định, các ký tự độn nằm tr•ớc, ví dụ: 35 -89 Chú ý: Mặc định cờ ios::right bật. Trang - 182 -
  96. Lập trình h•ớng đối t•ợng Cờ ios::internal: Cờ ios:internal có tác dụng giống nh• cờ ios::right chỉ khác là dấu (nếu có) in đầu tiên, ví dụ: 35 - 89 Ví dụ 5.4: #include #include void main() { clrscr(); float x=-87.1551, y=23.45421; cout.precision(2); cout.fill('*'); cout.setf(ios::left); // Bật cờ ios::left cout << "\n" ; cout.width(8); cout << x; cout << "\n" ; cout.width(8); cout << y; cout.setf(ios::right); // Bật cờ ios::right cout << "\n" ; cout.width(8); cout << x; cout << "\n" ; cout.width(8); cout << y; cout.setf(ios::internal); // // Bật cờ ios::internal cout << "\n" ; Trang - 183 -
  97. Lập trình h•ớng đối t•ợng cout.width(8); cout << x; cout << "\n" ; cout.width(8); cout << y; getch(); } Sau khi thực hiện ch•ơng trình in ra 6 dòng nh• sau: -87.16 23.45 -87.16 23.45 - 87.16 23.45 Nhóm 2 : gồm các cờ định dạng số nguyên ios::dec, ios::oct, ios::hex + Khi ios::dec bật (mặc định): Số nguyên đ•ợc in d•ới dạng cơ số 10 + Khi ios::oct bật : Số nguyên đ•ợc in d•ới dạng cơ số 8 + Khi ios::hex bật : Số nguyên đ•ợc in d•ới dạng cơ số 16 Nhóm 3: gồm các cờ định dạng số thực ios::fĩxed, ios::scientific, ios::showpoint Mặc định: Cờ ios::fixed bật (on) và cờ ios::showpoint tắt (off). + Khi ios::fixed bật và cờ ios::showpoint tắt thì số thực in ra d•ới dạng thập phân, số chữ số phần phân (sau dấu chấm) đ•ợc tính bằng độ chính xác n nh•ng khi in thì bỏ đi các chữ số 0 ở cuối. Ví dụ nếu độ chính xác n = 4 thì: Số thực -87.1500 đ•ợc in: -87.15 Số thực 23.45425 đ•ợc in: 23.4543 Trang - 184 -
  98. Lập trình h•ớng đối t•ợng Số thực 678.0 đ•ợc in: 678 + Khi ios::fixed bật và cờ ios::showpoint bật thì số thực in ra d•ới dạng thập phân, số chữ số phần phân (sau dấu chấm) đ•ợc in ra đúng bằng độ chính xác n. Ví dụ nếu độ chính xác n = 4 thì: Số thực -87.1500 đ•ợc in: -87.1500 Số thực 23.45425 đ•ợc in: 23.4543 Số thực 678.0 đ•ợc in: 678.0000 + Khi ios::scientific bật và cờ ios::showpoint tắt thì số thực in ra d•ới dạng mũ (dạng khoa học). Số chữ số phần phân (sau dấu chấm) của phần định trị đ•ợc tính bằng độ chính xác n nh•ng khi in thì bỏ đi các chữ số 0 ở cuối. Ví dụ nếu độ chính xác n = 4 thì: Số thực -87.1500 đ•ợc in: -8.715e+01 Số thực 23.45425 đ•ợc in: 2.3454e+01 Số thực 678.0 đ•ợc in: 6.78e+02 + Khi ios::scientific bật và cờ ios::showpoint bật thì số thực in ra d•ới dạng mũ. Số chữ số phần phân (sau dấu chấm) của phần định trị đ•ợc in đúng bằng độ chính xác n. Ví dụ nếu độ chính xác n = 4 thì: Số thực -87.1500 đ•ợc in: -8.7150e+01 Số thực 23.45425 đ•ợc in: 2.3454e+01 Số thực 678.0 đ•ợc in: 6.7800e+01 Nhóm 4: gồm các hiển thị ios::showpos, ios::showbase, ios::uppercase Cờ ios::showpos + Nếu cờ ios::showpos tắt (mặc định) thì dấu cộng không đ•ợc in tr•ớc số d•ơng. + Nếu cờ ios::showpos bật thì dấu cộng đ•ợc in tr•ớc số d•ơng. Cờ ios::showbase Trang - 185 -
  99. Lập trình h•ớng đối t•ợng + Nếu cờ ios::showbase bật thì số nguyên hệ 8 đ•ợc in bắt đầu bằng ký tự 0 và số nguyên hệ 16 đ•ợc bắt đầu bằng các ký tự 0x. Ví dụ nếu a = 40 thì: dạng in hệ 8 là: 050 dạng in hệ 16 là 0x28 + Nếu cờ ios::showbase tắt (mặc định) thì không in 0 tr•ớc số nguyên hệ 8 và không in 0x tr•ớc số nguyên hệ 16. Ví dụ nếu a = 40 thì: dạng in hệ 8 là: 50 dạng in hệ 16 là 28 Cờ ios::uppercase + Nếu cờ ios::uppercase bật thì các chữ số hệ 16 (nh• A, B, C, ) đ•ợc in d•ới dạng chữ hoa. + Nếu cờ ios::uppercase tắt (mặc định) thì các chữ số hệ 16 (nh• A, B, C, ) đ•ợc in d•ới dạng chữ th•ờng. 5.5.3. Các ph•ơng thức bật tắt cờ Các ph•ơng thức này định nghĩa trong lớp ios. Cú pháp: long cout.setf(long f); Chức năng: Bật các cờ liệt kê trong f và trả về một giá trị long biểu thị các cờ đang bật. Thông th•ờng giá trị f đ•ợc xác định bằng cách tổ hợp các cờ. Ví dụ câu lệnh: cout.setf(ios::showpoint | ios::scientific) ; sẽ bật các cờ ios::showpoint và ios::scientific. Cú pháp: long cout.unsetf(long f); Chức năng: Tắt các cờ liệt kê trong f và trả về một giá trị long biểu thị các cờ đang bật. Thông th•ờng giá trị f đ•ợc xác định bằng cách tổ hợp các cờ. Ví dụ câu lệnh: cout.unsetf(ios::showpoint | ios::scientific); sẽ tắt các cờ ios::showpoint và ios::scientific. Cú pháp: long cout.flags(long f); Trang - 186 -
  100. Lập trình h•ớng đối t•ợng Chức năng: Giống nh• cout.setf(long). Ví dụ câu lệnh: cout.flags(ios::showpoint | ios::scientific) ; sẽ bật các cờ ios::showpoint và ios::scientific. Cú pháp: long cout.flags(); Chức năng: Trả về một giá trị long biểu thị các cờ đang bật. 5.6. Các dòng tin chuẩn Để nhập, xuất dữ liệu lên tệp, chúng ta cần tạo các dòng tin mới (khai báo các đối t•ợng stream) và gắn chúng với một tập tin cụ thể. C++ cung cấp 3 lớp stream để làm điều này: Lớp ofstream: dùng để tạo các dòng xuất (ghi tệp) Lớp ifstream: dùng để tạo các dòng nhập (đọc tệp) Lớp fstream: dùng để nhập, xuất hoặc cả nhập và xuất Sơ đồ dẫn xuất nh• sau: ios ostream fstreambase istream ofstream ifstream fstream Sơ đồ 5.2 5.7. Ghi dữ liệu lên tệp Trong C++, khi thao tác với một tệp dữ liệu, cần thực hiện tuần tự theo các b•ớc nh• sau: 1) Mở tệp tin 2) Thực hiện các thao tác đọc, ghi trên tệp tin đang mở 3) Đóng tệp tin Trang - 187 -