MỤC LỤC LỜI NÓI ĐẦU .................................................................................................................... 3 MỤC LỤC .......................................................................................................................... 5 Chương 1. GIỚI THIỆU LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG ................................. 11 1.1. MỞ ĐẦU .................................................................................................................. 11 1.2. CÁC PHƯƠNG PHÁP LẬP TRÌNH ....................................................................... 11 1.2.1. Lập trình không có cấu trúc (lập trình tuyến tính) ............................................ 11 1.2.2. Lập trình thủ tục hay lập trình có cấu trúc ......................................................... 12 1.2.3. Lập trình module ................................................................................................ 12 1.3. LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG ....................................................................... 13 1.3.1. Giới thiệu ........................................................................................................... 13 1.3.2. Mục tiêu ............................................................................................................. 13 1.4. MỘT SỐ KHÁI NIỆM CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG ....................... 14 1.4.1. Trừu tượng hóa .................................................................................................. 14 1.4.2. Đối tượng (object).............................................................................................. 14 1.4.3. Lớp (class) ......................................................................................................... 16 1.4.4. Thuộc tính và phương thức của lớp ................................................................... 17 1.4.5. Thể hiện (instance)............................................................................................. 17 1.4.6. Thông điệp và truyền thông điệp ....................................................................... 17 1.4.7. Nguyên tắc đóng gói dữ liệu .............................................................................. 17 1.4.8. Tính ẩn thông tin (information hiding) .............................................................. 18 1.4.9. Tính kế thừa (inheritance) .................................................................................. 18 1.4.10. Tính đa hình (polymorphism) .......................................................................... 19 1.4.11. Sơ đồ lớp đối tượng ......................................................................................... 20 1.4.12. Các bước thiết kế đối tượng ............................................................................. 20 1.5. CÁC ƯU ĐIỂM CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG .................................. 20 1.6. NHỮNG ỨNG DỤNG CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG ....................... 21 1.7. CÁC NGÔN NGỮ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG ....................................... 21 1.8. NGÔN NGỮ LẬP TRÌNH C++ ............................................................................... 22 1.8.1. Các đặc điểm mở rộng trong C++ ..................................................................... 22 1.8.2. Lập trình hướng đối tượng trong C++ ............................................................... 23 1.9. CẤU TRÚC MỘT CHƯƠNG TRÌNH C++ ............................................................. 23 1.9.1. Cấu trúc .............................................................................................................. 23 1.9.2. Các tệp tin thư viện thông dụng ......................................................................... 24 1.9.3. Không gian tên ................................................................................................... 24 1.10. THỰC HIỆN CHƯƠNG TRÌNH C++ BẰNG DEV-C++ ..................................... 26 1.10.1. Môi trường phát triển tích hợp IDE ................................................................. 26 1.10.2. Các bước thực hiện .......................................................................................... 26 CÂU HỎI, BÀI TẬP ....................................................................................................... 29 Chương 2. CÁC MỞ RỘNG CỦA C++ ........................................................................ 31 5 2.1. TỔNG QUAN ........................................................................................................... 31 2.1.1. Lịch sử ............................................................................................................... 31 2.1.2. Cấu trúc chương trình C++ cơ bản .................................................................... 31 2.2. NHỮNG KHÁC BIỆT VÀ BỔ SUNG SO VỚI C .................................................. 32 2.2.1. Nhưng từ khóa mới ............................................................................................ 32 2.2.2. Chuyển kiểu dữ liệu ........................................................................................... 32 2.2.3. Hằng số .............................................................................................................. 32 2.2.4. Kiểu có cấu trúc: struct, union, enum ................................................................ 33 2.3. CÁC ĐIỂM KHÔNG TƯƠNG THÍCH GIỮA C++ VÀ ANSI C ........................... 33 2.3.1. Định nghĩa hàm .................................................................................................. 33 2.3.2. Khai báo hàm nguyên mẫu ................................................................................ 34 2.3.3. Sự tương thích giữa con trỏ void và các con trỏ khác ....................................... 34 2.4. CÁC KHẢ NĂNG VÀO/RA MỚI CỦA C++ ......................................................... 35 2.4.1. Ghi dữ liệu lên thiết bị ra chuẩn (màn hình) cout .............................................. 35 2.4.2. Các khả năng viết ra trên cout ........................................................................... 36 2.4.3. Đọc dữ liệu từ thiết bị vào chuẩn (bàn phím) cin .............................................. 37 2.5. NHỮNG TIỆN ÍCH CHO NGƯỜI LẬP TRÌNH .................................................... 38 2.5.1. Chú thích cuối dòng ........................................................................................... 38 2.5.2. Khai báo mọi nơi ............................................................................................... 38 2.5.3. Toán tử phạm vi “::” .......................................................................................... 39 2.5.4. Hàm inline.......................................................................................................... 39 2.6. THAM CHIẾU ......................................................................................................... 40 2.6.1. Tham chiếu tới một biến .................................................................................... 41 2.6.2. Truyền tham số cho hàm bằng tham chiếu ........................................................ 42 2.6.3. Giá trị trả về của hàm là tham chiếu .................................................................. 44 2.7. ĐA NĂNG HÓA (Overloading) ............................................................................... 45 2.7.1. Đa năng hóa toán tử ........................................................................................... 45 2.7.2. Đa năng hóa hàm ............................................................................................... 45 2.8. THAM SỐ NGẦM ĐỊNH TRONG LỜI GỌI HÀM ............................................... 47 2.9. CÁC TOÁN TỬ QUẢN LÝ BỘ NHỚ ĐỘNG: new và delete ................................ 49 2.9.1. Toán tử cấp phát bộ nhớ động new .................................................................... 49 2.9.2. Toán tử giải phóng vùng nhớ động delete ......................................................... 50 CÂU HỎI, BÀI TẬP ....................................................................................................... 55 Chương 3. LỚP VÀ ĐỐI TƯỢNG................................................................................. 57 3.1. GIỚI THIỆU ............................................................................................................. 57 3.2. ĐỊNH NGHĨA LỚP .................................................................................................. 58 3.3. TỪ KHÓA XÁC ĐỊNH THUỘC TÍNH TRUY XUẤT........................................... 59 3.4. TẠO LẬP ĐỐI TƯỢNG .......................................................................................... 60 3.5. TRUY NHẬP TỚI CÁC THÀNH PHẦN CỦA LỚP .............................................. 61 3.6. PHÉP GÁN ĐỐI TƯỢNG ........................................................................................ 67 3.7. CON TRỎ ĐỐI TƯỢNG .......................................................................................... 67 3.8. CON TRỎ THIS ....................................................................................................... 69 3.9. HÀM BẠN ................................................................................................................ 70 3.10. THÀNH PHẦN DỮ LIỆU HẰNG VÀ HÀM THÀNH PHẦN TĨNH .................. 74 6 3.10.1. Dữ liệu thành phần tĩnh.................................................................................... 74 3.10.2. Hàm thành phần tĩnh ........................................................................................ 75 3.11. HÀM TẠO (CONSTRUCTOR) ............................................................................. 77 3.12. HÀM TẠO SAO CHÉP .......................................................................................... 81 3.12.1. Hàm tạo sao chép mặc định ............................................................................. 81 3.12.2. Hàm tạo sao chép ............................................................................................. 83 3.13. HÀM HỦY (DESTRUCTOR)................................................................................ 87 3.14. MỘT SỐ VÍ DỤ ..................................................................................................... 89 3.14.1. Xây dựng lớp tam giác ..................................................................................... 89 3.14.2. Lớp mảng 1 chiều ............................................................................................ 90 3.14.3. Quản lý điểm tuyển sinh .................................................................................. 91 3.14.4. Cài đặt và sử dụng phương thức thiết lập (constructor) và phương thức huỷ (destructor) ................................................................................................................... 93 3.14.5. Lớp ngày tháng ................................................................................................ 95 3.14.6. Lớp phân số...................................................................................................... 96 3.14.7. Lớp số phức ..................................................................................................... 98 3.14.8. Xây dựng lớp thời gian time ............................................................................ 99 3.14.9. Xây dựng lớp sinh viên .................................................................................. 101 CÂU HỎI, BÀI TẬP ..................................................................................................... 103 Chương 4. ĐA NĂNG HÓA TOÁN TỬ ...................................................................... 107 4.1. GIỚI THIỆU CHUNG ............................................................................................ 107 4.1.1. Khái niệm ......................................................................................................... 107 4.1.2. Cú pháp ............................................................................................................ 108 4.1.3. Đa năng hóa toán tử một ngôi (++,--) .............................................................. 108 4.1.4. Đa năng hóa toán tử hai ngôi (+,-,*,/,…) ......................................................... 109 4.1.5. Định nghĩa lại phép gán (=) ............................................................................. 110 4.1.6. Đa năng hóa toán tử nhập/xuất (>>,<<)........................................................... 112 4.2. VÍ DỤ TRÊN LỚP SỐ PHỨC ............................................................................... 113 4.2.1. Hàm toán tử là hàm thành phần ……………………………………………...117 4.2.2. Hàm toán tử là hàm bạn ................................................................................... 114 4.3. KHẢ NĂNG VÀ GIỚI HẠN CỦA ĐỊNH NGHĨA CHỒNG TOÁN TỬ ............. 121 4.4. CÁC CHÚ Ý KHI SỬ DỤNG HÀM TOÁN TỬ ................................................... 122 4.5. ĐA NĂNG HÓA MỘT SỐ TOÁN TỬ .................................................................. 123 4.5.1. Định nghĩa chồng phép gán “ =” ..................................................................... 123 4.5.2. Định nghĩa chồng phép “[]" ............................................................................. 126 4.5.3. Định nghĩa chồng << và >> ............................................................................. 128 4.5.4. Định nghĩa chồng các toán tử new và delete ................................................... 129 4.5.5. Phép nhân ma trận véc tơ ................................................................................. 131 4.6. CHUYỂN ĐỔI KIỂU ............................................................................................. 134 4.6.1. Hàm toán tử chuyển kiểu ép buộc.................................................................... 134 4.6.2. Hàm toán tử chuyển kiểu trong lời gọi hàm .................................................... 136 4.6.3. Hàm toán tử chuyển kiểu trong biểu thức ........................................................ 137 4.6.4. Hàm toán tử chuyển đổi kiểu cơ sở sang kiểu lớp ........................................... 138 4.6.5. Hàm thiết lập trong các chuyển đổi kiểu liên tiếp ........................................... 139 7 4.6.6. Lựa chọn giữa hàm thiết lập và phép toán gán ................................................ 139 4.6.7. Sử dụng hàm thiết lập để mở rộng ý nghĩa một phép toán .............................. 140 4.7. CHUYỂN ĐỔI KIỂU TỪ LỚP NÀY SANG MỘT LỚP KHÁC .......................... 142 4.7.1. Hàm toán tử chuyển kiểu bắt buộc .................................................................. 142 4.7.2. Hàm thiết lập dùng làm hàm toán tử................................................................ 143 4.8. MỘT SỐ VÍ DỤ ..................................................................................................... 143 4.8.1. Thực hiện đa năng hóa toán tử [] để truy cập đến một phần tử của vector ...... 143 4.8.2. Thực hiện đa năng hóa toán tử () để truy cập đến một phần tử của vector ...... 144 4.8.3. Thực hiện đa năng hóa toán tử () để truy cập đến một phần tử của ma trận.... 145 4.8.4. Thực hiện đa năng hóa toán tử ++ và - - .......................................................... 147 CÂU HỎI, BÀI TẬP ..................................................................................................... 148 Chương 5. TÍNH KẾ THỪA VÀ ĐA HÌNH ............................................................... 149 5.1. GIỚI THIỆU ........................................................................................................... 149 5.2. ĐƠN THỪA KẾ ..................................................................................................... 150 5.3. ĐA KẾ THỪA ........................................................................................................ 156 5.4. HÀM ẢO ................................................................................................................ 161 5.4.1. Giới thiệu ......................................................................................................... 161 5.4.2. Định nghĩa........................................................................................................ 162 5.4.3. Quy tắc gọi hàm ảo .......................................................................................... 164 5.4.4. Quy tắc gán địa chỉ đối tượng cho con trỏ lớp cơ sở ....................................... 164 5.5. LỚP CƠ SỞ ẢO ..................................................................................................... 166 5.5.1. Khai báo ........................................................................................................... 166 5.5.2. Hàm tạo và hàm hủy đối với lớp cơ sở ảo ....................................................... 168 5.6. TÍNH ĐA HÌNH TRONG KẾ THỪA .................................................................... 170 5.7. MỘT SỐ VÍ DỤ ..................................................................................................... 172 5.7.1. Cài đặt lớp bệnh nhân ...................................................................................... 172 5.7.2. Cài đặt lớp điểm ............................................................................................... 174 5.7.3. Xây dựng lớp phân số ...................................................................................... 178 5.7.4. Xây dựng lớp số phức ...................................................................................... 180 5.7.5. Quản lý và sinh viên trong trường đại học....................................................... 183 CÂU HỎI, BÀI TẬP ..................................................................................................... 187 Chương 6. KHUÔN HÌNH ........................................................................................... 195 6.1. KHUÔN HÌNH HÀM ............................................................................................. 195 6.1.1. Khái niệm ......................................................................................................... 195 6.1.2. Tạo một khuôn hình hàm ................................................................................. 195 6.1.3. Sử dụng khuôn hình hàm ................................................................................. 196 6.1.4. Các tham số kiểu của khuôn hình hàm ............................................................ 196 6.1.5. Định nghĩa chồng các khuôn hình hàm ........................................................... 197 6.2. KHUÔN HÌNH LỚP .............................................................................................. 197 6.2.1. Khái niệm ......................................................................................................... 197 6.2.2. Tạo một khuôn hình lớp ................................................................................... 198 6.2.3. Sử dụng khuôn hình lớp ................................................................................... 198 6.2.4. Các tham số trong khuôn hình lớp ................................................................... 199 6.3. MỘT SỐ VÍ DỤ ..................................................................................................... 199 8 6.3.1. Viết khuôn hình hàm để sắp xếp kiểu dữ liệu bất kỳ ....................................... 199 6.3.2. Cài đặt và sử dụng hàm template cho mảng 1 chiều số nguyên ...................... 200 6.3.3. Cài đặt và sử dụng lớp template cho mảng 1 chiều ......................................... 201 6.3.4. Cài đặt và sử dụng operator trên lớp template điểm trong mặt phẳng ............. 203 CÂU HỎI, BÀI TẬP ..................................................................................................... 206 Chương 7. THIẾT KẾ CHƯƠNG TRÌNH THEO HƯỚNG ĐỐI TƯỢNG ............ 207 7.1. CÁC GIAI ĐOẠN PHÁT TRIỂN HỆ THỐNG ..................................................... 207 7.1.1. Phân tích yêu cầu ............................................................................................. 207 7.1.2. Phân tích .......................................................................................................... 207 7.1.3. Thiết kế ............................................................................................................ 207 7.1.4. Lập trình ........................................................................................................... 207 7.1.5. Kiểm tra ........................................................................................................... 207 7.2. CÁC BƯỚC ĐỂ THIẾT KẾ CHƯƠNG TRÌNH ................................................... 208 7.3. MỘT SỐ VÍ DỤ ..................................................................................................... 209 7.3.1. Ví dụ 1 ............................................................................................................. 209 7.3.2. Ví dụ 2 ............................................................................................................. 211 7.4. KỸ THUẬT THIẾT KẾ MỘT LỚP ĐỐI TƯỢNG................................................ 213 7.4.1. Thiết kế thuộc tính ........................................................................................... 213 7.4.2. Thiết kế các hành động của lớp ....................................................................... 214 7.4.3. Mẫu cài đặt ràng buộc ...................................................................................... 215 CÂU HỎI, BÀI TẬP ..................................................................................................... 218 Chương 8. CÁC DÒNG NHẬP, XUẤT VÀ LÀM VIỆC VỚI TỆP TIN ................. 219 8.1. GIỚI THIỆU CHUNG ............................................................................................ 219 8.1.1. Khái niệm về dòng ........................................................................................... 219 8.1.2. Thư viện các lớp vào ra.................................................................................... 220 8.2. NHẬP/XUẤT VỚI CIN/COUT ............................................................................. 220 8.2.1. Toán tử nhập >> ............................................................................................... 221 8.2.2. Các hàm nhập kí tự và xâu kí tự ...................................................................... 222 8.2.3. Toán tử xuất <<................................................................................................ 223 8.3. ĐỊNH DẠNG .......................................................................................................... 224 8.3.1. Chỉ định độ rộng cần in.................................................................................... 224 8.3.2. Chỉ định kí tự chèn vào khoảng trống trước giá trị cần in ............................... 224 8.3.3. Chỉ định độ chính xác (số số lẻ thập phân) cần in ........................................... 224 8.3.4. Các cờ định dạng ............................................................................................. 225 8.3.5. Nhóm căn lề ..................................................................................................... 225 8.3.6. Nhóm định dạng số nguyên ............................................................................. 225 8.3.7. Nhóm định dạng số thực .................................................................................. 225 8.3.8. Nhóm định dạng hiển thị ................................................................................. 226 8.4. CÁC BỘ VÀ HÀM ĐỊNH DẠNG ......................................................................... 226 8.4.1. Các bộ định dạng ............................................................................................. 226 8.4.2. Các hàm định dạng (#include <iomanip.h>) ................................................... 227 8.5. IN RA MÁY IN ...................................................................................................... 227 8.6. LÀM VIỆC VỚI FILE ............................................................................................ 227 8.6.1. Tạo đối tượng gắn với file ............................................................................... 228 9 8.6.2. Đóng file và giải phóng đối tượng ................................................................... 228 8.6.3. Kiểm tra sự tồn tại của file, kiểm tra hết file ................................................... 231 8.6.4. Đọc ghi đồng thời trên file ............................................................................... 231 8.6.5. Di chuyển con trỏ file ...................................................................................... 231 8.7. NHẬP/XUẤT NHỊ PHÂN ..................................................................................... 233 8.7.1. Khái niệm về 2 loại file: văn bản và nhị phân ................................................. 233 8.7.2. Đọc, ghi kí tự ................................................................................................... 233 8.7.3. Đọc, ghi dãy kí tự............................................................................................. 234 8.7.4. Đọc ghi đồng thời ............................................................................................ 235 8.8. MỘT SỐ VÍ DỤ ..................................................................................................... 238 8.8.1. Ghi các số chẵn từ 1 đến 1000 vào file “So Chan.txt” .................................... 238 8.8.2. Đọc file “So Chan.txt” ..................................................................................... 238 8.8.3. Tạo tệp tin văn bản chứa số nguyên................................................................. 239 8.8.4. Tệp chứa ma trận ............................................................................................. 240 8.8.5. Tệp tin nhị phân ............................................................................................... 241 CÂU HỎI, BÀI TẬP ..................................................................................................... 242 CÂU HỎI TRẮC NGHIỆM ÔN TẬP ......................................................................... 243 TÀI LIỆU THAM KHẢO............................................................................................. 265 10 Chương 1 GIỚI THIỆU LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG Chương 1 trình bày những vấn đề sau: những nhược điểm của các phương pháp 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 (LTHĐT). Chương này cũng cung cấp những khái niệm cơ sở của phương pháp hướng đối tượng (đối tượng, lớp, kế thừa,…); 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. Đồng thời, môi trường tích hợp Dev-C++ cũng được giới thiệu như là một công cụ soạn thảo và biên dịch chương trình viết bằng ngôn ngữ lập trình C/ C++. 1.1. MỞ ĐẦU Xây dựng phần mềm bao gồm rất nhiều công đoạn: phân tích và thiết kế, cài đặt, kiểm tra/thử nghiệm và bảo trì. Cài đặt (programming/coding) chỉ là 1 phần trong quá trình trên. C++/C#/Java/… là những ngôn ngữ lập trình để viết chương trình. Phương pháp lập trình là hệ thống hướng dẫn các giai đoạn cần thiết, cấu trúc của một chương trình. Phương pháp lập trình là các cách tiếp cận giúp cho quá trình cài đặt hiệu quả hơn. Các yêu cầu chính của phần mềm: Tính tái sử dụng (reusability); Tính mở rộng (extensibility); Tính mềm dẻo (flexibility). 1.2. CÁC PHƯƠNG PHÁP LẬP TRÌNH Sự phát triển của các phương pháp (kỹ thuật) lập trình liên quan chặt chẽ tới sự phát triển phần cứng của máy vi tính cũng như việc ứng dụng máy tính vào giải quyết các vấn đề trong thực tế. Có thể chia các phương pháp lập trình thành các kiểu sau: Lập trình không có cấu trúc; Lập trình hướng thủ tục; Lập trình theo kiểu module hóa; Lập trình hướng đối tượng. 1.2.1. Lập trình không có cấu trúc (lập trình tuyến tính) Dùng để viết các chương trình nhỏ và đơn giản chỉ chứa một “chương trình chính”. Ở đây một chương trình chính có nghĩa là một tập các lệnh hoặc câu lệnh làm việc với các dữ liệu toàn cục trong cả chương trình (các biến dùng trong chương trình là các biến toàn cục). Một số nhược điểm của lập trình không có cấu trúc: Lập trình không có cấu trúc không có khả năng kiểm soát tính thấy được của dữ liệu. Mọi dữ liệu trong chương trình đều là biến toàn cục do đó có thể bị thay đổi bởi bất kỳ phần nào đó của chương trình. Việc không kiểm soát được tính thấy được của dữ liệu dẫn đến các khó khăn trong việc gỡ lỗi chương trình, đặc biệt là các chương trình lớn. Kỹ thuật lập trình không có cấu trúc có rất nhiều bất lợi lớn khi chương trình đủ lớn. Ví dụ nếu chúng ta cần thực hiện lại một đoạn câu lệnh trên một tập dữ liệu khác thì buộc phải copy đoạn lệnh đó tới vị trí trong chương trình mà chúng ta muốn thực hiện. Điều này làm nảy sinh ý 11 tưởng trích ra các đoạn lệnh thường xuyên cần thực hiện đó, đặt tên cho chúng và đưa ra một kỹ thuật cho phép gọi và trả về các giá trị từ các thủ tục này. Ví dụ 1.1: Nhận xét qua ví dụ trên: Chương trình là một dãy các lệnh; Lập trình là viết các lệnh trong dãy lệnh; Không mang tính thiết kế; Tiêu biểu là ngôn ngữ Basic, Fortran; Chương trình đơn giản, số dòng lệnh ít; Thực hiện trình tự từ đầu đến cuối; Không có cấu trúc; Dùng các lệnh “goto/ gosub” để nhảy đến một vị trí nào đó trong chương trình. 1.2.2. Lập trình thủ tục hay lập trình có cấu trúc Với lập trình thủ tục hay hướng thủ tục ta có thể nhóm các câu lệnh thường xuyên thực hiện trong chương trình chính lại một chỗ và đặt tên đoạn câu lệnh đó thành một thủ tục. Một lời gọi tới thủ tục sẽ được sử dụng để thực hiện đoạn câu lệnh đó. Sau khi thủ tục thực hiện xong điều khiển trong chương trình được trả về ngay sau vị trí lời gọi tới thủ tục trong chương trình chính. Với các cơ chế truyền tham số cho thủ tục chúng ta có các chương trình con. Một chương trình chính bao gồm nhiều chương trình con và các chương trình được viết mang tính cấu trúc cao hơn, đồng thời cũng ít lỗi hơn. Nếu một chương trình con là đúng đắn thì kết quả thực hiện trả về luôn đúng và chúng ta không cần phải quan tâm tới các chi tiết bên trong thủ tục. Còn nếu có lỗi chúng ta có thể thu hẹp phạm vi gỡ lỗi trong các chương trình con chưa được chứng minh là đúng, đây được xem như trừu tượng hàm và là nền tảng cho lập trình thủ tục. 1.2.3. Lập trình module Trong lập trình module các thủ tục có cùng một chức năng chung sẽ được nhóm lại với nhau tạo thành một module riêng biệt. Một chương trình sẽ không chỉ bao gồm một phần đơn lẻ. Nó được chia thành một vài phần nhỏ hơn tương tác với nhau qua các lời gọi thủ tục và tạo thành toàn bộ chương trình. Mỗi module có dữ liệu riêng của nó. Điều này cho phép các module có thể kiểm soát các dữ liệu riêng của nó bằng các lời gọi tới các thủ tục trong module đó. Tuy nhiên mỗi module chỉ xuất hiện nhiều nhất một lần trong cả chương trình. Nhược điểm của lập trình thủ tục và lập trình module hóa: Khi độ phức tạp của chương trình tăng lên sự phụ thuộc của nó vào các kiểu dữ liệu cơ bản mà nó xử lý cũng tăng theo. Do đó khi thay đổi cài đặt của một kiểu dữ liệu sẽ dẫn đến nhiều thay đổi trong các thủ tục sử dụng nó. Trong lập trình có cấu trúc mỗi người sẽ được giao xây dựng một số thủ tục và kiểu dữ liệu. Những lập trình viên xử lý các thủ tục khác nhau nhưng lại có liên quan tới các kiểu dữ liệu dùng chung nên nếu một người thay đổi kiểu dữ liệu thì sẽ làm ảnh hưởng tới công việc của nhiều người khác. 12 Việc phát triển các phầm mềm mất nhiều thời gian tập trung xây dựng lại các cấu trúc dữ liệu cơ bản. Khi xây dựng một chương trình mới trong lập trình có cấu trúc lập trình viên thường phải xây dựng lại các cấu trúc dữ liệu cơ bản cho phù hợp với bài toán. 1.3. LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG 1.3.1. Giới thiệu Lập trình hướng đối tượng đặt trọng tâm vào đối tượng, yếu tố quan trọng trong quá trình phát triển chương trình và không cho phép dữ liệu biến động tự do trong hệ thống. Dữ liệu được gắn chặt với các hàm thành các vùng riêng mà chỉ có các hàm đó tác động lên và cấm các hàm bên ngoài truy nhập tới một cách tuỳ tiện. LTHĐT cho phép chúng ta phân tích bài toán thành 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 các hàm xung quanh các đối tượng đó. Các đối tượng có thể tác động, trao đổi thông tin với nhau thông qua cơ chế thông báo (message). Tổ chức một chương trình hướng đối tượng có thể mô tả như trong hình 1.1. Hình 1.1: Các đối tượng trao đổi thông tin qua thông báo Lập trình hướng đối tượng có các đặc tính chủ yếu sau: Tập trung vào dữ liệu thay cho các hàm; Chương trình được chia thành các đối tượng; Các cấu trúc dữ liệu được thiết kế sao cho đặc tả được đối tượng; Các hàm thao tác trên các vùng dữ liệu của đối tượng được gắn với cấu trúc dữ liệu đó; Dữ liệu được đóng gói lại, được che giấu và không cho phép các hàm ngoại lai truy nhập tự do; Các đối tượng tác động và trao đổi thông tin với nhau qua các hàm; Có thể dễ dàng bổ sung dữ liệu và các hàm mới vào đối tượng nào đó khi cần thiết; Chương trình được thiết kế theo cách tiếp cận từ dưới lên (bottom-up). 1.3.2. Mục tiêu • Loại bỏ những thiếu sót của tiếp cận theo thủ tục; • Tiếp cận theo hướng trừu tượng hoá (abstraction); • Dữ liệu được xem là phần trung tâm và được bảo vệ; • Hàm gắn kết với dữ liệu; • Phân tách bài toán thành nhiều đối tượng và yêu cầu chúng thực hiện hành động của mình; • Tăng cường khả năng sử dụng lại; • Tiếp cận bottom-up. Ưu điểm • Cung cấp một cấu trúc module rõ ràng: Giao diện định nghĩa rõ ràng và chi tiết cài đặt ẩn; • Duy trì và sửa đổi mã nguồn dễ dàng; • Cung cấp framework tốt với các thư viện mã nguồn. 13 1.4. MỘT SỐ KHÁI NIỆM CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG 1.4.1. Trừu tượng hóa Trừu tượng hóa là quá trình đơn giản hóa một đối tượng mà trong đó chỉ bao gồm những đặc điểm quan tâm và bỏ qua những đặc điểm chi tiết nhỏ. Quá trình trừu tượng hóa dữ liệu giúp ta xác định được những thuộc tính, hành động nào của đối tượng cần thiết sử dụng cho chương trình. Trừu tượng hóa dữ liệu là quá trình trừu tượng hóa một đối tượng một cách đủ thông tin để lưu vào hệ thống dữ liệu. Quá trình này rất giống với bộ phận phân tích hệ thống dữ liệu, lấy thông tin của khách hàng và trừu tượng hóa để đưa vào CSDL. Tóm lại: Trừu tượng hóa dữ liệu là quá trình tổ chức một bài toán phức tạp thành những đối tượng có cấu trúc chặt chẽ, trong đó các dữ liệu và hành động của đối tượng được định nghĩa. Trong đối tượng dữ liệu và hành động có sự gắn kết chặt chẽ với nhau. Thế giới thực Trừu tượng hóa Phần mềm Thuộc tính Thuộc tính Thực thể Hành động Phương thức Hình 1.2: Trừu tượng hóa Bảng 1.1: Trừu tượng hóa thế giới thực Thế giới thực Đối tượng trong thế giới thực Khái niệm chung về đối tượng Các thông tin được quan tâm về 1 đối tượng Các khả năng của đối tượng Phân công giữa các đối tượng PPLT Đối tượng Lớp đối tượng Thuộc tính Phương thức Yêu cầu Ngôn ngữ lập trình Biến có kiểu cấu trúc Kiểu dữ liệu cấu trúc Thành phần thuộc tính của kiểu cấu trúc Các phương thức Gọi thực hiện phương thức 1.4.2. Đối tượng (object) Hình 1.3: Một số đối tượng trong thế giới thực 14 Đối tượng trong thế giới thực (Real Object): • Thực thể cụ thể mà ta có thể sờ, nhìn thấy hay cảm nhận được; • Có trạng thái (state) và hành động (behaviour); • Một đối tượng có tên gọi và chu trình sống: sinh ra, hoạt động và mất đi. Hình 1.4: Ví dụ về đối tượng Đối tượng Phần mềm (Software Object): • Trạng thái: thuộc tính (attribute/ property); • Hành động: phương thức (method). Hình 1.5: Đối tượng phần mềm Hình 1.6: Đối tượng xe đạp Đối tượng (object) là một thực thể phần mềm bao bọc các thuộc tính và các phương thức liên quan. • Thuộc tính được xác định bởi giá trị cụ thể gọi là thuộc tính thể hiện; • Một đối tượng cụ thể được gọi là một thể hiện. Như vậy, đối tượng là sự kết hợp giữa dữ liệu và thủ tục (hay còn gọi là các phương thức method) thao tác trên dữ liệu đó. Có thể đưa ra công thức phản ánh bản chất kỹ thuật của LTHĐT như sau: Đối tượng = Dữ liệu + Phương thức 15 1.4.3. Lớp (class) • Nhiều đối tượng cùng loại lớp; • Nhiều đối tượng cùng loại chia sẻ những đặc điểm chung. Hình 1.7: Lớp ô tô Hình 1.8: Đặc điểm chung của lớp ô tô Hình 1.9: Lớp động vật 16 • Một lớp là một thiết kế (blueprint) hay mẫu (prototype) cho các đối tượng cùng kiểu. VD: lớp XeDap là một thiết kế chung cho nhiều đối tượng xe đạp được tạo ra; • Lớp định nghĩa các thuộc tính và các phương thức chung cho tất cả các đối tượng của cùng một loại nào đó; • Một đối tượng là một thể hiện cụ thể của một lớp. VD: mỗi đối tượng xe đạp là một thể hiện của lớp XeDap; • Mỗi thể hiện có thể có những thuộc tính thể hiện khác nhau. VD: một xe đạp có thể đang ở bánh răng thứ 5/ một xe khác có thể là đang ở bánh răng thứ 3. Như vậy, lớp là một khái niệm mới trong LTHĐT so với các kỹ thuật lập trình khác. Đó là một tập các đối tượng có cấu trúc dữ liệu và các phương thức giống nhau (hay nói cách khác là một tập các đối tượng cùng loại). Như vậy khi có một lớp thì chúng ta sẽ biết được một mô tả cấu trúc dữ liệu và phương thức của các đối tượng thuộc lớp đó. Mỗi đối tượng sẽ là một thể hiện cụ thể (instance) của lớp đó. Trong lập trình, chúng ta có thể coi một lớp như là một kiểu, còn các đối tượng sẽ là các biến có kiểu của lớp. Ví dụ 1.2: Người là một lớp đối tượng. Một lớp đối tượng được đặc trưng bằng các thuộc tính và các hoạt động (hành vi, thao tác). 1.4.4. Thuộc tính và phương thức của lớp Thuộc tính (attribute) là dữ liệu trình bày các đặc điểm về một đối tượng. Mỗi thao tác trên một lớp đối tượng cụ thể tương ứng với một cài đặt cụ thể khác nhau. Một cài đặt như vậy được gọi là một phương thức (method). Phương thức (method) có liên quan tới những việc mà đối tượng có thể làm. Một phương thức đáp ứng một chức năng tác động lên dữ liệu của đối tượng (thuộc tính). 1.4.5. Thể hiện (instance) Một đối tượng cụ thể thuộc một lớp được gọi là một thể hiện (instance) của lớp đó. Ví dụ 1.3: Liên, 25 tuổi, nặng 58kg là một thể hiện của lớp người. 1.4.6. Thông điệp và truyền thông điệp Thông điệp (message) là một yêu cầu một hoạt động • Đối tượng nhận thông điệp; • Tên của phương thức thực hiện; • Các tham số mà phương thức cần. Truyền thông điệp: một đối tượng gọi một hay nhiều phương thức của đối tượng khác để yêu cầu thông tin. 1.4.7. Nguyên tắc đóng gói dữ liệu Tính đóng gói là việc che giấu việc thực thi chi tiết của một đối tượng. Trong LTCT ta đã thấy là các hàm hay thủ tục được sử dụng mà không cần biết đến nội dung cụ thể của nó. Người sử dụng chỉ cần biết chức năng của hàm cũng như các tham số cần truyền vào để gọi hàm chạy mà không cần quan tâm đến những lệnh cụ thể bên trong nó. Người ta gọi đó là sự đóng gói về chức năng. Trong LTHĐT, không những các chức năng được đóng gói mà cả dữ liệu cũng như vậy. Với mỗi đối tượng người ta không thể truy nhập trực tiếp vào các thành phần dữ liệu cảu nó mà phải thông qua các thành phần chức năng (các phương thức) để làm việc đó. 17 Hình 1.10: Nguyên tắc đóng gói Chúng ta sẽ thấy sự đóng gói thực sự về dữ liệu chỉ có trong một ngôn ngữ LTHĐT “thuần khiết” (pure) theo nghĩa các ngôn ngữ được thiết kế ngay từ đầu chỉ cho LTHĐT. Còn đối với các ngôn ngữ “lai” (hybrid) được xây dựng trên các ngôn ngữ khác ban đầu chưa phải là HĐT như C++ được nói đến trong cuốn sách này, vẫn có những ngoại lệ nhất định vi phạm nguyên tắc đóng gói dữ liệu. Là cơ chế ràng buộc dữ liệu và thao tác trên dữ liệu đó thành một thể thống nhất, tránh được các tác động bất ngờ từ bên ngoài. Ví dụ 1.4: Một lớp có thể có nhiều thuộc tính và hàm. Ta có thể xét quyền truy cập cho các thuộc tính và hàm này. 1.4.8. Tính ẩn thông tin (information hiding) Thuộc tính được lưu trữ hay phương thức được cài đặt như thế nào được che giấu khỏi các đối tượng khác. Hình 1.11: Ẩn thông tin 1.4.9. Tính kế thừa (inheritance) Một khái niệm quan trọng của LTHĐT là sự kế thừa. Sự kế thừa cho phép chúng ta định nghĩa một lớp mới trên cơ sở các lớp đã tồn tại, tất nhiên có bổ sung những phương thức hay các thành phần dữ liệu mới. Khả năng kế thừa cho phép chúng ta sử dụng lại một cách dễ dàng các module chương trình mà không cần một thay đổi các module đó. Rõ ràng đây là một điểm mạnh của LTHĐT so với LTCT. 18 Animals Hình 1.12: Tính kế thừa của lớp động vật Thừa kế có khả năng tái sử dụng các lớp có các đặc tính chung với nhau để tạo ra các lớp mới từ một hay nhiều lớp đã có. Thừa kế cho phép các lớp được định nghĩa kế thừa từ các lớp khác. Ví dụ 1.5: Lớp xe đạp leo núi và xe đạp đua là những lớp con (subclass) của lớp xe đạp. Thừa kế nghĩa là các phương thức và các thuộc tính được định nghĩa trong một lớp có thể được thừa kế hoặc được sử dụng lại bởi lớp khác. Ví dụ 1.6: Xét về bản chất: NV_VP và NV_SX đều là nhân viên nên nó phải có các thuộc tính chung: MaNV, Hoten, CMND... của một người nhân viên => kế thừa từ lớp NV. 1.4.10. Tính đa hình (polymorphism) Một hành động cùng tên có thể được thực hiện khác nhau bởi các đối tượng/các lớp khác nhau. Ngữ cảnh khác kết quả khác. Tính đa hình xuất hiện khi có khái niệm kế thừa. Giả sử chúng ta có một kế thừa lớp hình tứ giác và lớp hình tam giác kế thừa từ lớp hình đa giác (hình tam giác và tứ giác sẽ có đầy đủ các thuộc tính và tính chất của một hình đa giác). Lúc này một đối tượng thuộc lớp hình tam giác hay tứ giác đều có thể hiểu rằng nó là một hình đa giác. Mặt khác với mỗi đa giác ta có thể tính diện tích của nó. Như vậy làm thế nào mà một đa giác có thể sử dụng đúng công thức để tính diện tích phù hợp với nó là hình tam giác hay tứ giác. Ta gọi đó là tính đa hình. Điểm Đường Thẳng Hình Tròn Hình Vuông Vẽ Hình 1.13: Tính đa hình 19 Ví dụ 1.7: Đối với nhân viên trong cùng một công ty nhưng cách tính lương của nhân viên văn phòng khác cách tính lương của nhân viên sản xuất. 1.4.11. Sơ đồ lớp đối tượng Là sơ đồ dùng để mô tả lớp đối tượng. Sơ đồ đối tượng bao gồm sơ đồ lớp và sơ đồ thể hiện. Sơ đồ lớp: mô tả các lớp đối tượng trong hệ thống. Một lớp đối tượng được diễn tả bằng một hình chữ nhật có 3 phần: Phần đầu chỉ tên lớp. Phần thứ hai mô tả các thuộc tính. Phần thứ ba mô tả các thao tác của các đối tượng trong lớp đó. Hình 1.14: Sơ đồ lớp 1.4.12. Các bước thiết kế đối tượng Bước 1: Xây dựng sơ đồ đối tượng Xác định các lớp đối tượng; Xác định các quan hệ giữa các lớp. Bước 2: Thiết kế các lớp Thiết kế thuộc tính, các hành động Bước 3: Cài đặt các lớp Bước 4: Sử dụng các lớp để tạo ra các đối tượng 1.5. CÁC ƯU ĐIỂM CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG Lập trình hướng đối tượng (LTHĐT) đem lại một số lợi thế cho người thiết kế lẫn người lập trình. 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 phần mềm có độ phức tạp và chất lượng cao. Phương pháp này mở ra một triển vọng to lớn cho người lập trình. Những ưu điểm chính của LTHĐT là: Thông qua nguyên lý kế thừa, chúng ta 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à có thể mở rộng khả năng sử dụng của các lớp đã xây dựng mà không cần phải viết lại; 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 nữa. Đ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; 20 Nguyên lý đóng gói hay 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 đổi bởi những đoạn chương trình khác; 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 chương trình; Cách tiếp cận thiết kế đặt trọng tâm vào dữ liệu, giúp chúng ta xây dựng được mô hình chi tiết và dễ dàng cài đặt hơn; Các 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; Kỹ thuật truyền thông báo trong việc trao đổi thông tin giữa các đối tượng làm 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; Có thể quản lý được độ phức tạp của những sản phẩm phần mềm. 1.6. NHỮNG ỨNG DỤNG CỦA LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG LTHĐT là một trong những thuật ngữ được nhắc đến nhiều nhất hiện nay trong công nghệ phần mềm và nó được ứng dụng để phát triển phần mềm trong nhiều lĩnh vực khác nhau. Trong số đó, ứng dụng quan trọng và nổi tiếng nhất hiện nay là thiết kế giao diện với người sử dụng, kiểu như Windows. Các hệ thông tin quản lý trong thực tế thường rất phức tạp, chứa nhiều đối tượng với các thuộc tính và hàm phức tạp. Để giải quyết những hệ thông tin phức tạp như thế, LTHĐT tỏ ra rất hiệu quả. Các lĩnh vực ứng dụng phù hợp với kỹ thuật LTHĐT có thể liệt kê như dưới đây: Các hệ thống làm việc theo thời gian thực; Các hệ mô hình hoá hoặc mô phỏng các quá trình; Các hệ cơ sở dữ liệu hướng đối tượng; Các hệ siêu văn bản (hypertext), đa phương tiện (multimedia); Các hệ thống trí tuệ nhân tạo và các hệ chuyên gia; Các hệ thống song song và mạng nơron; Các hệ tự động hoá văn phòng hoặc trợ giúp quyết định; Các hệ CAD/CAM. Với nhiều đặc tính phong phú của LTHĐT nói riêng, của phương pháp phân tích thiết kế và phát triển hướng đối tượng nói chung chúng ta hy vọng công nghiệp phần mềm sẽ có những cải tiến nhảy vọt không những về chất lượng, mà còn gia tăng nhanh về số lượng trong tương lai. 1.7. CÁC NGÔN NGỮ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG LTHĐT không phải là đặc quyền của một ngôn ngữ đặc biệt nào. Cũng giống như kỹ thuật lập trình có cấu trúc, các khái niệm trong LTHĐT được thể hiện trong nhiều ngôn ngữ lập trình khác nhau. Những ngôn ngữ cung cấp được những khả năng LTHĐT được gọi là ngôn ngữ lập trình hướng đối tượng. Tuy vẫn có những ngôn ngữ chỉ cung cấp khả năng tạo lớp và đối tượng mà không cho phép kế thừa, do đó hạn chế khả năng LTHĐT. Các ngôn ngữ SIMULA, SMALLTALK, JAVA thuộc họ ngôn ngữ LTHĐT thuần khiết, nghĩa là nó không cho phép phát triển các chương trình cấu trúc trên các ngôn ngữ loại này. Còn ngôn ngữ C++ thuộc loại ngôn ngữ “lai” bởi vì nó được phát triển từ ngôn ngữ C. Do đó trên C++ vẫn có thể sử dụng tính cấu trúc và đối tượng của chương trình. Điều này tỏ ra rất phù hợp khi chúng ta mới bắt đầu học một ngôn ngữ lập trình. Đó chính là lý do mà chúng tôi sử dụng ngôn ngữ C++ để giới thiệu phương pháp LTHĐT trong cuốn sách này. Một lý do khác nữa là C++ sử dụng cú pháp của ngôn ngữ C là ngôn ngữ rất thông dụng trong lập trình chuyên nghiệp. 21 Hình 1.15: Các ngôn ngữ lập trình hướng đối tượng 1.8. NGÔN NGỮ LẬP TRÌNH C++ Vào năm 1983, giáo sư Bjarne Stroustrap bắt đầu nghiên cứu và phát triển việc cài đặt khả năng LTHĐT vào ngôn ngữ C tạo ra một ngôn ngữ mới gọi là C++. Tên gọi này có thể phân tích ý nghĩa rằng nó là ngôn ngữ C mà có hai đặc điểm mới tương ứng với hai dấu cộng. Đặc điểm thứ nhất là một số khả năng mở rộng so với C như tham chiếu, chồng hàm, tham số mặc định... Đặc điểm thứ hai chính là khả năng LTHĐT. Hiện nay C++ chưa phải là một ngôn ngữ hoàn toàn ổn định. Kể từ khi phiên bản đầu tiên ra đời vào năm 1986 đã có rất nhiều thay đổi trong các phiên bản C++ khác nhau: bản 1.1 ra đời vào năm 1986, 2.0 vào năm 1989 và 3.0 vào năm 1991. Phiên bản 3.0 này được sử dụng để làm cơ sở cho việc định nghĩa một ngôn ngữ C++ chuẩn (kiểu như Ansi C). Trên thực tế hiện nay tất cả các chương trình dịch C++ đều tương thích với phiên bản 3.0. Vì vậy C++ hầu như không gây bất kỳ một khó khăn nào khi chuyển đổi từ một môi trường này sang môi trường khác, như chúng ta đã biết C++ như là một sự bổ sung khả năng LTHĐT vào ngôn ngữ C. Sẽ có nhiều người nghĩ rằng ngôn ngữ C nói ở đây là C theo chuẩn ANSI. Thực ra không phải hoàn toàn như vậy. Tên thực tế vẫn tồn tại một vài điểm không tương thích giữa ANSI C và C++. Mặt khác cũng cần thấy rằng những mở rộng có trong C++ so với Ansi C không chỉ là để phục vụ cho mục đích tạo cho ngôn ngữ khả năng LTHĐT. Có những thay đổi chỉ với mục đích đơn thuần là tăng sức mạnh cho ngôn ngữ C hiện thời. Ngoài ra có một vài thay đổi nhỏ ở C++ so với ANSI C như sau: Định nghĩa các hàm: khai báo, truyền tham số và giá trị trả lại. Sự tương thích giữa các con trỏ. Tính linh hoạt của các hằng (const). 1.8.1. Các đặc điểm mở rộng trong C++ Như đã đề cập ở trên C++ chứa cả những mở rộng so với C mà không liên quan đến kỹ thuật hướng đối tượng. Những mở rộng này sẽ được mô tả cụ thể trong chương sau, ở đây chúng ta chỉ tóm tắt lại một vài điểm chính. 22 Khả năng viết các dòng chú thích mới. Khả năng khai báo linh hoạt hơn. Khả năng định nghĩa lại các hàm: các hàm cùng tên có thể thực hiện theo những thao tác khác nhau. Các lời gọi hàm sẽ dùng kiểu và số tham số để xác định đúng hàm nào cần thực hiện. Có thêm các toán tử định nghĩa bộ nhớ động mới: new và delete. Khả năng định nghĩa các hàm inline để tăng tốc độ thực hiện chương trình. Tạo các biến tham chiếu đến các biến khác. 1.8.2. Lập trình hướng đối tượng trong C++ C++ chứa đựng khái niệm lớp. Một lớp bao gồm các thành phần dữ liệu hay là thuộc tính và các phương thức hay là hàm thành phần. Từ một lớp ta có thể tạo ra các đối tượng hoặc bằng cách khai báo thông thường một biến có kiểu là lớp đó hoặc bằng cách cấp phát bộ nhớ động nhờ sử dụng toán tử new. C++ cho phép chúng ta đóng gói dữ liệu nhưng nó không bắt buộc chúng ta thực hiện điều đó. Đây là một nhược điểm của C++. Tuy nhiên cũng cần thấy rằng bản thân C++ chỉ là sự mở rộng của C nên nó không thể là một ngôn ngữ LTHĐT thuần khiết được. C++ cho phép ta định nghĩa các hàm thiết lập (constructor) cho một lớp. Hàm thiết lập là một phương thức đặc biệt được gọi đến tại thời điểm một đối tượng của lớp được tạo ra. Hàm thiết lập có nhiệm vụ khởi tạo một đối tượng: cấp phát bộ nhớ, gán các giá trị cho các thành phần dữ liệu cũng như việc chuẩn bị chỗ cho các đối tượng mới. Một lớp có thể có một hay nhiều hàm thiết lập. Để xác định hàm thiết lập nào cần gọi đến, chương trình biên dịch sẽ so sánh các đối số với các tham số truyền vào. Tương tự như hàm thiết lập, một lớp có thể có một hàm huỷ bỏ (destructor), một phương thức đặc biệt được gọi đến khi đối tượng được giải phóng khỏi bộ nhớ. Lớp trong C++ thực chất là một kiểu dữ liệu do người sử dụng định nghĩa. Khái niệm định nghĩa chồng toán tử cho phép định nghĩa các phép toán trên một lớp giống như các kiểu dữ liệu chuẩn của C. Ví dụ ta có thể định nghĩa một lớp số phức với các phép toán cộng, trừ, nhân, chia. Cũng giống như C, C++ có khả năng chuyển đổi kiểu. Không những thế, C++ còn cho phép mở rộng sự chuyển đổi này sang các kiểu do người sử dụng tự định nghĩa (các lớp). Ví dụ, ta có thể chuyển đổi từ kiểu chuẩn int của C sang kiểu số phức mà ta định nghĩa chẳng hạn. C++ cho phép thực hiện kế thừa các lớp đã xây dựng. Từ phiên bản 2.0 trở đi, C++ còn cho phép một lớp kế thừa cùng một lúc từ nhiều lớp khác nhau (gọi là sự đa kế thừa). C++ cung cấp những thao tác vào ra mới dựa trên cơ sở khái niệm luồng dữ liệu (flow). Sự ưu việt của các thao tác này ở chỗ: Sử dụng đơn giản; Kích thước bộ nhớ được rút gọn; Khả năng áp dụng trên các kiểu do người sử dụng định nghĩa bằng cách sử dụng cơ chế định nghĩa chồng toán tử. 1.9. CẤU TRÚC MỘT CHƯƠNG TRÌNH C++ 1.9.1. Cấu trúc Thông thường một chương trình được viết bằng C++ gồm các phần chính sau: Phần khai báo các tệp nguyên mẫu (tệp tiêu đề); Phần khai báo sử dụng không gian tên; Định nghĩa các kiểu dữ liệu; 23 Phần khai báo các kiểu dữ liệu, biến, hằng, hàm,… do người lập trình định nghĩa và được sử dụng chung trong toàn bộ chương trình; Hàm main và thân chương trình; Định nghĩa các hàm con (nếu có). Ví dụ 1.8: #include <iostream> int binh_phuong(int); int lap_phuong(int); using namespace std; void main() { cout<<“binh phuong cua 4 la: ”<<binh_phuong(4)<<endl; cout<<“lap phuong cua 4 la: ”<<lap_phuong(4)<<endl; } int binh_phuong( int x) { return x*x; } int lap_phuong( int x) { return x*x*x; } 1.9.2. Các tệp tin thư viện thông dụng Thư viện là các đoạn mã được viết sẵn Do nhà sản xuất: thư viện chuẩn; Do người lập trình: thư viện mở rộng. Thư viện gồm hai phần: Giao diện: chứa trong tệp tiêu đề (.h) cho biết các mục có trong thư viện và cách sử dụng chúng; Phần thực thi: chứa trong tệp khác (.cpp), gồm các định nghĩa của các mục có trong thư viện. Chẳng hạn: #include <iostream> #include <string> Một số tập tin thư viện thông dụng iostream: thư viện chứa các hàm nhập xuất dữ liệu như: cout, cin, … string: thư viện chứa các hàm thao tác trên chuỗi ký tự: strcpy(), strcat(), strcmp(), … cmath: thư viện chứa các hàm toán học: sqrt(), pow(), fabs(), abs(), … iomanip: thư viện chứa các hàm định dạng dữ liệu xuất: setprecision(n), setw(n), setfill(ch), setiosflags(), … 1.9.3. Không gian tên Không gian tên (namespace) là một đặc trưng của ANSI C++, cho phép chúng ta gộp một nhóm các lớp, các đối tượng toàn cục và các hàm dưới một cái tên. Không gian tên là một cơ chế dùng để hạn chế phạm vi sử dụng của một tên Cú pháp: namespace Ten_khong_gian_ten { Thân của namespace } Ví dụ 1.9: 24 Định nghĩa không gian tên General như sau: namespace General { int a, b; } Lúc này, để truy xuất vào các biến a, b ta sử dụng toán tử :: như sau: General::a General::b C++ có nhiều thư viện có sẵn, mỗi thư viện được phân cấp trong một không gian tên riêng. Theo chuẩn ANSI C++, tất cả định nghĩa của các lớp, đối tượng và hàm của thư viện chuẩn đều được định nghĩa trong namespace std. Để sử dụng một thư viện hay hàm có sẵn thì phải khai báo không gian tên chứa thư viện hay hàm đó. Có hai cách sử dụng không gian tên: Truy xuất bằng toán tử phạm vi :: Dùng chỉ thị using. Truy xuất bằng toán tử phạm vi. Cú pháp: Ten_khong_gian_ten::Dinh_danh; Ví dụ 1.10: namespace first { int a = 5; } namespace second { double a = 2.25; } void main() { std::cout<<first::a<<std::endl; std::cout<<second::a<<std::endl; } Dùng chỉ thị using Cú pháp: using namespace Ten_khong_gian_ten; Ví dụ 1.11: using namespace std; namespace first { int a = 5; } namespace second { double a = 2.25; } void main() { using namespace first; cout<<first::a<<endl; cout<<a + 2<<endl; } Ví dụ 1.12: Xét 2 đoạn chương trình sau: Dùng toán tử phạm vi Dùng chỉ thị using 25 #include <iostream> int main() { std::cout<<“Hello”<< std::endl; return 0; } #include <iostream> using namespace std; int main() { cout<< “Hello” << endl; return 0; } 1.10. THỰC HIỆN CHƯƠNG TRÌNH C++ BẰNG DEV-C++ 1.10.1. Môi trường phát triển tích hợp IDE Dev-C++ là một môi trường phát triển tích hợp tự do (IDE) được phân phối dưới hình thức giấy phép Công cộng GNU hỗ trợ việc lập trình bằng C/C++. Nó cũng nằm trong bộ trình dịch mã nguồn mở MinGW. Chương trình IDE này được viết bằng ngôn ngữ Delphi. Dùng để soạn thảo và biên dịch chương trình viết bằng NNLT C/ C++. Bloodshed Dev-C++ là một Môi trường Phát triển Tích hợp (IDE) có hỗ trợ đầy đủ tính năng cho ngôn ngữ lập trình C/C++. Nó sử dụng trình MinGW của GCC (Bộ trình dịch GNU) làm trình biên dịch. Dev-C++ cũng có thể được dùng kết hợp với Cygwin hay bất kỳ trình dịch nền tảng GCC nào khác. 1.10.2. Các bước thực hiện 1.10.2.1. Tạo tệp mới và soạn thảo mã nguồn Vào File > New > Source File (Hoặc Project nếu chương trình gồm nhiều file > Chọn “Console Application” > Chọn C project hoặc C++ Project) hoặc nhấn Ctrl+N Soạn thảo mã nguồn Lưu: File/Save/ đặt tên (chọn đường dẫn nếu có) > OK Hình 1.16: Giao diện Dev-C++ 26 1.10.2.2. Dịch chương trình Kích Menu Execute > Compile (Ctrl+ F9). Hình 1.17: Dịch chương trình Nếu xuất hiện lỗi cú pháp, hãy quan sát dòng lệnh được đánh dấu màu nâu trong mã nguồn và các dòng gợi ý nội dung lỗi. Ta xác định lỗi và tự sửa lại, sau đó Cltr+F9 để biên dịch lại đến khi chương trình hết lỗi. Hình 1.18: Quá trình biên dịch Một số lỗi hay gặp: thiếu dấu chấm phẩy ;, ngoặc tròng (,), ngoặc móc {,}, các phép quan hệ, các lệnh if, for, while không đúng cú pháp … 27 1.10.2.3. Chạy chương trình Kích menu Execute > Run (Ctrl+ F10) Hình 1.19: Kết quả chạy chương trình Nếu không dừng màn hình để xem kết quả ta thêm vào lệnh system(“pause”) trước lệnh return 0; trong hàm main(); hoặc kích menu tools/Enviroment options/ General/ chọn Pause console programs after return. 1.10.2.4. Debug chương trình Để theo dõi quá trình thực hiện của máy trên từng dòng lệnh và các giá trị thay đổi của biến ta sử dụng chức năng debug của phần mềm DevC++ theo hướng dẫn sau: Bước 1: Giả sử ta có chương trình sau: Hình 1.20: Sửa lỗi Tạo Breakpoint để chỉ nơi bắt đầu theo dõi chương trình bằng cách kích chuột vào số thứ tự dòng, xuất hiện nền của dòng đó có màu đỏ, sau đó kích menu Execute/ Debug (hoặc nhấn phím F5) 28 Để theo dõi máy thực hiện từng dòng lệnh ta nhấn phím F7 hoặc kích nút Next line (dòng có nền xanh là dòng máy đang thực hiện đến đó) Để xem giá trị của biến nào, ta kích nút Add watch, gõ tên biến vào hộp thoại mới xuất hiện/ OK … như Hình 1.21. Hình 1.21: Debug Giải thích hình trên: Đầu tiên tạo Breakpoint tại dòng 11, nhấn F5. Nền xanh tại dòng này x=8, y=3, z là giá trị chưa xác định cụ thể. Nhấn phím F7, dòng xanh nhảy xuống dòng 12. Lệnh z = ++x + y–; được thực hiện: Lệnh ++x thực hiện nên x=9 đến lệnh z = x+y và z=12 sau đó đến y –, khi đó y=2 như kết quả hiện thời trên khung debug. Bằng cách nhấn F7 tiếp cho đến khi kết thúc hoặc kích nút Stop Execute để ngừng debug chương trình. Để soạn theo nhanh một tệp C++, ta có thể mở cửa sổ Codeblocks và kích menu File/ New/ Empty file, một file trắng xuất hiện và ta bắt đầu soạn thảo mã nguồn tại đây, lưu, dịch và thực hiện chương trình một cách bình thường và nhanh. Chỉ có tồn tại là làm theo cách này không Debug được mã nguồn. Nếu muốn debug thì phải thêm tệp này vào một project nào đó và debug như cách hướng dẫn ban đầu. CÂU HỎI, BÀI TẬP 1. Nêu đặc điểm của lập trình hướng đối tượng? 2. Trong số các phát biểu sau, phát biểu nào đúng, phát biểu nào sai: - Đối tượng là một thực thể cụ thể, tồn tại thực tế trong các ứng dụng. - Đối tượng là một thể hiện cụ thể của lớp. 29 - Lớp là một khái niệm trừu tượng dùng để biểu diễn các đối tượng. - Lớp là một sự trừu tượng hoá của đối tượng. - Lớp và đối tượng có bản chất giống nhau. - Trừu tượng hoá đối tượng theo chức năng tạo ra các thuộc tính của lớp. - Trừu tượng hoá đối tượng theo chức năng tạo ra các phương thức của lớp. - Trừu tượng hoá đối tượng theo dữ liệu tạo ra các thuộc tính của lớp. - Trừu tượng hoá đối tượng theo dữ liệu tạo ra các phương thức của lớp. - Kế thừa cho phép hạn chế sự trùng lặp mã nguồn. - Kế thừa cho phép tăng khả năng sử dụng lại mã nguồn. 3. Thực hiện công việc sau: a. Liệt kê tất cả các thuộc tính và hành động của đối tượng xe ô tô, xe bus. Đề xuất lớp car (ô tô), lớp xe bus. b. Từ hai lớp Car và Bus của bài 2 và bài 3. Đề xuất một lớp động cơ (Engine) cho hai lớp trên kế thừa, để tránh trùng lặp dữ liệu giữa hai lớp car và bus. 4. Cài đặt bộ công cụ Dev-C++ lên máy tính của bạn. Tự tìm kiếm và tải file cài đặt 5. Soạn thảo chương trình sau: #include < iostream> using namespace std; int main() { cout << "Xin chao cac ban!" << endl; } Lưu chương trình thành file “xinchao.cpp” Bấm F11 để dịch và chạy thử, sửa lỗi nếu có. 30 Chương 2 CÁC MỞ RỘNG CỦA C++ Vào những năm đầu thập niên 1980, người dùng biết C++ với tên gọi "C with Classes". Về thực chất C++ giống như C nhưng bổ sung thêm một số mở rộng quan trọng, đặc biệt là ý tưởng về đối tượng, lập trình định hướng đối tượng. Chương 2 trình bày và thảo luận các vấn đề sau: những điểm khác biệt chủ yếu giữa C và C++, các điểm mới của C++ so với C (những vấn đề cơ bản nhất), truyền tham số, đa năng hóa, … 2.1. TỔNG QUAN 2.1.1. Lịch sử C được phát minh vào trước năm 1970 bởi Dennis Ritchie. Ngôn ngữ cài đặt hệ thống cho hệ điều hành Unix. C++ được phát minh bởi Bijarne Stroustroup, bắt đầu năm 1979, dựa trên ngôn ngữ C. Các chuẩn ngôn ngữ C++ hiện tại được điều khiển bởi ANSI và ISO. C Đa dụng Hiệu quả Gần với ngôn ngữ máy Khả chuyển Các mở rộng Tham chiếu Overloading Exception handling Template … C++ OOP Lớp và đối tượng Kế thừa Đa hình … Hình 2.1: Các đặc điểm của C++ 2.1.2. Cấu trúc chương trình C++ cơ bản #include <iostream.h> int main (void) { cout << "Hello World\n"; } 31 C++ Program Hello.cpp C++ TRANSLATOR C++ Program C Code C COMPILER C++ Object NATIVE COMPILER Code Hello.obj LINKER Executable Hello.exe Hình 2.2: Quá trình biên dịch một chương trình C++ 2.2. NHỮNG KHÁC BIỆT VÀ BỔ SUNG SO VỚI C 2.2.1. Nhưng từ khóa mới Bổ sung một số từ khóa mới như trong Bảng 2.1. Bảng 2.1: Từ khóa mới trong C++ asm catch class delete friend inline new operator private protected public template this throw try virtual Do đó phải thay đổi định danh trong chương trình C khi chuyển qua C++ nếu trùng với keyword. 2.2.2. Chuyển kiểu dữ liệu Tự động chuyển kiểu từ kiểu có giá trị thấp thành kiểu có giá trị cao hơn cho phù hợp với phép toán: char short int long float double Tuy nhiên nếu chuyển ngược lại có thể gây mất dữ liệu Trình biên dịch sẽ sinh warning đối với các trường hợp này. Chuyển kiểu C: float x = (float) 3/4; C++: float x = float(3)/4; Để hạn chế các lỗi, C++ cung cấp một cách mới sử dụng 4 loại chuyển kiểu tường minh: Bảng 2.2: Chuyển kiểu static_cast reinterpret_cast const_cast dynamic_cast float x = static_cast<float>(3)/4; 2.2.3. Hằng số C định nghĩa hằng bằng tiền xử lý #define, nên có một số hạn chế sau: Biên dịch chậm hơn (do trình tiền xử lý tìm và thay thế); 32 Không gắn được kiểu dữ liệu với giá trị hằng; Trình debug không biết đến tên hằng. const của ANSI C ít dùng hơn và có ý nghĩa khác: Không dùng để khai báo mảng //Error in C const int SIZE=100; int a[SIZE]; //OK in C++ const int SIZE=100; int a[SIZE]; 2.2.4. Kiểu có cấu trúc: struct, union, enum Tên struct, union và enum được xem là tên kiểu dữ liệu. //C struct Complex { float r, i; }; struct Complex c; //C++ struct Complex { float r, i; }; Complex c; struct và union có thể được xem như lớp. 2.3. CÁC ĐIỂM KHÔNG TƯƠNG THÍCH GIỮA C++ VÀ ANSI C 2.3.1. Định nghĩa hàm Trong định nghĩa hàm, ANSI C cho phép hai kiểu khai báo dòng tiêu đề của hàm, trong khi đó C++ chỉ chấp nhận một cách: /* C++ không khai báo kiểu này */ double ham(a,b) int a; double b ; /* Cả C và C++ cho phép*/ double ham(int a, double b) int a; double b; 33 2.3.2. Khai báo hàm nguyên mẫu Trong ANSI C, khi sử dụng một hàm chưa được định nghĩa trước đó trong cùng một tệp, ta có thể: Không cần khai báo (khi đó ngầm định giá trị trả về của hàm là int) Chỉ cần khai báo tên hàm và giá trị trả về, không cần danh sách kiểu của các tham số. Khai báo hàm nguyên mẫu. Với C++, chỉ có phương pháp thứ 3 là chấp nhận được. Nói cách khác, một lời gọi hàm chỉ được chấp nhận khi trình biên dịch biết được kiểu của các tham số, kiểu của giá trị trả về. Mỗi khi trình biên dịch gặp một lời gọi hàm, nó sẽ so sánh các kiểu của các đối số được truyền với các tham số hình thức tương ứng. Trong trường hợp có sự khác nhau, có thể thực hiện một số chuyển kiểu tự động để cho hàm nhận được có danh sách các tham số đúng với kiểu đã được khai báo của hàm. Tuy nhiên phải tuân theo nguyên tắc chuyển kiểu tự động sau đây: Char → int → longint → float → double Ví dụ 2.1: double fexple (int, double) /*khai báo hàm fexple*/ .... main(){ int n; char c; double z,res1,res2,res3; .... res1 = fexple(n,z); /* không có chuyển đổi kiểu*/ res2 = fexple(c,z); /* có chuyển đổi kiểu, từ char (c) thành int*/ res3 = fexple(z,n); /* có chuyển đổi kiểu, từ double(z) thành int và từ int(n) thành double*/ .... } Trong C++ bắt buộc phải có từ khoá void trước tên của hàm trong phần khai báo để chỉ rằng hàm không trả về giá trị. Trường hợp không có, trình biên dịch ngầm hiểu kiểu của giá trị trả về là int và như thế trong thân hàm bắt buộc phải có câu lệnh return. Điều này hoàn toàn không cần thiết đối với mô tả trong ngôn ngữ C. Thực ra, các khả năng vừa mô tả không hoàn toàn là điểm không tương thích giữa C và C++ mà đó chỉ là sự “gạn lọc” các điểm yếu và hoàn thiện các mặt còn chưa hoàn chỉnh của C. 2.3.3. Sự tương thích giữa con trỏ void và các con trỏ khác Trong ANSI C, kiểu void * tương thích với các kiểu trỏ khác cả hai chiều. Chẳng hạn với các khai báo sau: void *g; int *i; hai phép gán sau đây là hợp lệ trong ANSI C: g = i; i = g; Thực ra, hai câu lệnh trên đã kèm theo các phép “chuyển kiểu ngầm định”: int* --->void* đối với câu lệnh thứ nhất, và void* --->int* đối với câu lệnh thứ hai. Trong C++, chỉ có chuyển đổi kiểu ngầm định từ một kiểu trỏ tuỳ ý thành void* là chấp nhận được, còn muốn chuyển đổi ngược lại, ta phải thực hiện chuyển kiểu tường minh như cách viết sau đây: 34 g = i; i = (int *)g; 2.4. CÁC KHẢ NĂNG VÀO/RA MỚI CỦA C++ Các tiện ích vào/ra (hàm hoặc macro) của thư viện C chuẩn đều có thể sử dụng trong C++. Để sử dụng các hàm này chúng ta chỉ cần khai báo tệp tiêu đề trong đó có chứa khai báo hàm nguyên mẫu của các tiện ích này. Bên cạnh đó, C++ còn cài đặt thêm các khả năng vào/ra mới dựa trên hai toán tử “<<” (xuất) và “>>” (nhập) với các đặc tính sau đây: đơn giản trong sử dụng; có khả năng mở rộng đối với các kiểu mới theo nhu cầu của người lập trình. Trong tệp tiêu đề iostream.h người ta định nghĩa hai đối tượng cout và cin tương ứng với hai thiết bị chuẩn ra/vào được sử dụng cùng với “<<” và “>>”. Thông thường ta hiểu cout là màn hình còn cin là bàn phím. 2.4.1. Ghi dữ liệu lên thiết bị ra chuẩn (màn hình) cout Trong phần này ta xem xét một số ví dụ minh hoạ cách sử dụng cout và “<<” để đưa thông tin ra màn hình. Ví dụ 2.2 Chương trình sau minh hoạ cách sử dụng cout để đưa ra màn hình một xâu ký tự. #include <iostream.h> /*phải khai báo khi muốn sử dụng cout*/ main() { cout << "Welcome C++"; } Welcome C++ “<<” là một toán tử hai ngôi, toán hạng ở bên trái mô tả nơi kết xuất thông tin (có thể là một thiết bị ngoại vi chuẩn hay là một tập tin ), toán hạng bên phải của “<<” là một biểu thức nào đó. Trong chương trình trên, câu lệnh cout <<"Welcome C++" đưa ra màn hình xâu ký tự “Welcome C++”. Ví dụ 2.3: Sử dụng cout và “<<” đưa ra các giá trị khác nhau: #include <iostream.h> /*phải khai báo khi muốn sử dụng cout*/ void main() { int n = 25; cout << "Value : "; cout << n; } Value : 25 Trong ví dụ 2.3 chúng ta đã sử dụng toán tử “<<” để in ra màn hình đầu tiên là một xâu ký tự, sau đó là một số nguyên. Chức năng của toán tử “<<” rõ ràng là khác nhau trong hai lần kết xuất dữ liệu: với câu lệnh thứ nhất, chỉ đưa ra màn hình một dãy các ký tự, ở câu lệnh sau, đã sử dụng một khuôn mẫu để chuyển đổi một giá trị nhị phân thành một chuỗi các ký tự chữ số. Việc một toán tử có nhiều vai trò khác nhau liên quan đến một khái niệm mới trong C++, đó là “Định nghĩa chồng toán tử”. Điều này sẽ được đề cập đến trong các chương sau. Ví dụ 2.4: Trong ví dụ 2.4 ta gộp cả hai câu lệnh kết xuất trong ví dụ 2.3 thành một câu lệnh phức tạp hơn, tuy kết quả không khác trước: 35 #include <iostream.h> /*phải khai báo khi muốn sử dụng cout*/ void main() { int n = 25; cout << "Value: " << n; } Value: 25 2.4.2. Các khả năng viết ra trên cout Chúng ta vừa xem xét một vài ví dụ viết một xâu ký tự, một số nguyên. Một cách tổng quát, chúng ta có thể sử dụng toán tử “<<” cùng với cout để đưa ra màn hình giá trị của một biểu thức có các kiểu sau: kiểu dữ liệu cơ sở (char, int, float, double), xâu ký tự: (char *), con trỏ (trừ con trỏ char *). Trong trường hợp muốn đưa ra địa chỉ biến xâu ký tự phải thực hiện phép chuyển kiểu tường minh, chẳng hạn (char *) → (void *). Xét ví dụ sau đây: Ví dụ 2.5: #include <iostream.h> void void main(){ int n = 25; long p = 250000; unsigned q = 63000; char c = 'a'; float x = 12.3456789; double y = 12.3456789e16; char * ch = "Welcome C++"; int *ad = &n; cout <<"Value of n : " << n <<"\n"; cout <<"Value of p : " << p <<"\n"; cout <<"Value of c : " << c <<"\n"; cout <<"Value of q : " << q <<"\n"; cout <<"Value of x : " << x <<"\n"; cout <<"Value of y : " << y <<"\n"; cout <<"Value of ch : " << ch <<"\n"; cout <<"Addrese of n : " << ad <<"\n"; cout <<"Addrese de ch : " << (void *)ch <<"\n"; } Value of n : 25 Value of p : 250000 Value of c : a Value of q : 63000 Value of x : 12.345679 Value of y : 1.234567e+17 Value of ch : Welcome C++ Addrese of n : 0xfff2 Addrese de ch : 0x00b2 36 2.4.3. Đọc dữ liệu từ thiết bị vào chuẩn (bàn phím) cin Nếu như cout dùng để chỉ thiết bị ra chuẩn, thì cin được dùng để chỉ một thiết bị vào chuẩn. Một cách tương tự, toán tử “>>” được dùng kèm với cin để nhập vào các giá trị; hai câu lệnh. int n; cin >> n; yêu cầu đọc các ký tự trên bàn phím và chuyển chúng thành một số nguyên và gán cho biến n. Giống như cout và “<<”, có thể nhập nhiều giá trị cùng kiểu hay khác kiểu bằng cách viết liên tiếp tên các biến cần nhập giá trị cùng với “>>” ngay sau cin. Chẳng hạn: int n; float p; char c; cin >> c >> n >> p; Có thể sử dụng toán tử “>>” để nhập dữ liệu cho các biến có kiểu char, int, float, double và char *. Giống với hàm scanf(), cin tuân theo một số quy ước dùng trong việc phân tích các ký tự: Các giá trị số được phân cách bởi: SPACE, TAB, CR, LF. Khi gặp một ký tự “không hợp lệ” (dấu “.” đối với số nguyên, chữ cái đối với số, ...) sẽ kết thúc việc đọc từ cin; ký tự không hợp lệ này sẽ được xem xét trong lần đọc sau. Đối với giá trị xâu ký tự, dấu phân cách cũng là SPACE, TAB, CR, còn đối với giá trị ký tự, dấu phân cách là ký tự CR. Trong hai trường hợp này không có khái niệm “ký tự không hợp lệ”. Mã sinh ra do bấm phím Enter của lần nhập trước vẫn được xét trong lần nhập xâu/ký tự tiếp theo và do vậy sẽ có “nguy cơ” không nhập được đúng giá trị mong muốn khi đưa ra lệnh nhập xâu ký tự hoặc ký tự ngay sau các lệnh nhập các giá trị khác. Giải pháp khắc phục vấn đề này để đảm bảo công việc diễn ra đúng theo ý là trước mỗi lần gọi lệnh nhập dữ liệu cho xâu/ký tự ta sử dụng một trong hai chỉ thị sau đây: fflush(stdin); //khai báo trong stdio.h cin.clear(); //hàm thành phần của lớp định nghĩa đối tượng cin Xét chương trình sau: Ví dụ 2.6: #include <iostream.h> #include <conio.h> void main() { int n; float x; char t[81]; clrscr(); do { cout << "Nhap vao mot so nguyen, mot xau, mot so thuc cin >> n >> t >> x; cout << "Da nhap " << n << "," << t << " va " << x << } while (n); } Nhap vao mot so nguyen, mot xau, mot so thuc : 3 long Da nhap 3,long va 3.4 Nhap vao mot so nguyen, mot xau, mot so thuc : 5 hung Da nhap 5,hung and 5.6 : "; "\n"; 3.4 5.6 37 Nhap vao mot so nguyen, mot xau, mot so thuc : 3 Da nhap 0,4 va 3 0 4 2.5. NHỮNG TIỆN ÍCH CHO NGƯỜI LẬP TRÌNH 2.5.1. Chú thích cuối dòng Mọi ký hiệu đi sau “//” cho đến hết dòng được coi là chú thích, được chương trình dịch bỏ qua khi biên dịch chương trình. Xét câu lệnh sau: cout << "Xin chao\n"; //lời chào hỏi Thường ta sử dụng chú thích cuối dòng khi muốn giải thích ý nghĩa của một câu lệnh gì đó. Đối với một đoạn chương trình kiểu chú thích giới hạn bởi “/*” và “*/” cho phép mô tả được nhiều thông tin hơn. Ví dụ 2.7: /* Chương trình in các sô t0 0 dên 9. */ #include <iostream.h> void main() { int I; for(I = 0; I < 10 ; ++ I)// 0 - 9 cout<<I<<"\n"; // In ra 0 - 9 } Nhận xét: Mọi thứ nằm giữa /*…*/ từ dòng 1 đến dòng 3 đều được chương trình bỏ qua. Chương trình này còn minh họa cách chú thích thứ hai. Đó là cách chú thích bắt đầu bằng // ở dòng 8 và dòng 9. 2.5.2. Khai báo mọi nơi Trong C++ không nhất thiết phải nhóm lên đầu các khai báo đặt bên trong một hàm hay một khối lệnh, mà có thể đặt xen kẽ với các lệnh xử lý. Ví dụ 2.8: { int n; n=23; cout <<n<<"\n"; ... int *p=&n; cout <<p<<"\n"; ... } Giá trị khởi đầu cho các biến có thể thay đổi tại các thời điểm chạy chương trình khác nhau. Một ví dụ minh hoạ cho khả năng định nghĩa khắp mọi nơi là có thể khai báo các biến điều khiển ngay bên trong các vòng lặp nếu biến đó chưa được khai báo trước đó trong cùng khối lệnh cũng như không được khai báo lại ở phần sau. Xem đoạn chương trình sau: { 38 ... //chưa có i for(int i=0;...;...) ... //không được khai báo i } 2.5.3. Toán tử phạm vi “::” Bình thường, biến cục bộ che lấp biến toàn cục cùng tên. Chẳng hạn: #include <iostream.h> int x; main() { int x = 10; //x cục bộ cout<<x<<“\n”;//x cục bộ } Trong những trường hợp cần thiết, khi muốn truy xuất tới biến toàn cục phải sử dụng toán tử “::” trước tên biến: #include <iostream.h> int x; main() { int x = 10; //x cục bộ ::x = 10; //x toàn cục cout<<x<<“\n”;//x cục bộ cout<<::x<<“\n”;//x toàn cục } 2.5.4. Hàm inline Trong C++ có thể định nghĩa các hàm được thay thế trực tiếp thành mã lệnh máy tại chỗ gọi (inline) mỗi lần được tham chiếu. Điểm này rất giống với cách hoạt động của các macro có tham số trong C. Ưu điểm của các hàm inline là chúng không đòi hỏi các thủ tục bổ sung khi gọi hàm và trả giá trị về. Do vậy hàm inline được thực hiện nhanh hơn so với các hàm thông thường. Một hàm inline được định nghĩa và được sử dụng giống như bình thường. Điểm khác nhau duy nhất là phải đặt mô tả inline trước khai báo hàm. Xét ví dụ sau đây: Ví dụ 2.9: #include <iostream.h> #include <math.h> #include <conio.h> inline double norme(double vec[3]); // khai báo hàm inline void main(){ clrscr(); double v1[3],v2[3]; int i; for(i=0;i<3;i++) { v1[i]=i;v2[i]=2*i-1;} cout <<"norme cua v1 : "<<norme(v1) <<" - norme cua "<<norme(v2); getch(); } v2 : 39 /*Định nghĩa hàm inline*/ inline double norme(double vec[3]) { int i;double s=0; for(i=0;i<3;i++) s+=vec[i]*vec[i]; return(sqrt(s)); } norme cua v1 : 2.236068 - norme cua v2 : 3.316625 Hàm norme() nhằm mục đích tính chuẩn của vector với ba thành phần. Từ khoá inline yêu cầu chương trình biên dịch xử lý hàm norme khác với các hàm thông thường. Cụ thể là, mỗi lần gọi norme(), trình biên dịch ghép trực tiếp các chỉ thị tương ứng của hàm vào trong chương trình (ở dạng ngôn ngữ máy). Do đó cơ chế quản lý lời gọi và trở về không cần nữa (không cần lưu ngữ cảnh, sao chép các thông số...), nhờ vậy tiết kiệm thời gian thực hiện. Bên cạnh đó các chỉ thị tương ứng sẽ được sinh ra mỗi khi gọi hàm do đó chi phí lưu trữ tăng lên khi hàm được gọi nhiều lần. Điểm bất lợi khi sử dụng các hàm inline là nếu chúng quá lớn và được gọi thường xuyên thì kích thước chương trình sẽ tăng lên rất nhanh. Vì lý do này, chỉ những hàm đơn giản, không chứa các cấu trúc lặp mới được khai báo là hàm inline. Việc sử dụng hàm inline so với các macro có tham số có hai điểm lợi. Trước hết hàm inline cung cấp một cách có cấu trúc hơn khi mở rộng các hàm đơn giản thành hàm inline. Thực tế cho thấy khi tạo một macro có tham số thường hay quên các dấu đóng ngoặc, rất cần đến để đảm bảo sự mở rộng nội tuyến riêng trong mỗi trường hợp. Với macro có thể gây ra hiệu ứng phụ hoặc bị hạn chế khả năng sử dụng. Chẳng hạn với macro: #define square(x) {x++*x++} Với lời gọi square(a) với a là biến sẽ sản sinh ra biểu thức a++*a++ và kết quả là làm thay đổi giá trị của biến a tới hai lần. Còn lời gọi square (3) sẽ gây lỗi biên dịch vì ta không thể thực hiện các phép toán tăng giảm trên các toán hạng là hằng số. Việc sử dụng hàm inline như một giải pháp thay thế sẽ tránh được các tình huống như thế. Ngoài ra, các hàm inline có thể được tối ưu bởi chương trình biên dịch. Điều quan trọng là đặc tả inline chỉ là một yêu cầu, chứ không phải là một chỉ thị đối với trình biên dịch. Nếu vì một lý do nào đó trình biên dịch không thể đáp ứng được yêu cầu (chẳng hạn khi bên trong định nghĩa hàm inline có các cấu trúc lặp) thì hàm sẽ được biên dịch như một hàm bình thường và yêu cầu inline sẽ bị bỏ qua. Hàm inline phải được khai báo bên trong tệp tin nguồn chứa các hàm sử dụng nó. Không thể dịch tách biệt các hàm inline. 2.6. THAM CHIẾU Ngôn ngữ C++ giới thiệu một khái niệm mới “reference” tạm dịch là “tham chiếu”. Về bản chất, tham chiếu là “bí danh” của một vùng nhớ được cấp phát cho một biến nào đó. 40 Một tham chiếu có thể là một biến, tham số hình thức của hàm hay dùng làm giá trị trả về của một hàm. Các phần tiếp sau lần lượt giới thiệu các khả năng của tham chiếu được sử dụng trong chương trình viết bằng ngôn ngữ C++. 2.6.1. Tham chiếu tới một biến Xét hai lệnh sau: int n; int &p = n; Trong chỉ thị thứ hai, dấu “&” để xác định p là một biến tham chiếu còn dấu “=” và tên biến n để xác định vùng nhớ mà p tham chiếu tới. Lúc này cả hai định danh p và n cùng xác định vùng nhớ được cấp phát cho biến n. Như vậy các lệnh sau: n =3; cout <<p; cho kết quả 3 trên thiết bị hiển thị. Xét về bản chất tham chiếu và tham trỏ giống nhau vì cùng chỉ đến các đối tượng có địa chỉ, cùng được cấp phát địa chỉ khi khai báo. Nhưng cách sử dụng chúng thì khác nhau. Khi nói đến tham chiếu “&p” ta phải gắn nó với một biến nào đó đã khai báo qua “&p=n”, trong khi đó khai báo con trỏ “*p” không nhất thiết phải khởi tạo giá trị cho nó. Trong chương trình biến trỏ có thể tham chiếu đến nhiều biến khác nhau còn biến tham chiếu thì “ván đã đóng thuyền” từ khi khai báo, có nghĩa là sau khi khởi tạo cho tham chiếu gắn với một biến nào đó rồi thì ta không thể thay đổi để gắn tam chiếu với một biến khác. Xét các chỉ thị sau: Ví dụ 2.10: int n=3,m=4; int *p; p=&n; //p chỉ đến n *p=4; //gán giá trị của m cho n thông qua con trỏ p ... p=&m; //cho p chỉ đến m int &q=n; //khai báo tham chiếu q chỉ đến n q=4; //gán cho biến n giá trị 4 ... q=m; //gán giá trị của biến m cho biến n. Nói một cách đơn giản, tham chiếu của một biến giống như bí danh của một con người nào đó. Có nghĩa là để chỉ đến một con người cụ thể nào đó, ta có thể đồng thời sử dụng tên của anh ta hoặc bí danh. Do vậy, để truy nhập đến vùng nhớ tương ứng với một biến, chúng ta có thể sử dụng hoặc là tên biến hoặc là tên tham chiếu tương ứng. Đối với con người, bí danh bao giờ cũng nhằm nói đến một người đã tồn tại, và như vậy tham chiếu cũng phải được khai báo và khởi tạo sau khi biến được khai báo. Chương trình sau đây sẽ gây lỗi biên dịch do tham chiếu y chưa được khởi tạo. Ví dụ 2.11: Chương trình sai #include <iostream.h> void main() { int x=3, &y;//lỗi vì y phải được khởi tạo 41 cout <<" x = "<<x<<"\n" <<" y = "<<y<<"\n"; y = 7; cout <<" x = "<<x<<"\n" <<" y = "<<y<<"\n"; } Chương trình đúng như sau: #include <iostream.h> void main() { int x=3, &y=x;//y bây giờ là một “bí danh” của x cout <<" x = "<<x<<"\n" <<" y = "<<y<<"\n"; y = 7; cout <<" x = "<<x<<"\n" <<" y = "<<y<<"\n"; } x = 3 y = 3 x = 7 y = 7 Lưu ý cuối cùng là không thể gắn một tham chiếu với một hằng số trừ trường hợp có từ khoá const đứng trước khai báo tham chiếu. Dễ dàng kiểm tra các nhận xét sau: int &p =3; //không hợp lệ const int & p =3; //hợp lệ 2.6.2. Truyền tham số cho hàm bằng tham chiếu Trong C, các tham số và giá trị trả về của một hàm được truyền bằng giá trị. Để giả lập cơ chế truyền tham biến ta phải sử dụng con trỏ. Trong C++, việc dùng khái niệm tham chiếu trong khai báo tham số hình thức của hàm sẽ yêu cầu chương trình biên dịch truyền địa chỉ của biến cho hàm và hàm sẽ thao tác trực tiếp trên các biến đó. Chương trình sau đưa ra ba cách viết khác nhau của hàm thực hiện việc hoán đổi nội dung của hai biến. Ví dụ 2.12: #include <conio.h> #include <iostream.h> /*Hàm swap1 được gọi với các tham số được truyền theo tham trị*/ void swap1(int x, int y) { int temp = x; x = y; y = temp; } /*Hàm swap2 thực hiện việc truyền tham số bằng tham trỏ*/ void swap2(int *x, int *y) { int temp = *x; *x = *y; *y = temp; } 42 /*Hàm swap3 thực hiện việc truyền tham số bằng tham chiếu*/ void swap3(int &x, int &y) { int temp = x; x = y; y = temp; } void main() { int a=3, b=4; clrscr(); cout <<"Truoc khi goi swap1:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; swap1(a,b); cout <<"Sau khi goi swap:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; a=3; b =4; cout <<"Truoc khi goi swap2:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; swap2(&a,&b); cout <<"Sau khi goi swap2:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; a=3;b=4; cout <<"Truoc khi goi swap3:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; swap3(a,b); cout <<"Sau khi goi swap3:\n"; cout<<"a = "<<a<<" b = "<<b<<"\n"; getch(); } Truoc khi goi swap1: a = 3 b = 4 Sau khi goi swap1: a = 3 b = 4 Truoc khi goi swap2 a = 3 b = 4 Sau khi goi swap2: a = 4 b = 3 Truoc khi goi swap3: a = 3 b = 4 Sau khi goi swap3: a = 4 b = 3 Trong chương trình trên, ta truyền tham số a, b cho hàm swap1() theo tham trị cho nên giá trị của chúng không thay đổi trước và sau khi gọi hàm. Giải pháp đưa ra trong hàm swap2() là thay vì truyền trực tiếp giá trị hai biến a và b người ta truyền địa chỉ của chúng rồi thông qua các địa chỉ này để xác định giá trị biến. Bằng cách đó giá trị của hai biến a và b sẽ hoán đổi cho nhau sau lời gọi hàm. Khác với swap2(), hàm swap3() đưa ra giải pháp sử dụng tham chiếu. Các tham số hình thức của hàm swap3() bây giờ là các tham chiếu đến các tham số thực được truyền cho hàm. Nhờ vậy mà giá trị của hai tham số thực a và b có thể hoán đổi được cho nhau. 43 Có một vấn đề mà chắc rằng bạn đọc sẽ phân vân: “làm thế nào để khởi tạo các tham số hình thức là tham chiếu”. Xin thưa rằng điều đó được thực hiện tự động trong mỗi lời gọi hàm chứ không phải trong định nghĩa. Từ đó nảy sinh thêm một nhận xét quan trọng: “tham số ứng với tham số hình thức là tham chiếu phải là biến, trừ trường hợp có từ khoá const đứng trước khai báo tham số hình thức”. Chẳng hạn không thể thực hiện lời gọi hàm sau: swap3(2,3); Bạn đọc có thể xem thêm phần “Hàm thiết lập sao chép lại” để thấy được ích lợi to lớn của việc truyền tham số cho hàm bằng tham chiếu. Chú ý: khi muốn truyền bằng tham biến một biến trỏ thì viết như sau: int * & adr; //adr là một tham chiếu tới một biến trỏ chỉ đến một biến nguyên. 2.6.3. Giá trị trả về của hàm là tham chiếu Định nghĩa của hàm có dạng như sau: <type> & fct( ...) { ... return <bien co pham vi toan cuc>; } 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ì chỉ 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 và do vậy, tham chiếu của hàm cũng không còn có ý 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 “kỳ dị” 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 lẽ 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 (biểu thức 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. Xét ví dụ sau đây: Ví dụ 2.13: #include <iostream.h> #include <conio.h> int a[5]; int &fr(int *d,int i); void main() { clrscr(); cout<<"Nhap gia tri cho mang a:\n"; for(int i=0;i<5;i++) { cout<<"a["<<i<<"]= "; cin>>fr(a,i); } cout<<"Mang a sau khi nhap\n"; for(i=0;i<5;i++) cout<<a[i]<<" "; cout<<"\n"; getch(); } 44 int &fr(int *d,int i) { return d[i]; } Nhap gia tri cho mang a: a[0]= 6 a[1]= 4 a[2]= 3 a[3]= 5 a[4]= 6 Mang a sau khi nhap 6 4 3 5 6 2.7. ĐA NĂNG HÓA (Overloading) Cho phép đa năng hóa hàm và đa năng hóa toán tử Là phương pháp cung cấp nhiều hơn một định nghĩa cho tên hàm đã cho trong cùng một phạm vi Trình biên dịch sẽ lựa chọn phiên bản thích hợp của hàm hay toán tử dựa trên các tham số mà nó được gọi 2.7.1. Đa năng hóa toán tử Các toán tử +, - , * , /, … chỉ áp dụng đối với các kiểu cài sẵn (int, char, float, …) Muốn dùng các toán tử này trên các kiểu dữ liệu tự định nghĩa thì phải sử dụng đa năng hoá toán tử struct Complex { float r, i; }; Complex c1, c2; …………… Complex c = c1 + c2; Sử dụng cú pháp toán học nên đã đơn giản hóa chương trình. Số lượng tham số của hàm chính là số ngôi của toán tử. Các toán tử được đa năng hóa sẽ được lựa chọn bởi trình biên dịch cũng theo cách thức tương tự như việc chọn lựa giữa các hàm được đa năng hóa. 2.7.2. Đa năng hóa hàm C++ cho phép sử dụng một tên cho nhiều hàm khác nhau ta gọi đó là sự “chồng hàm”. Trong trường hợp đó, các hàm sẽ khác nhau ở giá trị trả về và danh sách kiểu các tham số. Chẳng hạn chúng ta muốn định nghĩa các hàm trả về số nhỏ nhất trong: hai số nguyên; hai số thực; hai ký tự; ba số nguyên; một dãy các số nguyên ... 45 Dĩ nhiên có thể tìm cho mỗi hàm như vậy một tên. Sử dụng khả năng “định nghĩa chồng hàm” của C++, chúng ta có thể viết các hàm như sau: Ví dụ 2.14: #include <iostream.h> //Hàm nguyên mẫu int min(int, int); //Hàm 1 double min(double, double);//Hàm 2 char min(char, char);//Hàm 3 int min(int, int, int);//Hàm 4 int min(int, int *);//Hàm 5 main() { int n=10, p =12, q = -12; double x = 2.3, y = -1.2; char c = ‘A’, d= ‘Q’; int td[7] = {1,3,4,-2,0,23,9}; cout<<“min (n,p) : ”<<min(n,p)<<“\m”; //Hàm 1 cout<<“min (n,p,q) : ”<<min(n,p,q)<<“\m”; //Hàm 4 cout<<“min (c,d) : ”<<min(c,d)<<“\m”; //Hàm 3 cout<<“min (x,y) : ”<<min(n,p)<<“\m”; //Hàm 2 cout<<“min (td) : ”<<min(7,td)<<“\m”; //Hàm 5 cout<<“min (n,x) : ”<<min(n,x)<<“\m”; //Hàm 2 //cout<<“min (n,p,x) : ”<<min(n,p,x)<<“\m”; //Lỗi } int min(int a, int b) { return (a> b? a: b); } int min(int a, int b, int c) { return (min(min(a,b),c)); } double min(double a, double b) { return (a> b? a: b); } char min(char a, char b) { return (a> b? a: b); } int min(int n, int *t) { int res = t[0]; for (int i=1; i<n; i++) res = min(res,t[i]); return res; } Nhận xét: Một hàm có thể gọi đến hàm cùng tên với nó. Trong trường hợp có các hàm trùng tên trong chương trình, việc xác định hàm nào được gọi do chương trình dịch đảm nhiệm và tuân theo các nguyên tắc sau: Trường hợp các hàm có một tham số 46 Chương trình dịch tìm kiếm “sự tương ứng nhiều nhất” có thể được; có các mức độ tương ứng như sau (theo độ ưu tiên giảm dần): Tương ứng thật sự: ta phân biệt các kiểu dữ liệu cơ sở khác nhau đồng thời lưu ý đến cả dấu. Tương ứng dữ liệu số nhưng có sự chuyển đổi kiểu dữ liệu tự động (“numeric promotion”): char và short →int; float →int. Các chuyển đổi kiểu chuẩn được C và C++ chấp nhận. Các chuyển đổi kiểu do người sử dụng định nghĩa. Quá trình tìm kiếm bắt đầu từ mức cao nhất và dừng lại ở mức đầu tiên cho phép tìm thấy sự phù hợp. Nếu có nhiều hàm phù hợp ở cùng một mức, chương trình dịch đưa ra thông báo lỗi do không biết chọn hàm nào giữa các hàm phù hợp. Trường hợp các hàm có nhiều tham số Ý tưởng chung là phải tìm một hàm phù hợp nhất so với tất cả những hàm còn lại. Để đạt mục đích này, chương trình dịch chọn cho mỗi tham số các hàm phù hợp (ở tất cả các mức độ). Trong số các hàm được lựa chọn, chương trình dịch chọn ra (nếu tồn tại và tồn tại duy nhất) hàm sao cho đối với mỗi đối số nó đạt được sự phù hợp hơn cả so với các hàm khác. Trong trường hợp vẫn có nhiều hàm thoả mãn, lỗi biên dịch xảy ra do chương trình dịch không biết chọn hàm nào trong số các hàm thỏa mãn. Đặc biệt lưu ý khi sử dụng định nghĩa chồng hàm cùng với việc khai báo các hàm với tham số có giá trị ngầm định sẽ được trình bày trong mục tiếp theo. 2.8. THAM SỐ NGẦM ĐỊNH TRONG LỜI GỌI HÀM Ta xét ví dụ sau: Ví dụ 2.15: #include <iostream.h> void main() { int n=10,p=20; void fct(int, int = 12) ;//khai báo hàm với một giá trị ngầm định fct(n,p); //lời gọi thông thường, có hai tham số fct(n); //lời gọi chỉ với một tham số //fct() sẽ không được chấp nhận } //khai báo bình thường void fct(int a, int b) { cout <<"tham so thu nhat : " <<a <<"\n"; cout<<"tham so thu hai : "<<b<<"\n"; } tham so thu nhat : 10 tham so thu hai : 20 tham so thu nhat : 10 tham so thu hai : 12 Trong khai báo của fct() bên trong hàm main() : void fct(int,int =12); khai báo int = 12 47 chỉ ra rằng trong trường hợp vắng mặt tham số thứ hai ở lời gọi hàm fct() thì tham số hình thức tương ứng sẽ được gán giá trị ngầm định 12. Lời gọi fct(); không được chấp nhận bởi vì không có giá trị ngầm định cho tham số thứ nhất. Ví dụ 2.16: #include <iostream.h> void main(){ int n=10,p=20; void fct(int = 0, int = 12);//khai báo hàm với hai tham số có giá trị ngầm định fct(n,p); //lời gọi thông thường, có hai tham số fct(n); //lời gọi chỉ với một tham số fct() ; //fct() đã được chấp nhận } void fct(int a, int b) //khai báo bình thường { cout<<"tham so thu nhat : " <<a <<"\n"; cout<<"tham so thu hai : "<<b<<"\n"; } tham so thu nhat : 10 tham so thu hai : 20 tham so thu nhat : 10 tham so thu hai : 12 tham so thu nhat : 0 tham so thu hai : 12 Chú ý: Các tham số với giá trị ngầm định phải được đặt ở cuối trong danh sách các tham số của hàm để tránh nhầm lẫn các giá trị. Các giá trị ngầm định của tham số được khai báo khi sử dụng chứ không phải trong phần định nghĩa hàm. Ví dụ sau đây gây ra lỗi biên dịch: Ví dụ 2.17: #include <conio.h> #include <iostream.h> void f(); void main() { clrscr(); int n=10,p=20; void fct(int =0,int =12); cout<<"Goi fct trong main\n"; fct(n,p); fct(n); fct(); getch(); } void fct(int a=10,int b=100) { cout<<"Tham so thu nhat : "<<a<<"\n"; 48 cout<<"Tham so thu hai : "<<b<<"\n"; } Nếu muốn khai báo giá trị ngầm định cho một tham số biến trỏ, thì phải chú ý viết * và = cách xa nhau ít nhất một dấu cách. Các giá trị ngầm định có thể là một biểu thức bất kỳ (không nhất thiết chỉ là biểu thức hằng), có giá trị được tính tại thời điểm khai báo: float x; int n; void fct(float = n*2+1.5); Chồng hàm và gọi hàm với tham số có giá trị ngầm định có thể sẽ dẫn đến lỗi biên dịch khi chương trình dịch không xác định được hàm phù hợp. Xét ví dụ sau: Ví dụ 2.18: #include <iostream.h> void fct(int, int=10); void fct(int); void main() { int n=10, p=20; fct(n,p);//OK fct(n);//ERROR } 2.9. CÁC TOÁN TỬ QUẢN LÝ BỘ NHỚ ĐỘNG: new và delete 2.9.1. Toán tử cấp phát bộ nhớ động new Với khai báo int * adr; chỉ thị ad = new int; cho phép cấp phát một vùng nhớ cần thiết cho một phần tử có kiểu int và gán cho adr địa chỉ tương ứng. Lệnh tương đương trong C: ad =(int *) malloc(sizeof(int)); Với khai báo char *adc; chỉ thị adc =new char [100]; cho phép cấp phát vùng nhớ đủ cho một bảng chứa 100 ký tự và đặt địa chỉ đầu của vùng nhớ cho biến adc. Lệnh tương ứng trong C như sau: adc =(char *)malloc(100); Hai cách sử dụng new như sau: Dạng 1: 49 new type; giá trị trả về là một con trỏ đến vị trí tương ứng khi cấp phát thành công, là NULL trong trường hợp trái lại. Dạng 2: new type[n]; trong đó n là một biểu thức nguyên không âm nào đó, khi đó toán tử new xin cấp phát vùng nhớ đủ để chứa n thành phần kiểu type và trả lại con trỏ đến đầu vùng nhớ đó nếu như cấp phát thành công. 2.9.2. Toán tử giải phóng vùng nhớ động delete Một vùng nhớ động được cấp phát bởi new phải được giải phóng bằng delete mà không thể dùng free được, chẳng hạn: delete adr; delete adc; Ví dụ 2.19: Cấp phát bộ nhớ động cho mảng hai chiều #include<iostream.h> void Nhap(int **mat);//nhập ma trận hai chiều void In(int **mat);//In ma trận hai chiều void main() { int **mat; int i; /*cấp phát mảng 10 con trỏ nguyên*/ mat = new int *[10]; for(i=0; i<10; i++) /*mỗi con trỏ nguyên xác định vùng nhớ 10 số nguyên*/ mat[i] = new int [10]; /*Nhập số liệu cho mảng vừa được cấp phát*/ cout<<"Nhap so lieu cho matran 10*10\n"; Nhap(mat); /*In ma trận*/ cout<<"Ma tran vua nhap \n"; In(mat); /*Giải phóng bộ nhớ*/ for(i=0;i<10;i++) delete mat[i]; delete mat; } void Nhap(int ** mat) { int i,j; for(i=0; i<10;i++) for(j=0; j<10;j++) { cout<<"Thanh phan thu ["<<i<<"]["<<j<<"]= "; cin>>mat[i][j]; } } void In(int ** mat) { 50 int i,j; for(i=0; i<10;i++) { for(j=0; j<10;j++) cout<<mat[i][j]<<" "; cout<<"\n"; } } Ví dụ 2.20 Quản lý tràn bộ nhớ set_new_handler #include <iostream> main() { void outof(); set_new_handler(&outof); long taille; int *adr; int nbloc; cout<<"Kich thuoc can nhap? "; cin >>taille; for(nbloc=1;;nbloc++) { adr =new int [taille]; cout <<"Cap phat bloc so : "<<nbloc<<"\n"; } } void outof()//hàm được gọi khi thiếu bộ nhớ { cout <<"Het bo nho -Ket thuc \n"; exit(1); } Ví dụ 2.21 Viết một hàm hoanvi dùng để hoán vị hai số nguyên. Sau đó viết chương trình nhập và sắp xếp một mảng số nguyên. #include <iostream.h> void hoanvi(int &a,int &b) { int tam=a; a=b; b=tam; } void main() { // Nhap du lieu int n; cout<<” Ban hay cho so phan tu cua mang n=”;cin>>n; //Cap phat bo nho cho mang int *a=new int(n); cout<<”\n Hay nhap gia tri cho cac phan tu cua mang \n”; for(int i=0;i<n;++i) { 51 cout<<”a[“<<i<<”]=”;cin>>a[i]; } // Sap xep for(i=0;i<n-1;i++) for(int j=i++;j<n;j++) if (a[i]>a[j]) hoanvi(a[i],a[j]); // In ket qua cout<<”\n Cac phan tu cua mang sau khi da sap xep la \n”; for(i=0;i<n;i++) cout<<a[i]<<” “; delete a; } Ví dụ 2.22 Viết chương trình tạo một mảng động, khởi tạo mảng này với các giá trị ngẫu nhiên và thực hiện việc sắp xếp chúng. #include <iostream.h> #include <time.h> #include <stdlib.h> void main() { int N; cout<<"Nhap vao so phan tu cua mang:"; cin>>N; int *P=new int[N]; if (P==NULL) { cout<<"Khong con bo nho de cap phat\n"; } srand((unsigned)time(NULL)); for(int I=0;I<N;++I) P[I]=rand()%100; //T_o các sô ngau nhiên t0 0 dên 99 cout<<"Mang truoc khi sap xep\n"; for(I=0;I<N;++I) cout<<P[I]<<" "; for(I=0;I<N-1;++I) for(int J=I+1;J<N;++J) if (P[I]>P[J]) { int Temp=P[I]; P[I]=P[J]; P[J]=Temp; } cout<<"\nMang sau khi sap xep\n"; for(I=0;I<N;++I) cout<<P[I]<<" "; delete []P; } Ví dụ 2.23 52 Viết chương trình cộng hai ma trận trong đó ma trận được cấp phát động. Có thể coi mảng 2 chiều là mảng 1 chiều như hình 2.3 sau đây: Hình 2.3: Mảng 2 chiều Gọi X là mảng 2 chiều có kích thước m dòng và n cột A là mảng 1 chiều tương ứng Nếu X[i][j] chính là A[k] thì k=i*n+j Chương trình như sau: #include <iostream.h> #include <conio.h> //prototype void AddMatrix(int * A,int *B,int*C,int M,int N); int AllocMatrix(int **A,int M,int N); void FreeMatrix(int *A); void InputMatrix(int *A,int M,int N,char Symbol); void DisplayMatrix(int *A,int M,int N); int main() { int M,N; int *A = NULL,*B = NULL,*C = NULL; clrscr(); cout<<"Nhap so dong cua ma tran:"; cin>>M; cout<<"Nhap so cot cua ma tran:"; cin>>N; //Câp phát vùng nh_ cho ma tran A if (!AllocMatrix(&A,M,N)) { //endl: Xuât ra kí t_ xuông dòng (‘\n’) cout<<"Khong con du bo nho!"<<endl; return 1; 53 } //Câp phát vùng nh_ cho ma tran B if (!AllocMatrix(&B,M,N)) { cout<<"Khong con du bo nho!"<<endl; FreeMatrix(A);//Gi_i phóng vùng nh_ A return 1; } //Câp phát vùng nh_ cho ma tran C if (!AllocMatrix(&C,M,N)) { cout<<"Khong con du bo nho!"<<endl; FreeMatrix(A);//Gi_i phóng vùng nh_ A FreeMatrix(B);//Gi_i phóng vùng nh_ B return 1; } cout<<"Nhap ma tran thu 1"<<endl; InputMatrix(A,M,N,'A'); cout<<"Nhap ma tran thu 2"<<endl; InputMatrix(B,M,N,'B'); clrscr(); cout<<"Ma tran thu 1"<<endl; DisplayMatrix(A,M,N); cout<<"Ma tran thu 2"<<endl; DisplayMatrix(B,M,N); AddMatrix(A,B,C,M,N); cout<<"Tong hai ma tran"<<endl; DisplayMatrix(C,M,N); FreeMatrix(A);//Gi_i phóng vùng nh_ A FreeMatrix(B);//Gi_i phóng vùng nh_ B FreeMatrix(C);//Gi_i phóng vùng nh_ C return 0; } //Cong hai ma tran void AddMatrix(int *A,int *B,int*C,int M,int N) { for(int I=0;I<M*N;++I) C[I] = A[I] + B[I]; } //Câp phát vùng nh_ cho ma tran int AllocMatrix(int **A,int M,int N) { *A = new int [M*N]; if (*A == NULL) return 0; return 1; } //Gi_i phóng vùng nh_ void FreeMatrix(int *A) { 54 if (A!=NULL) delete [] A; } //Nhap các giá tr_ c_a ma tran void InputMatrix(int *A,int M,int N,char Symbol) { for(int I=0;I<M;++I) for(int J=0;J<N;++J) { cout<<Symbol<<"["<<I<<"]["<<J<<"]="; cin>>A[I*N+J]; } } //Hien th_ ma tran void DisplayMatrix(int *A,int M,int N) { for(int I=0;I<M;++I) { for(int J=0;J<N;++J) { out.width(7);//Hien thi canh le phai voi chieu dai 7 ky tu cout<<A[I*N+J]; } cout<<endl; } } CÂU HỎI, BÀI TẬP 1. Tìm lỗi sai của đoạn chương trình sau: int n; cin>>n; for(int i=0;i<n;i++) { int a[100]; cin>>a[i]; } for(i=0;i<n;i++) cout<<a[i]; 2. Cho biết kết quả khi thực hiện chương trình sau: #include <iostream.h> int & foo(int &a,int b) { b+=a; if (b>5) a++; return a; } void main() { int i=2,j=4; 55 int k=foo(i,j); k++; cout<<i<<” “<<j<<” “<<k<<endl; int &l=foo(i,j); l++ ; cout<<i<<” “<<j<<” “<<l<<endl; } 3. Cho biết giá trị của k khi thực hiện đoạn chương trình sau: int i=5,k; { int i=6; ::i--; k=i; } k-=i; 4. Cho biết giá trị của y sau khi thực hiện: int &foo(int &a) { a++; return a; } int i=5; int &r=foo(i); r++; 5. Tìm giá trị của x và y void test(int &a, int b) {a+=b; b=a; } int x=1,y=2; test(x,y); 6. Viết chương trình nhân hai ma trận A m x n và B n x p. Các ma trận được cấp phát động và các giá trị của chúng được phát sinh ngẫu nhiên (các giá trị m, n và p được nhập từ bàn phím). 56 Chương 3 LỚP VÀ ĐỐI TƯỢNG 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 trong 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ác định nghĩa lớp, tạo lập đối tượng 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. GIỚI THIỆU Đối tượng là "một cái gì đó" có ý nghĩa cho vấn đề chúng ta đang quan tâm. Phục vụ cho 2 mục đích: Giúp hiểu rõ thế giới thực; Cung cấp cơ sở cho việc cài đặt; Ví dụ 3.1: Đối tượng Sinh viên, Nhân viên; Đối tượng thời gian. Các đối tượng có các đặc tính tương tự nhau được gom chung lại thành lớp đối tượng. Ví dụ người là một lớp đối tượng. Một lớp đối tượng được đặc trưng bằng các thuộc tính và các hoạt động (hành vi, thao tác). Một thuộc tính (attribute): là một giá trị dữ liệu cho mỗi đối tượng trong lớp. (Ví dụ: tên, tuổi, cân nặng là các thuộc tính của người) Một thao tác (operation): là một hàm hay một phép biến đổi có thể áp dụng vào hay áp dụng bởi các đối tượng trong lớp. Ví dụ 3.2: Mỗi sinh viên là một đối tượng với các thuộc tính: tên, tuổi, khoa, lớp, khoá, ... và có thể có các thao tác: học bài, làm bài tập, nghe giảng, làm bài kiểm tra, … Mỗi chiếc điện thoại là một đối tượng với các thuộc tính: số SIM, model, kích thước, … và có các thao tác: gọi số, nhắn tin, nghe cuộc gọi tới, từ chối cuộc gọi,… Lớp (class) là phần mô tả các thuộc tính và các tác vụ tương ứng của đối tượng có thể hiểu một cách đơn giản: mỗi sinh viên là một đối tượng trong khi khái niệm sinh viên là một lớp, tương tự với mỗi chiếc điện thoại và khái niệm điện thoại. Lớp là kiểu dữ liệu mở rộng của kiểu cấu trúc (struct). Ngoài các trường (field) tương ứng cho thuộc tính của đối tượng, các phương thức (method) tương tự như các hàm được bổ sung thêm tương ứng với các tác vụ có thể thực hiện của đối tượng. Đối tượng là một biến được khai báo với kiểu là lớp đã được định nghĩa. Từ lập trình cấu trúc: lấy hàm làm trung tâm. struct SinhVien { char ten[20]; int khoa; }; void len_lop(SinhVien& sv, intphong_hoc){ ... } 57 void kiem_tra(SinhVien& sv){ ... } SinhVien sv= { ... }; len_lop(sv, 103); kiem_tra(sv); Đến lập trình hướng đối tượng: class SinhVien { char ten[20]; int khoa; void len_lop(intphong_hoc){ ... } void kiem_tra(){ ... } }; SinhVien sv= { ... }; sv.len_lop(103); sv.kiem_tra(); Các hàm (function) trở thành phương thức (method) của lớp và có thể truy cập trực tiếp các thuộc tính (biến thành phần) của đối tượng gọi. Đối tượng (biến) trở thành chủ thể của phương thức (hàm) được gọi chứ không còn được truyền như tham số → lấy đối tượng làm trung tâm của việc lập trình. 3.2. ĐỊNH NGHĨA LỚP Cú pháp: Lớp được định nghĩa theo mẫu: class ten_lop { private: [khai báo các thuộc tính] [Định nghĩa các hàm thành phần] public: [khai báo các thuộc tính] [Định nghĩa các hàm thành phần] }; //chú ý cuối lớp phải có dấu; Ví dụ 3.3: class HINHCHUNHAT{ private: int dai,rong; public: void nhap(); void xuat(); int tinhS(); }; 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) để qui đị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, public) thì C++ ngầm định đó là private. Các thành phần private chỉ được sử dụng bên trong lớp (trong thân 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. 58 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ụ 3.4: class A { A x;//không cho phép A *p // Cho phép }; Đị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: Kiểu trả về TênLớ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à các 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á 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 private nhằm bảo đảm nguyên lý che dấu thông tin, bảo vệ an toàn dữ liệu, 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.3. TỪ KHÓA XÁC ĐỊNH THUỘC TÍNH TRUY XUẤT Trong lớp có thể có nhiều nhãn private và private. Mỗi nhãn này có phạm vi ảnh hưởng cho đến khi gặp một nhãn kế tiếp hoặc hết khai báo lớp. Nhãn private đầu tiên có thể bỏ đi vì C++ ngầm hiểu rằng các thành phần trước nhãn public đầu tiên là private. Ý nghĩa của các từ khoá truy xuất: các từ khoá này xác định khả năng truy xuất thành phần (thuộc tính, phương thức) của một lớp. private: những thành phần được liệt kê bên trong phần private chỉ được truy xuất bên trong phạm vi lớp và không được phép kế thừa. public: những thành phần được liệt kê bên trong phần public được truy xuất từ bất kỳ phạm vi nào và được kế thừa lại trong lớp dẫn xuất (kế thừa). protected: những thành phần được liệt kê bên trong phần protected được truy xuất từ bên trong phạm vi lớp, từ lớp dẫn xuất (kế thừa) và được kế thừa lại trong lớp dẫn xuất (kế thừa). Ví dụ 3.5: Trong phạm vi lớp. class ABC{ private: 59 int x; public: int y; public: void xuat(); }; // Định nghĩa các hàm void ABC::xuat() { printf("%d",x); //đúng printf("%d",y); //đúng } Từ lớp dẫn xuất class TEST: public ABC{ private: int z; public: void in(); }; // Định nghĩa các hàm void TEST::in() { printf("%d",x); //sai printf("%d",y); //đúng printf("%d",z); //đúng } Bên ngoài lớp void main(){ ABC a; TEST b; a.x = 5; //sai a.y = 10 //đúng a.xuat(); b.x = 10; //sai b.y = 15; //đúng b.z = 20; //sai b.in(); } 3.4. 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 { …. } <danh_sách_biến>; 60 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.5. TRUY NHẬP TỚI CÁC THÀNH PHẦN CỦA LỚP Để truy nhập tới 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ể truy nhập bởi các 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.6: class HCN{ private: int dai,rong; public: void nhap(); void xuat(); int tinhS(); }; // Định nghĩa các hàm void HCN::nhap() { printf("Nhap dai:"); scanf("%d",&dai); printf("Nhap rong:"); scanf("%d",&rong); } int HCN::tinhS() { return dai*rong; } class HCN{ private: int dai,rong; public: void nhap(); void xuat(); int tinhS(); }; // Định nghĩa các hàm void HCN::nhap() { printf("Nhap dai:"); scanf("%d",&dai); printf("Nhap rong:"); 61 scanf("%d",&rong); } void HCN::xuat() { printf("Dai = %d",dai); printf("Rong = %d",rong); } int HCN::tinhS() { return dai*rong; } // Sử dụng lớp void main() { HCN s; s.nhap(); s.xuat(); printf("Dien tich = %d",s.tinhS()); } Ví dụ 3.7: #include <conio.h> #include <iostream.h> class DIEM { private: int x,y; public : void nhap() { Cout<<”\n Nhap hoanh do va tung do cua diem:”; cin>>x>>y } void hienthi() { cout <<”\nx=”<<x<<” y=”<<y<<endl; } }; void main() { clrscr; DIEM d1; d1.nhap(); d1.hienthi(); getch(); } Ví dụ 3.8: #include <conio.h> #include <iostream.h> class A 62 { int m,n; public: void nhap() { cout<<”\n Nhap hai so nguyen:”; cin>>m>>n; } int max { return m>n?:n; } void hienthi() { cout<<”\n So lon nhat la:<<max()<<endl; } }; void main() { clrscr; A obj; obj.nhap(); obj.hienthi(); getch(); } 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ụ 3.9: Giả sử đã định nghĩa lớp: class DIEM{ private: double x,y; //toa do điểm public: void nhap(){ cout<<”Toa do x, y=”; cin>>x>>y; } void in(){ cout<<”x=”<<x<<” y=”<<y; } }; Dùng lớp điểm, ta xây dựng hàm tự do tính độ dài của đoạn thẳng đi qua hai điểm như sau: double do_dai(DIEM d1, DIEM d2) { return sqrt(pow(d1.x-d2.x)+pow(d1.y-d2.y)); } 63 Chương trình dịch sẽ báo lỗi đối với hàm này. Bởi vì trong thân hàm không cho phép sử dụng các thuộc tính d1.x, d2.x, d1.y, d2.y của các đối tượng d1 và d2 thuộc lớp DIEM. Ví dụ 3.10: Viết chương trình xây dựng một lớp a tính giá trị của tổng sau: S=1+2+3+...+n (n nguyên dương). #include<iostream.h> class a{ int n,i; float s; public: void nhap(){ cout<<"nhap n="; cin>>n;} float tong(){ s=0; for(i=1;i<=n;i++) s=s+i; return s;} void hienthi(){ cout<<" tong s="<<s;} }; void main(){ a th; th.nhap(); th.tong(); th.hienthi();} Ví dụ 3.11: Sử dụng hàm thành phần với tham số mặc định. #include <iostream.h> #include <conio.h> 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; } void main() { Box ob; int x = 10, y = 12, z = 15; cout <<"Dai Rong Cao Thetich\n"; cout << ob.get_thetich(x, y, z) << "\n"; 64 cout << ob.get_thetich(x, cout << ob.get_thetich(x) cout << ob.get_thetich(x, cout << ob.get_thetich(5, getch(); } y) << 7) 5, << "\n"; "\n"; << "\n"; 5) << "\n"; Kết quả chương trình như sau: Dai Rong Cao Thetich 10 12 15 1800 10 12 3 360 10 2 3 60 10 7 3 210 5 5 5 125 Ví dụ 3.12: Sử dụng hàm inline. #include <iostream.h> #include <string.h> #include <conio.h> 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) { strcpy(dongtu, in_dongtu); } inline char* phrase::get_phrase(void) { strcpy(cumtu,dongtu); strcat(cumtu," the "); 65 strcat(cumtu,danhtu); return cumtu; } void main() { phrase text; cout << "Cum tu la : -> " << text.get_phrase() << "\n"; text.set_danhtu("file"); cout << "Cum tu la : -> " << text.get_phrase()<<"\n"; text.set_dongtu("Save"); cout << "Cum tu la : -> " << text.get_phrase()<<"\n"; text.set_danhtu("program"); cout << "Cum tu la : -> " << text.get_phrase()<<"\n"; } Cum Cum Cum Cum Kết quả chương trình như sau: tu la : -> the tu la : -> the file tu la : -> Save the file tu la : -> Save the program Ví dụ 3.13: Sử dụng từ khóa const. #include <iostream.h> #include <conio.h> 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; const int STOP = 6; int index; for (index=START; index<=STOP; index++) { cout<< "index = " ; num.print_it(index); cout<< "START = " ; num.print_it(START); } 66 getch(); } index START index START index START index START Kết quả chương trình như sau: = 3 = 3 = 4 = 3 = 5 = 3 = 6 = 3 3.6. PHÉP GÁN ĐỐI TƯỢNG Trong lập trình hướng đối tượng, phép gán đối tượng là việc sao chép các giá trị của các thành phần dữ liệu từ đối tượng a sang đối tượng b. Ví dụ 3.14: class Person{ private: int tuoi,CMND; public: void khoitao(int t, int c); }; void Person::khoitao(int t, int c) { tuoi = t; CMND = c; } void main() { Person a; a.khoitao(36,131361665); Person b; b = a; } Hình 3.1: Phép gán đối tượng 3.7. CON TRỎ ĐỐI TƯỢNG Con trỏ đối tượng dùng để chứa địa chỉ của biến đối tượng, được khai báo như sau: 67 Tên_lớp *Tên_con_trỏ; Ví dụ 3.15: DIEM *p1, *p2, *p3; DIEM d1, d2; DIEM d[20]; Có thể thực hiện câu lệnh: p1=&d2; p2=d; p3= new DIEM Để truy xuất tới các thành phần của lớp từ con trỏ đối tượng, ta viết như sau: Tên_con_trỏTen_thuoc_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.16: #include <iostream.h> #include <conio.h> 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; } }; const int k=5; void main() { clrscr(); mhang *p = new mhang[k]; mhang *d = p; int x,i; float y; cout<<"\nNhap vao du lieu 5 mat hang :"; for (i = 0; i <k; i++) { cout <<"\nNhap ma hang va don gia cho mat hang thu " <<i+1; cin>>x>>y; p -> getdata(x,y); p++;} for (i = 0; i <k; i++) { cout <<"\nMat hang thu : " << i + 1 << d -> show(); d++; 68 endl; } getch(); } 3.8. CON TRỎ THIS Mỗi hàm thành phần của lớp có một tham số ẩn, dó là con trỏ this. Con trỏ this trỏ tới từng đối tượng cụ thể. Ta hãy xét lại hàm nhap() của lớp DIEM trong ví dụ ở trên: void nhap() { cout << "\n Nhap hoanh do va tung do cua diem : "; cin >>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 có vẻ 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àn 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 nhap() có thể viết một cách tường minh như sau: void nhap() { cout<<”\nNhap hoanh do, tung do cua diem:”; cin>>thisx>>thisy; } 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 đó. Con trỏ this được dùng để tham chiếu đến chính địa chỉ của đối tượng của lớp. Dùng toán tử để truy cập đến các thành viên của đối tượng. Giúp ta truy cập, phân biệt được các thành viên ngay bên trong lớp. Khi hàm thành viên cần trả giá trị về là chính đối tượng. Ví dụ 3.17: DIEM d1; d1.nhap(); Trong trường hợp này của d1 thì this=&d1. Do đó thisx chính là d1.x và thisy chính là d1.y Chú ý: Ngoài tham số đặc biệt thí 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 số khác được khai báo như trong các hàm thông thường. Ví dụ 3.18: class Person{ private: int age; public: void display(int age); Person getPerson(); }; // Định nghĩa các hàm void Person::display(int age) { age = 25; this->age = 30; 69 } Person Person::getPerson() { return *this; // trả về chính đối tượng } Ví dụ 3.19: #include <iostream.h> #include <conio.h> class time { int h,m; public : void nhap(int h1, int m1) { h= h1; m = m1;} void hienthi(void) { cout <<h << " gio "<<m << " phut"<<endl;} void tong(time, time); }; void time::tong(time t1, time t2) { m= t1.m+ t2.m; //this->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.9. HÀM BẠN Trong thực tế thường xảy ra các 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: Cách 1: Dùng từ khóa friend để khai báo hàm trong lớp và xây dựng bên ngoài như các hàm thông thường. Cú pháp như sau: 70 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 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. Cú pháp class A { private: //khai báo thuộc tính public: … //Khai báo các hàm bạn 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; 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 lớp này. Để làm cho f trở thành bạn của các lớp A, B, và C ta viết như 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 71 //Định nghĩa A class A { //khai báo f là bạn của A friend void f(...) }; //ĐỊnh nghĩa B class B { //Khao báo f là bạn của B friend void f(…) }; //Định nghĩa 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.20: #include <iostream.h> #include <conio.h> 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; c3.a=c1.a + c2.a ; c3.b=c1.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); 72 sophuc d2 (1.2,2.3) ; sophuc d3 ; d3 = tong(d1,d2); cout<<"d1= ";hienthi(d1); cout<<"d2= ";hienthi(d2); cout<<"d3= ";hienthi(d3); getch(); } Chương trình cho kết quả như sau: d1= 2.1 + 3.4i d2= 1.2 + 2.3i d3= 3.3 + 5.7i Ví dụ 3.21: #include <iostream.h> #include <conio.h> 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) { 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(); } 73 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.10. THÀNH PHẦN DỮ LIỆU HẰNG VÀ HÀM THÀNH PHẦN TĨNH 3.10.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; … }; 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. Xét chương trình sau: #include <conio.h> #include <iostream.h> class HDBH { private: char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() { cout <<”\n” << tshd; cout <<”\n” << tstienban; } }; void main () { HDBH::in(); getch(); 74 } Các thành phần tĩnh tshd và tstienban chưa khai báo, nên chưa tồn tại. Vì vậy các câu lệnh in giá trị các thành phần này trong hàm in() là không thể được. Khi dịch chương trình, sẽ nhận được các thông báo lỗi. Có thể sửa chương trình trên bằng cách đưa vào các lệnh khai báo các thành phần tĩnh tshd và tstienban như sau: Ví dụ 3.22: #include <conio.h> #include <iostream.h> class HDBH { private: int shd; char *tenhang; double tienban; static int tshd; static double tstienban; public: s tatic void in() { cout <<”\n” <<tshd; cout <<”\n” <<tstienban; } }; int HDBH::tshd=5 double HDBH::tstienban=20000.0; void main() { HDBH::in(); getch(); } 3.10.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; 75 double tienban; static int tshd; static double tstienban; public: static void in() { cout <<”\n” << tshd; cout << ”\n” << tstienban; cout <<”\n”<< tenhang //loi cout <<”\n” << tienban; //loi } }; Ví dụ: #include <iostream.h> #include <conio.h> 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 = m = Doi Doi Doi 76 3 4 tuong thu : 2 tuong thu : 3 tuong thu : 4 3.11. HÀM TẠO (CONSTRUCTOR) Là hàm khởi tạo có đối số là một đối tượng khác của cùng lớp đó. 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. Bất kể loại cấp phát bộ nhớ nào được sử dụng (tự động, tĩnh, động), mỗi khi một thể hiện của lớp được tạo, một hàm constructor nào đó của lớp sẽ được gọi. Một lớp không có khai báo bất kỳ hàm thiết lập nào thì hàm thiết lập không đối số được tạo ra mặc định. Khi xây dựng hàm tạo cần lưu ý những đặc điểm 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ề (kể cả void). 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.23: 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 }; Ví dụ 3.24: class HCN{ private: int dai,rong; public: HCN(); HCN(int d,int r); HCN(int d=10,int r=10); void nhap(); void xuat(); }; 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.25: #include <conio.h> #inlcule <iostream.h> 77 class DIEM { private: int x,y; public: void in() { cout <<”\n” << y <<” ” << m; } }; void main() { DIEM d; d.in(); DIEM *p; p= new DIEM [10]; clrscr(); d.in(); for (int i=0;1<10;++i) (p+i)->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.26: #include <conio.h> #include <iostream.h> #include <iomanip.h> class DIEM { private: int x,y; public: DIEM() { x = y = 0; } DIEM(int x1, int y1) { 78 x = x1; y = y1; } friend void in(DIEM d) { cout <<"\n" << d.x <<" " << d.y; } void in() { cout <<"\n" << x <<" " << y ; } }; void main() { DIEM d1; DIEM d2(2,3); DIEM *d; d = new DIEM (5,6); clrscr(); in(d1); // Goi ham ban in() d2.in(); // Goi ham thanh phan in() in(*d); // Goi ham ban in() DIEM(2,2).in();// Goi ham thanh phan in() DIEM t[3]; // 3 lan goi ham tao khong doi DIEM *q; // Goi ham tao khong doi int n; cout << "\n N = "; cin >> n; q = new DIEM [n+1]; //n+1 lan goi ham tao khong doi for (int i=0;i<=n;++i) q[i]=DIEM (3+i,4+i);//n+1 lan goi ham tao co doi for (i=0;i<=n;++i) q[i].in(); // Goi ham thanh phan in() for (i=0;i<=n;++i) DIEM(5+i,6+i).in(); //Goi ham thanh phan in() getch(); } Chú ý 3: Nếu trong lớp đã có ít nhất một hàm tạo, thì hàm tạo mặc định sẽ không được phát sinh nữa. Khi đó mọi câu lệnh xây dựng đối tượng mới đều sẽ gọi đến một hàm tạo của lớp. Nếu không tìm thấy hàm tạo cần gọi thì chương trình dịch sẽ báo lỗi. Điều này thường xảy ra khi chúng ta không xây dựng hàm tạo không đối, nhưng lại sử dụng các khai báo không tham số như ví dụ sau: Ví dụ 3.27: #include <conio.h> #include <iostream.h> class DIEM { private: int x,y; public: 79 DIEM(int x1, int y1) } void in(){ cout << “\n” << x << “ ” << y }; void main() { DIEM d1(200,200); // Goi ham DIEM d2; // Loi, goi ham tao d2= DIEM_DH (3,5); // Goi ham d1.in(); d2.in(); getch(); } { <<” ” << m; x=x1; y=y1; } tao co doi khong doi tao co doi Trong ví dụ này, câu lệnh DIEM d2; trong hàm main() sẽ bị chương trình dịch báo lỗi. Bởi vì lệnh này sẽ gọi tới hàm tạo không đối, mà hàm tạo này chưa được xây dựng. Có thể khắc phục điều này bằng cách chọn một trong hai giải pháp sau: - Xây dựng thêm hàm tạo không đối. Gán giá trị mặc định cho tất cả các đối x1, y1 của hàm tạo đã xây dựng ở trên. Theo giải pháp thứ 2, chương trình trên có thể sửa lại như sau: Ví dụ 3.28: #include <conio.h> #include <iostream.h> 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; } }; 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.29: #include <iostream.h> #include <conio.h> class rectangle { private: int dai; int rong; 80 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 : "<< medium.get_area()<<"\n"; cout<<"Dien tich la : "<<large.get_area()<<"\n"; cout <<"Gia tri du lieu tinh la : "<< small.get_extra()<<"\n"; cout <<"Gia tri du medium.get_extra()<<"\n"; cout <<"Gia tri du large.get_extra()<<"\n"; } lieu tinh la : "<< lieu tinh la : "<< Chương trình cho kết quả như sau Dien tich la : 35 Dien tich la : 64 Dien tich la : 300 Gia tri du lieu tinh la : 1 Gia tri du lieu tinh la : 2 Gia tri du lieu tinh la : 3 3.12. HÀM TẠO SAO CHÉP 3.12.1. Hàm tạo sao chép mặc định Giả sử đã định nghĩa một lớp ABC nào đó. Khi đó ta có thể dùng câu lệnh khai báo hoặc cấp phát bộ nhớ để tạo các đối tượng mới, ví dụ: ABC p1, p2; ABC *p = new ABC; 81 Ta cũng có thể dùng lệnh khai báo để tạo một đối tượng mới từ một đối tượng đã tồn tại, ví dụ: ABC u; ABC v(u); // Tạo v theo u Câu lệnh này có ý nghĩa như sau: - Nếu trong lớp ABC chưa xây dựng hàm tạo sao chép, thì câu lệnh này sẽ gọi tới một hàm tạo sao chép mặc định của C++. Hàm này sẽ sao chép nội dung từng bit của u vào các bit tương ứng của v. Như vậy các vùng nhớ của u và v sẽ có nội dung như nhau. - Nếu trong lớp ABC đã có hàm tạo sao chép thì câu lệnh: PS v(u); sẽ tạo ra đối tượng mới v, sau đó gọi tới hàm tạo sao chép để khởi gán v theo u. Ví dụ sau minh họa cách dùng hàm tạo sao chép mặc định: Trong chương trình đưa vào lớp PS (phân số): + Các thuộc tính gồm: t (tử số) và m (mẫu). + Trong lớp mà không có phương thức nào cả mà chỉ có hai hàm bạn là các hàm toán tử nhập (>>) và xuất (<<). + Nội dung chương trình là: Dùng lệnh khai báo để tạo một đối tượng u (kiểu PS) có nội dung như đối tượng đã có d. Ví dụ 3.30: #include <conio.h> #include <iostream.h> class PS { private: int t,m; public: friend ostream& operator<< (ostream& os,const PS &p) { os<<" = "<<p.t<<"/"<<p.m; return os; } friend istream& operator>> (istream& is, PS &p) { cout <<" Nhap tu va mau : "; is>>p.t>>p.m; return is; } }; void main() { PS d; cout <<"\n Nhap phan so d "; cin>>d; cout<<"\n PS d "<<d; PS u(d); cout<<"\n PS u "<<u; getch(); } 82 3.12.2. Hàm tạo sao chép Hàm tạo sao chép sử dụng một đối số kiểu tham chiếu đối tượng để khởi án cho đối tượng mới và được viết theo mẫu sau: Tên_lớp(const Tên_lớp &obj) { //Các câu lệnh dùng các thuộc tính của đối tựợng obj để khởi gán //cho các thuộc tính của đối tượng mới } Ví dụ 3.31: class PS { private: int t,m; public: PS(const PS &p) { t=p.t; m=p.m; } … }; Hàm tạo sao chép cho ví dụ trên không khác hàm tạo sao chép mặc định Chú ý: 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; double *a; public: DT() { n=0; a=NULL; } DT(int n1) { n=n1; a=new double[n1+1] } friend ostream& operator<< (ostream& os, const DT &d); friend istream& operator>>(istream& is, DT &d); … }; 83 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 tới sai lầm như thế nào: DT d; cin>>d; Câu lệnh trên 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. Còn câu lệnh: 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.32: #include <conio.h> #include <iostream.h> #include <math.h> 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<< (ostream& os,const DT &d); friend istream& operator>> (istream& is,DT &d); }; ostream& operator<< (ostream& os,const DT &d) { os <<" Cac he so "; for (int i=0; i<=d.n; ++i) os << d.a[i]<<" "; return os; } istream& operator>> (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout << " \nBac da thuc:"; 84 cin >>d.n; d.a = new double[d.n+1]; cout<<"Nhap cac he so da thuc:\n"; for (int i=0 ; i<=d.n ; ++i) { cout<<"He so bac"<< i << "="; is >> d.a[i]; } return is; } void main() { DT d; clrscr(); cout<<"\nNhap da thuc d" ; cin >> d; DT u(d); cout <<"\nDa thuc d" << d ; cout <<"\nDa thuc u" << u ; cout <<"\nNhap da thuc d" << d ; cin >> d; cout <<"\nDa thuc d" << d ; cout <<"\nDa thuc u" << u ; cout <<"\nDa thuc u" << u ; cin >> u; cout <<"\nDa thuc d" << d ; cout <<"\nDa thuc u" << u ; getch(); } Ví dụ 3.33: Ví dụ sau minh họa về hàm tạo sao chép: #include <conio.h> #include <iostream.h> #include <math.h> class DT { private: int n; // Bac da thuc double *a; // Tro toi vung // a0, a1,... public: DT() { nho chua cac da thuc this->n=0; this->a=NULL; } DT(int n1) { this->n=n1; this->a= new double[n1+1]; } 85 DT(const DT &d); friend ostream& operator<< (ostream& os,const DT &d); friend istream& 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) this->a[i] = d.a[i]; } ostream& operator<< (ostream& os,const DT &d) { os<<"-Cac he so (tu ao): "; for (int i=0 ; i<=d.n ; ++i) os << d.a[i] <<" "; return os; } istream& operator>> (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout << "\n Bac da thuc:"; cin >> d.n; d.a = new double[d.n+1]; cout << "Nhap cac he so da thuc:\n"; for (int i=0 ; i<= d.n ; ++i) { cout << "He so bac " << i << "="; is >> d.a[i]; } return is; } void main() { DT d; clrscr(); cout <<"\nNhap da thuc d " ; cin >> d; DT u(d); cout << "\nDa thuc d " << d; cout << "\nDa thuc u " << u; cout << "\nNhap da thuc d "; cin >> d; cout << "\nDa thuc d " << d; cout << "\nDa thuc u " << u; cout << "\nNhap da thuc u " ; cin >> u; cout << "\nDa thuc d " << d; cout << "\nDa thuc u " << u; getch(); } Ví dụ 3.34: Có bao nhiêu lần hàm tạo (thiết lập) sao chép được gọi trong đoạn mã chương trình sau: Widget f(Widget u) { Widget v(u); 86 Widget w=v; return w; } void main() { Widget x; Widget y=f(f(x)); } Hàm tạo sao chép được gọi 7 lần trong đoạn mã chương trình trên. Mỗi lần gọi hàm f đòi hỏi 3 lần gọi đến hàm tạo sao chép: khi tham số truyền vào bằng giá trị u, khi v và w được khởi tạo. Lần thứ 7 là để khởi tạo y. 3.13. HÀM HỦY (DESTRUCTOR) Hàm hủy được gọi ngay trước khi thu hồi một đối tượng. Hàm hủy 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 hủy đượ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ý, xóa đối 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à các 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 hủy thì một hàm hủy mặc định không làm gì cả được phát sinh. Đối với nhiều lớp thì hàm hủy mặc định là đủ, không cần đưa vào hàm hủy mới. Trường hợp cần xây dựng hàm hủy thì ta phải 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 trả về (kể cả void), không có giá trị trả về và không có tham số; Tên hàm hủy có một dẫu ngã ngay trước tên lớp. Ví dụ 3.35: class HCN{ private: int dai,rong; public: HCN(); HCN(int d,int r); HCN(int d=10,int r=10); void nhap(); void xuat(); ~HCN(); }; Ví dụ 3.36: class A { private: int n; double *a; public: ~A() { 87 n-0; delete a; } }; Ví dụ 3.37: #include <iostream.h> 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() { 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 So cac doi tuong duoc tao ra la 88 = 1 = 3 Object ID Object ID Object ID Doi tuong Doi tuong Doi tuong la 1 la 2 la 3 3 duoc huy bo 2 duoc huy bo 1 duoc huy bo 3.14. MỘT SỐ VÍ DỤ 3.14.1. Xây dựng lớp tam giác Xây dựng một lớp TamGiac để mô tả các đối tượng tam giác bao gồm các hàm thành phần cần thiết để phân loại tam giác, xác định chu vi, diện tích của tam giác. #include <iostream.h> #include <math.h> #include <conio.h> class tamgiac { private: double a,b,c; public: tamgiac(double aa=0, double bb=0, double cc=0) { a=aa; b=bb; c=cc; } void nhap() { cout<<"Nhap 3 canh tam giac \n"; cout<<"Nhap canh a= "; cin>>a; cout<<"Nhap canh b= "; cin>>b; cout<<"Nhap canh c= "; cin>>c; } void xuat() { cout<<"Thong tin tam giac \n"; cout<<"Canh a= "<<a<<", Canh b= "<<b<<", Canh c= "<<c; } int hople() { if ((a+b>c)&&(b+c>a)&&(c+a>b)&&(a>0)&&(b>0)&&(c>0)) return 1; else return 0; } void phanloai() { if (((a!=b)&&(a!=c))||((b!=a)&&(b!=c))||((c!=a)&&(c!=b))) { 89 if ((a==b)||(b==c)||(c==a)) cout<<"Tam giac can"; else if ((a*a==b*b+c*c)||(b*b==a*a+c*c)||(c*c==a*a+b*b)) cout<<"Tam giac vuong"; else cout<<"Tam giac thuong"; } else cout<<"Tam giac deu"; } double chuvi() { double p; p=a+b+c; return p; } double dientich() { double p,s; p=(a+b+c)/2; s=sqrt(p*(p-a)*(p-b)*(p-c)); return s; } }; void main() { tamgiac a; a.nhap(); a.xuat(); cout<<endl; if (a.hople()==1) { cout<<"3 canh tam giac hop le"; cout<<endl; a.phanloai(); cout<<"\nChu vi tam giac: "<<a.chuvi(); cout<<"\nDien tich tam giac: "<<a.dientich(); } else cout<<"3 canh tam giac khong hop le"; getch(); } 3.14.2. Lớp mảng 1 chiều Xây dựng một lớp Mang1c dữ liệu thành phần kiểu số nguyên, các hàm thành phần gồm: phương thức khởi tạo mảng, phương thức in mảng, phương thức in phần tử lớn nhất, phần tử nhỏ nhất của mảng #include <iostream.h> #include <conio.h> class Mang1c 90 { private: int a[100]; int n; public: void input() { cout<<”Input n= “; cin>>n; for (int i=0; i<n; i++) { cout<<”a[“<<i<<”]= “; cin>>a[i]; } } void show() { for (int i=0; i<n; i++) cout<<a[i]<<” “; } void max_min() { int max, min; max=a[0]; for (int i=1; i<n; i++) { if (max<a[i]) max=a[i]; } cout<<”Maximum value: “<<max; min=a[0]; for (int i=1; i<n; i++) { if (min>a[i]) min=a[i]; } cout<<”\nMinimum value: “<<min; } }; void main() { Mang1c a; a.input(); a.show(); cout<<endl; a.max_min(); getch(); } 3.14.3. Quản lý điểm tuyển sinh Để quản lý điểm thi vào trường đại học của các thí sinh, cần xây dựng lớp ThiSinh mô tả các thí sinh bao gồm các thuộc tính và phương thức sau: 91 - Tên thí sinh - Điểm của ba môn Toán, Lý, Hóa - Nhập thông tin của các thí sinh bao gồm tên và điểm thi của ba môn nêu trên - In thông tin tên, điểm và tổng điểm của 3 môn Sử dụng lớp vừa xây dựng, viết chương trình thực hiện công việc sau: - Nhập danh sách kết quả thi của các thí sinh từ bàn phím ; - Đưa ra màn hình danh sách thí sinh trúng tuyển (điểm chuẩn là 20). #include <iostream.h> #include <stdio.h> #include <conio.h> class ThiSinh { char ten[25];// Tên của thí sinh không dài quá 24 ký tự int toan, ly, hoa;// Điểm ba môn toán, lý, hoá public: void nhapdl();//Nhap dữ liệu cho thí sinh void inkq();// In kêt quả thi của thí sinh int tong();// Tính tong diem của thí sinh }; void ThiSinh::nhapdl() { cout<<”Nhap ten:”;fflush(stdin);gets(ten); cout<<”Nhap diem toan:”;cin>>toan; cout<<”Nhap diem ly:”;cin>>ly; cout<<”Nhap diem hoa:”;cin>>hoa; } void ThiSinh::inkq() { printf(“%-25s%6d%6d%6%6d\n”,ten,toan,ly,hoa,tong()); } int ThiSinh::tong() { return (toan+ly+hoa); } void main() { clrscr(); int n; cout<<”Cho so thi sinh:”;cin>>n; ThiSinh *dsts=new ThiSinh[n]; // Nhap d lieu cho t0ng thí sinh for(int i=0;i<n;++i) { cout<<” Nhap du lieu cho thi sinh thu:”<<i+1<<endl; // G_i phương th'c nhap d lieu c_a thí sinh th' i trong m_ng dsts[i].nhapdl(); } cout<<”Danh sach nhung nguoi trung truyen \n”; printf(“%-25s%6s%6s%s%6s\n”,”Ten”,”Toan”,”Ly”,”Hoa”,”Tong”); 92 for(i=0;i<n;++i) if(dsts[i].tong()>=20) dsts[i].inkq(); delete dsts; getch(); } 3.14.4. Cài đặt và sử dụng phương thức thiết lập (constructor) và phương thức huỷ (destructor) Minh hoạ trên lớp mảng 1 chiều số nguyên. Nội dung file Mang.h #pragma once #include <stdlib.h> #include <time.h> #include <iostream> using namespace std; #define MAX 100 class CMang { private: int *a; int n; public: CMang(); CMang(int nn); //Tạo mảng có kích thước nn CMang(CMang &arr); CMang(int *aa, int nn); ~CMang(); void Print(); //Xuất mảng void PrintLn(); }; Nội dung file Mang.cpp #include "Mang.h" CMang::CMang() { n = 0; a = new int[n]; } CMang::CMang(int nn) { n = nn; a = new int[n]; for (int i = 0; i < n; i++) { a[i] = rand() % MAX; } } 93 CMang::CMang(CMang &arr) { n = arr.n; a = new int[n]; for (int i = 0; i < n; i++) { a[i] = arr.a[i]; } } CMang::CMang(int *aa, int nn) { n = nn; a = new int[n]; for (int i = 0; i < n; i++) { a[i] = aa[i]; } } CMang::~CMang() { delete[]a; } void CMang::Print() { cout << "So phan tu: " << n << endl; for (int i = 0; i < n; i++) { cout << a[i] << "\t"; } } void CMang::PrintLn() { cout << "So phan tu: " << n << endl; for (int i = 0; i < n; i++) { cout << a[i] << "\t"; } cout << endl; } Nội dung file main.cpp #include "Mang.h" void main() { srand((unsigned int)time(NULL)); CMang arr1(9); cout << "Mang arr1:" << endl; arr1.PrintLn(); 94 CMang arr2 = arr1; //CMang arr2(arr1); cout << "Mang arr2:" << endl; arr2.PrintLn(); int b[5] = { 2, 5, 6, 10, 11 }; CMang arr3(b, 5); cout << "Mang arr3:" << endl; arr3.PrintLn(); } 3.14.5. Lớp ngày tháng Nhập một ngày tháng năm từ bàn phím sau đó in ra màn hình. #include <iostream.h> #include <conio.h> #define FALSE 0 #define TRUE !FALSE char* Thang[]={"","gieng","hai","ba","bon","nam","sau","bay","tam", "chin","muoi","muoi mot","chap"}; int NgayThang[]={0,31,28,31,30,31,30,31,31,30,31,30,31}; class CDate { private: int mNgay,mThang,mNam; int laNamNhuan(int); public: void nhap(); int hopLe(); void in(); }; void CDate::nhap() { cout<<endl<<"Ngay: ";cin>>mNgay; cout<<endl<<"Thang: "; cin>>mThang; cout<<endl<<"Nam: ";cin>>mNam; } int CDate::hopLe() { if ((mThang<1)||(mThang>12)) return FALSE; else { if ((mNgay>=1)&&(mNgay<=NgayThang[mThang])) return TRUE; else if ((mNgay==29)&&laNamNhuan(mNgay)) return TRUE; else return FALSE; } 95 } int CDate::laNamNhuan(int nam) { if (((nam%400)==0)||(((nam%4)==0)&&((nam%100)!=0))) return TRUE; else return FALSE; } void CDate::in() { cout<<endl<<"Ban da nhap vao ngay "<<mNgay; cout<<" thang "<<Thang[mThang]; cout<<" nam "<<mNam; } void main() { CDate ngay; ngay.nhap(); if (ngay.hopLe()) ngay.in(); else cout<<"BAN NHAP NGAY KHONG HOP LE"; getch(); } 3.14.6. Lớp phân số Xây dựng lớp phanso để mô tả các đối tượng phân số. Xây dựng các phương thức (hàm thành viên) cho lớp phanso như sau: - Khởi tạo giá trị cho các thành phần dữ liệu của phân số (hàm tạo). - Nhập giá trị cho các thành phần dữ liệu của phân số. - In ra màn hình giá trị phân số theo định dạng “tử số/mẫu số”. - Thực hiện các phép toán cộng, trừ, nhân và chia hai phân số. - Thực hiện tối giản phân số. Viết chương trình sử dụng lớp trên: nhập vào 2 phân số, in ra màn hình giá trị phép cộng, trừ, nhân, chia dưới dạng tối giản. using namespace std; #include<iostream> class phanso {int tu,mau; public: phanso() {tu=mau=0;} phanso(int a,int b){ tu=a;mau=b;} void nhap() {cout<<"nhap tu so:";cin>>tu; cout<<"nhap mau so:";cin>>mau; } void xuat() {if(tu==mau) 96 cout<<"phan so:1"; else cout<<"phan so:"<<tu<<"/"<<mau<<endl; } phanso cong(phanso a,phanso b) {phanso c; c.tu=a.tu*b.mau+b.tu*a.mau; c.mau=a.mau*b.mau; return c; } phanso tru(phanso a,phanso b) {phanso c; c.tu=a.tu*b.mau-b.tu*a.mau; c.mau=a.mau*b.mau; return c; } phanso nhan(phanso a,phanso b) {phanso c; c.tu=a.tu*b.tu; c.mau=a.mau*b.mau; return c; } phanso chia(phanso a,phanso b) {phanso c; c.tu=a.tu*b.mau; c.mau=a.mau*b.tu; return c; } //tim uoc so chung lon nhat int uscln(int m,int n) {while (n!=0) {int du=m%n; m=n; n=du; } return m; } void toigian() {int us=uscln(tu,mau); tu=tu/us; mau=mau/us; } }; main() {phanso ob1,ob2(2,5),ob3,ob4,ob5,ob6; cout<<"Nhap phan so 1: \n"; ob1.nhap(); cout<<"Phan so 1: \n"; ob1.xuat(); 97 cout<<"Phan so 2: \n"; ob2.xuat(); ob3=ob3.cong(ob1,ob2); ob3.toigian(); cout<<"Tong hai phan so la: \n"; ob3.xuat(); ob4=ob4.tru(ob1,ob2); ob4.toigian(); cout<<"Hieu hai phan so la: \n"; ob4.xuat(); ob5=ob5.nhan(ob1,ob2); ob5.toigian(); cout<<"Tich hai phan so la: \n"; ob5.xuat(); ob6=ob6.chia(ob1,ob2); ob6.toigian(); cout<<"Ket qua phep chia hai phan so la: \n"; ob6.xuat(); return 0; } 3.14.7. Lớp số phức Xây dựng lớp complex để mô tả các đối tượng số phức. Xây dựng các phương thức cho lớp complex như sau: - Khởi tạo giá trị cho các thành phần dữ liệu của số phức (hàm tạo). - Nhập giá trị cho các thành phần dữ liệu của số phức. - In ra màn hình giá trị phân số theo định dạng “thực + ảo *j”. - Thực hiện các phép toán cộng, trừ, nhân hai số phức. Viết chương trình sử dụng lớp trên: nhập vào 2 số phức, in ra màn hình kết quả của phép cộng, trừ, nhân. #include<iostream.h> class complex{int thuc,ao; public: complex(){thuc=ao=0;} complex(int a){thuc=a;ao=0;} complex(int a,int b){thuc=a;ao=b;} void nhap(){cout<<"nhap phan thuc:";cin>>thuc; cout<<"nhap phan ao:";cin>>ao; } void xuat() {cout<<thuc; if(ao>=0)cout<<"+"<<ao<<"*j"; else cout<<ao<<"*j"; } //Tong:(a + b*j) + (c +d*j) = (a + c) + (b + d)*j friend complex tong(complex a,complex b) {complex c; c.thuc=a.thuc+b.thuc; c.ao=a.ao+b.ao; 98 return c; } //Hieu:(a + b*j) - (c + d*j) =(a + c) - (b + d)*j friend complex hieu(complex a,complex b) {complex c; c.thuc=a.thuc+b.thuc; c.ao=-(a.ao+b.ao); return c; } //Tich:(a + b*j) * (c + d*j) = (ac - bd) + (bc + ad)*j friend complex tich(complex a,complex b) {complex c; c.thuc=a.thuc*b.thuc-a.ao*b.ao; c.ao=a.ao*b.thuc+a.thuc*b.ao; return c; } }; main() {complex ob,ob1,t,h,ob2; cout<<"Nhap so phuc thu 1:\n";ob.nhap(); cout<<"So phuc thu 1:"; ob.xuat(); cout<<endl; cout<<"Nhap so phuc thu 2:\n"; ob1.nhap(); cout<<"So phuc thu 2:"; ob1.xuat(); cout<<endl; //tong hai so phuc t=tong(ob,ob1); cout<<"tong hai so phuc:";t.xuat(); h=hieu(ob,ob1); cout<<"\nHieu hai so phuc:";h.xuat(); ob2=tich(ob,ob1); cout<<"\ntich so phuc 1 va so phuc 2:";ob2.xuat(); return 0;} 3.14.8. Xây dựng lớp thời gian time Viết chương trình thực hiện các công việc sau: - Khai báo lớp time với các thuộc tính: giờ, phút, giây. - Xây dựng hàm tạo không đối, hàm tạo có đối, hàm hủy một đối tượng time. - Xây dựng các phương thức: nhập, xuất, chuẩn hóa một đối tượng time. - Định nghĩa toán từ ++, -- để tăng, giảm một giây. Thực hiện tăng, giảm một giây và in kết quả ra màn hình (kết quả phải được chuẩn hóa dưới định dạng 24 tiếng hh:mm:ss). #include <iostream> #include <conio.h> using namespace std; class time { public: time() { this->h = 0; 99 this->p = 0; this->s = 0; }; time(int h1, h = h1; p = p1; s = s1; } ~time() { this->h this->p this->s }; int p1, int s1) { = 0; = 0; = 0; void nhap() { cout << "Nhap gio: "; cin >> h; cout << "Nhap phut: "; cin >> p; cout << "Nhap giay: "; cin >> s; cout << endl; } void xuat() { cout << "Bay gio la: " << endl; cout << h << ":" << p << ":" << s << endl; cout << endl; } void chuanhoa() { while (this->s >= 60) { this->p += 1; this->s -= 60; } while (this->p >= 60) { this->h += 1; this->p -= 60; } while (this->h >= 24) this->h -= 24; } time operator +(time t2) { time t; t.h = h + 0; t.p = p + 0; t.s = s + t2.s; return *this; } time operator -(time t2) { time t; t.h = h - 0; t.p = p - 0; 100 t.s = s - t2.s; return *this; } time operator =(time t2) { h = t2.h; p = t2.p; s = t2.s; return *this; } private: int h, p, s; }; main() { time t, t1(0, 0, 1), tang, giam; t.nhap(); t.chuanhoa(); t.xuat(); tang = t + t1; giam = t - t1; tang.chuanhoa(); tang.xuat(); giam.chuanhoa(); giam.xuat(); getch(); } 3.14.9. Xây dựng lớp sinh viên Viết chương trình thực hiện các công việc sau: - Khai báo lớp người với các thuộc tính: họ tên, năm sinh. - Khai báo lớp sinh viên kế thừa từ lớp người và thêm các thuộc tính: mã sinh viên, điểm trung bình. - Xây dựng các phương thức: nhập, xuất cho các đối tượng người, sinh viên. - Nhập vào n sinh viên. Sắp xếp lại và in ra màn hình danh sách sinh viên theo thứ tự giảm dần của điểm trung bình. #include <iostream> #include <string.h> using namespace std; class nguoi { protected: char hoten[20]; int ns; public: void nhap() { cout << "Nhap ho ten: "; fflush(stdin); gets(hoten); cout << "Nhap nam sinh: "; cin >> ns; } 101 void xuat() { cout << "Ho ten: " << hoten << endl; cout << "Nam sinh: " << ns << endl; } }; class sinhvien : public nguoi { private: char masv[20]; float tb; public: void nhapsv() { cout << "Nhap ma sinh vien: "; fflush(stdin); gets(masv); cout << "Nhap diem TB: "; cin >> tb; } void xuatsv() { cout << "Ma sv: " << masv << endl; cout << "Diem TB: " << tb << endl; } sinhvien operator =(sinhvien s2) { strcpy(hoten, s2.hoten); ns = s2.ns; strcpy(masv, s2.masv); tb = s2.tb; return *this; } int operator <(sinhvien s2) { if (tb < s2.tb) return 1; else return 0; } }; main() { int n, i; sinhvien s[100], temp; cout << "Nhap vao so sinh vien: n= "; cin >> n; for (i = 1; i <= n; i++) { cout << "Sinh vien thu " << i << ": " << endl; s[i].nhap(); s[i].nhapsv(); cout<<endl; } cout << endl; for (i = 1; i <= n; i++) { cout << "Sinh vien thu " << i << ": " << endl; s[i].xuat(); s[i].xuatsv(); cout<<endl; } int j; 102 for (i = 1; i <= n - 1; i++) for (j = i + i; j <= n; j++) { if (s[i]<s[j]) { temp = s[i]; s[i] = s[j]; s[j] = temp; } } cout << endl; cout << "Sau khi sap xep: " << endl; for (i = 1; i <= n; i++) { cout << "Sinh vien thu " << i << ": " << endl; s[i].xuat(); s[i].xuatsv(); cout << endl; } } CÂU HỎI, BÀI TẬP 1. Cho biết giá trị của n với đoạn chương trình sau: class A { public: static int i; }; int A::i=5; A a1; a1.i++; A a2; int n=a2.i+1; 2. Cho biết kết quả khi thực hiện chương trình sau: #include <iostream.h> class samp { int a; public: void set_a(int n) { a=n; } int get_a() {return a; } }; void main() { samp ob[4]; int i; for(i=0; i<4; i++) ob[i].set_a(i); for(i=0; i<4; i++) cout<< ob[i].get_a(); cout << “\n”; } 3. Cho biết kết quả khi thực hiện chương trình sau: #include <iostream.h> class samp { int i; 103 public: samp(int n) {i=n; } int get_i() {return i; } }; int sqr_it(samp o) {return o.get_i() * o.get_i(); } void main() { samp a(10), b(2); cout << sqr_it(a) <<”\n”; cout << sqr_it(b) << “\n”; } 4. Cho biết kết quả khi thực hiện chương trình sau: #include <iostream.h> class samp { int i; public: samp(int n) {i=n; } int get_i() {return i; } }; int sqr_it(samp o){ return o.get_i()*o.get_i(); } void main() { samp a(10), b(4); cout << sqr_it(a) <<”\n”; cout << sqr_it(b) << “\n”; } 5. Khai báo lớp phân số(tuso,mauso) gồm các hàm khởi tạo ứng với định nghĩa trong hàm main như sau: void main(){ Phanso a,b(4),c(3,7); } 6. Khai báo lớp phân số (tuso,mauso) ứng với hàm main như sau: void main(){ Phanso a,b(4),c(3,7); cout<< a.tuso<<"/"<<a.mauso; } 7. Khai báo lớp mảng các số nguyên gồm n phần tử. 8. Khai báo lớp ma trận vuông n*n phần tử. 9. Xây dựng các lớp để quản lý thu nhập hàng tháng của 1 cơ quan, biết rằng: Cơ quan có 2 dạng người được hưởng lương: biên chế thì hưởng lương theo quỹ lương của nhà nước, và hợp đồng thì hưởng lương theo số giờ làm việc. Mỗi người trong công ty đều có các thông tin sau: Hoten, số CMND, Phòng ban. Biên chế: có thông tin riêng là Bậc lương Hợp đồng: có thông tin riêng là Số giờ, Tiền công 1 giờ. 104 10. Chương trình cho phép nhập vào 1 danh sách các nhân viên theo biên chế, 1 danh sách các nhân viên theo hợp đồng và cuối cùng in ra lương của từng nhân viên, tổng lương của các nhân viên thuộc dạng biên chế, tổng lương của các nhân viên thuộc dạng hợp đồng. 11. Thiết kế chương trình quản lý các đối tượng sau trong một Viện khoa học: nhà khoa học, nhà quản lý và nhân viên phòng thí nghiệm. Một nhà khoa học cũng có thể làm công tác quản lý. Các thành phần dữ liệu của các đối tượng trên: Nhà khoa học: họ tên, năm sinh, bằng cấp, chức vụ, số bài báo đã công bố, số ngày công trong tháng, bậc lương; Nhà quản lý: họ tên, năm sinh, bằng cấp, chức vụ, số ngày công trong tháng, bậc lương; Nhân viên phòng thí nghiệm: họ tên, năm sinh, bằng cấp, lương trong tháng. 12. Biết rằng nhân viên phòng thí nghiệm lãnh lương khoán, còn lương của nhà khoa học và nhà quản lý bằng số ngày công trong tháng * bậc lương. Nhập, xuất danh sách nhân viên và in tổng lương đã chi trả cho từng loại đối tượng. 13. Thiết kế chương trình quản lý việc nhập/ xuất các ấn phẩm sau trong một nhà sách: băng, đĩa, sách. Các thành phần dữ liệu của các ấn phẩm: Băng: tựa đề, giá mua, thời gian (tính theo phút), nhà sản xuất, số lượng bán, giá bán. Đĩa: tựa đề, giá mua, thời gian (tính theo phút), nhà sản xuất, số lượng bán, giá bán. Sách: tựa đề, giá mua, số trang, nhà xuất bản, số lượng bán, giá bán. Nhập, xuất và tính tổng trị giá bán của từng loại ấn phẩm. 14. Thiết kế chương trình quản lý danh sách các hình vẽ, gồm các loại hình vẽ sau: Hình chữ nhật: tọa độ tâm, màu sắc, chiều rộng và chiều dài. Hình tròn: tọa độ tâm, màu sắc, bán kính. Hình tam giác: tọa độ tâm, màu sắc, chiều dài 3 cạnh. Nhập, xuất danh sách các hình, cho biết số lượng hình và hình có diện tích lớn nhất của từng loại. 105 106 Chương 4 ĐA NĂNG HÓA TOÁN TỬ Thao tác trên các đối tượng của lớp được thực hiện bằng việc gửi các thông điệp tới các đối tượng. Việc gọi hàm này cồng kềnh, do đó nên sử dụng tập các toán tử có sẵn của C++ để chỉ rõ các thao tác của đối tượng. Đây là gọi là đa năng hóa toán tử (operator overloading). Chương này cung cấp cho người đọc những kiến thức về cách định nghĩa, xây dựng toán tử tải bội và một số chú ý. 4.1. GIỚI THIỆU CHUNG 4.1.1. Khái niệm Là khả năng kết hợp một toán tử đã có (+, -, *, /, >, <, ...) với một hàm thành viên và dùng nó với các đối tượng của lớp như là những toán hạng. Ví dụ 4.1: void main(){ Phanso a,b,c,d; c=a.Cong(b); d=c.Nhan(a.Cong(b)) } Ví dụ 4.2: void main(){ Phanso a,b,c,d; c=a+b; d=(a+b)*c; } Có thể nhận thấy đa năng hóa toán tử giúp cho chương trình dễ hiểu và dễ truy tìm lỗi. Thực ra, vấn đề định nghĩa chồng toán tử đã từng có trong C, ví dụ trong biểu thức: a + b. ký hiệu + tuỳ theo kiểu của a và b có thể biểu thị: phép cộng hai số nguyên; phép cộng hai số thực độ chính xác đơn (float); phép cộng hai số thực chính xác đôi (double); phép cộng một số nguyên vào một con trỏ. Trong C++, có thể định nghĩa chồng đối với hầu hết các phép toán (một ngôi hoặc hai ngôi) trên các lớp, nghĩa là một trong số các toán hạng tham gia phép toán là các đối tượng. Đây là một khả năng mạnh vì nó cho phép xây dựng trên các lớp các toán tử cần thiết, làm cho chương trình được viết ngắn gọn dễ đọc hơn và có ý nghĩa hơn. Chẳng hạn, khi định nghĩa một lớp complex để biểu diễn các số phức, có thể viết như sau trong C++: a+b, a-b, a*b, a/b với a, b là các đối tượng complex. Để có được điều này, ta định nghĩa chồng các phép toán +, -, * và / bằng cách định nghĩa hoạt động của từng phép toán giống như định nghĩa một hàm, chỉ khác là đây là hàm toán tử (operator function). Hàm toán tử có tên được ghép bởi từ khoá operator và ký hiệu của phép toán tương ứng. Bảng 4.1 đưa ra một số ví dụ về tên hàm toán tử. Hàm toán tử có thể dùng như là một hàm thành phần của một lớp hoặc là hàm tự do; khi đó hàm toán tử phải được khai báo là bạn của các lớp có các đối tượng mà hàm thao tác. 107 Bảng 4.1: Một số tên hàm toán tử quen thuộc Tên hàm Mục đích operator+ định nghĩa phép + operator* định nghĩa phép nhân * operator/ định nghĩa phép chia / operator+= định nghĩa phép tự cộng += operator!= định nghĩa phép so sánh khác nhau 4.1.2. Cú pháp <kiểu dữ liệu trả về> operator tt(danh sách_đối số) Trong đó: tt là toán tử cần đa năng. Ví dụ 4.3: Cách khai báo trước: Phanso cong(Phanso) Cách khai báo đa năng: Phanso operator+(Phanso) Đặc điểm: Không được phép thay đổi chức năng cơ bản, ý nghĩa nguyên thủy của toán tử hoặc thay đổi thứ tự ưu tiên của chúng. Các toán tử không thể đa năng hóa: . .* :: ?: sizeof 4.1.3. Đa năng hóa toán tử một ngôi (++,--) Các toán tử này được sử dụng theo 2 cách: toán tử đứng trước (prefix) (++a) hay toán tử đứng sau (postfix) (a++) . Khai báo trong lớp: Toán tử đứng trước (prefix): <Kiểu dữ liệu trả về> operator++() Toán tử đứng sau (postfix): <Kiểu dữ liệu trả về> operator++(int) Tham số int được gọi là tham số giả (chỉ định toán tử đứng sau) Định nghĩa bên ngoài lớp: <Kiểu dữ liệu trả về> <Tên_lớp>::operator++() { …. } Ví dụ 4.4: class Phanso{ private: int tu,mau; public: Phanso(int t=0,int m=1); Phanso operator++(); Phanso operator++(int); void xuat(); }; Phanso::Phanso(int t,int m){ tu=t; mau=m; 108 } Phanso Phanso::operator++(){ tu=tu+mau; return *this; } Phanso Phanso::operator++(int){ Phanso temp=*this; tu=tu+mau; return temp; } void Phanso::xuat(){ cout<<tu<<"/"<<mau<<endl; } } void main(){ Phanso a(1,2),b; a++; a.xuat(); ++a; a.xuat(); b=++a; a.xuat(); b.xuat(); b=a++; a.xuat(); b.xuat(); cout<<endl; system("pause"); } 4.1.4. Đa năng hóa toán tử hai ngôi (+,-,*,/,…) Dùng 1 đối số <kiểu dữ liệu trả về> operator<Toán tử>(đối số thứ hai) Dùng 2 đối số bằng cách dùng hàm bạn friend friend <kiểu dữ liệu trả về> operator<Toán tử> ( đối số thứ nhất,đối số thứ hai) Ví dụ 4.5: Xây dựng lớp phân số giải quyết các trường hợp sau: Phanso a(1,2),b(3,2),c; c=a+b; c=a+5; c=5+a; Cách 1: Dùng 1 đối số class PS{ private: int tu,mau; public: PS(int t=0,int m=1){tu=t;mau=m;} PS operator+(PS); 109 }; PS PS:: operator+(PS p){ PS kq; kq.tu=tu*p.mau+p.tu*mau; kq.mau=mau*p.mau; return kq; } void main(){ PS a(1,2),b(1,2),c(3,4),d; d=a+b+c; } Cách 2: Dùng hàm bạn friend class PS{ private: int tu,mau; public: PS(int t=0,int m=1){tu=t;mau=m;} friend PS operator+(PS,PS); }; PS operator+(PS a,PS b){ PS kq; kq.tu=a.tu*b.mau+a.tu*b.mau; kq.mau=a.mau*b.mau; return kq; } void main(){ PS a(1,2),b(1,2),c(3,4),d; d=a+b+c; } 4.1.5. Định nghĩa lại phép gán (=) Định nghĩa lại phép gán “=” để giải quyết các trường hợp sau: Vấn đề con trỏ Null trong cấp phát động; Vấn đề chuyển đổi kiểu. Cú pháp: <Kiểu dữ liệu trả về> operator=(DS_Đối số_nếu có) Ví dụ 4.6: Vấn đề con trỏ Null trong cấp phát động: class String{ private: char *p; public: String(char *s=“”){p=strdup(s);} ~String(){delete p;} }; void main(){ String s1(“Hello”); String s2; s2=s1; 110 } Khi kết thúc chương trình s1 giải phóng vùng nhớ làm cho s2 trỏ vào vùng nhớ NULL. Hình 4.1: Vấn đề con trỏ Giải quyết vấn đề trên như sau: Hình 4.2: Giải quyết vấn đề con trỏ class String{ private: char *p; public: String(char *s=“”){p=strdup(s);} String& operator=(const String&); ~String(){delete p;} }; String& String::operator=(const String &s){ p=strdup(s.p); return (*this); } Ví dụ 4.7: Vấn đề chuyển đổi kiểu: class PS{ private: int tu,mau; public: PS(int t=0,int m=1){ tu=t;mau=m; } }; void main(){ PS a(1,2); PS b=3; } Khi gặp câu lệnh gán như trên, chương trình lần lượt thực hiện các thao tác sau: Có định nghĩa phép gán “=” tương ứng hay không? 111 Hàm khởi tạo chuyển đổi kiểu có hay không? Giải quyết class PS{ private: int tu,mau; public: PS(int t=0,int m=1){ tu=t;mau=m; } PS operator=(int); }; PS PS::operator=(int t){ return PS(t,1); } void main(){ PS a(1,2); PS b=3; } 4.1.6. Đa năng hóa toán tử nhập/xuất (>>,<<) Khái niệm: Là cách thức để các toán tử nhập/xuất (>>,<<) có thể thực hiện được trên các đối tượng người dùng. Cú pháp: Đa năng hóa toán tử xuất:<< friend ostream& operator <<(ostream &,const <tenlop>&) Đa năng hóa toán tử nhập:>> friend istream& operator >>(istream &, <tenlop>&) Ví dụ 4.8: Đa năng hoá toán tử nhập/xuất (>>,<<) cho lớp phân số: class PS{ private: int tu,mau; public: PS(int t=0,int m=1){tu=t;mau=m;} friend ostream& operator <<(ostream &,const PS&); friend istream& operator >>(istream &, PS&); }; ostream& operator <<(ostream & os,const PS& p){ os<<p.tu<< “/ "<<p.mau; return os; } istream& operator >>(istream & is, PS& p){ cout<< "Nhap tu: "; is>>p.tu; cout<< "Nhap mau: "; is>>p.mau; return is; } 112 void main(){ PS a(1,2); PS b; cin>>b; cout<<b; } 4.2. VÍ DỤ TRÊN LỚP SỐ PHỨC 4.2.1. Hàm toán tử là hàm thành phần Trong chương trình đa năng hóa toán tử + giữa hai đối tượng complex được định nghĩa như một hàm thành phần. Hàm toán tử thành phần có một tham số ngầm định là đối tượng gọi hàm nên chỉ có một tham số tường minh. Ví dụ 4.9: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; public: complex(float r=0, float i =0) { real = r; image = i; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } /*hàm operator+ định nghĩa phép toán + hai ngôi trên lớp số phức complex*/ complex operator+(complex b) { complex c; c.real = a.real+b.real; c.image =a.image+b.image; return c; } }; void main() { clrscr(); complex a(-2,5); complex b(3,4); cout<<"Hai so phuc:\n"; a.display(); b.display(); cout<<"Tong hai so phuc:\n"; complex c; c=a+b;//a.operator+(b) c.display(); getch(); } Hai so phuc: 113 -2+j*5 3+j*4 Tong hai so phuc: 1+j*9 Chỉ thị c = a+b; trong ví dụ trên được chương trình dịch hiểu là: c = a.operator+(b); Nhận xét Thực ra cách viết a+b chỉ là một quy ước của chương trình dịch cho phép người sử dụng viết gọn lại, nhờ đó cảm thấy tự nhiên hơn. Hàm toán tử operator+ phải có thuộc tính public vì nếu không chương trình dịch không thể thực hiện được nó ở ngoài phạm vi lớp. Trong lời gọi a.operator+(b), a đóng vai trò của tham số ngầm định của hàm thành phần và b là tham số tường minh. Số tham số tường minh cho hàm toán tử thành phần luôn ít hơn số ngôi của phép toán là 1 vì có một tham số ngầm định là đối tượng gọi hàm toán tử. Chương trình dịch sẽ không thể hiểu được biểu thức 3+a vì cách viết tương ứng 3.operator+(a) không có ý nghĩa. Để giải quyết tình huống này ta dùng hàm bạn để định nghĩa hàm toán tử. 4.2.2. Hàm toán tử là hàm bạn Chương trình sau được phát triển từ chương trình trước bằng cách thêm hàm toán tử cộng thêm một số thực float vào phần thực của một đối tượng complex, được biểu thị bởi phép cộng với số thực float là toán hạng thứ nhất, còn đối tượng complex là toán hạng thứ hai. Trong trường hợp này không thể dùng phép cộng như hàm thành phần vì tham số thứ nhất của hàm toán tử không còn là một đối tượng. Ví dụ 4.10: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; public: complex(float r=0, float i =0) { real = r; image = i; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } /*hàm thành phần operator+ định nghĩa phép toán + hai ngôi trên lớp số phức complex*/ complex operator+(complex b) { cout<<”Goi toi complex::operator+(float, complex)\n”; complex c; c.real = real+b.real; c.image =image+b.image; return c; 114 } /*hàm tự do operator+ định nghĩa phép toán + giữa một số thực và một đối tượng số phức*/ friend complex operator+(float x, complex b); }; complex operator+(float x, complex b) { cout<<”Goi toi operator+(float, complex)\n”; complex c; c.real = x+b.real; c.image = b.image; return c; } void main() { clrscr(); complex a(-2,5); complex b(3,4); cout<<"Hai so phuc:\n"; cout<<"a = "; a.display(); cout<<"b = "; b.display(); cout<<"Tong hai so phuc:\n"; complex c; c=a+b; //a.operator+(b); cout<<"c = "; c.display(); cout<<"Tang them phan thuc cua a 3 don vi\n"; complex d; d=3+a; //operator+(3,a); cout<<"d = "; d.display(); getch(); } Hai so phuc: a = -2+j*5 b = 3+j*4 Tong hai so phuc: Goi toi complex::operator+(complex) c = 1+j*9 Tang them phan thuc cua a 3 don vi Goi toi operator+(float, complex) d = 1+j*5 Trong chương trình trên, biểu thức a+b được chương trình hiểu là lời gọi hàm thành phần a.operator+(b), trong khi đó với biểu thức 3+a, chương trình dịch sẽ thực hiện lời gọi hàm tự do operator+(3,a). Số tham số trong hàm toán tử tự do operator+(...) đúng bằng số ngôi của phép + mà nó định nghĩa. Trong định nghĩa của hàm toán tử tự do, tham số thứ nhất có thể có kiểu bất kỳ chứ không nhất thiết phải có kiểu lớp nào đó. 115 Với một hàm operator+ nào đó chỉ có thể thực hiện được phép + tương ứng giữa hai toán hạng có kiểu như đã được mô tả trong tham số hình thức, nghĩa là muốn có được phép cộng “vạn năng” áp dụng cho mọi kiểu toán hạng ta phải định nghĩa rất nhiều hàm toán tử operator+ (định nghĩa chồng các hàm toán tử). Vấn đề bảo toàn các tính chất tự nhiên của các phép toán không được C++ đề cập, mà nó phụ thuộc vào cách cài đặt cụ thể trong chương trình dịch C++ hoặc bản thân người sử dụng khi định nghĩa các hàm toán tử. Chẳng hạn, phép gán: c = a+b; được chương trình dịch hiểu như là: c = a.operator+(b); trong khi đó với phép gán: d = a+b+c; ngôn ngữ C++ không đưa ra diễn giải nghĩa duy nhất. Một số chương trình biên dịch sẽ tạo ra đối tượng trung gian t: t=a.operator+(b); và d=t.operator+(c); Chương trình trong ví dụ 4.11 sau đây minh hoạ lý giải này: Ví dụ 4.11: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; public: complex(float r=0, float i =0) { cout<<"Tao doi tuong :"<<this<<endl; real = r; image = i; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } complex operator+(complex b) { cout<<"Goi toi complex::operator+(complex)\n"; cout<<this<<endl; complex c; c.real=real+b.real; c.image=image+b.image; return c; } friend complex operator+(float x, complex b); }; complex operator+(float x, complex b) { cout<<"Goi toi operator+(float, complex)\n"; complex c; c.real = x+b.real; c.image = b.image; return c; } void main() { clrscr(); 116 cout<<"so phuc a \n"; complex a(-2,5); cout<<"so phuc b \n"; complex b(3,4); cout<<"Hai so phuc:\n"; cout<<"a = "; a.display(); cout<<"b = "; b.display(); complex c(2,3); cout<<"Cong a+b+c\n"; cout<<"so phuc d \n"; complex d; d = a+b+c; cout<<"a = ";a.display(); cout<<"b = ";b.display(); cout<<"c = ";c.display(); cout<<"d = a+b+c : "; d.display(); getch(); } so phuc a Tao doi tuong :0xffee so phuc b Tao doi tuong :0xffe6 Hai so phuc: a = -2+j*5 b = 3+j*4 Tao doi tuong :0xffde Cong a+b+c so phuc d Tao doi tuong :0xffd6 Goi toi complex::operator+(complex) 0xffee Tao doi tuong :0xffa0 Goi toi complex::operator+(complex) 0xffce Tao doi tuong :0xffa8 a = -2+j*5 b = 3+j*4 c = 2+j*3 d = a+b+c : 3+j*12 Cũng có thể làm như sau: trong định nghĩa của hàm toán tử, ta trả về tham chiếu đến một trong hai đối tượng tham gia biểu thức (chẳng hạn a). Khi đó a+b+c được hiểu là a.operator+(b) và sau đó là a.operator+(c). Tất nhiên, trong trường hợp này nội dung của đối tượng a bị thay đổi sau mỗi phép cộng. Xét chương trình sau: Ví dụ 4.12: #include <iostream.h> #include <conio.h> 117 #include <math.h> class complex { float real, image; public: complex(float r=0, float i =0) { cout<<"Tao doi tuong :"<<this<<endl; real = r; image = i; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } complex & operator+(complex b) { cout<<"Goi toi complex::operator+(complex)\n"; cout<<this<<endl; real+=b.real; image+=b.image; return *this; } friend complex operator+(float x, complex b); }; complex operator+(float x, complex b) { cout<<"Goi toi operator+(float, complex)\n"; complex c; c.real = x+b.real; c.image = b.image; return c; } void main() { clrscr(); cout<<"so phuc a \n"; complex a(-2,5); cout<<"so phuc b \n"; complex b(3,4); cout<<"Hai so phuc:\n"; cout<<"a = "; a.display(); cout<<"b = "; b.display(); cout<<"so phuc c \n"; complex c; c=a+b; //a.operator+(b); cout<<"c = a+b: "; c.display(); cout<<"a = "; a.display(); cout<<"Cong a+b+c\n"; cout<<"so phuc d \n"; complex d; d = a+b+c;//a.operator+(b);a.operator+(c); cout<<"a = ";a.display(); 118 cout<<"b = ";b.display(); cout<<"c = ";c.display(); cout<<"d = a+b+c : "; d.display(); getch(); } so phuc a Tao doi tuong :0xffee so phuc b Tao doi tuong :0xffe6 Hai so phuc: a = -2+j*5 b = 3+j*4 so phuc c Tao doi tuong :0xffde Goi toi complex::operator+(complex) 0xffee c = a+b: 1+j*9 a = 1+j*9 Cong a+b+c so phuc d Tao doi tuong :0xffd6 Goi toi complex::operator+(complex) 0xffee Goi toi complex::operator+(complex) 0xffee a = 5+j*22 b = 3+j*4 c = 1+j*9 d = a+b+c : 5+j*22 Trong hai ví dụ trên, việc truyền các đối số và giá trị trả về của hàm toán tử được thực hiện bằng giá trị. Với các đối tượng có kích thước lớn, người ta thường dùng tham chiếu để truyền đối cho hàm. complex operator+(float , complex &); Tuy nhiên việc dùng tham chiếu như là giá trị trả về của hàm toán tử, có nhiều điều đáng nói. Biểu thức nằm trong lệnh return bắt buộc phải tham chiếu đến một vùng nhớ tồn tại ngay cả khi thực hiện xong biểu thức tức là khi hàm toán tử kết thúc thực hiện. Vùng nhớ ấy có thể là một biến được cấp tĩnh static (các biến toàn cục hay biến cục bộ static), một biến thể hiện (một thành phần dữ liệu) của một đối tượng nào đó ở ngoài hàm. Vấn đề tương tự cũng được đề cập khi giá trị trả về của hàm toán tử là địa chỉ; trong trường hợp này, một đối tượng được tạo ra nhờ cấp phát động trong vùng nhớ heap dùng độc lập với vùng nhớ ngăn xếp dùng để cấp phát biến, đối tượng cục bộ trong chương trình, do vậy vẫn còn lưu lại khi hàm toán tử kết thúc công việc. Hàm toán tử cũng có thể trả về kiểu void khi ảnh hưởng chỉ tác động lên một trong các toán hạng tham gia biểu thức. Xem định nghĩa của hàm đảo dấu số phức trong ví dụ sau: Ví dụ 4.13: #include <iostream.h> #include <conio.h> #include <math.h> class complex { 119 float real, image; public: complex(float r=0, float i =0) { real = r; image = i; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } /*Hàm đảo dấu chỉ tác động lên toán hạng, không sử dụng được trong các biểu thức */ void operator-() { real = -real; image = -image; } complex operator+(complex b) { complex c; c.real=real+b.real; c.image=image+b.image; return c; } friend complex operator+(float x, complex b); }; complex operator+(float x, complex b) { cout<<"Goi toi operator+(float, complex)\n"; complex c; c.real = x+b.real; c.image = b.image; return c; } void main() { clrscr(); cout<<"so phuc a \n"; complex a(-2,5); cout<<"so phuc b \n"; complex b(3,4); cout<<"Hai so phuc:\n"; cout<<"a = "; a.display(); cout<<"b = "; b.display(); complex c; -a; cout<<"a = ";a.display(); getch(); } so phuc a so phuc b Hai so phuc: a = -2+j*5 120 b = 3+j*4 a = 2-j*5 Chú ý: Câu lệnh complex c; c=-a+b sẽ gây lỗi vì -a có giá trị void. 4.3. KHẢ NĂNG VÀ GIỚI HẠN CỦA ĐỊNH NGHĨA CHỒNG TOÁN TỬ Ký hiệu đứng sau từ khoá operator phải là một trong số các ký hiệu toán tử áp dụng cho các kiểu dữ liệu cơ sở, không thể dùng các ký hiệu mới. Một số toán tử không thể định nghĩa chồng (chẳng hạn toán tử truy nhập thành phần cấu trúc“.”, toán tử phạm vi “::”, toán tử điều kiện “? :”) và có một số toán tử ta phải tuân theo các ràng buộc sau: phép =, [] nhất định phải được định nghĩa như hàm thành phần của lớp. phép << và >> dùng với cout và cin phải được định nghĩa như hàm bạn. hai phép toán ++ và -- có thể sử dụng theo hai cách khác nhau ứng với dạng tiền tố ++a, --b và dạng hậu tố a++, b--. Điều này đòi hỏi hai hàm toán tử khác nhau. Các toán tử được định nghĩa chồng phải bảo toàn số ngôi của chính toán tử đó theo cách hiểu thông thường, ví dụ: có thể định nghĩa toán tử “-” một ngôi và hai ngôi trên lớp tương ứng với phép đảo dấu (một ngôi) và phép trừ số học (hai ngôi), nhưng không thể định nghĩa toán tử gán một ngôi, còn ++ lại cho hai ngôi. Nếu làm vậy, chương trình dịch sẽ hiểu là tạo ra một ký hiệu phép toán mới. Khi định nghĩa chồng toán tử, phải tuân theo nguyên tắc là Một trong số các toán hạng phải là đối tượng. Nói cách khác, hàm toán tử phải: hoặc là hàm thành phần, khi đó, hàm đã có một tham số ngầm định có kiểu lớp chính là đối tượng gọi hàm. Tham số ngầm định này đóng vai trò toán hạng đầu tiên (đối với phép toán hai ngôi) hay toán hạng duy nhất (đối với phép toán một ngôi). Do vậy, nếu toán tử là một ngôi thì hàm toán tử thành phần sẽ không chứa một tham số nào khác. Ngược lại khi toán tử là hai ngôi, hàm sẽ có thêm một đối số tường minh. hoặc là một hàm tự do. Trong trường hợp này, ít nhất tham số thứ nhất hoặc tham số thứ hai (nếu có) phải có kiểu lớp. Hơn nữa, mỗi hàm toán tử chỉ có thể áp dụng với kiểu toán hạng nhất định; cần chú ý rằng các tính chất vốn có, chẳng hạn tính giao hoán của toán tử không thể áp dụng một cách tuỳ tiện cho các toán tử được định nghĩa chồng. Ví dụ: a + 3,5 khác với 3,5 + a, ở đây a là một đối tượng complex nào đó. Cần lưu ý rằng không nên định nghĩa những hàm hàm toán tử khác nhau cùng làm những công việc giống nhau vì dễ xảy ra nhập nhằng. Chẳng hạn, đã có một hàm operator+ là một hàm thành phần có tham số là đối tượng complex thì không được định nghĩa thêm một hàm operator+ là một hàm tự do có hai tham số là đối tượng complex. Bảng 4.2: Trường hợp các toán tử ++ và – Hàm cho dạng tiền tố Hàm cho dạng hậu tố operator++() operator++(int) operator--() operator--(int) Lưu ý rằng tham số int trong dạng hậu tố chỉ mang ý nghĩa tượng trưng (dump type) 121 Lựa chọn giữa hàm thành phần và hàm bạn Phải tuân theo các quy tắc sau đây: Lưu ý đến hạn chế của chương trình dịch, xem dạng nào được phép. Nếu đối số đầu tiên là một đối tượng, có thể một trong hai dạng. Ngược lại phải dùng hàm bạn. Trái lại, phải dùng hàm bạn. 4.4. CÁC CHÚ Ý KHI SỬ DỤNG HÀM TOÁN TỬ Về nguyên tắc, định nghĩa chồng một phép toán là khá đơn giản, nhưng việc sử dụng phép toán định nghĩa chồng lại không phải dễ dàng và đòi hỏi phải cân nhắc bởi lẽ nếu bị lạm dụng sẽ làm cho chương trình khó hiểu. Phải làm sao để các phép toán vẫn giữ được ý nghĩa trực quan nguyên thuỷ của chúng. Chẳng hạn không thể định nghĩa cộng “+” như phép trừ “-” hai giá trị. Phải xác định trước ý nghĩa các phép toán trước khi viết định nghĩa của các hàm toán tử tương ứng. Các phép toán một ngôi Các phép toán một ngôi là: *, &, ~, !, ++, --, sizeof (KIỂU) Các hàm toán tử tương ứng chỉ có một đối số và phải trả về giá trị cùng kiểu với toán hạng, riêng sizeof có giá trị trả về kiểu nguyên không dấu và toán tử (kiểu) dùng để trả về một giá trị có kiểu như đã ghi trong dấu ngoặc. Các phép toán hai ngôi Các phép toán hai ngôi như: *,/,%,+,-,<<,>>,<,>,<=,>=,==,!=,&,|,^,&&,|| Hai toán hạng tham gia các phép toán không nhất thiết phải cùng kiểu, mặc dù trong thực tế sử dụng thì thường là như vậy. Như vậy chỉ cần một trong hai đối số của hàm toán tử tương ứng là đối tượng là đủ. Các phép gán Các toán tử gán gồm có: =,+=,-=,*=,/=,%=,>>=,<<=,&=,^=,|= Do các toán tử gán được định nghĩa dưới dạng hàm thành phần, nên chỉ có một tham số tường minh và không có ràng buộc gì về kiểu đối số và kiểu giá trị trả về của các phép gán. Toán tử truy nhập thành phần “->” Phép toán này được dùng để truy xuất các thành phần của một cấu trúc hay một lớp và cần phân biệt với những cách sử dụng khác để tránh dẫn đến sự nhầm lẫn. Có thể định nghĩa phép toán lấy thành phần giống như đối với các phép toán một ngôi. Toán tử truy nhập thành phần theo chỉ số Toán tử lấy thành phần theo chỉ số được dùng để xác định một thành phần cụ thể trong một khối dữ liệu (cấp phát động hay tĩnh). Thông thường phép toán này được dùng với mảng, nhưng cũng có thể định nghĩa lại nó khi làm việc với các kiểu dữ liệu khác. Chẳng hạn với kiểu dữ liệu vector có thể định nghĩa phép lấy theo chỉ số để trả về một thành phần toạ độ nào đó vector. Và phải được định nghĩa như hàm thành phần có một đối số tường minh. Toán tử gọi hàm Đây là một phép toán đặc biệt nhưng nói chung rất khó đưa ra một ví dụ cụ thể. 122 4.5. ĐA NĂNG HÓA MỘT SỐ TOÁN TỬ 4.5.1. Định nghĩa chồng phép gán “ =” Việc định nghĩa chồng phép gán chỉ cần khi các đối tượng có các thành phần dữ liệu động. Chúng ta xét vấn đề này qua phân tích định nghĩa chồng phép gán “=” áp dụng cho lớp vector. Điểm đầu tiên cần lưu ý là hàm operator= nhất thiết phải được định nghĩa như là hàm thành phần của lớp vector. Như vậy hàm operator= sẽ chỉ có một tham số tường minh (toán hạng bên phải dấu =). Giả sử a và b là hai đối tượng thuộc lớp vector, khi đó: a=b; được hiểu là: a.operator=(b); do đó b được truyền cho hàm dưới dạng tham trị hoặc tham chiếu. Việc truyền bằng tham trị đòi hỏi sự có mặt của hàm thiết lập sao chép, hơn thế nữa sẽ làm cho chương trình chạy chậm vì mất thời gian sao chép một lượng lớn dữ liệu. Vì vậy, b sẽ được truyền cho hàm operator= dưới dạng tham chiếu. Giá trị trả về của hàm operator= phụ thuộc vào mục đích sử dụng của biểu thức gán. Chúng ta chọn giải pháp trả về tham chiếu của đối tượng đứng bên trái dấu bằng nhằm giữ hai tính chất quan trọng của biểu thức gán: (i) trật tự kết hợp từ bên phải sang trái, (ii) có thể sử dụng kết quả biểu thức gán trong các biểu thức khác. Ngoài ra giải pháp này cũng hạn chế việc sao chép dữ liệu từ nơi này đi nơi khác trong bộ nhớ. Chúng ta phân biệt hai trường hợp: Trường hợp 1 a=a; Với hai toán hạng là một. Trong trường hợp này hàm operator= không làm gì, ngoài việc trả về tham chiếu đến a. Trường hợp 2 a=b; khi hai đối tượng tham gia biểu thức gán hoàn toàn khác nhau, việc đầu tiên là phải giải phóng vùng nhớ động chiếm giữ trước đó trong a, trước khi xin cấp phát một vùng nhớ động khác bằng kích thước vùng nhớ động có trong b, cuối cùng sao chép nội dung từ vùng nhớ động trong b sang a. Và không quên “sao chép” giá trị của các thành phần “không động” còn lại. Ta xét chương trình minh hoạ. Ví dụ 4.14: #include <iostream.h> #include <conio.h> class vector{ int n; //số toạ độ của vector float *v; //con trỏ tới vùng nhớ toạ độ public: vector(); //hàm thiết lập không tham số vector(int size); //hàm thiết lập 1 tham số vector(int size, float *a); vector(vector &); vector & operator=(vector & b); ~vector(); 123 void display(); }; vector::vector() { int i; cout<<"Tao doi tuong tai "<<this<<endl; cout<<"So chieu :";cin>>n; v= new float [n]; cout<<"Xin cap phat vung bo nho "<<n<<" so thuc tai"<<v<<endl; for(i=0;i<n;i++) { cout<<"Toa do thu "<<i+1<<" : "; cin>>v[i]; } } vector::vector(int size) { int i; cout<<"Su dung ham thiet lap 1 tham so\n"; cout<<"Tao doi tuong tai "<<this<<endl; n=size; cout<<"So chieu :"<<size<<endl; v= new float [n]; cout<<"Xin cap phat vung bo nho "<<n<<" so thuc tai"<<v<<endl; for(i=0;i<n;i++) { cout<<"Toa do thu "<<i+1<<" : "; cin>>v[i]; } } vector::vector(int size,float *a ) { int i; cout<<"Su dung ham thiet lap 2 tham so\n"; cout<<"Tao doi tuong tai "<<this<<endl; n=size; cout<<"So chieu :"<<n<<endl; v= new float [n]; cout<<"Xin cap phat vung bo nho "<<n<<" so thuc tai"<<v<<endl; for(i=0;i<n;i++) v[i] = a[i]; } vector::vector(vector &b) { int i; cout<<"Su dung ham thiet lap sao chep\n"; cout<<"Tao doi tuong tai "<<this<<endl; v= new float [n=b.n]; cout<<"Xin cap phat vung bo nho "<<n<<" so thuc tai"<<v<<endl; for(i=0;i<n;i++) v[i] = b.v[i]; } vector::~vector() { cout<<"Giai phong "<<v<<"cua doi tuong tai"<<this<<endl; 124 delete v; } vector & vector::operator=(vector & b) { cout<<"Goi operator=() cho "<<this<<" va "<<&b<<endl; if (this !=&b){ /*xoá vùng nhớ động đã có trong đối tượng vế trái */ cout<<"xoa vung nho dong"<<v<<" trong "<<this<<endl; delete v; /*cấp phát vùng nhớ mới có kích thước như trong b*/ v=new float [n=b.n]; cout<<"cap phat vung nho dong moi"<<v<<" trong "<<this<<endl; for(int i=0;i<n;i++) v[i]=b.v[i]; } /*khi hai đối tượng giống nhau, không làm gì */ else cout<<"Hai doi tuong la mot\n"; return *this; } void vector::display() { int i; cout<<"Doi tuong tai :"<<this<<endl; cout<<"So chieu :"<<n<<endl; for(i=0;i<n;i++) cout <<v[i] <<" "; cout <<"\n"; } void main() { clrscr(); vector s1;//gọi hàm thiết lập không tham số s1.display(); vector s2 = s1;//gọi hàm thiết lập sao chép s2.display(); vector s3(0); s3=s1;//gọi hàm toán tử vector::operator=(...) s1=s1; getch(); } Tao doi tuong tai 0xfff2 So chieu :3 Xin cap phat vung bo nho 3 so thuc tai0x148c Toa do thu 1 : 2 Toa do thu 2 : 3 Toa do thu 3 : 2 Doi tuong tai :0xfff2 So chieu :3 2 3 2 Su dung ham thiet lap sao chep Tao doi tuong tai 0xffee Xin cap phat vung bo nho 3 so thuc tai0x149c Doi tuong tai :0xffee 125 So chieu :3 2 3 2 Su dung ham thiet lap 1 tham so Tao doi tuong tai 0xffea So chieu :0 Xin cap phat vung bo nho 0 so thuc tai0x14ac Goi operator=() cho 0xffea va 0xfff2 xoa vung nho dong0x14ac trong 0xffea cap phat vung nho dong moi0x14ac trong 0xffea Goi operator=() cho 0xfff2 va 0xfff2 Hai doi tuong la mot 4.5.2. Định nghĩa chồng phép “[]" Xét chương trình sau: Ví dụ 4.15: #include <iostream.h> #include <conio.h> class vector{ int n; //số giá trị float *v; //con trỏ tới vùng nhớ toạ độ public: vector(); //hàm thiết lập không tham số vector(vector &); int length() { return n;} vector & operator=(vector &); float & operator[](int i) { return v[i]; } ~vector(); }; vector::vector() { int i; cout<<"So chieu :";cin>>n; v= new float [n]; } vector::vector(vector &b) { int i; v= new float [n=b.n]; for(i=0;i<n;i++) v[i] = b.v[i]; } vector::~vector() { delete v; } vector & vector::operator=(vector & b){ cout<<"Goi operator=() cho "<<this<<" va "<<&b<<endl; if (this !=&b) { /*xoá vùng nhớ động đã có trong đối tượng vế trái*/ cout<<"xoa vung nho dong"<<v<<" trong "<<this<<endl; 126 delete v; /*cấp phát vùng nhớ mới có kích thước như trong b*/ v=new float [n=b.n]; cout<<"cap phat vung nho dong moi"<<v<<" trong "<<this<<endl; for(int i=0;i<n;i++) v[i]=b.v[i]; } /*khi hai đối tượng giống nhau, không làm gì */ else cout<<"Hai doi tuong la mot\n"; return *this; } void Enter_Vector(vector &s) { for (int i=0; i<s.length();i++) { cout<<"Toa do thu "<<i+1<<" : "; cin>>s[i]; } } void Display_Vector(vector &s) { cout<<"So chieu : "<<s.length()<<endl; for(int i=0; i<s.length(); i++) cout<<s[i]<<" "; cout<<endl; } void main() { clrscr(); cout<<"Tao doi tuong s1\n"; vector s1; /*Nhập các toạ độ cho vector s1*/ cout<<"Nhap cac toa do cua s1\n"; Enter_Vector(s1); cout<<"Thong tin ve vector s1\n"; Display_Vector(s1); vector s2 = s1; cout<<"Thong tin ve vector s2\n"; Display_Vector(s2); getch(); } Tao doi tuong s1 So chieu :4 Nhap cac toa do cua s1 Toa do thu 1 : 2 Toa do thu 2 : 3 Toa do thu 3 : 2 Toa do thu 4 : 3 Thong tin ve vector s1 So chieu : 4 2 3 2 3 Thong tin ve vector s2 So chieu : 4 127 2 3 2 3 Nhận xét: Nhờ giá trị trả về của hàm operator[] là tham chiếu đến một thành phần toạ độ của vùng nhớ động nên ta có thể đọc/ghi các thành phần toạ độ của mỗi đối tượng vector. Như vậy có thể sử dụng các đối tượng vector giống như các biến mảng. Trong ví dụ trên chúng ta cũng không cần đến hàm thành phần vector::display() để in ra các thông tin của các đối tượng. Có thể cải tiến hàm toán tử operator[] bằng cách bổ sung thêm phần kiểm tra tràn chỉ số. 4.5.3. Định nghĩa chồng << và >> Có thể định nghĩa chồng hai toán tử vào/ra << và >> cho phép các đối tượng đứng bên phải chúng khi thực hiện các thao tác vào ra. Chương trình sau đưa ra một cách định nghĩa chồng hai toán tử này. Ví dụ 4.16: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; friend ostream & operator<<(ostream &o, complex &b); friend istream & operator>>(istream &i, complex &b); }; ostream & operator<<(ostream &os, complex &b) { os<<b.real<<(b.image>=0?'+':'-')<<"j*"<<fabs(b.image)<<endl; return os; } istream & operator>>(istream &is, complex &b) { cout<<"Phan thuc : "; is>>b.real; cout<<"Phan ao : "; is>>b.image; return is; } void main() { clrscr(); cout<<"Tao so phuc a\n"; complex a; cin>>a; cout<<"Tao so phuc b\n"; complex b; cin >>b; cout<<"In hai so phuc\n"; cout<<"a = "<<a; cout<<"b = "<<b; getch(); } Tao so phuc a Phan thuc : 3 128 Phan ao : 4 Tao so phuc b Phan thuc : 5 Phan ao : 3 In hai so phuc a = 3+j*4 b = 5+j*3 Nhận xét: Trong chương trình trên, ta không thấy các hàm thiết lập tường minh để gán giá trị cho các đối tượng. Thực tế, việc gán các giá trị cho các đối tượng được đảm nhiệm bởi hàm toán tử operator>>. Việc hiển thị nội dung của các đối tượng số phức có trước đây do hàm thành phần display() đảm nhiệm thì nay đã có thể thay thế nhờ hàm toán tử operator<<. Hai hàm operator<< và operator>> cho phép sử dụng cout và cin cùng lúc với nhiều đối tượng khác nhau: giá trị số nguyên, số thực, xâu ký tự, ký tự và các đối tượng của lớp complex. Có thể thử nghiệm các cách khác để thấy được rằng giải pháp đưa ra trong chương trình trên là tốt nhất. 4.5.4. Định nghĩa chồng các toán tử new và delete Các toán tử new và delete được định nghĩa cho từng lớp và chúng chỉ có ảnh hưởng đối với các lớp liên quan, còn các lớp khác vẫn sử dụng các toán tử new và delete như bình thường. Định nghĩa chồng toán tử new buộc phải sử dụng hàm thành phần và đáp ứng các ràng buộc sau: Có một tham số kiểu size_t ( trong tệp tiêu đề stddef.h). Tham số này tương ứng với kích thước (tính theo byte) của đối tượng xin cấp phát. Lưu ý rằng đây là tham số giả (dump argument) vì nó sẽ không được mô tả khi gọi tới toán tử new, mà do chương trình biên dịch tự động tính dựa trên kích thước của đối tượng liên đới. Trả về một giá trị kiểu void * tương ứng với địa chỉ vùng nhớ động được cấp phát. Khi định nghĩa chồng toán delete ta phải sử dụng hàm thành phần, tuân theo các quy tắc sau đây: Nhận một tham số kiểu con trỏ tới lớp tương ứng; con trỏ này mang địa chỉ vùng nhớ động đã được cấp phát cần giải phóng, Không có giá trị trả về (trả về void) Nhận xét: Có thể gọi được các toán tử new và delete chuẩn (ngay cả khi chúng đã được định nghĩa chồng) thông qua toán tử phạm vi. Các toán tử new và delete là các hàm thành phần static của các lớp bởi vì chúng không có tham số ngầm định. Sau đây giới thiệu ví dụ định nghĩa chồng các toán tử new và delete trên lớp point. Ví dụ cũng chỉ ra cách gọi lại các toán tử new và delete truyền thống. Ví dụ 4.17: #include <iostream.h> #include <stddef.h> #include <conio.h> class point { static int npt;/*số điểm tĩnh*/ 129 static int npt_dyn;/*số điểm động*/ int x, y; public: point(int ox=0, int oy = 0) { x = ox; y = oy; npt++; cout<<"++Tong so diem : "<<npt<<endl; } ~point () { npt--; cout<<"--Tong so diem : "<<npt<<endl; } void * operator new (size_t sz) { npt_dyn++; cout<<" Co "<<npt_dyn<<" diem dong "<<endl; return ::new char [sz]; } void operator delete (void *dp) { npt_dyn--; cout<<" Co "<<npt_dyn<<" diem dong "<<endl; ::delete (dp); } }; int point::npt = 0; int point::npt_dyn = 0; void main() { clrscr(); point * p1, *p2; point a(3,5); p1 = new point(1,3); point b; p2 = new point (2,0); delete p1; point c(2); delete p2; getch(); } ++Tong Co ++Tong ++Tong Co ++Tong --Tong Co ++Tong --Tong Co 130 so diem : 1 1 diem dong so diem : 2 so diem : 3 2 diem dong so diem : 4 so diem : 3 1 diem dong so diem : 4 so diem : 3 0 diem dong Nhận xét: Dù cho new có được định nghĩa chồng hay không, lời gọi tới new luôn luôn cần đến các hàm thiết lập. 4.5.5. Phép nhân ma trận véc tơ Chương trình trong ví dụ 4.18 sau đây được cải tiến từ ví dụ trước đó trong đó hàm prod() được thay thế bởi operator*. Ví dụ 4.18: #include <iostream.h> #include <conio.h> class matrix;/*khai báo trước lớp matrix*/ /*lớp vector*/ class vector{ static int n; //số chiều của vector float *v; //vùng nhớ chứa các toạ độ public: vector(); vector(float *); vector(vector &);//hàm thiết lập sao chép ~vector(); void display(); static int & Size() {return n;} friend vector operator*(matrix &, vector &); friend class matrix; }; int vector::n = 0; /*các hàm thành phần của lớp vector*/ vector::vector() { int i; v= new float [n]; for(i=0;i<n;i++) { cout<<"Toa do thu "<<i+1<<" : "; cin>>v[i]; } } vector::vector(float *a) { for(int i =0; i<n; i++) v[i]=a[i]; } vector::vector(vector &b){ int i; for(i=0;i<n;i++) v[i] = b.v[i]; } vector::~vector(){ delete v; } void vector::display() //hiển thị kết qu { 131 for(int i=0;i<n;i++) cout <<v[i] <<" "; cout <<"\n"; } /* lớp matrix*/ class matrix { static int n; //số chiều của vector vector *m; //vùng nhớ chứa các toạ độ public: matrix(); matrix(matrix &);//hàm thiết lập sao chép ~matrix(); void display(); static int &Size() {return n;} friend vector operator*(matrix &, vector &); }; int matrix::n =0; /*hàm thành phần của lớp matrix*/ matrix::matrix(){ int i; m= new vector [n]; } matrix::matrix(matrix &b) { int i,j; m= new vector[n]; for (i=0; i<n; i++) for (j=0; j<n; j++) m[i].v[j]=b.m[i].v[j]; } matrix::~matrix() { delete m; } void matrix::display() { for (int i=0; i<n; i++) m[i].display(); } /*hàm toán tử*/ vector operator*(matrix &m,vector &v) { float *a = new float [vector::Size()]; int i,j; for (i=0; i<matrix::Size(); i++) { a[i]=0; for(j=0; j<vector::Size(); j++) a[i]+=m.m[i].v[j]*v.v[j]; } return vector(a); } void main() { clrscr(); 132 int size; cout<<"Kich thuoc cua vector "; cin>>size; vector::Size() = size; matrix::Size() = size; cout<<"Tao mot vector \n"; vector v; cout<<" v= \n"; v.display(); cout<<"Tao mot ma tran \n"; matrix m; cout<<" m = \n"; m.display(); cout<<"Tich m*v \n"; vector u = m*v;/* opertaor*(m,v) */ u.display(); getch(); } Kich thuoc cua vector 4 Tao mot vector Toa do thu 1 : 1 Toa do thu 2 : 2 Toa do thu 3 : 3 Toa do thu 4 : 4 v= 1 2 3 4 Tao mot ma tran Toa do thu 1 : 3 Toa do thu 2 : 2 Toa do thu 3 : 1 Toa do thu 4 : 2 Toa do thu 1 : 3 Toa do thu 2 : 2 Toa do thu 3 : 3 Toa do thu 4 : 2 Toa do thu 1 : 3 Toa do thu 2 : 2 Toa do thu 3 : 3 Toa do thu 4 : 2 Toa do thu 1 : 2 Toa do thu 2 : 3 Toa do thu 3 : 2 Toa do thu 4 : 3 m = 3 2 1 2 3 2 3 2 3 2 3 2 2 3 2 3 Tich m*v 133 18 24 24 26 4.6. CHUYỂN ĐỔI KIỂU Với các kiểu dữ liệu chuẩn, ta có thể thực hiện các phép chuyển kiểu ngầm định, chẳng hạn có thể gán một giá trị int vào một biến long, hoặc cộng giá trị long vào một biến float. Thường có hai kiểu chuyển kiểu: chuyển kiểu ngầm định (tự động) và chuyển kiểu tường minh (ép kiểu). Phép chuyển kiểu ngầm định được thực hiện bởi chương trình biên dịch. Phép chuyển kiểu tường minh xảy ra khi sử dụng phép ép kiểu bắt buộc. Phép ép kiểu thường được dùng trong các câu lệnh gọi hàm để gửi các tham số có kiểu khác với các tham số hình thức tương ứng. Các kiểu lớp không thể thoải mái chuyển sang các kiểu khác được mà phải do người tự làm lấy. C++ cũng cung cấp cách thức định nghĩa phép chuyển kiểu ngầm định và tường minh. Phép chuyển kiểu ngầm định được định nghĩa bằng một hàm thiết lập chuyển kiểu (conversion constructor), còn phép chuyển kiểu tường minh được xác định thông qua toán tử chuyển kiểu hoặc ép kiểu (cast operator). Phép chuyển kiểu ngầm định được định nghĩa thông qua một hàm thiết lập chuyển kiểu cho lớp. Với đối số có kiểu cần phải chuyển thành một đối tượng của lớp đó. Tham số này có thể có kiểu cơ sở hay là một đối tượng thuộc lớp khác. Hàm thiết lập một tham số trong lớp point trong các chương trình ở chương trước là ví dụ cho hàm thiết lập chuyển kiểu. Trong câu lệnh point p=2; đã chuyển kiểu từ giá trị nguyên 2 sang một đối tượng point. Thực tế ở đây chương trình dịch gọi tới hàm thiết lập một tham số. Đây là sự chuyển kiểu một chiều, nhận giá trị hoặc đối tượng nào đó và chuyển nó thành đối tượng của lớp. Các hàm thiết lập chuyển kiểu không thể sử dụng để chuyển các đối tượng của lớp mình sang các kiểu khác và chúng chỉ có thể được sử dụng trong các phép gán và phép khởi tạo giá trị. Tuy nhiên, các toán tử chuyển kiểu có thể được dùng để chuyển các đối tượng sang các kiểu khác và cũng có thể được dùng cho các mục đích khác ngoài phép gán và khởi tạo giá trị. C++ qui định rằng một hàm toán tử chuyển kiểu như thế buộc phải là hàm thành phần của lớp liên quan và không có tham số hoặc kiểu trả về. Tên của nó được cho theo dạng như sau: operator type(); trong đó type là tên của kiểu dữ liệu mà một đối tượng sẽ được chuyển sang; có thể là kiểu dữ liệu cơ sở (khi đó ta phải chuyển kiểu từ đối tượng sang kiểu cơ sở) hay một kiểu lớp khác (khi đó ta phải chuyển kiểu từ đối tượng lớp này sang lớp khác). 4.6.1. Hàm toán tử chuyển kiểu ép buộc Chương trình ví dụ 4.19 sau đây minh hoạ cách cài đặt các hàm toán tử chuyển kiểu ngầm định và chuyển kiểu từ lớp complex sang một số thực. Vấn đề chuyển kiểu từ lớp này sang lớp khác sẽ được giới thiệu sau. Ví dụ 4.19: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; public: complex( ) { real = 0; image = 0; } 134 /*hàm thiết lập đóng vai trò một hàm toán tử chuyển kiểu tự động*/ complex(float r) { real = r; image = 0; } complex(float r, float i ) { real = r; image = i; } /*Hàm toán tử chuyển kiểu ép buộc*/ operator float() { return real; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } /*hàm operator+ định nghĩa phép toán + hai ngôi trên lớp số phức complex*/ complex operator+(complex b) { complex c; c.real = real+b.real; c.image =image+b.image; return c; } }; void main() { clrscr(); cout<<"a = "; complex a(-2,5); a.display(); cout<<"b = "; complex b(3,4); b.display(); cout<<"c = a+b : "; complex c; c=a+b;//a.operator+(b) c.display(); cout<<"d = 3+c : "; complex d; d= complex(3)+c; d.display(); cout<<"float x = a : "; float x = a;//float(complex) cout<<x<<endl; getch(); } a = -2+j*5 b = 3+j*4 c = a+b : 1+j*9 135 d = 3+c : 4+j*9 float x = a : -2 Chú ý: Phép cộng 3+c khác với complex(3)+c vì trong phép thứ nhất người ta thực hiện chuyển đổi c thành số thực và phép cộng đó thực hiện giữa hai số thực. Có thể thử nghiệm để kiểm tra lại kết quả sau: Đoạn chương trình d = 3 + c; d.display() cho ra kết quả 4+j*0 Các phép toán nguyên thuỷ có độ ưu tiên hơn so với các phép toán được định nghĩa chồng. 4.6.2. Hàm toán tử chuyển kiểu trong lời gọi hàm Trong chương trình dưới đây ta định nghĩa hàm fct() với một tham số thực (float) và sẽ gọi hàm này hai lần: lần thứ nhất với một tham số thực, lần thứ hai với một tham số kiểu complex. Trong lớp complex còn có một hàm thiết lập sao chép, không được gọi khi ta truyền đối tượng complex cho hàm fct() vì ở đây xảy ra sự chuyển đổi kiểu dữ liệu. Ví dụ 4.20: #include <iostream.h> #include <conio.h> class complex { float real, image; public: complex(float r, float i ) { real = r; image = i; } complex(complex &b ) { cout<<”Ham thiet lap sao chep\n”; real = b.r; image = b.i; } /*Hàm toán tử chuyển kiểu ép buộc*/ operator float() { cout<<"Goi float() cho complex\n"; return real; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } }; void fct(float n) { cout<<"Goi fct voi tham so : "<<n<<endl; } void main() { clrscr(); complex a(3,4); fct(6);//lời gọi hàm thông thường fct(a);//lời gọi hàm có xảy ra chuyển đổi kiểu dữ liệu getch(); 136 } Goi fct voi tham so : 6 Goi float() cho complex Goi fct voi tham so : 3 Trong chương trình này, lời gọi hàm fct(a) đã được chương trình dịch chuyển thành các thao tác: chuyển đổi đối tượng thành float, lời gọi hàm fct() với tham số là giá trị thu được sau chuyển đổi. Sự chuyển đổi được thực hiện khi gọi hàm do đó không xảy ra việc sao chép lại đối tượng a. 4.6.3. Hàm toán tử chuyển kiểu trong biểu thức Chương trình dưới đây cho ta biết biểu thức dạng a+b hoặc a+3 được tính như thế nào với a, b là các đối tượng kiểu complex. Ví dụ 4.21: #include <iostream.h> #include <conio.h> class complex { float real, image; public: complex(float r, float i ) { real = r; image = i; } /*Hàm toán tử chuyển kiểu ép buộc*/ operator float() { cout<<"Goi float() cho complex\n"; return real; } void display() { cout<<real<<(image>=0?'+':'-')<<"j*"<<fabs(image)<<endl; } }; void main() { clrscr(); complex a(3,4); complex b(5,7); float n1, n2; n1 = a+3; cout<<"n1 = "<<n1<<endl; n2 = a + b;cout<<"n2 = "<<n2<<endl; double z1, z2; z1 = a+3; cout<<"z1 = "<<z1<<endl; z2 = a + b;cout<<"z2 = "<<z2<<endl; getch(); } Goi float() cho complex n1 = 6 137 Goi float() cho complex Goi float() cho complex n2 = 8 Goi float() cho complex z1 = 6 Goi float() cho complex Goi float() cho complex z2 = 8 Khi gặp biểu thức dạng a+3 với phép toán + được định nghĩa với các toán hạng có kiểu lớp complex và số thực, chương trình dịch trước hết đi tìm xem đã có một toán tử + được định nghĩa chồng tương ứng với các kiểu dữ liệu của các toán hạng này hay chưa. Trong trường hợp này vì không có, nên chương trình dịch sẽ chuyển đổi kiểu dữ liệu của các toán hạng để phù hợp với một trong số các phép toán đã định nghĩa, cụ thể là chuyển đổi từ đối tượng a sang float. 4.6.4. Hàm toán tử chuyển đổi kiểu cơ sở sang kiểu lớp Trở lại chương trình về số phức complex ở trên, ta có thể thực hiện các chỉ thị kiểu như: complex e=10; hoặc a=1; Chỉ thị thứ nhất nhằm tạo một đối tượng tạm thời có kiểu complex tương ứng với phần thực bằng 10, phần ảo bằng 0 rồi sao chép sang đối tượng e mới được khai báo. Trong chỉ thị thứ hai, cũng có một đối tượng tạm thời kiểu complex được tạo ra và nội dung của nó (phần thực 1, phần ảo 0) được gán cho a. Như vậy, trong cả hai trường hợp đều phải gọi tới hàm thiết lập một tham số của lớp complex. Tương tự, nếu có hàm fct() với khai báo: fct(complex) thì lời gọi fct(4) sẽ đòi hỏi phải chuyển đổi từ giá trị nguyên 4 thành một đối tượng tạm thời có kiểu complex, để truyền cho fct(). Sau đây là chương trình nhận được do sửa đổi từ chương trình về lớp số phức complex đã trình bày ở các ví dụ trước. Ví dụ 4.22: #include <iostream.h> #include <conio.h> class complex { float real, image; public: complex(float r) { cout<<"Ham thiet lap dong vai tro cua ham toan tu chuyen kieu ngam dinh\n"; real = r; image = 0; } complex(float r, float i ) { cout<<"Ham thiet lap \n"; real = r; image = i; } complex(complex &b) { 138 cout<<"Ham thiet lap sao "<<this<<endl; real = b.real; image = b.image; } }; void fct(complex p) { cout<<"Goi fct \n"; } void main() { clrscr(); complex a(3,4); a = complex(12); a = 12; fct(4); getch(); } Ham Ham Ham Ham Goi thiet thiet thiet thiet fct chep lai "<<&b<<" Sang lap lap dong vai tro cua ham toan tu chuyen kieu ngam dinh lap dong vai tro cua ham toan tu chuyen kieu ngam dinh lap dong vai tro cua ham toan tu chuyen kieu ngam dinh 4.6.5. Hàm thiết lập trong các chuyển đổi kiểu liên tiếp Trong lớp số phức complex hàm thiết lập với một tham số cho phép thực hiện chuyển đổi float -->complex đồng thời cả chuyển đổi ngầm định. int --> float. tức là nó có thể cho phép một chuỗi các chuyển đổi: int --> float -->complex Chẳng hạn khi gặp phép gán kiểu như: a = 2; Cần chú ý khả năng chuyển đổi này phải dựa trên các quy tắc chuyển đổi thông thường như đã nói ở trên. 4.6.6. Lựa chọn giữa hàm thiết lập và phép toán gán Với chỉ thị gán: a=12; trong chương trình về số phức complex không có định nghĩa toán tử gán một số nguyên cho một đối tượng complex. Giả sử có một toán tử gán như vậy thì có thể sẽ xảy ra xung đột giữa: chuyển đổi float-->complex bởi hàm thiết lập và phép gán complex-->complex. sử dụng trực tiếp toán tử gán float-->complex. Để giải quyết vấn đề này ta tuân theo quy tắc: “Các chuyển đổi do người sử dụng định nghĩa chỉ được thực hiện khi cần thiết”, nghĩa là với chỉ thị gán: a = 12; nếu như trong lớp complex có định nghĩa toán tử gán, nó sẽ được ưu tiên thực hiện. Chương trình sau đây minh hoạ nhận xét này: Ví dụ 4.23: #include <iostream.h> #include <conio.h> 139 class complex { float real, image; public: complex(float r) { cout<<"Ham thiet lap dong vai tro cua ham toan tu chuyen kieu ngam dinh\n"; real = r; image = 0; } complex(float r, float i ) { cout<<"Ham thiet lap \n"; real = r; image = i; } complex & operator=(complex &p) { real = p.real;image = p.image; cout<<"gan complex -->complex tu "<<&p<<" sang "<<this<<endl; return *this; } complex & operator=(float n) { real = n;image = 0; cout<<"gan float -->complex "<<endl; return *this; } }; void main() { clrscr(); complex a(3,4); a = 12; getch(); } Ham thiet lap gan float -->complex 4.6.7. Sử dụng hàm thiết lập để mở rộng ý nghĩa một phép toán Ta xét lớp complex và hàm thiết lập một tham số của lớp được bổ sung thêm một hàm toán tử dưới dạng hàm bạn (trường hợp này không nên sử dụng hàm toán tử thành phần). Với các điều kiện này, khi a là một đối tượng kiểu complex, biểu thức kiểu như: a + 3 sẽ có ý nghĩa. Thực vậy trong trường hợp này chương trình dịch sẽ thực hiện các thao tác sau: chuyển đổi từ số thực 3 sang số phức complex. cộng giữa đối tượng nhận được với a bằng cách gọi hàm toán tử operator+. Kết quả sẽ là một đối tượng kiểu complex. Nói cách khác, biểu thức a + 3 tương đương với operator+(a, point(3)). Tương tự, biểu thức 5 + a sẽ tương đương với: operator+(point(5),a). 140 Tuy nhiên trường hợp sau sẽ không còn đúng khi operator+ là hàm toán tử thành phần. Sau đây là một chương trình minh hoạ các khả năng mà chúng ta vừa đề cập. Ví dụ 4.24: #include <iostream.h> #include <conio.h> #include <math.h> class complex { float real, image; public: complex(float r) { cout<<"Ham thiet lap dong vai tro cua ham toan tu chuyen kieu ngam dinh\n"; real = r; image = 0; } complex(float r, float i ) { cout<<"Ham thiet lap 2 tham so\n"; real = r; image = i; } void display() { cout<<real<<(image>=0?"+j*":"-j*")<<fabs(image)<<endl; } friend complex operator+(complex , complex); }; complex operator+(complex a, complex b) { complex c(0,0); c.real = a.real + b.real; c.image = a.image + b.image; return c; } void main() { clrscr(); complex a(3,4),b(9,4); a = b + 5;a.display(); a = 2 + b;a.display(); getch(); } Ham thiet lap 2 tham so Ham thiet lap 2 tham so Ham thiet lap dong vai tro cua ham toan tu chuyen kieu ngam dinh Ham thiet lap 2 tham so 14+j*4 Ham thiet lap dong vai tro cua ham toan tu chuyen kieu ngam dinh Ham thiet lap 2 tham so 11+j*4 Nhận xét: Hàm thiết lập làm nhiệm vụ của hàm toán tử chuyển đổi kiểu cơ sở sang kiểu lớp không nhất thiết chỉ có một tham số hình thức. Trong trường hợp hàm thiết lập có nhiều tham số hơn, các tham số tính từ tham số thứ hai phải có giá trị ngầm định. 141 4.7. CHUYỂN ĐỔI KIỂU TỪ LỚP NÀY SANG MỘT LỚP KHÁC Khả năng chuyển đổi qua lại giữa kiểu cơ sở và một kiểu lớp có thể được mở rộng cho hai kiểu lớp khác nhau: Trong lớp A, ta có thể định nghĩa một hàm toán tử để thực hiện chuyển đổi từ kiểu A sang kiểu B (cast operator). Hàm thiết lập của lớp A chỉ với một tham số kiểu B sẽ thực hiện chuyển đổi kiểu từ B sang A. 4.7.1. Hàm toán tử chuyển kiểu bắt buộc Ví dụ sau đây minh hoạ khả năng dùng hàm toán tử complex của lớp point cho phép thực hiện chuyển đổi một đối tượng kiểu point thành một đối tượng kiểu complex. Ví dụ 4.25: #include <iostream.h> #include <conio.h> #include <math.h> class complex;//khai báo trước lớp complex class point { int x, y; public: point(int ox = 0, int oy =0) {x = ox; y = oy;} operator complex();//chuyển đổi point-->complex }; class complex { float real, image; public: complex(float r=0, float i=0 ) { real = r; image = i; } friend point::operator complex(); void display() { cout<<real<<(image>=0?"+j*":"-j*")<<fabs(image)<<endl; } }; point::operator complex() { complex r(x,y); cout<<"Chuyen doi "<<x<<" "<<y <<" thanh "<<r.real<<" + "<< r.image<<endl; return r; } void main() { clrscr(); point a(2,5); complex c; c = (complex) a; c.display(); point b(9,12); c = b; c.display(); getch(); } 142 Chuyen doi 2 5 thanh 2 + 5 2+j*5 Chuyen doi 9 12 thanh 9 + 12 9+j*12 4.7.2. Hàm thiết lập dùng làm hàm toán tử Chương trình sau đây minh hoạ khả năng dùng hàm thiết lập complex(point) biểu để thực hiện chuyển đổi một đối tượng kiểu point thành một đối tượng kiểu complex. Ví dụ 4.26 #include <iostream.h> #include <conio.h> #include <math.h> class point;//khai báo trước lớp complex class complex { float real, image; public: complex(float r=0, float i=0 ) { real = r; image = i; } complex(point); void display() { cout<<real<<(image>=0?"+j*":"-j*")<<fabs(image)<<endl; } }; class point { int x, y; public: point(int ox = 0, int oy =0) {x = ox; y = oy;} friend complex::complex(point); }; complex::complex(point p) { real= p.x; image = p.y; } void main() { clrscr(); point a(3,5); complex c = a; c.display(); getch(); } 3+j*5 4.8. MỘT SỐ VÍ DỤ 4.8.1. Thực hiện đa năng hóa toán tử [] để truy cập đến một phần tử của vector #include <iostream.h> class Vector { private: int Size; int *Data; 143 public: Vector(int S=2,int V=0); ~Vector(); void Print() const; int & operator [] (int I); }; Vector::Vector(int S,int V) { Size = S; Data=new int[Size]; for(int I=0;I<Size;++I) Data[I]=V; } Vector::~Vector() { delete []Data; } void Vector::Print() const { cout<<"Vector:("; for(int I=0;I<Size-1;++I) cout<<Data[I]<<","; cout<<Data[Size-1]<<")"<<endl; } int & Vector::operator [](int I) { return Data[I]; } int main() { Vector V(5,1); V.Print(); for(int I=0;I<5;++I) V[I]*=(I+1); V.Print(); V[0]=10; V.Print(); return 0; } 4.8.2. Thực hiện đa năng hóa toán tử () để truy cập đến một phần tử của vector #include <iostream.h> class Vector { private: int Size; int *Data; public: Vector(int S=2,int V=0); ~Vector(); void Print() const; int & operator () (int I); }; 144 Vector::Vector(int S,int V) { Size = S; Data=new int[Size]; for(int I=0;I<Size;++I) Data[I]=V; } Vector::~Vector() { delete []Data; } void Vector::Print() const { cout<<"Vector:("; for(int I=0;I<Size-1;++I) cout<<Data[I]<<","; cout<<Data[Size-1]<<")"<<endl; } int & Vector::operator ()(int I) { return Data[I]; } int main() { Vector V(5,1); V.Print(); for(int I=0;I<5;++I) V(I)*=(I+1); V.Print(); V(0)=10; V.Print(); return 0; } 4.8.3. Thực hiện đa năng hóa toán tử () để truy cập đến một phần tử của ma trận #include <iostream.h> class Matrix { private: int Rows,Cols; int **Data; public: Matrix(int R=2,int C=2,int V=0); ~Matrix(); void Print() const; int & operator () (int R,int C); }; Matrix::Matrix(int R,int C,int V) { 145 int I,J; Rows=R; Cols=C; Data = new int *[Rows]; int *Temp=new int[Rows*Cols]; for(I=0;I<Rows;++I) { Data[I]=Temp; Temp+=Cols; } for(I=0;I<Rows;++I) for(J=0;J<Cols;++J) Data[I][J]=V; } Matrix::~Matrix() { delete [] Data[0]; delete [] Data; } void Matrix::Print() const { int I,J; for(I=0;I<Rows;++I) { for(J=0;J<Cols;++J) { cout.width(5);//Hien thi canh ler phai voi chieu dai 5 ky tu cout<<Data[I][J]; } cout<<endl; } } int & Matrix::operator () (int R,int C) { return Data[R][C]; } int main() { int I,J; Matrix M(2,3,1); cout<<"Matrix:"<<endl; M.Print(); for(I=0;I<2;++I) for(J=0;J<3;++J) M(I,J)*=(I+J+1); cout<<"Matrix:"<<endl; M.Print(); return 0; } 146 4.8.4. Thực hiện đa năng hóa toán tử ++ và - #include <iostream.h> class Point { private: int X,Y; public: Point(int A=0,int B=0); Point operator ++(); Point operator --(); void Print() const; }; Point::Point(int A,int B) { X = A; Y = B; } Point Point::operator++() { ++X; ++Y; return *this; } Point Point::operator--() { --X; --Y; return *this; } void Point::Print() const { cout<<"X="<<X<<",Y="<<Y<<endl; } int main() { Point P1(2,6),P2(5,8); cout<<"Point 1:"; P1.Print(); cout<<"Point 2:"; P2.Print(); ++P1; --P2; cout<<"Point 1:"; P1.Print(); cout<<"Point 2:"; P2.Print(); return 0; } 147 CÂU HỎI, BÀI TẬP 1. Xây dựng lớp String để thực hiện các thao tác trên các chuỗi, trong lớp này có các phép toán: Phép toán + để nối hai chuỗi lại với nhau. Phép toán = để gán một chuỗi cho một chuỗi khác. Phép toán [] truy cập đến một ký tự trong chuỗi. Các phép toán so sánh: ==, !=, >, >=, <, <= 2. Xây dựng lớp ma trận Matrix gồm các phép toán cộng, trừ và nhân hai ma trận bất kỳ. 3. Xây dựng lớp Rational chứa các số hữu tỷ gồm các phép toán +, - , *, /, ==, !=, >, >=, <, <=. 4. Xây dựng lớp Time để lưu trữ giờ, phút, giây gồm các phép toán: Phép cộng giữa dữ liệu thời gian và một số nguyên là số giây, kết quả là một dữ liệu thời gian. Phép trừ giữa hai dữ liệu thời gian, kết quả là một số nguyên chính là số giây. ++ và – để tăng hay giảm thời gian xuống một giây. Các phép so sánh. 5. Xây dựng lớp Date để lưu trữ ngày, tháng, năm gồm các phép toán: Phép cộng giữa dữ liệu Date và một số nguyên là số ngày, kết quả là một dữ liệu Date. Phép trừ giữa hai dữ liệu Date, kết quả là một số nguyên chính là số ngày. ++ và – để tăng hay giảm thời gian xuống một ngày. Các phép so sánh. 148 Chương 5 TÍNH KẾ THỪA VÀ ĐA HÌNH Một trong những khái niệm quan trọng nhất trong lập trình hướng đối tượng là tính kế thừa (inheritance). Tính kế thừa cho phép chúng ta định nghĩa một lớp trong điều kiện một lớp khác, mà làm cho nó dễ dàng hơn để tạo và duy trì một ứng dụng. Điều này cũng cung cấp một cơ hội để tái sử dụng và thời gian thực thi viết chương trình nhanh hơn. 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 trong lập trình hướng đối tượng là sự tái sử dụng các lớp có các đặc tính chung với nhau để tạo ra các lớp mới từ một hay nhiều lớp đã có. Ưu điểm: Tái sử dụng chương trình đã có Cho phép tạo ra các thư viện lớp (là tập hợp dữ liệu và hàm được đóng gói thành các lớp, ví dụ: thư viện math.h, string.h…) Thành phần kế thừa Lớp kế thừa sẽ kế thừa: Thành phần dữ liệu không thuộc private của lớp được kế thừa. Được quyền truy xuất các hàm thành viên không thuộc private của lớp được kế thừa. Ví dụ 5.1: Xét về bản chất: NV_VANPHONG và NV_SANXUAT đều là nhân viên nên nó phải có các thuộc tính chung: MaNV, Hoten, CMND,… của một người nhân viên. Hình 5.1: Sơ đồ kế thừa lớp NHANVIEN Phân loại 149 Kế thừa đơn: Hình 5.2: Kế thừa đơn Đa kế thừa: Hình 5.3: Đa kế thừa 5.2. KẾ THỪA ĐƠN Định nghĩa Kế thừa đơn là tiến trình tạo ra một lớp mới từ một lớp đã có. Phân loại: Kế thừa đơn một cấp, kế thừa đơn nhiều cấp (đa tầng) Hình 5.4: Kế thừa nhiều cấp và kế thừa một cấp Lưu ý: trong kế thừa đơn nhiều cấp ta cần phân biệt lớp cơ bản trực tiếp (Lớp A) và lớp cơ bản gián tiếp (lớp B) => Lớp cơ bản trực tiếp: là lớp có tên trong khai báo của lớp dẫn xuất. 150 Cú pháp: class Coban{ … }; class Danxuat: <từ khóa chỉ định kiểu kế thừa> Coban{ … }; Trong đó: <từ khóa chỉ định kiểu kế thừa>: có thể là public, protected hoặc private Lưu ý: khi không có từ khoá chỉ định thì mặc định kiểu kế thừa là private. Ví dụ 5.2: class HINH{ private: int mau; }; class HCN:public HINH{ private: int dai,rong; }; Kiểu kế thừa: Kiểu kế thừa public: Trong dẫn xuất public, các thành phần, các hàm bạn và các đối tượng của lớp dẫn xuất không thể truy nhập đến các thành phần private của lớp cơ sở. Các thành phần protected trong lớp cơ sở trở thành các thành phần private trong lớp dẫn xuất. Các thành phần public của lớp cơ sở vẫn là public trong lớp dẫn xuất. Kiểu kế thừa private: Trong trường hợp này, các thành phần public trong lớp cơ sở không thể truy nhập được từ các đối tượng của lớp dẫn xuất, nghĩa là chúng trở thành các thành phần private trong lớp dẫn xuất. Các thành phần protected trong lớp cơ sở có thể truy nhập được từ các hàm thành phần và các hàm bạn của lớp dẫn xuất. Dẫn xuất private được sử dụng trong một số tình huống đặc biệt khi lớp dẫn xuất không khai báo thêm các thành phần hàm mới mà chỉ định nghĩa lại các phương thức đã có trong lớp cơ sở. Kiểu kế thừa protected: Trong dẫn xuất loại này, 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. Bảng 1.1: Các loại kế thừa LỚP THỪA KẾ THỪA KẾ THỪA KẾ CƠ BẢN PUBLIC PRIVATE PROTECTED PUBLIC PRIVATE PROTECTED PUBLIC PROTECTED PRIVATE PROTECTED PROTECTED NO NO NO PRIVATE Ví dụ 5.3: Kiểu kế thừa: public class A{ private: int data_pri; public: int data_pub; }; 151 class B: public A{ void output() { cout<<data_pri; //lỗi cout<<data_pub; //ok } }; class C: public B{ void output(){ cout<<data_pri; //lỗi cout<<data_pub; //ok } }; void main{ B obj; obj.data_pri=5; //lỗi obj.data_pub=6; //ok } Ví dụ 5.4: Kiểu kế thừa: private class A{ private: int data_pri; public: int data_pub; }; class B: private A{ void output(){ cout<<data_pri; //lỗi cout<<data_pub; //ok } }; class C: public B{ void output(){ cout<<data_pri; //lỗi cout<<data_pub; //lỗi } }; void main{ B obj; obj.data_pri=5; //lỗi obj.data_pub=6; //lỗi } Do lớp B kế thừa kiểu private nên tính truy cập của các thành viên được kế thừa từ lớp A sẽ chuyển thành private, mà private chỉ được phép truy xuất bên trong lớp. Ví dụ 5.5: Kiểu kế thừa: protected class A{ 152 private: int data_pri; public: int data_pub; }; class B: protected A{ void output(){ cout<<data_pri; //lỗi cout<<data_pub; //ok } }; class C: public B{ void output() { cout<<data_pri; //lỗi cout<<data_pub; //ok } }; void main{ B obj; obj.data_pri=5; //lỗi obj.data_pub=6; //lỗi } Do lớp B kế thừa kiểu protected nên tính truy cập của các thành viên được kế thừa từ lớp A sẽ chuyển thành protected, mà protected chỉ được phép truy xuất bên trong lớp và lớp dẫn xuất. Trình tự gọi hàm khởi tạo: Khi có kế thừa, trình tự gọi hàm khởi tạo thực hiện theo nguyên tắc: hàm khởi tạo của lớp cơ bản được gọi trước và lớp dẫn xuất gọi sau. Lưu ý: ta có thể chỉ định hàm khởi tạo nào của lớp cơ bản được gọi bằng toán tử “:” Ví dụ 5.6: class A{ A(); A(int); }; class B:public A{ B():A(){..}; // gọi hàm A() B(int c):A(c){..}; // gọi hàm A(int) }; Trình tự gọi hàm huỷ: Hàm hủy được gọi theo trình tự ngược lại: lớp dẫn xuất được gọi trước và lớp cơ bản gọi sau. Gọi hàm thành viên trong kế thừa: Hàm thành viên của lớp dẫn xuất có thể cùng tên với tên hàm thành viên của lớp cơ bản. Hàm thành viên được gọi ứng với đối tượng gọi nó. Ví dụ 5.7: Đối tượng lớp cơ bản gọi thì hàm thành viên của lớp cơ bản thực hiện. Đối tượng lớp dẫn xuất gọi thì hàm thành viên của lớp dẫn xuất thực hiện. 153 Trong lớp dẫn xuất, muốn gọi hàm thành viên cùng tên của lớp cơ bản, ta dùng toán tử phạm vi “::” theo cú pháp sau: <Tên lớp cơ bàn>::<Tên hàm>(…) Con trỏ trong kế thừa: Con trỏ lớp cơ bản có thể trỏ tới địa chỉ đối tượng của lớp dẫn xuất nhưng ngược lại thì không. Đối tượng lớp dẫn xuất được xem như là một đối tượng của lớp cơ bản khi xử lý qua con trỏ nhưng ngược lại thì không. Ví dụ 5.8: Trình tự gọi hàm khởi tạo và hàm huỷ trong kế thừa đơn: class base{ public: base(){cout<<“base 1”;} base(int){cout<<“base 2”;} ~base(){cout<<“Huy base”;} }; class derived: public base{ public: derived():base(){cout<<“derived 1”;} derived(int):base(5){cout<<“derived 2”;} ~derived(){cout<<“Huy derived”;} }; Nếu: void main(){ derived d; } thì kết quả sẽ là : base 1 derived 1 Huy derived Huy base Nếu: void main(){ derived d(2); } thì kết quả sẽ là : base 2 derived 2 Huy derived Huy base Ví dụ 5.9: Gọi hàm thành viên trong kế thừa: class base{ public: void out(){ cout<<"Hello";} }; class derived: public base{ public: 154 void out(){cout<<"Chao";} void show(){ out(); } }; void main(){ derived d; d.show(); } Kết quả khi thực hiện hàm main Chao Sửa lại lớp derived để hàm main() xuất ra chữ hello: class derived: public base{ public: void out(){cout<<"Chao";} void show(){ base::out(); } }; Kết quả khi thực thi hàm main() như sau: Hello Ví dụ 5.10: Con trỏ trong kế thừa: class A{ protected: int dataA; public: A(){dataA=5;} void showA(){cout<<dataA;} }; class B: public A{ public: int dataB; public: B():A(){dataB=10;} void showB(){cout<<dataB;} }; void main(){ A a; B *p; p=&a; //lỗi } Vì con trỏ lớp dẫn xuất không được phép tham chiếu đến đối tượng lớp cơ bản. Nếu sửa thành: void main(){ A *p; B b; p=&b; //ok } Vì con trỏ lớp cơ bản được phép tham chiếu đến đối tượng lớp dẫn xuất. 155 5.3. ĐA KẾ THỪA Định nghĩa Đa kế thừa là tiến trình tạo ra một lớp mới từ nhiều lớp đã có. Đa thừa kế cho phép một lớp có thể là dẫn xuất của nhiều lớp cơ sở, do vậy những gì đã đề cập trong phần đơn thừa kế được tổng quát hoá cho trường hợp đa thừa kế. Tuy vậy cần phải giải quyết các vấn đề sau: Làm thế nào biểu thị được tính độc lập của các thành phần cùng tên bên trong một lớp dẫn xuất? Các hàm thiết lập và huỷ bỏ được gọi nhứ thế nào: thứ tự, truyền thông tin v.v.? Làm thế nào giải quyết tình trạng thừa kế xung đột trong đó. Phân loại Kế thừa từ các lớp khác nhau Kế thừa từ một lớp cơ bản chung Hình 5.5: Đa kế thừa Cú pháp: class A{ … }; class B{ … }; class C: <từ khóa chỉ định kiểu kế thừa> A, <từ khóa chỉ định kiểu kế thừa> B [,..]{ … }; Lưu ý: Quy tắc về tính truy cập và kiểu thừa kế trong đa kế thừa cũng giống như trong kế thừa đơn. Ví dụ 5.11: class GIAOVIEN{ protected: char hoten[40]; int luong; public: void nhap(); void xuat(); }; class SINHVIEN{ protected: 156 char hoten[40]; int luong; public: void nhap(); void xuat(); }; class TROGIANG: public GIAOVIEN, public SINHVIEN { … }; Đặc điểm đa kế thừa: Trình tự gọi hàm khởi tạo: Khi có đa kế thừa, trình tự gọi hàm khởi tạo thực hiện theo nguyên tắc sau: Hàm khởi tạo của lớp cơ bản được gọi trước theo trình tự từ trái sang phải (liệt kê trong kế thừa) Nếu trong lớp có chứa các đối tượng là thành viên dữ liệu của lớp thì nó sẽ được khởi tạo tiếp theo. Gọi hàm khởi tạo của lớp dẫn xuất. Trình tự gọi hàm huỷ. Hàm hủy được gọi theo trình tự ngược lại: Hàm hủy của lớp dẫn xuất; Hàm hủy của các đối tượng; Hàm hủy của lớp cơ bản. Ví dụ 5.12: Trình tự gọi hàm khởi tạo và hàm huỷ trong đa kế thừa class A{ public: A(){ cout<<"A“<<endl; } ~A(){ cout<<"Huy A"<<endl; } }; class B{ public: B() {cout<<"B"<<endl; } ~B(){ cout<<"Huy B"<<endl; } }; class C: public A, public B{ public: C(){cout<<"C"<<endl;} ~C(){cout<<"Huy C"<<endl;} }; void main(){ C c; } Kết quả thực hiện như sau: A B C 157 Huy C Huy B Huy A Ví dụ 5.13: Trình tự gọi hàm khởi tạo và hàm huỷ trong đa kế thừa khi lớp có chứa các đối tượng là thành viên dữ liệu của lớp class A{ public: A(){cout<<"A“<<endl;} ~A(){cout<<"Huy A"<<endl;} }; class B{ public: B(){ cout<<"B"<<endl;} ~B(){ cout<<"Huy B"<<endl;} }; class C: public A, public B{ private: A a; public: C(){cout<<"C"<<endl;} ~C(){cout<<"Huy C"<<endl;} }; void main(){ C c; } Kết quả thực hiện như sau: A B A C Huy Huy Huy Huy C A B A Vấn đề trong đa kế thừa: Để giải quyết vấn đề trùng lắp dữ liệu trong đa kế thừa, ta thực hiện một trong các phương pháp sau: Cách 1: dùng toán tử phạm vi “::” chỉ rõ thành viên dữ liệu hay hàm của lớp nào được gọi. Cách 2: định nghĩa một hàm cụ thể trong lớp dẫn xuất. Cách 3: nếu kế thừa từ một lớp cơ bản chung thì ta có thể dùng lớp cơ bản ảo cho việc kế thừa của lớp dẫn xuất theo cú pháp sau: class A{ }; class B: virtual public A{ }; class C: virtual public A{ }; 158 class D:public B,public C{ }; Hình 5.6: Vấn đề trong đa kế thừa Cách 1: dùng toán tử phạm vi “::” chỉ rõ thành viên dữ liệu hay hàm của lớp nào được gọi. class person{ public: char name[20]; public: person(){strcpy(name, “chua biet");} void out(){cout<<"chao "<<name<<endl;} }; class teacher:public person{ public: teacher(){strcpy(name,“Phong");} void out(){cout<<"chao "<<name<<endl;} }; class student:public person{ public: student(){strcpy(name,“Teo");} void out(){cout<<"hi "<<name;} }; class assistant:public teacher,public student{ }; Sẽ gây nhầm lẫn nếu: void main(){ assistant a; cout<<a.name; a.out(); } 159 Sửa lại thành: void main(){ assistant a; cout<<a.student::name; a.teacher::out(); } Kết quả sẽ là: Teo chao Phong Cách 2: định nghĩa một hàm cụ thể trong lớp dẫn xuất. class person{ public: char name[20]; public: person(){strcpy(name, “chua biet");} void out(){cout<<"chao "<<name<<endl;} }; class teacher:public person{ public: teacher(){strcpy(name,“Phong");} void out(){cout<<"chao "<<name<<endl;} }; class student:public person{ public: student(){strcpy(name,“Teo");} void out(){cout<<"hi "<<name;} }; class assistant:public teacher,public student{ public: void out(){cout<<"hello "<<teacher::name; } }; void main(){ assistant a; cout<<a.student::name; a.out(); } Kết quả sẽ là: Teo hello Phong Cách 3: sử dụng lớp cơ bản ảo trong kế thừa class person{ public: char name[20]; public: person(){strcpy(name, “chua biet");} void out(){cout<<"chao "<<name<<endl;} }; class teacher: virtual public person{ public: teacher(){strcpy(name,“teacher");} 160 void out(){cout<<"chao "<<name<<endl;} }; class student: virtual public person{ public: student(){strcpy(name,“student");} void out(){cout<<"hi "<<name;} }; class assistant:public teacher,public student{ public: assistant(){strcpy(name,“Phong");} void out(){cout<<"hello "<<name; } }; void main(){ assistant a; cout<<a.name; a.out(); } Kết quả sẽ là: Phong hello Phong Lợi ích của việc dùng lớp cơ bản ảo khi có kế thừa từ lớp cơ bản chung: – Tránh việc gọi hàm khởi tạo của lớp cơ bản nhiều lần từ lớp dẫn xuất. – Tránh việc trùng lắp các bản sao thành phần dữ liệu của lớp cơ bản. – Trình tự hàm khởi tạo và hàm hủy khi dùng lớp cơ bản ảo: • Hàm khởi tạo của lớp cơ bản ảo được gọi trước tiên (nếu có nhiều lớp cơ bản ảo thì trình tự bắt đầu từ trên xuống, từ trái sang phải theo đúng trình tự trong cây thư mục kế thừa. • Hàm hủy được gọi theo trình tự ngược lại. 5.4. HÀM ẢO 5.4.1. Giới thiệu Trước khi đưa ra khái niệm về hàm ảo, xét ví dụ sau: Giả sử có 3 lớp A, B và C được xây dựng như sau: class A { public: void xuat() { cout <<”\n Lop A”; } }; class B : public A { public: void xuat() { cout <<”\n Lop B”; } }; class C : public B 161 { public: void xuat() { cout <<”\n Lop C”; } }; Cả 3 lớp này đều có hàm thành phần là xuat(). Lớp C có hai lớp cơ sở là A, B và C kế thừa các hàm thành phần của A và B. Do đó một đối tượng của C sẽ có 3 hàm xuat(). Xem các câu lệnh sau: C ob; // ob là đối tượng kiểu C ob.xuat(); // Gọi tới hàm thành phần xuat() của lớp D ob.B::xuat() ; // Gọi tới hàm thành phần xuat() của lớp B ob.A::xuat() ; // Gọi tới hàm thành phần xuat() của lớp A Các lời gọi hàm thành phần trong ví dụ trên đều xuất phát từ đối tượng ob và mọi lời gọi đều xác định rõ hàm cần gọi. Ta xét tiếp tình huống các lời gọi không phải từ một biến đối tượng mà từ một con trỏ đối tượng. Xét các câu lệnh: A *p, *q, *r; // p,q,r là các con trỏ kiểu A A a; // a là đối tượng kiểu A B b; // b là đối tượng kiểu B C c; // c là đối tượng kiểu C Bởi vì con trỏ của lớp cơ sở có thể dùng để chứa địa chỉ các đối tượng của lớp dẫn xuất, nên cả 3 phép gán sau đều hợp lệ: p = &a; q = &b; r = &c; Ta xét các lời gọi hàm thành phần từ các con trỏ p, q, r: p->xuat(); q->xuat(); r->xuat(); Cả 3 câu lệnh trên đều gọi tới hàm thành phần xuat() của lớp A, bởi vì các con trỏ p, q, r đều có kiểu lớp A. Sở dĩ như vậy là vì một lời gọi (xuất phát từ một đối tượng hay con trỏ) tới hàm thành phần luôn luôn liên kết với một hàm thành phần cố định và sự liên kết này xác định trong quá trình biên dịch chương trình. Ta gọi đây là sự liên kết tĩnh. Có thể tóm lược cách thức gọi các hàm thành phần như sau: Nếu lời gọi xuất phát từ một đối tượng của lớp nào đó, thì hàm thành phần của lớp đó sẽ được gọi. Nếu lời gọi xuất phát từ một con trỏ kiểu lớp, thì hàm thành phần của lớp đó sẽ được gọi bất kể con trỏ chứa địa chỉ của đối tượng nào. Vấn đề đặt ra là: Ta muốn tại thời điểm con trỏ đang trỏ đến đối tượng nào đó thì lời gọi hàm phải liên kết đúng hàm thành phần của lớp mà đối tượng đó thuộc vào chứ không phụ thuộc vào kiểu lớp của con trỏ. C++ giải quyết vấn đề này bằng cách dùng khái niệm hàm ảo. 5.4.2. Định nghĩa Hàm ảo là hàm thành phần của lớp, nó được khai báo trong lớp cơ sở và định nghĩa lại trong lớp dẫn xuất. Để định nghĩa hàm ảo thì phần khai báo hàm phải bắt đầu bằng từ khóa virtual. Khi một lớp có chứa hàm ảo được kế thừa, lớp dẫn xuất sẽ định nghĩa lại hàm ảo đó cho chính mình. Các hàm ảo triển khai tư tưởng chủ đạo của tính đa hình là “một giao diện cho nhiều hàm thành phần”. Hàm ảo bên trong lớp cơ sở định nghĩa hình thức giao tiếp đối với hàm đó. Việc định nghĩa 162 lại hàm ảo ở lớp dẫn xuất là thi hành các tác vụ của hàm liên quan đến chính lớp dẫn xuất đó. Nói cách khác, định nghĩa lại hàm ảo chính là tạo ra phương thức cụ thể. Trong phần định nghĩa lại hàm ảo ở lớp dẫn xuất, không cần phải sử dụng lại từ khóa virtual. Khi xây dựng hàm ảo, cần tuân theo những quy tắc sau: Hàm ảo phải là hàm thành phần của một lớp; Những thành phần tĩnh (static) không thể khai báo ảo; Sử dụng con trỏ để truy nhập tới hàm ảo; Hàm ảo được định nghĩa trong lớp cơ sở, ngay khi nó không được sử dụng; Mẫu của các phiên bản (ở lớp cơ sở và lớp dẫn xuất) phải giống nhau. Nếu hai hàm cùng tên nhưng có mẫu khác nhau thì C++ sẽ xem như hàm tải bội; Không được tạo ra hàm tạo ảo, nhưng có thể tạo ra hàm hủy ảo; Con trỏ của lớp cơ sở có thể chứa địa chỉ của đối tượng thuộc lớp dẫn xuất, nhưng ngược lại thì không được; Nếu dùng con trỏ của lớp cơ sở để trỏ đến đối tượng của lớp dẫn xuất thì phép toán tăng giảm con trỏ sẽ không tác dụng đối với lớp dẫn xuất, nghĩa là không phải con trỏ sẽ trỏ tới đối tượng trước hoặc tiếp theo trong lớp dẫn xuất. Phép toán tăng giảm chỉ liên quan đến lớp cơ sở. Ví dụ 5.14: class A { ... virtual void hienthi() { cout<<”\nDay la lop A”; }; }; class B : public A { ... void hienthi() { cout<<”\nDay la lop B”; } }; class C : public B { ... void hienthi() { cout<<”\nDay la lop C”; } }; class D : public A { ... void hienthi() { cout<<”\nDay la lop D”; } 163 }; Chú ý: Từ khoá virtual không được đặt bên ngoài định nghĩa lớp. Xét ví dụ : class A { ... virtual void hienthi(); }; virtual void hienthi() // sai { cout<<”\nDay la lop A”; } 5.4.3. Quy tắc gọi hàm ảo Hàm ảo chỉ khác hàm thành phần thông thường khi được gọi từ một con trỏ. Lời gọi tới hàm ảo từ một con trỏ chưa cho biết rõ hàm thành phần nào (trong số các hàm thành phần cùng tên của các lớp có quan hệ thừa kế) sẽ được gọi. Điều này sẽ phụ thuộc vào đối tượng cụ thể mà con trỏ đang trỏ tới: con trỏ đang trỏ tới đối tượng của lớp nào thì hàm thành phần của lớp đó sẽ được gọi. 5.4.4. Quy tắc gán địa chỉ đối tượng cho con trỏ lớp cơ sở C++ cho phép gán địa chỉ đối tượng của một lớp dẫn xuất cho con trỏ của lớp cơ sở bằng cách sử dụng phép gán = và phép toán lấy địa chỉ &. Ví dụ 5.15: Giả sử A là lớp cơ sở và B là lớp dẫn xuất từ A. Các phép gán sau là đúng: A *p; // p là con trỏ kiểu A A a; // a là biến đối tượng kiểu A B b; // b là biến đối tượng kiểu B p = &a; // p và a cùng lớp A p = &b; // p là con trỏ lớp cơ sở, b là đối tượng lớp dẫn xuất Chú ý: Không cho phép gán địa chỉ đối tượng của lớp cơ sở cho con trỏ của lớp dẫn xuất, chẳng hạn với khai báo B *q; A a; thì câu lệnh q = &a; là sai. Ví dụ 5.16: Chương trình sau đây minh họa việc sử dụng hàm ảo: #include <iostream.h> #include <conio.h> class A { public: virtual void hienthi() { cout <<"\n Lop A"; } }; class B : public A { public: void hienthi() { cout <<"\n Lop B"; 164 } }; class C : public B { public: void hienthi() { cout <<"\n Lop C"; } }; void main() { clrscr(); A *p; A a; B b; C c; a.hienthi(); //goi ham cua lop A p = &b; //p tro to doi tuong b cua lop B p->hienthi(); //goi ham cua lop B p=&c; //p tro to doi tuong c cua lop C p->hienthi(); //goi ham cua lop C getch(); } Chương trình này cho kết quả như sau: Lop A Lop B Lop C Chú ý: Cùng một câu lệnh p->hienthi(); được tương ứng với nhiều hàm khác nhau khác nhau khi hienthi() là hàm ảo. Đây chính là sự tương ứng bội. Khả năng này cho phép xử lý nhiều đối tượng khác nhau theo cùng một cách thức. Cũng với lời gọi: p->hienthi(); (hienthi() là hàm ảo) thì lời gọi này không liên kết với một phương thức cố định, mà tùy thuộc và nội dung con trỏ. Đó là sự liên kết động và phương thức được liên kết (được gọi) thay đổi mỗi khi có sự thay đổi nội dung con trỏ trong quá trình chạy chương trình. Ví dụ 5.17: Chương trình sau tạo ra một lớp cơ sở có tên là num lưu trữ một số nguyên, và một hàm ảo của lớp có tên là shownum(). Lớp num có hai lớp dẫn xuất là outhex và outoct. Trong hai lớp này sẽ định nghĩa lại hàm ảo shownum() để chúng in ra số nguyên dưới dạng số hệ 16 và số hệ 8. #include <iostream.h> #include <conio.h> class num { public : int i; num(int x) { i=x; } virtual void shownum() { cout<<"\n So he 10 : "; cout <<dec<<i<<'\n'; } 165 }; class outhex : public num { public : outhex(int n) : num(n) {} void shownum() { cout <<"\n So he 10 : "<<dec<<i<<endl; cout <<"\n So he 16 : "<<hex << i <<'\n'; } }; class outoct : public num { public : outoct(int n) : num(n) {} void shownum() { cout <<"\n So he 10 : "<<dec<<i<<endl; cout <<"\n So he 8 : "<< oct << i <<'\n'; } }; void main() { clrscr(); num n (1234); outoct o (342); outhex h (747); num *p; p=&n; p->shownum(); //goi ham cua lop co so, 100 p=&o; p->shownum(); //goi ham cua lop dan xuat, 12 p=&h; p->shownum(); //goi ham cua lop dan xuat, f getch(); } So So So So So Chương trình trên cho kết quả: he 10 : 1234 he 10 : 342 he 8 : 526 he 10 : 747 he 16 : 2eb 5.5. LỚP CƠ SỞ ẢO 5.5.1. Khai báo Một vấn đề tồn tại là khi nhiều lớp cơ sở được kế thừa trực tiếp bởi một lớp dẫn xuất. Để hiểu rõ hơn vấn đề này, xét tình huống các lớp kế thừa theo sơ đồ như sau: 166 Hình 5.7: Sơ đồ các lớp kế thừa Ở đây, lớp A được kế thừa bởi hai lớp B và C. Lớp D kế thừa trực tiếp cả hai lớp B và C. Như vậy lớp A được kế thừa hai lần bởi lớp D: lần thứ nhất nó được kế thừa thông qua lớp B và lần thứ hai được kế thừa thông qua lớp C. Bởi vì có hai bản sao của lớp A có trong lớp D nên một tham chiếu đến một thành phần của lớp A sẽ tham chiếu về lớp A được kế thừa gián tiếp thông qua lớp B hay tham chiếu về lớp A được kế thừa gián tiếp thông qua lớp C? Để giải quyết tính không rõ ràng này, C++ có một cơ chế mà nhờ đó chỉ có một bản sao của lớp A ở trong lớp D: đó là sử dụng lớp cơ sở ảo (còn gọi là lớp cơ sở trừu tượng). Trong ví dụ trên, C++ sử dụng từ khóa vitual để khai báo lớp A là ảo trong các lớp B và C theo mẫu như sau: class B : virtual public A {...}; class C : virtual public A {...}; class D : public B, public C {...}; Việc chỉ định A là ảo trong các lớp B và C nghĩa là A sẽ chỉ xuất hiện một lần trong lớp D. Khai báo này không ảnh hưởng đến các lớp B và C. Chú ý: Từ khóa virtual có thể đặt trước hoặc sau từ khóa public, private, protected. Ví dụ 5.18: #include <iostream.h> #include <conio.h> class A { float x,y; public: void set(float x1, float y1) { x = x1; y = y1; } float getx() { return x; } float gety() { return y; } }; 167 class B : virtual public A { }; class C : virtual public A { }; class D : public B, public C { }; void main() { clrscr(); D d; cout<<"\nd.B::set(2,3)\n"; d.B::set(2,3); cout<<"\nd.C::getx() = "; cout<<d.C::getx()<<endl; cout<<"\nd.B::getx() = "; cout<<d.B::getx()<<endl; cout<<"\nd.C::gety() = "; cout<<d.C::gety()<<endl; cout<<"\nd.B::gety() = "; cout<<d.B::gety()<<endl; cout<<"\nd.C::set(2,3)\n"; d.C::set(2,3); cout<<"\nd.C::getx() = "; cout<<d.C::getx()<<endl; cout<<"\nd.B::getx() = "; cout<<d.B::getx()<<endl; cout<<"\nd.C::gety() = "; cout<<d.C::gety()<<endl; cout<<"\nd.B::gety() = "; cout<<d.B::gety()<<endl; getch(); } Chương trình trên sẽ cho kết quả: d.B::set(2,3) d.C::getx() = 2 d.B::getx() = 2 d.C::gety() = 3 d.B::gety() = 3 d.C::set(2,3) d.C::getx() = d.B::getx() = d.C::gety() = d.B::gety() = 2 2 3 3 5.5.2. Hàm tạo và hàm hủy đối với lớp cơ sở ảo Ta đã biết, khi khởi tạo đối tượng lớp dẫn xuất thì các hàm tạo được gọi theo thứ tự xuất hiện trong danh sách các lớp cơ sở được khai báo, rồi đến hàm tạo của lớp dẫn xuất. Thông tin được chuyển từ hàm tạo của lớp dẫn xuất sang hàm tạo của lớp cơ sở. Trong tình huống có lớp cơ sở ảo, chẳng hạn như Hình vẽ 5.4., cần phải tuân theo quy định sau: Thứ tự gọi hàm tạo: Hàm tạo của một lớp ảo luôn luôn được gọi trước các hàm tạo khác. Với sơ đồ kế thừa như hình vẽ, thứ tự gọi hàm tạo sẽ là A, B, C và cuối cùng là D. Chương trình sau minh họa điều này: Ví dụ 5.19: #include <iostream.h> #include <conio.h> class A { float x,y; 168 public: A() {x = 0; y = 0;} A(float x1, float y1) { cout<<"A::A(float,float)\n"; x = x1; y = y1; } float getx() { return x; } float gety() { return y; } }; class B : virtual public A { public: B(float x1, float y1):A(x1,y1) { cout<<"B::B(float,float)\n"; } }; class C : virtual public A { public: C(float x1, float y1):A(x1,y1) { cout<<"C::C(float,float)\n"; } }; class D : public B, public C { public: D(float x1, float y1):A(x1,y1), B(10,4), C(1,1) { cout<<"D::D(float,float)\n"; } }; void main() { clrscr(); D d(2,3); cout<<"\nD d (2,3)\n"; cout<<"\nd.C::getx() = "; cout<<d.C::getx()<<endl; cout<<"\nd.B::getx() = "; cout<<d.B::getx()<<endl; cout<<"\nd.C::gety() = "; cout<<d.C::gety()<<endl; cout<<"\nd.B::gety() = "; cout<<d.B::gety()<<endl; cout<<"\nd1 (10,20) \n"; 169 D d1 (10,20); cout<<"\nd.C::getx() cout<<d.C::getx()<<endl; cout<<"\nd.B::getx() = "; cout<<d.B::getx()<<endl; cout<<"\nd.C::gety() = "; cout<<d.C::gety()<<endl; cout<<"\nd.B::gety() = "; cout<<d.B::gety()<<endl; getch(); } Kết quả thực hiện chương trình trên như sau: A::A(float,float) B::B(float,float) C::C(float,float) D::D(float,float) D d (2,3) d.C::getx() = 2 d.B::getx() = 2 d.C::gety() = 3 d.B::gety() = 3 d1 (10,20) A::A(float,float) B::B(float,float) C::C(float,float) D::D(float,float) d.C::getx() = 2 d.B::getx() = 2 d.C::gety() = 3 d.B::gety() = 3 = "; 5.6. TÍNH ĐA HÌNH TRONG KẾ THỪA Khái niệm: Là cách thức truy cập các hàm khác nhau tùy thuộc vào đối tượng mà con trỏ đang trỏ tới khi ta thực thi cùng một lời gọi hàm. Ví dụ 5.20: Đối với nhân viên trong cùng một công ty nhưng cách tính lương của nhân viên văn phòng khác cách tính lương của nhân viên sản xuất. Hoặc cùng là đối tượng sinh viên nhưng sinh viên chính quy có thời lượng học khác với thời lượng học của sinh viên từ xa. Cú pháp: Cách thể hiện tính đa hình trong OOP: Hàm đa năng là một cách thể hiện tính đa hình trong OOP. Dùng hàm ảo theo cú pháp khai báo sau: dùng từ khóa virtual trước kiểu dữ liệu trả về của hàm: virtual <Kiểu_data> Tên_hàm(DS_đối số); Lưu ý: Hàm ảo khi chỉ có khai báo mà không có định nghĩa ngay trong lớp thì được gọi là hàm thuần ảo (pure virtual). Cú pháp khai báo như sau: virtual <Kiểu_data> Tên_hàm(DS_đối số) = 0; 170 Một lớp nếu có chứa hàm thuần ảo thì được gọi là lớp trừu tượng (abstract class). Lớp trừu tượng là lớp không thể tạo ra đối tượng. Ví dụ 5.21: Cho biết kết quả của chương trình sau. class hinh{ public: void ve(){cout<<"ve hinh "<<endl;} }; class hinhtron:public hinh{ public: void ve(){cout<<"hinh tron "<<endl;} }; class hinhchunhat:public hinh{ public: void ve(){cout<<"hinh cn "<<endl;} }; void main(){ hinhtron ht; hinhchunhat hcn; hinh *p; p=&ht; p->ve(); p=&hcn; p->ve(); } Kết quả thực hiện như sau: ve hinh ve hinh Khắc phục: Để kết quả của chương trình trên thực thi đúng: class hinh{ public: virtual void ve(){cout<<"ve hinh "<<endl;} }; class hinhtron:public hinh{ public: void ve(){cout<<"hinh tron "<<endl;} }; class hinhchunhat:public hinh{ public: void ve(){cout<<"hinh cn "<<endl;} }; void main(){ hinhtron ht; hinhchunhat hcn; hinh *p; p=&ht; p->ve(); p=&hcn; p->ve(); 171 } Kết quả thực thi chương trình như sau: hinh tron hinh cn 5.7. MỘT SỐ VÍ DỤ 5.7.1. Cài đặt lớp bệnh nhân Viết chương trình thực hiện các công việc sau: - Xây dựng lớp cơ sở bệnh nhân gồm: + Thuộc tính: họ tên, quê quán, năm sinh; + Phương thức: Nhập, xuất thông tin. - Xây dựng lớp bệnh án kế thừa từ lớp bệnh nhân có thêm: + Thuộc tính: tên bệnh án, số tiền viện phí; + Phương thức: Nhập, xuất thông tin, tính tuổi hiện tại. - Chương trình chính thực hiện: + Nhập danh sách N bệnh án; + Sắp xếp danh sách theo tuổi giảm dần của các bệnh nhân; + Hiện ra màn hình danh sách các bệnh nhân tuổi<=10. - Cho biết thông tin các bệnh nhân có tiền viện phí cao nhất #include <iostream.h> #include <stdio.h> #include <conio.h> class benhnhan { public: char hoten[25]; char quequan[30]; int namsinh; public: void nhap() { cout<<"Nhap ten: "; fflush(stdin); gets(hoten); cout<<"Nhap que quan: "; fflush(stdin); gets(quequan); cout<<"Nhap nam sinh: "; cin>>namsinh; } void xuat() { cout<<"\nTen: "<<hoten; cout<<"\nQue quan: "<<quequan; cout<<"\nNam sinh: "<<namsinh; } }; class benhan: public benhnhan { public: char tenba[30]; double tienvp; int tuoi() { 172 int tuoi=2015-namsinh; //year at the moment – year of birth return tuoi; } void nhap() { benhnhan::nhap(); cout<<"Nhap ten benh an: "; fflush(stdin); gets(tenba); cout<<"Nhap tien vien phi: "; cin>>tienvp; cout<<endl; } void xuat() { benhnhan::xuat(); cout<<"\nTen benh an: "<<tenba; cout<<"\nTien vien phi: "<<tienvp; cout<<endl; } }; void main() { benhan a[50]; int n,i; do { cout<<"Nhap so benh an n= "; cin>>n; } while (n<0); for (i=0; i<n; i++) { cout<<"Nhap thong tin thu "<<i+1<<" \n"; a[i].nhap(); } cout<<"\n------------\n"; for (i=0; i<n; i++) { cout<<"\n-Benh nhan thu : "<<i+1<<" \n"; a[i].xuat(); } for (i=0; i<n-1; i++) for (int j=i+1; j<n; j++) { if (a[j].tuoi()>a[i].tuoi()) { benhan c=a[i]; a[i]=a[j]; a[j]=c; } } cout<<"\n------------\n"; cout<<"\nThong tin benh an giam dan ve tuoi: \n"; 173 for (i=0; i<n; i++) { a[i].xuat(); } cout<<"\n------------\n"; cout<<"\nDanh sach benh nhan duoi 10 tuoi: "; for (i=0; i<n; i++) { if (a[i].tuoi()<=10) a[i].xuat(); } benhan vpmax=a[0]; for (i=0; i<n; i++) { if (vpmax.tienvp<a[i].tienvp) { vpmax=a[i]; } } cout<<"\n------------\n"; cout<<"\nThong tin benh nhan co vien phi cao nhat: "; vpmax.xuat(); getch(); } 5.7.2. Cài đặt lớp điểm Cách khai báo lớp kế thừa đơn, cài đặt các phương thức thiết lập, phương thức huỷ và các overriding các phương thức của lớp cơ sở. Minh hoạ thứ tự thực hiện của phương thức thiết lập và phương thức huỷ trong lớp kế thừa. Nội dung file MyPoint.h #pragma once #include <iostream> using namespace std; class CMyPoint { protected: float x, y; public: CMyPoint(); ~CMyPoint(); CMyPoint(float xx, float yy); void Input(); void Print(); friend istream& operator >>(istream &is, CMyPoint &p) { cout << "* Nhap vao hoanh do: "; is >> p.x; cout << "* Nhap vao tung do: "; is >> p.y; 174 return is; } friend ostream& operator <<(ostream &os, CMyPoint &p) { os << "(" << p.x << "," << p.y << ")"; return os; } }; Nội dung file MyPoint.cpp #include "MyPoint.h" CMyPoint::CMyPoint() { cout << "* Thiet lap mac dinh cho toa do" << endl; x = 0; y = 0; } CMyPoint::~CMyPoint() { cout << "* Huy doi tuong toa do" << endl; } CMyPoint::CMyPoint(float xx, float yy) { cout << "* Thiet lap 2 tham so cho toa do" << endl; x = xx; y = yy; } void CMyPoint::Input() { cout << "* Nhap vao hoanh do: "; cin >> x; cout << "* Nhap vao tung do: "; cin >> y; } void CMyPoint::Print() { cout << "* Toa do: (" << x << "," << y << ")"; } Nội dung file Circle.h #pragma once #include "MyPoint.h" class CCircle: public CMyPoint { float r; public: CCircle(); ~CCircle(); CCircle(float xx, float yy, float rr); void Input(); void Print(); 175 friend istream& operator >>(istream &is, CCircle &c) { cout << "Nhap vao ban kinh: "; is >> c.r; return is; } friend ostream& operator <<(ostream &os, CCircle &c) { os << "* Ban kinh = " << c.r; return os; } }; Nội dung file Circle.cpp #include "Circle.h" CCircle::CCircle() { cout << "* Thiet lap mac dinh cho hinh tron" << endl; r = 1; } CCircle::~CCircle() { cout << "* Huy doi tuong hinh tron" << endl; } CCircle::CCircle(float xx, float yy, float rr) :CMyPoint(xx, yy) { cout << "* Thiet lap co tham so cho hinh tron" << endl; r = rr; } void CCircle::Input() { CMyPoint::Input(); cout << "* Nhap vao ban kinh: "; cin >> r; } void CCircle::Print() { CMyPoint::Print(); cout << "\n* Ban kinh = " << r; } Nội dung file Rectangle.h #pragma once #include "MyPoint.h" class CRectangle :public CMyPoint { float n, m; public: CRectangle(); CRectangle(float nn, float mm); ~CRectangle(); 176 &rec) void Input(); void Print(); friend istream& operator >>(istream& is, CRectangle os, CRectangle { cout << "Nhap vao chieu dai: "; is >> rec.n; cout << "Nhap vao chieu rong: "; is >> rec.m; return is; } friend &rec) ostream& operator <<(ostream& { os << "* Chieu dai = " << rec.n << endl; os << "* Chieu rong = " << rec.m; return os; } }; Nội dung file Rectangle.cpp #include "Rectangle.h" CRectangle::CRectangle() { cout << "* Thiet lap mac dinh cho hinh chu nhat" << endl; n = 1; m = 1; } CRectangle::~CRectangle() { cout << "* Huy doi tuong hinh chu nhat" << endl; } CRectangle::CRectangle(float nn, float mm) { cout << "* Thiet lap 2 tham so cho hinh chu nhat" << endl; n = nn; m = mm; } void CRectangle::Input() { CMyPoint::Input(); cout << "* Nhap vao chieu dai: "; cin >> n; cout << "* Nhap vao chieu rong: "; cin>> m; } void CRectangle::Print() { CMyPoint::Print(); 177 cout << "* Chieu dai = " << n << endl; cout << "* Chieu rong = " << m; } Nội dung file main.cpp #include "Circle.h" #include "Rectangle.h" void main() { cout << "(1) Thiet lap hinh tron co toa do (2, 3) va ban kinh la 10\n"; CCircle cir(2, 3, 10); cout << "\n(2) Xuat thong tin cua hinh tron:" << endl; cir.Print(); cout << "\n\n(3) Tao doi tuong hinh chu nhat:\n"; CRectangle rec; cout << "\n(4) Nhap thong tin hinh chu nhat:\n"; rec.Input(); cout << "\n(5) Xuat thong tin cua hinh chu nhat:\n"; rec.Print(); cout << "\n\n(6) Bat dau huy cac doi tuong:\n"; } 5.7.3. Xây dựng lớp phân số Viết chương trình thực hiện các yêu cầu sau: Khai báo lớp PS1 cho các đối tượng là phân số với các thuộc tính: tử số, mẫu số. Xây dựng phương thức nhập phân số (mẫu số khác 0), in phân số, tối giản phân số Xây dựng lớp PS2 kế thừa từ lớp PS1 và bổ sung: Nạp chồng các toán tử: = (gán), > (lớn hơn), + (cộng). Viết chương trình sử dụng lớp PS2 để nhập một danh sách các đối tượng là các phân số (tối đa 10 phần tử). Tìm phân số có giá trị lớn nhất, tính tổng các phân số trong danh sách có giá trị lớn hơn 1/4. #include <iostream> using namespace std; class PS1 { public: /*PS1() { this->tuso = 0; this->mauso = 1; } PS1(int tuso1, int mauso1) { tuso = tuso1; mauso = mauso1; } ~PS1() { this->tuso = 0; 178 this->mauso = 1; }*/ void nhap() { cout << "Nhap tu so: "; cin >> tuso; while (mauso == 1) { cout << "Nhap mau so: "; cin >> mauso; if (mauso != 0) break; else cout << "Mau so phai khac 0!" << endl; } cout << endl; } void xuat() { cout << tuso << "/" << mauso << endl; } void toigian(){ int a, b; a= tuso; b= mauso; while(b>0){ int tmp=a%b; a=b; b=tmp; } tuso=tuso/a; mauso=mauso/a; } protected: int tuso, mauso; }; class PS2 : public PS1 { public: PS2() { this->tuso = 0; this->mauso = 1; } PS2(int tuso1, int mauso1) { tuso = tuso1; mauso = mauso1; } ~PS2() { this->tuso = 0; this->mauso = 1; } 179 PS2 operator =(PS2 p) { tuso = p.tuso; mauso = p.mauso; return *this; } int operator >(PS2 p) { if (tuso*p.mauso > p.tuso / mauso) return 1; else return 0; } PS2 operator +(PS2 p2) { PS2 p; p.tuso = tuso * p2.mauso + mauso* p2.tuso; p.mauso = mauso * p2.mauso; p.toigian(); return p; } }; main() { PS2 p[10], max, s, tmp(1,4); int i, n; cout << "Nhap so cac phan so: n= "; cin >> n; for (i = 1; i <= n; i++) { cout << "Phan so thu " << i << ": " << endl; p[i].nhap(); } cout << endl; for (i = 1; i <= n; i++) { cout << "Phan so thu " << i << ": "; p[i].xuat(); } cout << endl; max = p[1]; for (i=1;i<=n;i++){ if(p[i]>max) max=p[i]; if(p[i]>tmp) s=s+p[i]; } cout << "Phan so lon nhat la: " << endl; max.xuat(); cout << endl << "Tong cac phan so lon hon 1/4 la: "; s.xuat(); } 5.7.4. Xây dựng lớp số phức Viết chương trình thực hiện các yêu cầu sau: Khai báo lớp SP1 mô tả các đối tượng là số phức với các thuộc tính: phần thực, phần ảo. Xây dựng hàm tạo, phương thức nhập số phức, in số phức, tính module số phức 180 Xây dựng lớp SP2 kế thừa từ lớp SP1 và bổ sung: Nạp chồng các toán tử: = (gán), < (nhỏ hơn), + (cộng) Viết chương trình sử dụng lớp SP2 để nhập một danh sách các đối tượng là các số phức (tối đa 10 phần tử). Tìm số phức có giá trị nhỏ nhất (theo module) và tính tổng các số phức trong dãy số. using namespace std; #include<conio.h> #include<stdio.h> #include<math.h> #include<iostream> class sp1 { private: int thuc, ao; public: sp1() { //Ham tao khong co doi so this->thuc=0; this->ao=1; } //Ham tao co doi so sp1(int thuc1, int ao1) { thuc=thuc1; ao=ao1; } //qua tai toan tu nhap so phuc friend istream &operator>>(istream &is, sp1 &p) { cout<<"\nnhap phan thuc: ";is>>p.thuc; cout<<"\nnhap phan ao: ";is>>p.ao; return is; } //qua tai toan tu xuat so phuc friend ostream &operator<<(ostream &os, sp1 p) { os<<p.thuc; if(p.ao>=0) cout<<"+"<<p.ao<<"*i"; else os<<p.ao<<"*i"; return os; } float module() { float z; z=sqrt(thuc*thuc + ao*ao); return z; 181 } }; class sp2: public sp1 { protected: int thuc, ao; public: sp2() { //Ham tao khong co doi so thuc=0; ao=1; } //Ham tao co doi so sp2(int thuc1, int ao1) { thuc=thuc1; ao=ao1; } sp2 operator =(sp2 p); int operator <(sp2 p2); sp2 operator +(sp2 p); }; sp2 sp2::operator =(sp2 p) { thuc=p.thuc; ao=p.ao; return *this; } int sp2::operator <(sp2 p2) { if(module()<p2.module()) return 1; else return 0; } sp2 sp2::operator +(sp2 p) { thuc=thuc+p.thuc; ao=ao+p.ao; return *this; } main() { sp2 a[100]; int i, n; cout<<"Nhap n: "; cin>>n; for(i=1; i<=n; i++) { cin>>a[i]; 182 cout<<"\nSo phuc thu "<<i<<":"<<a[i]; cout<<"\nModule cua so phuc la: "<<a[i].module(); } float min; min=a[1].module(); for (i=1; i<=n; i++) { if(min<a[i].module()) min=a[i].module(); } cout<<"\n..................\n"; cout<<"\nGia tri nho nhat cua so phuc la: "<<min; cout<<"\nSo phuc co gia tri nho nhat la: "<<a[i]; sp2 t; for(i=1; i<=n; i++) t=t+a[i]; cout<<"\nTong cac so phuc la: "<<t; return 0; } 5.7.5. Quản lý và sinh viên trong trường đại học Cuối năm học cần trao giải thưởng cho các sinh viên xuất sắc và các giảng viên có nhiều công trình khoa học được công bố trên tạp chí. Các lớp trong sơ đồ kế thừa như Hình 5.8: lớp Nguoi để quản lý hồ sơ cá nhân, lớp SinhVien quản lý về sinh viên và lớp GiangVien quản lý giảng viên. Lớp Nguoi: Dữ liệu họ và tên. Phương thức kiểm tra khả năng được khen thưởng. Đây là phương thức thuần ảo. Phương thức xuất. Đây là phương thức thuần ảo. Lớp SinhVien: Dữ liệu điểm trung bình. Phương thức kiểm tra khả năng được khen thưởng. Phương thức xuất. Lớp GiangVien: Dữ liệu điểm trung bình. Phương thức kiểm tra khả năng được khen thưởng. Phương thức xuất. 183 Hình 5.8: Sơ đồ lớp Nội dung file PERSON.H //PERSON.H #ifndef PERSON_H #define PERSON_H #include <iostream.h> #define MAX_TEN 50 class Nguoi { protected: char HoTen[MAX_TEN]; public: Nguoi(char *HT); virtual int DuocKhenThuong() const=0; virtual void Xuat() const=0; }; #endif Nội dung file PERSON.CPP: //Định nghĩa hàm thành viên cho lớp Nguoi #include <string.h> #include "person.h" Nguoi::Nguoi(char *HT) { strcpy(HoTen,HT); } Nội dung file STUDENT.H: //Định nghĩa lớp SinhVien #ifndef STUDENT_H #define STUDENT_H #include "person.h" 184 class SinhVien : public Nguoi { protected: float DiemTB; public: SinhVien(char *HT,float DTB); virtual int DuocKhenThuong() const; virtual void Xuat() const; }; #endif Nội dung file STUDENT.CPP: // Định nghĩa hàm thành viên cho lớp SinhVien #include "student.h" SinhVien::SinhVien(char *HT,float DTB):Nguoi(HT) { DiemTB=DTB; } int SinhVien::DuocKhenThuong() const { return DiemTB>9.0; } void SinhVien::Xuat() const { cout<<"Ho va ten cua sinh vien:"<<HoTen; } Nội dung file TEACHER.H: //TEACHER.H // Định nghĩa lớp GiangVien #ifndef TEACHER_H #define TEACHER_H #include "person.h" class GiangVien : public Nguoi { protected: int SoBaiBao; public: GiangVien(char *HT,int SBB); virtual int DuocKhenThuong() const; virtual void Xuat() const; }; #endif Nội dung file TEACHER.CPP: // Định nghĩa hàm thành viên cho lớp GiangVien #include "teacher.h" GiangVien::GiangVien(char *HT,int SBB):Nguoi(HT) { SoBaiBao=SBB; } 185 int GiangVien::DuocKhenThuong() const { return SoBaiBao>5; } void GiangVien::Xuat() const { cout<<"Ho va ten cua giang vien:"<<HoTen; } Nội dung chương trình chính main: #include <ctype.h> #include "person.h" #include "student.h" #include "teacher.h" int main() { Nguoi *Ng[100]; int N=0; char Chon,Loai; char HoTen[MAX_TEN]; do { cout<<"Ho va ten:"; cin.getline(HoTen,MAX_TEN); cout<<"Sinh vien hay Giang vien(S/G)? "; cin>>Loai; Loai=toupper(Loai); if (Loai=='S') { float DTB; cout<<"Diem trung binh:"; cin>>DTB; Ng[N++]=new SinhVien(HoTen,DTB); } else { int SoBaiBao; cout<<"So bai bao:"; cin>>SoBaiBao; Ng[N++]=new GiangVien(HoTen,SoBaiBao); } cout<<"Tiep tuc (C/K)? "; cin>>Chon; Chon=toupper(Chon); cin.ignore(); if ((N==100)||(Chon=='K')) break; } while (1); for(int I=0;I<N;++I) 186 { Ng[I]->Xuat(); if (Ng[I]->DuocKhenThuong()) cout<<". Nguoi nay duoc khen thuong"; cout<<endl; } return 0; } CÂU HỎI, BÀI TẬP 1. a. Xây dựng một lớp SV để mô tả các sinh viên trong một Khoa của một Trường Đại học, lớp SV gồm các thành phần sau: - Lop: Mô tả lớp học của sinh viên. - Hoten: Mô tả họ tên của sinh viên. - Hàm thiết lập. - Hàm huỷ bỏ. - Hàm hiển thị thông tin về một SV. b. Xây dựng một lớp SVSP để mô tả các sinh viên thuộc hệ sư phạm. Lớp được kế thừa từ lớp SV và bổ sung thêm các thành phần sau: - Dtb: Mô tả điểm trung bình của sinh viên. - Hocbong: Mô tả học bổng của sinh viên. - Hàm thiết lập. - Hàm hiển thị thông tin về một SVSP. c. Xây dựng một lớp SVCN để mô tả các sinh viên thuộc hệ cử nhân. Lớp được kế thừa từ lớp SVSP và bổ sung thêm các thành phần sau: - Hocphi: Mô tả học phí của sinh viên cử nhân. - Hàm thiết lập. - Hàm hiển thị thông tin về một SVCN. d. Viết chương trình khai báo một mảng 3 con trỏ đối tượng kiểu SVCN, nhập dữ liệu vào để tạo 3 đối tượng SVCN. Gọi hàm hiển thị của lớp SVCN thông qua các con trỏ này để in thông tin về một SVCN ra màn hình. 2. Xây dựng chương trình quản lý sách và băng video của một cửa hàng, chương trình gồm: a. Xây dựng 1 lớp Media mô tả các đối tượng phương tiện truyền thông, lớp gồm: - Thuộc tính tên gọi, giá bán. - Hàm thiết lập hai tham số. - Hàm nhập dữ liệu. - Hàm hiển thị dữ liệu. b. Xây dựng lớp Book mô tả các đối tượng sách. Lớp được kế thừa từ lớp Media và bổ sung thêm: - Thuộc tính mô tả số trang, tác giả. - Hàm thiết lập. - Hàm nhập dữ liệu 187 - Hàm hiển thị dữ liệu. c. Xây dựng lớp Video mô tả các đối tượng băng video, lớp kế thừa từ lớp Media và bổ sung thêm: - Thuộc tính thời gian chạy, giá bán. - Hàm thiết lập. - Hàm nhập dữ liệu - Hàm hiển thị dữ liệu. d. Viết chương trình khai báo 2 mảng con đối tượng, một mảng gồm các đối tượng sách, một mảng gồm các đối tượng băng video. Nhập dữ liệu cho các mảng đối tượng, hiển thị dữ liệu của các đối tượng sách và băng video ra màn hình. 3. a. Xây dựng 1 lớp MyAddress mô tả thông tin về địa chỉ của một con người. Lớp gồm các thành phần: - Các thuộc tính mô tả Tỉnh và Huyện. - Hàm thiết lập. - Hàm hủy bỏ. - Hàm hiển thị thông tin về Tỉnh và Huyện. b. Xây dựng một lớp Person mô tả các thông tin về người. Lớp được kế thừa từ lớp MyAddress và bổ sung thêm các thành phần: - Name: Mô tả tên của người. - Phone: Mô tả số điện thoại. - Hàm thiết lập. - Hàm hủy bỏ. c. Xây dựng một lớp Officer mô tả các thông về một cán bộ viên chức, lớp kế thừa từ lớp Person và bổ sung thêm các thành phần: - Salary: Mô tả lương của cán bộ. - Hàm thiết lập. - Hàm hiển thị thông tin về một đối tượng Officer ra màn hình. - Toán tử > để so sánh 2 đối tượng Officer dựa trên Salary. d. Viết chương trình khai báo một mảng 4 con trỏ đối tượng kiểu Officer, nhập dữ liệu, sắp xếp tăng dần theo lương của các đối tượng và hiển thị các đối tượng ra màn hình. 4. a. Xây dựng một lớp Printer mô tả các đối tượng máy in, lớp gồm các thành phần: - Thuộc tính Name mô tả tên máy in. - Thuộc tính Soluong mô tả số lượng trong kho. - Hàm nhapkho(int q) để nhập vào kho q số lượng mặt hàng. - Hàm xuatkho(int q) để xuất ra khỏi kho q số lượng mặt hàng. b. Xây dựng lớp Laser mô tả các máy in Laser, lớp được kế thừa từ lớp Printer và có thêm thuộc tính Dpi. c. Xây dựng lớp ColorPrinter mô tả các máy in màu, lớp được kế thừa từ lớp Printer và có thêm thuộc tính Color. 188 d. Xây dựng lớp ColorLaser mô tả các máy in Laser màu, lớp được kế thừa từ lớp Laser và lớp ColorPrinter. e. Viết chương trình tạo 3 đối tượng kiểu ColorLaser. Gọi các hàm nhập, xuất và in ra số lượng có trong kho. 5. a. Xây dựng một lớp SV để mô tả các sinh viên trong một Khoa của một Trường Đại học, lớp SV gồm các thành phần sau: - Lop: Mô tả lớp học của sinh viên. - Hoten: Mô tả họ tên của sinh viên. - Hàm thiết lập. - Hàm huỷ bỏ. - Hàm hiển thị thông tin về một SV. b. Xây dựng một lớp SVTC để mô tả các sinh viên thuộc hệ tại chức. Lớp được kế thừa từ lớp SV và bổ sung thêm các thành phần sau: - Hocphi: Mô tả học phí. - Hàm thiết lập. - Hàm hiển thị thông tin về một SVTC. c. Xây dựng một lớp SVCN để mô tả các sinh viên thuộc hệ cử nhân. Lớp được kế thừa từ lớp SVTC và bổ sung thêm các thành phần sau: - Dtb: Mô tả điểm trung bình của sinh viên cử nhân. - Hocbong: Mô tả học bổng của sinh viên cử nhân. - Hàm thiết lập. - Hàm hiển thị thông tin về một SVCN. - Toán tử > so sánh 2 đối tượng SVCN theo Dtb. d. Viết chương trình khai báo một mảng 3 con trỏ đối tượng kiểu SVCN, nhập dữ liệu vào để tạo 3 đối tượng SVCN, sắp xếp các đối tượng SVCN theo điểm trung bình giảm dần và in ra màn hình. 6. a. Xây dựng 1 lớp Mytime mô tả thông tin về giờ, phút, giây. Lớp gồm các thành phần: - Các thuộc tính mô tả giờ, phút, giây. - Hàm nhập giờ, phút, giây (không cần biện luận dữ liệu nhập). - Hàm hiển thị thông tin về giờ, phút, giây theo dạng: Giờ : phút : giây. b. Xây dựng 1 lớp Mydate mô tả thông tin ngày, tháng, năm. Lớp gồm các thành phần: - Các thuộc tính mô tả ngày, tháng, năm. - Hàm nhập ngày, tháng, năm (không cần biện luận dữ liệu nhập). - Hàm hiển thị thông tin về ngày, tháng, năm theo dạng: Ngày-tháng-năm. c. Xây dựng 1 lớp Myfile kế thừa từ 2 lớp Mydate và Mytime đồng thời bổ sung thêm các thành phần: - filename: Mô tả tên tệp, là một xâu không quá 255 ký tự. - filesize: Mô tả kích thước tệp, là một số nguyên. - Hàm nhập dữ liệu. - Hàm hiển thị tên tệp, kích thước, ngày tháng năm và giờ phút giây. 189 - Toán tử > để so sánh 2 đối tượng Myfile dựa trên filesize. d. Viết chương trình khai báo một mảng 5 con trỏ đối tượng kiểu Myfile, nhập dữ liệu vào để tạo các đối tượng Myfile. Sắp xếp các đối tượng theo kích thuớc tệp giảm dần và in ra các đối tượng đã sắp. 7. a. Xây dựng 1 lớp MyColor mô tả các thông tin về màu. Lớp gồm các thành phần: - Thuộc tính Color mô tả số hiệu màu là một số nguyên. - Hàm thiết lập. - Hàm hiển thị số hiệu màu. b. Xây dựng 1 lớp Point mô tả các đối tượng điểm trên mặt phẳng. Lớp gồm các thành phần: - Các thuộc tính x, y mô tả toạ độ của điểm. - Hàm thiết lập. - Hàm tịnh tiến điểm đến toạ độ x+dx, y+dy. - Hàm hiển thị toạ độ của điểm trong mặt phẳng. c. Xây dựng 1 lớp Triangle mô tả các đối tượng tam giác. Lớp được kế thừa từ lớp MyColor và bổ sung thêm các thành phần: - Ba đỉnh của tam giác là 3 điểm A, B, C. - Hàm thiết lập tam giác tại 3 điểm X, Y, Z và màu bằng k. - Hàm move(dx, dy) để tịnh tiến tam giác đến vị trí mới: A.x=A.x+dx; A.y=A.y+dy; B.x=B.x+dx; B.y=B.y+dy; C.x=C.x+dx; C.y=C.y+dy; - Hàm hiển thị toạ độ đỉnh của tam giác, màu của tam giác. d. Viết chương trình nhập vào 4 cặp số thực (x1,y1), (x2,y2), (x3,y3), (dx,dy) và một số k. Tạo tam giác với 3 đỉnh A(x1,y1), B(x2,y2), C(x3,y3) có màu bằng k. Tịnh tiến tam giác theo dx, dy. Hiển thị toạ độ và màu của tam giác trước và sau khi tịnh tiến. 8. a. Xây dựng 1 lớp MyColor mô tả các thông tin về màu. Lớp gồm các thành phần: - Thuộc tính Color mô tả số hiệu màu. - Hàm nhập số hiệu màu. - Hàm hiển thị số hiệu màu. b. Xây dựng 1 lớp Point mô tả các điểm trên mặt phẳng. Lớp gồm các thành phần: - Các thuộc tính x, y mô tả toạ độ của điểm. - Hàm nhập toạ độ của điểm. - Hàm hiển thị toạ độ của điểm trong mặt phẳng. - Khai báo một hàm tự do tính khoảng cách giữa hai điểm là bạn với lớp Point. c. Viết một hàm tự do tính khoảng cách giữa 2 điểm. d. Xây dựng 1 lớp Line mô tả các đối tượng đoạn thẳng. Lớp được kế thừa từ lớp MyColor và bổ sung thêm các thành phần: - Hai điểm A, B xác định đoạn thẳng. - Hàm nhập toạ độ của 2 điểm xác định đoạn thẳng và màu đoạn thẳng. - Hàm tính khoảng cách giữa hai điểm xác định đoạn thẳng. 190 - Hàm hiển thị toạ độ của hai điểm xác định đoạn thẳng, màu của đoạn thẳng và khoảng cách giữa hai điểm xác định đoạn thẳng. đ. Viết chương trình nhập dữ liệu để tạo đoạn thẳng xác định bởi hai điểm A, B và màu bằng k. Hiển thị toạ độ của 2 điểm xác định đoạn thẳng, màu của đoạn thẳng và chiều dài của đoạn thẳng. 9. a. Xây dựng 1 lớp MyColor mô tả các thông tin về màu. Lớp gồm các thành phần: - Thuộc tính Color mô tả số hiệu màu. - Hàm thiết lập. - Hàm hiển thị số hiệu màu. b. Xây dựng 1 lớp Point mô tả các điểm trên mặt phẳng. Lớp gồm các thành phần: - Các thuộc tính x, y mô tả toạ độ của điểm. - Hàm thiết lập. - Hàm hiển thị toạ độ của điểm trong mặt phẳng. c. Xây dựng 1 lớp Circle mô tả các đối tượng đường tròn. Lớp được kế thừa từ lớp MyColor và bổ sung thêm các thành phần: - Thuộc tính tâm là 1 điểm O. - Thuộc tính r mô tả bán kính của đường tròn. - Hàm tính diện tích. - Hàm thiết lập. - Hàm hiển thị diện tích, toạ độ tâm, bán kính và màu của đường tròn. - Toán tử > so sánh 2 đối tượng đường tròn dựa trên diện tích. d. Viết chương trình nhập dữ liệu và tạo một mảng n con trỏ đối tượng đường tròn. Tìm và in ra màn hình đường tròn có diện tích lớn nhất. 10. a. Xây dựng 1 lớp MyColor mô tả các màu. Lớp gồm các thành phần: - Thuộc tính Color mô tả số hiệu màu. - Hàm thiết lập. - Hàm hiển thị số hiệu màu. b. Xây dựng 1 lớp Point mô tả các điểm trên mặt phẳng. Lớp gồm các thành phần: - Các thuộc tính x, y mô tả toạ độ của điểm. - Hàm thiết lập. - Khai báo một hàm tự do tính khoảng cách giữa hai điểm là bạn với lớp. - Hàm hiển thị toạ độ của điểm. c. Viết một hàm tự do tính khoảng cách giữa 2 điểm. d. Xây dựng 1 lớp Triangle mô tả các tam giác. Lớp được kế thừa từ lớp MyColor và bổ sung thêm các thành phần: - Ba đỉnh của tam giác là 3 điểm A, B, C. - Hàm thiết lập tam giác tại 3 điểm. - Hàm tính chu vi của tam giác. - Hàm hiển thị toạ độ đỉnh của tam giác, màu và chu vi của tam giác. - Toán tử > để so 2 đối tượng Triangle dựa trên chu vi tam giác. đ. Viết chương trình nhập dữ liệu một mảng n tam giác. Hiển thị toạ độ, màu và chu vi của tam giác của các đối tượng đã tạo. Tìm và in ra màn hình tam giác có diện tích lớn nhất. 11. a. Xây dựng một lớp Printer mô tả các đối tượng máy in. Lớp gồm các thành phần: - Các thuộc tính Sohieu và Soluong mô tả số hiệu máy in và số lượng máy in có trong kho. - Hàm thiết lập. 191 - Hàm Nhapkho(int q) để nhập vào kho q số lượng mặt hàng. - Hàm Xuatkho(int q) để xuất ra khỏi kho q số lượng mặt hàng. - Hàm hiển thị thông tin về một đối tượng máy in gồm: Số liệu, số lượng. b. Xây dựng lớp Laser mô tả các đối tượng máy in Laser. Lớp được kế thừa từ lớp Printer in và bổ sung thêm: - Thuộc tính dpi mô tả số điểm in trên 1 đơn vị in của máy in Laser. - Hàm in thông tin về một đối tượng Laser gồm: Số hiệu, số lượng, dpi. c. Xây dựng lớp ColorLaser mô tả các đối tượng máy in Laser màu. Lớp được kế thừa từ lớp Laser và bổ sung thêm: - Thuộc tính Somau mô tả số màu của máy in màu. - Hàm in thông tin về một đối tượng ColorLaser gồm: Số hiệu, số lượng, dpi, số màu. d. Viết chương trình nhập dữ liệu để tạo 2 đối tượng kiểu ColorLaser. In thông tin về các đối tượng đã nhập. 12. a. Xây dựng 1 lớp Mytime mô tả thông tin về giờ, phút, giây. Lớp gồm các thành phần: - Các thuộc tính mô tả giờ, phút, giây. - Hàm thiết lập. - Hàm nhập thông tin về giờ, phút, giây. - Hàm hiển thị giờ theo 24 giờ dạng: Giờ : phút : giây. b. Xây dựng 1 lớp Mydate mô tả thông tin ngày, tháng, năm. Lớp gồm các thành phần: - Các thuộc tính mô tả ngày, tháng, năm. - Hàm thiết lập. - Hàm nhập thông tin về ngày, tháng, năm. - Hàm hiển thị thông tin về ngày, tháng, năm theo dạng: ngày-tháng-năm. c. Xây dựng 1 lớp Datetime kế thừa từ 2 lớp Mydate và Mytime để mô tả thông tin đồng thời về ngày, tháng, năm, giờ, phút, giây. Lớp gồm các hàm thành phần: - Hàm thiết lập. - Hàm nhập ngày, tháng, năm, giờ, phút, giây. - Hàm hiển thị thời gian gồm: ngày-tháng-năm giờ : phút : giây. d. Viết chương trình nhập dữ liệu vào để tạo 3 đối tượng kiểu Datetime. Gọi hàm hiển thị thời gian của đối tượng đã tạo. 13. a. Xây dựng một lớp SV để mô tả các sinh viên trong một Khoa của một Trường Đại học, lớp SV gồm các thành phần sau: - Lop: Mô tả lớp học của sinh viên. - Hoten: Mô tả họ tên của sinh viên. - Hàm thiết lập. - Hàm huỷ bỏ. - Hàm hiển thị dữ liệu b. Xây dựng một lớp SVTC để mô tả các sinh viên thuộc hệ tại chức. Lớp được kế thừa từ lớp SV và bổ sung thêm các thành phần sau: 192 - Hocphi: Mô tả học phí phải nộp của sinh viên, là một số nguyên. - Hàm thiết lập. - Hàm hiển thị dữ liệu - Toán tử > để so sánh 2 đối tượng SVTC dựa trên Hocphi. c. Xây dựng một lớp SVCN để mô tả các sinh viên thuộc hệ cử nhân. Lớp được kế thừa từ lớp SVTC và bổ sung thêm các thành phần sau: - Dtb: Mô tả điểm trung bình của sinh viên cử nhân. - Hocbong: Mô tả học bổng của sinh viên cử nhân. - Hàm thiết lập - Hàm hiển thị dữ liệu. d. Viết chương trình khai báo một mảng 5 con trỏ đối tượng kiểu SVCN, nhập dữ liệu vào để tạo các đối tượng SVCN. Hiển thị các đối tượng đã tạo ra màn hình, sắp xếp các đối tượng theo học phí giảm dần và hiển thị các đối tượng sau khi sắp xếp. 14. a. Xây dựng 1 lớp MyAddress mô tả thông tin về địa chỉ của một con người. Lớp gồm các thành phần: - Các thuộc tính mô tả Tỉnh và Huyện. - Hàm thiết lập. - Hàm huỷ bỏ. - Hàm hiển thị dữ liệu. b. Xây dựng 1 lớp MyDate mô tả thông tin ngày, tháng, năm. Lớp MyDate gồm có các thành phần: - Các thuộc tính mô tả ngày, tháng, năm. - Hàm thiết lập. - Hàm hiển thị dữ liệu - Toán tử > so sánh 2 đối tượng MyDate c. Xây dựng 1 lớp Person mô tả thông tin về một người. Lớp được kế thừa từ 2 lớp MyDate, MyAddress và bổ sung thêm các thành phần: - Name: Mô tả tên của người, là một xâu không quá 30 ký tự. - Phone: Mô tả số điện thoại, là một số nguyên. - Hàm thiết lập. - Hàm huỷ bỏ. - Hàm hiển thị dữ liệu. d. Viết chương trình khai báo một mảng 4 con trỏ đối tượng kiểu Person. Nhập dữ liệu và sắp xếp dữ liệu tăng dần theo ngày, tháng, năm. Hiển thị dữ liệu đã sắp ra màn hình. 15. a. Xây dựng một lớp Printer mô tả các đối tượng máy in, lớp gồm các thành phần: - Các thuộc tính số hiệu và số lượng trong kho. - Hàm nhapkho(int q) để nhập vào kho q số lượng mặt hàng. - Hàm xuatkho(int q) để xuất ra khỏi kho q số lượng mặt hàng. b. Xây dựng lớp Laser mô tả các máy in Laser, lớp được kế thừa từ lớp Printer và có thêm thuộc tính Dpi. 193 c. Xây dựng lớp ColorPrinter mô tả các máy in màu, lớp được kế thừa từ lớp Printer và có thêm thuộc tính Color. d. Xây dựng lớp ColorLaser mô tả các máy in Laser màu, lớp được kế thừa từ lớp Laser và lớp ColorPrinter. e. Viết chương trình tạo 3 đối tượng kiểu ColorLaser. Gọi các hàm nhập, xuất và in ra số lượng có trong kho. 194 Chương 6 KHUÔN HÌNH Khuôn hình (template) là một trong những tính năng mạnh của lập trình hướng đối tượng. Trong chương này sẽ cung cấp cho người đọc những kiến thức và cách xây dựng khuôn hình hàm, khuôn hình lớp. 6.1. KHUÔN HÌNH HÀM 6.1.1. Khái niệm Hàm quá tải cho phép dùng một tên duy nhất cho nhiều hàm để thực hiện các công việc khác nhau. Khái niệm khuôn hình hàm cũng cho phép sử dụng cùng một tên duy nhất để thực hiện các công việc khác nhau, tuy nhiên so với định nghĩa hàm quá tải, nó có phần mạnh hơn và chặt chẽ hơn. Mạnh hơn vì chỉ cần viết định nghĩa khuôn hình hàm một lần, rồi sau đó chương trình biên dịch làm cho nó thích ứng với các kiểu dữ liệu khác nhau. Chặt chẽ hơn bởi vì dựa theo khuôn hình hàm, tất cả các hàm thể hiện được sinh ra bởi chương trình dịch sẽ tương ứng với cùng một định nghĩa và như vậy sẽ có cùng một giải thuật. 6.1.2. Tạo một khuôn hình hàm Giả thiết rằng chúng ta cần viết một hàm min đưa ra giá trị nhỏ nhất trong hai giá trị có cùng kiểu. Ta có thể viết một định nghĩa như thế với kiểu int như sau: int min (int a, int b) { if (a<b) return a; else return b; } Nếu ta muốn sử dụng hàm min cho kiểu double, float, char,.... ta lại phải viết lại định nghĩa hàm min, ví dụ: float min (float a, float b){ if (a < b) return a; else return b; } Như vậy ta phải viết rất nhiều định nghĩa hàm hoàn toàn tương tự nhau, chỉ có kiểu dữ liệu là thay đổi. Chương trình dịch C++ cho phép giải quyết đơn giản vấn đề trên bằng cách định nghĩa một khuôn hình hàm duy nhất theo cú pháp: template<danh sách tham số kiểu> <kiểu trả về> tên hàm(khai báo tham số) { // định nghĩa hàm } trong đó <danh sách tham số kiểu> là các kiểu dữ liệu được khai báo với từ khoá class, cách nhau bởi dấu phẩy. Kiểu dữ liệu là một kiểu bất kỳ, kể cả kiểu class. Ví dụ 6.1: Xây dựng khuôn hình cho hàm tìm giá trị nhỏ nhất của hai số: template <class Kieuso> Kieuso min(Kieuso a, Kieuso b) { if (a<b) return a; else return b; } 195 6.1.3. Sử dụng khuôn hình hàm Để sử dụng khuôn hình hàm min vừa tạo ra, chỉ cần sử dụng hàm min trong những điều kiện phù hợp, trong trường hợp này là hai tham số của hàm phải cùng kiểu dữ liệu. Như vậy, nếu trong một chương trình có hai tham số nguyên n và m (kiểu int) với lời gọi min(n,m) thì chương trình dịch tự động sản sinh ra hàm min(), gọi là một hàm thể hiện, tương ứng với hai tham số kiểu int. Nếu chúng ta gọi min() với hai tham số kiểu float, chương trình biên dịch cũng tự động sản sinh một hàm thể hiện min khác tương ứng với các tham số kiểu float và cứ thế với các kiểu dữ liệu khác. Chú ý: Các biến truyền cho danh sách tham số của hàm phải chính xác với kiểu khai báo. Muốn áp dụng được với kiểu lớp thì trong lớp phải định nghĩa các toán tử tải bội tương ứng. 6.1.4. Các tham số kiểu của khuôn hình hàm Khuôn hình hàm có thể có một hay nhiều tham số kiểu, mỗi tham số đi liền sau từ khoá class. Các tham số này có thể ở bất kỳ đâu trong định nghĩa của khuôn hình hàm, nghĩa là: Trong dòng tiêu đề (ở dòng đầu khai báo template). Trong các khai báo biến cục bộ. - Trong các chỉ thị thực hiện. Trong mọi trường hợp, mỗi tham số kiểu phải xuất hiện ít nhất một lần trong khai báo danh sách tham số hình thức của khuôn hình hàm. Điều đó hoàn toàn logic, bởi vì nhờ các tham số này, chương trình dịch mới có thể sản sinh ra hàm thể hiện cần thiết. Ở khuôn hình hàm min trên, mới chỉ cho phép tìm min của hai số cùng kiểu, nếu muốn tìm min hai số khác kiểu thì khuôn hình hàm trên chưa đáp ứng được. Ví dụ sau sẽ khắc phục được điều này. Ví dụ 6.2: #include <iostream.h> template <class kieuso1,class kieuso2> kieuso1 min(kieuso1 a,kieuso2 b) { return a<b ? a : b; } void main() { float a =2.5; int b = 8; cout << "so nho nhat la :" << min(a,b); } Ví dụ 6.3: Giả sử trong lớp SO các số int đã xây dựng, ta có xây dựng các toán tử tải bội < , << cho các đối tượng của class SO. Nội dung file như sau: class SO { private: int giatri; public: SO(int x=0) {giatri = x; } SO (SO &tso) { giatri = tso.giatri; } SO (){}; //Giong nhu ham thiet lap ngam dinh ~SO() { } int operator<(SO & s) { return (giatri <s.giatri); } friend istream& operator>>(istream&,SO&); friend ostream& operator<<(ostream&,SO&); }; Chương trình sau đây cho phép thử hàm min trên hai đối tượng kiểu class: 196 Ví dụ 6.4: #include <iostream.h> #include <ttclsso.h> template <class kieuso1,class kieuso2> kieuso1 min(kieuso1 a,kieuso2 b) { if (a<b) return a; return b;} void main() { float a =2.5; int b = 8; cout << "so nho nhat la :" << min(a,b)<<endl; SO so1(15),so2(20); cout << "so nho nhat la :" << min(so2,so1); } else 6.1.5. Định nghĩa chồng các khuôn hình hàm Tương tự việc định nghĩa các hàm quá tải, C++ cho phép định nghĩa chồng các khuôn hình hàm, tức là có thể định nghĩa một hay nhiều khuôn hình hàm có cùng tên nhưng với các tham số khác nhau. Điều đó sẽ tạo ra nhiều họ các hàm (mỗi khuôn hình hàm tương ứng với họ các hàm). Ví dụ có ba họ hàm min: Một họ gồm các hàm tìm giá trị nhỏ nhất trong hai giá trị; Một họ gồm các hàm tìm giá trị nhỏ nhất trong ba giá trị; Một họ gồm các hàm tìm giá trị nhỏ nhất trong một mảng giá trị. Một cách tổng quát, ta có thể định nghĩa một hay nhiều khuôn hình cùng tên, mỗi khuôn hình có các tham số kiểu cũng như là các tham số biểu thức riêng. Hơn nữa, có thể cung cấp các hàm thông thường với cùng tên với cùng một khuôn hình hàm, trong trường hợp này ta nói đó là sự cụ thể hoá một hàm thể hiện. Trong trường hợp tổng quát khi có đồng thời cả hàm quá tải và khuôn hình hàm, chương trình dịch lựa chọn hàm tương ứng với một lời gọi hàm dựa trên các nguyên tắc sau: Đầu tiên, kiểm tra tất cả các hàm thông thường cùng tên và chú ý đến sự tương ứng chính xác; nếu chỉ có một hàm phù hợp, hàm đó được chọn; Còn nếu có nhiều hàm cùng thỏa mãn sẽ tạo ra một lỗi biên dịch và quá trình tìm kiếm bị gián đoạn. Nếu không có hàm thông thường nào tương ứng chính xác với lời gọi, khi đó ta kiểm tra tất cả các khuôn hình hàm có trùng tên với lời gọi, khi đó ta kiểm tra tất cả các khuôn hình hàm có trùng tên với lời gọi; nếu chỉ có một tương ứng chính xác được tìm thấy, hàm thể hiện tương ứng được sản sinh và vấn đề được giải quyết; còn nếu có nhiều hơn một khuôn hình hàm điều đó sẽ gây ra lỗi biên dịch và quá trình dừng. Cuối cùng, nếu không có khuôn hình hàm phù hợp, ta kiểm tra một lần nữa tất cả các hàm thông thường cùng tên với lời gọi. Trong trường hợp này chúng ta phải tìm kiếm sự tương ứng dựa vào cả các chuyển kiểu cho phép trong C/C++. 6.2. KHUÔN HÌNH LỚP 6.2.1. Khái niệm Bên cạnh khái niệm khuôn hình hàm, C++ còn cho phép định nghĩa khuôn hình lớp. Cũng giống như khuôn hình hàm, ở đây ta chỉ cần viết định nghĩa các khuôn hình lớp một lần rồi sau đó có thể áp dụng chúng với các kiểu dữ liệu khác nhau để được các lớp thể hiện khác nhau. 197 6.2.2. Tạo một khuôn hình lớp Trong chương trước ta đã định nghĩa cho lớp SO, giá trị các số là kiểu int. Nếu ta muốn làm việc với các số kiểu float, double, ... thì ta phải định nghĩa lại một lớp khác tương tự, trong đó kiểu dữ liệu int cho dữ liệu giatri sẽ được thay bằng float, double,... Để tránh sự trùng lặp trong các tình huống như trên, chương trình dịch C++ cho phép định nghĩa một khuôn hình lớp và sau đó, áp dụng khuôn hình lớp này với các kiểu dữ liệu khác nhau để thu được các lớp thể hiện như mong muốn. Ví dụ 6.5: template <class kieuso> class SO { kieuso giatri; public: SO (kieuso x =0); void Hienthi(); ... }; Cũng giống như các khuôn hình hàm, template <class kieuso> xác định rằng đó là một khuôn hình trong đó có một tham số kiểu kieuso . C++ sử dụng từ khoá class chỉ để nói rằng kieuso đại diện cho một kiểu dữ liệu nào đó. Việc định nghĩa các hàm thành phần của khuôn hình lớp, người ta phân biệt hai trường hợp: Khi hàm thành phần được định nghĩa bên trong định nghĩa lớp thì không có gì thay đổi. Khi hàm thành phần được định nghĩa bên ngoài lớp, khi đó cần phải nhắc lại cho chương trình biết các tham số kiểu của khuôn hình lớp, có nghĩa là phải nhắc lại template <class kieuso> chẳng hạn, trước định nghĩa hàm. Ví dụ 6.6: hàm Hienthi() được định nghĩa ngoài lớp: template <class kieuso> void SO<kieuso>::Hienthi() { cout <<giatri; } 6.2.3. Sử dụng khuôn hình lớp Sau khi một khuôn hình lớp đã được định nghĩa, nó sẽ được dùng để khai báo các đối tượng theo dạng sau: Tên_lớp <Kiểu> Tên_đối_tượng; Ví dụ câu lệnh khai báo SO <int> so1; sẽ khai báo một đối tượng so1 có thành phần dữ liệu giatri có kiểu nguyên int. SO <int> có vai trò như một kiểu dữ liệu lớp; người ta gọi nó là một lớp thể hiện của khuôn hình lớp SO. Một cách tổng quát, khi áp dụng một kiểu dữ liệu nào đó với khuôn hình lớp SO ta sẽ có được một lớp thể hiện tương ứng với kiểu dữ liệu. Tương tự với các khai báo SO <float> so2; cho phép khai báo một đối tượng so2 mà thành phần dữ liệu giatri có kiểu float. Ví dụ 6.7: #include <iostream.h> #include <conio.h> template <class kieuso> class SO { kieuso giatri; public : SO (kieuso x =0); void Hienthi(){ cout<<"Gia tri cua so :"<<giatri<<endl; } }; void main() 198 { clrscr(); SO <int> soint(10); soint.Hienthi(); SO <float> sofl(25.4); sofl.Hienthi(); getch(); } Kết quả thực thi là: Gia tri cua so : 10 Gia tri cua so : 25.4 6.2.4. Các tham số trong khuôn hình lớp Hoàn toàn giống như khuôn hình hàm, các khuôn hình lớp có thể có các tham số kiểu và tham số biểu thức. Ví dụ một lớp mà các thành phần có các kiểu dữ liệu khác nhau được khai báo theo dạng: template <class T, class U,.... class Z> class <ten lop>{ T x; U y; ..... Z fct1 (int); ..... }; Một lớp thể hiện được khai báo bằng cách liệt kê đằng sau tên khuôn hình lớp các tham số thực, là tên kiểu dữ liệu, với số lượng bằng các tham số trong danh sách của khuôn hình lớp (template<...>) Khuôn hình lớp/hàm là phương tiện mô tả ý nghĩa của một lớp/hàm tổng quát, còn lớp/hàm thể hiện là một bản sao của khuôn hình tổng quát với các kiểu dữ liệu cụ thể. Các khuôn hình lớp/hàm thường được tham số hoá. Tuy nhiên vẫn có thể sử dụng các kiểu dữ liệu cụ thể trong các khuôn hình lớp/hàm nếu cần. 6.3. MỘT SỐ VÍ DỤ 6.3.1. Viết khuôn hình hàm để sắp xếp kiểu dữ liệu bất kỳ Sắp xếp theo chiều tăng dần. #include <iostream.h> #include <conio.h> template <class x> void sx(x a[], int n) { x *p, *q; for (int i=0; i<n-1; i++) for (int j=i+1; j<n; j++) { p=&a[i]; q=&a[j]; if (*p>*q) { x tg=*p; *p=*q; 199 *q=tg; } } } void main() { int a[50], n; cout<<"Nhap so phan tu n= "; cin>>n; for (int i=0; i<n; i++) { cin>>a[i]; } sx(a,n); cout<<"Day sau khi sap xep: "; for (int i=0; i<n; i++) { cout<<a[i]<<" "; } getch(); } 6.3.2. Cài đặt và sử dụng hàm template cho mảng 1 chiều số nguyên Mục đích dùng để cài đặt những hàm tổng quát dùng chung cho các kiểu dữ liệu khác nhau Nội dung file Header.h #include <iostream> using namespace std; #define MAX 100 template < class T, int n > void Nhap(T a[]); template < class T, int n > void Xuat(T a[]); template < class T, int n > T TimMin(T a[]); template < class T, int n > void Nhap(T a[MAX]) { for (int i = 0; i < n; i++) { cout << "Nhap gia tri cho vi tri " << i << ": "; cin >> a[i]; } } template < class T, int n > void Xuat(T a[]) { cout << "So phan tu: " << n << endl; for (int i = 0; i < n; i++) { cout << a[i] << "\t"; } } template < class T, int n > T TimMin(T a[]) { T min = a[0]; 200 for (int i = 1; i < n; i++) { if (a[i] < min) min = a[i]; } return min; } Nội dung file main.cpp #include "Header.h" void main() { cout << "Vi du mang so nguyen a, 5 phan tu" << endl; int a[MAX], kq; const int n = 5; Nhap<int, n>(a); Xuat<int, n>(a); kq = TimMin<int, n>(a); cout << "\nGia tri min: " << kq << endl; cout << "Vi du mang so thuc b, 5 phan tu" << endl; float b[MAX], kq2; const int m = 5; Nhap<float, m>(b); Xuat<float, m>(b); kq2 = TimMin<float, m>(b); cout << "\nGia tri min: " << kq2 << endl; } 6.3.3. Cài đặt và sử dụng lớp template cho mảng 1 chiều Mục đích dùng để cài đặt những lớp có thuộc tính (thành viên dữ liệu) và phương thức (thành viên hàm) tổng quát dùng chung cho các đối tượng có kiểu dữ liệu thuộc tính khác nhau. Nội dung file Mang1Chieu.h #pragma once #include <iostream> using namespace std; template <class T, int n> class CMang1Chieu { T *a; public: CMang1Chieu(); CMang1Chieu(T *aa); CMang1Chieu(CMang1Chieu &arr); ~CMang1Chieu(); int TimX(T x); void Print(); 201 }; template <class T, int n> CMang1Chieu<T, n>::CMang1Chieu() { n = 0; a = new T[n]; } template <class T, int n> CMang1Chieu<T, n>::CMang1Chieu(T *aa) { a = new T[n]; for (int i = 0; i < n; i++) { a[i] = aa[i]; } } template <class T, int n> CMang1Chieu<T, n>::CMang1Chieu(CMang1Chieu &arr) { n = arr.n; a = new T[n]; for (int i = 0; i < n; i++) { a[i] = arr.a[i]; } } template <class T, int n> CMang1Chieu<T, n>::~CMang1Chieu() { delete []a; } template <class T, int n> void CMang1Chieu<T, n>::Print() { for (int i = 0; i < n; i++) { cout << a[i] << "\t"; } cout << endl; } template <class T, int n> int CMang1Chieu<T, n>::TimX(T x) { for (int i = 0; i < n; i++) 202 { if (a[i] == x) return i; } return -1; } Nội dung file Mang1Chieu.cpp #include "Mang1Chieu.h" void main() { int a[5] = { 1, 3, 9, 2, 7 }, x; CMang1Chieu<int, 5> arr(a); arr.Print(); cout << "Nhap gia tri x can tim: "; cin >> x; int kq = arr.TimX(x); if (kq == -1) cout << "\nKhong co phan tu " << x << endl; else cout << "Phan tu " << x << " xuat hien tai vi tri: " << kq << endl; float b[5] = { 1.5, 3.2, 3.9, 2.2, 8.7 }, x2; CMang1Chieu<float, 5> arr2(b); arr2.Print(); cout << "Nhap gia tri x can tim: "; cin >> x2; int kq2 = arr2.TimX(x2); if (kq2 == -1) cout << "\nKhong co phan tu " << x2 << endl; else cout << "Phan tu " << x2 << " xuat hien tai vi tri: " << kq2 << endl; } 6.3.4. Cài đặt và sử dụng operator trên lớp template điểm trong mặt phẳng Minh hoạ kỹ thuật cài đặt operator +, -, >, <, ...; cài đặt operator >>, << cho lớp template CToaDo và cách sử dụng. Nội dung file ToaDo.h #pragma once #include <iostream> using namespace std; template <class T> class CToaDo { T x, y; public: 203 CToaDo(); CToaDo(T xx, T yy); CToaDo(CToaDo &td); ~CToaDo(); CToaDo operator +(CToaDo td2); CToaDo operator -(CToaDo td2); CToaDo operator +=(CToaDo td2); CToaDo operator -=(CToaDo td2); bool operator >(CToaDo td2); bool operator >=(CToaDo td2); bool operator <(CToaDo td2); bool operator <=(CToaDo td2); bool operator !=(CToaDo td2); bool operator ==(CToaDo td2); CToaDo operator ++(); CToaDo operator --(); CToaDo operator ++(int); CToaDo operator --(int); CToaDo operator -(); friend istream& operator >>(istream &is, CToaDo &td) { cout << "Nhap vao hoanh do: "; is >> td.x; cout << "Nhap vao tung do: "; is >> td.y; return is; } friend ostream& operator <<(ostream &os, CToaDo &td) { os << "(" << td.x << "," << td.y << ")"; return os; } }; template <class T> CToaDo<T>::CToaDo() { x = 0; y = 0; } template <class T> CToaDo<T>::CToaDo(T xx, T yy) { x = xx; y = yy; } template <class T> CToaDo<T>::CToaDo(CToaDo &td) { 204 } x = td.x; y = td.y; template <class T> CToaDo<T>::~CToaDo() { } template <class T> CToaDo<T> CToaDo<T>::operator +(CToaDo td2) { T x1 = x + td2.x; T y1 = y + td2.y; return CToaDo(x1, y1); } Nội dung file ToaDo.cpp #include "ToaDo.h" void main() { CToaDo<int> d1(4, 5); cout << "Khoi tao toa do diem d1" << d1 << endl; CToaDo<int> d2; cout << "* Nhap vao toa do diem d2" << endl; cin >> d2; cout << "Toa do diem d2" << d2 << endl; CToaDo<int> d3 = d1 + d2; cout << d1 << " + " << d2 << " = " << d3 << endl; } 205 CÂU HỎI, BÀI TẬP 1. a. Xây dựng 1 lớp Frac mô tả các phân số gồm: - Các thuộc tính a, b là các số thực mô tả tử số và mẫu số của phân số. - Hàm thiết lập. - Hàm nhập 1 phân số. - Hàm in 1 phân số - Định nghĩa toán tử > để so sánh hai phân số. - Định nghĩa toán tử = để gán một phân số cho một phân số. b. Xây dựng 1 khuôn hình hàm max để tìm phần tử lớn nhất của một dãy các phần tử nguyên, thực, ký tự, phân số. c. Viết chương trình: - Nhập vào một mảng n phân số, in ra phân số lớn nhất. - Nhập vào một mảng n số thực, in ra số lớn nhất. 2. a. Xây dựng 1 lớp Frac mô tả các phân số gồm: - Các thuộc tính a, b là các số thực mô tả tử số và mẫu số của phân số. - Hàm thiết lập. - Hàm nhập 1 phân số. - Hàm in 1 phân số dạng. - Định nghĩa toán tử + để cộng hai phân số. - Định nghĩa toán tử = để gán một phân số cho một phân số. b. Xây dựng 1 khuôn hình hàm sum để tính tổng của n phần tử nguyên, thực, phân số. c. Viết chương trình: - Nhập vào một mảng n phân số, tính và in ra tổng các phân số. - Nhập vào một mảng n số nguyên, tính và in ra tổng các phần tử. 3. Xây dựng khuôn hình hàm để tìm giá trị lớn nhất của 1 mảng dữ liệu các số nguyên, số thực, xâu ký tự gồm n phần tử. Viết chương trình nhập vào 1 mảng 3 xâu ký tự, in ra giá trị lớn nhất của mảng đó. 206 Chương 7 THIẾT KẾ CHƯƠNG TRÌNH THEO HƯỚNG ĐỐI TƯỢNG Trong chương này, chúng ta tìm hiểu một ít về cách thiết kế chương trình theo quan điểm hướng đối tượng, những bước cơ bản cần thiết khi bắt tay vào viết chương trình trên quan điểm thiết kế và lập trình hướng đối tượng. 7.1. CÁC GIAI ĐOẠN PHÁT TRIỂN HỆ THỐNG Có năm giai đoạn để phát triển hệ thống phần mềm theo hướng đối tượng: Phân tích yêu cầu (Requirement analysis); Phân tích (Analysis); Thiết kế (Design); Lập trình (Programming); Kiểm tra (Testing). 7.1.1. Phân tích yêu cầu Bằng việc tìm hiểu các use case để nắm bắt các yêu cầu của khách hàng, của vấn đề cần giải quyết. Dựa vào use case xác định các nhân tố bên ngoài có tham gia vào hệ thống cũng được mô hình hóa bằng các tác nhân (actor). Mỗi use case được mô tả bằng văn bản, đặc tả yêu cầu của khách hàng. 7.1.2. Phân tích Từ các đặc tả yêu cầu trên, hệ thống sẽ bước đầu được mô hình hóa bởi các khái niệm lớp, đối tượng và các cơ chế để diễn tả hoạt động của hệ thống Trong giai đoạn phân tích, ta chỉ mô tả các lớp trong lĩnh vực của vấn đề cần giải quyết chứ không đi sâu vào các chi tiết kỹ thuật. 7.1.3. Thiết kế Các kết quả của quá trình phân tích được mở rộng thành một giải pháp kỹ thuật. Một số các lớp được thêm vào để cung cấp cơ sở hạ tầng kỹ thuật như lớp giao diện, lớp cơ sở dữ liệu, lớp chức năng, … 7.1.4. Lập trình Giai đoạn này sẽ đặc tả chi tiết kết quả của giai đoạn thiết kế. Các lớp của bước thiết kế sẽ được chuyển thành mã nguồn theo một ngôn ngữ lập trình theo hướng đối tượng nào đó. 7.1.5. Kiểm tra Kiểm tra: có bốn hình thức kiểm tra hệ thống. Kiểm tra từng đơn thể (unit testing): dùng kiểm tra các lớp hoặc các nhóm đơn. Kiểm tra tính tích hợp (integration testing): kết hợp với các thành phần và các lớp để kiểm tra xem chúng hoạt động với nhau có đúng không. Kiểm tra hệ thống (system testing): kiểm tra xem hệ thống có đáp ứng được chức năng mà người dùng yêu cầu không. 207 Kiểm tra tính chấp nhận được (acceptance testing): được thực hiện bởi khách hàng, việc kiểm tra cũng thực hiện giống như kiểm tra hệ thống. 7.2. CÁC BƯỚC ĐỂ THIẾT KẾ CHƯƠNG TRÌNH Để thiết kế một chương trình theo hướng đối tượng, ta phải trải qua bốn bước sau: Xác định các dạng đối tượng (lớp) của bài toán (định danh các đối tượng). 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 sẻ. Xác định được 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. Từ lớp cơ sở, sử dụng quan hệ tổng quát hóa để đặc tả trong việc đưa ra các lớp dẫn xuất chứa các thành viên, những đặc tính không chung còn lại của dạng đối tượng. Từ đó xây dựng được một cây kế thừa và các mối quan hệ giữa các lớp. Đối với hệ thống phức tạp hơn, cần phải phân tích để giải quyết được vấn đề đặt ra theo nguyên tắc: Phân tích một cách cẩn thận về các đối tượng của bài toán theo trật tự từ dưới lên (bottom up). Tìm ra những gì tồn tại chung giữa các đối tượng, nhóm các đặc tính này lại để được các lớp cơ sở. Hình 7.1: Tìm đặc tính chung Tiếp tục theo hướng từ dưới lên, ta thiết kế được các đối tượng phù hợp Hình 7.2: Tìm đặc tính chung cấp cao hơn Bằng cách này, tiếp tục tìm các đặc tính chung cho đến tột cùng của các đối tượng Sau đó cài đặt theo hướng đối tượng từ trên xuống bằng cách cài đặt lớp cơ sở chung nhất Tiếp tục cài đặt các lớp dẫn xuất trên cơ sở các đặc tính chung của từng nhóm đối tượng Cho đến khi tất cả các dạng đối tượng của hệ thống được cài đặt xong để được cây kế thừa 208 7.3. MỘT SỐ VÍ DỤ 7.3.1. Ví dụ 1 Tính tiền lương của các nhân viên trong cơ quan theo các dạng: Biến chế: người lao động lãnh lương từ ngân sách nhà nước được gọi là cán bộ, công chức. Hợp đồng: người lao động lãnh lương từ ngân sách của cơ quan được gọi là người làm. Hệ thống có hai đối tượng: biên chế và hợp đồng. Hai loại đối tượng này có đặc tính chung đó là viên chức làm việc cho cơ quan. Tạo lớp cơ sở để quản lý một viên chức (lớp CNguoi) bao gồm mã số, họ tên và lương. Xây dựng các lớp còn lại kế thừa từ lớp cơ sở trên. Lớp dành cho cán bộ, công chức (lớp CBienChe) gồm các thuộc tính: hệ số lương, tiền phụ cấp chức vụ. Lớp dành cho người làm hợp đồng (lớp CHopDong) gồm các thuộc tính: tiền công lao động, số ngày làm việc trong tháng, hệ số vượt giờ. Hình 7.3: Sơ đồ lớp #define MAX_TEN 50 #define MAX_MASO 5 #define MUC_CO_BAN 120000 class CNguoi { protected: char HoTen[MAX_TEN+1]; char MaSo[MAX_MASO+1]; double Luong; public: CNguoi(); virtual void TinhLuong()=0; void Xuat() const; virtual void Nhap(); }; 209 CNguoi::CNguoi() { strcpy(HoTen,""); strcpy(MaSo,""); Luong=0; } void CNguoi::Xuat() const { cout<<"Ma so:"<<MaSo; cout<<",Ho ten:"<<HoTen; cout<<",Luong:"<<Luong<<endl; } void CNguoi::Nhap() { cout<<"Ma so:"; cin>>MaSo; cin.ignore(); cout<<"Ho ten:"; cin.getline(HoTen,MAX_TEN); } class CBienChe: public CNguoi { protected: double HeSoLuong; double HeSoPhuCap; public: CBienChe(); virtual void TinhLuong(); virtual void Nhap(); }; CBienChe::CBienChe() { HeSoLuong=HeSoPhuCap=0; } void CBienChe::Nhap() { CNguoi::Nhap(); cout<<"He so luong:"; cin>>HeSoLuong; cout<<"He so phu cap chuc vu:"; cin>>HeSoPhuCap; } void CBienChe::TinhLuong() { Luong=MUC_CO_BAN*(1.0+HeSoLuong+HeSoPhuCap); } class CHopDong : public CNguoi { protected: double TienCong; 210 double NgayCong; double HeSoVuotGio; public: CHopDong(); virtual void TinhLuong(); virtual void Nhap(); }; CHopDong::CHopDong() { TienCong=NgayCong=HeSoVuotGio=0; } void CHopDong::Nhap() { CNguoi::Nhap(); cout<<"Tien cong:"; cin>>TienCong; cout<<"Ngay cong:"; cin>>NgayCong; cout<<"He so vuot gio:"; cin>>HeSoVuotGio; } void CHopDong::TinhLuong() { Luong=TienCong*NgayCong*(1+HeSoVuotGio); } 7.3.2. Ví dụ 2 Giả sử cuối năm học cần trao giải thưởng cho các sinh viên xuất sắc và các giảng viên có nhiều công trình khoa học được công bố trên tạp chí. Hình 7.4: Sơ đồ lớp #define MAX_TEN class CNguoi { protected: 50 211 char HoTen[MAX_TEN+1]; public: CNguoi(char *ht); virtual bool DuocKhenThuong() const=0; virtual void Xuat() const=0; }; CNguoi::CNguoi(char *ht) { strcpy(HoTen,ht); } class CSinhVien : public CNguoi { protected: double DiemTB; public: CSinhVien(char *ht,double dtb); virtual bool DuocKhenThuong() const; virtual void Xuat() const; }; CSinhVien::CSinhVien(char *ht,double dtb):CNguoi(ht) { DiemTB=dtb; } bool CSinhVien::DuocKhenThuong() const { return DiemTB>9.0; } void CSinhVien::Xuat() const { cout<<"Ho va ten cua sinh vien:"<<HoTen<<endl; } class CGiangVien : public CNguoi { protected: int SoBaiBao; public: CGiangVien(char *ht,int sbb); virtual bool DuocKhenThuong() const; virtual void Xuat() const; }; CGiangVien::CGiangVien(char *ht,int sbb):CNguoi(ht) { SoBaiBao=sbb; } bool CGiangVien::DuocKhenThuong() const { return SoBaiBao>5; } void CGiangVien::Xuat() const { 212 cout<<"Ho va ten cua giang vien:"<<HoTen<<endl; } 7.4. KỸ THUẬT THIẾT KẾ MỘT LỚP ĐỐI TƯỢNG 7.4.1. Thiết kế thuộc tính Đối với mỗi đối tượng, xác định các thông tin cần lưu trữ. Sau đó lập bảng mô tả thuộc tính như sau: Bảng 7.1: Mô tả thuộc tính Stt Thuộc tính Kiểu/ lớp Ràng buộc Diễn giải Nếu có ràng buộc liên thuộc tính Bảng 7.2: Mô tả thuộc tính trong trường hợp có ràng buộc Stt Mô tả ràng buộc Thuộc tính liên quan Ghi chú Ràng buộc trên lớp là các quy định, quy tắc áp đặt trên các giá trị thuộc tính của đối tượng sao cho đối tượng này thể hiện đúng với thực tế. Ràng buộc tĩnh: ràng buộc trên giá trị thuộc tính. Ràng buộc trên thuộc tính (Ràng buộc MGT); Ràng buộc liên thuộc tính; Ràng buộc động: ràng buộc trên biến đổi giá trị thuộc tính. Ví dụ 7.1: “Lương của nhân viên ít nhất là 1.500.000 đồng” Ràng buộc tĩnh. “Lương của nhân viên chỉ có thể tăng” Ràng buộc động. Ví dụ 7.2: Xét lớp điểm ký tự (CDiemKT) trên cửa sổ Console. Bảng 7.3: Ràng buộc lớp điểm STT Thuộc tính Kiểu/ lớp Ràng buộc Diễn giải 1 x Số nguyên 0 ≤ x < Kích thước ngang Cột 2 y Số nguyên 0 ≤ y < Kích thước dọc Dòng 3 ch Ký tự Ký tự hiển thị Xét lớp hình chữ nhật 213 Hình 7.4: Lớp hình chữ nhật Bảng 7.4. Ràng buộc của lớp hình chữ nhật STT Thuộc tính 1 goc CDiemKT 2 cngang Số nguyên 1<ngang< K/thước ngang Chiều ngang 3 cdung Số nguyên 1<dung< K/thước dọc Chiều đứng Kiểu/ lớp Ràng buộc Diễn giải Toạ độ góc Bảng 7.5: Ràng buộc của lớp hình chữ nhật (tt) STT Mô tả ràng buộc Thuộc tính liên quan 1 Tổng của hoành độ góc và m nhỏ hơn kích thước ngang Goc, m 2 Tổng của tung độ góc và n nhỏ hơn kích thước dọc Goc, n Ghi chú Mô tả ràng buộc liên thuộc tính cho lớp Cdate Bảng 7.6: Ràng buộc cho lớp Cdate STT Mô tả ràng buộc Thuộc tính liên quan 1 Nếu Th là 4, 6, 9, 11 thì Ng tối đa là 30 Ng, Th 2 Nếu Th là 2 và Nm nhuận thì Ng tối đa là 29 Nếu Th là 2 và Nm không nhuận thì Ng tối đa là 28 Ng, Th, Nm 7.4.2. Thiết kế các hành động của lớp 1. 2. 3. 4. 5. 214 Nhóm kiểm tra ràng buộc: Kiểm tra tính hợp lệ giá trị thuộc tính của đối tượng; Nhóm khởi tạo: Cung cấp giá trị ban đầu cho đối tượng; Nhóm cập nhật: Thay đổi giá trị thuộc tính của đối tượng; Nhóm xử lý tính toán: Xử lý tính toán các yêu cầu từ thông tin của đối tượng; Nhóm cung cấp thông tin: Cung cấp thuộc tính nội bộ của đối tượng. Ghi chú 2. Khởi tạo 1. Kiểm tra ràng buộc 3. Cập nhật 4. Xử lý, tính toán 5. Cung cấp thông tin Hình 7.5: Thiết kế hành động của lớp 7.4.3. Mẫu cài đặt ràng buộc Mẫu: KiemTra... ( tham số ) Giá trị trả về: o true: Thoả ràng buộc. o false: Không thoả ràng buộc. Tham số: o Ràng buộc miền giá trị: Chỉ có 1 tham số ứng với tham số cần kiểm tra. Tên phương thức. Bắt đầu bằng dãy kí tự KiemTra o Ràng buộc miền giá trị: Ghép thêm tên thuộc tính; o Ràng buộc liên thuộc tính: Ghép thêm số thứ tự ràng buộc. Ví dụ 7.3: Cài đặt ràng buộc cho lớp CHCN class CHCN { private: CDIEM Goc; int ngang, dung; public: bool KiemTraNgang(int ng); public bool KiemTraDung(int d); public bool KiemTra1(int ng, CDiem X); public bool KiemTra2(int d, CDiem Y); }; Cài đặt phương thức khởi tạo và cập nhật Các phương thức thuộc nhóm khởi tạo và cập nhật có liên quan đến ràng buộc phải được bổ sung thêm kiểm tra ràng buộc; Việc kiểm tra tham số thoả hoặc không thoả ràng buộc bằng cách gọi phương thức kiểm tra ràng buộc tương ứng. bool Tên hàm ( Tham số ) { //Trả về true: thực hiện được //Trả về false: không thực hiện được bool kq = false; if (Tham số thoả ràng buộc) bool 215 { gán giá trị tương ứng cho thuộc tính của lớp kq=true; } return kq; } hoặc bool { Tên hàm ( Tham số ) //Trả về true: thực hiện được, false: không thực hiện được if (Tham số không thoả ràng buộc) return false; gán giá trị tương ứng cho thuộc tính của lớp return true; } bool CHCN::CapNhatX(int xx) { if(!KiemTraX(xx)) return false; x=xx; return true; } bool CHCN::CapNhatM(int mm) { if(!KiemTra1(mm, Goc)) return false; m=mm; return true; } Ví dụ 7.4: Thiết kế các hành động của lớp CDiemKT 1. Nhóm kiểm tra ràng buộc bool KiemTraX(int xx); bool KiemTraY(int yy); 2. Nhóm khởi tạo void Nhap(); bool KhoiTao (int xx, int yy, char cc); void PhatSinh(); 3. Nhóm cập nhật //Trực tiếp bool CapNhatX(int xx); bool CapNhatY(int yy); void CapNhatCh(char c); //Gián tiếp bool DichPhai(uint k); bool DichTrai(uint k); bool DichLen(uint k); bool DichXuong(uint k); bool DichXien1(uint k); bool DichXien2(uint k); 216 4. Nhóm xử lý tính toán double KhoangCach(CDiemKT M); int KhoangCachX(CDiemKT M); int KhoangCachY(CDiemKT M); 5. Nhóm cung cấp thông tin void Xuat(); void Xoa(); int GiaTriX(); int GiaTriY(); char GiaTriCh(); Ví dụ 7.5: Thiết kế các hành động của lớp CHCN 1. Nhóm kiểm tra ràng buộc bool KiemTraM(int mm); bool KiemTraN(int nn); 2. Nhóm khởi tạo void Nhap(); bool KhoiTao(CDiemKT M,int cng, int cd); bool KhoiTao(int x, int y, int cng, int cd); void KhoiTao(CDiemKT X, CDiemKT Y); void PhatSinh(); 3. Nhóm cập nhật //Trực tiếp bool CapNhatGoc(CDiemKT M); bool CapNhatNgang(int cng); bool CapNhatDung(int cd); 4. Nhóm cập nhật //Gián tiếp bool DichPhai(int k); bool DichTrai(int k); bool DichLen(int k); bool DichXuong(int k); bool TangNgang(int k); bool GiamNgang(int k); bool TangDung(int k); bool GiamDung(int k); bool XoayThuan(); void XoayNghich(); 5. Nhóm xử lý tính toán int XetViTri(CDiemKT M); //-1: Bên trong, 0: Trên cạnh, 1: Bên ngoài int KhoangCachX(CDiemKT M); int KhoangCachY(CDiemKT M); 6. Nhóm cung cấp thông tin void Xuat(); void Xoa(); CDiemKT ToaDoGoc(); int ChieuNgang(); int ChieuDung(); 217 int ChuVi(); long DienTich(); double DuongCheo(); CÂU HỎI, BÀI TẬP 1. Thiết kế thuộc tính lớp thời gian Ctime? 2. Thiết kế thuộc tính lớp ngày tháng năm Cdate? 3. Thiết kế thuộc tính lớp phân số CphanSo? 4. Thiết kế thuộc tính lớp CDaThuc (Đa thức 1 ẩn): Pn(x) = a0 + a1x + a2x2+ a3x3 + ... + anxn 5. Thiết kế thuộc tính lớp đường thẳng trong mặt phẳng CduongThang? 6. Thiết kế phương thức lớp thời gian Ctime? 7. Thiết kế phương thức lớp ngày tháng năm Cdate? 8. Thiết kế phương thức lớp phân số CphanSo? 9. Thiết kế phương thức lớp CDaThuc (Đa thức 1 ẩn): Pn(x) = a0 + a1x + a2x2+ a3x3 + ... + anxn 10. Thiết kế phương thức lớp đường thẳng trong mặt phẳng CduongThang? 218 Chương 8 CÁC DÒNG NHẬP, XUẤT VÀ LÀM VIỆC VỚI TỆP TIN C++ sử dụng khái niệm dòng (stream) và đưa ra các lớp dòng để tổ chức việc nhập xuất. Dòng có thể xem như một dãy tuần tự các byte. Thao tác nhập là đọc các byte từ dòng (gọi là dòng nhập – input) vào bộ nhớ. Thao tác xuất là đưa các byte từ bộ nhớ ra dòng (gọi là dòng xuấtoutput). Các thao tác này là độc lập thiết bị. Để thực hiện việc nhập, xuất lên một thiết bị cụ thể, chúng ta chỉ cần gắn dòng tin với thiết bị này. Trong chương này, chúng ta sẽ xét các đối tượng chuẩn cin, cout và một số toán tử, hàm nhập xuất đặc trưng của lớp iostream cũng như cách tạo và sử dụng các đối tượng thuộc các lớp ifstream, ofstream, fstream để làm việc với các thiết bị như máy in và file trên đĩa. 8.1. GIỚI THIỆU CHUNG 8.1.1. Khái niệm về dòng Trong C++ có sẵn một số lớp chuẩn chứa dữ liệu và các phương thức phục vụ cho các thao tác nhập/xuất dữ liệu của NSD, thường được gọi chung là stream (dòng). Trong số các lớp này, lớp có tên ios là lớp cơ sở, chứa các thuộc tính để định dạng việc nhập/xuất và kiểm tra lỗi. Mở rộng (kế thừa) lớp này có các lớp istream, ostream cung cấp thêm các toán tử nhập/xuất như >>, << và các hàm get, getline, read, ignore, put, write, flush … Một lớp rộng hơn có tên iostream là tổng hợp của 2 lớp trên. Bốn lớp nhập/xuất cơ bản này được khai báo trong các file tiêu đề có tên tương ứng (với đuôi *.h). Sơ đồ thừa kế của 4 lớp trên được thể hiện qua Hình 8.1. Hình 8.1: Sơ đồ thừa kế các lớp nhập, xuất Đối tượng của các lớp trên được gọi là các dòng dữ liệu. Một số đối tượng thuộc lớp iostream đã được khai báo sẵn (chuẩn) và được gắn với những thiết bị nhập/xuất cố định như các đối tượng cin, cout, cerr, clog gắn với bàn phím (cin) và màn hình (cout, cerr, clog). Điều này có nghĩa các toán tử >>, << và các hàm kể trên khi làm việc với các đối tượng này sẽ cho phép NSD nhập dữ liệu thông qua bàn phím hoặc xuất kết quả thông qua màn hình. Để nhập/xuất thông qua các thiết bị khác (như máy in, file trên đĩa …), C++ cung cấp thêm các lớp ifstream, ofstream, fstream cho phép NSD khai báo các đối tượng mới gắn với thiết bị và từ đó nhập/xuất thông qua các thiết bị này. Để rõ hơn, trong các chương trước, chúng ta thường sử dụng các chỉ thị viết ra thiết bị ra chuẩn như: cout<<n; 219 Chỉ thị này gọi đến toán tử “<<” và cung cấp cho nó hai toán hạng, một tương ứng với “kênh xuất - output stream”( ở đây là cout), toán hạng thứ hai là biểu thức mà chúng ta muốn viết giá trị của nó (ở đây là n). Tương tự, các chỉ thị đọc từ thiết bị vào chuẩn kiểu như: cin >> x; gọi tới toán tử “>>” và cung cấp cho nó hai toán hạng, một là “kênh nhập-input stream” (ở đây là cin), còn toán hạng thứ hai là một biến mà ta muốn nhập giá trị cho nó. Một cách tổng quát, một kênh(stream) được hiểu như một kênh truyền: nhận thông tin, trong trường hợp ta nói đến dòng xuất cung cấp thông tin, trong trường hợp ta nói đến dòng nhập. Các toán tử “<<” và “>>” ở đây đóng vai trò chuyển giao thông tin, cùng với khuôn dạng của chúng. Một kênh có thể được nối với một thiết bị ngoại vi hoặc một tập tin. Kênh cout được định nghĩa nối đến thiết bị ra chuẩn (tương đương stdout). Cũng vậy, kênh cin được định nghĩa trước để nối đến thiết bị vào chuẩn (stdin). Thông thường cout tương ứng với màn hình, còn cin thì đại diện cho bàn phím. Tuy nhiên trong trường hợp cần thiết thì có thể đổi hướng các vào ra chuẩn này đến một tập tin. Ngoài các kênh chuẩn cin và cout, người sử dụng có thể định nghĩa cho mình các kênh xuất nhập khác để kết nối với các tập tin. 8.1.2. Thư viện các lớp vào ra C++ cung cấp một thư viện các lớp phục vụ cho công việc vào ra. Lớp streambuf là cơ sở cho tất cả các thao tác vào ra bằng toán tử; nó định nghĩa các đặc trưng cơ bản của các vùng đệm lưu trữ các ký tự để xuất hay nhập. Lớp ios là lớp dẫn xuất từ streambuf, ios định nghĩa các dạng cơ bản và khả năng kiểm tra lỗi dùng cho streambuf. ios là một lớp cơ sở ảo cho các lớp istream và ostream. Mỗi lớp này có định nghĩa chồng toán tử “<<” và “>>” cho các kiểu dữ liệu cơ sở khác nhau. Để sử dụng các khả năng này phải dùng chỉ thị #include đối với tập tin tiêu đề iostream.h. Cơ chế lớp của C++ cho phép tạo ra hệ thống giao tiếp có khả năng mở rộng và nhất quán. Trong chương 4 đã đưa ra hai định nghĩa chồng cho các toán tử vào/ra trong C++. Phần này này tập trung trình bày các khả năng vào ra do C++ cung cấp, bao gồm các nội dung sau: khả năng của ostream, istream, kiểm soát lỗi vào ra. 8.2. NHẬP/XUẤT VỚI CIN/COUT Như đã nhắc ở trên, cin là dòng dữ liệu nhập (đối tượng) thuộc lớp istream. Các thao tác trên đối tượng này gồm có các toán tử và hàm phục vụ nhập dữ liệu vào cho biến từ bàn phím. Hình 8.2: Mô tả nhập xuất với cin và cout 220 8.2.1. Toán tử nhập >> Toán tử này cho phép nhập dữ liệu từ một dòng Input_stream nào đó vào cho một danh sách các biến. Cú pháp chung như sau: Input_stream >> biến1 >> biến2 >> … trong đó Input_stream là đối tượng thuộc lớp istream. Trường hợp Input_stream là cin, câu lệnh nhập sẽ được viết: cin >> biến1 >> biến2 >> … câu lệnh này cho phép nhập dữ liệu từ bàn phím cho các biến. Các biến này có thể thuộc các kiểu chuẩn như: kiểu nguyên, thực, ký tự, xâu kí tự. Chú ý 2 đặc điểm quan trọng của câu lệnh trên. Lệnh sẽ bỏ qua không gán các dấu trắng (dấu cách <>, dấu Tab, dấu xuống dòng ) vào cho các biến (kể cả biến xâu kí tự). Khi người sử dụng nhập vào dãy byte nhiều hơn cần thiết để gán cho các biến thì số byte còn lại và kể cả dấu xuống dòng sẽ nằm lại trong cin. Các byte này sẽ tự động gán cho các biến trong lần nhập sau mà không chờ người sử dụng gõ thêm dữ liệu vào từ bàn phím. Do vậy câu lệnh: cin >> a >> b >> c; cũng có thể được viết thành cin >> a; cin >> b; cin >> c; và chỉ cần nhập dữ liệu vào từ bàn phím một lần chung cho cả 3 lệnh (mỗi dữ liệu nhập cho mỗi biến phải cách nhau ít nhất một dấu trắng). Ví dụ 8.1: Nhập dữ liệu cho các biến int a; float b; char c; char *s; cin >> a >> b >> c >> s; giả sử NSD nhập vào dãy dữ liệu : <><>12<>34.517ABC<>12E<>D khi đó các biến sẽ được nhận những giá trị cụ thể sau: a = 12 b = 34.517 c = 'A' s = "BC" trong cin sẽ còn lại dãy dữ liệu : <>12E<>D . Nếu trong đoạn chương trình tiếp theo có câu lệnh cin >> s; thì s sẽ được tự động gán giá trị "12E" mà không cần NSD nhập thêm dữ liệu vào cho cin. Qua ví dụ trên một lần nữa ta nhắc lại đặc điểm của toán tử nhập >> là các biến chỉ lấy dữ liệu vừa đủ cho kiểu của biến (ví dụ biến c chỉ lấy một kí tự 'A', b lấy giá trị 34.517) hoặc cho đến khi gặp dấu trắng đầu tiên (ví dụ a lấy giá trị 12, s lấy giá trị "BC" dù trong cin vẫn còn dữ liệu). Từ đó ta thấy toán tử >> là không phù hợp khi nhập dữ liệu cho các xâu kí tự có chứa dấu cách. C++ giải quyết trường hợp này bằng một số hàm (phương thức) nhập khác thay cho toán tử >>. 221 8.2.2. Các hàm nhập kí tự và xâu kí tự 8.2.2.1. Nhập kí tự cin.get(): Hàm trả lại một kí tự (kể cả dấu cách, dấu ).. Ví dụ 8.2: char ch; ch = cin.get(); nếu nhập AB, ch nhận giá trị 'A', trong cin còn B. nếu nhập A, ch nhận giá trị 'A', trong cin còn . nếu nhập , ch nhận giá trị '', trong cin rỗng. cin.get(ch): Hàm nhập kí tự cho ch và trả lại một tham chiếu tới cin. Do hàm trả lại tham chiếu tới cin nên có thể viết các phương thức nhập này liên tiếp trên một đối tượng cin. Ví dụ: char c, d; cin.get(c).get(d); nếu nhập AB thì c nhận giá trị 'A' và d nhận giá trị 'B'. Trong cin còn 'C'. 8.2.2.2. Nhập xâu kí tự cin.get(s, n, fchar): Hàm nhập cho s dãy kí tự từ cin. Dãy được tính từ kí tự đầu tiên trong cin cho đến khi đã đủ n – 1 kí tự hoặc gặp kí tự kết thúc fchar. Kí tự kết thúc này được ngầm định là dấu xuống dòng nếu bị bỏ qua trong danh sách đối. Tức có thể viết câu lệnh trên dưới dạng cin.get(s, n) khi đó xâu s sẽ nhận dãy kí tự nhập cho đến khi đủ n-1 kí tự hoặc đến khi NSD kết thúc nhập (bằng dấu ). Chú ý: Lệnh sẽ tự động gán dấu kết thúc xâu ('\0') vào cho xâu s sau khi nhập xong. Các lệnh có thể viết nối nhau, ví dụ: cin.get(s1, n1).get(s2,n2); Kí tự kết thúc fchar (hoặc ) vẫn nằm lại trong cin. Điều này có thể làm trôi các lệnh get() tiếp theo. Ví dụ: struct Sinhvien { char *ht; // họ tên char *qq; // quê quán }; void main() { int i; for (i=1; i<=3; i++) { cout << "Nhap ho ten sv thu " << i; cin.get(sv[i].ht, 25); cout << "Nhap que quan sv thu "<< i; cin.get(sv[i].qq, 30); } … } 222 Trong đoạn lệnh trên sau khi nhập họ tên của sinh viên thứ 1, do kí tự vẫn nằm trong bộ đệm nên khi nhập quê quán chương trình sẽ lấy kí tự này gán cho qq, do đó quê quán của sinh viên sẽ là xâu rỗng. Để khắc phục tình trạng này chúng ta có thể sử dụng một trong các câu lệnh nhập kí tự để "nhấc" dấu enter còn "rơi vãi" ra khỏi bộ đệm. Có thể sử dụng các câu lệnh sau: cin.get(); // đọc một kí tự trong bộ đệm cin.ignore(n); //đọc n kí tự trong bộ đệm (với n=1) như vậy để đoạn chương trình trên hoạt động tốt ta có thể tổ chức lại như sau: void main() { int i; for (i=1; i<=3; i++) { cout << "Nhap ho ten sv thu " << i; cin.get(sv[i].ht, 25); cin.get(); // nhấc 1 kí tự (enter) cout << "Nhap que quan sv thu "<< i; cin.get(sv[i].qq, 30); cin.get() // hoặc cin.ignore(1); } … } cin.getline(s, n, fchar): Phương thức này hoạt động hoàn toàn tương tự phương thức cin.get(s, n, fchar), tuy nhiên nó có thể khắc phục "lỗi enter" của câu lệnh trên. Cụ thể hàm sau khi gán nội dung nhập cho biến s sẽ xóa kí tự enter khỏi bộ đệm và do vậy NSD không cần phải sử dụng thêm các câu lệnh phụ trợ (cin.get(), cin.ignore(1)) để loại enter ra khỏi bộ đệm. cin.ignore(n): Phương thức này của đối tượng cin dùng để đọc và loại bỏ n kí tự còn trong bộ đệm (dòng nhập cin). Chú ý: Toán tử nhập >> cũng giống các phương thức nhập kí tự và xâu kí tự ở chỗ cũng để lại kí tự enter trong cin. Do vậy, chúng ta nên sử dụng các phương thức cin.get(), cin.ignore(n) để loại bỏ kí tự enter trước khi thực hiện lệnh nhập kí tự và xâu kí tự khác. Tương tự dòng nhập cin, cout là dòng dữ liệu xuất thuộc lớp ostream. Điều này có nghĩa dữ liệu làm việc với các thao tác xuất (in) sẽ đưa kết quả ra cout mà đã được mặc định là màn hình. Do đó ta có thể sử dụng toán tử xuất << và các phương thức xuất trong các lớp ios (lớp cơ sở) và ostream. 8.2.3. Toán tử xuất << Toán tử này cho phép xuất giá trị của dãy các biểu thức đến một dòng Output_stream nào đó với cú pháp chung như sau: Output_stream << bt_1 << bt_2 << … Ở đây Output_stream là đối tượng thuộc lớp ostream. Trường hợp Output_stream là cout, câu lệnh xuất sẽ được viết: cout << bt_1 << bt_2 << … câu lệnh này cho phép in kết quả của các biểu thức ra màn hình. Kiểu dữ liệu của các biểu thức có thể là số nguyên, thực, kí tự hoặc xâu kí tự. 223 8.3. ĐỊNH DẠNG Các giá trị in ra màn hình có thể được trình bày dưới nhiều dạng khác nhau thông qua các công cụ định dạng như các phương thức, các cờ và các bộ phận khác được khai báo sẵn trong các lớp ios và ostream. Các phương thức định dạng bao gồm: 8.3.1. Chỉ định độ rộng cần in cout.width(n); Số cột trên màn hình để in một giá trị được ngầm định bằng với độ rộng thực (số chữ số, chữ cái và kí tự khác trong giá trị được in). Để đặt lại độ rộng màn hình dành cho giá trị cần in (thông thường lớn hơn độ rộng thực) ta có thể sử dụng phương thức trên. Phương thức này cho phép các giá trị in ra màn hình với độ rộng n. Nếu n bé hơn độ rộng thực sự của giá trị thì máy sẽ in giá trị với số cột màn hình bằng với độ rộng thực. Nếu n lớn hơn độ rộng thực, máy sẽ in giá trị căn theo lề phải và để trống các cột thừa phía trước giá trị được in. Phương thức này chỉ có tác dụng với giá trị cần in ngay sau nó. Ví dụ 8.3: int a = 12; b = 345; // độ rộng thực của a là 2, của b là 3 cout << a; // chiếm 2 cột màn hình cout.width(7); // đặt độ rộng giá trị in tiếp theo là 7 cout << b; // b in trong 7 cột với 4 dấu cách đứng trước Kết quả in ra sẽ là: 12<><><><>345 8.3.2. Chỉ định kí tự chèn vào khoảng trống trước giá trị cần in cout.fill(ch) ; Kí tự độn ngầm định là dấu cách, có nghĩa khi độ rộng của giá trị cần in bé hơn độ rộng chỉ định thì máy sẽ độn các dấu cách vào trước giá trị cần in cho đủ với độ rộng chỉ định. Có thể yêu cầu độn một kí tự ch bất kỳ thay cho dấu cách bằng phương thức trên. Ví dụ trong dãy lệnh trên, nếu ta thêm dòng lệnh cout.fill('*') trước khi in b chẳng hạn thì kết quả in ra sẽ là: 12****345. Phương thức này có tác dụng với mọi câu lệnh in sau nó cho đến khi gặp một chỉ định mới. 8.3.3. Chỉ định độ chính xác (số số lẻ thập phân) cần in cout.precision(n) ; Phương thức này yêu cầu các số thực in ra sau đó sẽ có n chữ số lẻ. Các số thực trước khi in ra sẽ được làm tròn đến chữ số lẻ thứ n. Chỉ định này có tác dụng cho đến khi gặp một chỉ định mới. Ví dụ 8.4: int a = 12.3; b = 345.678; // độ rộng thực của a là 4, của b là 7 cout << a; // chiếm 4 cột màn hình cout.width(10); // đặt độ rộng giá trị in tiếp theo là 10 cout.precision(2); // đặt độ chính xác đến 2 số lẻ cout << b; // b in trong 10 cột với 4 dấu cách đứng trước Kết quả in ra sẽ là: 12.3<><><><>345.68 224 8.3.4. Các cờ định dạng Một số các quy định về định dạng thường được gắn liền với các "cờ". Thông thường nếu định dạng này được sử dụng trong suốt quá trình chạy chương trình hoặc trong một khoảng thời gian dài trước khi gỡ bỏ thì ta "bật" các cờ tương ứng với nó. Các cờ được bật sẽ có tác dụng cho đến khi cờ với định dạng khác được bật. Các cờ được cho trong file tiêu đề iostream.h. Để bật/tắt các cờ ta sử dụng các phương thức sau: cout.setf(danh sách cờ); // Bật các cờ trong danh sách cout.unsetf(danh sách cờ); // Tắt các cờ trong danh sách Các cờ trong danh sách được viết cách nhau bởi phép toán hợp bit (|). Ví dụ lệnh cout.setf(ios::left | ios::scientific) sẽ bật các cờ ios::left và ios::scientific. Phương thức cout.unsetf(ios::right | ios::fixed) sẽ tắt các cờ ios::right | ios::fixed. Dưới đây là danh sách các cờ cho trong iostream.h. 8.3.5. Nhóm căn lề ios::left : nếu bật thì giá trị in nằm bên trái vùng in ra (kí tự độn nằm sau). ios::right : giá trị in nằm bên phái vùng in ra (kí tự độn nằm trước), đây là trường hợp ngầm định nếu ta không sử dụng cờ cụ thể. ios::internal : giống cờ ios::right tuy nhiên dấu của giá trị in ra sẽ được in đầu tiên, sau đó mới đến kí tự độn và giá trị số. Ví dụ 8.5: int a = 12.3; b = 345.678; // độ rộng thực của a là 4, của b là 8 cout << a; // chiếm 4 cột màn hình cout.width(10); // đặt độ rộng giá trị in tiếp theo là 10 cout.fill('*') ; // dấu * làm kí tự độn cout.precision(2); // đặt độ chính xác đến 2 số lẻ cout.setf(ios::left) ; // bật cờ ios::left cout << b; // kết qủa: 12.3345.68*** cout.setf(ios::right) ; // bật cờ ios::right cout << b; // kết qủa: 12.3***345.68 cout.setf(ios::internal) ; // bật cờ ios::internal cout << b; // kết qủa: 12.3***345.68 8.3.6. Nhóm định dạng số nguyên ios::dec : in số nguyên dưới dạng thập phân (ngầm định); ios::oct : in số nguyên dưới dạng cơ số 8; ios::hex : in số nguyên dưới dạng cơ số 16. 8.3.7. Nhóm định dạng số thực ios::fixed : in số thực dạng dấu phảy tĩnh (ngầm định); ios::scientific : in số thực dạng dấu phảy động; ios::showpoint : in đủ n chữ số lẻ của phần thập phân, nếu tắt (ngầm định) thì không in các số 0 cuối của phần thập phân. Ví dụ: giả sử độ chính xác được đặt với 3 số lẻ (bởi câu lệnh cout.precision(3)) nếu fixed bật + showpoint bật: 225 123.2500 được in thành 123.250 123.2599 được in thành 123.260 123.2 được in thành 123.200 123.2500 được in thành 123.25 123.2599 được in thành 123.26 123.2 được in thành 123.2 nếu fixed bật + showpoint tắt: nếu scientific bật + showpoint bật: 12.3 được in thành 1.230e+01 2.32599 được in thành 2.326e+00 324 được in thành 3.240e+02 nếu scientific bật + showpoint tắt: 12.3 được in thành 1.23e+01 2.32599 được in thành 2.326e+00 324 được in thành 3.24e+02 8.3.8. Nhóm định dạng hiển thị ios::showpos: nếu tắt (ngầm định) thì không in dấu cộng (+) trước số dương. Nếu bật trước mỗi số dương sẽ in thêm dấu cộng. ios::showbase: nếu bật sẽ in số 0 trước các số nguyên hệ 8 và in 0x trước số hệ 16. Nếu tắt (ngầm định) sẽ không in 0 và 0x. ios::uppercase: nếu bật thì các kí tự biểu diễn số trong hệ 16 (A..F) sẽ viết hoa, nếu tắt (ngầm định) sẽ viết thường. 8.4. CÁC BỘ VÀ HÀM ĐỊNH DẠNG iostream.h cũng cung cấp một số bộ và hàm định dạng cho phép sử dụng tiện lợi hơn so với các cờ và các phương thức vì nó có thể được viết liên tiếp trên dòng lệnh xuất. 8.4.1. Các bộ định dạng dec // tương tự ios::dec oct // tương tự ios::dec hex // tương tự ios::hex endl // xuất kí tự xuống dòng ('\n') flush // đẩy toàn bộ dữ liệu ra dòng xuất Ví dụ 8.6: cout.setf(ios::showbase) ; // cho phép in các kí biểu thị cơ số cout.setf(ios::uppercase) ; // dưới dạng chữ viết hoa int a = 171; int b = 32 ; 226 tự cout << hex << a << endl << b ; // in 0xAB và 0x20 8.4.2. Các hàm định dạng (#include <iomanip.h>) setw(n) setprecision(n) setfill(c) setiosflags(l) resetiosflags(l) // // // // // tương tương tương tương tương tự tự tự tự tự cout.width(n) cout.precision(n) cout.fill(c) cout.setf(l) cout.unsetf(l) 8.5. IN RA MÁY IN Như trong phần đầu chương đã trình bày, để làm việc với các thiết bị khác với màn hình và đĩa… chúng ta cần tạo ra các đối tượng (thuộc các lớp ifstream, ofstream và fstream) tức các dòng tin bằng các hàm tạo của lớp và gắn chúng với thiết bị bằng câu lệnh: ofstream Tên_dòng(thiết bị) ; Ví dụ để tạo một đối tượng mang tên Mayin và gắn với máy in, chúng ta dùng lệnh: ofstream Mayin(4) ; trong đó 4 là số hiệu của máy in. Khi đó mọi câu lệnh dùng toán tử xuất << và cho ra Mayin sẽ đưa dữ liệu cần in vào một bộ đệm mặc định trong bộ nhớ. Nếu bộ đệm đầy, một số thông tin đưa vào trước sẽ tự động chuyển ra máy in. Để chủ động đưa tất cả dữ liệu còn lại trong bộ đệm ra máy in chúng ta cần sử dụng bộ định dạng flush (Mayin << flush << …) hoặc phương thức flush (Mayin.flush(); ). Ví dụ: Sau khi đã khai báo một đối tượng mang tên Mayin bằng câu lệnh như trên Để in chu vi và diện tích hình chữ nhật có cạnh cd và cr ta có thể viết: Mayin << "Diện tích HCN = " << cd * cr << endl; Mayin << "Chu vi HCN = " << 2*(cd + cr) << endl; Mayin.flush(); hoặc : Mayin << "Diện tích HCN = " << cd * cr << endl; Mayin << "Chu vi HCN = " << 2*(cd + cr) << endl << flush; khi chương trình kết thúc mọi dữ liệu còn lại trong các đối tượng sẽ được tự động chuyển ra thiết bị gắn với nó. Ví dụ máy in sẽ in tất cả mọi dữ liệu còn sót lại trong Mayin khi chương trình kết thúc. 8.6. LÀM VIỆC VỚI FILE Làm việc với một file trên đĩa cũng được quan niệm như làm việc với các thiết bị khác của máy tính (ví dụ như làm việc với máy in với đối tượng Mayin trong phần trên hoặc làm việc với màn hình với đối tượng chuẩn cout). Các đối tượng này được khai báo thuộc lớp ifstream hay ofstream tùy thuộc ta muốn sử dụng file để đọc hay ghi. Như vậy, để sử dụng một file dữ liệu đầu tiên chúng ta cần tạo đối tượng và gắn cho file này. Để tạo đối tượng có thể sử dụng các hàm tạo có sẵn trong hai lớp ifstream và ofstream. Đối tượng sẽ được gắn với tên file cụ thể trên đĩa ngay trong quá trình tạo đối tượng (tạo đối tượng với tham số là tên file) hoặc cũng có thể được gắn với tên file sau này bằng câu lệnh mở file. Sau khi đã gắn một đối tượng với file trên đĩa, có thể sử dụng đối tượng như đối với Mayin hoặc cin, cout. Điều này có nghĩa trong các câu lệnh in ra màn hình chỉ cần thay từ khóa cout bởi tên đối tượng mọi dữ liệu cần in trong câu lệnh sẽ được ghi lên file mà đối tượng đại diện. Cũng tương tự nếu thay cin bởi tên đối tượng, dữ liệu sẽ được đọc vào từ file thay cho từ bàn phím. Để tạo đối tượng dùng cho việc ghi ta khai báo chúng với lớp ofstream còn để dùng cho việc đọc ta khai báo chúng với lớp ifstream. 227 8.6.1. Tạo đối tượng gắn với file Mỗi lớp ifstream và ofstream cung cấp 4 phương thức để tạo file. Ở đây chúng tôi chỉ trình bày 2 cách (tương ứng với 2 phương thức) hay dùng. + Cách 1: <Lớp> đối_tượng; đối_tượng.open(tên_file, chế_độ); Lớp là một trong hai lớp ifstream và ofstream. Đối tượng là tên do NSD tự đặt. Chế độ là cách thức làm việc với file (xem dưới). Cách này cho phép tạo trước một đối tượng chưa gắn với file cụ thể nào. Sau đó dùng tiếp phương thức open để đồng thời mở file và gắn với đối tượng vừa tạo. Ví dụ 8.7: ifstream f; // tạo đối tượng có tên f để đọc hoặc ofstream f; // tạo đối tượng có tên f để ghi f.open("Baitap"); // mở file Baitap và gắn với f + Cách 2: <Lớp> đối_tượng(tên_file, chế_độ) Cách này cho phép đồng thời mở file cụ thể và gắn file với tên đối tượng trong câu lệnh. Ví dụ 8.8: ifstream f("Baitap"); // mở file Baitap gắn với đối tượng f để ofstream f("Baitap); // đọc hoặc ghi. Sau khi mở file và gắn với đối tượng f, mọi thao tác trên f cũng chính là làm việc với file Baitap. Trong các câu lệnh trên có các chế độ để qui định cách thức làm việc của file. Các chế độ này gồm có: ios::binary : quan niệm file theo kiểu nhị phân. Ngầm định là kiểu văn bản; ios::in : file để đọc (ngầm định với đối tượng trong ifstream); ios::out : file để ghi (ngầm định với đối tượng trong ofstream), nếu file đã có trên đĩa thì nội dung của nó sẽ bị ghi đè (bị xóa).ios::app : bổ sung vào cuối file; ios::trunc : xóa nội dung file đã có; ios::ate : chuyển con trỏ đến cuối file; ios::nocreate : không làm gì nếu file chưa có; ios::replace : không làm gì nếu file đã có. Có thể chỉ định cùng lúc nhiều chế độ bằng cách ghi chúng liên tiếp nhau với toán tử hợp bit |. Ví dụ để mở file bài tập như một file nhị phân và ghi tiếp theo vào cuối file ta dùng câu lệnh: ofstream f("Baitap", ios::binary | ios::app); 8.6.2. Đóng file và giải phóng đối tượng Để đóng file được đại diện bởi f, sử dụng phương thức close như sau: đối_tượng.close(); Sau khi đóng file (và giải phóng mối liên kết giữa đối tượng và file) có thể dùng đối tượng để gắn và làm việc với file khác bằng phương thức open như trên. Ví dụ 8.9: Đọc một dãy số từ bàn phím và ghi lên file. File được xem như file văn bản (ngầm định), các số được ghi cách nhau 1 dấu cách. 228 #include <iostream.h> #include <fstream.h> #include <conio.h> void main() { ofstream f; // khai báo (tạo) đối tượng f int x; f.open("DAYSO"); // mở file DAYSO và gắn với f for (int i = 1; i<=10; i++) { cin >> x; f << x << ' '; } f.close(); } Ví dụ 8.10: Chương trình sau nhập danh sách sinh viên, ghi vào file 1, đọc ra mảng, sắp xếp theo tuổi và in ra file 2. Dòng đầu tiên trong file ghi số sinh viên, các dòng tiếp theo ghi thông tin của sinh viên gồm họ tên với độ rộng 24 kí tự, tuổi với độ rộng 4 kí tự và điểm với độ rộng 8 kí tự. #include <iostream.h> #include <iomanip.h> #include <fstream.h> #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <ctype.h> struct Sv { char *hoten; int tuoi; double diem; }; class Sinhvien { int sosv ; Sv *sv; public: Sinhvien() { sosv = 0; sv = NULL; } void nhap(); void sapxep(); void ghifile(char *fname); }; void Sinhvien::nhap() { cout << "\nSố sinh viên: "; cin >> sosv; int n = sosv; sv = new Sinhvien[n+1]; // Bỏ phần tử thứ 0 229 for (int i = 1; i <= n; i++) { cout << "\nNhập sinh viên thứ: " << i << endl; cout << "\nHọ tên: "; cin.ignore(); cin.getline(sv[i].hoten); cout << "\nTuổi: "; cin >> sv[i].tuoi; cout << "\nĐiểm: "; cin >> sv[i].diem; } } void Sinhvien::ghi(char fname) { ofstream f(fname) ; f << sosv; f << setprecision(1) << setiosflags(ios::showpoint) ; for (int i=1; i<=sosv; i++) { f << endl << setw(24) << sv[i].hoten << setw(4) << tuoi; f << setw(8) << sv[i].diem; } f.close(); } void Sinhvien::doc(char fname) { ifstream f(fname) ; f >> sosv; for (int i=1; i<=sosv; i++) { f.getline(sv[i].hoten, 25); f >> sv[i].tuoi >> sv[i].diem; } f.close(); } void Sinhvien::sapxep() { int n = sosv; for (int i = 1; i < n; i++) { for (int j = j+1; j <= n; j++) { if (sv[i].tuoi > sv[j].tuoi) { Sinhvien t = sv[i]; sv[i] = sv[j]; sv[j] = t; } } void main() { clrscr(); Sinhvien x ; x.nhap(); x.ghi("DSSV1"); x.doc("DSSV1"); x.sapxep(); x.ghi("DSSV2"); cout << "Đã xong"; getch(); } 230 8.6.3. Kiểm tra sự tồn tại của file, kiểm tra hết file Việc mở một file chưa có để đọc sẽ gây nên lỗi và làm dừng chương trình. Khi xảy ra lỗi mở file, giá trị trả lại của phương thức bad là một số khác 0. Do vậy có thể sử dụng phương thức này để kiểm tra một file đã có trên đĩa hay chưa. Ví dụ: ifstream f("Bai tap"); if (f.bad()) { cout << "file Baitap chưa có"; exit(1); } Khi đọc hoặc ghi, con trỏ file sẽ chuyển dần về cuối file. Khi con trỏ ở cuối file, phương thức eof() sẽ trả lại giá trị khác không. Do đó có thể sử dụng phương thức này để kiểm tra đã hết file hay chưa. Chương trình sau cho phép tính độ dài của file Baitap. File cần được mở theo kiểu nhị phân. #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <conio.h> void main() { clrscr(); long dodai = 0; char ch; ifstream f("Baitap", ios::in | ios::binary) ; if (f.bad()) { cout << "File Baitap không có"; exit(1); } while (!f.eof()) { f.get(ch)); dodai++; } cout << "Độ dài của file = " << dodai; getch(); } 8.6.4. Đọc ghi đồng thời trên file Để đọc ghi đồng thời, file phải được gắn với đối tượng của lớp fstream là lớp thừa kế của 2 lớp ifstream và ofstream. Khi đó chế độ phải được bao gồm chỉ định ios::in | ios::out. Ví dụ 8.11: fstream f("Data", ios::in | ios::out) ; hoặc fstream f ; f.open("Data", ios::in | ios::out) ; 8.6.5. Di chuyển con trỏ file Các phương thức sau cho phép làm việc trên đối tượng của dòng xuất (ofstream). đối_tượng.seekp(n); Di chuyển con trỏ đến byte thứ n (các byte được tính từ 0) 231 đối_tượng.seekp(n, vị trí xuất phát); Di chuyển đi n byte (có thể âm hoặc dương) từ vị trí xuất phát. Vị trí xuất phát gồm: ios::beg : từ đầu file ios::end : từ cuối file ios::cur : từ vị trí hiện tại của con trỏ. đối_tượng.tellp(n) ; Cho biết vị trí hiện tại của con trỏ. Để làm việc với dòng nhập tên các phương thức trên được thay tương ứng bởi các tên: seekg và tellg. Đối với các dòng nhập lẫn xuất có thể sử dụng được cả 6 phương thức trên. Ví dụ sau tính độ dài tệp đơn giản hơn ví dụ ở trên. fstream f("Baitap"); f.seekg(0, ios::end); cout << "Độ dài bằng = " << f.tellg(); Ví dụ 8.12: Chương trình nhập và in danh sách sinh viên trên ghi/đọc đồng thời. #include <iostream.h> #include <iomanip.h> #include <fstream.h> #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <ctype.h> void main() { int stt ; char *hoten, *fname, traloi; int tuoi; float diem; fstream f; cout << "Nhập tên file: "; cin >> fname; f.open(fname, ios::in | ios::out | ios::noreplace) ; if (f.bad()) { cout << "Tệp đã có. Ghi đè (C/K)?" ; cin.get(traloi) ; if (toupper(traloi) == 'C') { f.close() ; f.open(fname, ios::in | ios::out | ios::trunc) ; } else exit(1); } stt = 0; f << setprecision(1) << setiosflags(ios::showpoint) ; // nhập danh sách while (1) { stt++; cout << "\nNhập sinh viên thứ " << stt ; cout << "\nHọ tên: "; cin.ignore() ; cin.getline(hoten, 25); if (hoten[0] = 0) break; cout << "\nTuổi: "; cin >> tuoi; cout << "\nĐiểm: "; cin >> diem; 232 f << setw(24) << hoten << endl; f << setw(4) << tuoi << set(8) << diem ; } // in danh sách f.seekg(0) ; // quay về đầu danh sách stt = 0; clrscr(); cout << "Danh sách sinh viên đã nhập\n" ; cout << setprecision(1) << setiosflags(ios::showpoint) ; while (1) { f.getline(hoten,25); if (f.eof()) break; stt++; f >> tuoi >> diem; f.ignore(); cout << "\nSinh viên thứ " << stt ; cout << "\nHọ tên: " << hoten; cout << "\nTuổi: " << setw(4) << tuoi; cout << "\nĐiểm: " << setw(8) << diem; } f.close(); getch(); } 8.7. NHẬP/XUẤT NHỊ PHÂN 8.7.1. Khái niệm về 2 loại file: văn bản và nhị phân 8.7.1.1. File văn bản Trong file văn bản mỗi byte được xem là một kí tự. Tuy nhiên nếu 2 byte 10 (LF), 13 (CR) đi liền nhau thì được xem là một kí tự và nó là kí tự xuống dòng. Như vậy file văn bản là một tập hợp các dòng kí tự với kí tự xuống dòng có mã là 10. Kí tự có mã 26 được xem là kí tự kết thúc file. 8.7.1.2. File nhị phân Thông tin lưu trong file được xem như dãy byte bình thường. Mã kết thúc file được chọn là -1, được định nghĩa là EOF trong stdio.h. Các thao tác trên file nhị phân thường đọc ghi từng byte một, không quan tâm ý nghĩa của byte. Một số các thao tác nhập/xuất sẽ có hiệu quả khác nhau khi mở file dưới các dạng khác nhau. Ví dụ 8.13: Giả sử ch = 10, khi đó f << ch sẽ ghi 2 byte 10,13 lên file văn bản f, trong khi đó lệnh này chỉ khi 1 byte 10 lên file nhị phân. Ngược lại, nếu f la file văn bản thì f.getc(ch) sẽ trả về chỉ 1 byte 10 khi đọc được 2 byte 10, 13 liên tiếp nhau. Một file luôn ngầm định dưới dạng văn bản, do vậy để chỉ định file là nhị phân ta cần sử dụng cờ ios::binary. 8.7.2. Đọc, ghi kí tự put(c); // ghi kí tự ra file get(c); // đọc kí tự từ file 233 Ví dụ 8.14: Sao chép file 1 sang file 2. Cần sao chép và ghi từng byte một do vậy để chính xác ta sẽ mở các file dưới dạng nhị phân. #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <conio.h> void main() { clrscr(); fstream fnguon("DATA1", ios::in | ios::binary); fstream fdich("DATA2", ios::out | ios::binary); char ch; while (!fnguon.eof()) { fnguon.get(ch); fdich.put(ch); } fnguon.close(); fdich.close(); } 8.7.3. Đọc, ghi dãy kí tự write(char *buf, int n); // ghi n kí tự trong buf ra dòng xuất read(char *buf, int n); // nhập n kí tự từ buf vào dòng nhập gcount(); // cho biết số kí tự read đọc được Ví dụ 8.15: Chương trình sao chép file ở trên có thể sử dụng các phương thức mới này như sau: #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <conio.h> void main() { clrscr(); fstream fnguon("DATA1", ios::in | ios::binary); fstream fdich("DATA2", ios::out | ios::binary); char buf[2000] ; int n = 2000; while (n) { fnguon.read(buf, 2000); n = fnguon.gcount(); fdich.write(buf, n); } fnguon.close(); fdich.close(); } 234 8.7.4. Đọc ghi đồng thời #include #include #include #include #include #include #include #include <iostream.h> <iomanip.h> <fstream.h> <stdlib.h> <stdio.h> <conio.h> <string.h> <ctype.h> struct Sv { char *hoten; int tuoi; double diem; }; class Sinhvien { int sosv; Sv x; char fname[30]; static int size; public: Sinhvien(char *fn); void tao(); void bosung(); void xemsua(); }; int Sinhvien::size = sizeof(Sv); Sinhvien::Sinhvien(char *fn) { strcpy(fname, fn) ; fstream f; f.open(fname, ios::in | ios::ate | ios::binary); if (!f.good) sosv = 0; else { sosv = f.tellg() / size; } } void Sinhvien::tao() { fstream f; f.open(fname, ios::out | ios::noreplace | ios::binary); if (!f.good()) { cout << "danh sach da co. Co tao lai (C/K) ?"; char traloi = getch(); if (toupper(traloi) == 'C') return; else { f.close() ; 235 f.open(fname, ios::out | ios::binary); } } sosv = 0 while (1) { cout << "\nSinh viên thứ: " << sosv+1; cout << "\nHọ tên: "; cin.getline(x.hoten); if (x.hoten[0] == 0) break; cout << "\nTuổi: "; cin >> x.tuoi; cout << "\nĐiểm: "; cin >> x.diem; f.write((char*)(&x), size); sosv++; } f.close(); } ios::trunc | cin.ignore(); void Sinhvien::bosung() { fstream f; f.open(fname, ios::out | ios::app | ios::binary); if (!f.good()) { cout << "danh sach chua co. Tao moi (C/K) ?"; char traloi = getch(); if (toupper(traloi) == 'C') return; else { f.close() ; f.open(fname, ios::out | ios::binary); } } int stt = 0 while (1) { cout << "\nBổ sung sinh viên thứ: " << stt+1; cout << "\nHọ tên: "; cin.ignore(); cin.getline(x.hoten); if (x.hoten[0] == 0) break; cout << "\nTuổi: "; cin >> x.tuoi; cout << "\nĐiểm: "; cin >> x.diem; f.write((char*)(&x), size); stt++; } sosv += stt; f.close(); } void Sinhvien::xemsua() { fstream f; int ch; 236 f.open(fname, ios::out | ios::app | ios::binary); if (!f.good()) { cout << "danh sach chua co"; getch(); return; } cout << "\nDanh sách sinh viên" << endl; int stt ; while (1) { cout << "\nCần xem (sua) sinh viên thứ (0: dừng): " ; cin >> stt; if (stt < 1 || stt > sosv) break; f.seekg((stt-1) * size, ios::beg); f.read((char*)(&x), size); cout << "\nHọ tên: " << x.hoten; cout << "\nTuổi: " << x.tuoi; cout << "\nĐiểm: " << x.diem; cout << "Có sửa không (C/K) ?"; cin >> traloi; if (toupper(traloi) == 'C') { f.seekg(-size, ios::cur); cout << "\nHọ tên: "; cin.ignore(); cin.getline(x.hoten); cout << "\nTuổi: "; cin >> x.tuoi; cout << "\nĐiểm: "; cin >> x.diem; f.write((char*)(&x), size); } } f.close(); } void main() { int chon; Sinhvien SV("DSSV") ; while (1) { clrscr(); cout << "\n1: Tạo danh sách sinh viên"; cout << "\n2: Bổ sung danh sách"; cout << "\n3: Xem – sửa danh sách"; cout << "\n0: Kết thúc"; chon = getch(); chon = chon – 48; clrscr(); if (chon == 1) SV.tao(); else if (chon == 2) SV.bosung(); else if (chon == 3) SV.xemsua(); else break; } } 237 8.8. MỘT SỐ VÍ DỤ 8.8.1. Ghi các số chẵn từ 1 đến 1000 vào file “So Chan.txt” #include <iostream> #include <fstream> #include <iostream> using namespace std; int main() { ofstream SoChan ("So Chan.txt"); SoChan<<"Day so chan tu 1 -> 1000 \n"; for(int a = 1; a <= 1000; a++) { if(a%2 == 0) { SoChan<<a; SoChan<<"\n"; } } SoChan.close(); return 0; } 8.8.2. Đọc file “So Chan.txt” #include <iostream> #include <fstream> #include <iostream> using namespace std; int main() { int a[501]; ifstream SoChan ("So Chan.txt"); if(! SoChan.is_open()) { cout<<"Khong the mo file.\n"; return 0; } else { for(int i = 1; i <= 500; i++) { SoChan>>a[i]; } } for(int i =1; i <= 500; i++) { cout<<a[i]<<" "; } SoChan.close(); 238 system("pause"); return 0; } 8.8.3. Tạo tệp tin văn bản chứa số nguyên Viết chương trình tạo tập tin văn bản SO.OUT gồm n số nguyên, các số của dãy được tạo ngẫu nhiên có giá trị tuyệt đối không vượt quá M (n, M đọc từ tập tin SO.INP). Kết quả chương trình là 1 tập tin văn bản có dòng thứ nhất ghi số n; n dòng tiếp theo ghi các số tạo được, mỗi số trên một dòng. # include < conio.h > # include < stdio.h > # define in “SO.INP” # define out “SO.OUT” int n, M ; void Nhap () { FILE *fi; fi = fopen ( in , “rt” ); fscanf ( fi, “ %d %d ”, &n, &M ); fclose ( fi ); } void Xuat () { FILE *fo; fo = fopen ( out , “ wt ” ); fprintf ( fo , “ %d\n”, n ); randomize ( ); for (; n > 0 ; n -- ) fprintf ( fo , “%d\n” , random ( ( 2 * M + 1 ) - M ) ); fclose (fo); } void main () { clrscr(); Nhap(); Xuat(); } 239 8.8.4. Tệp chứa ma trận Viết chương trình phát sinh ngẫu nhiên ma trận a kích thước 5x6, lưu ma trận này vào file test.inp. Đọc lại file test.inp đưa dữ liệu vào ma trận b và xuất ra màn hình xem kết quả lưu đúng không? Cấu trúc của file test.inp như sau: - Dòng đầu lưu 2 số nguyên: m, n thể hiện số dòng và số cột của ma trận. - m dòng tiếp theo, mỗi dòng gồm n phần tử là giá trị các phần tử trên một dòng của ma trận. #include<conio.h> #include<stdlib.h> #define MAX 100 #define dl "test.inp" void LuuFile(int a[MAX][MAX], int m, int n) { FILE *f; f=fopen(dl, "wt"); if(f==NULL) { printf("\nKhong tao duoc file."); getch(); exit(0); } fprintf(f, "%d %d\n", m, n); for(int i=0; i<m; i++) { for(int j=0; j<n; j++) fprintf(f, "%d\t", a[i][j]); fprintf(f, "\n"); } fclose(f); } void DocFile(int a[MAX][MAX], int &m, int &n) { FILE *f; f=fopen(dl, "rt"); if(f==NULL) { printf("\nKhong doc duoc file."); getch(); exit(0); } fscanf(f, "%d%d", &m, &n); for(int i=0; i<m; i++) { for(int j=0; j<n; j++) fscanf(f, "%d", &a[i][j]); } fclose(f); } void main() { 240 int a[MAX][MAX], m=5, n=6, i, j; int b[MAX][MAX], x, y; randomize(); for(i=0; i<m; i++) for(j=0; j<n; j++) a[i][j]=random(1000); LuuFile(a, m, n); DocFile(b, x, y); for(i=0; i<x; i++) { for(j=0; j<y; j++) printf("%d\t", b[i][j]); printf("\n"); } } 8.8.5. Tệp tin nhị phân Viết hàm đọc/ ghi một danh sách sinh viên của một lớp vào tập tin SV.DAT SINHVIEN ds[100]; int siso; void nhap() { FILE *fi; fi = fopen(“SV.DAT” , “ rb” ); fseek (fi , 0 , SEEK_END ); siso = (ftell ( fi ) + 1 ) / sizeof ( SINHVIEN ); fseek (fi , 0 , SEEK_SET ); fread (ds , sizeof ( SINHVIEN ) , siso , fi); fclose (fi); } void xuat( ) { FILE *fo; fo = fopen( “SV.DAT”, “ wb” ); fwrite(ds , sizeof ( SINHVIEN ) , siso , fo); fclose(fo); } 241 CÂU HỎI, BÀI TẬP 1. Viết chương trình kiểm tra các giá trị nguyên nhập vào dạng hệ 10, hệ 8 và hệ 16. 2. Viết chương trình in bảng mã ASCII cho các ký tự có mã ASCII từ 33 đến 126. Chương trình in gồm giá trị ký tự, giá trị hệ 10, giá trị hệ 8 và giá trị hệ 16. 3. Viết chương trình in các giá trị nguyên dương luôn có dấu + ở phía trước. 4. Viết chương trình tương tự như lệnh COPY của DOS để sao chép nội dung của file. 5. Viết chương trình cho biết kích thước file. 6. Viết chương trình đếm số lượng từ có trong một file văn bản tiếng Anh. 7. a. Viết chương trình nhập mảng số nguyên có số phần tử bất kỳ từ bàn phím rồi in ra các số chẵn bằng cách duyệt mảng. b. Sửa chương trình trên thay duyệt mảng bằng dùng hàm find_if(...) để in ra các số chẵn. c. Sửa lại chương trình trên để dùng DSLK thay cho mảng. 8. Cho hai mảng a1 và a2 đều có giá trị tăng dần, viết hàm trộn hai mảng này thành mảng a3 cũng có giá trị tăng dần. 9. Đọc dữ liệu từ file và lưu dưới dạng danh sách các dòng, sau đó in ra các dòng có độ dài từ 10 đến 20 ký tự. 10. Định nghĩa toán tử << và >> cho lớp Fraction và thử dùng nó để xuất/nhập dữ liệu với cin/cout, file, chuỗi. 11. Định nghĩa các toán tử << và >> cho lớp Complex để đọc/ghi dữ liệu dưới dạng nhị phân. 242 CÂU HỎI TRẮC NGHIỆM ÔN TẬP Câu 1: Cho biết kết quả của đoạn chương trình sau: class Phanso{ private: int tuso,mauso; public: Phanso(int t=0,m=1){ tuso=t; mauso=m; cout<< " ("<<tuso<< "/ "<<mauso<< ") "<<endl; } void In(){ cout<< " ("<<tuso<< "/ "<<mauso<< ") "<<endl; } }; void main(){ Phanso x(1,0); x.In(); } A. Kết quả in ra là (1/0) B. Chương trình bị lỗi khi biên dịch C. Kết quả in ra là (1/1) D. Kết quả in ra là (1/0)(1/0) Câu 2: Cho biết kết quả của đoạn chương trình sau: class some{ public: ~some(){ cout<<"ABC"<<endl; } }; void main(){ some s; s.~some(); } A. Chương trình báo lỗi B. ABCABC C. Chương trình thực thi mà không in ra gì cả D. ABC Câu 3: Constructor của lớp sau thuộc loại constructor nào: class Phanso{ private: int tuso,mauso; 243 public: Phanso(int tu=0,int mau=1); }; A. Constructor ảo (virtual constructor) B. Constructor sao chép (copy constructor) C. Không có hàm nào cả D. Constructor mặc định(default constructor) Câu 4: Cho biết kết quả của đoạn chương trình sau: int main(){ int x=5; int &n=x; n=9; cout<<x; }; A. Chương trình không in ra gì hết B. 9 C. 5 D. Chương trình báo lỗi Câu 5: Cho biết kết quả của đoạn chương trình sau: void cap_phat_bo_nho(int *a){ a = new int[5]; for (int i=0;i<5;i++) a[i] = i+1; }; int main(){ int n=5; int *a=&n; cap_phat_bo_nho(a); cout<<a[0]; } A. 5 B. Chương trình báo lỗi khi biên dịch C. 1 D. Chương trình in ra địa chỉ của biến n Câu 6: Cho đoạn chương trình sau: 1: class A { 2: private: 3: int x, y; 4: public: 5: A(int x1,int y1) ; 6: void In(); 7: }; 244 8: A::A(int x1, int y1) { 9: x =x1; y=y1; } 10: void A ::In() { 11: cout << x <<y; } 12: void main() { 13: A a1(1); 14: A a2(20, 10); 15: a1 = a2; 16: a1.In(); 17: } Ðoạn lệnh bên khi dịch sẽ thông báo lỗi tại: A. Dòng 9 , do sai lỗi cú pháp B. Dòng 15, do không có toán tử gán “=” trong định nghĩa lớp C. Dòng 11, do sai lỗi cú pháp D. Dòng 13, do không có hàm khởi tạo với tham số tương ứng Câu 7: Cho đoạn chương trình sau: class A { private: int x,y; public: A(int x1, int y1) { x = x1; y=y1; } void In() { cout<<x<<", "<<y; } }; void F() { A a(10, 10); a.x += 10; a.y = a.x; a.In(); } Khi gọi hàm F(), kết quả hiển thị trên màn hình là: A. Chương trình sẽ báo lỗi do truy cập đến thành phần private của lớp B. Màn hình xuất ra: 10, 10 C. Màn hình xuất ra: 20, 10 D. Màn hình xuất ra: 20, 20 Câu 8: Kết quả biên dịch – thực thi đoạn chương trình sau: class Test { }; void main() { Test t; } A. Lỗi thực thi. B. Chương trình thực thi mà không xuất ra gì hết. C. Lỗi biên dịch. D. Chương trình chạy vô tận 245 Câu 9: Kết quả biên dịch – thực thi chương trình sau: class Test { public: int n; private: void Display(){ cout<<n; } public: Test(){ n=5; } }; void main() { Test t; t.Display(); } A. Lỗi thực thi B. Chương trình thực thi xuất ra màn hình: 5. C. Lỗi biên dịch. D. Chương trình thực thi mà không xuất gì ra màn hình. Câu 10: Kết quả biên dịch - thực thi chương trình sau: class ABC { int n; public: ABC(int x){n=x; } void Print(){ cout<<n; } }; void main() { ABC t; t.Print(); } A. Lỗi thực thi B. Chương trình thực thi xuất ra màn hình một số âm. C. Lỗi biên dịch. D. Chương trình thực thi mà không xuất gì ra màn hình. Câu 11: Kết quả biên dịch – thực thi chương trình sau: class Point { int xVal, yVal; public: Point(int x = 0, int y = 0){ xVal = x ; yVal = y ; } void Print(){ cout<<" ("<<xVal <<","<<yVal<<" )"; } }; void main() { Point pt(5); pt.Print(); 246 } A. B. C. D. Lỗi do khởi tạo đối tượng không đúng Hiển thị trên màn hình (5,5) Hiển thị trên màn hình (0,5) Hiển thị trên màn hình (5,0) Câu 12: Kết quả biên dịch - thực thi chương trình sau: class Point { int xVal, yVal; public: Point(int x = 0, int y = 0){ xVal = x ; yVal = y ; cout<< "So nguyen " ; } Point(double x = 0, double y = 0){ xVal = x ; yVal = y ; cout<< "So thuc " ; } void Print() ; }; void Point :: Print(){ cout<< " ("<<xVal <<","<<yVal<<" )"; } void main() { Point pt(5.7); pt.Print(); getch(); } A. Hiển thị trên màn hình So thuc (5,0) B. Hiển thị trên màn hình So nguyen (5,0) C. Hiển thị trên màn hình So nguyen (5,5) D. Chương trình bi lỗi biên dịch. Câu 13: Kết quả biên dịch - thực thi chương trình sau: class Point { int n; Point(int x) { n=x; } void Print(){cout<<n; } }; void main() { Point pt(4); pt.Print(); } A. Lỗi thực thi B. Chương trình thực thi xuất ra màn hình : 4 247 C. Lỗi biên dịch. D. Chương trình thực thi mà không xuất gì ra màn hình. Câu 14: Kết quả biên dịch - thực thi chương trình sau: class Test { int t; public : Test(int t){ Test::t = t ; } void Print(){cout<<t; } }; void main() { Test test(4); test.Test::Print(); } A. Lỗi thực thi B. Chương trình thực thi xuất ra màn hình : 4 C. Lỗi biên dịch. D. Chương trình thực thi mà không xuất gì ra màn hình. Câu 15: Khi thực thi đoạn chương trình sau kết quả sẽ là : class AAA { int na; public : AAA(int a=0) { na = a ; } ~AAA(){cout<< " "<<na ;} }; void Func(AAA aaa){ AAA *a1 = new AAA(3); delete a1; } void main() { AAA aaa(4); Func(aaa); } A. B. C. D. Xuất ra màn hình : 0 3 4 Xuất ra màn hình : 3 3 4 Xuất ra màn hình : 3 4 4 Xuất ra màn hình : 3 4 Câu 16: Khi thực thi đoạn chương trình sau kết quả sẽ là: class AAA { int na; 248 public : AAA(int a=0) { na = a ; } ~AAA(){cout<< " "<<na ;} }; void Func(const AAA& aaa){ AAA *a1 = new AAA(3); delete a1; } void main() { AAA aaa(4); Func(aaa); } A. Xuất ra màn hình : 0 3 4 B. Xuất ra màn hình : 3 3 4 C. Xuất ra màn hình : 3 4 4 D. Xuất ra màn hình : 3 4 Câu 17: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Teacher { public: static int n; public : Teacher (){ cout<<" "<<n++ ; } }; int Teacher::n = 0; void main() { Teacher t1; Teacher t2; Teacher t3; cout<<" "<<t1.n ; getch(); } A. Xuất ra màn hình : 0 1 2 3 B. Xuất ra màn hình : 0 1 2 2 C. Xuất ra màn hình : 1 2 3 4 D. Xuất ra màn hình : 1 2 3 1 Câu 18: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Employee { char ten[30]; char ms[10]; int tuoi; public : Employee (char te[], char m[], int tu):tuoi(tu) 249 { strcpy(ten, te); strcpy(ms, m); } void Display(){ cout<<"Ma so: "<<ms<<" Ten: "<<ten<<" Tuoi:"<<tuoi<<endl ; } }; void main() { Employee e(" Nguyen Van A", "001",20); e.Display(); } A. B. C. D. Lỗi biên dịch. Lỗi thực thi Xuất hiện trên,màn hinh: “Ma so: 001 Ten: Nguyen Van A Tuoi: 20” Không hiển thị gì trên màn hình. Câu 19: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Base{ public: Base(){ cout<<”Base class”<<endl; } }; class Derive:Base { public: Derive(){ cout<<”Derive class”<<endl; } }; void main(){ Derive d; } A. Base class Derive class B. Base class C. Derive class Base class D. Derive class Câu 20: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Base{ public: Base(){} ~Base(){ cout<<”Base class”<<endl; } }; class Derive:Base { public: Derive(){} ~Derive(){ cout<<”Derive class”<<endl; } }; 250 A. B. C. D void main(){ Derive d; } Base class Derive class Base class Derive class Base class Derive class Câu 21: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Base{ public: int xVal; Base(int x=0) : xVal(x){ cout<<”xVal = ”<<xVal<<endl; } }; class Derive:Base { public: Derive(){xVal = 10;} void Print(){ cout<<”xVal = ”<<xVal<<endl; } }; void main(){ Derive d; d.Print(); } A. Màn hình xuất hiện: xVal = 0 xVal = 10 B. Màn hình xuất hiện: xVal = 10 C. Màn hình xuất hiện: xVal = 0 D Chương trình bị lỗi Câu 22: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Base{ protected: int xVal; public: Base(){ xVal = 5; } public: void Print(){ cout<<”xVal = ”<<xVal<<endl; } }; class Derive:public Base { public: 251 Derive(int x) { xVal = x;} }; void main(){ Derive d(10); d.Print(); } A. Màn hình xuất hiện: xVal = 5 xVal = 10 B. Màn hình xuất hiện: xVal = 10 C. Màn hình xuất hiện: xVal = 5 D. Chương trình bị lỗi. Câu 23: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Base{ protected: int xVal; public: Base(){ xVal = 5; } public: void Print(){ cout<<”xVal = ”<<xVal<<endl; } }; class Derive:protected Base { public: Derive(int x) { xVal = x; } }; void main(){ Derive d(10); d.Print(); } A. Màn hình xuất hiện: xVal = 5 xVal = 10 B. Màn hình xuất hiện: xVal = 10 C. Màn hình xuất hiện: xVal = 5 D. Chương trình bị lỗi. Câu 24: Khi thực thi đoạn chương trình sau kết quả sẽ là: class BaseA{ protected: int A; public: BaseA(){ A = 5; } void Print(){ cout<<”A = ”<<A<<endl; } }; class BaseB { protected: int B; public: BaseB(){ B = 10; } 252 void Print(){ cout<<”B = ”<<B<<endl; } }; class Derive:public BaseA,public BaseB {}; void main(){ Derive d(); d.BaseA::Print(); A. B. C. D. } Chương trình bị lỗi. Màn hình xuất hiện: Màn hình xuất hiện: Màn hình xuất hiện: B= A= A= 10 5 5 B= 10 Câu 25: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Point{ private: int xVal, yVal; public: void Print(){ cout<<"("<<xVal<<","<<yVal<<")"; } Point(int x=0, int y=0):xVal(x),yVal(y){} Point(int x){ Point::xVal = Point::yVal = x; } friend Point operator + (Point, Point); }; Point operator + (Point p1, Point p2) { return Point(p1.xVal+p2.xVal, p1.yVal+p2.yVal); } void main(){ Point p1(3,4); Point p = p1+3; p.Print(); } A. Lỗi biên dịch B. Kết quả là (6,7) C. Lỗi thực thi D. Kết quả là (6,4) Câu 26: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Point{ private: int xVal, yVal; public: void Print(){ cout<<"("<<xVal<<","<<yVal<<")"; 253 } Point(int x, int y):xVal(x),yVal(y){ } Point(int x){ Point::xVal = Point::yVal = x; } friend Point operator + (Point, Point); }; Point operator + (Point p1, Point p2) { return Point(p1.xVal+p2.xVal, p1.yVal+p2.yVal); } void main(){ Point p1(3,4); Point p = p1+6; p.Print(); } A. Lỗi biên dịch B. Kết quả là (9,10) C. Lỗi thực thi D. Kết quả là (9,4) Câu 27: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Set{ int n, size, *elems; public: Set(int s=0):size(s), n(0), elems(new int[s]) { for(int i=0; i<s; i++) elems[i] = i; } void Print(){ for(int i=0; i<size; i++) cout<<" "<<elems[i]; } Set Func(Set); ~Set() {delete []elems;} Set Set::Func (Set s1 ){ Set s = s1; return s; } void main(){ Set s(10); Set s1= s.Func(s); s.Print(); } A. Lỗi biên dịch B. Kết quả là : 0 1 2 3 4 5 6 7 8 9 C. Lỗi thực thi D. Là một kết quả khác 254 Câu 28: Khi thực thi đoạn chương trình sau kết quả sẽ là: class Test{ public: Test(){cout<<" A ";} Test(const Test& t){ cout<<" B "; } void operator=(const Test&t){ cout<<" C "; } }; void main(){ Test t1; Test t2; t2 = t1; } A. Kết quả là : A A B B. Kết quả là : A B C C. Kết quả là : A A C D. Là một kết quả khác Câu 29: Khi thực thi đoạn chương trình sau kết quả sẽ là: class A{ public: A(){cout<<" A ";} }; class B{ public: B(){cout<<" B ";} }; void SinhLoi(int a){ if(a>0) throw A(); throw B(); } void main(){ int n=-1; try{ SinhLoi(n); }catch(A){ catch(B){ } A. Kết quả là : B D B. Kết quả là : B C C. Kết quả là : A D D. Kết quả là : A C Câu 30: 1: 2: 3: cout<<" C " ; } cout<<" D "; } template <class T> T max(T a, T b) { if (a > b) return a; return b; 255 4: } 5: float max (float fa, float fb) { 6: if (fa - fb >0) return fa; 7: return fb; 8: } 9: void main(){ 10: 11: cout<<max(a,b); 12: } Khi thêm lệnh nào vào dòng 10 thì chương trình báo lỗi: A. float a = 5.0, b = 6.0 ; B. float a = 5.0; int b = 6 ; C. char a = ‘A’, b = ‘B’ ; D. int a = 5; int b = 6 ; Câu 31: 1: template <class T, class U> 2: void func (T a, U b){ 3: cout<<a<<b ; 4: } 5: void main(){ 6: 7: func(a,b); 8: } Thêm lệnh nào đúng nhất dưới đây mà khi thêm vào dòng 6 thì chương trình không báo lỗi A. float a = 5.0, b = 6.0 ; B. float a = 5.0; int b = 6 ; C. char a = ‘A’ ; char *b = “Hello” ; D. Tất cả câu trên Câu 32: Khi thực thi đoạn chương trình sau kết quả sẽ là: template <class T> class Test { T x, y; public: Test(T a = 0, T b = 0) { x = b; y = a; } void display(); } void point<T>::display() { cout<<"("<<x<<","<<y<<") "; } void main() { Test<int> ti(3,5); ti.display(); Test<char> tc('a','b'); tc.display(); 256 A. B. C. D. } Kết quả là : (3, 5) (a, b) Kết quả là : (5, 3) (b, a) Chương trình bị lỗi Một kết quả khác Câu 33: Khi thực thi đoạn chương trình sau kết quả sẽ là: template <class T, int n> class table{ T data[n]; public: table() { } T & operator[](int i){ return data[i]; } }; void main(){ int n = 2; table <int, n>t; t[0] =0; t[1] =1; cout<<t[0]<<" "<<t[1]; } A. B. C. D. Lỗi biên dịch 01 Lỗi thực thi Một kết quả khác Câu 34: Cho đoạn chương trình sau: class A { private: int x,y; public: A(int x1, int y1) { x = x1; y=y1; } void In_X() { cout<< " x= " <<x; } void In() const { cout<<x<<", "<<y; } }; void F(const A& b) { A a(10, 10); // Lệnh L1 a.In_X(); // Lệnh L2 a.In(); // Lệnh L3 b.In_X(); // Lệnh L4 b.In(); } A. Lệnh L1 B. Lệnh L2 C. Lệnh L3 D. Lệnh L4 257 Câu 35: class A { public: int x,y; public: A(int x1, int y1) { x = x1; y=y1; } void In() { cout<<x<< ", "<<y; } }; void F() { A a(10, 10); a.x+= 10; a.y = a.x; a.In(); } Khi gọi hàm F(), kết quả hiển thị trên màn hình là: A. Chương trình sẽ báo lỗi do truy cập đến thành phần private của lớp B. Màn hình xuất ra: 10, 10 C. Màn hình xuất ra: 20, 10 D. Màn hình xuất ra: 20, 20 Câu 36: Xét đoạn chương trình khai báo class Data như sau: class Data{ private: int a; public: Data operator --(int); void Giam(){ a=a-1;} }; A. Data Data::operator – (int){ Data temp=*this; Giam(); return *temp; } B. Data Data::operator – (int){ Giam(); Data temp=*this; return temp; } C. Data Data::operator – (int){ Data temp=*this; Giam(); return temp; } D. Data Data::operator – (int){ 258 Data temp=*this; temp.Giam(); return this; } Câu 37: Khi thực thi đoạn chương trình kết quả sẽ là: class Phanso{ private: int tuso,mauso; public: Phanso(int t=0,int m=1){ tuso=t; mauso=m; cout<< " ("<<tuso<< "/ "<<mauso<< ") "<<endl; } Phanso(const Phanso &x){ tuso=x.tuso; mauso=x.mauso; cout<< " ("<<tuso<< "/ "<<mauso<< ") "<<endl; } Phanso operator++(int){ Phanso old = *this; tuso += mauso; return old; } }; void main(){ Phanso x(1,2); Phanso y=x++; }; A. Kết quả in ra là (1/2)(3/2) B. Chương trình thực thi mà không in ra kết quả gì C. Chương trình bị lỗi khi biên dịch D. Kết quả in ra là (1/2)(1/2) (1/2) Câu 38: Cho biết kết quả của đoạn chương trình sau: class a{ public: void fun(){ cout<<"class a"; } }; class b: public a{ public: void fun(){ cout<<"class b"; 259 } }; void main() { a *obj = new b(); obj->fun(); } A. class a class b B. class a C. Chương trình báo lỗi D. class b Câu 39: Cho biết kết quả của đoạn chương trình sau: class A{ public: virtual void In() { cout<<"A"; } }; class B: public A{ public: void In() { cout<<"B"; } }; class C: public B{ public: void In() { cout<<"C"; } }; void main() { B *pb,b; C c; pb = &b; pb ->A::In(); pb->In(); pb = &c; pb->In(); } A. Chương trình báo lỗi B. Kết quả in ra: ABC C. Kết quả in ra: ABB D. Kết quả in ra: BBC 260 Câu 40: Cho biết kết quả của đoạn chương trình sau: class A{ public: int x; }; class B:protected A{ protected: int y; }; class C:private B{ private: int z; C() { x =1; // lệnh L1 y =1; // lệnh L2 z =1; // lệnh L3 } }; A. Các lệnh L1, L2 sai, lệnh L3 đúng B. Các lệnh L1 sai, lệnh L2, L3 đúng C. Các lệnh L1, L2 và L3 đều sai D. Các lệnh L1, L2 và L3 đều đúng Câu 41: Cho biết kết quả của đoạn chương trình sau: class BaseA{ protected: int A; public: BaseA(){ A=5;} void Print() { cout<<"A = "<<A<<endl;} }; class BaseB{ protected: int B; public: BaseB(){ B=10;} void Print() { cout<<"B = "<<B<<endl;} }; class Derive:public BaseA,public BaseB{}; void main(){ Derive d; d.BaseA::Print(); } A. Màn hình xuất hiện: A=5 B=10 B. Màn hình xuất hiện: B=10 C. Chương trình báo lỗi D. Màn hình xuất hiện: A=5 Câu 42: Cho biết kết quả của đoạn chương trình sau: 261 class A{ public: virtual void In()=0; }; class B:public A{ }; class C:public B{ public: void In(){cout<<"C";} }; void main(){ C c; B *pb = &c; pb->In(); } A. Chương trình chạy và hiển thị ký tự C trên màn hình B. Chương trình chạy và không hiển thị kết quả gì C. Chương trình báo lỗi do khai báo biến con trỏ pb thuộc kiểu lớp trừu tượng D. Chương trình báo lỗi do khai báo biến c thuộc kiểu lớp trừu tượng Câu 43: Cho biết kết quả của đoạn chương trình sau: class Base{ public: Base(){ cout<<"Base class " <<endl;} }; class Derive: Base { Derive(){ cout<<"Derive class " <<endl;} }; void main(){ Base b; Derive d; } A. Base class Derive class B. Derive class Base class C. Base class Base class Derive class D. Base class Derive class Base class Câu 44: Lớp D là lớp kế thừa public từ lớp B. Hàm F là hàm friend của lớp D. Khi đó: A. Hàm F cũng là hàm bạn của lớp B B. Hàm F có thể truy xuất các thành viên có giới hạn private của lớp B C. Hàm F có thể truy xuất các thành viên có giới hạn private của lớp B và D D. Hàm F có thể truy xuất các thành viên có giới hạn private của lớp D Câu 45: Phát biểu nào sau đây là SAI: 262 A. Lớp dẫn xuất có thể khởi tạo giá trị khác cho các thuộc tính có giới hạn public của lớp cơ sở. B. Destructor của lớp cơ sở đƣợc gọi sau destructor của lớp dẫn xuất. C. Hàm ảo khác với hàm thuần ảo. D. Hàm ảo của một lớp có thể là hàm friend của lớp đó. Câu 46: Phát biểu nào sau đây là ĐÚNG trong đa năng hoá toán tử (operator overloading) A. Có thể thay đổi kết quả thực hiện của các toán tử trên các kiểu dữ liệu cơ sở (built-in). B. Có thể định nghĩa thêm toán tử mới ngoài những toán tử đã có trong C++. C. Có thể thay đổi số lượng tham số (số ngôi) của các toán tử. D. Không thể thay đổi độ ưu tiên của các toán tử. Câu 47: Để đa năng hoá toán tử >> dùng để nhập các thông tin của đối tượng thuộc class Data (ví dụ như: cin>>dataToPrint). Dòng đầu tiên trong hàm cài đặt toán tử là: A. ostream operator>>(ostream &input,const Data &dataToPrint) B. ostream operator>>(istream &input,const Data &dataToPrint) C. istream &operator>>(istream &input, Data dataToPrint) D. istream &operator>>(istream &input, Data &dataToPrint) Câu 48: Trong các lệnh khai báo đối tượng sau đây, lệnh nào sẽ gọi constructor sao chép: A. Phanso t,x; t = x; B. Phanso x; Phanso y = x; C. Phanso x; D. Phanso y(3,4); Câu 49: Trong các khai báo hàm sau đây, hàm nào là constructor mặc định (default constructor) của lớp Phanso: A. Phanso() B. Phanso(int ts, int ms) C. Phanso(const Phanso& x ) D. Phanso(int ts) Câu 50: Phát biểu nào sau đây là đúng: A. Trong hàm constructor ta có thể dùng lệnh return để trả về một giá trị cho hàm B. Mỗi lớp chỉ có duy nhất một hàm destructor C. Hàm destructor không thể là một hàm ảo D. Tất cả đều sai 263 264 TÀI LIỆU THAM KHẢO [1]. Trần Đình Quế, Nguyễn Mạnh Hùng, Ngôn ngữ lập trình C++, Học viện Công nghệ bưu chính viễn thông, 2006. [2]. Lê Hải Trung, Bài tập lập trình hướng đối tượng, Đại học Kỹ thuật Công nghiệp Thái Nguyên, 2015 [3]. Trần Minh Thái, Bài giảng Lập trình hướng đối tượng, Website: www.minhthai.edu.vn, ngày truy cập 03/05/2018. [4]. Stroustrup, Bjarne, The C++ Programming Language, Reading, MA: Addison-Wesley, 1993. [5]. Ivar Jacobson, Object - Oriented Software Engineering, Addison-Wesley Publishing Company, 1992. [6]. Michael Blaha, William Premerlani, Object - Oriented Modeling and Design for Database Applications, Prentice Hall, 1998. [7]. Phạm Văn Ất, C++ và lập trình hướng đối tượng, Nxb Khoa học và Kỹ thuật, 1999. [8]. Website: http://www.cplusplus.com/doc/tutorial, ngày truy cập 03/05/2018. 265 NHÀ XUẤT BẢN KHOA HỌC TỰ NHIÊN VÀ CÔNG NGHỆ Nhà A16 - Số 18 Hoàng Quốc Việt, Cầu Giấy, Hà Nội Điện thoại: Phòng Phát hành: 024.22149040; Phòng Biên tập: 024.37917148; Phòng Quản lý Tổng hợp: 024.22149041; Fax: 024.37910147; Email: nxb@vap.ac.vn; Website: www.vap.ac.vn LẬP TRÌNH HƯỚNG TỚI ĐỐI TƯỢNG C++ Đỗ Quang Hưng, Nguyễn Thị Vân Anh, Lã Quang Trung Chịu trách nhiệm xuất bản Giám đốc, Tổng biên tập TRẦN VĂN SẮC Biên tập: Đinh Như Quang Trình bày kỹ thuật: Đỗ Hồng Ngân Trình bày bìa: Đỗ Hồng Ngân Liên kết xuất bản: Trường Đại học Công nghệ Giao thông vận tải Số 54 phố Triều Khúc, Thanh Xuân, Hà Nội ISBN: 978-604-913-766-2 In 180 cuốn, khổ19×27cm, tại Công ty CP Khoa học & Công nghệ Hoàng Quốc Việt. Địa chỉ: Số 18 Hoàng Quốc Việt, Cầu Giấy, Hà Nội. Số xác nhận đăng ký xuất bản: 3721-2018/CXBIPH/07-47/KHTNVCN Số quyết định xuất bản: 53/QĐ-KHTNCN, cấp ngày 07 tháng 11 năm 2018 In xong và nộp lưu chiểu quý IV năm 2018. 266