Bài giảng Lập trình Hướng đối tượng C++ - Chương 5: Dẫn xuất và thừa kế

doc 42 trang hoanguyen 3320
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 C++ - Chương 5: Dẫn xuất và thừa kế", để 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:

  • doctai_lieu_lap_trinh_huong_doi_tuong_chuong_5_dan_xuat_va_thua.doc

Nội dung text: Bài giảng Lập trình Hướng đối tượng C++ - Chương 5: Dẫn xuất và thừa kế

  1. chương 5 A Dẫn xuất và thừa kế Có 2 khái niệm rất quan trọng đã làm nên toàn bộ thế mạnh của B C D phương pháp lập trình hướng đối tượng đó là tính kế thừa (inheritance) và tính tương ứng bội (polymorphism). Tính kế thừa Sơ đồ 3: Lớp D dẫn xuất từ 3 lớp A, B, C cho phép các lớp được xây dựng trên các lớp đã có. Trong chương này sẽ nói về sự thừa kế của các lớp. A B C § 1. Sự dẫn xuất và tính thừa kế D 1.1. Lớp cơ sở và lớp dẫn xuất Sơ đồ 4: Lược đồ dẫn xuất tổng quát Một lớp được xây dựng thừa kế một lớp khác gọi là lớp dẫn xuất. Lớp dùng để xây dựng lớp dẫn xuất gọi là lớp cơ sở. A B C Lớp nào cũng có thể là một lớp cơ sở. Hơn thế nữa, một lớp có thể là cơ sở cho nhiều lớp dẫn xuất khác nhau. Đến lượt mình, lớp dẫn xuất lại có thể dùng làm cơ sở để xây dựng các lớp dân xuất D E khác. Ngoài ra một lớp có thể dẫn xuất từ nhiều lớp cơ sở. Dưới đây là một số sơ đồ về quan hệ dẫn xuất của các lớp: Sơ đồ 1: Lớp B dẫn xuất từ lớp A, lớp C dẫn xuất từ lớp B F G H A Tính thừa kế: Một lớp dẫn xuất ngoài các thành phần của riêng nó, nó còn đượ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. Ví dụ trong sơ đồ 1 thì lớp C được thừa kế các thành phần B của các lớp B và A. Trong sơ đồ 3 thì lớp D được thừa kế các thành phần của các lớp A, B và C. Trong sơ đồ 4 thì lớp G được thừa kế các thành phần của các lớp D, E, A, B và C. C 1.2. Cách xây dựng lớp dân xuất Giả sử đã định nghĩa các lớp A và B. Để xây dựng lớp C dân xuất từ A và B, ta viết như sau: class C : public A, public B Sơ đồ 2: Lớp A là cơ sở của các lớp B, C và D { 237 238
  2. private: Chú ý: Cho phép đặt trùng tên thuộc tính trong các lớp cơ sở và // Khai báo các thuộc tính lớp dẫn xuất. public: Ví dụ: // Các phương thức class A } ; { private: 1.3. Thừa kế private và public int a, b, c; Trong ví dụ trên, lớp C thừa kế public các lớp A và B. Nếu thay public: từ khoá public bằng private, thì sự thừa kế là private. Chú ý: Nếu bỏ qua không dùng từ khoá thì hiểu là private, ví dụ nếu định nghĩa: }; class C : public A, B class B { { private: private: double a, b, x; // Khai báo các thuộc tính public: public: // Các phương thức }; } ; class C : public A, B thì A là lớp cơ sở public của C , còn B là lớp cơ sở private của C. { Theo kiểu thừa kế public thì tất cả các thành phần public của lớp private: cơ sở cũng là các thành phần public của lớp dẫn xuất. char *a , *x ; Theo kiểu thừa kế private thì tất cả các thành phần public của lớp int b ; cơ sở sẽ trơ thành các thành phần private của lớp dẫn xuất. public: 1.4. Thừa kế các thành phần dữ liệu (thuộc tính) 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 của lớp dẫn xuất sẽ gồm: các thuộc tính mới Khi đó lớp C sẽ có các thuộc tính: khai báo trong định nghĩa lớp dẫn xuất và các thuộc tính của lớp cơ sở. A::a , A::b, A::c (kiểu int) - thừa kế từ A Tuy vậy trong lớp dẫn xuất không cho phép truy nhập đến các B::a , B::b, B::x (kiểu double) - thừa kế từ B thuộc tính private của lớp cơ sở. a, x (kiểu char*) và b (kiểu int) - khai báo trong C 239 240
  3. Trong các phương thức của C chỉ cho phép truy nhập trực tiếp tới #include các thuộc tính khai báo trong C. class DIEM 1.5. Thừa kế phương thức { 241 242 Trừ: private: + Hàm tạo double x, y; + Hàm huỷ public: + Toán tử gán DIEM() 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. x = y =0.0; Ví dụ: Trong chương trình dưới đây: } + Đầu tiên định nghĩa lớp DIEM có: DIEM(double x1, double y1) Các thuộc tính x, y { Hai hàm tạo x = x1; y = y1; Phương thức in() } + Sau đó xây dựng lớp HINH_TRON dẫn xuất từ lớp DIEM, đưa void in() thêm: { Thuộc tính r cout }
  4. HINH_TRON(double x1, double y1, #include double r1): DIEM(x1,y1) class DIEM { { r = r1; private: } double x, y; 243 244 double getR() public: { return r; DIEM() } { }; x = y =0.0; void main() } { DIEM (double x1, double y1) HINH_TRON h(2.5,3.5,8); { clrscr(); x = x1; y = y1; } cout r = 0.0; }
  5. HINH_TRON(double x1, double y1, double r1): d(x1,y1) + Hàm tạo cần có các đối để khởi gán cho các thuộc tính (thành { phần dữ liệu) của lớp. r = r1; + Có thể phân thuộc tính làm 3 loại ứng với 3 cách khởi gán khác } nhau: void in() 1. Các thuộc tính mới khai báo trong lớp dẫn xuất. Trong các { phương thức của lớp dẫn xuất có thể truy xuất đến các thuộc tính này. Vì vậy chúng thường được khởi gán bằng các câu lệnh gán viết d.in(); trong thân hàm tạo. } 245 246 2. Các thành phần kiểu đối tượng. Trong lớp dẫn xuất không cho double getR() phép truy nhập đến các thuộc tính của các đối tượng này. Vì vậy để { khởi gán cho các đối tượng thành phần cần dùng hàm tạo của lớp return r; tương ứng. Điều này đã trình bầy trong mục §8 chương 4. } 3. Các thuộc tính thừa kế từ các lớp cở sở. Trong lớp dẫn xuất }; không được phép truy nhập đến các thuộc tính này. Vì vậy để khởi void main() gán cho các thuộc tính nói trên, cần sử dụng hàm tạo của lớp cơ sở. Cách thức cũng giống như khởi gán cho các đối tượng thành phần, { chỉ khác nhau ở chỗ: Để khởi gán cho các đối tượng thành phần ta HINH_TRON h(2.5,3.5,8); dùng tên đối tượng thành phần, còn để khởi gán cho các thuộc tính clrscr(); thừa kế từ các lớp cơ sở ta dùng tên lớp cơ sở: cout << "\nHinh tron co tam: "; Tên_đối_tượng_thành_phần(danh sách giá trị) h.in(); Tên_lớp_cơ_sở(danh sách giá trị) cout << "\nCo ban kinh= " << h.getR(); Danh sách giá trị lấy từ các đối của hàm tạo của lớp dẫn xuất đang xây dựng getch(); (xem ví dụ mục 2.4 và §6, ví dụ 1) } 2.3. Hàm huỷ § 2. Hàm tạo, hàm huỷ đối với tính thừa kế Khi một đối tượng của lớp dẫn xuất được giải phóng (bị huỷ), thì các đối tượng thành phần và các đối tượng thừa kế từ các lớp cơ sở 2.1. Lớp dẫn xuất không thừa kế các hàm tạo, hàm huỷ, toán tử cũng bị giải phóng theo. Do đó các hàm huỷ tương ứng sẽ được gọi gán của các lớp cơ sở đến. 2.2. Cách xây dựng hàm tạo của lớp dẫn xuất Như vậy khi xây dựng hàm huỷ của lớp dẫn xuất, chúng ta chỉ cần quan tâm đến các thuộc tính (không phải là đối tượng) khai báo thêm trong lớp dẫn xuất mà thôi. Ta không cần để ý đến các đối
  6. tượng thành phần và các thuộc tính thừa kế từ các lớp cơ sở. (xem ví #include dụ mục 2.4 và §6, ví dụ 2) #include 2.4. Ví dụ xét các lớp #include + Lớp NGUOI gồm: class MON_HOC { - Các thuộc tính private: char *ht ; // Họ tên char *monhoc; int ns ; 247 248 int st; - Hai hàm tạo, phương thức in() và hàm huỷ public: + Lớp MON_HOC gồm: MON_HOC() - Các thuộc tính { char *monhoc ; // Tên môn học monhoc=NULL; int st ; // Số tiết st=0; - Hai hàm tạo, phương thức in() và hàm huỷ } + Lớp GIAO_VIEN : MON_HOC(char *monhoc1, int st1) - Kế thừa từ lớp NGUOI { - Đưa thêm các thuộc tính int n = strlen(monhoc1); char *bomon ; // Bộ môn công tác monhoc = new char[n+1]; MON_HOC mh ; // Môn học đang dậy strcpy(monhoc,monhoc1); - Hai hàm tạo , phương thức in() và hàm huỷ st=st1; Hãy để ý cách xây dựng các hàm tạo, hàm huỷ của lớp dẫn xuất } GIAO_VIEN. Trong lớp GIAO_VIEN có thể gọi tới 2 phương thức ~ MON_HOC() in(): { GIAO_VIEN::in() // Được xây dựng trong lớp GIAO_VIEN if (monhoc!=NULL) NGUOI::in() // Thừa kế từ lớp NGUOI { Hãy chú ý cách gọi tới 2 phương thức in() trong chương trình delete monhoc; dưới đây. st=0; //CT5-03 } // Ham tao cua lop dan suat }
  7. void in() } { } cout << "\nTen mon: " << monhoc << " so tiet: " << st; void in() } { } ; cout << "\nHo ten : " << ht << " nam sinh: " << ns; class NGUOI { } private: } ; char *ht; class GIAO_VIEN : public NGUOI int ns; { 249 250 public: private: NGUOI() char *bomon; { MON_HOC mh; ht=NULL; public: ns=0; GIAO_VIEN():mh(),NGUOI()//Su dung ham tao khong doi } { NGUOI(char *ht1, int ns1) bomon=NULL; { } int n = strlen(ht1); GIAO_VIEN(char *ht1, int ns1, char *monhoc1,int st1, ht = new char[n+1]; char *bomon1 ): strcpy(ht,ht1); NGUOI(ht1,ns1),mh(monhoc1, st1) ns=ns1; { } int n = strlen(bomon1); ~NGUOI() bomon = new char[n+1]; { strcpy(bomon,bomon1); if (ht!=NULL) } { ~GIAO_VIEN() delete ht; { if (bomon!=NULL) ns=0; delete bomon;
  8. } 3.1. Các từ khoá quy định phạm vi truy nhập của lớp cơ sở void in() + Mặc dù lớp dẫn xuất được thừa kế tất cả các thành phần của lớp { cơ sở, nhưng trong lớp dẫn xuất không thể truy nhập tới tất cả các // Su dung phuong thuc in thành phần này. Giải pháp thường dùng là sử dụng các phương thức của lớp cở sở để truy nhập đến các thuộc tính của chính lớp cơ sở đó. NGUOI::in(); Cũng có thể sử dụng các giải pháp khác dưới đây. cout in(); các thành phần public và protected của lớp dẫn xuất theo kiểu public. /* + Các thành phần public và protected của lớp cơ sở sẽ trở thành co the viet các thành phần private của lớp dẫn xuất theo kiểu private. g2->GIAO_VIEN::in(); */ Ví dụ : g2->NGUOI::in(); Giả sử lớp A có: getch(); thuộc tính public a1 delete g2; // Goi toi cac ham huy thuộc tính protected a2 getch(); và lớp B dẫn xuất public từ A, thì A::a1 trở thành public trong B, } A::a2 trở thành protected trong B. Do đó nếu dùng B làm lớp cở để xây dựng lớp C. Thì trong C có thể truy nhập tới A::a1 và A::a2. § 3. Phạm vi truy nhập đến các thành phần của lớp cơ sở
  9. Thế nhưng nếu sửa đổi để B dẫn xuất private từ A, thì cả A::a1 và { A::a2 trơ thành private trong B, và khi đó trong C không được phép protected: truy nhập tới các thuộc tính A::a1 và A::a2. int b1; Để biết tường tận hơn, chúng ta hãy biên dịch chương trình: public: //CT5-04 int b2; // Pham vi truy nhap #include B() #include { #include b1=b2=0; class A } { B(int t1, int t2, int u1, int u2) protected: { 253 254 int a1; a1=t1; a2=t2; b1=u1;b2=u2; public: int a2; } A() void in() { { a1=a2=0; cout << a1 <<" " << a2 << " " << b1 << " " << b2; } } A(int t1, int t2) } ; { class C : public B a1=t1; a2= t2; { } public: void in() C() { { cout << a1 <<" " << a2 ; b1=b2=0; } } } ; C(int t1, int t2, int u1,int u2) class B: private A {
  10. a1=t1; a2=t2; b1=u1;b2=u2; + Đến lượt mình, B có thể dùng làm cơ sở để xây dựng lớp dẫn } xuất mới (ví dụ C). void in() + Tiếp đó lại có thể dùng C làm cơ sở để xây dựng lớp dẫn xuất mới. { + Sự tiếp tục theo cách trên là không hạn chế. cout << a1; Sơ đồ trên chính là sự thừa kế nhiều mức. Ngoài ra chúng ta cũng cout <<" " << a2 << " " << b1 << " " << b2; đã biết: } + Một lớp có thể được dẫn xuất từ nhiều lớp cơ sở. }; + Một lớp có thể dùng làm cơ sở cho nhiều lớp. void main() Hình vẽ dưới đây là một ví dụ về sơ đồ thừa kế khá tổng quát, thể { hiện được các điều nói trên: C c(1,2,3,4); A B C c.in(); 255 256 getch(); } D E Chúng ta sẽ nhận được 4 thông báo lỗi sau trong lớp C (tại hàm tạo có đối và phương thức in): A::a1 is not accessible F G H A::a2 is not accessible Diễn giải: A::a1 is not accessible Lớp D dẫn xuất từ A và B A::a2 is not accessible Lớp E dẫn xuất từ C Bây giờ nếu sửa đổi để lớp B dẫn xuất public từ A thì chương Lớp F dẫn xuất từ D trình sẽ không có lỗi và làm việc tốt. Lớp G dẫn xuất từ D và E Lớp H dẫn xuất từ E § 4. Thừa kế nhiều mức và sự trùng tên 4.1. Sơ đồ xây dựng các lớp dẫn xuất theo nhiều mức 4.2. Sự thừa kế nhiều mức. Như đã biết: + Như đã biết: Lớp dẫn xuất 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ơ + Khi đã định nghĩa một lớp (ví dụ lớp A), ta có thể dùng nó làm sở được thừa kế. cơ sở để xây dựng lớp dẫn xuất (ví dụ B).
  11. + Hãy áp dụng nguyên lý trên để xét lớp G: D h; // h là đối tượng của lớp D dẫn xuất từ A và B - Lớp G thừa kế các thành phần của các lớp D và E h.D::n là thuộc tính n khai báo trong D - Lớp D thừa kế các thành phần của lớp A và B h.A::n là thuộc tính n thừa kế từ A (khai báo trong A) - Lớp E thừa kế các thành phần của lớp C h.D::nhap() là phương thức nhap() định nghĩa trong D Như vậy các thành phần có thể sử trong lớp G gồm: h.A::nhap() là phương thức nhap() định nghĩa trong A - Các thành phần khai báo trong G (của riêng G) Cách 2: Không dùng tên lớp, chỉ dùng tên thành phần. Khi đó - Các thành phần khai báo trong các lớp D, E, A, B, C (được Chương trình dịch C++ phải tự phán đoán để biết thành phần đó thừa kế). thuộc lớp nào. Cách phán đoán như sau: Trước tiên xem thành phần đang xét có trùng tên với một thành phần nào của lớp dẫn xuất không? Nếu trùng thì đó là thành phần của lớp dẫn xuất. Nếu không trùng thì tiếp tục xét các lớp cơ sở theo thứ tự: Các lớp có quan hệ gần với lớp dẫn xuất xét trước, các lớp quan hệ xa xét sau. Hãy chú ý trường hợp sau: Thành phần đang xét có mặt đồng thời trong 2 lớp 4.3. Sự trùng tên cơ sở có cùng một đẳng cấp quan hệ với lớp dẫn xuất. Gặp trường Như đã nói trong 4.2: Trong lớp G có thể sử dụng (trực tiép hay hợp này Chương trình dịch C++ không thể quyết định được thành gián tiếp) các thành phần của riêng G và các thành phần mà nó được phần này thừa kế từ lớp nào và buộc phải đưa ra một thông báo lỗi257 258 thừa kế từ các lớp D, E, A, B, C. Yêu cầu về cách đặt tên ở đây là: (xem ví dụ dưới đây). Cách khắc phục: Trường hợp này phải sử dụng thêm tên lớp như trình bầy trong cách 1. + Tên các lớp không được trùng lặp Ví dụ xét lớp dẫn xuất D. Lớp D có 2 cơ sở là các lớp A và B. + Tên các thành phần trong một lớp cũng không được trùng lặp Giả sử các lớp A, B và D được định nghĩa: + Tên các thành phần trong các lớp khác nhau có quyền được class A trùng lặp. { Để phân biệt các thành phần trùng tên trong lớp dẫn xuất, cần sử dụng thêm tên lớp (xem ví dụ trong 4.4). private: int n; 4.4. Sử dụng các thành phần trong lớp dẫn xuất float a[20]; Như đã nói ở trên: Thành phần của lớp dẫn xuất gồm: public: + Các thành phần khai báo trong lớp dẫn xuất void nhap(); + Các thành phần mà lớp dẫn xuất thừa kế từ các lớp cơ sở void xuat(): Quy tắc sử dụng các thành phần trong lớp dẫn xuất: } ; Cách 1: Dùng tên lớp và tên thành phần. Khi đó Chương trình class B dịch C++ dễ dàng phân biệt thành phần thuộc lớp nào. Ví dụ: {
  12. private: void D::xuat() int m,n; { float a[20][20]; cout > k ; // k là thuộc tính của D Tuy nhiên vẫn có thể có trường hợp cùng một lớp cơ sở được đề cập nhiều hơn một lần trong các lớp cơ sở trung gian của một lớp A::nhap(); // Nhập các thuộc tính mà D thừa kế từ A dẫn xuất. Ví dụ: B::nhap(); // Nhập các thuộc tính mà D thừa kế từ B #include } class A // Xây dựng phương thức xuat() {
  13. public: h.a = 1 ; int a; thì Chương trình dịch C++ không thể nhận biết được thuộc tính a } ; thừa kế thông qua B hay thông qua C và nó sẽ đưa ra thông báo lỗi sau: class B : public A { Member is ambiguous: ‘A::a’ and ‘A::a’ public: 5.2. Các lớp cơ sở ảo int b; Giải pháp cho vấn đề nói trên là khai báo A như một lớp cơ sở } ; kiểu virtual cho cả B và C. Khi đó B và C được định nghĩa như sau: class C : public A class B : virtual public A { { public: public: int c; int b; } ; } ; class D : public B , public C class C : virtual public A { { public: public: int d; int c; } ; } ; void main() Các lớp cơ sở ảo (virtual) 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 ví dụ trên, hai261 262 { lớp cơ sở A ( A là cơ sở của B và A là cơ sở của C) sẽ kết hợp lại để D h ; trở thành một lớp cơ sở A duy nhất cho bất kỳ lớp dẫn xuất nào từ B h.d = 4 ; // tốt và C. Như vậy bây giờ D sẽ chỉ có một lớp cơ sở A duy nhất, do đó h.c = 3 ; // tốt phép gán: h.b = 2 ; // tốt h.a = 1 ; h.a = 1 ; // lỗi sẽ gán 1 cho thuộc tính a của lớp cơ sở A duy nhất mà D kế thừa. } § Trong ví dụ này A là cơ sở cho cả 2 lớp cơ sở trực tiếp của D là B 6. Một số ví dụ về hàm tạo, hàm huỷ trong thừa kế nhiều mức và C. Nói cách khác có 2 lớp cơ sở A cho lớp D. Vì vậy trong câu Ví dụ 1. Ví dụ này minh hoạ cách xây dựng hàm tạo trong các lớp lệnh: dẫn xuất. Ngoài ra còn minh hoạ cách dùng các phương thức của các lớp cơ sở trong lớp dẫn xuất và cách xử lý các đối tượng thành phần.
  14. Xét 4 lớp A, B, C và D. Lớp C dẫn xuất từ B, lớp D dẫn xuất từ private: C và có thành phần là đối tượng kiểu A. int b; //CT5-06 char *str ; // Thua ke nhieu muc public: // Ham tao B() #include { #include b=0; str=NULL; #include } class A { B(int b1,char *str1) private: { int a; b=b1; str=strdup(str1); char *str ; } public: void xuat() A() { { cout << "\n" << "So nguyen lop B = " << b a=0; str=NULL; << " Chuoi lop B: " << str ; } } A(int a1,char *str1) } ; { class C : public B a=a1; str=strdup(str1); { 263 264 } private: void xuat() int c; { char *str ; cout << "\n" << "So nguyen lop A= " << a public: << " Chuoi lop A: " << str ; C():B() } { } ; c=0; str=NULL; class B } { C(int b1,char *strb,int c1, char *strc) : B(b1,strb)
  15. { u.xuat(); c=c1; str=strdup(strc); C::xuat(); } cout void xuat() #include {
  16. #include int b; class A char *str ; { public: private: B() int a; { char *str ; b=0; str=NULL; } public: B(int b1,char *str1) A() { { b=b1; str=strdup(str1); a=0; str=NULL; } } ~B() A(int a1,char *str1) { { cout <<"\n Huy B"; getch(); a=a1; str=strdup(str1); b=0; } if (str!=NULL) delete str; ~A() } { void xuat() cout <<"\n Huy A"; getch(); { a=0; cout << "\n" << "So nguyen lop B = " << b if (str!=NULL) delete str; << " Chuoi lop B: " << str ; } } void xuat() } ; { class C : public B 267 268 cout << "\n" << "So nguyen lop A= " << a { << " Chuoi lop A: " << str ; private: } int c; } ; char *str ; class B public: { C():B() private:
  17. { D():C(),u() c=0; str=NULL; { } d=0; str=NULL; C(int b1,char *strb,int c1, char *strc) : B(b1,strb) } { D(int a1, char *stra,int b1,char *strb,int c1, char *strc, c=c1; str=strdup(strc); int d1, char *strd) : u(a1,stra), C(b1,strb,c1,strc) } { ~C() d=d1; str=strdup(strd); { } cout <<"\n Huy C"; getch(); ~D() c=0; { if (str!=NULL) delete str; cout <<"\n Huy D"; getch(); } d=0; void xuat() if (str!=NULL) delete str; { } B::xuat(); void xuat() { cout << "\n" << "So nguyen lop C = " << c u.xuat(); << " Chuoi lop C: " << str ; C::xuat(); } cout << "\n" << "So nguyen lop D = " << d } ; << " Chuoi lop D: " << str ; class D : public C } { } ; private: void main() int d; { 269 270 char *str ; D *h; A u; h = new D(1,"AA",2,"BB",3,"CC",4,"DD"); public: clrscr();
  18. cout B::xuat(); thức trên để nhận địa chỉ đối tượng của lớp cơ sở mà lớp dẫn xuất thừa kế. Sau đó thực hiện phép gán trên các đối tượng này. cout C::xuat(); sau: Giả sử lớp B dân xuất từ A. Để xây dựng toán tử gán cho B, thì: cout xuat(); delete h; // Lan luot goi toi cac ham huy cua cac lop D, A, C, B class A getch(); { } A & operator=(A& h) § 7. Toán tử gán của lớp dẫn xuất { //các câu lệnh để thực hiện gán trong A 7.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 (kể cả thuộc tính thừa kế từ các lớp cơ sở) là con trỏ, thì } nhất thiết không được dùng toán tử gán mặc định, mà phải xây dựng // Phương thức nhận địa chỉ đối tượng ẩn của A cho lớp dẫn xuất một toán tử gán. A* get_A() 7.2. Cách xây dựng toán tử gán cho lớp dẫn xuất { + Trước hết cần xây dựng toán tử gán cho các lớp cơ sở return this; + Vấn đề mấu chốt là: Khi xây dựng toán tử gán cho lớp dẫn xuất } thì làm thế nào để sử dụng được các toán tử gán của lớp cơ sở. Cách giải quyết như sau: } ; - Xây dựng các phương thức (trong các lớp cơ sở) để nhận được địa chỉ của đối tượng ẩn của lớp. Phương thức này được viết đơn 2. Toán tử gán trong lớp B cần như sau: giản theo mẫu: class B : public A Tên_lớp * get_DT ( ) { { return this ; B & operator=(B& h) } { 271 272 A *u1, *u2;
  19. u1 = this->get_A(); } u2 = h.get_A(); A& operator=(A& h) *u1 = *u2 ; // Sử dụng phép gán trong A để gán các { // thuộc tính mà B kế thừa từ A this->a = h.a; //Các câu lệnh thực hiện gán các thuộc tính riêng của B if (this->str!=NULL) delete this->str; } this->str = strdup(h.str); } ; return h; } 7.3. Ví dụ void nhap() Chương trình dưới đây minh hoạ cách xây dựng toán tử gán cho { lớp D có 2 lớp cơ sở là C và B (C là lớp cơ sở trực tiếp, còn B là cơ sở của C) . Ngoài ra D còn có một thuộc tính là đối tượng của lớp A. cout > a ; //CT5-08 if (str!=NULL) delete str; // Thua ke nhieu muc cout fflush(stdin); gets(tg); #include str = strdup(tg); #include } #include void xuat() class A { { cout << "\n" << "So nguyen lop A= " << a private: << " Chuoi lop A: " << str ; int a; } char *str ; } ; public: class B { A() private: { int b; a=0; str=NULL; 273 274
  20. char *str ; cout b = h.b; c=0; str=NULL; if (this->str!=NULL) delete this->str; } this->str = strdup(h.str); C* getC() return h; { } return this; void nhap() } { C& operator=(C& h) cout > b ; { if (str!=NULL) delete str; B *b1, *b2; cout getB(); char tg[30]; b2= h.getB(); fflush(stdin); gets(tg); *b1 = *b2; str = strdup(tg); this->c = h.c; } if (this->str!=NULL) delete this->str; void xuat() this->str = strdup(h.str); { return h;
  21. } d=0; str=NULL; void nhap() } { 275 276 D& operator=(D& h) B::nhap(); { cout > c ; this->u = h.u; if (str!=NULL) delete str; C *c1,*c2; cout getC(); c2 = h.getC(); char tg[30]; *c1 = *c2; fflush(stdin); gets(tg); this->d = h.d; str = strdup(tg); if (this->str!=NULL) delete this->str; } this->str = strdup(h.str); return h; void xuat() } { void nhap() B::xuat(); { cout > d ; } ; if (str!=NULL) delete str; class D : public C cout << "\nNhap chuoi lop D: " ; { char tg[30]; private: fflush(stdin); gets(tg); int d; str = strdup(tg); char *str ; } A u; public: void xuat() D():C(),u() { { u.xuat();
  22. C::xuat(); 8.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ó cout #include § 8. Hàm tạo sao chép của lớp dẫn xuất #include
  23. #include { class A cout a = h.a; b=0; str=NULL; if (this->str!=NULL) delete this->str; this->str = strdup(h.str); } return h; B* getB() } { void nhap() return this; { } cout > a ; B& operator=(B& h) if (str!=NULL) delete str; { cout b = h.b; char tg[30]; if (this->str!=NULL) delete this->str; fflush(stdin); gets(tg); this->str = strdup(h.str); str = strdup(tg); return h; } } void xuat() void nhap() {
  24. cout > b ; { if (str!=NULL) delete str; B *b1, *b2; cout getB(); char tg[30]; b2= h.getB(); fflush(stdin); gets(tg); *b1 = *b2; str = strdup(tg); this->c = h.c; } if (this->str!=NULL) delete this->str; void xuat() this->str = strdup(h.str); 281 282 { return h; cout > c ; { if (str!=NULL) delete str; private: cout << "\nNhap chuoi lop C: " ; int c; char tg[30]; char *str ; fflush(stdin); gets(tg); public: str = strdup(tg); C():B() } { c=0; str=NULL; void xuat() } { C* getC() B::xuat(); { cout << "\n" << "So nguyen lop C = " << c return this; << " Chuoi lop C: " << str ; } } C& operator=(C& h) } ;
  25. class D : public C { { u.nhap(); private: C::nhap(); int d; cout > d ; char *str ; if (str!=NULL) delete str; A u; cout u = h.u; } ; C *c1,*c2; void main() c1 = this->getC(); { c2 = h.getC(); D h1; *c1 = *c2; clrscr(); this->d = h.d; h1.nhap(); if (this->str!=NULL) delete this->str; D h2(h1); this->str = strdup(h.str); cout<<"\n\nH2:"; return h; h2.xuat(); } h1.nhap(); void nhap() cout<<"\n\nH2:";
  26. h2.xuat(); //CT5-10 cout getch(); #include } #include #include § 9. Phát triển, hoàn thiện chương trình #include Có thể dùng tính thừa kế để phát triển khả năng của chương trình. class DIEM { 9.1. ý tưởng của việc phát triển chương trình như sau: Sau khi đã xây dựng được một lớp, ta sẽ phát triển lớp này bằng cách xây private: một lớp dân xuất trong đó đưa thêm các thuộc tính và phương thức int x,y; mới. Quá trình trên lại tiếp tục với lớp vừa nhận được. Ta cũng có public: 285 286 thể xây dựng các lớp mới có thuộc tính là đối tượng của các lớp cũ. DIEM() Bằng cách này, sẽ nhận được một dẫy các lớp càng ngày càng hoàn thiện và có nhiều khả năng hơn. { x=y=0; 9.2. Ví dụ về việc phát triển chương trình } Giả sử cần xây dựng chương trình vẽ một số hình phẳng. Chúng DIEM(int x1, int y1) ta có thể phát triển chương trình này như sau: { Đầu tiên định nghĩa lớp DIEM (Điểm) gồm 2 thuộc tính x, y. Từ x=x1; y=y1; lớp DIEM xây dựng lớp DUONG_TRON (Đương tròn) bằng cách } bổ sung 2 biến nguyên là r (bán kính) và md (mầu đường). Từ lớp DUONG_TRON xây dựng lớp HINH_TRON bằng cách thêm vào DIEM(DIEM &d) biến nguyêm mt (mầu tô). Đi theo một nhánh khác: Xây dựng lớp { DOAN_THANG (Đoạn thẳng) gồm 2 đối tượng kiểu DIEM, và lớp this->x= d.x; TAM_GIAC gồm 3 đối tượng DIEM. this->y= d.y; Chương trình dưới đây cho phép vẽ các đường tròn, hình tròn, } đoạn thẳng và hình tam giác. int operator[](int i) Chương trình còn minh hoạ cách dùng con trỏ this trong lớp dẫn xuất để thực hiện các phương thức của lớp co sở. Ngoài ra còn minh { hoạ cách dùng toán tử chỉ số [] để nhận các toạ độ x, y từ các đối if (i==1) return x; tượng của lớp DIEM. else return y;
  27. } int mt; }; public: class DUONG_TRON : public DIEM HINH_TRON() : DUONG_TRON() { { private: mt=0; int r,md; } public: HINH_TRON(DIEM d, int r1, int md1, int mt1) : DUONG_TRON(d,r1,md1) DUONG_TRON() : DIEM() { { mt=mt1; r=md=0; } } void ve() DUONG_TRON(DIEM d, int r1, int md1) : DIEM(d) { { 287 288 DUONG_TRON::ve(); r=r1; md=md1; setfillstyle(1,mt); } floodfill((*this)[1],(*this)[2],this->getmd()); void ve() } { } ; setcolor(md); class DOAN_THANG circle ( (*this)[1],(*this)[2],r); { private: } DIEM d1, d2; int getmd() int md; { public: return md; DOAN_THANG() : d1(), d2() } { } ; md=0; class HINH_TRON : public DUONG_TRON } { DOAN_THANG(DIEM t1, DIEM t2, int md1) private: {
  28. d1=t1; d2 = t2; md=md1; floodfill((d1[1]+d2[1]+d3[1])/3,(d1[2]+d2[2]+d3[2])/3, } md); void ve() } { } ; setcolor(md); void ktdh() line(d1[1],d1[2] ,d2[1],d2[2]); { } int mh=0,mode=0; } ; initgraph(&mh,&mode,""); class TAM_GIAC } { void main() private: { DIEM d1,d2,d3; ktdh(); int md, mt; DUONG_TRON dt(DIEM(100,100),80,MAGENTA); public: 289 290 HINH_TRON ht(DIEM(400,100),80,RED,YELLOW); TAM_GIAC(): d1(), d2(), d3() DOAN_THANG t(DIEM(100,100),DIEM(400,100),BLUE); { TAM_GIAC tg(DIEM(250,150), DIEM(100,400), md=mt=0; DIEM(400,400), CYAN, CYAN); } TAM_GIAC(DIEM t1,DIEM t2,DIEM t3,int md1,int mt1) dt.ve(); { ht.ve(); d1=t1; d2=t2; d3 = t3; md=md1;mt=mt1; t.ve(); } tg.ve(); void ve() getch(); { closegraph(); DOAN_THANG(d1,d2,md).ve(); } DOAN_THANG(d1,d3,md).ve(); DOAN_THANG(d2,d3,md).ve(); setfillstyle(1,mt); § 10. Bổ sung, nâng cấp chương trình
  29. Có thể dùng tính thừa kế để sửa đổi, bổ sung, nâng cấp chương MON_HOC *mh ; //Danh sach cac mon hoc trình. public: 10.1. ý tưởng của việc nâng cấp chương trình như sau: Giả sử đã GV() ; // Hàm tạo có một chương trình hoạt động tốt. Bây giờ cần có một số bổ sung ~GV() ; //Hàm huỷ thay đổi không nhiều lắm. Có 2 giải pháp: Hoặc sửa chữa các lớp int getsm() ; // Cho biết số môn (dùng trong BM::sapxep) đang hoạt động ổn định, hoặc xây dựng một lớp dẫn xuất để thực hiện các bổ sung, sửa chữa trên lớp này. Rõ ràng giải pháp thứ 2 tỏ const GV& operator=(const GV& g); // Gán (dùng trong ra hợp lý hơn . // BM::sapxep) void nhap(); // Nhập 10.2. Ví dụ void xuat(); // Xuất Giả sử đã có chương trình quản lý giáo viên gồm 3 lớp MON_HOC (Môn học), GV (Giáo viên) và BM (Bộ môn) : } ; class MON_HOC class BM // Bo mon { { private: private: char tenmh[20] ; // Tên môn học 291 292 char tenbm[20]; // Tên bộ môn int sotiet ; // Số tiết int n; // So giao vien public: GV *gv; // Danh sach giao vien MON_HOC() ; // Hàm tạo public: const MON_HOC& operator=(const MON_HOC& m) ; BM() // Hàm tạo // Gán void nhap(); // Nhập void nhap() ; // Nhập void xuat(); // Xuất void xuat() ; // Xuất void sapxep(); // Sắp xếp } ; } ; class GV Chương trình cho phép: { 1. Nhập danh sách giáo viên của bộ môn. private: 2. Sắp xếp danh sách giáo viên theo thứ tự giảm của số môn mà char ht[25]; // Ho ten mỗi giáo viên có thể giảng dậy. int ns; // Nam sinh 3. In danh sách giáo viên. int sm; // So mon hoc co the day
  30. Nội dung chương trình như sau: } //CT5-11 void xuat() // Nang cap chuong trinh { // CT ban dau cout } #include } ; #include class GV class MON_HOC { { private: private: char ht[25]; // Ho ten char tenmh[20]; int ns; // Nam sinh int sotiet; public: int sm; // So mon hoc co the day MON_HOC() MON_HOC *mh ; //Danh sach cac mon hoc { 293 294 public: tenmh[0]=sotiet=0; GV() } { const MON_HOC& operator=(const MON_HOC& m) ht[0]= ns= sm= 0 ; { mh = NULL; strcpy(this->tenmh,m.tenmh); } this->sotiet = m.sotiet; return m; ~GV() } { void nhap() ht[0]= ns= sm= 0 ; { if (mh) delete mh; cout > sotiet; {
  31. return sm; cin >> sm; } if (this->mh) delete this->mh; const GV& operator=(const GV& g); if(sm) void nhap(); { void xuat(); this->mh = new MON_HOC[sm+1]; } ; for (int i=1; i mh[i].nhap(); { } strcpy(this->ht,g.ht); } this->ns=g.ns; void GV::xuat() int n = g.sm; { this->sm = n; cout mh) delete this->mh; cout mh = new MON_HOC[n+1]; cout mh[i] = g.mh[i]; this->mh[i].xuat(); } } return g; } } class BM // Bo mon void GV::nhap() { { private: cout > ns; public: cout << "So mon co the giang day: " ; BM()
  32. { cout > n; { tg=gv[i]; gv[i]=gv[j]; gv[j]=tg; if (gv) delete gv; } if (n) } { gv = new GV[n+1]; void main() 297 298 for (int i=1; i<=n; ++i) { BM b; gv[i].nhap(); b.nhap(); } b.sapxep(); } b.xuat(); void BM::xuat() getch(); { } cout << "\nBo mon: " << tenbm; Vấn đề đặt ra là: Hiện nay các giáo viên đã bắt đầu hướng dẫn cout << "\nSo giao vien: " << n; luận văn tốt nghiệp cho sinh viên. Vì vậy cần bổ sung thông tin này if (n) vào phần dữ liệu giáo viên. Để nâng cấp chương trình chúng ta sẽ định nghĩa lớp GV2 dẫn xuất từ lớp GV, sau đó trong lớp BM sẽ {
  33. thay GV bằng GV2. Có 2 chỗ cần bổ sung và một chỗ cần sửa đổi LV *lv; // Danh sách luận văn như sau: public: 1. Bổ sung trong lớp GV phương thức: GV2(); // Hàm tạo GV* getGV() ~GV2() ; // Hàm huỷ { GV2& operator=(GV2& g); // Gán return this; void nhap(); // Nhập } void xuat(); // Xuất Phương thức này dùng để xây dựng toán tử gán cho lớp GV2. } ; 2. Trong lớp BM thay GV bằng GV2. Điều này có thể đạt được bằng sửa chữa trực tiếp hoặc bằng một câu lệnh #define : Chương trình nâng cấp như sau: #define GV GV2 //CT5-12B 3. Định nghĩa thêm 2 lớp: LV (Luận văn) và GV2 (Lớp GV2 dẫn // Nang cap chuong trinh xuất từ lớp GV) như sau: // CT nang cap class LV // Luan van #include { #include private: #include char tenlv[30]; // Ten luan van #include char tensv[25]; // Ten sinh vien class MON_HOC int nambv; // Nam bao ve luan van { public: private: LV() ; // Hàm tạo 299 300 const LV& operator=(const LV& l) ; // Gán char tenmh[20]; void nhap() ; // Nhập int sotiet; void xuat() ; public: } ; MON_HOC() class GV2 : public GV { { tenmh[0]=sotiet=0; private: } int solv; // Số luận văn đã hướng dẫn const MON_HOC& operator=(const MON_HOC& m)
  34. { GV() strcpy(this->tenmh,m.tenmh); { this->sotiet = m.sotiet; ht[0]= ns= sm= 0 ; return m; mh = NULL; } } void nhap() ~GV() { { cout > sotiet; // Bo sung phuong thuc getGV } GV* getGV() void xuat() { { return this; cout ht,g.ht); int sm; // So mon hoc co the day MON_HOC *mh ; //Danh sach cac mon hoc this->ns=g.ns; public: int n = g.sm;
  35. this->sm = n; { if (this->mh) delete this->mh; cout mh = new MON_HOC[n+1]; if (sm) for (int i=1; i mh[i] = g.mh[i]; cout mh[i].xuat(); return g; } } } void GV::nhap() { // Bo sung cac lop LV va GV2 cout > ns; char tenlv[30]; // Ten luan van cout > sm; int nambv; // Nam bao ve luan van if (this->mh) delete this->mh; public: if (sm) LV() { { this->mh = new MON_HOC[sm+1]; tenlv[0]=tensv[0] = nambv = 0; for (int i=1; i mh[i].nhap(); const LV& operator=(const LV& l) } 303 304 { } strcpy(this->tenlv,l.tenlv); void GV::xuat() strcpy(this->tensv,l.tensv);
  36. this->nambv = l.nambv ; solv = 0 ; return l; lv = NULL; } } void nhap() ~GV2() { { cout > nambv ; GV2& GV2::operator=(GV2& g) } { void xuat() GV *g1, *g2; { g1 = this->getGV(); cout solv = n; } ; if (this->lv) delete this->lv; class GV2 : public GV if (n) { private: { int solv; this->lv = new LV[n+1]; LV *lv; for (int i=1; i lv[i] = g.lv[i]; GV2():GV() } { 305 306 return g;
  37. } class BM // Bo mon void GV2::nhap() { { private: GV::nhap(); char tenbm[20]; cout > solv; GV *gv; // Danh sach giao vien if (this->lv) delete this->lv; public: BM() if (solv) { { tenbm[0] = n = 0; this->lv = new LV[solv+1]; gv = NULL; for (int i=1; i lv[i].nhap(); void nhap(); } void xuat(); } void sapxep(); void GV2::xuat() } ; { void BM::nhap() { GV::xuat(); cout > n; cout lv[i].xuat(); { } gv = new GV[n+1]; } for (int i=1; i<=n; ++i) // Sua lop BM: thay GV bang GV2 gv[i].nhap(); #define GV GV2 } 307 308
  38. } b.nhap(); void BM::xuat() b.sapxep(); { b.xuat(); 309 310 cout << "\nBo mon: " << tenbm; getch(); cout << "\nSo giao vien: " << n; } if (n) { § 11. Từ khái quát đến cụ thể cout << "\n Danh sach giao vien cua bo mon:"; Tính thừa kế cũng thường dùng để thiết kế các bài toán theo for (int i=1; i<=n; ++i) hướng từ khái quát đến cụ thể, từ chung đến riêng. Đầu tiên đưa ra gv[i].xuat(); các lớp để mô tả những đối tượng chung, sau đó dẫn xuất tới các đối tượng ngày một cụ thể hơn. } Một trường hợp khác cũng thường gặp là: Quản lý nhiều thực thể } có những phần dữ liệu chung. Khi đó ta có thể xây dựng một lớp cơ void BM::sapxep() sở gồm các phần dữ liệu chung. Mỗi thực thể sẽ được mô tả bằng { một lớp dẫn xuất từ lớp cơ sở này. GV tg; Sau đây là một số ví dụ minh hoạ: int i,j; Ví dụ 1 (minh hoạ tư tưởng đi từ khái quát đến cụ thể) : Giả sử if (n) cần quản lý sinh viên của một trường đại học. Khi đó ta có thể bắt đầu từ lớp SINH_VIEN (Sinh viên). Sau đó dùng nó làm cơ sở để for (i=1;i<n;++i) dẫn xuất tới các lớp mô tả các đối tượng sinh viên cụ thể hơn, ví dụ: for (j=i+1;j<=n;++j) SV Tin, SV Toán, SV Luật, SV Du lịch, if (gv[i].getsm()<gv[j].getsm()) Các bài toán kiểu như vậy rất thường gặp trong thực tế. { Ví dụ 2 (minh hoạ phần chung của nhiều thực thể). Giả sử cần tg=gv[i]; gv[i]=gv[j]; gv[j]=tg; xây dựng phần mềm để thực hiện các phép tính về ma trân vuông và véc tơ cấp n. Ta có nhận xét là n chung cho cả véc tơ và ma trận. } Hơn nữa nó còn chung cho tất cả các ma trận và véc tơ cùng xét } trong bài toán. Vì vậy có thể định nghĩa một lớp cơ sở chỉ có một #undef GV thuộc tính tĩnh (static) n. Các lớp ma trận, véc tơ dẫn xuất từ lớp này và sử dụng chung cùng một giá trị n. void main() Dưới đây là chương trình thực hiện các phép toán ma trận, véc tơ. { Chương trình được tổ chức thành 3 lớp: BM b;
  39. Lớp CAP (Cấp ma trận, véc tơ) gồm một thành phần tĩnh n và cout > n; phương thức nhập n. } Lớp VT (Véc tơ) có một thuộc tính là mảng một chiều (chứa các else phần tử của véc tơ) và các phương thức nhập, xuất. { Lớp MT (Ma trận) có một thuộc tính là mảng 2 chiều (chứa các 311 312 cout > n; // Dùng thuộc tính static } #include } } #include int getN() #include { #include return n; class CAP; } class MT; } ; class VT; int CAP::n=0; class CAP class MT : public CAP { { private: private: static int n; double a[20][20]; public: public: void nhap() void nhap(); { void xuat(); int ch; VT operator*(VT x); if (n==0) }; {
  40. class VT : public CAP n = this->getN(); { if (n) private: for (int i=1; i getN(); n = this->getN(); if (n==0) for (i=1; i CAP::nhap(); for (j=1; j getN(); y.x[i] += a[i][j]*x.x[j]; } } for (i=1; i > a[i][j]; { } int n,i; } n = this->getN(); void MT::xuat() if (n==0) { { int n,i,j; this->CAP::nhap();
  41. n = this->getN(); cout > x[i]; cout getN(); § 12. Toàn thể và bộ phận if (n) Thông thường khi xem xét, giải quyết một bài toán, ta thường { chia nó thành các bài toán nhỏ hơn. Nói cách khác: Một bài toán lớn cout << "\n"; bao gồm nhiều bài toán bộ phận. Khi đó ta có thể định nghĩa các lớp for (int i=1; i<=n; ++i) cho các bài toán bộ phận. Lớp cho bài toán chung được dẫn xuất từ các lớp nói trên. { Xét một thí dụ đơn giản là bài toán quản lý thư viện. Nó gồm 2 cout << x[i] << " "; bài toán bộ phận là quản lý sách và quản lý đọc giả. Chúng ta sẽ xây } dựng lớp SACH và lớp DOC_GIA. Sau đó dùng các lớp này làm cơ sở để xây dựng lớp THU_VIEN. } } void main() { MT a; VT x,y; clrscr(); cout<<"\nNhap ma tran A:"; a.nhap();