Giáo trình Lập trình hướng đối tượng

doc 117 trang phuongnguyen 4210
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Lập trình hướng đối tượng", để 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:

  • docgiao_trinh_lap_trinh_huong_doi_tuong.doc

Nội dung text: Giáo trình Lập trình hướng đối tượng

  1. CHƯƠNG 1 CÁC KHÁI NIỆM CƠ SỞ CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG Chương 1 trình bày những vấn đề sau:  Thảo luận về cách tiếp cận hướng đối tượng, những nhược điểm của lập trình truyền thống và các đặc điểm của lập trình hướng đối tượng.  Các khái niệm cơ sở của phương pháp hướng đối tượng: Đối tượng Lớp Trừu tượng hóa dữ liệu và bao gói thông tin Kế thừa Tương ứng bội Liên kết động Truyền thông báo  Các bước cần thiết để thiết kế chương trình theo hướng đối tượng  Các ưu điểm của lập trình hướng đối tượng  Các ngôn ngữ hướng đối tượng  Một số ứng dụng của lập trình hướng đối tượng 1.1. Giới thiệu 1.1.1. Tiếp cận hướng đối tượng Trong thế giới thực, chung quanh chúng ta là những đối tượng, đó là các thực thể có mối quan hệ với nhau. Ví dụ các phòng trong một công ty kinh doanh được xem như những đối tượng. Các phòng ở đây có thể là: phòng quản lý, phòng bán hàng, phòng kế toán, phòng tiếp thị, Mỗi phòng ngoài những cán bộ đảm nhiệm những công việc cụ thể, còn có những dữ liệu riêng như thông tin về nhân viên, doanh số bán hàng, hoặc các dữ liệu khác có liên quan đến bộ phận đó. Việc phân chia các phòng chức năng trong công ty sẽ tạo điều kiện dễ dàng cho việc quản lý các hoạt động. Mỗi nhân viên trong phòng sẽ điều khiển và xử lý dữ liệu của phòng đó. Ví dụ phòng kế toán phụ trách về lương bổng nhân viên trong công ty. Nếu bạn đang ở bộ phận tiếp thị và cần tìm thông tin chi tiết về lương của đơn vị mình thì sẽ gởi yêu cầu về phòng kế toán. Với cách làm này bạn được đảm bảo là chỉ có nhân viên của bộ phận kế toán được quyền truy cập dữ liệu và cung cấp thông tin cho bạn. Điều này cũng cho thấy rằng, không có người nào thuộc bộ phận khác có thể truy cập và thay đổi dữ liệu của bộ phận kế toán. Khái niệm như thế về đối tượng hầu như có thể được mở rộng đối với mọi lĩnh vực trong đời sống xã hội và hơn nữa - đối với việc tổ chức chương trình. Mọi ứng dụng có thể được định nghĩa như một tập các thực thể - hoặc các đối tượng, sao cho quá trình tái tạo những suy nghĩa của chúng ta là gần sát nhất về thế giới thực. Trong phần tiếp theo chúng ta sẽ xem xét phương pháp lập trình truyền thống để từ đó thấy rằng vì sao chúng ta cần chuyển sang phương pháp lập trình hướng đối tượng. 1.1.2. Những nhược điểm của lập trình hướng thủ tục Cách tiếp cận lập trình truyền thống là lập trình hướng thủ tục (LTHTT). Theo cách tiếp cận này thì một hệ thống phần mềm được xem như là dãy các công việc cần thực hiện như đọc dữ liệu, tính toán, xử lý, lập báo cáo và in ấn kết quả v.v Mỗi công việc đó sẽ được thực hiện bởi một số hàm nhất định. Như vậy trọng tâm của cách tiếp cận này là các hàm chức năng. LTHTT sử dụng kỹ thuật phân rã hàm chức năng theo cách tiếp cận trên xuống (top-down) để tạo ra cấu trúc phân cấp. Các ngôn ngữ lập trình bậc cao như COBOL, FORTRAN, PASCAL, C, v.v , là những ngôn ngữ lập trình hướng thủ tục. Những nhược điểm chính của LTHTT là: 1
  2.  Chương trình khó kiểm soát và khó khăn trong việc bổ sung, nâng cấp chương trình. Chương trình được xây dựng theo cách TCHTT thực chất là danh sách các câu lệnh mà theo đó máy tính cần thực hiện. Danh sách các lệnh đó được tổ chức thành từng nhóm theo đơn vị cấu trúc của ngôn ngữ lập trình và được gọi là hàm/thủ tục. Trong chương trình có nhiều hàm/thủ tục, thường thì có nhiều thành phần dữ liệu quan trọng sẽ được khai báo tổng thể (global) để các hàm/thủ tục có thể truy nhập, đọc và làm thay đổi giá trị của biến tổng thể. Điều này sẽ làm cho chương trình rất khó kiểm soát, nhất là đối với các chương trình lớn, phức tạp thì vấn đề càng trở nên khó khăn hơn. Khi ta muốn thay đổi, bổ sung cấu trúc dữ liệu dùng chung cho một số hàm/thủ tục thì phải thay đổi hầu như tất cả các hàm/thủ tục liên quan đến dữ liệu đó.  Mô hình được xây dựng theo cách tiếp cận hướng thủ tục không mô tả được đầy đủ, trung thực hệ thống trong thực tế.  Phương pháp TCHTT đặt trọng tâm vào hàm là hướng tới hoạt động sẽ không thực sự tương ứng với các thực thể trong hệ thống của thế giới thực. 1.1.3. Lập trình hướng đối tượng Lập trình hướng đối tượng (Object Oriented Programming - LTHĐT) là phương pháp lập trình lấy đối tượng làm nền tảng để xây dựng thuật giải, xây dựng chương trình. Đối tượng được xây dựng trên cơ sở gắn cấu trúc dữ liệu với các phương thức (các hàm/thủ tục) sẽ thể hiện được đúng cách mà chúng ta suy nghĩ, bao quát về thế giới thực. LTHĐT cho phép ta kết hợp những tri thức bao quát về các quá trình với những khái niệm trừu tượng được sử dụng trong máy tính. Điểm căn bản của phương pháp LTHĐT là thiết kế chương trình xoay quanh dữ liệu của hệ thống. Nghĩa là các thao tác xử lý của hệ thống được gắn liền với dữ liệu và như vậy khi có sự thay đổi của cấu trúc dữ liệu thì chỉ ảnh hưởng đến một số ít các phương thức xử lý liên quan. LTHĐT không cho phép dữ liệu chuyển động tự do trong hệ thống. Dữ liệu được gắn chặt với từng phương thức thành các vùng riêng mà các phương thức đó tác động lên và nó được bảo vệ để cấm việc truy nhập tùy tiện từ bên ngoài. LTHĐT cho phép phân tích bài toán thành tập các thực thể được gọi là các đối tượng và sau đó xây dựng các dữ liệu cùng với các phương thức xung quanh các đối tượng đó. Tóm lại LTHĐT có những đặc tính chủ yếu như sau: 1. Tập trung vào dữ liệu thay cho các phương thức. 2. Chương trình được chia thành các lớp đối tượng. 3. Các cấu trúc dữ liệu được thiết kế sao cho đặc tả được các đối tượng. 4. Các phương thức xác định trên các vùng dữ liệu của đối tượng được gắn với nhau trên cấu trúc dữ liệu đó. 5. Dữ liệu được bao bọc, che dấu và không cho phép các thành phần bên ngoài truy nhập tự do. 6. Các đối tượng trao đổi với nhau thông qua các phương thức. 7. Dữ liệu và các phương thức mới có thể dễ dàng bổ sung vào đối tượng nào đó khi cần thiết. 8. Chương trình được thiết kế theo cách tiếp cận bottom-up (dưới -lên). 1.2. Các khái niệm cơ bản của lập trình hướng đối tượng Những khái niệm cơ bản trong LTHĐT bao gồm: Đối tượng; Lớp; Trừu tượng hóa dữ liệu, bao gói thông tin; Kế thừa; Tương ứng bội; Liên kết động; Truyền thông báo. 1.2.1. Đối tượng Trong thế giới thực, khái niệm đối tượng được hiểu như là một thực thể, nó có thể là người, vật hoặc một bảng dữ liệu cần xử lý trong chương trình, Trong LTHĐT thì đối tượng là biến thể hiện của lớp. 2
  3. 1.2.2. Lớp Lớp là một khái niệm mới trong LTHĐT so với kỹ thuật LTHTT. Nó là một bản mẫu mô tả các thông tin cấu trúc dữ liệu và các thao tác hợp lệ của các phần tử dữ liệu. Khi một phần tử dữ liệu được khai báo là phần tử của một lớp thì nó được gọi là đối tượng. Các hàm được định nghĩa hợp lệ trong một lớp được gọi là các phương thức (method) và chúng là các hàm duy nhất có thể xử lý dữ liệu của các đối tượng của lớp đó. Mỗi đối tượng có riêng cho mình một bản sao các phần tử dữ liệu của lớp. Mỗi lớp bao gồm: danh sách các thuộc tính (attribute) và danh sách các phương thức để xử lý các thuộc tính đó. Công thức phản ánh bản chất của kỹ thuật LTHĐT là: Đối tượng = Dữ liệu + Phương thức Chẳng hạn, chúng ta xét lớp HINH_CN bao gồm các thuộc tính: (x1,y1) toạ độ góc trên bên trái, d,r là chiều dài và chiều rộng của HCN. Các phương thức nhập số liệu cho HCN, hàm tính diện tích, chu vi và hàm hiển thị. Lớp HINH_CN có thể được mô tả như sau: HINH_CN Thuộc tính : x1,y1 d,r Phương thức : Nhập_sl Diện tích Hình 2.2 Mô tả lớp HINH_CN Chu vi Hiển thị Chú ý: Trong LTHĐT thì lớp là khái niệm tĩnh, có thể nhận biết ngay từ văn bản chương trình, ngược lại đối tượng là khái niệm động, nó được xác định trong bộ nhớ của máy tính, nơi đối tượng chiếm một vùng bộ nhớ lúc thực hiện chương trình. Đối tượng được tạo ra để xử lý thông tin, thực hiện nhiệm vụ được thiết kế, sau đó bị hủy bỏ khi đối tượng đó hết vai trò. 1.2.3. Trừu tượng hóa dữ liệu và bao gói thông tin Trừu tượng hóa là cách biểu diễn những đặc tính chính và bỏ qua những chi tiết vụn vặt hoặc những giải thích. Khi xây dựng các lớp, ta phải sử dụng khái niệm trừu tượng hóa. Ví dụ ta có thể định nghĩa một lớp để mô tả các đối tượng trong không gian hình học bao gồm các thuộc tính trừu tượng như là kích thước, hình dáng, màu sắc và các phương thức xác định trên các thuộc tính này. Việc đóng gói dữ liệu và các phương thức vào một đơn vị cấu trúc lớp được xem như một nguyên tắc bao gói thông tin. Dữ liệu được tổ chức sao cho thế giới bên ngoài (các đối tượng ở lớp khác) không truy nhập vào, mà chỉ cho phép các phương thức trong cùng lớp hoặc trong những lớp có quan hệ kế thừa với nhau mới được quyền truy nhập. Chính các phương thức của lớp sẽ đóng vai trò như là giao diện giữa dữ liệu của đối tượng và phần còn lại của chương trình. Nguyên tắc bao gói dữ liệu để ngăn cấm sự truy nhập trực tiếp trong lập trình được gọi là sự che giấu thông tin. 1.2.4. Kế thừa Kế thừa là quá trình mà các đối tượng của lớp này được quyền sử dụng một số tính chất của các đối tượng của lớp khác. Sự kế thừa cho phép ta định nghĩa một lớp mới trên cơ sở các lớp đã tồn tại. Lớp mới này, ngoài những thành phần được kế thừa, sẽ có thêm những thuộc tính và các hàm mới. Nguyên lý kế thừa hỗ trợ cho việc tạo ra cấu trúc phân cấp các lớp. 3
  4. 1.2.5. Tương ứng bội Tương ứng bội là khả năng của một khái niệm (chẳng hạn các phép toán) có thể sử dụng với nhiều chức năng khác nhau. Ví dụ, phép + có thể biểu diễn cho phép “cộng” các số nguyên (int), số thực (float), số phức (complex) hoặc xâu ký tự (string) v.v Hành vi của phép toán tương ứng bội phụ thuộc vào kiểu dữ liệu mà nó sử dụng để xử lý. Tương ứng bội đóng vai quan trọng trong việc tạo ra các đối tượng có cấu trúc bên trong khác nhau nhưng cùng dùng chung một giao diện bên ngoài (như tên gọi). 1.2.6. Liên kết động Liên kết động là dạng liên kết các thủ tục và hàm khi chương trình thực hiện lời gọi tới các hàm, thủ tục đó. Như vậy trong liên kết động, nội dung của đoạn chương trình ứng với thủ tục, hàm sẽ không được biết cho đến khi thực hiện lời gọi tới thủ tục, hàm đó. 1.2.7. Truyền thông báo Các đối tượng gửi và nhận thông tin với nhau giống như con người trao đổi với nhau. Chính nguyên lý trao đổi thông tin bằng cách truyền thông báo cho phép ta dễ dàng xây dựng được hệ thống mô phỏng gần hơn những hệ thống trong thế giới thực. Truyền thông báo cho một đối tượng là yêu cầu đối tượng thực hiện một việc gì đó. Cách ứng xử của đối tượng được mô tả bên trong lớp thông qua các phương thức. Trong chương trình, thông báo gửi đến cho một đối tượng chính là yêu cầu thực hiện một công việc cụ thể, nghĩa là sử dụng những hàm tương ứng để xử lý dữ liệu đã được khai báo trong đối tượng đó. Vì vậy, trong thông báo phải chỉ ra được hàm cần thực hiện trong đối tượng nhận thông báo. Thông báo truyền đi cũng phải xác định tên đối tượng và thông tin truyền đi. Ví dụ, lớp CONGNHAN có thể hiện là đối tượng cụ thể được đại diện bởi Hoten nhận được thông báo cần tính lương thông qua hàm TINHLUONG đã được xác định trong lớp CONGNHAN. Thông báo đó sẽ được xử lý như sau: CONGNHAN.TINHLUONG (Hoten) Đối tượng Thông báo Trong chươngThông trình hướng tin đối tượng, mỗi đối tượng chỉ tồn tại trong thời gian nhất định. Đối tượng được tạo ra khi nó được khai báo và sẽ bị hủy bỏ khi chương trình ra khỏi miền xác định của đối tượng đó. Sự trao đổi thông tin chỉ có thể thực hiện trong thời gian đối tượng tồn tại. 1.3. Các bước cần thiết để thiết kế chương trình theo hướng đối tượng Chương trình theo hướng đối tượng bao gồm một tập các đối tượng và mối quan hệ giữa các đối tượng với nhau. Vì vậy, lập trình trong ngôn ngữ hướng đối tượng bao gồm các bước sau: 1. Xác định các dạng đối tượng (lớp) của bài tóan. 2. Tìm kiếm các đặc tính chung (dữ liệu chung) trong các dạng đối tượng này, những gì chúng cùng nhau chia xẻ. 3. Xác định lớp cơ sở dựa trên cơ sở các đặc tính chung của các dạng đối tượng. 4. Từ lớp cơ sở, xây dựng các lớp dẫn xuất chứa các thành phần, những đặc tính không chung còn lại của các dạng đối tượng. Ngoài ra, ta còn đưa ra các lớp có quan hệ với các lớp cơ sở và lớp dẫn xuất. 1.4. Các ưu điểm của lập trình hướng đối tượng Cách tiếp cận hướng đối tượng giải quyết được nhiều vấn đề tồn tại trong quá trình phát triển phần mềm và tạo ra được những sản phẩm phần mềm có chất lượng cao. Những ưu điểm chính của LTHĐT là: 4
  5. 1. Thông qua nguyên lý kế thừa, có thể loại bỏ được những đoạn chương trình lặp lại trong quá trình mô tả các lớp và mở rộng khả năng sử dụng các lớp đã được xây dựng. 2. Chương trình được xây dựng từ những đơn thể (đối tượng) trao đổi với nhau nên việc thiết kế và lập trình sẽ được thực hiện theo quy trình nhất định chứ không phải dựa vào kinh nghiệm và kỹ thuật như trước. Điều này đảm bảo rút ngắn được thời gian xây dựng hệ thống và tăng năng suất lao động. 3. Nguyên lý che giấu thông tin giúp người lập trình tạo ra được những chương trình an toàn không bị thay bởi những đoạn chương trình khác. 4. Có thể xây dựng được ánh xạ các đối tượng của bài toán vào đối tượng của chương trình. 5. Cách tiếp cận thiết kế đặt trọng tâm vào đối tượng, giúp chúng ta xây dựng được mô hình chi tiết và gần với dạng cài đặt hơn. 6. Những hệ thống hướng đối tượng dễ mở rộng, nâng cấp thành những hệ lớn hơn. 7. Kỹ thuật truyền thông báo trong việc trao đổi thông tin giữa các đối tượng giúp cho việc mô tả giao diện với các hệ thống bên ngoài trở nên đơn giản hơn. 8. Có thể quản lý được độ phức tạp của những sản phẩm phần mềm. Không phải trong hệ thống hướng đối tượng nào cũng có tất cả các tính chất nêu trên. Khả năng có các tính chất đó còn phụ thuộc vào lĩnh vực ứng dụng của dự án tin học và vào phương pháp thực hiện của người phát triển phần mềm. 1.5. Các ngôn ngữ hướng đối tượng Lập trình hướng đối tượng không là đặc quyền của một ngôn ngữ nào đặc biệt. Cũng giống như lập trình có cấu trúc, những khái niệm trong lập trình hướng đối tượng có thể cài đặt trong những ngôn ngữ lập trình như C hoặc Pascal, Tuy nhiên, đối với những chương trình lớn thì vấn đề lập trình sẽ trở nên phức tạp. Những ngôn ngữ được thiết kế đặc biệt, hỗ trợ cho việc mô tả, cài đặt các khái niệm của phương pháp hướng đối tượng được gọi chung là ngôn ngữ đối tượng. Dựa vào khả năng đáp ứng các khái niệm về hướng đối tượng, ta có thể chia ra làm hai loại: 1. Ngôn ngữ lập trình dựa trên đối tượng 2. Ngôn ngữ lập trình hướng đối tượng Lập trình dựa trên đối tượng là kiểu lập trình hỗ trợ chính cho việc bao gói, che giấu thông tin và định danh các đối tượng. Lập trình dựa trên đối tượng có những đặc tính sau: Bao gói dữ liệu Cơ chế che giấu và truy nhập dữ liệu Tự động tạo lập và xóa bỏ các đối tượng Phép toán tải bội Ngôn ngữ hỗ trợ cho kiểu lập trình trên được gọi là ngôn ngữ lập trình dựa trên đối tượng. Ngôn ngữ trong lớp này không hỗ trợ cho việc thực hiện kế thừa và liên kết động, chẳng hạn Ada là ngôn ngữ lập trình dựa trên đối tượng. Lập trình hướng đối tượng là kiểu lập trình dựa trên đối tượng và bổ sung thêm nhiều cấu trúc để cài đặt những quan hệ về kế thừa và liên kết động. Vì vậy đặc tính của LTHĐT có thể viết một cách ngắn gọn như sau: Các đặc tính dựa trên đối tượng + kế thừa + liên kết động. Ngôn ngữ hỗ trợ cho những đặc tính trên được gọi là ngôn ngữ LTHĐT, ví dụ như C++, Smalltalk, Object Pascal v.v Việc chọn một ngôn ngữ để cài đặt phần mềm phụ thuộc nhiều vào các đặc tính và yêu cầu của bài toán ứng dụng, vào khả năng sử dụng lại của những chương trình đã có và vào tổ chức của nhóm tham gia xây dựng phần mềm. 5
  6. 1.6. Một số ứng dụng của LTHĐT LTHĐT đang được ứng dụng để phát triển phần mềm trong nhiều lĩnh vực khác nhau. Trong số đó, có ứng dụng quan trọng và nổi tiếng nhất hiện nay là hệ điều hành Windows của hãng Microsoft đã được phát triển dựa trên kỹ thuật LTHĐT. Một số những lĩnh vực ứng dụng chính của kỹ thuật LTHĐT bao gồm: + Những hệ thống làm việc theo thời gian thực. + Trong lĩnh vực mô hình hóa hoặc mô phỏng các quá trình + Các cơ sở dữ liệu hướng đối tượng. + Những hệ siêu văn bản, multimedia + Lĩnh vực trí tuệ nhân tạo và các hệ chuyên gia. + Lập trình song song và mạng nơ-ron. + Những hệ tự động hóa văn phòng và trợ giúp quyết định. CHƯƠNG 2 CÁC MỞ RỘNG CỦA NGÔN NGỮ C++ Chương 2 trình bày những vấn đề sau đây:  Giới thiệu chung về ngôn ngữ C++  Một số mở rộng của ngôn ngữ C++ so với ngôn ngữ C  Các đặc tính của C++ hỗ trợ lập trình hướng đối tượng  Vào ra trong C++  Cấp phát và giải phóng bộ nhớ  Biến tham chiếu, hằng tham chiếu  Truyền tham số cho hàm theo tham chiếu  Hàm trả về giá trị tham chiếu  Hàm với tham số có giá trị mặc định  Các hàm nội tuyến (inline)  Hàm tải bội 2.1. Giới thiệu chung về C++ C++ là ngôn ngữ lập trình hướng đối tượng và là sự mở rộng của ngôn ngữ C. Vì vậy mọi khái niệm trong C đều dùng được trong C++. Phần lớn các chương trình C đều có thể chạy được trong C++. Trong chương này chỉ tập trung giới thiệu những khái niệm, đặc tính mới của C++ hỗ trợ cho lập trình hướng đối tượng. Một số kiến thức có trong C++ nhưng đã có trong ngôn ngữ C sẽ không được trình bày lại ở đây. 2.2. Một số mở rộng của C++ so với C 2.2.1. Đặt lời chú thích Ngoài kiểu chú thích trong C bằng /* */ , C++ đưa thêm một kiểu chú thích thứ hai, đó là chú thích bắt đầu bằng //. Kiểu chú thích /* */ được dùng cho các khối chú thích lớn gồm nhiều dòng, còn kiểu // được dùng cho các chú thích trên một dòng. Chương trình dịch sẽ bỏ qua tất cả các chú thích trong chương trình. Ví dụ: /* Đây là câu chú thích trên nhiều dòng */ // Đây là chú thích trên một dòng 6
  7. 2.2.2. Khai báo biến Trong C tất cả các câu lệnh khai báo biến, mảng cục bộ phải đặt tại đầu khối. Vì vậy vị trí khai báo và vị trí sử dụng của biến có thể ở cách khá xa nhau, điều này gây khó khăn trong việc kiểm soát chương trình. C++ đã khắc phục nhược điểm này bằng cách cho phép các lệnh khai báo biến có thể đặt bất kỳ chỗ nào trong chương trình trước khi các biến được sử dụng. Phạm vi hoạt động của các biến kiểu này là khối trong đó biến được khai báo. Ví dụ 2.1 Chương trình sau đây nhập một dãy số thực rồi sắp xếp theo thứ tự tăng dần: #include #include #include void main() { int n; printf("\n So phan tu cua day N="); scanf("%d",&n); float *x=(float*)malloc((n+1)*sizeof(float)); for (int i=0;i x[j]) { float tg=x[i]; x[i]=x[j]; x[j]=tg; } printf("\n Day sau khi sap xep\n"); for (i=0;i<n;++i) printf("%0.2f ",x[i]); getch(); } 2.2.3. Phép chuyển kiểu bắt buộc Ngoài phép chuyển kiểu bắt buộc được viết trong C theo cú pháp: (kiểu) biểu thức C++ còn sử dụng một phép chuyển kiểu mới như sau: Kiểu(biểu thức) Phép chuyển kiểu này có dạng như một hàm số chuyển kiểu đang được gọi. Cách chuyển kiểu này thường được sử dụng trong thực tế. 1 1 1 Ví dụ 2.2 Chương trình sau đây tính sau tổng S = 1 2 3 n Với n là một số nguyên dương nhập từ bàn phím. 7
  8. #include #include void main() { int n; printf("\n So phan tu cua day N="); scanf("%d",&n); float s=0.0; for (int i=1;i #include void main() { float a[20][20],smax; int m,n,i,j,imax,jmax; clrscr(); puts(" Cho biet so hang va so cot cua ma tran: "); scanf("%d%d",&m,&n); for (i=0;i<m;++i) for (j=0;j<n;++j) { printf("\n a[%d][%d]=",i,j); scanf("%f",&a[i][j]); } smax=a[0][0]; imax=0; jmax=0; for (i=0;i<m;++i) for(j=0;j<n;++j) if(smax<a[i][j]) { smax=a[i][j]; imax=i; 8
  9. jmax=j; } puts("\n\n Ma tran"); for (i=0;i >biến 1>>. . . >>biến N; Toán tử cin được định nghĩa trước như một đối tượng biểu diễn cho thiết bị vào chuẩn của C++ là bàn phím, cin được sử dụng kết hợp với toán tử trích >> để nhập dữ liệu từ bàn phím cho các biến 1, 2, , N. Chú ý: Để nhập một chuỗi không quá n ký tự và lưu vào mảng một chiều a (kiểu char) có thể dùng hàm cin.get như sau: cin.get(a,n); Toán tử nhập cin>> sẽ để lại ký tự chuyển dòng ’\n’ trong bộ đệm. Ký tự này có thể làm trôi phương thức cin.get. Để khắc phục tình trạng trên cần dùng phương thức cin.ignore(1) để bỏ qua một ký tự chuyển dòng. Để sử dụng các loại toán tử và phương thức nói trên cần khai báo tập tin dẫn hướng iostream.h 2.3.3. Định dạng khi in ra màn hình Để quy định số thực được hiển thị ra màn hình với p chữ số sau dấu chấm thập phân, ta sử dụng đồng thời các hàm sau: setiosflags(ios::showpoint); // Bật cờ hiệu showpoint(p) setprecision(p); Các hàm này cần đặt trong toán tử xuất như sau: cout<<setiosflag(ios::showpoint)<<setprecision(p); 9
  10. Câu lệnh trên sẽ có hiệu lực đối với tất cả các toán tử xuất tiếp theo cho đến khi gặp một câu lệnh định dạng mới. Để quy định độ rộng tối thiểu để hiển thị là k vị trí cho giá trị (nguyên, thực, chuỗi) ta dùng hàm: setw(k) Hàm này cần đặt trong toán tử xuất và nó chỉ có hiệu lực cho một giá trị được in gần nhất. Các giá trị in ra tiếp theo sẽ có độ rộng tối thiểu mặc định là 0, như vậy câu lệnh: cout #include #include void main() { struct { char ht[25]; float d1,d2,d3,td; }ts[100],tg; int n,i,j; clrscr(); cout > n; for (i=0;i >ts[i].d1>>ts[i].d2>>ts[i].d3; ts[i].td=ts[i].d1+ts[i].d2+ts[i].d3; } for (i=0;i<n-1;++i) for(j=i+1;j<n;++j) if(ts[i].td<ts[j].td) { tg=ts[i]; ts[i]=ts[j]; ts[j]=tg; } cout<< "\ Danh sach thi sinh sau khi sap xep :"; 10
  11. for (i=0;i #include #include void main() { float a[50][50],smax; int m,n,i,j,imax,jmax; clrscr(); cout >m>>n; for (i=0;i > a[i][j]; } smax= a[0][0]; imax=0; jmax=0; for (i=0;i<m;++i) for (j=0;j<n;++j) if (smax<a[i][j]) { smax=a[i][j]; imax=i; jmax=j; } cout << "\n\n Mang da nhap"; cout << setiosflags(ios::showpoint)<<setprecision(1); for (i=0;i<m;++i) for (j=0;j<n;++j) { if (j==0) cout<<"\n"; cout << setw(6)<<a[i][j]; } 11
  12. cout >n; p = new double[n] if (p == NULL) { cout #include #include 12
  13. #include #include struct TS { char ht[20]; long sobd; float td; }; void main(void) { TS *ts; int n; cout >n; ts = new TS[n+1]; if (ts == NULL) { cout > ts[i].sobd; cout >ts[i].td; } for (i=0;i<n-1;++i) for (int j=i+1;j<n;++j) if (ts[i].td<ts[j].td) { TS tg=ts[i]; ts[i]=ts[j]; ts[j]=tg; } cout << setiosflags(ios::showpoint)<<setprecision(1); for (i=0;i<n;++i) cout << "\n" << setw(20)<<ts[i].ht<<setw(6)<<ts[i].td; 13
  14. delete ts; getch(); } 2.5. Biến tham chiếu Trong C có 2 loại biến là: Biến giá trị dùng để chứa dữ liệu (nguyên, thực, ký tự, ) và biến con trỏ dùng để chứa địa chỉ. Các biến này đều được cung cấp bộ nhớ và có địa chỉ. C++ cho phép sử dụng loại biến thứ ba là biến tham chiếu. Biến tham chiếu là một tên khác (bí danh) cho biến đã định nghĩa trước đó. Cú pháp khai báo biến tham chiếu như sau: Kiểu &Biến tham chiếu = Biến; Biến tham chiếu có đặc điểm là nó được dùng làm bí danh cho một biến (kiểu giá trị) nào đó và sử dụng vùng nhớ của biến này. Ví dụ: Với câu lệnh: int a, &tong=a; thì tong là bí danh của biến a và biến tong dùng chung vùng nhớ của biến a. Lúc này, trong mọi câu lệnh, viết a hay viết tong đều có ý nghĩa như nhau, vì đều truy nhập đến cùng một vùng nhớ. Mọi sự thay đổi đối với biến tong đều ảnh hưởng đối với biến a và ngược lại. Ví dụ: int a, &tong = a; tong =1; //a=1 cout<< tong; //in ra số 1 tong++; //a=2 ++a; //a=3 cout<<tong; //in ra số 3 Chú ý: Trong khai báo biến tham chiếu phải chỉ rõ tham chiếu đến biến nào. Biến tham chiếu có thể tham chiếu đến một phần tử mảng, nhưng không cho phép khai báo mảng tham chiếu. Biến tham chiếu có thể tham chiếu đến một hằng. Khi đó nó sử dụng vùng nhớ của hằng và có thể làm thay đổi giá trị chứa trong vùng nhớ này. Biến tham chiếu thường được sử dụng làm đối của hàm để cho phép hàm truy nhập đến các tham biến trong lời gọi hàm 2.6. Hằng tham chiếu Cú pháp khai báo hằng tham chiếu như sau: const Kiểu dữ liệu &Biến = Biến/Hằng; Ví dụ: int n = 10; const int &m = n; const int &p = 123; Hằng tham chiếu có thể tham chiếu đến một biến hoặc một hằng. Chú ý:  Biến tham chiếu và hằng tham chiếu khác nhau ở chỗ: không cho phép dùng hằng tham chiếu để làm thay đổi giá trị của vùng nhớ mà nó tham chiếu. Ví dụ: int y=12, z; const int &p = y //Hằng tham chiếu p tham chiếu đến biến y p = p + 1; //Sai, trình biên dịch sẽ thông báo lỗi  Hằng tham chiếu cho phép sử dụng giá trị chứa trong một vùng nhớ, nhưng không cho phép thay đổi giá trị này.  Hằng tham chiếu thường được sử dụng làm tham số của hàm để cho phép sử dụng giá trị của 14
  15. các tham số trong lời gọi hàm, nhưng tránh làm thay đổi giá trị tham số. 2.7. Truyền tham số cho hàm theo tham chiếu Trong C chỉ có một cách truyền dữ liệu cho hàm là truyền theo theo giá trị. Chương trình sẽ tạo ra các bản sao của các tham số thực sự trong lời gọi hàm và sẽ thao tác trên các bản sao này chứ không xử lý trực tiếp với các tham số thực sự. Cơ chế này rất tốt nếu khi thực hiện hàm trong chương trình không cần làm thay đổi giá trị của biến gốc. Tuy nhiên, nhiều khi ta lại muốn những tham số đó thay đổi khi thực hiện hàm trong chương trình. C++ cung cấp thêm cách truyền dữ liệu cho hàm theo tham chiếu bằng cách dùng đối là tham chiếu. Cách làm này có ưu diểm là không cần tạo ra các bản sao của các tham số, do dó tiết kiệm bộ nhớ và thời gian chạy máy. Mặt khác, hàm này sẽ thao tác trực tiếp trên vùng nhớ của các tham số, do đó dễ dàng thay đổi giá trị các tham số khi cần. Ví dụ 2.7 Chương trình sau sẽ nhập dãy số thực, sắp xếp dãy theo thứ tự tăng dần và hiển thị ra màn hình. #include #include #include void nhapds(double *a,int n) { for(int i=0;i >a[i]; } } void hv(double &x,double &y) { double tam=x;x=y;y=tam; } void sapxep(double *a,int n) { for(int i=0;i a[j]) hv(a[i],a[j]); } void main() { double x[100]; int i,n; clrscr(); cout >n; nhapds(x,n); sapxep(x,n); cout<<"\nCac phan tu mang sau khi sap xep :"; 15
  16. for(i=0;i #include #include #include struct TS { char ht[20]; float d1,d2,d3,td; }; void ints(const TS &ts) { cout >ts[i].d1>>ts[i].d2>>ts[i].d3; ts[i].td=ts[i].d1+ts[i].d2+ts[i].d3; } } void hvts(TS &ts1,TS &ts2) { TS tg=ts1; ts1=ts2; ts2=tg; } void sapxep(TS *ts,int n) { for(int i=0;i<n-1;++i) 16
  17. for(int j=i+1;j >n; nhapsl(ts,n); sapxep(ts,n); float dc; cout =dc) ints(ts[i]); else break; getch(); } Ví dụ 2.9 Chương trình sau sẽ nhập một mảng thực kích thước 20x20, in mảng đã nhập và in các phần tử lớn nhất và nhỏ nhất trên mỗi hàng của mảng. #include #include #include #include void nhapmt(float a[20][20],int m,int n) { for(int i=0;i > a[i][j]; } } void inmt(float a[20][20],int m,int n) { cout<<setiosflags(ios::showpoint)<<setprecision(1); cout<<"\nMang da nhap : "; for(int i=0;i<m;++i) for(int j=0;j<n;++j) 17
  18. { if(j==0) cout x[vtmax]) vtmax=i; if(x[i] >m>>n; nhapmt(a,m,n); inmt(a,m,n); float *p=(float*) a; int vtmax,vtmin; for(int i=0;i ; } 18
  19. Trong trường hợp này biểu thức được trả lại trong câu lệnh return phải là tên của một biến xác định từ bên ngoài hàm, bởi vì khi đó mới có thể sử dụng được giá trị của hàm. Khi ta trả về một tham chiếu đến một biến cục bộ khai báo bên trong hàm, biến cục bộ này sẽ bị mất đi khi kết thúc thực hiện hàm. Do vậy tham chiếu của hàm sẽ không còn ý nghĩa nữa. Khi giá trị trả về của hàm là tham chiếu, ta có thể gặp các câu lệnh gán hơi khác thường, trong đó vế trái là một lời gọi hàm chứ không phải là tên của một biến. Điều này hoàn toàn hợp lý, bởi vì bản thân hàm đó có giá trị trả về là một tham chiếu. Nói cách khác, vế trái của lệnh gán có thể là lời gọi đến một hàm có giá trị trả về là một tham chiếu.Xem các ví dụ sau: Ví dụ 2.10 #include #include int z; int &f()// ham tra ve mot bi danh cua bien toan bo z { return z; } void main() { f()=50;//z=50 cout #include #include #include int & max(int& a, int& b); void main() { clrscr(); int b =10, a= 7, c= 20; cout b ? a:b; } Kết quả trên màn hình sẽ là : 19
  20. Max a,b : 10 Gia tri cua b va a : 11 7 Gia tri cua b va a va c : 11 7 5 2.9. Hàm với tham số có giá trị mặc định C++ cho phép xây dựng hàm với các tham số được khởi gán giá trị mặc định. Quy tắc xây dựng hàm với tham số mặc định như sau: Các đối có giá trị mặc định cần là các tham số cuối cùng tính từ trái qua phải. Nếu chương trình sử dụng khai báo nguyên mẫu hàm thì các tham số mặc định cần được khởi gán trong nguyên mẫu hàm, không được khởi gán khởi gán lại cho các đối mặc định trong dòng đầu của định nghĩa hàm. void f(int a, float x, char *st=”TRUNG TAM”, int b=1, double y = 1.234); void f(int a, float x, char *st=”TRUNG TAM”, int b=1, double y = 1.234) { //Các câu lệnh } Khi xây dựng hàm, nếu không khai báo nguyên mẫu, thì các đối mặc định được khởi gán trong dòng đầu của định nghĩa hàm, ví dụ: void f(int a, float x, char *st=”TRUNG TAM”, int b=1, double y = 1.234) { //Các câu lệnh } Chú ý: Đối với các hàm có tham số mặc định thì lời gọi hàm cần viết theo quy định: Các tham số vắng mặt trong lời gọi hàm tương ứng với các tham số mặc định cuối cùng (tính từ trái sang phải), ví dụ với hàm: void f(int a, float x, char *st=”TRUNG TAM”, int b=1, double y = 1.234); thì các lời gọi hàm đúng: f(3,3.4,”TIN HOC”,10,1.0);//Đầy đủ tham số f(3,3.4,”ABC”); //Thiếu 2 tham số cuối f(3,3.4); //Thiếu 3 tham số cuối Các lời gọi hàm sai: f(3); f(3,3.4, ,10); Ví dụ 2.12 #include #include void ht(char *dc="TRUNG TAM",int n=5); void ht(char *dc,int n) { for(int i=0;i<n;++i) cout<<"\n" <<dc; } void main() { ht();// in dong chu "TRUNG TAM"tren 5 dong 20
  21. ht("ABC",3);// in dong chu "ABC"tren 3 dong ht("DEF");// in dong chu "DEF"tren 5 dong getch(); } 2.10. Các hàm nội tuyến (inline) Việc tổ chức chương trình thành các hàm có ưu điểm chương trình được chia thành các đơn vị độc lập, điều này giảm được kích thước chương trình, vì mỗi đoạn chưong trình thực hiện nhiệm vụ của hàm được thay bằng lời gọi hàm. Tuy nhiên hàm cũng có nhược điểm là làm là chậm tốc độ thực hiện chương trình vì phải thực hiện một số thao tác có tính thủ tục mỗi khi gọi hàm như: cấp phát vùng nhớ cho các tham số và biến cục bộ, truyền dữ liệu của các tham số cho các đối, giải phóng vùng nhớ trước khi thoát khỏi hàm. C++ cho khả năng khắc phục được nhược điểm nói trên bằng cách dùng hàm nội tuyến. Để biến một hàm thành hàm nội tuyến ta viết thêm từ khóa inline vào trước khai báo nguyên mẫu hàm. Chú ý: Trong mọi trường hợp, từ khóa inline phải xuất hiện trước các lời gọi hàm thì trình biên dịch mới biết cần xử lý hàm theo kiểu inline. Ví dụ hàm f() trong chương trình sau sẽ không phải là hàm nội tuyến vì inline viết sau lời gọi hàm. Ví dụ 2.13 #include #include void main() { int s ; s=f(5,6); cout #include inline void dtcvhcn(int a,int b,int &dt,int &cv) { 21
  22. dt=a*b; cv=2*(a+b); } void main() { int a[20],b[20],cv[20],dt[20],n; cout >n; for(int i=0;i >a[i]>>b[i]; dtcvhcn(a[i],b[i],dt[i],cv[i]); } clrscr(); for(i=0;i #include inline void dtcvhcn(int a,int b,int &dt,int &cv); void main() { int a[20],b[20],cv[20],dt[20],n; cout >n; for(int i=0;i >a[i]>>b[i]; dtcvhcn(a[i],b[i],dt[i],cv[i]); } clrscr(); for(i=0;i<n;++i) { cout<<"\n Hinh chu nhat thu "<<i+1<<":"; 22
  23. cout #include #include void nhapds(int *x,int n); void nhapds(double *x,int n); int max(int x,int y); double max(double x,double y); void nhapds(int *x,int n) { for(int i=0;i >x[i]; } } void nhapds(double *x,int n) { for (int i=0;i >x[i]; } } int max(int x,int y) { 23
  24. return x>y?x:y; } double max(double x,double y) { return x>y?x:y; } int max(int *x,int n) { int s=x[0]; for(int i=1;i >ni; cout >nd; cout<<"\n Nhap day so thuc: "; nhapds(x,nd); maxi=max(a,ni); maxd=max(x,nd); cout<<"\n Max day so nguyen ="<<maxi; cout<<"\n Max day so thuc="<<maxd; getch(); } Chú ý: Nếu hai hàm trùng tên và trùng đối thì trình biên dịch không thể phân biệt được. Ngay cả khi hai hàm này có cùng kiểu khác nhau thì trình biên dịch vẫn báo lỗi. Ví dụ sau xây dựng hai hàm cùng có tên là f và cùng một đối nguyên a, nhưng kiểu hàm khác nhau. Hàm thứ nhất có kiểu nguyên( trả về a*a), hàm thứ hai có kiểu void. Chương trình sau sẽ bị thông báo lỗi khi biên dịch. 24
  25. Ví dụ 2.17 #include #include int f(int a); void f(int a); int f(int a) { return a*a; } void f(int a) { cout<<"\n"<<a; } void main() { int b = f(5); f(b); getch(); } 25
  26. BÀI TẬP 1. Viết chương trình thực hiện các yêu cầu sau đây: - Nhập dữ liệu cho các sinh viên (dùng cấu trúc danh sách liên kết đơn), các thông tin của sinh viên bao gồm: mã sinh viên, họ tên, lớp, điểm trung bình. - Chương trình có sử dụng toán tử new và delete. - In ra danh sách sinh viên có sắp xếp vị thứ theo điểm trung bình. 2. Viết chương trình để sắp xếp một mảng thực hai chiều theo thứ tự tăng dần, trong chương trình có có sử dụng toán tử new và delete. 3. Viết các hàm tải bội để tính diện tích tam giác, diện tích hình chữ nhật, diện tích hình tròn. 4. Viết chương trình nhân hai ma trận A mxn và Bnxp , mỗi ma trân được cấp phát động và các giá trị của chúng phát sinh ngẫu nhiên. 26
  27. CHƯƠNG 3 LỚP Chương này trình bày những vấn đề sau đây:  Định nghĩa lớp  Tạo lập đối tượng  Truy nhập đến các thành phần của lớp  Con trỏ đối tượng  Con trỏ this  Hàm bạn  Dữ liệu thành phần tĩnh, hàm thành phần tĩnh  Hàm tạo, hàm hủy  Hàm tạo sao chép Lớp là khái niệm trung tâm của lập trình hướng đối tượng, nó là sự mở rộng của các khái niệm cấu trúc (struct) của C. Ngoài các thành phần dữ liệu, lớp còn chứa các thành phần hàm, còn gọi là phương thức (method) hoặc hàm thành viên (member function). Lớp có thể xem như một kiểu dữ liệu các biến, mảng đối tượng. Từ một lớp đã định nghĩa, có thể tạo ra nhiều đối tượng khác nhau, mỗi đối tượng có vùng nhớ riêng. Chương này sẽ trình bày cách định nghĩa lớp, cách xây dựng phương thức, giải thích về phạm vi truy nhập, sử dụng các thành phần của lớp, cách khai báo biến, mảng cấu trúc, lời gọi tới các phương thức . 3.1. Định nghĩa lớp Cú pháp: Lớp được định nghĩa theo mẫu : class tên_lớp { private: [Khai báo các thuộc tính] [Định nghĩa các hàm thành phần (phương thức)] public : [Khai báo các thuộc tính] [Định nghĩa các hàm thành phần (phương thức)] } ; Thuộc tính của lớp được gọi là dữ liệu thành phần và hàm được gọi là phương thức hoặc hàm thành viên. Thuộc tính và hàm được gọi chung là các thành phần của lớp. Các thành phần của lớp được tổ chức thành hai vùng: vùng sở hữu riêng (private) và vùng dùng chung (public) để quy định phạm vi sử dụng của các thành phần. Nếu không quy định cụ thể (không dùng các từ khóa private và public) thì C++ hiểu đó là private. Các thành phần private chỉ được sử dụng bên trong lớp (trong thân của các hàm thành phần). Các thành phần public được phép sử dụng ở cả bên trong và bên ngoài lớp. Các hàm không phải là hàm thành phần của lớp thì không được phép sử dụng các thành phần này. Khai báo các thuộc tính của lớp: được thực hiện y như việc khai báo biến. Thuộc tính của lớp không thể có kiểu chính của lớp đó, nhưng có thể là kiểu con trỏ của lớp này, Ví dụ: class A { A x; //Không cho phép, vì x có kiểu lớp A A *p ; // Cho phép, vì p là con trỏ kiểu lớp A } ; 27
  28. Định nghĩa các hàm thành phần: Các hàm thành phần có thể được xây dựng bên ngoài hoặc bên trong định nghĩa lớp. Thông thường, các hàm thành phần đơn giản, có ít dòng lệnh sẽ được viết bên trong định nghĩa lớp, còn các hàm thành phần dài thì viết bên ngoài định nghĩa lớp. Các hàm thành phần viết bên trong định nghĩa lớp được viết như hàm thông thường. Khi định nghĩa hàm thành phần ở bên ngoài lớp, ta dùng cú pháp sau đây: Kiểu_trả_về_của_hàm Tên_lớp::Tên_hàm(khai báo các tham số) { [nội dung hàm] } Toán tử :: được gọi là toán tử phân giải miền xác định, được dùng để chỉ ra lớp mà hàm đó thuộc vào. Trong thân hàm thành phần, có thể sử dụng các thuộc tính của lớp, các hàm thành phần khác và các hàm tự do trong chương trình. Chú ý : Các thành phần dữ liệu khai báo là private nhằm bảo đảm nguyên lý che dấu thông tin, bảo vệ an toàn dữ liệu của lớp, không cho phép các hàm bên ngoài xâm nhập vào dữ liệu của lớp . Các hàm thành phần khai báo là public có thể được gọi tới từ các hàm thành phần public khác trong chương trình . 3.2. Tạo lập đối tượng Sau khi định nghĩa lớp, ta có thể khai báo các biến thuộc kiểu lớp. Các biến này được gọi là các đối tượng. Cú pháp khai báo biến đối tượng như sau: Tên_lớp Danh_sách_biến ; Đối tượng cũng có thể khai báo khi định nghĩa lớp theo cú pháp sau: class tên_lớp { } ; Mỗi đối tượng sau khi khai báo sẽ được cấp phát một vùng nhớ riêng để chứa các thuộc tính của chúng. Không có vùng nhớ riêng để chứa các hàm thành phần cho mỗi đối tượng. Các hàm thành phần sẽ được sử dụng chung cho tất cả các đối tượng cùng lớp. 3.3. Truy nhập tới các thành phần của lớp Để truy nhập đến dữ liệu thành phần của lớp, ta dùng cú pháp: Tên_đối_tượng. Tên_thuộc_tính Cần chú ý rằng dữ liệu thành phần riêng chỉ có thể được truy nhập bởi những hàm thành phần của cùng một lớp, đối tượng của lớp cũng không thể truy nhập. Để sử dụng các hàm thành phần của lớp, ta dùng cú pháp: Tên_đối_tượng. Tên_hàm (Các_khai_báo_tham_số_thực_sự) Ví dụ 3.1 #include #include class DIEM { private : int x,y ; 28
  29. public : void nhapsl( ) { cout >x>>y ; } void hienthi( ) { cout #include class A { int m,n; public : void nhap( ) { cout >m>>n ; } int max() { return m>n?m:n; } void hienthi( ) { cout<<"\n Thanh phan du lieu lon nhat x = " <<max()<<endl;} }; void main () { clrscr(); A ob; ob.nhap(); ob.hienthi(); getch(); } 29
  30. Chú ý: Các hàm tự do có thể có các đối là đối tượng nhưng trong thân hàm không thể truy nhập đến các thuộc tính của lớp. Ví dụ giả sử đã định nghĩa lớp : class DIEM { private : double x,y ; // toa do cua diem public : void nhapsl() { cout > x>>y ; } void in() { cout #include class Box { private: int dai; int rong; int cao; public: int get_thetich(int lth,int wdth = 2,int ht = 3); }; int Box::get_thetich(int l, int w, int h) { dai = l; rong = w; cao = h; cout<< dai<<'\t'<< rong<<'\t'<<cao<<'\t'; return dai * rong * cao; } 30
  31. void main() { Box ob; int x = 10, y = 12, z = 15; cout #include #include class phrase { private: char dongtu[10]; char danhtu[10]; char cumtu[25]; public: phrase(); inline void set_danhtu(char* in_danhtu); inline void set_dongtu(char* in_dongtu); inline char* get_phrase(void); }; void phrase::phrase() { strcpy(danhtu,""); strcpy(dongtu,""); strcpy(cumtu,""); } inline void phrase::set_danhtu(char* in_danhtu) { strcpy(danhtu, in_danhtu); } inline void phrase::set_dongtu(char* in_dongtu) { 31
  32. strcpy(dongtu, in_dongtu); } inline char* phrase::get_phrase(void) { strcpy(cumtu,dongtu); strcat(cumtu," the "); strcat(cumtu,danhtu); return cumtu; } void main() { phrase text; cout " " " " the Cum tu la : -> the file Cum tu la : -> Save the file Cum tu la : -> Save the program Ví dụ 3.5 Ví dụ sau minh họa việc sử dụng từ khóa const trong lớp: #include #include class constants { private: int number; public: void print_it(const int data_value); }; void constants::print_it(const int data_value) { number = data_value; cout << number << "\n"; } void main() { constants num; const int START = 3; 32
  33. const int STOP = 6; int index; for (index=START; index Tên_thuộc_tính Tên_con_trỏ -> Tên_hàm(các tham số thực sự) Nếu con trỏ chứa đầu địa chỉ của mảng, có thể dùng con trỏ như tên mảng. Ví dụ 3.6 #include #include class mhang { int maso; float gia; public: void getdata(int a, float b) {maso= a; gia= b;} void show() { cout << "maso" << maso<< endl; cout << "gia" << gia<< endl; 33
  34. } }; const int k=5; void main() { clrscr(); mhang *p = new mhang[k]; mhang *d = p; int x,i; float y; cout >x>>y; p -> getdata(x,y); p++;} for (i = 0; i show(); d++; } getch(); } 3.5. Con trỏ this Mỗi hàm thành phần của lớp có một tham số ẩn, đó là con trỏ this. Con trỏ this trỏ đến từng đối tượng cụ thể. Ta hãy xem lại hàm nhapsl() của lớp DIEM trong ví dụ ở trên: void nhapsl( ) { cout >x>>y; } Trong hàm này ta sử dụng tên các thuộc tính x,y một cách đơn độc. Điều này dường như mâu thuẩn với quy tắc sử dụng thuộc tính. Tuy nhiên nó được lý giải như sau: C++ sử dụng một con trỏ đặc biệt trong các hàm thành phần. Các thuộc tính viết trong hàm thành phần được hiểu là thuộc một đối tượng do con trỏ this trỏ tới. Như vậy hàm nhapsl() có thể viết một cách tường minh như sau: void nhapsl( ) { cout >this->x>>this->y ; } Con trỏ this là đối thứ nhất của hàm thành phần. Khi một lời gọi hàm thành phần được phát ra bởi một đối tượng thì tham số truyền cho con trỏ this chính là địa chỉ của đối tượng đó. Ví dụ: Xét một lời gọi tới hàm nhapsl() : 34
  35. DIEM d1 ; d1.nhapsl(); Trong trường hợp này của d1 thì this =&d1. Do đó this -> x chính là d1.x và this-> y chính là d1.y Chú ý: Ngoài tham số đặc biệt this không xuất hiện một cách tường minh, hàm thành phần lớp có thể có các thamô1 khác được khai báo như trong các hàm thông thường. Ví dụ 3.7 #include #include class time { int h,m; public : void nhap(int h1, int m1) { h= h1; m = m1;} void hienthi(void) { cout m = t1.m+ t2.m; h= m/60; //this->h = this->m/60; m= m%60; //this->m = this->m%60; h = h+t1.h+t2.h;//this->h = this->h + t1.h+t2.h; } void main() { clrscr(); time ob1, ob2,ob3; ob1.nhap(2,45); ob2.nhap(5,40); ob3.tong(ob1,ob2); cout <<"object 1 = "; ob1.hienthi(); cout <<"object 2 = "; ob2. hienthi(); cout <<"object 3 = "; ob3. hienthi(); getch(); } Chương trình cho kết quả như sau : object 1 = 2 gio 45 phut object 2 = 5 gio 40 phut object 3 = 8 gio 25 phut 3.6. Hàm bạn Trong thực tế thường xãy ra trường hợp có một số lớp cần sử dụng chung một hàm. C++ giải quyết vấn đề này bằng cách dùng hàm bạn. Để một hàm trở thành bạn của một lớp, có 2 cách viết: 35
  36. Cách 1: Dùng từ khóa friend để khai báo hàm trong lớp và xây dựng hàm bên ngoài như các hàm thông thường (không dùng từ khóa friend). Mẫu viết như sau : class A { private : // Khai báo các thuộc tính public : // Khai báo các hàm bạn của lớp A friend void f1 ( ) ; friend double f2 ( ) ; } ; // Xây dựng các hàm f1,f2,f3 void f1 ( ) { } double f2 ( ) { } Cách 2: Dùng từ khóa friend để xây dựng hàm trong định nghĩa lớp . Mẫu viết như sau : class A { private : // Khai báo các thuộc tính public : // Khai báo các hàm bạn của lớp A void f1 ( ) { } double f2 ( ) { } } ; Hàm bạn có những tính chất sau: - Hàm bạn không phải là hàm thành phần của lớp. - Việc truy nhập tới hàm bạn được thực hiện như hàm thông thường. 36
  37. - Trong thân hàm bạn của một lớp có thể truy nhập tới các thuộc tính của đối tượng thuộc lớp này. Đây là sự khác nhau duy nhất giữa hàm bạn và hàm thông thường. - Một hàm có thể là bạn của nhiều lớp. Lúc đó nó có quyền truy nhập tới tất cả các thuộc tính của các đối tượng trong các lớp này. Để làm cho hàm f trở thành bạn của các lớp A, B và C ta sử dụng mẩu viết sau : class B ; //Khai báo trước lớp A class B ; // Khai báo trước lớp B class C ; // Khai báo trước lớp C // Định nghĩa lớp A class A { // Khai báo f là bạn của A friend void f( ) } ; // Định nghĩa lớp B class B { // Khai báo f là bạn của B friend void f( ) } ; // Định nghĩa lớp C class C { // Khai báo f là bạn của C friend void f( ) } ; // Xây dựng hàm f void f( ) { } ; Ví dụ 3.8 #include #include class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} friend sophuc tong(sophuc,sophuc); friend void hienthi(sophuc); }; sophuc tong(sophuc c1,sophuc c2) {sophuc c3; 37
  38. c3.a=c1.a + c2.a ; c3.b=c1.b + c2.b ; return (c3); } void hienthi(sophuc c) {cout #include class LOP1; class LOP2 { int v2; public: void nhap(int a) { v2=a;} void hienthi(void) { cout<<v2<<"\n";} friend void traodoi(LOP1 &, LOP2 &); }; class LOP1 { int v1; public: void nhap(int a) { v1=a;} void hienthi(void) 38
  39. { cout<<v1<<"\n";} friend void traodoi(LOP1 &, LOP2 &); }; void traodoi(LOP1 &x, LOP2 &y) { int t = x.v1; x.v1 = y.v2; y.v2 = t; } void main() { clrscr(); LOP1 ob1; LOP2 ob2; ob1.nhap(150); ob2.nhap(200); cout << "Gia tri ban dau :" << "\n"; ob1.hienthi(); ob2.hienthi(); traodoi(ob1, ob2); //Thuc hien hoan doi cout << "Gia tri sau khi thay doi:" << "\n"; ob1.hienthi(); ob2.hienthi(); getch(); } Chương trình cho kết quả như sau: Gia tri ban dau : 150 200 Gia tri sau khi thay doi: 200 150 3.7. Dữ liệu thành phần tĩnh và hàm thành phần tĩnh 3.7.1. Dữ liệu thành phần tĩnh Dữ liệu thành phần tĩnh được khai báo bằng từ khoá static và được cấp phát một vùng nhí cố định, nó tồn tại ngay cả khi lớp chưa có một đối tượng nào cả. Dữ liệu thành phần tĩnh là chung cho cả lớp, nó không phải là riêng của mỗi đối tượng, ví dụ: class A { private: static int ts; // Thành phần tĩnh int x; 39
  40. }; A u, v; // Khai báo 2 đối tượng Giữa các thành phần x và ts có sự khác nhau như sau: u.x và v.x có 2 vùng nhớ khác nhau, trong khi u.ts và v.ts chỉ là một, chúng cùng biểu thị một vùng nhớ, thành phần ts tồn tại ngay khi u và v chưa khai báo. Để biểu thị thành phần tĩnh, ta có thể dùng tên lớp, ví dụ: A::ts Khai báo và khởi gán giá trị cho thành phần tĩnh: Thành phần tĩnh sẽ được cấp phát bộ nhớ và khởi gán giá trị đầu bằng một câu lệnh khai báo đặt sau định nghĩa lớp theo mẫu như sau: int A::ts; // Khởi gán cho ts giá trị 0 int A::ts = 1234; // Khởi gán cho ts giá trị 1234 Chú ý: Khi chưa khai báo thì thành phần tĩnh chưa tồn tại. Hãy xem chương trình sau: Ví dụ 3.10 #include #include class HDBH { private: char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() { cout #include class HDBH { private: 40
  41. int shd; char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() { cout <<”\n” <<tshd; cout <<”\n” <<tstienban; } }; int HDBH::tshd=5 double HDBH::tstienban=20000.0; void main() { HDBH::in(); getch(); } 3.7.2. Hàm thành phần tĩnh Hàm thành phần tĩnh được viết theo một trong hai cách: - Dùng từ khoá static đặt trước định nghĩa hàm thành phần viết bên trong định nghĩa lớp. - Nếu hàm thành phần xây dựng bên ngoài định nghĩa lớp, thì dùng từ khoá static đặt trước khai báo hàm thành phần bên trong định nghĩa lớp. Không cho phép dùng từ khoá static đặt trước định nghĩa hàm thành phần viết bên ngoài định nghĩa lớp. Các đặc tính của hàm thành phần tĩnh: - Hàm thành phần tĩnh là chung cho toàn bộ lớp và không lệ thuộc vào một đối tượng cụ thể, nó tồn tại ngay khi lớp chưa có đối tượng nào. - Lời gọi hàm thành phần tĩnh như sau: Tên lớp :: Tên hàm thành phần tĩnh(các tham số thực sự) - Vì hàm thành phần tĩnh là độc lập với các đối tượng, nên không thể dùng hàm thành phần tĩnh để xử lý dữ liệu của các đối tượng trong lời gọi phương thức tĩnh. Nói cách khác không cho phép truy nhập các thuộc tính (trừ thuộc tính tĩnh) trong thân hàm thành phần tĩnh. Đoạn chương trình sau minh họa điều này: class HDBH { private: int shd; char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() 41
  42. { cout #include class A { int m; static int n; //n la bien tinh public: void set_m(void) { m= ++n;} void show_m(void) { cout << "\n Doi tuong thu:" << m << endl; } static void show_n(void) { cout << " m = " << n << endl; } }; int A::n=1; //khoi gan gia tri ban dau 1 cho bien tinh n void main() { clrscr(); A t1, t2; t1.set_m(); t2.set_m(); A::show_n(); A t3; t3.set_m(); A::show_n(); t1.show_m(); t2.show_m(); t3.show_m(); getch(); } Kết quả chương trình trên là: m = 3 42
  43. m = 4 Doi tuong thu : 2 Doi tuong thu : 3 Doi tuong thu : 4 3.8. Hàm tạo (constructor) Hàm tạo là một hàm thành phần đặc biệt của lớp làm nhiệm vụ tạo lập một đối tượng mới. Chương trình dịch sẽ cấp phát bộ nhớ cho đối tượng, sau đó sẽ gọi đến hàm tạo. Hàm tạo sẽ khởi gán giá trị cho các thuộc tính của đối tượng và có thể thực hiện một số công việc khác nhằm chuẩn bị cho đối tượng mới. Khi xây dựng hàm tạo cần lưu ý những đặc tính sau của hàm tạo: - Tên hàm tạo trùng với tên của lớp. - Hàm tạo không có kiểu trả về. - Hàm tạo phải được khai báo trong vùng public. - Hàm tạo có thể được xây dựng bên trong hoặc bên ngoài định nghĩa lớp. - Hàm tạo có thể có tham số hoặc không có tham số. - Trong một lớp có thể có nhiều hàm tạo (cùng tên nhưng khác các tham số). Ví dụ 3.13 class DIEM { private: int x,y; public: DIEM() //Ham tao khong tham so { x = y = 0; } DIEM(int x1, int y1) //Ham tao co tham so { x = x1;y=y1; } //Cac thanh phan khac }; Chú ý 1: Nếu lớp không có hàm tạo, chương trình dịch sẽ cung cấp một hàm tạo mặc định không đối, hàm này thực chất không làm gì cả. Như vậy một đối tượng tạo ra chỉ được cấp phát bộ nhớ, còn các thuộc tính của nó chưa được xác định. Ví dụ 3.14 #include #inlcule class DIEM { private: int x,y; public: void in() 43
  44. { cout in(); getch(); } Chú ý 2: Khi một đối tượng được khai báo thì hàm tạo của lớp tương ứng sẽ tự động thực hiện và khởi gán giá trị cho các thuộc tính của đối tượng. Dựa vào các tham số trong khai báo mà chương trình dịch sẽ biết cần gọi đến hàm tạo nào. Khi khai báo mảng đối tượng không cho phép dùng các tham số để khởi gán cho các thuộc tính của các đối tượng mảng. Câu lệnh khai báo một biến đối tượng sẽ gọi tới hàm tạo một lần. Câu lệnh khai báo một mảng n đối tượng sẽ gọi tới hàm tạo mặc định n lần. Với các hàm có các tham số kiểu lớp, thì chúng chỉ xem là các tham số hình thức, vì vậy khai báo tham số trong dòng đầu của hàm sẽ không tạo ra đối tượng mới và do đó không gọi tới các hàm tạo. Ví dụ 3.15 #include #include #include class DIEM { private: int x,y; public: DIEM() { x = y = 0; } DIEM(int x1, int y1) { x = x1; y = y1; } 44
  45. friend void in(DIEM d) { cout > n; q = new DIEM [n+1]; //n+1 lan goi ham tao khong doi for (int i=0;i #include class DIEM { 45
  46. private: int x,y; public: DIEM(int x1, int y1) { x=x1; y=y1; } void in() { cout #include class DIEM { private: int x,y; public: DIEM(int x1=0, int y1=0) { x = x1; y = y1; } void in() { cout << “\n” << x << “ ” << y <<” ” << m; } 46
  47. }; void main() { DIEM d1(2,3); //Goi ham tao,khong dung tham so mac dinh DIEM d2; //Goi ham tao, dung tham so mac dinh d2= DIEM(6,7);//Goi ham tao,khong dung tham so mac dinh d1.in(); d2.in(); getch(); } Ví dụ 3.18 #include #include class rectangle { private: int dai; int rong; static int extra_data; public: rectangle(); void set(int new_dai, int new_rong); int get_area(); int get_extra(); }; int rectangle::extra_data; rectangle::rectangle() { dai = 8; rong = 8; extra_data = 1; } void rectangle::set(int new_dai,int new_rong) { dai = new_dai; rong = new_rong; } int rectangle::get_area() { return (dai * rong); } int rectangle::get_extra() { return extra_data++; } void main() { rectangle small, medium, large; small.set(5, 7); large.set(15, 20); cout<<"Dien tich la : "<<small.get_area()<<"\n"; cout<<"Dien tich la : "<< 47
  48. medium.get_area() >) và xuất ( #include class PS { private: int t,m; 48
  49. public: friend ostream& operator > (istream& is, PS &p) { cout >p.t>>p.m; return is; } }; void main() { PS d; cout >d; cout<<"\n PS d "<<d; PS u(d); cout<<"\n PS u "<<u; getch(); } 3.9.2. Hàm tạo sao chép Hàm tạo sao chép sử dụng một đối kiểu tham chiếu đối tượng để khởi gán cho đối tượng mới và được viết theo mẫu sau: Tên_lớp (const Tên_lớp &ob) { // Các câu lệnh dùng các thuộc tính của đối tượng ob để khởi gán // cho các thuộc tính của đối tượng mới } Ví dụ: class PS { private: int t, m; public: PS(const PS &p) { t= p.t; m= p.m; } }; Hàm tạo sao chép trong ví dụ trên không khác gì hàm tạo sao chép mặc định. Chú ý: 49
  50. - Nếu lớp không có các thuộc tính kiểu con trỏ hoặc tham chiếu thì dùng hàm tạo sao chép mặc định là đủ. - Nếu lớp có các thuộc tính con trỏ hoặc tham chiếu, thì hàm tạo sao chép mặc định chưa đáp ứng được yêu cầu. class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac he so da thuc a0, a1, public: DT() { n = 0; a = NULL; } DT(int n1) { n = n1; a = new double[n1+1]; } friend ostream& operator > (istream& is,DT &d); }; Bây giờ chúng ta hãy theo dõi xem việc dùng hàm tạo mặc định trong đoạn chương trình sau sẽ dẫn đến sai lầm như thế nào: DT d; cin >> d; /* Nhập đối tượng d gồm: nhập một số nguyên dương và gán cho d.n, cấp phát vùng nhớ cho d.n, nhập các hệ số của đa thức và chứa vào vùng nhớ được cấp phát */ DT u(d); /* Dùng hàm tạo mặc định để xây dựng đối tượng u theo d. Kết quả: u.n = d.n và u.a = d.a. Như vậy hai con trỏ u.a và d.a cùng trỏ đến một vùng nhớ. */ Nhận xét: Mục đích của ta là tạo ra một đối tượng u giống như d, nhưng độc lập với d. Nghĩa là khi d thay đổi thì u không bị ảnh hưởng gì. Thế nhưng mục tiêu này không đạt được, vì u và d có chung một vùng nhớ chứa hệ số của đa thức, nên khi sửa đổi các hệ số của đa thức trong d thì các hệ số của đa thức trong u cũng thay đổi theo. Còn một trường hợp nữa cũng dẫn đến lỗi là khi một trong hai đối tượng u và d bị giải phóng (thu hồi vùng nhớ chứa đa thức) thì đối tượng còn lại cũng sẽ không còn vùng nhớ nữa. Ví dụ sau sẽ minh họa nhận xét trên: Khi d thay đổi thì u cũng thay đổi và ngược lại khi u thay đổi thì d cũng thay đổi theo. Ví dụ 3.20 #include #include 50
  51. #include class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac he //so da thuc a0,a1, public: DT() { this->n=0; this->a=NULL; } DT(int n1) { this->n=n1; this->a= new double[n1+1]; } friend ostream& operator > (istream& is,DT &d); }; ostream& operator > (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout >d.n; d.a = new double[d.n+1]; cout > d.a[i]; } return is; } void main() { 51
  52. DT d; clrscr(); cout > d; DT u(d); cout > d; cout > u; cout #include #include class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac da thuc // a0, a1, public: DT() { this->n=0; this->a=NULL; } DT(int n1) { this->n=n1; this->a= new double[n1+1]; } DT(const DT &d); friend ostream& operator > (istream& is,DT &d); }; DT::DT(const DT &d) { this->n = d.n; this->a = new double[d.n+1]; for (int i=0;i<=d.n;++i) 52
  53. this->a[i] = d.a[i]; } ostream& operator > (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout > d.n; d.a = new double[d.n+1]; cout > d.a[i]; } return is; } void main() { DT d; clrscr(); cout > d; DT u(d); cout > d; cout > u; cout << "\nDa thuc d " << d; cout << "\nDa thuc u " << u; getch(); } 3.10. Hàm hủy (destructor) Hàm huỷ là một hàm thành phần của lớp, có chức năng ngược với hàm tạo. Hàm huỷ được gọi trước khi giải phóng một đối tượng để thực hiện một số công việc có tính “dọn dẹp” trước khi đối tượng được hủy bỏ, ví dụ giải phóng một vùng nhớ mà đối tượng đang quản lý, xoá đối 53
  54. tượng khỏi màn hình nếu như nó đang hiển thị Việc hủy bỏ đối tượng thường xảy ra trong 2 trường hợp sau: - Trong các toán tử và hàm giải phóng bộ nhớ như delete, free - Giải phóng các biến, mảng cục bộ khi thoát khỏi hàm, phương thức. Nếu trong lớp không định nghĩa hàm huỷ thì một hàm huỷ mặc định không làm gì cả được phát sinh. Đối với nhiều lớp thì hàm huỷ mặc định là đủ, không cần đưa vào một hàm huỷ mới. Trường hợp cần xây dựng hàm hủy thì tuân theo quy tắc sau: - Mỗi lớp chỉ có một hàm hủy. - Hàm hủy không có kiểu, không có giá trị trả về và không có tham số. - Tên hàm hàm huỷ có một dấu ngã ngay trước tên lớp. Ví dụ: class A { private: int n; double *a; public: ~A() { n = 0; delete a; } }; Ví dụ 3.22 #include class Count{ private: static int counter; int obj_id; public: Count(); static void display_total(); void display(); ~Count(); }; int Count::counter; Count::Count() { counter++; obj_id = counter; } Count::~Count() { counter ; cout<<"Doi tuong "<<obj_id<<" duoc huy bo\n"; } void Count::display_total() { 54
  55. cout <<"So cac doi tuong duoc tao ra la = "<< counter << endl; } void Count::display() { cout << "Object ID la "<<obj_id<<endl; } void main() { Count a1; Count::display_total(); Count a2, a3; Count::display_total(); a1.display(); a2.display(); a3.display(); } Kết quả chương trình như sau: So cac doi tuong duoc tao ra la = 1 So cac doi tuong duoc tao ra la = 3 Object ID la 1 Object ID la 2 Object ID la 3 Doi tuong 3 duoc huy bo Doi tuong 2 duoc huy bo Doi tuong 1 duoc huy bo 55
  56. BÀI TẬP 1. Xây dựng lớp thời gian Time. Dữ liệu thành phần bao gồm giờ, phút giây. Các hàm thành phần bao gồm: hàm tạo, hàm truy cập dữ liệu, hàm normalize() để chuẩn hóa dữ liệu nằm trong khoảng quy định của giờ (0 giờ Tiêu chuẩn để xét như sau: - Sinh viên làm khóa luận phải có điểm trung bình từ 7 trở lên, trong đó không có môn nào dưới 5. - Sinh viên thi tố nghiệp khi điểm trung bình nhỏ hơn 7 và điểm các môn không dưới 5. - Sinh viên thi lại môn dưới 5. 9. Xây dựng lớp vector để lưu trữ các vectơ gồm các số thực. Các thành phần dữ liệu bao gồm: - Kích thước vectơ. - Một mảng động chứa các thành phần của vectơ. Các hàm thành phần bao gồm hàm tạo, hàm hủy, hàm tính tích vô hướng hai vectơ, tính chuẩn của vectơ (theo chuẩn bất kỳ nào đó). 10. Xây dựng lớp Phanso với dữ liệu thành phần là tử và mẫu số. Các hàm thành phần bao gồm: - Cộng hai phân số, kết quả phải dược tối giản - Trừ hai phân số, kết quả phải dược tối giản - Nhân hai phân số, kết quả phải dược tối giản - Chia hai phân số, kết quả phải dược tối giản 56
  57. CHƯƠNG 4 TOÁN TỬ TẢI BỘI Chương 4 trình bày các vấn đề sau:  Định nghĩa toán tử tải bội  Một số lưu ý khi xây dựng toán tử tải bội  Một số ví dụ minh họa 4.1. Định nghĩa toán tử tải bội Các toán tử cùng tên thực hiện nhiều chức năng khác nhau được gọi là toán tử tải bội. Dạng định nghĩa tổng quát của toán tử tải bội như sau: Kiểu_trả_về operator op(danh sách tham số) {//thân toán tử} Trong đó: Kiểu_trả_về là kiểu kết quả thực hiện của toán tử. op là tên toán tử tải bội operator op(danh sách tham số) gọi là hàm toán tử tải bội, nó có thể là hàm thành phần hoặc là hàm bạn, nhưng không thể là hàm tĩnh. Danh sách tham số được khai báo tương tự khai báo biến nhưng phải tuân theo những quy định sau: - Nếu toán tử tải bội là hàm thành phần thì: không có tham số cho toán tử một ngôi và một tham số cho toán tử hai ngôi. Cũng giống như hàm thành phần thông thường, hàm thành phần toán tử có đối đầu tiên (không tường minh) là con trỏ this . - Nếu toán tử tải bội là hàm bạn thì: có một tham số cho toán tử một ngôi và hai tham số cho toán tử hai ngôi. Quá trình xây dựng toán tử tải bội được thực hiện như sau: - Định nghĩa lớp để xác định kiểu dữ liệu sẽ được sử dụng trong các toán tử tải bội - Khai báo hàm toán tử tải bội trong vùng public của lớp - Định nghĩa nội dung cần thực hiện 4.2. Một số lưu ý khi xây dựng toán tử tải bội 1. Trong C++ ta có thể đưa ra nhiều định nghĩa mới cho hầu hết các toán tử trong C++, ngoại trừ những toán tử sau đây: - Toán tử xác định thành phần của lớp (‘.’) - Toán tử phân giải miền xác định (‘::’) - Toán tử xác định kích thước (‘sizeof’) - Toán tử điều kiện 3 ngôi (‘?:’) 2. Mặc dầu ngữ nghĩa của toán tử được mở rộng nhưng cú pháp, các quy tắc văn phạm như số toán hạng, quyền ưu tiên và thứ tự kết hợp thực hiện của các toán tử vẫn không có gì thay đổi. 3. Không thể thay đổi ý nghĩa cơ bản của các toán tử đã định nghĩa trước, ví dụ không thể định nghĩa lại các phép toán +, - đối với các số kiểu int, float. 4. Các toán tử = , ( ) , [ ] , -> yêu cầu hàm toán tử phải là hàm thành phần của lớp, không thể dùng hàm bạn để định nghĩa toán tử tải bội. 4.3. Một số ví dụ Ví dụ 4.1 Toán tử tải bội một ngôi, dùng hàm bạn #include #include class Diem { private: 58
  58. float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1; z=z1;} friend Diem operator -(Diem d) { Diem d1; d1.x = -d.x; d1.y = -d.y;d1.z=-d.z; return d1; } void hienthi() { cout #include class Diem { private: float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1; z=z1;} friend Diem operator +(Diem d1, Diem d2) { Diem tam; tam.x = d1.x + d2.x; tam.y = d1.y + d2.y; tam.z = d1.z + d2.z; return tam; 59
  59. } void hienthi() { cout #include class Diem { private: float x,y; public: Diem() {} Diem(float x1,float y1) { x = x1; y = y1;} Diem operator -() { x = -x; y = -y; z = -z; return (*this); } void hienthi() { cout<<"Toa do: "<<x<<" "<<y <<” z = "<<z <<" \n";} }; void main() { clrscr(); Diem p(2,3,-4),q; p.hienthi(); q = -p; q.hienthi(); getch(); 60
  60. } Ví dụ 4.4 Toán tử tải bội hai ngôi, dùng hàm thành phần #include #include class Diem { private: float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1; z=z1;} Diem operator +(Diem d2) { x = x + d2.x; y = y + d2.y; z = z + d2.z; return (*this); } void hienthi() { cout #include class Diem { private: int x,y; public: 61
  61. Diem() {} Diem(int x1,int y1) { x = x1; y = y1;} void operator -() { x = -x; y = -y; } void hienthi() { cout #include class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} sophuc operator +(sophuc c2) { sophuc c3; c3.a= a + c2.a ; c3.b= b + c2.b ; return (c3); } void hienthi(sophuc c) { cout<<c.a<<" + "<<c.b<<"i"<<endl; } }; void main() { clrscr(); sophuc d1 (2.1,3.4); sophuc d2 (1.2,2.3) ; sophuc d3 ; d3 = d1+d2; //d3=d1.operator +(d2); cout<<"d1= ";d1.hienthi(d1); 62
  62. cout #include class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} sophuc operator +(sophuc c2) { a= a + c2.a ; b= b + c2.b ; return (*this); } void hienthi(sophuc c) { cout #include class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} 63
  63. friend sophuc operator +(sophuc c1,sophuc c2) { sophuc c; c.a= c1.a + c2.a ; c.b= c1.b + c2.b ; return (c); } void hienthi(sophuc c) { cout #include #include class string { char s[80]; public: string() { *s='\0'; } string(char *p) { strcpy(s,p); } char *get() { return s;} string operator + (string s2); string operator = (string s2); int operator (string s2); int operator == (string s2); }; string string::operator +(string s2) { strcat(s,s2.s); return *this ; } string string::operator =(string s2) 64
  64. { strcpy(s,s2.s) ; return *this; } int string::operator (string s2) { return strcmp(s,s2.s)>0 ; } int string::operator ==(string s2) { return strcmp(s,s2.s)==0 ; } void main() { clrscr(); string o1 ("Trung Tam "), o2 (" Tin hoc"), o3; cout o2) cout o2 \n"; if (o1 #include 65
  65. class Diem { private: int x,y; public: Diem() {x = y = 0;} Diem(int x1, int y1) {x = x1; y = y1;} Diem & operator ++(); //qua tai toan tu ++ tien to Diem operator ++(int); //qua tai toan tu ++ hau to Diem & operator (); //qua tai toan tu tien to Diem operator (int); //qua tai toan tu hau to void hienthi() { cout<<" x = "<<x<<" y = "<<y; } }; Diem & Diem::operator ++() { x++; y++; return (*this); } Diem Diem::operator ++(int) { Diem temp = *this; ++*this; return temp; } Diem & Diem::operator () { x ; y ; return (*this); } Diem Diem::operator (int) { Diem temp = *this; *this; return temp; } void main() 66
  66. { clrscr(); Diem d1(5,10),d2(20,25),d3(30,40),d4(50,60); cout > Ta có thể định nghĩa chồng cho hai toán tử vào/ra > kết hợp với cout và cin (cout >), cho phép các đối tượng đứng bên phải chúng. Lúc đó ta có thể thực hiện các thao tác vào ra như nhập dữ liệu từ bàn phím cho các đối tượng, hiển thị giá trị thành phần dữ liệu của 67
  67. các đối tượng ra màn hình. Hai hàm toán tử > phải là hàm tự do và khai báo là hàm bạn của lớp. Ví dụ 4.11 #include #include class SO { private: int giatri; public: SO(int x=0) { giatri = x; } SO (SO &tso) { giatri = tso.giatri; } friend istream& operator>>(istream&,SO&); friend ostream& operator >so1; cin>>so2; cout >(istream& nhap,SO& so) { cout > so.giatri; return nhap; } ostream& operator<<(ostream& xuat,SO& so) { xuat<< so.giatri; return xuat; } 68
  68. BÀI TẬP 1. Định nghĩa các phép toán tải bội =, ==, ++, , +=, > trên lớp Time (bài tập 1 chương 3). 2. Định nghĩa các phép toán tải bội =, ==, ++, , +=, > trên lớp Date (bài tập 2 chương 3). 1. Định nghĩa các phép toán tải bội +, -, *, =, ==, != trên lớp các ma trận vuông. 2. Định nghĩa các phép toán tải bội +, -, * trên lớp đa thức. 3. Định nghĩa các phép toán tải bội +, -, *, /, =, ==, +=, -=, *=, /= , , =, != , ++, trên lớp Phanso (bài tập 10 chương 3). 4. Ma trận được xem là một vectơ mà mỗi thành phần của nó là một vectơ. Theo nghĩa đó, hãy định nghĩa lớp Matran dựa trên vectơ. Tìm cách để chương trình dịch hiểu được phép truy nhập m[i][j], trong đó m là một đối tượng thuộc lớp Matran. 69
  69. CHƯƠNG 5 KẾ THỪA Chương 5 trình bày các vấn đề sau:  Đơn kế thừa, đa kế thừa  Hàm tạo và hàm hủy đối với sự kế thừa  Hàm ảo, lớp cơ sở ảo 5.1. Giới thiệu Kế thừa là một trong các khái niệm cơ sở của phương pháp lập trình hướng đối tượng. Tính kế thừa cho phép định nghĩa các lớp mới từ các lớp đã có. Một lớp có thể là lớp cơ sở cho nhiều lớp dẫn xuất khác nhau. Lớp dẫn xuất sẽ kế thừa một số thành phần (dữ liệu và hàm) của lớp cơ sở, đồng thời có thêm những thành phần mới. Có hai loại kế thừa là: đơn kế thừa và đa kế thừa, có thể minh họa qua các hình vẽ sau đây: A B Hình 5.1. Đơn kế thừa, lớp A là lớp cơ sở của lớp B A A A B C B C B C D D (a) (b) (c) Hình 5.2. Đa kế thừa Hình (a): Lớp A là lớp cơ sở của lớp B, lớp B là lớp cơ sở của lớp C Hình (b): Lớp A là lớp cơ sở của các lớp B, C, D Hình (c): Lớp A, B, C là lớp cơ sở của lớp D 5.2. Đơn kế thừa 5.2.1. Định nghĩa lớp dẫn xuất từ một lớp cơ sở Giả sử đã định nghĩa lớp A. Cú pháp để xây dựng lớp B dẫn xuất từ lớp A như sau: class B: mode A { private: // Khai báo các thuộc tính của lớp B public: // Định nghĩa các hàm thành phần của lớp B }; Trong đó mode có thể là private hoặc public với ý nghĩa như sau: - Kế thừa theo kiểu public thì tất cả các thành phần public của lớp cơ sở cũng là thành phần public của lớp dẫn xuất. 70
  70. - Kế thừa theo kiểu private thì tất cả các thành phần public của lớp cơ sở sẽ trở thành các thành phần private của lớp dẫn xuất. Chú ý: Trong cả hai trường hợp ở trên thì thành phần private của lớp cơ sở là không được kế thừa. Như vậy trong lớp dẫn xuất không cho phép truy nhập đến các thành phần private của lớp cơ sở. 5.2.2. Truy nhập các thành phần trong lớp dẫn xuất Thành phần của lớp dẫn xuất bao gồm: các thành phần khai báo trong lớp dẫn xuất và các thành phần mà lớp dẫn xuất thừa kế từ các lớp cơ sở. Quy tắc sử dụng các thành phần trong lớp dẫn xuất được thực hiện theo theo mẫu như sau: Tên đối tượng.Tên_lớp::Tên_thành_phần Khi đó chương trình dịch C++ dễ dàng phân biệt thành phần thuộc lớp nào. Ví dụ 5.1 Giả sử có các lớp A và B như sau: class A { public: int n; void nhap() { cout >n; } }; class B: public A { public: int m; void nhap() { cout >m; } }; Xét khai báo: B ob; Lúc đó: ob.B::m là thuộc tính n khai báo trong B ob.A::n là thuộc tính n thừa kế từ lớp A ob.D::nhap() là hàm nhap() định nghĩa trong lớp B ob.A::nhap() là hàm nhap() định nghĩa trong lớp A Chú ý: Để sử dụng các thành phần của lớp dẫn xuất, có thể không dùng tên lớp, chỉ dùng tên thành phần. Khi đó chương trình dịch phải tự phán đoán để biết thành phần đó thuộc lớp nào: trước tiên xem thành phần đang xét có trùng tên với các thành phần nào của lớp dẫn xuất không? Nếu trùng thì đó 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 sẽ được xét trước, các lớp quan hệ xa hơn xét sau. Chú ý trường hợp thành phần đang xét có mặt đồng thời trong 2 lớp cơ sở có cùng một đẳng cấp quan hệ với lớp dẫn xuất. Trường hợp này chương trình dịch không thể quyết định được thành phần này thừa kế từ lớp nào và sẽ đưa ra một thông báo lỗi. 71
  71. 5.2.3. Định nghĩa lại các hàm thành phần của lớp cơ sở trong lớp dẫn xuất Trong lớp dẫn xuất có thể định nghĩa lại hàm thành phần của lớp cơ sở. Như vậy có hai phiên bản khác nhau của hàm thành phần trong lớp dẫn xuất. Trong phạm vi lớp dẫn xuất, hàm định nghĩa lại “che khuất” hàm được định nghĩa. Việc sử dụng hàm nào cần tuân theo quy định ở trên. Chú ý: Việc định nghĩa lại hàm thành phần khác với định nghĩa hàm quá tải. Hàm định nghĩa lại và hàm bị định nghĩa lại giống nhau về tên, tham số và giá trị trả về. Chúng chỉ khác nhau về vị trí: một hàm đặt trong lớp dẫn xuất và hàm kia thì ở trong lớp cơ sở. Trong khi đó, các hàm quá tải chỉ có cùng tên, nhưng khác nhau về danh sách tham số và tất cả chúng thuộc cùng một lớp. Định nghĩa lại hàm thành phần chính là cơ sở cho việc xây dựng tính đa hình của các hàm. C++ cho phép đặt trùng tên thuộc tính trong các lớp cơ sở và lớp dẫn xuất. Các thành phần cùng tên này có thể cùng kiểu hay khác kiểu. Lúc này bên trong đối tượng của lớp dẫn xuất có tới hai thành phần khác nhau có cùng tên, nhưng trong phạm vi lớp dẫn xuất, tên chung đó nhằm chỉ định thành phần được khai báo lại trong lớp dẫn xuất. Khi muốn chỉ định thành phần trùng tên trong lớp cơ sở phải dùng tên lớp toán tử ‘::’ đặt trước tên hàm thành phần. Ví dụ 5.2 Xét các lớp A và B được xây dựng như sau: class A { private: int a, b, c; public: void nhap() { cout >a; cout >b; cout >c; } void hienthi() { cout >d; } }; Lớp B kế thừa theo kiểu private từ lớp A, các thành phần public của lớp A là các hàm nhap() và hienthi() trở thành thành phần private của lớp B. Xét khai báo : B ob; Lúc đó các câu lệnh sau đây là lỗi : ob.nhap(); ob.d=10; ob.a = ob.b = ob.c = 5; ob.hienthi(); 72
  72. bởi vì đối tượng ob không thể truy nhập vào các thành phần private của lớp A và B. Ví dụ 5.3 Chương trình minh họa đơn kế thừa theo kiểu public: #include #include class A { int a; //Bien private, khong duoc ke thua public: int b; //Bien public, se duoc ke thua void get_ab(); int get_a(void); void show_a(void); }; class B: public A { int c; public: void mul(void); void display(void); }; void A::get_ab(void) { a = 5; b = 10;} int A::get_a() { return a;} void A::show_a() { cout << "a = " << a << " \n";} void B::mul() { c = b*get_a();} void B::display() { cout << "a = " << get_a() << "\n"; cout << "b = " << b << "\n"; cout << "c = " << c << "\n"; } void main() { clrscr(); B d; d.get_ab(); //Ke thua tu A d.mul(); d.show_a(); //Ke thua tu A d.display(); d.b = 20; d.mul(); 73
  73. d.display(); getch(); } Chương trình cho kết quả: a = 5 a = 5 b = 10 c = 50 a = 5 b = 20 c = 100 Ví dụ 5.4 Chương trình sau là minh họa khác cho trường hợp kế thừa đơn: #include #include class Diem { private: double x, y; public: void nhap() { cout >x; cout >y; } void hienthi() { cout >r; } double get_r() { return r; } }; 74
  74. void main() { Hinhtron h; clrscr(); cout #include class Diem { private: double x, y; public: Diem() 75
  75. { x=y=0.0; } Diem(double x1, double y1) { x=x1; y=y1; } void in() { cout<<”\nx=”<<x<<”y=”<<y; } }; class Hinhtron: public Diem { private: double r; public: Hinhtron() { r = 0.0; } Hinhtron(double x1,double y1,double r1): Diem (x1, y1) { r=r1; } double get_r() { return r; } }; void main() { Hinhtron h(2.5, 3.5, 8); clrscr(); cout<<”\n Hinh tron co tam:”; h.in(); cout<<”\n Co ban kinh =” << h.get_r(); getch(); } Chú ý: Các tham số mà hàm tạo của lớp dẫn xuất truyền cho hàm tạo của lớp cơ sở không nhất thiết phải lấy hoàn toàn y như từ các tham số nó nhận được. Ví dụ: Hinhtron(double x1,double y1,double r1):Diem (x1/2, y1/2) 76
  76. 5.2.5. Hàm hủy đối với tính kế thừa Hàm hủy của lớp cơ sở cũng không được kế thừa. Khi cả lớp cơ sở và lớp dẫn xuất có các hàm hủy và hàm tạo, các hàm tạo thi hành theo thứ tự dẫn xuất. Các hàm hủy được thi hành theo thứ tự ngược lại. Nghĩa là, hàm tạo của lớp cơ sở thi hành trước hàm tạo của lớp dẫn xuất, hàm hủy của lớp dẫn xuất thi hành trước hàm hủy của lớp cơ sở. Ví dụ 5.6 #include #include class CS { public: CS() {cout<<"\nHam tao lop co so";} ~CS() {cout<<"\nHam huy lop co so";} }; class DX:public CS { public: DX() {cout<<"\nHam tao lop dan xuat";} ~DX() {cout<<"\nHam huy lop dan xuat";} }; void main() { clrscr(); DX ob; getch(); } Chương trình này cho kết quả như sau : Ham tao lop co so Ham tao dan xuat Ham huy lop dan xuat Ham huy lop co so 5.2.6. Khai báo protected Ta đã biết các thành phần khai báo private không được kế thừa trong lớp dẫn xuất. Có thể giải quyết vấn đề này bằng cách chuyển chúng sang vùng public. Tuy nhiên cách làm này lại phá vỡ nguyên lý che dấu thông tin của LTHĐT. C++ đưa ra cách giải quyết khác là sử dụng khai báo protected. Các thành phần protected có phạm vi truy nhập rộng hơn so với các thành phần private, nhưng hẹp hơn so với các thành phần public. Các thành phần protected của lớp cơ sở hoàn toàn giống các thành phần private ngoại trừ một điểm là chúng có thể kế thừa từ lớp dẫn xuất trực tiếp từ lớp cơ sở. Cụ thể như sau: - Nếu kế thừa theo kiểu public thì các thành phần proteted của lớp cơ sở sẽ trở thành các thành phần protected của lớp dẫn xuất. - Nếu kế thừa theo kiểu private thì các thành phần proteted của lớp cơ sở sẽ trở thành các thành phần private của lớp dẫn xuất. 77
  77. 5.2.7. Dẫn xuất protected Ngoài hai kiểu dẫn xuất đã biết là private và public, C++ còn đưa ra kiểu dẫn xuất protected. Trong dẫn xuất loại này thì các thành phần public, protected trong lớp cơ sở trở thành các thành phần protected trong lớp dẫn xuất. 5.3. Đa kế thừa 5.3.1. Định nghĩa lớp dẫn xuất từ nhiều lớp cơ sở Giả sử đã định nghĩa các lớp A, B. Cú pháp để xây dựng lớp C dẫn xuất từ lớp A và B như sau: class C: mode A, mode B { private: // Khai báo các thuộc tính public: // Các hàm thành phần }; trong đó mode có thể là private, public hoặc protected. Ý nghĩa của kiểu dẫn xuất này giống như trường hợp đơn kế thừa. 5.3.2. Một số ví dụ về đa kế thừa Ví dụ 5.7 Chương trình sau minh họa một lớp kế thừa từ hai lớp cơ sở: #include #include #include class M { protected : int m; public : void getm(int x) {m=x;} }; class N { protected : int n; public : void getn(int y) {n=y;} }; class P : public M,public N { public : void display(void) { cout<<"m= "<<m<<endl; cout<<"n= "<<n<<endl; cout<<"m * n = "<<m*n<<endl; } 78