Lập trình hướng đối tượng C++ (Phần 2) - Đặng Ngọc Hoàng Thành

pdf 111 trang Gia Huy 17/05/2022 2310
Bạn đang xem 20 trang mẫu của tài liệu "Lập trình hướng đối tượng C++ (Phần 2) - Đặng Ngọc Hoàng Thành", để 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:

  • pdflap_trinh_huong_doi_tuong_c_phan_2_dang_ngoc_hoang_thanh.pdf

Nội dung text: Lập trình hướng đối tượng C++ (Phần 2) - Đặng Ngọc Hoàng Thành

  1. Chương 13. Lập trình hướng đối tượng CHƯƠNG 13. LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG Lịch sử hình thành Trước khi kĩ thuật lập trình hướng đối tượng ra đời, con người đ~ trải qua các thời kì lập trình tuyến tính, lập trình hướng thủ tục. 1. Lập trình tuyến tính M|y tính đầu tiên được lập trình bằng mã nhị phân, sử dụng các công tắc cơ khí để nạp chương trình. Cùng với sự xuất hiện của các thiết bị lưu trữ lớn và bộ nhớ m|y tính có dung lượng lớn, nên các ngôn ngữ lập trình cấp cao bắt đầu xuất hiện. Các ngôn ngữ lập trình n{y được thiết kế làm cho công việc lập trình trở nên đơn giản hơn. C|c chương trình ban đầu chủ yếu liên quan đến tính toán, chúng tương đối ngắn. Chúng chủ yếu chạy theo các dòng lệnh một cách tuần tự, dòng trước chạy trước, dòng sau chạy sau. Nhược điểm: o Nếu ta cần sử dụng một đoạn lệnh n{o đó nhiều lần, thì ta phải sao chép nó nhiều lần. o Không có khả năng kiểm soát phạm vi nhìn thấy của biến. o Chương trình dài dòng, khó hiểu, khó nâng cấp. 2. Lập trình hướng thủ tục Với những nhược điểm trên, đòi hỏi có một ngôn ngữ lập trình mới thay thế. Đó chính l{ nguyên nh}n ra đời của ngôn ngữ lập trình hướng thủ tục. Về bản chất, chương trình được chia nhỏ thành c|c modul (đơn vị chương trình). Mỗi đơn vị chương trình chứa các hàm hay thủ tục (nên gọi là hướng thủ tục). Tuy tách rời thành các modul riêng biệt, nhưng ngôn ngữ lập trình hướng thủ tục vẫn đảm bảo thông tin thông suốt giữa các modul nhờ v{o cơ chế hoạt động của h{m, cơ chế truyền theo tham biến và tham trị. Với lập trình hướng thủ tục, một chương trình lớn có thể được chia nhỏ Trang | 120
  2. Chương 13. Lập trình hướng đối tượng th{nh c|c modul, để mỗi lập trình viên có thể đảm nhận. Tiêu biểu trong số này là C, Pascal. Nhược điểm: o Các hàm và thủ tục thường gắn kết với nhau, nếu muốn nâng cấp chương trình, thường phải chỉnh sửa tất cả các hàm và thủ tục liên quan. o Không phù hợp với xu thế hiện đại vì không mô tả được thực thể trong cuộc sống thực. 3. Lập trình hướng đối tượng a. Giới thiệu Với xu thế hiện đại, ngôn ngữ lập trình hướng đối tượng đ~ ra đời. Cơ sở của lập trình hướng đối tượng l{ đối tượng. Đối tượng là sự thể hiện của một thực thể trong thế giới thực. Một thực thể trong thế giới thực thường có: c|c đặc trưng v{ c|c h{nh động. Ví dụ: con người trong thế giới thực có c|c đặc trưng như - tên gọi, tuổi, màu tóc, màu mắt, m{u da v{ c|c h{nh động như – ăn, nói, chạy, nhảy Cách thức lập trình này mô tả một cách chính xác các sự vật, con người trong thế giới thực. Bây giờ, ta sẽ xét một vài ví dụ để cho thấy sự cần thiết của lập trình hướng đối tượng. Ví dụ 1. Chúng ta muốn xây dựng một chương trình quản lý sinh viên. Khi đó, ta cần lưu trữ c|c thông tin liên quan đến đối tượng sinh viên này: họ tên sinh viên, mã số sinh viên, ng{y th|ng năm sinh, quê qu|n, điểm các môn, điểm tổng kết, . v{ rất nhiều thông tin khác liên quan. Sau khi kết thúc năm học, sinh viên sẽ nhận được đ|nh gi| kết quả học tập của mình. Chúng ta cần có phương thức tiếp nhận kết quả để sinh viên đó có thể phản ứng lại với những gì mà mình nhận được, họ phải thực hiện c|c h{nh động học tập, tham gia vào các hoạt động của trường, của khoa đó l{ những h{nh động mà mỗi sinh viên cần thực hiện. Ví dụ 2. Chúng ta sẽ điểm qua một số tính năng trong chương trình soạn thảo văn bản Word của Microsoft. Chúng ta sẽ thảo luận về c|c đối tượng Drawing trong Word. Mỗi đối tượng đều có các thuộc tính: màu viền, dạng đường viền, kích thước viền, màu sắc viền, màu nền, có văn bản hay không trong đối tượng drawing Khi chúng ta biến đổi hình dạng của mỗi đối C++ T r a n g | 121
  3. Chương 13. Lập trình hướng đối tượng tượng: kéo giãn, làm lệch xiêng, quay vòng chúng ta cần đưa ra một thông điệp để c|c đối tượng hình thể n{y thay đổi theo. C|c h{nh động này thuộc quyền sở hữu của đối tượng. Trong hai ví dụ minh họa trên, chúng ta thấy rằng hướng tiếp cận theo lập trình hướng đối tượng là rất gần gũi với cuộc sống thực. Chúng ta không quan t}m đến những khía cạnh không cần thiết của đối tượng, chúng ta chỉ tập trung v{o c|c đặc trưng v{ c|c h{nh động của đối tượng. Kể từ thời điểm này trở đi, chúng ta sẽ gọi các đặc trưng của đối tượng là các thuộc tính thành viên của đối tượng đó (hoặc dữ liệu thành viên, biến thành viên của đối tượng) v{ c|c h{nh động của đối tượng l{ c|c phương thức thành viên (hay hàm thành viên) của đối tượng. Các cách gọi dữ liệu thành viên, thuộc tính thành viên, biến thành viên hay thuộc tính (tương ứng phương thức th{nh viên, h{m th{nh viên, phương thức) là không có sự phân biệt. Tôi chỉ đưa ra nhiều cách gọi kh|c nhau để chúng ta có thể quen khi tham khảo các giáo trình khác nhau. Bởi lẽ, nhiều giáo trình chọn lựa các cách gọi khác nhau. Các cách gọi n{y cũng tùy thuộc vào ngôn ngữ lập trình (trong C++ thông thường người ta sử dụng khái niệm dữ liệu thành viên – member data hoặc biến thành viên – member variable và hàm thành viên – member function, trong khi đó, c|c ngôn ngữ như Java, Delphi hay C# lại sử dụng khái niệm phương thức – method và thuộc tính – property). Khái niệm thành viên sẽ áp dụng cho cả dữ liệu thành viên lẫn hàm thành viên. Phương ch}m của lập trình hướng thủ tục theo gi|o sư Niklaus Wirth Chương trình = Cấu trúc dữ liệu + Giải thuật Còn phương ch}m của lập trình hướng đối tượng là Chương trình = Đối tượng + Dữ liệu Tiêu biểu trong số này là C++, Java, C#, Delphi, Python b. Phương pháp phân tích và thiết kế hướng đối tượng Trước khi bắt đầu viết một chương trình theo hướng đối tượng, thì ta cần phân tích và thiết kế c|c đối tượng. Từ sơ đồ cấu trúc nhận được, chúng ta có thể xây dựng nên chương trình. Chi tiết về cách thức phân tích và thiết kế đối tượng, chúng ta sẽ được tìm hiểu kĩ hơn trong học phần phân tích thiết kế hệ thống thông tin. Trong nội dung của giáo trình này, chúng ta chỉ thảo luận một phần nhỏ, để giúp các bạn có thể xây dựng nên một cấu trúc chương trình theo hướng đối tượng. Sơ đồ cấu trúc trong lập trình hướng đối tượng được sử dụng phổ biến l{ sơ đồ mô tả trên ngôn ngữ UML (Unified Modeling Languages). UML là ngôn ngữ chuyên dùng để mô hình C++ T r a n g | 122
  4. Chương 13. Lập trình hướng đối tượng hóa c|c đối tượng. Nó không chỉ được áp dụng trong lập trình, m{ còn được sử dụng rộng r~i trong c|c lĩnh vực khác của cuộc sống. Trong UML có nhiều dạng sơ đồ được hoạch định. Nhưng trong phạm trù của lập trình hướng đối tượng, sơ đồ lớp là sự mô tả gần gũi nhất. Do đó, tôi sẽ trình bày cách xây dựng một chương trình được mô tả bằng sơ đồ lớp. Một số kí hiệu cần lưu ý trong UML. Trước khi tìm hiểu cách mô hình hóa một bài toán trong UML, chúng ta cần tìm hiểu một số kí hiệu được sử dụng trong UML. Các kí hiệu này có thể khác nhau trong nhiều chương trình mô phỏng. Những kí hiệu mà tôi sử dụng ở đ}y l{ kí hiệu dùng trên Visual Studio 2010. Kí hiệu Ý nghĩa Lớp Thuộc tính hoặc phương thức private Thuộc tính hoặc phương thức protected Thuộc tính hoặc phương thức public Biểu diễn tính kế thừa. Mũi tên luôn hướng về lớp cơ sở. Chiều còn lại luôn chỉ vào lớp con. Thuộc tính Phương thức Phân tích và thiết kế mô hình. Việc phân tích và thiết kế một mô hình là công việc đòi hỏi các nhà thiết kế phải có một tư duy tốt. Đối với một bài toán phân tích và thiết kế, không phải chỉ có duy nhất một mô hình kết quả, mà có thể có một vài, thậm chí là nhiều mô hình khác nhau. Tuy nhiên, công việc chọn lựa một mô hình tối ưu l{ điều cần thiết. Trong nội dung giáo trình này, tôi chỉ giới thiệu sơ qua về cách hoạch định một mô hình. Chúng ta sẽ không đi sâu nghiên cứu vấn đề này. Trong học phần phân tích và thiết kế hệ thống thông tin sẽ trình bày chi tiết và cụ thể hơn. Các bước phân tích và thiết kế. Để phân tích và thiết kế một mô hình hướng đối tượng, cần thực hiện c|c giai đoạn sau đ}y: C++ T r a n g | 123
  5. Chương 13. Lập trình hướng đối tượng - Bước 1. Mô tả bài toán. Một bài toán sẽ được miêu tả dưới dạng ngôn ngữ tự nhiên. Nó chủ yếu dựa vào yêu cầu của khác hàng và sự trợ giúp khách hàng. - Bước 2. Đặc tả các yêu cầu. Sau khi phân tích các nhân tố tham gia vào trong mô hình, ta cần tiến h{nh xem xét c|c t|c nh}n t|c động vào từng nhân tố. Mối quan hệ giữa các nhân tố - Bước 3. Trích chọn đối tượng. Sau khi tổng hợp các tác nhân và nhân tố trong mô hình. Chúng ta cần tiến hành lựa chọn chúng. Việc loại bỏ các nhân tố và tác nhân không cần thiết là rất quan trọng. Nó sẽ giúp cho mô hình tập trung vào các nhân tố quan trọng và cần thiết, tránh phân tích và thiết kế tràn lan. - Bước 3. Mô hình hóa các lớp đối tượng. Sau khi chọn lựa c|c đối tượng cần thiết. Chúng ta phân tích từng đối tượng. Khi ph}n tích đối tượng, ta cần lưu ý tập trung vào những thứ cốt lõi của mỗi đối tượng, tr|nh đưa v{o những phương thức và thuộc tính không cần thiết, không quan trọng của đối tượng – đó chính l{ tính trừu tượng hóa của dữ liệu. Khi phân tích, chúng ta cũng cần lưu ý đến các tính chất chung của từng đối tượng. Nếu c|c đối tượng có nhiều tính chất chung, chúng ta nên xây dựng một đối tượng mới, chứa các tính chất chung đó, mỗi đối tượng còn lại sẽ thừa kế từ đối tượng n{y, để nhận được các tính chất chung. - Bước 4. Thiết kế từng đối tượng. Chúng ta cần đảm bảo rằng, mỗi đối tượng phải có c|c phương thức và thuộc tính riêng lẫn c|c phương thức và thuộc tính chia sẻ. C|c phương thức riêng chỉ có bản th}n đối tượng mới có quyền thay đổi. C|c phương thức chia sẻ có thể được truy cập bởi đối tượng khác theo các mức khác nhau. - Bước 5. Xây dựng và kiểm thử mô hình. Bắt tay vào xây dựng mô hình. Ở đ}y, chúng ta sử dụng ngôn ngữ UML để mô tả. Sau khi xây dựng xong mô hình, cần tiến hành kiểm thử mô hình. Kiểm thử các mô hình trong các tình huống thực tế là cần thiết để đảm bảo rằng mô hình nhận được là phù hợp, trước khi bắt tay vào viết chương trình. Trên đ}y, chỉ là những bước đề nghị để chúng ta có một cái nhìn tổng quát khi phân tích và thiết kế. Có thể có nhiều c|ch để phần tích và thiết kế một mô hình. Nhưng hãy luôn đảm bảo rằng, mô hình thu được không những đạt hiệu quả cao, m{ còn đảm bảo rằng nó phải dễ dàng bảo trì và nâng cấp. Mỗi khi có một lỗi xuất hiện, chúng ta cũng cần biết khoanh vùng để thu nhỏ phạm vi phát hiện lỗi. Chúng ta sẽ lấy một ví dụ nhỏ. Ph}n tích hướng đối tượng mô hình quản lý cửa hàng bán xe đạp. Trong mô hình này, ta cần quản lý c|c nhóm đối tượng sau: đối tượng xe đạp, đối tượng chi nhánh bán hàng, đối tượng kh|ch h{ng v{ đối tượng nhân viên bán hàng. - Đối tượng xe đạp: chúng ta cần quản lý mã số xe (mã số gồm hai phần: phần id chi nhánh bán hàng + mã số vạch), loại xe, màu sắc, gi| b|n, nước C++ T r a n g | 124
  6. Chương 13. Lập trình hướng đối tượng sản xuất (các thuộc tính chung). Đối tượng xe đạp địa hình: số b|nh răng, c|ch lên răng (bằng tay/tự động), chống sooc. Đối tượng xe đạp du lịch: xe đơn/đôi, tự động (hỗ trợ tự chạy bằng điện hay không), chiếu sáng (có/không). Xe đua thể thao: điều chỉnh độ cao (có/không), các chế độ đạp (đạp thư giản, đạp tăng tốc, đạp chậm ). - Đối tượng khách hàng và nhân viên bán hàng: họ và tên, số CMND. Đối tượng khách hàng: cách thức thanh toán (tiền mặt/chuyển khoản), cách thức giao hàng (nhận tại chỗ/ đưa đến tận nh{). Đối tượng nhân viên bán h{ng: id chi nh|nh b|n h{ng, ng{y th|ng năm sinh, quê qu|n, địa chỉ, mã số thuế, lương - Đối tượng chi nh|nh b|n h{ng: id chi nh|nh b|n h{ng, địa chỉ. Nếu yêu cầu quản lý nhiều hơn c|c thông tin của từng đối tượng, khi đó ta cần bổ sung thêm các thuộc tính tương ứng này. Đối với c|c phương thức thực thi h{nh động, tương ứng với mỗi thuộc tính, ta có hai phương thức để chỉ định và tiếp nhận. Ví dụ, đối tượng nhân viên, có họ v{ tên. Tương ứng với thuộc tính n{y, ta có phương thức chỉ định để đặt tên cho nh}n viên (đặt tên là thiết lập tên gọi trong phần mềm quản lý) và tiếp nhận tên khi có yêu cầu. Đối tượng kh|ch h{ng có phương thức quyết định (quyết định thực hiện giao dịch). Đối tượng nh}n viên b|n h{ng có phương thức tiếp nhận (nhận giao dịch). Đối tượng địa điểm b|n h{ng có phương thức nhận hàng (nếu h{ng còn đầy thì không tiếp nhận thêm). Theo như ph}n tích ở trên, đối tượng xe đạp l{ đối tượng chung. C|c đối tượng xe đạp thể thao, xe đạp du lịch, xe đạp địa hình kế thừa từ lớp xe đạp. Đối tượng con người để quản lý thông tin chung. V{ c|c đối tượng nhân viên và khách hàng thừa kế từ lớp con người. Cuối cùng l{ đối tượng chi nh|nh b|n h{ng. Theo như c|ch ph}n tích n{y, ta có sơ đồ lớp như sau: Hình 20 – Minh họa sơ đồ lớp C++ T r a n g | 125
  7. Chương 13. Lập trình hướng đối tượng Trong sơ đồ n{y, c|c phương thức và thuộc tính của mỗi lớp đối tượng như đ~ ph}n tích ở trên. Để tr|nh rườm r{, c|c phương thức chỉ biểu diễn sơ lược. Lớp và đối tượng Lớp là sự biểu diễn của đối tượng trong lập trình v{ ngược lại đối tượng là sự thể hiện của lớp. Một đối tượng gồm có: thuộc tính v{ phương thức. Chúng ta có thể xem một lớp như l{ một kiểu dữ liệu, còn đối tượng là biến. Lớp được khai báo nhờ từ khóa class class tên_lớp{ các_thuộc_tính các_phương_thức } [tên_đối_tượng]; tên_lớp: là tên của lớp. tên_đối_tượng: là tên của đối tượng. Khai báo lớp tương đối giống khai báo struct. Các thuộc tính được khai b|o như khai b|o biến. C|c phương thức được khai b|o như khai b|o h{m. Chúng có thể được chỉ định bằng một trong ba từ khóa: private, protected và public. o private: các thành viên (biến thành viên hoặc hàm thành viên) của cùng một lớp hoặc từ một hàm bạn của nó có thể truy cập. o protected: các thành viên của cùng một lớp hoặc từ một hàm bạn của nó hoặc từ một lớp dẫn xuất của nó hoặc bạn của một lớp dẫn xuất của nó đều có thể truy cập. Mức truy cập lớn nhất trong trường hợp này là bạn của lớp dẫn xuất. Chúng ta sẽ thảo luận thêm trong những phần sau. o public: các thành viên có thể truy cập lẫn nhau từ mọi lớp. Theo mặc định, nếu không chỉ định từ khóa, thì private được ấn định. class Humans{ string name; int age; public: void setName(string); void setAge(string); string getName(void); int getAge(void); C++ T r a n g | 126
  8. Chương 13. Lập trình hướng đối tượng }man; Humans là tên lớp, chứa các thuộc tính l{ name v{ age, chúng không được chỉ định từ khóa, nên private sẽ được sử dụng. C|c phương thức setName, setAge, getName và getAge được chỉ định là public. Trong trường hợp này, man là một đối tượng thể hiện của lớp Humans. Sau khi khai báo lớp, ta cần bổ sung phần thân lớp – tương ứng với các hàm thành viên. Hoặc ta có thể bổ sung trực tiếp vào trong lớp – tương tự khai báo hàm trực tiếp, hoặc sử dụng khai báo prototype. Đối với khai báo prototype, để x|c định một phương thức là của một lớp n{o đó, ta sử dụng toán tử phạm vi :: theo sau tên lớp. Chương trình Kết quả #include The man: Jack, age 21 using namespace std; class Humans{ string name; int age; public: void setName(string); void setAge(int); string getName(void); int getAge(void); }; void Humans::setName(string s){ name = s; } void Humans::setAge (int a){ age = a; } string Humans::getName(void){ return name; } int Humans::getAge(void){ return age; } int main(){ Humans man; man.setName(“Jack”); man.setAge(21); cout<<”The man: “<<man.getName()<<”, age C++ T r a n g | 127
  9. Chương 13. Lập trình hướng đối tượng “ Name and age of Student using namespace std; 1 Nam class Humans{ 21 string name; Name and age of Student int age; 2 public: Binh void setName(string); 22 void setAge(int); Name and age of Student string getName(void); 3 int getAge(void); Xuan }; 22 Name and age of Student void Humans::setName(string s){ 4 name = s; Tuan C++ T r a n g | 128
  10. Chương 13. Lập trình hướng đối tượng } 21 void Humans::setAge (int a){ Name and age of Student age = a; 5 } Lan string Humans::getName(void){ 22 return name; ===Students=== } The man: Nam, age 21 int Humans::getAge(void){ The man: Binh, age 22 return age; The man: Xuan, age 22 } The man: Tuan, age 21 #define MAX 5 The man: Lan, age 22 int main(){ Humans man[MAX]; for(int i=0; i >s; cin>>a; man[i].setName(s); man[i].setAge(a); } cout<<"===Students==="<<endl; for(int i=0; i<MAX; i++) cout<<"The man: "<<man[i].getName()<<", age "<<man[i].getAge()<<endl; return 0; } Trong trường hợp này, biến man là một mảng c|c đối tượng Humans. Chương trình minh họa cho việc nhập tên sinh viên, tuổi của họ v{ lưu v{o một mảng. Sau đó, xuất kết quả ra màn hình. Dù là cùng là sự thể hiện của lớp Humans, nhưng c|c đối tượng man[1], man[2], có c|c thuộc tính hoàn toàn khác nhau. Cơ sở của lập trình hướng đối tượng là dữ liệu thành viên và các hàm thành viên của một đối tượng. Chúng ta hoàn toàn không sử dụng một tập các biến toàn cục để truyền qua một hàm (hay tập các biến cục bộ truyền theo tham biến), m{ thay v{o đó, chúng ta sử dụng c|c đối tượng cùng với C++ T r a n g | 129
  11. Chương 13. Lập trình hướng đối tượng dữ liệu thành viên và hàm thành viên của nó. C|c h{m th{nh viên t|c động trực tiếp lên các dữ liệu thành viên. Hàm tạo và hàm hủy Trước khi sử dụng một đối tượng, chúng ta cần khởi tạo giá trị cho nó để tránh gặp phải những giá trị không mong muốn khi thực thi chương trình. Một cách thức m{ chúng ta đ~ sử dụng ở trên là sử dụng phương thức setter. Một phương thức đơn giản hơn, chúng ta có thể sử dụng hàm khởi tạo (hay gọi tắt là hàm tạo). Việc khai báo hàm tạo cũng tương tự như khai báo hàm thành viên khác, tuy nhiên nhất thiết tên hàm tạo phải trùng với tên lớp. Chương trình Ví dụ #include The man: Jack, age 21 using namespace std; class Humans{ string name; int age; public: Humans(string, int); string getName(void); int getAge(void); }; Humans::Humans (string s, int a){ name = s; age = a; } string Humans::getName(void){ return name; } int Humans::getAge(void){ return age; } int main(){ Humans man(“Jack”, 21); cout<<”The man: “<<man.getName()<<”, age “<<man.getAge(); return 0; } Hàm tạo không có kiểu dữ liệu trả về - tương ứng với kiểu void. Tuy nhiên chúng ta sẽ không sử dụng từ khóa void trước khai báo hàm tạo. C++ T r a n g | 130
  12. Chương 13. Lập trình hướng đối tượng Nếu một đối tượng đ~ được tạo ra, nhưng ta không muốn sử dụng đến nó nữa, để thu hồi bộ nhớ, ta cần sử dụng một phương thức để hủy bỏ các dữ liệu thành viên của nó – đó l{ h{m hủy. Hàm hủy cũng l{ một hàm thành viên của lớp. Nó không có kiểu dữ liệu trả về, nhưng ta cũng không sử dụng từ khóa void trước khai báo hàm hủy. Hàm hủy có tên trùng với tên lớp v{ phía trước tên hàm hủy là dấu ~. Hàm hủy sẽ tự động được gọi khi phạm vi hoạt động của đối tượng kết thúc. Phạm vi hoạt động của một đối tượng cũng giống như phạm vi hoạt động của một biến cục bộ - khai báo trong phạm vi nào, thì chỉ có tác dụng trong phạm vi đó. Chương trình Ví dụ #include The man: Jack, age 21 using namespace std; class Humans{ string name; int age; public: Humans(string, int); ~Human(); string getName(void); int getAge(void); }; Humans::Humans (string s, int a){ name = s; age = a; } Humans::~Humans() { //do something //delete pointer; } string Humans::getName(void){ return name; } int Humans::getAge(void){ return age; } int main(){ Humans man(“Jack”, 21); cout<<”The man: “<<man.getName()<<”, age “<<man.getAge(); C++ T r a n g | 131
  13. Chương 13. Lập trình hướng đối tượng return 0; } Chồng chất hàm tạo Cũng như c|c h{m kh|c trong C++ cho phép chồng chất hàm, hàm tạo cũng có thể bị chồng chất. Khi chồng chất hàm tạo thì danh sách tham số phải khác nhau (kiểu dữ liệu hoặc số lượng tham số). Chúng ta cần nhớ rằng, khi chồng chất hàm thì trình biên dịch sẽ gọi một h{m tương ứng với danh sách tham số của nó. Trong trường hợp chồng chất hàm tạo, thì quá trình gọi là tự động khi đối tượng được tạo ra, do đó, một hàm tạo sẽ được gọi tự động tương ứng với danh sách tham số của nó. Chương trình Ví dụ #include The man: Jack, age 21 using namespace std; The default: Default, age class Humans{ 21 string name; int age; public: Humans(void); Humans(string, int); string getName(void); int getAge(void); }; Humans::Humans (void){ name = “Default”; age = 21; } Humans::Humans (string s, int a){ name = s; age = a; } string Humans::getName(void){ return name; } int Humans::getAge(void){ return age; } int main(){ Humans man(“Jack”, 21); Humans default; C++ T r a n g | 132
  14. Chương 13. Lập trình hướng đối tượng cout<<”The man: “<<man.getName()<<”, age “<<man.getAge()<<endl; cout<<”The default: “<<default.getName()<<”, age “<<default.getAge(); return 0; } Giải thích: trong trường hợp ví dụ trên, để tạo đối tượng thuộc lớp Humans, ta có thể sử dụng một trong hai hàm tạo tương ứng: Humans(void) hoặc Humans(string, int). Nếu gọi theo phương thức hàm tạo không đối số, thì tên gọi và tuổi sẽ được tạo mặc định. Còn nếu gọi theo phương thức có đối số, thì tên gọi và tuổi sẽ được tạo theo tham số truyền vào. Ta cũng cần lưu ý trong c|ch gọi hàm tạo, đối với hàm tạo có đối số, thì sau tên đối tượng, chúng ta cần cung cấp tham số tương ứng với tham số hàm tạo bên trong dấu (). Còn đối với hàm tạo không đối số, thì hãy khai b|o nó như khai b|o biến mà không hề có bất kì dấu () nào. Humans man(); //Sai Humans man; //Đúng Humans man = Humans(); //Đúng Chú ý: Khi ta không khai báo một hàm tạo mặc định không tham số và chỉ khai báo hàm tạo mặc định có tham số, thì việc khai báo một đối tượng theo c|ch Humans man; l{ không được phép. Nếu ta không tạo ra một hàm tạo n{o, thì điều này là hợp lệ. Sao chép hàm tạo Một đối tượng có thể được tạo ra từ hàm tạo theo cách khởi gán các dữ liệu thành viên của nó cho một giá trị n{o đó. Chúng ta hoàn toàn có thể khởi tạo một đối tượng từ một đối tượng khác bằng cách sử dụng toán tử gán. Tuy nhiên, trong thực tế, nếu dữ liệu của đối tượng lớn, phức tạp, thì việc sử dụng toán tử gán sẽ thực thi rất chậm và có thể gây ra một số lỗi liên quan đến hủy đối tượng trong vùng bộ nhớ. Để khắc phục nhược điểm này, ta có thể sử dụng hàm tạo sao chép. Khi sử dụng hàm tạo sao chép, trình biên dịch sẽ sao chép toàn bộ dữ liệu thành viên của đối tượng đó sang đối tượng khởi tạo. Humans man(“Jack”, 21); //Sao chép trực tiếp Humans man2 = man; //Sao chép hàm tạo C++ T r a n g | 133
  15. Chương 13. Lập trình hướng đối tượng Humans::Humans(const Humans& m){ name = m.name; age = m.age; } Humans man2(man); Chúng ta lưu ý rằng, việc sao chép hàm tạo sẽ được quy định theo tham chiếu hằng const Humans&. Nếu ta không viết hàm sao chép hàm tạo, thì trình biến dịch sẽ tự động làm giúp(nghĩa l{ ta luôn có thể sử dụng cách khởi tạo đối tượng theo kiểu Object newObj(oldObj); với oldObj l{ đối tượng thuộc lớp Object đ~ được tạo, dù ta có tạo ra phương thức sao chép hàm tạo hay không). Hay nói cách khác, hàm tạo sao chép là mặc định đối với đại đa số trình biên dịch ANSI C++ hiện đại (GCC, Visual C++, Borland C++, Intel C++). Tham chiếu hằng. Phương thức sao chép hàm tạo (hoặc tổng quát là các phương thức có sử dụng tham chiếu hằng đến lớp đối tượng) có thể thực hiện theo tham chiếu hoặc tham chiếu hằng (tương ứng với không hoặc có từ khóa const), nhưng hãy luôn quy định là tham chiếu (có toán tử &). Khi quy định tham chiếu, đối tượng tham chiếu sẽ tham chiếu đến địa chỉ của đối tượng gốc. Dữ liệu của đối tượng tham chiếu sẽ được ánh xạ theo địa chỉ của đối tượng được tham chiếu (không thực hiện việc sao chép trực tiếp mà là gián tiếp thông qua địa chỉ của biến tham chiếu). Tuy nhiên, cũng vì lí do n{y m{ đối tượng được tham chiếu có thể bị thay đổi giá trị (tương tự như truyền theo tham biến). Điều này làm vi phạm tính đóng gói trong lập trình hướng đối tượng. Cũng vì lí do n{y, C++ cung cấp cho ta từ khóa const để quy định một đối tượng khi được tham chiếu sẽ không bị làm thay đổi dữ liệu th{nh viên v{ nó được gọi là tham chiếu hằng. Như vậy, ta cần phân biệt ba cách truyền tham số đối tượng trong một phương thức: truyền theo tham trị – dữ liệu của đối tượng có thể được thay đổi bên trong phương thức nhưng sự thay đổi này không lưu lại, việc sao chép dữ liệu trong trường hợp này là thực thi trực tiếp nên thường chỉ áp dụng cho các kiểu dữ liệu nguyên thủy đơn giản; truyền theo tham chiếu – dữ liệu của đối tượng có thể bị thay đổi trong phương thức và nó được lưu lại, nó thực hiện việc sao chép dữ liệu một cách gián tiếp nên dữ liệu có cấu trúc phức tạp (như lớp đối tượng, con trỏ) có thể được sao chép nhanh hơn rất nhiều so với truyền theo tham trị; tham chiếu hằng – tương tự như tham chiếu nhưng không cho phép thay đổi dữ liệu của đối tượng ngay cả trong phương thức. C++ T r a n g | 134
  16. Chương 13. Lập trình hướng đối tượng Hợp lệ Không hợp lệ class Humans{ class Humans{ string name; string name; int age; int age; public: public: Humans(string, int); Humans(string, int); Humans(const Humans&); Humans(const Humans&); string getName(void); ~Human(); int getAge(void); string getName(void); }; int getAge(void); Humans::Humans(const Humans& }; m){ Humans::Humans(const Humans& name = m.name; m){ age = m.age; name = m.name; } age = m.age = 22;//Error } Ta có thể thấy trong trường hợp không hợp lệ, chúng ta quy định đối tượng được tham chiếu m sẽ cho phép đối tượng khác tham chiếu đến nó theo tham chiếu hằng. Nhưng đối tượng tham chiếu đến nó, lại cố gắng thay đổi thuộc tính age của nó. Trong trường hợp n{y, chương trình sẽ phát sinh lỗi. Nếu quy định là tham chiếu bình thường (bỏ đi từ khóa const) thì khai b|o được xem là hợp lệ (tuy nhiên vi phạm tính đóng gói). Khi một phương thức của lớp đối tượng sử dụng tham số chính là đối tượng của lớp đó, chúng ta có thể truy cập trực tiếp đến thuộc tính của đối tượng tham chiếu kể cả nó được quy định là private (cả tham chiếu lẫn không tham chiếu). Bên cạnh đó, nếu ta quy định là tham chiếu bình thường, thì ta có thể sử dụng c|c phương thức như getter v{ setter để truy cập đến các thuộc tính của nó. Nhưng nếu ta sử dụng tham chiếu hằng, thì không được phép truy cập đến c|c phương thức của đối tượng tham chiếu hằng. Chúng ta chỉ có thể truy cập đến các phương thức hằng của đối tượng tham chiếu hằng n{y. Phương thức hằng là những phương thức được bổ sung vào từ khóa const vào cuối khai b|o phương thức trong tiêu đề hàm prototype và tiêu đề trong khai b|o h{m đầy đủ. Hãy quan sát các ví dụ sau đ}y. Ví dụ Tham chiếu Hằng Ví dụ Tham chiếu class PhanSo class PhanSo C++ T r a n g | 135
  17. Chương 13. Lập trình hướng đối tượng { { private: private: int Tu; int Tu; int Mau; int Mau; public: public: //Khai báo các hàm tạo //Khai báo các hàm tạo PhanSo Nhan(const PhanSo&); PhanSo Nhan(PhanSo&); int GetTu(void); int GetTu(void); int GetMau(void); int GetMau(void); }; }; PhanSo PhanSo::Nhan(const PhanSo PhanSo::Nhan(PhanSo& p) PhanSo& p) { { return PhanSo(Tu*p.GetTu(), return PhanSo(Tu*p.Tu, Mau*p.GetMau()); Mau*p.Mau); //hoặc PhanSo(Tu*p.Tu, //không được phép viết Mau*p.Mau); PhanSo(Tu*p.GetTu(), } Mau*p.GetMau()); } int PhanSo::GetTu(void) { int PhanSo::GetTu(void) return Tu; { } return Tu; } int PhanSo::GetMau(void) { int PhanSo::GetMau(void) return Mau; { } return Mau; } Trong trường hợp ta muốn sử dụng phương thức cho đối tượng tham chiếu hằng, thì cần khai b|o phương thức GetTu v{ GetMau l{ phương thức hằng. Khi đó, chúng ta sử dụng cú ph|p sau đ}y: int GetTu(void) const; int GetMau(void) const; C++ T r a n g | 136
  18. Chương 13. Lập trình hướng đối tượng class PhanSo { private: int Tu; int Mau; public: //Khai báo các hàm tạo PhanSo Nhan(const PhanSo&); int GetTu(void) const; int GetMau(void) const; }; PhanSo PhanSo::Nhan(const PhanSo& p) { return PhanSo(Tu*p.Tu, Mau*p.Mau); //hoặc PhanSo(Tu*p.GetTu(), Mau*p.GetMau()); } int PhanSo::GetTu(void) const { return Tu; } int PhanSo::GetMau(void) const { return Mau; } Việc bổ sung từ khóa const v{o sau khai b|o phương thức sẽ giúp cho đối tượng tham chiếu hằng có thể gọi phương thức hằng này. Thêm một khái niệm nữa trong C++ mà chúng ta cần biết là phương thức tham chiếu. Một phương thức tham chiếu cho phép ta sử dụng nó như một biến – ta có thể gán trực tiếp một giá trị biến cho phương thức đó m{ không gặp phải một trở ngại nào. Chương trình #include using namespace std; C++ T r a n g | 137
  19. Chương 13. Lập trình hướng đối tượng class complex{ private: float img; float real; public: complex(); complex(float, float); float &getimg(); float &getreal(); }; complex::complex( float img, float real ) { this->img = img; this->real = real; } complex::complex() { real = 0; img = 0; } float & complex::getreal() { return real; } float & complex::getimg() { return img; } int main () { complex c; c.getreal() = 2; c.getimg() = 1; cout<<c.getreal()<<" + I*"<<c.getimg(); } Trong ví dụ này, chúng ta thấy c|c phương thức getter được khai báo là phương thức tham chiếu. Ta có thể gán trực tiếp giá trị cho c|c phương thức n{y. Trong trường hợp n{y, phương thức getter có cả hai tính năng: C++ T r a n g | 138
  20. Chương 13. Lập trình hướng đối tượng vừa là getter vừa là setter. Nhưng với tính năng của một phương thức setter đơn (tức chỉ thiết lập một giá trị duy nhất). Tính đóng gói – Encapsulation Ví dụ trên đưa ra cho ta hai phương |n: nên hay không nên sử dụng từ khóa const. Câu trả lời là h~y nên quy định việc sao chép hàm tạo là truyền theo tham chiếu hằng, bởi lẽ c|c đối tượng khác nhau, không có quyền chỉnh sửa dữ liệu thành viên của nhau, nó chỉ có thể truyền thông điệp cho nhau mà thôi, việc chỉnh sửa dữ liệu thành viên là do bản thân của đối tượng đó. Điều này là sự thể hiện tính đóng gói trong lập trình hướng đối tượng. Tính đóng gói của lập trình hướng đối tượng còn thể hiện ở các mức độ cho phép truy cập đối với dữ liệu và hàm thành viên – tương ứng với từ kho| private, protected v{ public m{ ta đ~ thảo luận ở trên. Khái niệm: tính đóng gói l{ tính chất không cho phép người dùng hay đối tượng kh|c thay đổi dữ liệu thành viên của đối tượng nội tại. Chỉ có các hàm thành viên của đối tượng đó mới có quyền thay đổi trạng thái nội tại của nó mà thôi. C|c đối tượng khác muốn thay đổi thuộc tính thành viên của đối tượng nội tại, thì chúng cần truyền thông điệp cho đối tượng, và việc quyết định thay đổi hay không vẫn do đối tượng nội tại quyết định. Chúng ta có thể khảo sát ví dụ sau: nếu một bệnh nhân cần phải thay nội tạng để có thể sống, thì việc thay thế nội tạng đó cần phải có sự đồng ý của bệnh nhân. Không ai có thể tự động thực hiện điều này (chỉ khi bệnh nh}n đ~ rơi v{o tình trạng hôn mê, thì người nhà bệnh nhân mới quyết định thay họ). Nội tạng là các thuộc tính cố hữu của bệnh nhân. C|c phương thức thay thế nội tạng của đối tượng b|c sĩ không phải l{ phương thức thành viên của đối tượng bệnh nhân (bệnh nhân không thể tự thay thế nội tạng cho mình v{ b|c sĩ không có quyền thay thế nội tạng cho bệnh nhân nếu không có sự đồng ý của họ). Do đó, họ muốn thực hiện thì cần có phương thức đồng ý của bệnh nhân (phương thức thành viên của đối tượng bệnh nhân). Phương thức đồng ý của bệnh nh}n n{y cũng không thể nào áp dụng cho bệnh nhân kia (bệnh nhân A không thể quyết định thay thế nội tạng cho bệnh nhân B). Như vậy, dữ liệu thành viên của đối tượng nào, thì chỉ có đối tượng đó mới có quyền thay đổi. Trong một vài giáo trình, tính chất này còn được gọi l{ tính đóng gói và ẩn dấu thông tin (encapsulation and information hiding). C++ T r a n g | 139
  21. Chương 13. Lập trình hướng đối tượng Con trỏ đối tượng Chúng ta đ~ l{m quen với mảng đối tượng v{ chúng ta cũng đ~ biết rằng có sự tương ứng 1-1 giữa mảng và con trỏ. Trong phần này, chúng ta sẽ thảo luận về con trỏ đối tượng. Chúng ta vẫn sử dụng lớp Humans ở trên cho các ví dụ minh họa trong phần này. Việc khai báo con trỏ đối tượng ho{n to{n tương tự như khai b|o con trỏ dữ liệu. Humans *man; Để truy cập đến c|c phương thức thành viên bên ngoài lớp (hàm thành viên), ta sử dụng dấu -> (vì chỉ có c|c phương thức th{nh viên được chỉ định là public). Khi gọi phương thức khởi tạo, ta có thể gọi theo cách mà ta đ~ sử dụng cho con trỏ dữ liệu. Hoặc có thể sử dụng toán tử new. Chương trình Kết quả Andy, 22 int main() Jack, 21 { Humans man(“Andy”, 22); Humans *man0 = &man; //Hoặc Humans *man1 = new Humans(“Jack”, 21); cout getName() getAge() getName() getAge(); return 0; } Ngay sau toán tử new, chúng ta gọi phương thức khởi tạo của nó. Trong ví dụ trên, ta đang khởi tạo một đối tượng duy nhất. Nếu muốn tạo một danh s|ch c|c đối tượng theo dạng con trỏ, ta có thể sử dụng toán tử new[] mà ta đ~ thảo luận ở trên. C++ T r a n g | 140
  22. Chương 13. Lập trình hướng đối tượng Khi liên đới đến con trỏ, có nhiều vấn đề liên quan đến c|ch đọc. Chúng ta có thể tổng kết theo bảng bên dưới đ}y Biểu thức Cách đọc *x trỏ bởi x &x địa chỉ của x x.y thành viên y của đối tượng x x->y thành viên y của đối tượng trỏ bởi x (*x).y thành viên y của đối tượng trỏ bởi x x[i] đối tượng thứ i trỏ bởi x Lớp được khai báo nhờ từ khóa struct và union Trong C++, một lớp có thể được khai báo nhờ vào từ khóa struct hoặc từ khóa union. Chúng ta đ~ biết từ khóa struct dùng để khai báo kiểu dữ liệu struct và nó chứa các dữ liệu thành viên. Từ khóa union dùng để khai báo kiểu dữ liệu union và cũng chứa các dữ liệu thành viên. Tuy nhiên, chúng vẫn có thể chứa các hàm thành viên. Khi khai báo lớp bằng từ khóa struct, không có một sự khác biệt nào so với từ khóa class. Chỉ có duy nhất một sự khác biệt, đó l{ theo mặc định, những phương thức thành viên và dữ liệu th{nh viên n{o không được chỉ định từ khóa quy định mức truy cập (private, protected, public) thì trong lớp được khai báo bằng từ khóa class sẽ là private còn trong lớp được khai báo bằng struct sẽ là public. Còn đối với từ khóa union có vài sự khác biệt, tuy không thể dùng để khai báo một lớp hoàn hảo như từ khóa struct hay class, nhưng nó vẫn có thể chứa các phương thức bên trong nó. Nếu không chỉ định từ khóa quy định mức truy cập, thì nó sẽ mặc định là public. Nếu viết một lớp với đầy đủ hàm tạo, hàm hủy v{ c|c phương thức khác bằng từ khóa class, thì khi thay thế bằng từ khóa struct, sẽ không có nhiều sự thay đổi. Nếu thay thế bằng từ khóa union, thì trình dịch sẽ thông báo lỗi. Sở dĩ như thế là bởi vì dù union cho phép chứa phương thức thành viên, nhưng nó không hỗ trợ khai báo prototype, không hỗ trợ dữ liệu kiểu string. Chú ý: Hãy luôn sử dụng từ khóa class để khai báo lớp. Con trỏ this Con trỏ this trỏ vào dữ liệu thành viên của chính nó. Điều này có nghĩa l{ con trỏ this chỉ có phạm vi tác dụng trong một lớp. Một điều cực kì C++ T r a n g | 141
  23. Chương 13. Lập trình hướng đối tượng quan trọng, là con trỏ this chỉ hoạt động với các dữ liệu thành viên và các h{m th{nh viên được khai b|o l{ không tĩnh (non-static). Các dữ liệu thành viên v{ h{m th{nh viên tĩnh (static) không hỗ trợ con trỏ this. Ví dụ trong phương thức hàm tạo của lớp Complex trên, chúng ta có thể sử dụng this->real để truy cập thuộc tính real, this->img – để truy cập thuộc tính img. Ta cũng có thể so sánh một đối tượng khác với đối tượng nội tại nhờ vào con trỏ this này. Ví dụ Kết quả #include 0 using namespace std; 1 class Complex{ float real; float img; public: Complex(float, float); bool isMe(const Complex&); }; Complex::Complex(float real, float img){ this->real = real; this->img = img; } bool Complex::isMe(const Complex& c){ if(&c==this) return true; else return false; } int main(){ Complex a(3, 2); Complex b(2, 2); Complex *c = &a; cout<<a.isMe(b)<<endl; cout<<a.isMe(*c); return 0; } Giải thích: với việc sử dụng con trỏ this trong hàm tạo, ta có thể đặt tên các tham số trong hàm tạo trùng với tên các dữ liệu của lớp. Để truy cập đến các thuộc tính của lớp, ta sử dụng con trỏ this. Hàm thành viên isMe sẽ kiểm tra một đối tượng có phải là chính nó hay không (có cùng địa chỉ trên bộ C++ T r a n g | 142
  24. Chương 13. Lập trình hướng đối tượng nhớ). Dù là một bản sao của nó (có dữ liệu thành viên giống nhau) thì kết quả nhận được cũng l{ sai (0). Trong hàm main, ta khởi tạo hai đối tượng a v{ b. Đối tượng con trỏ c sẽ trỏ v{o địa chỉ của đối tượng a. Điều này có nghĩa l{ c sẽ có cùng vùng địa chỉ với a, còn b thì không. Khi gọi hàm a.isMe(b) sẽ cho kết quả là sai (0) và a.isMe(*c) sẽ cho kết quả l{ đúng (1). Thành viên tĩnh – Từ khóa static Một lớp có thể chứa c|c th{nh viên tĩnh hoặc không tĩnh. Nếu không chỉ định từ khóa là static cho các thành viên, thì theo mặc định, nó sẽ là non- static. Nếu muốn quy định cho một th{nh viên n{o l{ tĩnh, thì ta bổ sung từ khóa static v{o trước nó. Nếu l{ th{nh viên không tĩnh, ta không cần khai báo bất kì từ khóa nào. Một dữ liệu th{nh viên tĩnh của lớp như l{ một biến toàn cục của lớp đó. Bởi mọi sự thay đổi dữ liệu thành viên tĩnh của đối tượng n{y đều có tác dụng lên toàn bộ các dữ liệu thành viên tĩnh của c|c đối tượng khác. Một phương thức không tĩnh có quyền truy cập đến các dữ liệu thành viên không tĩnh. Một phương thức tĩnh có thế truy cập đến dữ liệu thành viên không tĩnh. Trong trường hợp này, ta cần tạo ra một sự thể hiện của đối tượng và truy cập đến các thuộc tính không tĩnh từ đối tượng n{y.Để truy cập đến th{nh viên không tĩnh, ta sử dụng một sự thể hiện của đối tượng, sau đó l{ dấu chấm (hoặc ->), tiếp đến l{ th{nh viên không tĩnh.Để truy cập đến đối tượng tĩnh, ta sử dụng toán tử phạm vi ngay sau tên lớp, tiếp đến l{ th{nh viên tĩnh. C|c phương thức tĩnh v{ không tĩnh có thể truy cập lẫn nhau. Ví dụ Kết quả #include Call by NonStatic method using namespace std; Name: Ford class Car Serial: 123 { Call by Static method public: Name: Ford string name; Serial: 123 int serial; public: Count: 2 static int count; static void Show() { Car vehicle; C++ T r a n g | 143
  25. Chương 13. Lập trình hướng đối tượng vehicle.name = "Ford"; vehicle.serial = 123; cout << "\nName: " << vehicle.name; cout << "\nSerial: " << vehicle.serial; } static void CallShowStatic(){ Show(); } void CallShowNonStatic(){ Show(); } }; int Car::count = 2; int main() { Car a; cout<<" Call by NonStatic method "; a.CallShowNonStatic(); cout<<"\n Call by Static method "; Car::CallShowStatic(); cout << "\n\nCount: " <<Car::count; return 0; } Giải thích: Hàm thành viên Show là static, nên muốn truy cập đến các dữ liệu th{nh viên không tĩnh thì nó cần tạo một sự thể hiện của lớp đó l{ đối tượng vehicle. Hàm CallShowStatic là static, hàm CallShowNonStatic là non-static đều có thể truy cập đến hàm Show là static một cách trực tiếp. Trong hàm main, các hàm non-static được gọi thông qua một sự thể hiện lớp, còn h{m static được gọi thông qua toán tử phạm vi. Dữ liệu static là count cũng được truy cập thông qua toán tử phạm vi. Mặc dù thành viên static có thể được truy cập trực tiếp thông qua toán tử phạm vi, nhưng nó cũng chịu sự chi phối của các mức truy cập (private, protected, public). Chỉ có các thành viên không tĩnh mới có thể sử dụng con trỏ this. Hàm bạn và lớp bạn Hàm bạn: nếu một thành viên của lớp được quy định là private hoặc protected thì chỉ có các hàm thành viên của lớp mới có quyền truy cập đến nó. Nếu một phương thức không phải là thành viên của lớp muốn truy cập C++ T r a n g | 144
  26. Chương 13. Lập trình hướng đối tượng đến, thì nó phải là hàm bạn của lớp đó. Phương thức bạn có thể được khai báo nhờ từ khóa friend. Ví dụ Kết quả #include 10 using namespace std; class Rectangle { private: int w; int h; public: Rectangle(int, int); friend int Area(Rectangle); }; Rectangle::Rectangle(int w, int h){ this->w = w; this->h = h; } int Area(Rectangle rec){ return (rec.w*rec.h); } int main() { Rectangle rec(2, 5); cout Square: 5x5 using namespace std; C++ T r a n g | 145
  27. Chương 13. Lập trình hướng đối tượng class Rectangle { private: int w; int h; public: Rectangle(int, int); friend class MakeSquare; }; class MakeSquare{ private: int w; int h; public: MakeSquare(Rectangle); void ShowSquare(void); }; MakeSquare::MakeSquare(Rectangle rec){ this->w = max(rec.w, rec.h); this->h = max(rec.w, rec.h); } void MakeSquare::ShowSquare(void){ cout<<"Square: "<<w<<"x"<<h; } int main() { Rectangle rec(2, 5); MakeSquare mk(rec); mk.ShowSquare(); return 0; } Giải thích: Lớp Rectangle được quy định là lớp bạn của lớp Square, do đó, lớp Square có quyền truy cập đến các thuộc tính private và protected của lớp Rectangle. Hàm tạo của lớp Square truy cập đến các dữ liệu thành viên của lớp Rectangle để lấy chiều dài và chiều rộng của đối tượng rec (dù chúng l{ private), để tạo nên đối tượng mk. Đối tượng Square được tạo mới với cạnh của nó là số đo lớn nhất các cạnh của đối tượng Rectangle. Ta cũng lưu ý rằng A là bạn của B, thì không có nghĩa l{ B cũng l{ bạn của A. Như vậy, tình bạn có thể là một chiều hoặc hai chiều tùy thuộc vào sự quy định của người lập trình. C++ T r a n g | 146
  28. Chương 13. Lập trình hướng đối tượng Chồng chất toán tử Trong ngôn ngữ lập trình hướng đối tượng, có nhiều ngôn ngữ hỗ trợ chồng chất toán tử (các ngôn ngữ hỗ trợ bao gồm C++, Delphi 2009, C#,VB.net, nhưng mức độ hỗ trợ khác nhau; các ngôn ngữ không hỗ trợ bao gồm Java, Python, ). Chồng chất toán tử (operator overloading) là cách thức xây dựng các hàm thành viên mà tên gọi của chúng là các toán tử đ~ được định nghĩa trước đó (+, -, *, v.v.). C++ là ngôn ngữ hỗ trợ chồng chất toán tử hoàn hảo. Các toán tử sau đ}y có thể được chồng chất trong C++ Các toán tử được phép chồng chất + - * / = += -= *= /= > >= == != = ++ % & ^ ! | ~ &= ^= |= || && %= [] () , ->* -> new delete new[] delete[] Cấu trúc khai báo chồng chất toán tử type operator toán_tử(tham số){ thân hàm } Ví dụ sau đ}y sẽ minh họa cho việc chồng chất toán tử. Chúng ta sẽ xây dựng một lớp số phức, xây dựng các phép toán cộng hai số phức (phép toán hai ngôi) v{ phép tăng số phức lên 1 đơn vị thực v{ 1 đơn vị ảo(phép toán một ngôi) bằng cách sử dụng chồng chất toán tử. Ví dụ Kết quả #include 2 + 3*I using namespace std; 4 + 3*I class Complex{ float real; float img; public: Complex(float, float); Complex operator +(const Complex&); Complex operator ++(void); void toString(void); }; Complex::Complex(float a, float b){ real = a; img = b; } Complex Complex::operator +(const Complex& b){ Complex c(0, 0); c.real = real + b.real; C++ T r a n g | 147
  29. Chương 13. Lập trình hướng đối tượng c.img = img + b.img; return c; } Complex Complex::operator ++(void){ Complex c(0, 0); c.real += ++real; c.img += ++img; return c; } void Complex::toString(void){ cout<<real<<" + "<<img<<"*I"<<endl; } int main(){ Complex a(3, 2); Complex b(-1, 1); (a+b).toString(); (++a).toString(); return 0; } Ta lưu ý rằng, trong phương thức toán tử, số tham số hình thức luôn bằng hạng của toán tử trừ 1. Điều n{y có nghĩa l{ với phép toán một ngôi sẽ không có tham số hình thức, với toán tử hai ngôi sẽ có một tham số hình thức. Điều này là dễ hiểu, bởi đ~ có một tham số mặc định – đó chính l{ bản th}n đối tượng nội tại (đối tượng tương ứng với con trỏ this). Phép toán cộng, sẽ cộng đối tượng nội tại với một đối tượng kh|c. Phép to|n tăng một đơn vị thực, một đơn vị ảo sẽ l{m thay đổi giá trị của đơn vị thực v{ đơn vị ảo của đối tượng nội tại lên 1. Vì các toán tử này trả về kiểu số phức, nên ta hoàn toàn có thể thực hiện phép toán phức hợp với chúng (tức là một biểu thức có nhiều toán tử loại này thực hiện trên các hạng tử là các số phức). (a+ ++b+(a+b)).toString(); Bằng việc sử dụng chồng chất toán tử, biểu thức tính toán sẽ trở nên đơn giản hơn. Ta cũng có thể sử dụng cách gọi a.operator+(b). Hai cách này cho kết quả như nhau. Đối với hàm toán tử + và ++ ở trên, ta có thể viết ngắn gọn hơn m{ không cần khai báo thêm một biến tạm: Complex Complex::operator +(const Complex& b){ return Complex(real + b.real, img + b.img); } C++ T r a n g | 148
  30. Chương 13. Lập trình hướng đối tượng Complex Complex::operator ++(void){ return Complex(++real, ++img); } Việc thực hiện các toán tử trên c|c đối tượng cần yêu cầu đối tượng trước đó phải được khởi tạo giá trị. Nghĩa l{ phải có một hàm tạo cho đối tượng đó. Mặc dù C++ hỗ trợ chồng chất nhiều toán tử, nhưng ta không nên lạm dụng nó. Chúng ta nên sử dụng chồng chất toán tử với mục đích đúng đắn (cộng hai số phức thì sử dụng toán tử + mà không phải là toán tử kh|c, ). Việc sử dụng chồng chất toán tử như l{ h{m th{nh viên |p dụng cho tất cả các toán tử mà C++ hỗ trợ. Trừ các toán tử gán, hợp nhất, () và ->. Các toán tử còn lại cũng |p dụng cho các hàm toàn cục. Hàm toàn cục cũng như h{m th{nh viên, nhưng nó không thuộc một lớp nào. Việc khai báo hàm toàn cục sẽ được thực hiện như sau type operator@(A) Trong đó, @ l{ kí hiệu toán tử, A là tên lớp. Ví dụ Kết quả #include 4 + 1*I using namespace std; class Complex{ public: float real; float img; Complex(float, float); void toString(void); }; Complex::Complex(float a, float b){ real = a; img = b; } void Complex::toString(void){ cout<<real<<" + "<<img<<"*I"<<endl; } Complex operator -(const Complex &a, const Complex &b){ return Complex(a.real - b.real, a.img - b.img); } int main(){ C++ T r a n g | 149
  31. Chương 13. Lập trình hướng đối tượng Complex a(3, 2); Complex b(-1, 1); (a-b).toString(); return 0; } Giải thích: trong ví dụ này, hàm toán tử - không phải là hàm thành viên của lớp Complex. Do đó, muốn truy cập đến các thuộc tính của nó, ta phải quy định các thuộc tính này là public hoặc phải tạo thêm c|c phương thức getter để thu thập dữ liệu hoặc quy định nó là hàm bạn. Cũng vì nó không phải là hàm thành viên của lớp Complex, nên số tham số trong phép toán một ngôi là 1, trong phép toán hai ngôi là 2. Đối với chồng chất toán tử nhập xuất - IO overloading, chúng ta có một số chú ý: + Nếu khai báo hàm toán tử là một thành viên của lớp. Ví dụ Kết quả #include (4, 5) using namespace std; class Vector2D{ private: int x, y; public: Vector2D(){ x = 0; y = 0; } Vector2D(int x1, int y1){ x = x1; y = y1; } ostream& operator<<(ostream& os){ os<<"("<<x<<", "<< y<<")"; return os; } }; int main() { C++ T r a n g | 150
  32. Chương 13. Lập trình hướng đối tượng Vector2D ab(4, 5); //ab.operator (4, 5) using namespace std; class Vector2D{ private: int x, y; public: Vector2D(){ x = 0; y = 0; } Vector2D(int x1, int y1){ x = x1; y = y1; } friend ostream& operator<<(ostream&, const Vector2D&); }; ostream& operator<<(ostream& os, const Vector2D& v){ os<<"("<<v.x<<", "<<v.y<<")"; return os; } C++ T r a n g | 151
  33. Chương 13. Lập trình hướng đối tượng int main() { Vector2D ab(4, 5); cout > (hay toán tử trích tách dữ liệu), ta khai b|o ho{n to{n tương tự. Kiểu dữ liệu trả về lúc này là istream& thay vì sử dụng ostream& như trên. Chúng ta tiến hành nhập dữ liệu cho nên tham số Vector2D trong h{m cũng cần thay đổi – chúng ta cần bỏ đi từ khóa const bởi lẽ ta đang tiến hành nhập dữ liệu cho nó nên không thể quy định truyền giá trị theo tham chiếu hằng (tức không cho phép thay đổi giá trị). Ví dụ Kết quả #include (4, 5) using namespace std; class Vector2D{ private: int x, y; public: Vector2D(){ this->x = 0; this->y = 0; } Vector2D(int x, int y){ this->x = x; this->y = y; } friend istream& operator>>(istream&, Vector2D&); }; istream& operator>>(istream& is, Vector2D& v){ is>>v.x>>v.y; return is; } C++ T r a n g | 152
  34. Chương 13. Lập trình hướng đối tượng int main() { Vector2D ab; cin>>ab; return 0; } Các kiểu dữ liệu ostream& và istream& nằm trong thư viện iostream của namespace std (dấu & để quy định là truyền theo tham chiếu hoặc phương thức tham chiếu). Tính kế thừa - Inheritance Một tính năng theng chốt của lập trình hướng đối tượng đó l{ tính kế thừa. Nhờ vào tính kế thừa, nó cho phép một lớp có thể dẫn xuất từ một lớp khác, chính vì thế chúng sẽ tự động tiếp nhận các thành viên của bố mẹ và bổ sung thêm các thành viên của riêng chúng. Tính kế thừa cho phép lớp mới có thể nhận được mọi dữ liệu thành viên (private, protected, public) và các hàm thành viên (trừ hàm tạo, hàm hủy, hàm bạn và hàm toán tử gán =). Ta có thể xét ví dụ về lớp động vật Animal và minh họa tính kế thừa bằng lược đồ bên dưới (Hình 13). Lớp động vật Animal có các thuộc tính thành viên: tên gọi, cân nặng. Các hàm thành viên: di chuyển, ăn. Ta xét hai lớp dẫn xuất của nó là lớp mèo Cat và lớp cá Fish. Lớp Cat có các thuộc tính thành viên riêng: màu lông, màu mắt. Các hàm thành viên riêng: Bắt chuột, Leo cây. Lớp Fish có các thuộc tính thành viên riêng: kiểu vẩy, loại nước (nước ngọt, nước mặn, nước lợ). C|c h{m th{nh viên: bơi, sinh sản (cách thức sinh con như thế nào). Hình 21 – Tính kế thừa C++ T r a n g | 153
  35. Chương 13. Lập trình hướng đối tượng Theo như tính thừa kế, lớp Cat và Fish không những có những thuộc tính thành viên và hàm thành viên riêng của từng lớp, mà nó còn có những thuộc tính thành viên và hàm thành viên của lớp Animal. Từ nay, ta sẽ gọi lớp dẫn xuất Cat và Fish là các lớp con và lớp được dẫn xuất Animal là lớp cơ sở (hay lớp cha). Ta cần lưu ý rằng, tên gọi cũng mang tính tương đối, vì một lớp có thể là con của lớp n{y, nhưng lại là lớp cơ sở của lớp kh|c. Do đó, để tránh nhầm lẫn, trong những trường hợp cần phân biệt, ta sẽ gọi cụ thể là lớp con của lớp nào, hay lớp cơ sở của lớp nào. Để quy định một lớp là dẫn xuất từ lớp khác, ta sử dụng toán tử : theo cấu trúc sau class Animal{ }; class Cat: Từ_khóa_mức_truy _cập Animal{ }; class Fish: Từ_khóa_mức_truy _cập Animal{ }; Theo cấu trúc khai báo này, thì Cat và Fish là lớp con của lớp cơ sở Animal. Ví dụ Kết quả #include Animal Object using namespace std; I can eat class Animal{ I can move protected: Cat Object string name; I can eat int weight; I can move public: I can catch mouse Animal(void); I can climb tree Animal(string, int); void move(void); void eat(void); }; class Cat:public Animal{ private: C++ T r a n g | 154
  36. Chương 13. Lập trình hướng đối tượng string colorf; string colore; public: Cat(string, int, string, string); void catchmouse(void); void climb(void); }; void Animal::move(void){ cout name = ""; this->weight = 0; } Animal::Animal(string name, int weight){ this->name = name; this->weight = weight; } void Cat::catchmouse(void){ cout name = name; this->weight = weight; this->colorf = colorf; this->colore = colore; } int main() { C++ T r a n g | 155
  37. Chương 13. Lập trình hướng đối tượng Animal an("Gau", 100); Cat ca("Cat1", 3, "black", "blue"); cout<<" Animal Object "<<endl; an.eat(); an.move(); cout<<" Cat Object "<<endl; ca.eat(); ca.move(); ca.catchmouse(); ca.climb(); return 0; } Giải thích: trong chương trình n{y lớp Cat thừa kế từ lớp Animal. Nó sẽ kế thừa mọi dữ liệu thành viên và hàm thành viên của lớp Animal. Để hàm tạo của lớp Cat có thể truy cập đến các dữ liệu thành viên của lớp Animal, thì các dữ liệu thành viên này phải được khai báo mức truy cập là protected hoặc public. Đối tượng ca của lớp Cat chỉ có thể truy cập đến c|c phương thức thành viên của lớp cơ sở là Animal khi lớp Animal này được public (Cat:public Animal). Một điều cần lưu ý nữa đó l{ h{m tạo. Khi thừa kế, thì lớp con sẽ không thừa kế hàm tạo từ lớp cơ sở, nhưng lớp cơ sở cần có một hàm tạo mặc định không đối số (hàm tạo này luôn tồn tại; nếu ta khai báo thêm một vài hàm tạo, thì cần khai báo một hàm tạo không có đối số). Các mức truy cập Mức độ cho phép truy cập đến các dữ liệu thành viên từ một lớp được cho trong bảng sau Phạm vi public protected private Thành viên của cùng một lớp được phép được phép được phép Thành viên của lớp dẫn xuất được phép được phép không được phép Còn lại được phép không được phép không được phép Chúng ta cần lưu ý rằng trong cách viết về tính kế thừa Cat:public Animal có một số quy tắc chuyển đổi. Nếu các thành viên của lớp cơ sở có mức truy cập là A, khi thừa kế ta quy định mức truy cập của lớp con đối với lớp cơ sở là B (A và B có thể là private < protected < public) và giả sử rằng A<B, thì C++ T r a n g | 156
  38. Chương 13. Lập trình hướng đối tượng các thành viên này của lớp cha sẽ trở thành các thành viên của lớp con có mức truy cập là mức truy cập bé nhất A. Như tôi đ~ giới thiệu ở trên, một biến th{nh viên được chỉ định từ khóa chỉ mức truy cập là private thì chỉ có c|c phương thức trong cùng một lớp hoặc c|c phương thức bạn mới có quyền truy cập (bao gồm hàm bạn và lớp bạn). Nếu mức truy cập là public, thì mọi phương thức đều có quyền truy cập đến. Chúng ta sẽ tìm hiểu kĩ hơn về từ khóa protected. Tôi đ~ trình b{y về các khả năng m{ một phương thức có thể truy cập đến một biến thành viên được khai báo là protected: - Tương tự như c|c mức truy cập của private (chính nó và bạn của nó). - Từ c|c phương thức của một lớp dẫn xuất. - Từ c|c phương thức bạn của lớp dẫn xuất (bao gồm hàm bạn và lớp bạn). Đối với trường hợp đầu tiên, chúng ta đ~ tìm hiểu nó trong phần hàm bạn và lớp bạn. Chúng ta sẽ khảo sát hai khả năng sau cùng. Đối với khả năng thứ hai, hãy quan sát ví dụ sau đ}y: Ví dụ Kết quả #include 20 using namespace std; class Polygon{ protected: int w, h; public: void SetValue(int w, int h){ this->w = w; this->h = h; } }; class Rectangle:public Polygon{ public: int GetArea(){ return w*h; } }; int main() { C++ T r a n g | 157
  39. Chương 13. Lập trình hướng đối tượng Rectangle rec; rec.SetValue(4, 5); cout 4x5 using namespace std; class Polygon{ protected: int w, h; public: void SetValue(int w, int h){ this->w = w; this->h = h; } }; class Rectangle:public Polygon{ public: int GetArea(){ return w*h; } friend void ShowWH(Rectangle); }; void ShowWH(Rectangle p){ cout<<p.w<<"x"<<p.h; } int main() { Rectangle rec; rec.SetValue(4, 5); ShowWH(rec); return 0; } C++ T r a n g | 158
  40. Chương 13. Lập trình hướng đối tượng Giải thích: trong trường hợp n{y, phương thức ShowWH là bạn của lớp dẫn xuất Rectangle, nó có quyền truy cập đến các biến th{nh viên được chỉ định protected. Tính đa kế thừa – Multiple Inheritance Trong ngôn ngữ lập trình hướng đối tượng, tính kế thừa chia làm hai loại: ngôn ngữ đơn thừa kế và ngôn ngữ đa thừa kế. . Tính đơn thừa kế: là tính chất cho phép một lớp chỉ có thể kế thừa từ một lớp cơ sở duy nhất. Nếu muốn sử dụng tính năng đa thừa kế trong ngôn ngữ lập trình loại này, ta có thể cần phải sử dụng đến khái niệm giao diện interface. Ngôn ngữ đơn thừa kế tiêu biểu gồm: Java, C#, Delphi. . Tính đa thừa kế: là tính chất cho phép một lớp có thể kế thừa từ nhiều lớp cơ sở. Ngôn ngữ đa thừa kế tiêu biểu gồm: C++. Khai b|o tính đa kế thừa trong C++ tuân theo cú pháp sau class A: TKMTC1 B, TKMTC2 C, TKMTC3 D, ; Trong đó, + TKMTC1, TKMTC2, TKMTC3 là các từ khóa chỉ mức truy cập. Chúng có thể là public, protected hoặc private. + Lớp A gọi là lớp con; lớp B, C, D gọi là các lớp cơ sở. Ví dụ Kết quả #include I’m A using namespace std; I’m B class A{ I’m C int a; public: void showA(void); }; class B{ int b; public: void showB(void); }; C++ T r a n g | 159
  41. Chương 13. Lập trình hướng đối tượng class C: public A, public B{ int c; public: void showC(void); }; void A::showA(void){ cout<<"I'm A"<<endl; } void B::showB(void){ cout<<"I'm B"<<endl; } void C::showC(void){ cout<<"I'm C"<<endl; } int main() { C c; c.showA(); c.showB(); c.showC(); return 0; } Giải thích: trong ví dụ này, lớp C kế thừa từ lớp A và lớp B. Khi ta khai báo c l{ đối tượng của lớp C, do tính kế thừa nên đối tượng c chứa không chỉ thành viên của lớp c, mà còn có các thành viên của lớp A và B. Tính đa hình – Polymorphism Con trỏ trỏ vào lớp cơ sở Một trong những tính năng theng chốt của lớp dẫn xuất là con trỏ trỏ vào lớp dẫn xuất sẽ tương thích kiểu với một con trỏ của lớp cơ sở. Đó chính là sự thể hiện của tính đa hình (cùng một lớp cơ sở, nhưng mỗi con trỏ của lớp dẫn xuất có các hình thái thể hiện khác nhau). Tính đa hình n{y mang lại cho kĩ thuật lập trình hướng đối tượng thêm những ưu điểm trong việc tạo dựng những tính năng đơn giản nhưng hữu dụng và linh hoạt. Chúng ta sẽ bắt đầu viết chương trình về hình chữ nhật và tam giác. Lớp chữ nhật và tam giác kế thừa từ lớp đa gi|c v{ chúng có những phương C++ T r a n g | 160
  42. Chương 13. Lập trình hướng đối tượng thức th{nh viên riêng. Phương thức thành viên này cùng nội dung, nhưng lại có cách thể hiện khác nhau (cùng tính diện tích, nhưng diện tích hình chữ nhật và hình tam giác có công thức tính khác nhau). Ví dụ Kết quả #include Area of Rectangle: 20 using namespace std; Area of Triangle: 10 class Polygon{ protected: int w, h; public: void setValue(int w, int h){ this->w = w; this->h = h; } }; class Rectangle:public Polygon{ public: int area(){ return w*h; } }; class Triangle:public Polygon{ public: int area(){ return w*h/2; } }; int main() { Rectangle rec; Triangle tri; Polygon *pol1 = &rec; Polygon *pol2 = &tri; pol1->setValue(4, 5); pol2->setValue(4, 5); C++ T r a n g | 161
  43. Chương 13. Lập trình hướng đối tượng cout area() hay pol2- >area()). Bởi vì phương thức area() không phải là một thành viên của Polygon. Để giải quyết vấn đề này, chúng ta sẽ sử dụng phương thức thành viên ảo. Như chúng ta thấy trong ví dụ n{y, hai đối tượng tri v{ rec l{ hai đối tượng khác nhau thừa kế từ polygon. Nhưng chúng có cùng phương thức area để tính diện tích. Tuy nhiên, cách tính diện tích của hình chữ nhật và hình tam giác là hoàn toàn khác nhau. Tính đa hình. Là tính chất thể hiện nhiều hình thái của đối tượng. C|c đối tượng khác nhau có thể có cùng phương thức thực thi một hành động. Nhưng mỗi đối tượng lại thực thi h{nh động theo cách riêng của mình, mà không giống nhau cho tất cả c|c đối tượng. Thành viên ảo Để quy định một phương thức là ảo, chúng ta sử dụng từ khóa virtual. Nhờ v{o phương thức ảo, ta có thể định nghĩa lại một phương thức thành viên của lớp cơ sở bên trong lớp dẫn xuất. Ví dụ Kết quả #include Area of Rectangle: 20 using namespace std; Area of Triangle: 10 class Polygon{ Area of Polygon: 0 protected: int w, h; public: void setValue(int w, int h){ C++ T r a n g | 162
  44. Chương 13. Lập trình hướng đối tượng this->w = w; this->h = h; } virtual int area(){ return (0); }; }; class Rectangle:public Polygon{ public: int area(){ return w*h; } }; class Triangle:public Polygon{ public: int area(){ return w*h/2; } }; int main() { Rectangle rec; Triangle tri; Polygon pol; Polygon *pol1 = &rec; Polygon *pol2 = &tri; Polygon *pol3 = &pol; pol1->setValue(4, 5); pol2->setValue(4, 5); pol3->setValue(4, 5); cout area() area() area()<<endl; C++ T r a n g | 163
  45. Chương 13. Lập trình hướng đối tượng return 0; } Nếu gỡ bỏ từ khóa virtual thì kết quả sẽ là 0, 0 và 0. Sở dĩ l{ vì trong trường hợp n{y area() l{ phương thức thành viên của lớp Polygon. Dù phương thức n{y đ~ bị quá tải trong các lớp th{nh viên, nhưng c|c đối tượng *pol1, *pol2, *pol3 l{ c|c đối tượng của lớp Polygon, nên khi gọi phương thức area() là gọi đến phương thức area() của Polygon. Như vậy, với từ khóa virtual, c|c phương thức của các lớp dẫn xuất có thể thực hiện c|c phương thức th{nh viên riêng m{ phương thức th{nh viên đó có thể trùng tên với phương thức thành viên của lớp cơ sở. Ta cần lưu ý rằng, một số ngôn ngữ quy định virtual là mặc định cho các phương thức thành viên (nếu không khai báo từ khóa n{y) như Java, C#. Nhưng ngôn ngữ lập trình C++ thì không như vậy. Trong C++, từ khóa virtual là không mặc định cho các phương thức thành viên. Từ khóa virtual còn có thêm một ứng dụng trong định nghĩa lớp cơ sở ảo. Chúng ta xét ví dụ sau Hình 22 – Lớp cơ sở ảo C++ T r a n g | 164
  46. Chương 13. Lập trình hướng đối tượng Trong cây thừa kế này, ta thấy rằng lớp F thừa kế từ lớp D và E. Lớp D thừa kế từ lớp A và B; lớp E thừa kế từ lớp A v{ C. Điều đó có nghĩa l{ lớp F thừa kế lớp A 2 lần. Khi ta muốn truy cập đến thuộc tính a (thừa kế từ lớp D và E) thì trình biên dịch không nhận biết được ta muốn truy cập đến giá trị a của lớp nào. Ví dụ Kết quả #include error C2385: ambiguous access of 'a' using namespace std; class A { public: int a; }; class B { public: int b; }; class C { public: int c; }; class D:public A, public B { public: int d; }; class E:public A, public C C++ T r a n g | 165
  47. Chương 13. Lập trình hướng đối tượng { public: int e; }; class F:public D, public E { public: int f; }; int main() { F f; f.a = 1;//Lỗi return 0; } Trình biên dịch sẽ thông báo lỗi “error C2385: ambiguous access of 'a'”. Để khắc phục nhược điểm này, ta sử dụng lớp cơ sở ảo. Không có nhiều sự khác biệt trong trường hợp n{y. Đơn thuần chỉ bổ sung từ khóa virtual vào trước các mức thừa kế của lớp D và E (bởi hai lớp n{y đều thừa kế từ lớp A v{ được thừa kế bởi lớp F, đó l{ nguyên nh}n g}y nên lỗi thừa kế lớp A nhiều lần). Ví dụ Kết quả #include Không lỗi using namespace std; class A { public: int a; }; class B C++ T r a n g | 166
  48. Chương 13. Lập trình hướng đối tượng { public: int b; }; class C { public: int c; }; class D:virtual public A, public B { public: int d; }; class E:virtual public A, public C { public: int e; }; class F:public D, public E { public: int f; }; int main() { F f; f.a = 1; return 0; } C++ T r a n g | 167
  49. Chương 13. Lập trình hướng đối tượng Thêm một khái niệm mà ta cần quan tâm – quá tải hàm. Chúng ta cần lưu ý, khái niệm chồng chất hàm thành viên khác với khái niệm quá tải hàm thành viên (đôi lúc gọi l{ ghi đè hàm thành viên). Chồng chất hàm thành viên (overload) là việc quy định nhiều h{m cùng tên nhưng kh|c tham số, các hàm này là thành viên của cùng một lớp nội tại hoặc các hàm toàn cục. Trong khi đó, qu| tải hàm thành viên (override) là các hàm có cấu trúc giống nhau nhưng thuộc hai lớp khác nhau và một trong số chúng thừa kế từ lớp còn lại, khi đó, ta nói rằng phương thức của lớp cha đ~ bị phương thức của lớp con quá tải. Lớp cơ sở trừu tượng Lớp cơ sở trừu tượng (abstract base class) có nhiều nét tương đồng với lớp Polygon của ví dụ trên. Chỉ có sự khác biệt l{ phương thức area() của lớp Polygon không thực hiện hoàn toàn chức năng của một h{m m{ đơn thuần ta sẽ khai báo virtual int area() = 0;. Nghĩa l{ ta chỉ bổ sung vào giá trị 0 sau toán tử gán. Với sự khai báo dạng n{y, người ta gọi l{ phương thức ảo thuần túy (pure virtual function). Một lớp được gọi là lớp cơ sở trừu tượng, khi nó chứa ít nhất một phương thức thành viên ảo thuần túy. Sự khác biệt căn bản giữa lớp cơ sở trừu tượng và lớp đa hình l{ lớp cơ sở trừu tượng không thể tạo ra một sự thể hiện cho nó. Nghĩa l{ ta không thể viết Polygon pol; như trong ví dụ trên. Ta chỉ có thể sử dụng một con trỏ, để trỏ đến nó và sử dụng c|c tính năng đa hình của nó mà thôi. Ví dụ Kết quả #include Area of Rectangle: 20 using namespace std; Area of Triangle: 10 class Polygon{ protected: int w, h; public: void setValue(int w, int h){ this->w = w; this->h = h; } virtual int area() = 0; }; C++ T r a n g | 168
  50. Chương 13. Lập trình hướng đối tượng class Rectangle:public Polygon{ public: int area(){ return w*h; } }; class Triangle:public Polygon{ public: int area(){ return w*h/2; } }; int main() { Rectangle rec; Triangle tri; /* Polygon pol; Phát sinh lỗi vì Polygon là một lớp trừu tượng, ta không thể tạo một thể hiện cho lớp trừu tượng */ Polygon *pol1 = &rec; Polygon *pol2 = &tri; pol1->setValue(4, 5); pol2->setValue(4, 5); cout area() area()<<endl; return 0; } Trong ví dụ trên, ta đ~ sử dụng một phương thức ảo thuần túy đơn giản. Khi khai báo một phương thức ảo thuần túy, ta cần chú ý: . Tham số của nó nếu rỗng thì ta quy định là void. Nếu nó là một kiểu tham chiếu (như c|c đối tượng của lớp) thì ta nên quy định là con trỏ void* và sau này, khi cần sử dụng đến nó, ta có thể chuyển đổi con trỏ void sang con trỏ đối tượng. . Giá trị trả về trong phương thức ảo thuần túy NÊN quy định là 0. C++ T r a n g | 169
  51. Chương 13. Lập trình hướng đối tượng Ví dụ sau đ}y sẽ cho ta thấy cách sử dụng lớp trừu tượng trong trường hợp tham số bên trong phương thức ảo thuần túy là một đối tượng. Ta có một lớp trừu tượng Point (lớp điểm) và hai lớp Point2D và Point3D thừa kế thừa Point. Lớp Point là một lớp trừu tượng chứa phương thức ảo thuần túy là KhoangCach. Các lớp Point2D và Point3D sẽ quá tải phương thức ảo thuần túy này. Ví dụ Kết quả #include .:: Nhap toa do cho #include hai Point2D u va v ::. using namespace std; + Toa do u: class Point 1 { 1 protected: + Toa do v: float x, y; 1 public: 2 float& getX(void); D(u,v) = 1 float& getY(void); .:: Nhap toa do cho virtual float KhoangCach(void*)=0; }; hai Point3D u va v ::. float& Point::getX(void) + Toa do u: { 1 return x; 1 } 1 + Toa do v: float& Point::getY(void) { 1 return y; 1 } 2 D(u3,v3) = 1 class Point2D:public Point { public: float KhoangCach(void*); }; class Point3D:public Point { C++ T r a n g | 170
  52. Chương 13. Lập trình hướng đối tượng float z; public: float& getZ(void); float KhoangCach(void*); }; float Point2D::KhoangCach(void* p) { Point2D* q = (Point2D*)(p); return sqrt(pow(q->x-x,2)+pow(q->y-y,2)); } float& Point3D::getZ(void) { return z; } float Point3D::KhoangCach(void* p) { Point3D* q = (Point3D*)(p); return sqrt(pow(q->x-x,2)+pow(q->y- y,2)+pow(q->z-z,2)); } int main() { Point2D v2; Point2D u2; cout >v2.getX(); cin>>v2.getY(); cout >u2.getX(); cin>>u2.getY(); Point* u = &v2; Point* v = &u2; cout KhoangCach(v); Point3D v3; Point3D u3; cout<<endl<<".:: Nhap toa do cho hai C++ T r a n g | 171
  53. Chương 13. Lập trình hướng đối tượng Point3D u va v ::." >v3.getX(); cin>>v3.getY(); cin>>v3.getZ(); cout >u3.getX(); cin>>u3.getY(); cin>>u3.getZ(); Point* p = &v3; Point* q = &u3; cout KhoangCach(q); cout<<endl; return 0; } Giải thích: chúng ta cần chú ý rằng, nếu khai báo một lớp trừu tượng (chứa phương thức ảo thuần túy) thì lớp thừa kế từ nó nên có một phương thức có khai báo hoàn toàn trùng khớp với phương thức ảo thuần túy này (về tên gọi, danh sách tham số; chỉ trừ phép gán = 0 và từ khóa virtual sẽ không xuất hiện trong phương thức quá tải của lớp con). Nếu ta khai báo không trùng khớp, thì lớp con sẽ có phương thức ảo thuần túy này, và hiển nhiên nó cũng sẽ là lớp trìu tượng. Trong ví dụ này, khoảng cách có thể là giữa hai điểm Point2D hoặc hai điểm Point3D. Do đó, trong lớp trừu tượng, ta sẽ sử dụng void*. Trong lớp con, ta sẽ chuyển đổi từ void* sang Point2D* hoặc Point3D* tùy thuộc vào lớp Point2D hay Point3D. Tính trừu tượng hóa - Abstraction Tính trừu tượng hóa là tính chất chỉ tập trung vào những phần cốt lõi của đối tượng, bỏ qua những tiểu tiết không cần thiết. Nó còn thể hiện ở lớp trừu tượng cơ sở: lớp trừu tượng cơ sở chứa c|c đặc tính chung, tổng quát cho một nhóm đối tượng. Khi đó, nhóm đối tượng sẽ thừa kế từ lớp trừu tượng n{y để nhận các thuộc tính chung, đồng thời bổ sung thêm tính năng mới. Khi phân tích một nhóm đối tượng, ta thường tìm ra c|c điểm chung v{ đặc trưng cho c|c đối tượng, rồi từ đó x}y dựng nên một lớp trừu tượng cơ sở để chứa c|c phương thức t|c động đến c|c c|c đặc trưng chung đó. Mỗi một đối tượng trong nhóm đối tượng trên khi thừa kế từ lớp trừu tượng cơ sở C++ T r a n g | 172
  54. Chương 13. Lập trình hướng đối tượng sẽ có phương thức đặc trưng cho nhóm đối tượng này. Tính trừu tượng cũng l{ một đặc trưng của ngôn ngữ lập trình hướng đối tượng. Hàm mẫu – Template Function Trong nhiều trường hợp chúng ta cần xây dựng hàm mà kiểu dữ liệu của các tham số và kiểu dữ liệu của hàm trả về l{ không tường minh (có nghĩa l{ chúng có dạng tổng quát, có thể là số nguyên, số nguyên d{i, x}u ). Khi đó, để giải quyết vấn đề này, chúng ta có thể sử dụng quá tải h{m như trên, con trỏ hàm và thêm một cách thức nữa, đó l{ h{m mẫu – template function. Để khai báo kiểu dữ liệu không tường minh này, chúng ta có thể khai b|o như sau: template Ví dụ sau đ}y minh họa việc sử dụng hàm mẫu template function. Ví dụ Kết quả #include 3 using namespace std; abcd template T add(T a, T b) { return a+b; } int main() { cout (1, 2) (“ab”, “cd”) . Như vậy, trong ví dụ trên, đối tượng cout thứ nhất in ra giá trị tổng của hai số nguyên 1 v{ 2. Đối tượng cout thứ hai in ra giá trị ghép nối của hai x}u “ab” v{ “cd”. Lớp mẫu – Template class Tương tự hàm template, ta cũng có khai b|o lớp template. template C++ T r a n g | 173
  55. Chương 13. Lập trình hướng đối tượng class mypair{ T values[2]; public: mypair(T first, T second) { values[0]=first; values[1]=second; } }; int main() { mypair x(2, 3); mypair f(2.3, 3.3); return 0; } Ta có thể sử hàm tạo với tham số first và second thuộc loại T bằng các tham số cụ thể được ấn định. Ví dụ mypair x(1, 2) sẽ sử dụng hàm tạo của lớp mypair để tạo đối tượng chứa hai thuộc tính là 1 và 2 thuộc số nguyên. Tương tự cho trường hợp mypair f(2.3, 3.3). Khi cần sử dụng nhiều kiểu dữ liệu không tường minh, ta có thể bổ sung thêm trong khai báo template: template Và khi triệu gọi phương thức, ta chỉ đơn thuần chỉ đích danh kiểu dữ liệu tương ứng: myclass đối_tượng; Lớp template được sử dụng rộng rãi trong lập trình C++ bởi có nhiều lớp có chung c|c phương thức và dữ liệu nhưng chúng chỉ khác nhau kiểu dữ liệu cho các biến thành viên. Ví dụ như trong lớp số phức: các dữ liệu thành viên là phần thực và phần ảo có thể là số nguyên, số thực. Ví dụ sau đ}y sẽ xây dựng lớp số phức theo hướng tiếp cận template class. Ví dụ Kết quả #include real=1 image=2 using namespace std; real=3 image=4 template 4+I*6 C++ T r a n g | 174
  56. Chương 13. Lập trình hướng đối tượng class complex{ private: T real; T imag; public: complex(void); complex(T, T); complex(const complex &); complex operator+(const complex &); template friend basic_ostream & operator &, const complex &); template friend basic_istream & operator>>(basic_istream &, complex &); }; template complex ::complex(void) { real = 0; imag = 0; } template complex ::complex(T real, T imag) { this->real = real; this->imag = imag; } template complex ::complex(const complex & c) { this->real = c.real; this->imag = c.imag; } template complex complex ::operator+(const complex & c) { C++ T r a n g | 175
  57. Chương 13. Lập trình hướng đối tượng return complex(real+c.real, imag+c.imag); } template basic_ostream & operator & os, const complex & c) { os basic_istream & operator>>(basic_istream & is, complex & c) { cout >c.real; cout >c.imag; return is; } int main () { complex c; complex d; cin>>c>>d; complex x = c + d; cout v{o bên trước khai báo lớp v{ khai b|o phương thức có sử dụng kiểu dữ liệu dùng chung T. Riêng đối với phương thức nhập xuất dữ liệu, cần lưu ý: ta không thể sử dụng kiểu dữ liệu thông thường là ostream& và istream&, m{ thay v{o đó l{ basic_ostream v{ basic_istream . Đ}y cũng l{ hai lớp template với hai đối số. Điều này giải thích tại sao trước các toán tử nhập/xuất lại bổ sung thêm khai báo template . Chúng ta cũng cần chú ý thêm một điểm, tất cả các từ complex trong ví dụ trên được phân thành hai nhóm: nhóm kiểu dữ liệu complex và nhóm C++ T r a n g | 176
  58. Chương 13. Lập trình hướng đối tượng tên hàm complex. Tên hàm tạo complex – ta không bổ sung kiểu dữ liệu chỉ định T – chỉ đơn thuần là complex. Đối với các từ complex còn lại, nhất thiết phải chỉ định kiểu dữ liệu T – complex . Định nghĩa class template chỉ có phạm vi tác dụng trong lớp hoặc h{m được chỉ định ngay sau nó. Nó không phải là một khai báo có thể dùng chung. Điều này giải thích vì sao ta lại sử dụng quá nhiều khai b|o class template như trên. Tổng kết về lập trình hướng đối tượng Cơ sở của lập trình hướng đối tượng đó l{ c|c đối tượng và lớp. Đối tượng là một thực thể trong thế giới thực. Lớp mô tả đối tượng bằng ngôn ngữ lập trình. Ngược lại, một đối tượng là một sự thể hiện (instance) của lớp. Ngôn ngữ lập trình hướng đối tượng có 4 đặc trưng cơ bản: - Tính trừu tượng hóa dữ liệu (abstraction): là khía cạnh mà các ngôn ngữ không quan t}m đến những tiểu tiết của đối tượng. Nó chỉ tập trung vào những thứ cốt lõi của đối tượng. Tính trừu tượng còn thể hiện ở khái niệm lớp trừu tượng cơ sở. - Tính đóng gói (encapsulation) hay tính đóng gói và che dấu thông tin (encapsulation and information hiding): là tính chất chỉ mức độ chia sẻ thông tin. Một đối tượng khác không có quyền truy cập và làm thay đổi thông tin của đối tượng nội tại, chỉ có đối tượng nội tại mới quyết định có nên thay đổi thông tin của mình hay không. - Tính đa hình (polymorphism): là nhiều hình th|i. C|c đối tượng khác nhau có thể có cùng phương thức thực thi h{nh động, nhưng c|ch thức thực thi h{nh động đó có thể khác nhau. - Tính thừa kế (hay kế thừa - inheritance): là tính chất cho phép c|c đối tượng tiếp nhận dữ liệu từ một đối tượng khác mà nó thừa kế. Nhờ vào tính thừa kế, c|c đối tượng có thể sử dụng c|c tính năng của đối tượng khác. Tính thừa kế chia làm hai loại: đơn thừa kế v{ đa thừa kế. Đơn thừa kế là tính chất chỉ cho phép một lớp thừa kế từ một lớp cơ sở; còn tính đa thừa kế cho phép một lớp kế thừa từ nhiều lớp cơ sở. C++ là ngôn ngữ lập trình hỗ trợ đa thừa kế. C++ T r a n g | 177
  59. Chương 14. Namespace CHƯƠNG 14. NAMESPACE Từ khóa namespace Nhờ vào namespace, ta có thể nhóm các thực thể như lớp, đối tượng và các h{m dưới một tên gọi tương ứng với từ khóa namespace. Theo cách này, các phạm vi toàn cục lại được chia nhỏ ra thành các phạm vi toàn cục con, mà mỗi một phạm vi toàn cục con này có một tên gọi riêng. Để khai báo namespace, ta sử dụng từ khóa namespace theo cú pháp sau namespace tên_của_namespace { các_thực_thể } Để truy cập đến các thực thể của namespace, ta sử dụng toán tử phạm vi :: Ví dụ Kết quả #include 5 using namespace std; 3.14 namespace first { int var = 5; } namespace second { double var = 3.14; } int main() { cout<<first::var<<endl; cout<<second::var<<endl; return 0; } Như ta thấy, trong ví dụ này có hai biến toàn cục l{ var (lưu ý, hai biến var n{y đều là biến toàn cục vì phạm vi hoạt động của nó là toàn bộ chương trình). Dù l{ trùng tên, nhưng do chúng thuộc vào các namespace khác nhau, nên khai báo này là hợp lệ. Trang | 178
  60. Chương 14. Namespace Từ khóa using Từ khóa using được sử dụng để đưa một tên gọi từ namespace sang vùng khai báo hiện tại. Khi sử dụng using namespace tên_namespace, chúng ta không cần sử dụng tên_namespace khi gọi đến thực thể của nó. Ví dụ Kết quả #include 5 using namespace std; 5 namespace first { int x = 5; int y = 5; } namespace second { double x = 1.60; double y = 3.14; } int main() { using namespace first; cout 5 using namespace std; 3.14 namespace first { int x = 5; int y = 5; C++ T r a n g | 179
  61. Chương 14. Namespace } namespace second { double x = 1.60; double y = 3.14; } int main() { using first::x; using second::y; cout 5 using namespace std; 5 namespace first 1.6 { 3.14 int x = 5; int y = 5; } namespace second { double x = 1.60; double y = 3.14; } int main() { { using namespace first; cout<<x<<endl; cout<<y<<endl; } { C++ T r a n g | 180
  62. Chương 14. Namespace using namespace second; cout<<x<<endl; cout<<y<<endl; } return 0; } Tái định danh cho namespace Nếu muốn khai báo một tên mới cho namespace đ~ tồn tại, hãy sử dụng cú pháp namespace tên_mới_của_namespace = tên_đã_khai_báo_của_name_space; Namespace std Tất cả các tệp trong thư viện chuẩn của C++ đều được khai b|o như l{ thực thể của namespace std. Điều đó giải thích tại sao khi viết một chương trình trong C++ có sử dụng các hàm nhập xuất cơ bản, các kiểu dữ liệu như string thì ta phải sử dụng khai báo using namespace std. C++ T r a n g | 181
  63. Chương 15. Ngoại lệ CHƯƠNG 15. NGOẠI LỆ Các ngoại lệ là cách thức giúp chúng ta t|c động ngược trở lại với các tình huống sinh ra ngoại lệ đó. Để nắm bắt được ngoại lệ, chúng ta sử dụng cú ph|p try catch hoặc throw. Mệnh đề try catch Nếu viết một chương trình có khả năng nảy sinh ngoại lệ, chúng ta cần đặt nó vào trong khối lệnh của từ khóa try, nếu ngoại lệ ph|t sinh, h{nh động xử lý sẽ được đặt trong khối lệnh của từ khóa catch. Ví dụ Kết quả #include 20 using namespace std; int main() { int a; try{ throw 20; }catch(int e){ cout<<e; } return 0; } Giải thích: Trong chương trình n{y, lệnh throw đang cố vượt qua một ngoại lệ tương ứng với mã 20, nhưng ngoại lệ này bị nắm bắt bởi câu lệnh try catch. Do ngoại lệ phát sinh, nên lệnh trong mệnh đề catch được thực thi. Nếu có nhiều ngoại lệ phát sinh, ta có thể sử dụng cấu trúc đa tầng của mệnh đề catch: try{ }catch( ){ }catch( ){ } Mệnh đề throw Khi khai báo một hàm, nếu trong h{m đó có khả năng ph|t sinh ngoại lệ, chúng ta có thể chỉ định từ khóa throw cho nó type tên_hàm(danh_sách_tham_số) throw (int) Trang | 182
  64. Chương 15. Ngoại lệ Nếu chỉ có throw() – nghĩa l{ không chỉ định loại dữ liệu trong throw – thì hàm sẽ cho phép vượt qua mọi ngoại lệ. Nếu hàm không có mệnh đề throw thì sẽ không được phép vượt qua ngoại lệ. Thư viện chuẩn exception Thư viện chuẩn của C++ cung cấp cho chúng ta một thư viện để quản lý các ngoại lệ đó l{ exception. Nó nằm trong namespace std. Lớp này có một hàm tạo mặc định, một hàm tạo sao chép, các toán tử, hàm hủy và một hàm thành viên ảo what(). Hàm này trả về con trỏ kí tự phát sinh ngoại lệ. Nó có thể được quá tải trong lớp dẫn xuất. Ví dụ Kết quả #include Co ngoai le ! #include using namespace std; class myexception:public exception{ virtual const char* what() const throw(){ return “Co ngoai le !”; } }; int main(){ try{ myexception myex; throw myex; }catch(exception& e){ cout<<e.what(); } return 0; } Mỗi khi có ngoại lệ xảy ra, mệnh đề catch sẽ được thực hiện, và ngoại lệ sẽ được nắm bắt, kết quả in ra luôn l{ c}u thông b|o “Co ngoai le”. Khi ta xây dựng một ứng dụng có khả năng ph|t sinh ngoại lệ, ta cần quy định lớp đối tượng được xây dựng phải thừa kế từ lớp exception n{y. Điều này giúp chúng ta có thể nắm bắt ngoại lệ và bỏ qua chúng bởi trong một số trường hợp các ngoại lệ này có thể gây ra các lỗi không mong muốn cho ứng dụng. Ví dụ sau đ}y giúp ứng dụng của chúng ta vượt qua phép chia cho 0 và tiếp tục thực thi chương trình. C++ T r a n g | 183
  65. Chương 15. Ngoại lệ Ví dụ Kết quả #include Chia cho 0 using namespace std; class MyNumber:public exception { private: float x, y; public: virtual const char* what() const throw() { return "Chia cho 0"; } MyNumber(float, float); float DivMe(void); }; MyNumber::MyNumber(float x, float y) { this->x = x; this->y = y; } float MyNumber::DivMe(void) { x/y; } int main() { MyNumber m(1, 0); try { m.DivMe(); throw m; } catch(exception& e) { cout<<e.what(); } C++ T r a n g | 184
  66. Chương 15. Ngoại lệ return 0; } Giải thích: trong ví dụ này, lớp khai báo có khả năng ph|t sinh ngoại lệ vì nó thực hiện phép chia (ngoại lệ tương ứng l{ chia cho 0). Điều này giải thích vì sao ta cần cho nó thừa kế từ lớp exception. Mỗi khi giá trị nhập vào cho biến thành viên y là 0, thì ngoại lệ “Chia cho 0” sẽ phát sinh. Khi ngoại lệ phát sinh, ta sử dụng cú ph|p try để bao bọc quanh vùng lệnh phát sinh ra ngoại lệ (cụ thể là m.DivMe()). Khi ngoại lệ phát sinh, mệnh đề catch sẽ thực thi và in ra lỗi tương ứng với hàm thành viên what – tức l{ “Chia cho 0”. C++ T r a n g | 185
  67. Chương 16. L{m việc với file CHƯƠNG 16. LÀM VIỆC VỚI FILE C++ cung cấp cho ta các lớp sau đ}y để làm việc với file ofstream: lớp ghi dữ liệu ra file. ifstream: lớp đọc dữ liệu từ file. fstream: lớp để đọc/ghi dữ liệu từ/lên file. Các lớp này là dẫn xuất trực tiếp hoặc gián tiến từ lớp istream và ostream. Chúng ta sử dụng đối tượng cin là một thể hiện của lớp istream và cout là một thể hiện của lớp ostream. Chúng ta cũng có thể sử dụng c|c đối tượng của lớp ofstream, ifstream hoặc fstream để làm việc trực tiếp với file. Ví dụ sau đ}y sẽ cho thấy điều này Ví dụ #include #include using namespace std; int main(){ ofstream myfile; myfile.open(“example.txt”); myfile.<<”Ghi du lieu ra file”; myfile.close(); return 0; } Ví dụ này chỉ đơn thuần ghi c}u “Ghi du lieu ra file” lên tệp example.txt. Chúng ta sẽ nghiên cứu từng bước khi làm việc với c|c đối tượng của ba lớp mà chúng ta nêu ở trên Mở file Để mở file trong chương trình bằng một đối tượng stream, chúng ta sử dụng hàm thành viên open(tên_file, chế_độ_mở). Trong đó, Trang | 186
  68. Chương 16. L{m việc với file - tên_file: là tên của file mà chúng ta cần mở. Ta cần đảm bảo cung cấp đường dẫn chính x|c đến tập tin này. Ta cũng cần lưu ý đường dẫn đến tập tin. Đường dẫn có thể l{ đường dẫn tuyệt đối hoặc tương đối. Nếu cung cấp đường dẫn tương đối, ta cần tuân thủ nguyên tắc như khi l{m việc với tệp .cpp v{ .h như tôi đ~ trình b{y ở trên. - chế_độ_mở: là tham số tùy chọn, thường trong C++ nó có thể là các cờ hiệu sau đ}y: Cờ hiệu Giải thích ios::in Mở file để đọc. ios::out Mở file để ghi. ios::binary Mở file ở chế độ nhị ph}n (thường áp dụng cho các file mã hóa). ios::ate Thiết lập vị trí khởi tạo tại vị trí cuối cùng của file. Nếu cờ hiệu này không thiết lập bất kì giá trị nào, vị trí khởi tạo sẽ đặt ở đầu file. ios::app Mọi dữ liệu được ghi ra file sẽ tiến hành bổ sung vào cuối file (không ghi đè lên file). Cờ hiệu này chỉ có thể sử dụng trong tác vụ mở file để ghi. ios::trunc Nếu một file được mở để ghi đ~ tồn tại, nó sẽ ghi đè lên nội dung cũ. Các cờ hiệu này có thể được kết hợp bằng cách sử dụng toán tử dịch bit OR (|). Ví dụ, tôi muốn mở một file nhị phân example.bin để ghi dữ liệu và bổ sung dữ liệu ở cuối file này, tôi có thể viết như sau ofstream myfile; myfile.open(“example.bin”, ios::out|ios::app|ios::binary); Thành viên open của các lớp ofstream, ifstream và fstream có tham số chế_độ_mở mặc định (trong trường hợp tham số n{y không được chỉ định) được đưa ra trong bảng sau: Lớp chế_độ_mở mặc định ofstream ios::out ifstream ios::in fstream ios::in|ios::out Nếu tham số được ấn định một giá trị cụ thể, thì tham số được sử dụng sẽ ghi đè lên tham số mặc định mà không phải là kết hợp với tham số mặc định. Ví dụ, nếu sử dụng ofstream để mở file với tham số chế_độ_mở được C++ T r a n g | 187
  69. Chương 16. L{m việc với file quy định là ios::binary, thì tham số mở sẽ là ios::binary mà không phải là ios::out|ios::binary. Nếu sử dụng hàm khởi tạo cho các lớp n{y, thì phương thức thành viên open sẽ tự động được triệu gọi. Nghĩa l{ ta có thể viết ofstream myfile(“example.bin”, ios::out|ios::app, ios::binary); thay cho cách viết ở trên. Để kiểm tra một file đ~ mở th{nh công hay chưa, chúng ta có thể sử dụng phương thức is_open. Nếu đ~ mở thành công, nó sẽ trả về giá trị true và ngược lại, nếu mở không thành công, nó sẽ trả về giá trị false. Đóng file Khi chúng ta hoàn tất công việc với một file, chúng ta cần thực hiện thao tác đóng file lại. Tác vụ này là bắt buộc nếu ta đ~ ho{n tất các tác vụ trên file. Khi đó, ta chỉ đơn thuần triệu gọi phương thức thành viên close myfile.close(); Nếu phương thức hủy của đối tượng được triệu gọi, phương thức close sẽ tự động được gọi theo. File văn bản Đối với một file văn bản, thì cờ hiệu ios::binary sẽ không bao giờ được sử dụng. Những file văn bản chỉ đơn thuần chứa văn bản. Để đọc ghi dữ liệu trên file này ta sử dụng toán tử xuất – nhập dữ liệu ( >). Ghi dữ liệu lên file văn bản #include #include using namespace std; int main(){ ofstream myfile (“example.txt”); if (myfile.is_open(){ myfile<<”Dong 1 da ghi\n”; myfile<<”Dong 2 da ghi\n”; myfile.close(); } else cout<<”Khong the ghi du lieu len file”; C++ T r a n g | 188
  70. Chương 16. L{m việc với file return 0; } Ví dụ trên cho thấy việc ghi dữ liệu lên file văn bản nhờ vào toán tử >. Đọc dữ liệu từ file văn bản #include #include #include using namespace std; int main(){ ifstream myfile (“example.txt”); if (myfile.is_open(){ while(!myfile.eof()){ getline(myfile, line); cout<<line<<endl; } myfile.close(); } else cout<<”Khong the ghi du lieu len file”; return 0; } Trong ví dụ này, chúng ta có sử dụng hàm thành viên eof của đối tượng ifstream. Hàm thành viên này có chức năng kiểm tra vị trí đọc đ~ là vị trí cuối cùng của file hay chưa, nếu chưa, dữ liệu từ file sẽ tiếp tục được đọc. Ngược lại, nó sẽ dừng việc đọc dữ liệu. Kiểm tra trạng thái của các cờ hiệu Ví dụ trên cho ta một các thức để kiểm tra trạng thái của các cờ hiệu. Bảng sau đ}y sẽ liệt kê các trạng thái cờ hiệu có thể được sử dụng trong C++. Trạng thái Giải thích bad() Nếu tác vụ đọc/ghi file bị lỗi, nó sẽ trả về giá trị true; ngược lại, nó sẽ trả về giá trị false. fail() Trả về giá trị true trong trường hợp như bad(), nhưng nếu gặp lỗi về định dạng, nó cũng trả về giá trị true (ví dụ đọc số từ một file văn bản). eof() Trả về giá trị true nếu file đ~ được đọc đến vị trí cuối C++ T r a n g | 189
  71. Chương 16. L{m việc với file cùng của file, ngược lại, trả về giá trị false. good() Nó sẽ trả về giá trị true nếu bad(), fail() và eof() không phát sinh lỗi. Để thiết lập lại các mức kiểm tra trạng thái cờ hiệu, ta sử dụng phương thức thành viên clear(). Con trỏ get và put Mọi đối tượng luồng xuất nhập đều có ít nhất một con trỏ luồng: - Luồng ifstream có con trỏ istream mà ta gọi là con trỏ get để trỏ vào phần tử có thể đọc dữ liệu. - Luồng ofstream có con trỏ ostream mà ta gọi là con trỏ put để trỏ vào phần tử có thể ghi dữ liệu. - Luồng fstream có cả hai con trỏ get v{ put để đọc và ghi dữ liệu. Những con trỏ luồng nội tại này trỏ vào vị trí đọc và ghi với luồng có thể sử dụng c|c h{m th{nh viên sau đ}y: Hàm thành viên tellg() và tellp() Hai hàm thành viên này không có tham số và trả về giá trị của một kiểu dữ liệu dạng pos_type. Kiểu dữ liệu này bản chất là một số nguyên integer. Nó mô tả vị trí hiện tại của của con trỏ luồng get và con trỏ luồng put. Hàm thành viên seekg() và seekp() Những hàm thành viên này cho phép chúng ta thay đổi vị trí hiện tại của con trỏ luồng get và put. Cả hai h{m n{y được chồng chất với hai prototype khác nhau. Prototype thứ nhất: seekg(vị_trí); seekp(vị_trí); Việc sử dụng c|c prototype n{y giúp l{m thay đổi vị trí tuyệt đối (vị trí này tính từ đầu file). Kiểu dữ liệu của tham số này trùng với kiểu dữ liệu của hai hàm tellg() và tellp() ở trên. Prototype thứ hai: seekg(vị_trí, kiểu); C++ T r a n g | 190
  72. Chương 16. L{m việc với file seekp(vị_trí, kiểu); Việc sử dụng prototype này sẽ l{m thay đổi vị trí hiện tại của con trỏ get và con trỏ put được x|c định theo vị trí tương đối theo tham số vị_trí và tham số kiểu. Tham số vị_trí của một thành viên thuộc kiểu dữ liệu off_type, nó cũng l{ một kiểu số nguyên, nó tương ứng với vị trí của con trỏ get/put được đặt vào. Tham số kiểu là một kiểu dữ liệu seekdir, nó là một kiểu enum để x|c định vị_trí của con trỏ get/put kể từ tham số kiểu, nó có thể nhận một trong các giá trị sau đ}y. ios::beg vị_trí được đếm từ vị trí bắt đầu của luồng ios::cur vị_trí được đếm từ vị trí hiện tại của luồng ios::end vị_trí được đếm từ vị trí cuối của luồng Điều n{y có nghĩa l{ h{m chồng chất hai tham số n{y cũng tương tự hàm một tham số, nhưng vị trí bắt đầu tính trong hàm một tham số luôn là từ vị trí đầu tiên, còn hàm hai tham số có ba vị trí có thể bắt đầu đếm – bắt đầu (ios::beg), hiện tại (ios::cur) hay cuối file (ios::end). Ta hãy quan sát ví dụ sau đ}y Ví dụ Kết quả 1. #include Size=10 bytes 2. #include 3. using namespace std; 4. int main(){ 5. long begin, end; 6. ifstream myfile(“example.txt”); 7. begin = myfile.tellg(); 8. myfile.seekg(0, ios::end); 9. end = myfile.tellg(); 10. myfile.close(); 11. cout<<”Size=”<<(end-begin)<<” bytes”; 12. return 0; 13. } Giải thích: trong chương trình trên chúng ta đang mở một file example.txt. Chúng ta đếm kích thước của file này. Khi mở file, con trỏ get sẽ đặt vào vị trí đầu file. Khi đó, dòng lệnh 7 sẽ gán giá trị khởi đầu cho biến begin (trong trường hợp này sẽ là 0). Dòng lệnh 8 sẽ đặt con trỏ get vào vị trí cuối cùng của file (vị trí 0 kể từ cuối file tính lên). Dòng lệnh 9 sẽ gán vị trí hiện tại – vị trí cuối file cho biến end. Điều đó có nghĩa l{ gi| trị end-begin chính là kích C++ T r a n g | 191
  73. Chương 16. L{m việc với file thước của file. Ta cũng cần lưu ý rằng, trong file văn bản, một kí tự tương ứng với 1 byte – đó cũng chính l{ quy định trong C++ (một kiểu char chiếm 1 byte). Hay nói chính x|c, chương trình n{y đếm số kí tự trong file văn bản. File nhị phân Đối với file nhị phân, việc đọc ghi dữ liệu bằng toán tử tích trách >> và toán tử chèn #include using namespace std; ifstream::pos_type size; char* memblock; int main(){ ifstream file(“example.bin”, ios::in|ios::binary|ios::ate); if(file.is_open()){ size = file.tellg(); memblock = new char[size]; file.seekg(0, ios::beg); file.read(memblock, size); file.close(); cout<<”Hoan tat !”; //Làm việc với dữ liệu trong con trỏ memblock C++ T r a n g | 192
  74. Chương 16. L{m việc với file delete[] memblock; } else cout<<”Khong mo duoc file.”; return 0; } Giải thích: trong chương trình, ta mở file example.bin. Chế độ mở file để đọc (ios::in), theo kiểu file nhị phần (ios::binary), đặt con trỏ get vào cuối file (ios::ate). Sau khi mở file, hàm file.tellg() sẽ cho biết kích thước thực của file. Sau đó h{m file.seekg sẽ đặt vị trí con trỏ get v{o đầu file (vị trí 0 kể từ vị trí đầu tiên) và tiến h{nh đọc theo khối bộ nhờ nhờ vào file.read. Sau khi hoàn tất, phương thức close được triệu gọi để kết thúc việc đọc file. Khi đó, dữ liệu từ file đ~ đọc vào mảng memblock. Chúng ta có thể bổ sung tác vụ thao tác với dữ liệu nếu muốn. Cuối cùng, con trỏ memblock sẽ bị xóa để giải phóng bộ nhớ. Bộ đệm và Đồng bộ hóa Khi thực thi các tác vụ đọc/ghi dữ liệu với file, chúng ta thực thi như trên nhưng thông qua một bộ đệm có kiểu dữ liệu streambuf. Bộ đệm này là một khối bộ nhớ đóng vai trò trung gian giữa các luồng và file vật lý. Ví dụ, với ofstream, mỗi thời điểm h{m put được gọi, kí tự không ghi trực tiếp lên file mà nó sẽ được ghi lên bộ đệm. Khi bộ đệm đầy, mọi dữ liệu chứa trong đó sẽ được ghi lên file (nếu đó l{ luồng ghi dữ liệu) hay xóa bỏ để làm rãnh bộ nhớ (nếu đó là luồng đọc dữ liệu). Tiến trình n{y được gọi l{ đồng bộ hóa và có các tình huống sau đ}y: - Khi file đ~ đóng: trước khi đóng một file, tất cả dữ liệu trong bộ nhớ nếu chưa đầy vẫn được đồng bộ và chuẩn bị để đọc/ghi lên file. - Khi bộ nhớ đầy: bộ đệm có kích thước giới hạn. Khi nó đầy, nó sẽ tự động đồng bộ hóa. - Bộ điều phối: khi các bộ điều phối được sử dụng trên luồng, một tiến trình đồng bộ dứt điểm sẽ được diễn ra. Những bộ điều phối này bao gồm: flush và endl. - Hàm thành viên sync(): nếu hàm thành viên sync() được triệu gọi, tiến trình đồng bộ hóa sẽ diễn ra. Hàm này trả về một kiểu integer (int) tương ứng với -1, nếu luồng không có bộ đệm liên kết hoặc trong trường hợp đọc/ghi thất bại. Ngược lại, nó sẽ trả về giá trị 0. C++ T r a n g | 193
  75. Chương 17. C|c lớp thư viện CHƯƠNG 17. CÁC LỚP THƯ VIỆN 1. Lớp số phức complex Đ}y l{ lớp template. Khi khởi tạo một lớp đối tượng số phức, ta cần chỉ định kiểu dữ liệu cho nó. Nó nằm trong thư viện complex. Ví dụ về việc khởi tạo một số phức: complex c(10.2, 3); sẽ khởi tạo một đối tượng số phức mà phần thực và phần ảo của nó là các số thực. Thông thường, kiểu dữ liệu được chỉ định cho lớp số phức là kiểu dữ liệu thực: float, double hoặc long double. Nhưng chúng ta hoàn toàn có thể sử dụng kiểu số nguyên. Các hàm thành viên của lớp số phức Tên thành viên Mức truy cập Chức năng real private Phần thực và phần ảo. imag Phương thức khởi tạo: public C|c phương thức khởi complex c; tạo: không tham số, có complex c(real, imag); hai tham số và hàm tạo complex c(d); sao chép. imag() public Phương thức getImage – trả về giá trị phần ảo. real() public Phương thức getReal – trả về giá trị của phần thực. operator= public Các hàm toán tử thành operator+= viên: toán tử gán, toán operator-= tử cộng hợp nhất, toán operator*= tử trừ hợp nhất, toán tử operator/= nhân hợp nhất, toán tử chia hợp nhất. _Add(complex) protected C|c phương thức cộng, _Sub(complex) trừ, nhân và chia hai số _Mul(complex) phức. _Div(complex) Trang | 194
  76. Chương 17. C|c lớp thư viện Các hàm toàn cục (hoặc hàm bạn) Tên phương thức Chức năng operator + Toán tử cộng hai số phức. operator – Toán tử trừ hai số phức. operator * Toán tử nhân hai số phức. operator / Toán tử chia hai số phức. operator = Toán tử gán số phức. operator == Toán tử so sánh bằng. operator != Toán tử so sánh khác. operator >> Toán tử nhập số phức. operator ) Trả về lũy thừa của số phức. Tham số thứ hai có thể là số phức, số thực, số nguyên. C++ T r a n g | 195