Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức...

226
1 Mục lục Giới thiệu............................................................................................................... 7 Chương 1. Mở đầu .............................................................................................. 9 1.1. Chương trình là gì? .................................................................................. 9 1.2. Lập trình là gì?......................................................................................... 9 1.2.1. Mức cao độc lập với máy tính .......................................................... 9 1.2.2. Mức thấp phụ thuộc vào máy tính .................................................. 11 1.3. Ngôn ngữ lập trình và chương trình dịch .............................................. 11 1.4. Môi trường lập trình bậc cao ................................................................. 12 1.5. Lỗi và tìm lỗi ......................................................................................... 14 1.6. Lịch sử C và C++ .................................................................................. 15 1.7. Chương trình C++ đầu tiên.................................................................... 16 Bài tập ............................................................................................................. 20 Chương 2. Biến, kiểu dữ liệu và các phép toán................................................ 21 2.1. Kiểu dữ liệu ........................................................................................... 23 2.1.1. Kiểu dữ liệu cơ bản ........................................................................ 23 2.1.2. Kiểu dữ liệu dẫn xuất ..................................................................... 25 2.2. Khai báo và sử dụng biến ...................................................................... 25 2.2.1. Định danh và cách đặt tên biến ...................................................... 25 2.2.2. Khai báo biến .................................................................................. 26 2.3. Hằng....................................................................................................... 26 2.4. Các phép toán cơ bản............................................................................. 27 2.4.1. Phép gán ......................................................................................... 27 2.4.2. Các phép toán số học ...................................................................... 27 2.4.3. Các phép toán quan hệ .................................................................... 28

Transcript of Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức...

Page 1: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

1

Mục lục

Giới thiệu............................................................................................................... 7

Chương 1. Mở đầu .............................................................................................. 9

1.1. Chương trình là gì? .................................................................................. 9

1.2. Lập trình là gì? ......................................................................................... 9

1.2.1. Mức cao độc lập với máy tính .......................................................... 9

1.2.2. Mức thấp phụ thuộc vào máy tính .................................................. 11

1.3. Ngôn ngữ lập trình và chương trình dịch .............................................. 11

1.4. Môi trường lập trình bậc cao ................................................................. 12

1.5. Lỗi và tìm lỗi ......................................................................................... 14

1.6. Lịch sử C và C++ .................................................................................. 15

1.7. Chương trình C++ đầu tiên .................................................................... 16

Bài tập ............................................................................................................. 20

Chương 2. Biến, kiểu dữ liệu và các phép toán ................................................ 21

2.1. Kiểu dữ liệu ........................................................................................... 23

2.1.1. Kiểu dữ liệu cơ bản ........................................................................ 23

2.1.2. Kiểu dữ liệu dẫn xuất ..................................................................... 25

2.2. Khai báo và sử dụng biến ...................................................................... 25

2.2.1. Định danh và cách đặt tên biến ...................................................... 25

2.2.2. Khai báo biến .................................................................................. 26

2.3. Hằng ....................................................................................................... 26

2.4. Các phép toán cơ bản ............................................................................. 27

2.4.1. Phép gán ......................................................................................... 27

2.4.2. Các phép toán số học ...................................................................... 27

2.4.3. Các phép toán quan hệ .................................................................... 28

Page 2: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

2

2.4.4. Các phép toán logic ........................................................................ 29

2.4.5. Độ ưu tiên của các phép toán ......................................................... 29

2.4.6. Tương thích giữa các kiểu .............................................................. 30

Bài tập ............................................................................................................. 31

Chương 3. Các cấu trúc điều khiển .................................................................. 33

3.1. Luồng điều khiển ................................................................................... 33

3.2. Các cấu trúc rẽ nhánh ............................................................................ 34

3.2.1. Lệnh if-else................................................................................ 34

3.2.2. Lệnh switch .................................................................................. 40

3.3. Các cấu trúc lặp ..................................................................................... 44

3.3.1. Vòng while ................................................................................... 44

3.3.2. Vòng do-while ............................................................................. 47

3.3.3. Vòng for ........................................................................................ 50

3.4. Các lệnh break và continue .............................................................. 55

3.5. Biểu thức điều kiện trong các cấu trúc điều khiển ................................ 58

Bài tập ............................................................................................................. 60

Chương 4. Hàm ................................................................................................ 62

4.1. Các hàm có sẵn ...................................................................................... 63

4.2. Cấu trúc chung của hàm ........................................................................ 64

4.3. Cách sử dụng hàm ................................................................................. 65

4.4. Biến toàn cục và biến địa phương ......................................................... 66

4.4.1. Phạm vi của biến ............................................................................ 66

4.4.2. Thời gian sống của biến.................................................................. 68

4.5. Tham số, đối số, và cơ chế truyền tham số cho hàm ............................. 69

4.5.1. Truyền giá trị .................................................................................. 69

Page 3: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

3

4.5.2. Truyền tham chiếu .......................................................................... 70

4.5.3. Tham số mặc định .......................................................................... 73

4.6. Hàm trùng tên ........................................................................................ 75

4.7. Hàm đệ quy ............................................................................................ 77

Bài tập ............................................................................................................. 79

Chương 5. Mảng ............................................................................................... 81

5.1. Mảng một chiều ..................................................................................... 81

5.1.1. Khai báo và khởi tạo mảng ............................................................. 82

5.1.2. Ứng dụng của mảng ........................................................................ 84

5.1.3. Trách nhiệm kiểm soát tính hợp lệ của chỉ số mảng ...................... 86

5.1.4. Mảng làm tham số cho hàm ........................................................... 86

5.2. Mảng nhiều chiều .................................................................................. 87

5.3. Mảng và xâu kí tự .................................................................................. 89

5.3.1. Khởi tạo giá trị cho xâu kí tự .......................................................... 91

5.3.2. Thư viện xử lý xâu kí tự ................................................................. 91

5.4. Tìm kiếm và sắp xếp dữ liệu trong mảng .............................................. 92

5.4.1. Tìm kiếm tuyến tính ....................................................................... 92

5.4.2. Tìm kiếm nhị phân .......................................................................... 93

5.4.3. Sắp xếp chọn ................................................................................... 95

Bài tập ............................................................................................................. 97

Chương 6. Con trỏ và bộ nhớ ........................................................................... 99

6.1. Bộ nhớ máy tính .................................................................................... 99

6.2. Biến và địa chỉ của biến ......................................................................... 99

6.3. Biến con trỏ ......................................................................................... 100

6.4. Mảng và con trỏ ................................................................................... 105

Page 4: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

4

6.5. Bộ nhớ động ........................................................................................ 107

6.5.1. Cấp phát bộ nhớ động ................................................................... 107

6.5.2. Giải phóng bộ nhớ động ............................................................... 108

6.6. Mảng động và con trỏ .......................................................................... 109

6.7. Truyền tham số là con trỏ .................................................................... 111

Bài tập ........................................................................................................... 115

Chương 7. Các kiểu dữ liệu trừu tượng .......................................................... 118

7.1. Định nghĩa kiểu dữ liệu trừu tượng bằng cấu trúc struct .................... 118

7.2. Định nghĩa kiểu dữ liệu trừu tượng bằng cấu trúc class .................. 124

7.2.1. Quyền truy nhập ........................................................................... 127

7.2.2. Toán tử phạm vi và định nghĩa các hàm thành viên..................... 128

7.2.3. Hàm khởi tạo và hàm hủy ............................................................ 129

7.3. Lợi ích của lập trình hướng đối tượng ................................................. 132

7.4. Biên dịch riêng rẽ ................................................................................ 133

Bài tập ........................................................................................................... 137

Chương 8. Vào ra dữ liệu ............................................................................... 140

8.1. Khái niệm dòng dữ liệu ....................................................................... 140

8.2. Tệp văn bản và tệp nhị phân ................................................................ 141

8.3. Vào ra tệp ............................................................................................. 141

8.3.1. Mở tệp ........................................................................................... 142

8.3.2. Đóng tệp ....................................................................................... 143

8.3.3. Xử lý tệp văn bản.......................................................................... 144

8.3.4. Xử lý tệp nhị phân ........................................................................ 147

Bài tập ........................................................................................................... 151

Chương 9. Viết chồng toán tử ........................................................................ 153

Page 5: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

5

9.1. Hàm friends ......................................................................................... 153

9.1.1. Hàm friend .................................................................................... 153

9.1.2. Lớp Friend (Friend Class) ............................................................ 158

9.2. Khai báo viết chồng toán từ ................................................................. 158

9.3. Viết chồng hàm một số toán tử toán học ............................................. 159

9.4. Viết chồng hàm toán tử gán ................................................................. 162

9.5. Viết chồng hàm toán tử so sánh .......................................................... 163

9.6. Viết chồng toán tử << và >> ............................................................... 165

9.6.1. Tạo riêng toán tử << ..................................................................... 166

9.6.2. Tạo riêng toán tử >> .................................................................... 171

9.7. Nạp chồng new và delete ..................................................................... 173

9.7.1. Nạp chồng new và delete .............................................................. 173

9.7.2. Nạp chồng new và delete cho mảng. ............................................ 178

9.7.3. Nạp chồng new và delete không có throw ................................... 180

Chương 10. Tính chất của Lớp ....................................................................... 182

10.1. Tính đóng gói và quyền truy cập ....................................................... 182

10.2. Thừa kế .............................................................................................. 182

10.3. Kế thừa tạo tử và hủy tử .................................................................... 192

10.3.1. Khi hàm khởi tử và hủy tử được thi hành .................................. 192

10.3.2. Truyền tham số cho hàm tạo tử của lớp cơ sở ............................ 196

10.4. Phương thức ảo .................................................................................. 198

10.5. Tính đa hình ....................................................................................... 200

Chương 11. Một số vấn đề khác ..................................................................... 202

11.1. Các chỉ thị tiền xử lý.......................................................................... 202

11.1.1. Lệnh chỉ thị bao hàm tệp #include ............................................. 202

Page 6: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

6

11.1.2. Lệnh chỉ định nghĩa #define ....................................................... 202

11.1.3. Chỉ thị biên dịch có điều kiện #if, #else ..................................... 203

11.1.4. Chỉ thị biên dịch có điều kiện #ifdef và #ifndef ......................... 203

11.1.5. Chỉ thỉ thông báo lỗi #error ........................................................ 204

11.2. Kỹ thuật xử lý ngoại lệ và bắt lỗi ...................................................... 205

11.3. Lập trình trên nhiều file ..................................................................... 207

11.4. Mẫu (Template) và thư viện STL ...................................................... 209

11.4.1. Mẫu (Tempate) ........................................................................... 209

11.4.2. Hàm khái quát (Generic function) .............................................. 210

11.4.3. Lớp khái quát (Generic class) ..................................................... 212

11.4.4. Giới thiệu thư viện chuẩn STL ................................................... 215

Phụ lục A. Phong cách lập trình ..................................................................... 217

Phụ lục B. Dịch chương trình C++ bằng GNU C++ ...................................... 221

Phụ lục C. Xử lý xâu bằng thư viện cstring ................................................... 224

Tài liệu tham khảo ............................................................................................. 226

Page 7: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

7

Giới thiệu

Lập trình là một trong những bước quan trọng nhất trong quy trình giải quyết

một bài toán. Nhiều ngôn ngữ lập trình đã được ra đời nhằm giúp chúng ta giải

quyết các bài toán một cách hiệu quả nhất. Mỗi ngôn ngữ lập trình có những thế

mạnh và nhược điểm riêng. Các ngôn ngữ lập trình có thể chia ra thành hai loại

chính là ngôn ngữ lập trình bậc thấp (gần gũi với ngôn ngữ máy), và ngôn ngữ

lập trình bậc cao (ngần gũi với ngôn ngữ tự nhiên của con người).

Giáo trình này trang bị cho sinh viên những kiến thức cơ bản về lập trình. Ngôn

ngữ lập trình C++ được sử dụng để minh họa cho việc lập trình. Việc lựa chọn

C++ bởi vì nó là một ngôn ngữ lập trình hướng đối tượng chuyên nghiệp được

sử dụng rộng rãi trên toàn thế giới để phát triển các chương trình từ đơn giản

đến phức tạp. Hơn thế nữa, sự mềm dẻo của C++ cho phép chúng ta giải quyết

những bài toán thực tế một cách nhanh chóng, ngoài ra cho phép chúng ta quản

lý và tương tác trực tiếp đến hệ thống, bộ nhớ để nâng cao hiệu quả của chương

trình.

Giáo trình được chia thành 8 chương, mỗi chương trình bày một vấn đề lý

thuyết trong lập trình. Các vấn đề lý thuyết được mô tả bằng các ví dụ thực tế.

Kết thúc mỗi chương là phần bài tập để sinh viên giải quyết và nắm rõ hơn về lý

thuyết. Cấu trúc của giáo trình như sau:

Chương 1: giới thiệu các khái niệm cơ bản về lập trình và quy trình giải

quyết một bài toán. Sinh viên sẽ hiểu về ngôn ngữ lập trình bậc cao và ngôn

ngữ lập trình bậc thấp. Cuối chương chúng tôi giới thiệu về môi trường lập

trình cũng như ngôn ngữ lập trình C++.

Chương 2: Chúng tôi giới thiệu các khái niệm cơ bản về biến số, hằng số,

các kiểu dữ liệu cơ bản và các phép toán cơ bản. Sau khi học, sinh viên sẽ

biết cách khai báo và sử dụng biến số, hằng số, và các phép toán trên biến và

hằng số.

Chương 3: Trương này giới thiệu về cấu trúc chương trình cũng như các cấu

trúc điều khiển. Cụ thể là các cấu rẽ nhánh (if-else, switch), cấu trúc lặp (for,

while, do-while) sẽ được giới thiệu.

Chương 4: Chương trình con và hàm sẽ được giới thiệu để sinh viên hiểu

được chiến lược lập trình “chia để trị”. Chúng tôi sẽ trình bày chi tiết về

Page 8: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

8

cách khai báo và sử dụng hàm, cũng như cách truyền tham số, truyền giá trị

cho hàm.

Chương 5: Chương này trình bày cấu trúc dữ liệu kiểu mảng và xâu kí tự.

Cách khai báo và sử dụng mảng một chiều cũng như mảng nhiều chiều và ví

dụ liên quan được trình bày chi tiết ở chương này.

Chương 6: Đây là một chương tương đối đặc thù cho C++, khi chúng tôi

trình bày về bộ nhớ và kiểu dữ liệu con trỏ. Cấu trúc bộ nhớ, cách quản lý

và xin cấp phép bộ nhớ động thông qua việc sử dụng biến con trỏ sẽ được

trình bày.

Chương 7: Trong chương này chúng tôi sẽ trình bày về cấu trúc dữ liệu trừu

tượng (cụ thể là struct và class trong C++). Sinh viên sẽ hiểu và biết cách

tạo ra những cấu trúc dữ liệu trừu tượng phù hợp với các kiểu đối tượng dữ

liệu cần biểu diễn. Cuối chương, chúng tôi cũng giới thiệu về lập trình hướng

đối tượng, một thế mạnh của ngôn ngữ lập trình C++.

Chương 8: Chúng tôi giới thiệu về cách vào ra dữ liệu. Sinh viên sẽ được

giới thiệu chi tiết về cách làm việc với các tệp dữ liệu.

Page 9: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

9

Chương 1. Mở đầu

Trong chương này, chúng tôi sẽ giới thiệu qua một số khái niệm cơ bản về:

chương trình, lập trình, ngôn ngữ lập trình.

1.1. Chương trình là gì?

Bạn chắc chắn đã dùng qua nhiều chương trình khác nhau, ví dụ như chương

trình soạn thảo văn bản “Microsoft Word”. Chương trình, hay phần mềm, được

hiểu đơn giản là một tập các lệnh để máy tính thực hiện theo. Khi bạn đưa cho

máy tính một chương trình và yêu cầu máy tính thực hiện theo các lệnh của

chương trình, bạn đang chạy chương trình đó.

1.2. Lập trình là gì?

Lập trình là có thể hiểu đơn giản là quá trình viết ra các lệnh hướng dẫn máy

tính thực hiện để giải quyết một bài toán cụ thể nào đó. Lập trình là một bước

quan trọng trong quy trình giải quyết một bài toán như mô tả ở Hình 1.1.

Xác định

vấn đề

Thiết kế

thuật toánLập trình Dịch

Mã giảNgôn ngữ lập trình,

C++, Java...Mã máy

t/g

iấy

t/g

iấy

trìn

h s

oạ

n th

ảo

ch

ươ

ng

trì

nh

dịc

h

Ngôn ngữ tự nhiên

Mức cao độc lập với máy tínhMức thấp phụ thuộc

vào máy tính

Hình 1.1: Quy trình giải quyết một bài toán.

Quy trình trên có thể được chia ra thành hai mức: mức cao độc lập với máy tính

(machine independent) và mức thấp phụ thuộc vào máy tính (machine specific).

1.2.1. Mức cao độc lập với máy tính

Mức cao độc lập với máy tính thường được chia thành ba bước chính là: xác

định vấn đề, thiết kế thuật toán và lập trình.

Page 10: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

10

Xác định vấn đề: Bước này định nghĩa bài toán, xác định dữ liệu đầu vào, các

ràng buộc, yêu cầu cần giải quyết và kết quả đầu ra. Bước này thường sử dụng

bút/giấy và ngôn ngữ tự nhiên như tiếng Anh, tiếng Việt để mô tả và xác định

vấn đề cần giải quyết.

Thiết kế thuật toán: Một thuật toán là một bộ các chỉ dẫn nhằm giải quyết một

bài toán. Các chỉ dẫn này cần được diễn đạt một cách hoàn chỉnh và chính xác

sao cho mọi người có thể hiểu và tiến hành theo. Thuật toán thường được mô tả

dưới dạng mã giả (pseudocode). Bước này có thể sử dụng giấy bút và thường

không phụ thuộc vào ngôn ngữ lập trình. Ví dụ về thuật toán “tìm ước số chung

lớn nhất (UCLN) của hai số x và y” viết bằng ngôn ngữ tự nhiên:

Bước 1: Nếu x>y thì thay x bằng phần dư của phép chia x/y.

Bước 2: Nếu không, thay y bằng phần dư của phép chia y/x.

Bước 3: Nếu trong hai số x và y có một số bằng 0 thì kết luận UCLN

là số còn lại.

Bước 4: Nếu không, quay lại Bước 1.

hoặc bằng mã giả:

repeat

if x > y then x := x mod y

else y := y mod x

until x = 0 or y = 0

if x = 0 then UCLN := y

else UCLN := x

Lập trình là bước chuyển đổi thuật toán sang một ngôn ngữ lập trình, phổ biến

là các ngôn ngữ lập trình bậc cao, ví dụ như các ngôn ngữ C++, Java. Bước

này, lập trình viên sử dụng một chương trình soạn thảo văn bản để viết chương

trình. Trong và sau quá trình lập trình, người ta phải tiến hành kiểm thử và sửa

lỗi chương trình. Có ba loại lỗi thường gặp: lỗi cú pháp, lỗi trong thời gian

chạy, và lỗi logic (xem chi tiết ở Mục 1.5).

Page 11: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

11

1.2.2. Mức thấp phụ thuộc vào máy tính

Các ngôn ngữ lập trình bậc cao, ví dụ như C, C++, Java, Visual Basic, C#, được

thiết kế để con người tương đối dễ hiểu và dễ sử dụng. Tuy nhiên, máy tính

không hiểu được các ngôn ngữ bậc cao. Do đó, trước khi một chương trình viết

bằng ngôn ngữ bậc cao có thể chạy được, nó phải được dịch sang ngôn ngữ

máy, hay còn gọi là mã máy, mà máy tính có thể hiểu và thực hiện được. Việc

dịch đó được thực hiện bởi một chương trình máy tính gọi là chương trình dịch.

1.3. Ngôn ngữ lập trình và chương trình dịch

Như chúng ta thấy, quá trình giải quyết một bài toán thông qua các bước khác

nhau để chuyển đổi từ ngôn ngữ tự nhiên mà con người hiểu được sang ngôn

ngữ máy mà máy tính có thể hiểu và thực hiện được. Ngôn ngữ lập trình thường

được chia ra thành hai loại: ngôn ngữ lập trình bậc thấp và ngôn ngữ lập trình

bậc cao.

Ngôn ngữ lập trình bậc thấp như hợp ngữ (assembly language) hoặc mã máy

là ngôn ngữ gần với ngôn ngữ máy mà máy tính có thể hiểu được. Đặc điểm

chính của các ngôn ngữ này là chúng có liên quan chặt chẽ đến phần cứng của

máy tính. Các họ máy tính khác nhau sử dụng các ngôn ngữ khác nhau. Chương

trình viết bằng các ngôn ngữ này có thể chạy mà không cần qua chương trình

dịch. Các ngôn ngữ bậc thấp có thể dùng để viết những chương trình cần tối ưu

hóa về tốc độ. Tuy nhiên, chúng thường khó hiểu đối với con người và không

thuận tiện cho việc lập trình.

Ngôn ngữ lập trình bậc cao như Pascal, Ada, C, C++, Java, Visual Basic,

Python, … là các ngôn ngữ có mức độ trừu tượng hóa cao, gần với ngôn ngữ tự

nhiên của con người hơn. Việc sử dụng các ngôn ngữ này cho việc lập trình do

đó dễ dàng hơn và nhanh hơn rất nhiều so với ngôn ngữ lập trình bậc thấp. Khác

với ngôn ngữ bậc thấp, chương trình viết bằng các ngôn ngữ bậc cao nói chung

có thể sử dụng được trên nhiều loại máy tính khác nhau.

Các chương trình viết bằng một ngôn ngữ bậc cao muốn chạy được thì phải

được dịch sang ngôn ngữ máy bằng cách sử dụng chương trình dịch. Chương

trình dịch có thể chia ra thành hai loại là trình biên dịch và trình thông dịch.

Một số ngôn ngữ bậc cao như C, C++ yêu cầu loại chương trình dịch được gọi

là trình biên dịch (compiler). Trình biên dịch dịch mã nguồn thành mã máy –

dạng có thể thực thi được. Kết quả của việc dịch là một chương trình thực thi

được và có thể chạy nhiều lần mà không cần dịch lại. Ví dụ, với ngôn ngữ C++

Page 12: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

12

một trình biên dịch rất phổ biến là gcc/g++ trong bộ GNU Compiler Collection

(GCC) chạy trong các môi trường Unix/Linux cũng như Windows. Ngoài ra,

Microsoft Visual C++ là trình biên dịch C++ phổ biến nhất trong môi trường

Windows. Một số ngôn ngữ bậc cao khác như Perl, Python yêu cầu loại chương

trình dịch gọi là trình thông dịch (interpreter). Khác với trình biên dịch, thay

vì dịch toàn bộ chương trình một lần, trình thông dịch vừa dịch vừa chạy

chương trình, dịch đến đâu chạy chương trình đến đó.

Trong môn học này, C++ được chọn làm ngôn ngữ thể hiện. Đây là một trong

những ngôn ngữ lập trình chuyên nghiệp được sử dụng rộng rãi nhất trên thế

giới. Trong phạm vi nhập môn của môn học này, C++ chỉ được giới thiệu ở mức

rất cơ bản, rất nhiều tính năng mạnh của C++ sẽ không được nói đến hoặc chỉ

được giới thiệu sơ qua. Người học nên tiếp tục tìm hiểu về ngôn ngữ C++, vượt

ra ngoài giới hạn của cuốn sách này.

1.4. Môi trường lập trình bậc cao

Để lập trình giải quyết một bài toán bằng ngôn ngữ lập trình bậc cao, bạn cần có

công cụ chính là: chương trình soạn thảo, chương trình dịch dành cho ngôn ngữ

sử dụng, và các thư viện chuẩn của ngôn ngữ sử dụng (standard library), và

chương trình tìm lỗi (debugger).

Các bước cơ bản để xây dựng và thực hiện một chương trình:

1. Soạn thảo: Mã nguồn chương trình được viết bằng một phần mềm soạn thảo

văn bản dạng text và lưu trên ổ đĩa. Ta có thể dùng những phần mềm soạn

thảo văn bản đơn giản nhất như Notepad (trong môi trường Windows) hay vi

(trong môi trường Unix/Linux), hoặc các công cụ soạn thảo trong môi trường

tích hợp để viết mã nguồn chương trình. Mã nguồn C++ thường đặt trong

các tệp với tên có phần mở rộng là .cpp, cxx, .cc, hoặc .C (viết hoa).

2. Dịch: Dùng trình biên dịch dịch mã nguồn chương trình ra thành các đoạn

mã máy riêng lẻ (gọi là “object code”) lưu trên ổ đĩa. Các trình biên dịch phổ

biến cho C++ là vc.exe trong bộ Microsoft Visual Studio hay gcc trong bộ

GNU Compiler với các tham số thích hợp để dịch và liên kết để tạo ra tệp

chạy được. Với C++, ngay trước khi dịch còn có giai đoạn tiền xử lý

(preprocessing) khi các định hướng tiền xử lý được thực thi để làm các thao

tác như bổ sung các tệp văn bản cần dịch hay thay thế một số chuỗi văn bản.

Một số định hướng tiền xử lý quan trọng sẽ được giới thiệu dần trong cuốn

sách này.

Page 13: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

13

3. Liên kết: Một tệp mã nguồn thường không chứa đầy đủ những phần cần

thiết cho một chương trình hoàn chỉnh. Nó thường dùng đến dữ liệu hoặc

hàm được định nghĩa trong các tệp khác hoặc trong thư viện chuẩn. Trình

liên kết (linker) kết nối các đoạn mã máy riêng lẻ với nhau và với các thư

viện có sẵn để tạo ra một chương trình mã máy hoàn chỉnh chạy được.

4. Nạp: Trình nạp (loader) sẽ nạp chương trình dưới dạng mã máy vào bộ nhớ.

Các thành phần bổ sung từ thư viện cũng được nạp vào bộ nhớ.

5. Chạy: CPU nhận và thực hiện lần lượt các lệnh của chương trình, dữ liệu và

kết quả thường được ghi ra màn hình hoặc ổ đĩa.

Thường thì không phải chương trình nào cũng chạy được và chạy đúng ngay ở

lần chạy thử đầu tiên. Chương trình có thể có lỗi cú pháp nên không qua được

bước dịch, hoặc chương trình dịch được nhưng gặp lỗi trong khi chạy. Trong

những trường hợp đó, lập trình viên phải quay lại bước soạn thảo để sửa lỗi và

thực hiện lại các bước sau đó.

ổ đĩa

Soạn thảo

(edit)Nạp

(load)

Dịch

(compile)

Liên kết

(link)

Chạy

(execute)

Bộ nhớ

Hình 1.2: Các bước cơ bản để xây dựng một chương trình.

Để thuận tiện cho việc lập trình, các công cụ soạn thảo, dịch, liên kết, chạy...

nói trên được kết hợp lại trong một môi trường lập trình tích hợp (IDE –

integrated development environment), trong đó, tất cả các công đoạn đối với

người dùng chỉ còn là việc chạy các tính năng trong một phần mềm duy nhất.

IDE rất hữu ích cho các lập trình viên. Tuy nhiên, đối với những người mới học

lập trình, thời gian đầu nên tự thực hiện các bước dịch và chạy chương trình

thay vì thông qua các chức năng của IDE. Như vậy, người học sẽ có thể nắm

được bản chất các bước của quá trình xây dựng chương trình, hiểu được bản

Page 14: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

14

chất và đặc điểm chung của các IDE, tránh tình trạng bị phụ thuộc vào một IDE

cụ thể.

Ví dụ về các IDE phổ biến là Microsoft Visual Studio – môi trường lập trình

thương mại cho môi trường Windows, và Eclipse – phần mềm miễn phí với các

phiên bản cho cả môi trường Windows cũng như Unix/Linux, cả hai đều hỗ trợ

nhiều ngôn ngữ lập trình.

Dành cho C++, một số môi trường lập trình tích hợp phổ biến là Microsoft

Visual Studio, Dev-C++, Code::Blocks, KDevelop. Mỗi môi trường có thể hỗ

trợ một hoặc nhiều trình biên dịch. Chẳng hạn Code::Blocks hỗ trợ cả GCC và

MSVC Do C++ có các phiên bản khác nhau.

Có những bản cài đặt khác nhau của C++. Các bản ra đời trước chuẩn C++ 1998

(ISO/IEC 14882) có thể không hỗ trợ đầy đủ các tính năng được đặc tả trong

chuẩn ANSI/ISO 1998. Bản C++ do Microsoft phát triển khác với bản C++ của

GNU. Tuy nhiên, các trình biên dịch hiện đại hầu hết hỗ trợ C++ chuẩn, ta cũng

nên chọn dùng các phần mềm này. Ngôn ngữ C++ được dùng trong cuốn sách

này tuân theo chuẩn ISO/IEC 14882, còn gọi là "C++ thuần túy" (pure C++).

1.5. Lỗi và tìm lỗi

Trong và sau quá trình lập trình, chúng ta phải tiến hành kiểm thử và sửa lỗi

chương trình. Có ba loại lỗi thường gặp: lỗi cú pháp, lỗi run-time và lỗi logic.

Lỗi cú pháp là do lập trình viên viết sai với các quy tắc cú pháp của ngôn ngữ

lập trình, chẳng hạn thiếu dấu chấm phảy ở cuối lệnh. Chương trình biên dịch sẽ

phát hiện ra các lỗi cú pháp và cung cấp thông báo về vị trí mà nó cho là có lỗi.

Nếu trình biên dịch nói rằng chương trình có lỗi cú pháp thì chắc chắn là có lỗi

cú pháp trong chương trình. Tuy nhiên, lỗi là chỗ nào thì trình biên dịch chỉ có

thể đoán, và nó có thể đoán sai.

Lỗi run-time là lỗi xuất hiện trong khi chương trình đang chạy. Lỗi dạng này sẽ

gây ra thông báo lỗi và ngừng chương trình. Ví dụ là khi chương trình thực hiện

phép chia cho 0.

Lỗi logic có nguyên nhân là do thuật toán không đúng, hoặc do lập trình viên

gặp sai sót khi thể hiện thuật toán bằng ngôn ngữ lập trình (ví dụ viết nhầm dấu

cộng thành dấu trừ). Khi có lỗi logic, chương trình của bạn có thể dịch và chạy

bình thường, nhưng kết quả của chương trình đưa ra lại có trường hợp sai hoặc

Page 15: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

15

hoạt động của chương trình không như mong đợi. Lỗi logic là loại lỗi khó tìm ra

nhất.

Nếu chương trình của bạn dịch và chạy không phát sinh thông báo lỗi, thậm chí

chương trình cho ra kết quả có đúng với một vài bộ dữ liệu test, điều đó không

có nghĩa chương trình của bạn hoàn toàn không có lỗi. Để có thể chắc chắn hơn

về tính đúng đắn của chương trình, bạn cần chạy thử chương trình với nhiều bộ

dữ liệu khác nhau và so sánh kết quả mà chương trình tạo ra với kết quả mong

đợi.

1.6. Lịch sử C và C++

Ngôn ngữ lập trình C được tạo ra bởi Dennis Ritchie (phòng thí nghiệm Bell) và

được sử dụng để phát triển hệ điều hành UNIX. Một trong những đặc điểm nổi

bật của C là độc lập với phần cứng (portable), tức là chương trình có thể chạy

trên các loại máy tính và các hệ điều hành khác nhau. Năm 1983, ngôn ngữ C đã

được chuẩn hóa và được gọi là ANSI C bởi Viện chuẩn hóa quốc gia Hoa Kỳ

(American National Standards Institute). Hiện nay ANSI C vẫn là ngôn ngữ lập

trình chuyên nghiệp và được sử dụng rộng rãi để phát triển các hệ thống tính

toán hiệu năng cao.

Ngôn ngữ lập trình C++ do Bjarne Stroustrup (thuộc phòng thí nghiệm Bell)

phát triển trên nền là ngôn ngữ lập trình C và cảm hứng chính từ ngôn ngữ lập

trình Simula67. So với C, C++ là ngôn ngữ an toàn hơn, khả năng diễn đạt cao

hơn, và ít đòi hỏi các kỹ thuật bậc thấp. Ngoài những thế mạnh thừa kế từ C,

C++ hỗ trợ trừu tượng hóa dữ liệu, lập trình hướng đối tượng và lập trình tổng

quát, C++ giúp xây dựng dễ dàng hơn những hệ thống lớn và phức tạp.

Bắt đầu từ phiên bản đầu tiên năm 1979 với cái tên "C with Classes" (C kèm lớp

đối tượng)1 với các tính năng cơ bản của lập trình hướng đối tượng, C++ được

phát triển dần theo thời gian. Năm 1983, cái tên "C++" chính thức ra đời, các

tính năng như hàm ảo (virtual function), hàm trùng tên và định nghĩa lại toán tử

(overloading), hằng ... được bổ sung. Năm 1989, C++ có thêm lớp trừu tượng,

đa thừa kế, hàm thành viên tĩnh, hằng hàm, và thành viên kiểu protected. Các bổ

sung cho C++ trong thập kỉ sau đó là khuôn mẫu (template), không gian tên

(namespace), ngoại lệ (exception), các toán tử đổi kiểu dữ liệu mới, và kiểu dữ

1 Theo lời kể của Bjarne Stroustrup tại trang cá nhân của ông tại trang web của phòng thí nghiệm AT&T

http://www2.research.att.com/~bs/bs_faq.html#invention

Page 16: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

16

liệu Boolean. Năm 1998, lần đầu tiên C++ được chính thức chuẩn hóa quốc tế

bởi tổ chức ISO, kết quả là chuẩn ISO/IEC 148822.

Đi kèm với sự phát triển của ngôn ngữ là sự phát triển của thư viện chuẩn C++.

Bên cạnh việc tích hợp thư viện chuẩn truyền thống của C với các sửa đổi nhỏ

cho phù hợp với C++, thư viện chuẩn C++ còn có thêm thư viện stream I/O

phục vụ việc vào ra dữ liệu dạng dòng. Chuẩn C++ năm 1998 tích hợp thêm

phần lớn thư viện STL (Standard Template Library – thư viện khuôn mẫu

chuẩn)3. Phần này cung cấp các cấu trúc dữ liệu rất hữu ích như vector, danh

sách, và các thuật toán như sắp xếp và tìm kiếm.

Hiện nay, C++ là một trong các ngôn ngữ lập trình chuyên nghiệp được sử dụng

rộng rãi nhất.

1.7. Chương trình C++ đầu tiên

Chương trình đơn giản trong Hình 1.3 sẽ hiện ra màn hình dòng chữ “Hello

world!”. Trong chương trình có những đặc điểm quan trọng của C++. Ta sẽ xem

xét từng dòng.

2 Văn bản này (ISO/IEC 14882:1998) sau đó được phát hiện lỗi chỉnh sửa vào năm 2003, thành phiên bản

ISO/IEC 14882:2003.

3 STL vốn không nằm trong thư viện chuẩn mà là một thư viện riêng do HP và sau đó là SGI phát triển.

Page 17: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

17

Các chú thích

Định hướng tiền xử lý để khai báo sử

dụng thư viện chuẩn vào/ra iostream

Hàm main() là nơi chương trình bắt đầu

thực hiện và kết thúc. Hàm main() xuất

hiện đúng một lần trong chương trình. Hàm

main() trả lại một giá trị nguyên cho biết

trạng thái kết thúc của chương trình.

Viết ra màn hình dòng chữ

“Hello world!”

Lệnh return kết thúc hàm.

Giá trị 0 được trả về để cho biết

chương trình kết thúc bình thường.

// The first program in C++// Print "Hello world!" to the screen

#include <iostream>

using namespace std;

int main(){ cout << "Hello world!";

return 0;}

Khai báo sử dụng không gian tên chuẩn std

Hình 1.3: Chương trình C++ đầu tiên.

Hai dòng đầu tiên bắt đầu bằng chuỗi // là các dòng chú thích chương trình. Đó

là kiểu chú thích dòng đơn. Các dòng chú thích không gây ra hoạt động gì của

chương trình khi chạy, trình biên dịch bỏ qua các dòng này. Ngoài ra còn có

dạng chú thích kiểu C dùng chuỗi /* và */ để đánh dấu điểm bắt đầu và kết thúc

chú thích. Các lập trình viên dùng chú thích để giải thích và giới thiệu về nội

dung chương trình.

Dòng thứ ba, #include <iostream> là một định hướng tiền xử lý

(preprocessor directive) – chỉ dẫn về một công việc mà trình biên dịch cần thực

hiện trước khi dịch chương trình. #include là khai báo về thư viện sẽ được sử

dụng trong chương trình, trong trường hợp này là thư viện vào ra dữ liệu

iostream trong thư viện chuẩn C++.

Tiếp theo là hàm main, phần không thể thiếu của mỗi chương trình C++. Nó bắt

đầu từ dòng khai báo header của hàm:

int main()

Mỗi chương trình C++ thường bao gồm một hoặc nhiều hàm, trong đó có đúng

một hàm có tên main, đây là nơi chương trình bắt đầu thực hiện và kết thúc.

Bên trái từ main là từ khóa int, nó có nghĩa là hàm main sẽ trả về một giá trị là

số nguyên. Từ khóa là những từ đặc biệt mà C++ dành riêng cho những mục

Page 18: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

18

đích cụ thể. Chương 4 sẽ cung cấp thông tin chi tiết về khái niệm hàm và việc

hàm trả về giá trị.

Thân hàm main được bắt đầu và kết thúc bởi cặp ngoặc {}, bên trong đó là

chuỗi các lệnh mà khi chương trình chạy chúng sẽ được thực hiện tuần tự từ

lệnh đàu tiên cho đến lệnh cuối cùng. Hàm main trong ví dụ đang xét có chứa

hai lệnh. Mỗi lệnh đều kết thúc bằng một dấu chẩm phảy, các định hướng tiền

xử lý thì không.

Lệnh thứ nhất gồm cout, toán tử <<, xâu kí tự "Hello world!", và dấu chấm

phảy. Nó chỉ thị cho máy tính thực hiện một nhiệm vụ: in ra màn hình chuỗi kí

tự nằm giữa hai dấu nháy kép – "Hello world!". Khi lệnh được thực thi, chuỗi kí

tự Hello world sẽ được gửi cho cout – luồng dữ liệu ra chuẩn của C++,

thường được nối với màn hình. Chi tiết về vào ra dữ liệu sẽ được nói đến trong

Chương 8. Chuỗi kí tự nằm giữa hai dấu nháy kép được gọi là một xâu kí tự

(string). Để ý dòng

using namespace std;

nằm ở gần đầu chương trình. Tất cả thành phần của thư viện chuẩn C++, trong

đó có cout được dùng đến trong hàm main, được khai báo trong một không gian

tên (namespace) có tên là std. Dòng trên thông báo với trình biên dịch rằng

chương trình ví dụ của ta sẽ sử dụng đến một số thành phần nằm trong không

gian tên std. Nếu không có khai báo trên, tiền tố std:: sẽ phải đi kèm theo tên

của tất cả các thành phần của thư viện chuẩn được dùng trong chương trình,

chẳng hạn cout sẽ phải được viết thành std::cout. Chi tiết về không gian tên

nằm ngoài phạm vi của cuốn sách này, người đọc có thể tìm hiểu tại các tài liệu

[1] hoặc [2]. Nếu không có lưu ý đặc biệt thì tất cả các chương trình ví dụ trong

cuốn sách này đều sử dụng khai báo sử dụng không gian tên std như ở trên.

Lệnh thứ hai nhảy ra khỏi hàm và trả về giá trị 0 làm kết quả của hàm. Đây là

bước có tính chất quy trình do C++ quy định hàm main cần trả lại một giá trị là

số nguyên cho biết trạng thái kết thúc của chương trình. Giá trị 0 được trả về ở

cuối hàm main có nghĩa rằng hàm đã kết thúc thành công.

Để ý rằng tất các lệnh nằm bên trong cặp ngoặc {} của thân hàm đều được lùi

đầu dòng một mức. Với C++, việc này không có ý nghĩa về cú pháp. Tuy nhiên,

nó lại giúp cho cấu trúc chương trình dễ thấy hơn và chương trình dễ hiểu hơn

đối với người lập trình. Đây là một trong các điểm quan trọng trong các quy ước

về phong cách lập trình. Phụ lục A sẽ hướng dẫn chi tiết hơn về các quy ước

này.

Page 19: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

19

Đến đây ta có thể sửa chương trình trong Hình 1.3 để in ra lời chào "Hello

world!" theo các cách khác nhau. Chẳng hạn, ta có thể in ra cùng một nội dung

như cũ nhưng bằng hai lệnh gọi cout:

cout << "Hello "; cout << "world!";

hoặc in ra lời chào trên nhiều dòng bằng cách chèn vào giữa xâu kí tự các kí tự

xuống dòng (kí tự đặc biệt được kí hiệu là \n):

cout << "Hello \n world!\n";

Phụ lục B hướng dẫn về cách sử dụng bộ công cụ GNU C++ để dịch và chạy

chương trình.

Page 20: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

20

Bài tập

1. Trình bày các bước chính để giải quyết một bài toán. Phân tích nội dung

và đặc điểm chính của từng bước.

2. Tại sao cần phải có chương trình dịch, sự khác biệt giữa trình biên dịch

và trình thông dịch? Liệt kê các ngôn ngữ lập trình cần có trình biên dịch,

và các ngôn ngữ lập trình cần có trình thông dịch.

3. Sự khác biệt, ưu điểm và nhược điểm giữa ngôn ngữ lập trình bậc cao và

ngôn ngữ lập trình bậc thấp? Nêu một ví dụ mà nên sử dụng ngôn ngữ lập

trình bậc thấp để giải quyết, và một ví dụ mà nên sử dụng ngôn ngữ lập

trình bậc cao để giải quyết.

4. Trình bày các ngôn ngữ lập trình bậc thấp mà bạn biết, nêu ra các đặc

điểm nổi bật của từng ngôn ngữ lập trình đó.

5. Trình bày các ngôn ngữ lập trình bậc cao mà bạn biết, nêu ra các đặc

điểm nổi bật của từng ngôn ngữ lập trình đó.

6. Trình bày sự khác biệt, ưu điểm và nhược điểm giữa ngôn ngữ lập trình C

và C++.

7. Trình bày các loại lỗi thường gặp khi lập trình. Phân tích đặc điểm của

từng loại lỗi trên.

8. Trình bày 5 ví dụ về lỗi logic mà bạn có thể gặp trong lập trình.

9. Làm quen với môi trường lập trình Dev-C++. Liệt kê ra các chức năng

chính của môi trường Dev-C++. Tìm hiểu và so sánh các môi trường lập

trình khác cho C và C++.

10. Viết một chương trình C++ để hiện ra màn hình tên của bạn. Sử dụng

biên dịch dòng lệnh bằng bộ công cụ GNU C++ để dịch và chạy chương

trình.

Page 21: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

21

Chương 2. Biến, kiểu dữ liệu và các phép toán

Đa số chương trình không chỉ có những hoạt động đơn giản như là hiển thị một

xâu kí tự ra màn hình mà còn phải thao tác với dữ liệu. Trong một chương trình,

biến là tên của một vùng bộ nhớ được dùng để lưu dữ liệu trong khi chương

trình chạy. Dữ liệu lưu trong một biến được gọi là giá trị của biến đó. Chúng ta

có thể truy nhập, gán hay thay đổi giá trị của các biến, khi biến được gán một

giá trị mới, giá trị cũ sẽ bị ghi đè lên.

Khai báo biến toàn cục

totalApples kiểu int

Khai báo biến địa phương

numberOfBaskets

sau đó gán giá trị 5 cho nó

#include <iostream>

using namespace std;

int totalApples;

int main(){ int numberOfBaskets = 5; int applePerBasket;

cout << "Enter number apples per baskets: ";cin >> applePerBasket;

totalApples = numberOfBaskets * applePerBasket; cout << "Number of apples is " << totalApples;

return 0;}

Gán giá trị nhập từ bàn phím

cho biến applePerBasket

Hình 2.1: Khai báo và sử dụng biến.

Hình 2.1 minh họa việc khai báo và sử dụng biến. Trong đó, các dòng

int totalApples;

int numberOfBaskets = 5;

int applePerBasket;

là các dòng khai báo biến. totalApples, numberOfBaskets, và

applePerBasket là các tên biến. Các khai báo trên có nghĩa rằng totalApples,

numberOfBaskets, và applePerBasket là dữ liệu thuộc kiểu int, nghĩa là các

biến này sẽ giữ giá trị kiểu nguyên. Dòng khai báo numberOfBaskets có một

điểm khác với hai dòng còn lại, đó là numberOfBaskets được khởi tạo với giá

trị 5. C++ quy định rằng tất cả các biến đều phải được khai báo với một cái tên

Page 22: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

22

và một kiểu dữ liệu trước khi biến đó được sử dụng. Các biến thuộc cùng một

kiểu có thể được khai báo trên cùng một dòng, cách nhau bởi một dấu phảy.

Chẳng hạn, có thể thay hai dòng khai báo cho numberOfBaskets, và

applePerBasket bằng:

int numberOfBaskets = 5, applePerBasket;

Chương trình trong Hình 2.1 yêu cầu người dùng nhập số táo trong mỗi giỏ

(applePerBasket), tính tổng số táo (totalApples) với dữ kiện đã biết là số giỏ

táo (numberOfBasket), rồi in ra màn hình. Cụ thể, dòng

cout << "Enter number apples per baskets: ";

in ra màn hình xâu kí tự Enter number apples per baskets: . Đó là lời mời

nhập dữ liệu, là hướng dẫn dành cho người sử dụng chương trình.

Dòng tiếp theo

cin >> applePerBasket;

đọc dữ liệu được người dùng nhập vào từ đầu vào chuẩn – thường là từ bàn

phím. Khi chạy lệnh này, chương trình sẽ đợi người dùng nhập vào một giá trị

cho biến applePerBasket. Người dùng đáp ứng bằng cách gõ vào một số

nguyên dưới dạng chuỗi các chữ số rồi nhấn phím Enter để gửi các chữ số đó

cho máy tính. Đến lượt nó, máy tính biến đổi chuỗi các chữ số nó nhận được

thành một giá trị kiểu nguyên rồi chép giá trị này vào biến applePerBasket.

Tương ứng với cout là đối tượng quản lý dòng dữ liệu ra chuẩn của thư viện

C++, cin là đối tượng quản lý dòng dữ liệu vào chuẩn, thường là từ bàn phím.

Tiếp theo là lệnh gán

totalApples = numberOfBaskets * applePerBasket;

Lệnh này tính tích giá trị của hai biến numberOfBaskets và applePerBasket

rồi gán kết quả cho biến totalApples, trong đó * là kí hiệu của phép nhân và =

là kí hiệu của phép gán.

Lệnh in kết quả ra màn hình

cout << "Number of apples is " << totalApples;

hiển thị liên tiếp hai thành phần: xâu kí tự "Number of apples is " và giá trị

của biến totalApples.

Page 23: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

23

Ngoài việc in giá trị của một biến, C++ còn cho phép ta in kết quả của một biểu

thức. Do đó, ta có một lựa chọn khác là gộp công việc của hai lệnh trên (tính

tích hai biến và in tích ra màn hình) vào một lệnh:

cout << "Number of apples is " << numberOfBaskets *

applePerBasket;

Khi đó, biến totalApples vốn được dùng để lưu trữ kết quả của phép tính trở

nên không còn cần thiết, ta có thể xóa bỏ dòng khai báo biến này. Để ý là lệnh

trên dài và chiếm cả sang dòng thứ hai. C++ cho phép một lệnh nằm trên nhiều

dòng, dấu chấm phảy cuối mỗi lệnh sẽ giúp trình biên dịch hiểu đâu là kết thúc

của lệnh.

2.1. Kiểu dữ liệu

Mỗi biến phải được khai báo để lưu giữ giá trị thuộc một kiểu dữ liệu nào đó.

Ngôn ngữ lập trình bậc cao thường có hai loại kiểu dữ liệu: các kiểu dữ liệu cơ

bản và các kiểu dữ liệu dẫn xuất.

2.1.1. Kiểu dữ liệu cơ bản

Kiểu dữ liệu cơ bản là kiểu dữ liệu do ngôn ngữ lập trình định nghĩa sẵn. Ví dụ

như các kiểu số nguyên – char, int, long int. Biến thuộc kiểu nguyên được

dùng để lưu các số có giá trị nguyên. Đối với kiểu cơ bản chúng ta thường quan

tâm đến kích thước bộ nhớ của kiểu dữ liệu, giới hạn giá trị mà kiểu dữ liệu đó

có thể lưu giữ. Đối với các kiểu dấu chấm động (floating-point) để lưu các giá

trị thuộc kiểu số thực, chúng ta còn quan tâm đến độ chính xác của kiểu dữ liệu

đó. Tài liệu chuẩn C++ không quy định chính xác số byte cần dùng để lưu các

biến thuộc các kiểu dữ liệu cơ bản trong bộ nhớ mà chỉ quy định yêu cầu về

kích thước của kiểu dữ liệu này so với kiểu dữ liệu kia. Các kiểu nguyên có dấu,

signed char, short int, int và long int, phải có kích thước tăng dần. Mỗi kiểu

nguyên có dấu tương ứng với một kiểu nguyên không dấu với cùng kích thước.

Các kiểu nguyên không dấu không thể biểu diễn giá trị âm nhưng có thể biểu

diễn số giá trị dương nhiều gấp đôi kiểu có dấu tương ứng. Tương tự, các kiểu

chấm động, float, double và long double cũng phải có kích thước tăng dần.

Bảng 2.1 liệt kê một số kiểu dữ liệu cơ bản của C++ với kích thước được nhiều

bản cài đặt C++ sử dụng.

Page 24: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

24

Kiểu Mô tả

Kích thước

thông dụng

(byte)

Phạm vi (tương ứng với kích thước)

char ký tự /

số nguyên nhỏ 1

các kí tự ASCII

signed char: -128 → 127, hoặc

unsighed char: 0 → 255

bool giá trị Boolean 1 true hoặc false

short số nguyên 2 signed short: -32767→ 32767

unsigned short: 0→ 65536

int số nguyên lớn 4 signed int: 2147483648 → 2147483647

unsigned int: 0 → -4294967296

long số nguyên

rất lớn 4

signed long: 2147483648 → 2147483647

unsigned long: 0 → -4294967296

float số thực 4 +/- 1.4023x10-45

→ 3.4028x10+38

double số thực với

độ chính xác cao 8 +/- 4.9406x10

-324 → 1.7977x10

308

long

double

số thực với

độ chính xác

rất cao

8 +/- 4.9406x10-324

→ 1.7977x10308

Bảng 2.1: Một số kiểu dữ liệu cơ bản trong C++.

Một số lưu ý:

Kích thước và phạm vi của các kiểu dữ liệu cơ bản phụ thuộc vào hệ thống

mà chương trình được biên dịch tại đó. Tuy nhiên, ở tất cả các hệ thống, kiểu

char bao giờ cũng có kích thước là 1 byte; các kiểu dữ liệu char, short,

int, long phải có kích thước tăng dần; còn các kiểu float, double, long

double phải có độ chính xác cao dần.

Kiểu char dùng để lưu các kí tự đơn (có mã nhỏ hơn 256), chẳng hạn như

chữ cái La-tinh, chữ số, hay các kí hiệu. Trong C++, một kí tự đơn được

đóng trong cặp nháy đơn, ví dụ 'A'. Để lưu các kí tự có mã lớn hơn 255, ta

có thể sử dụng kiểu wchar_t.

Page 25: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

25

Kiểu dữ liệu bool chỉ có hai giá trị true và false. Ta có thể dùng biến

thuộc kiểu này để lưu câu trả lời của những câu hỏi đúng/sai chẳng hạn như

"Có phải index lớn hơn 100?" hay lưu các trạng thái "Ta đã tìm thấy giá trị

âm chưa?". Các giá trị true và false trong C++ thực ra chỉ là 0 và 1.

2.1.2. Kiểu dữ liệu dẫn xuất

Kiểu dữ liệu dẫn xuất là kiểu dữ liệu được xây dựng từ các kiểu dữ liệu cơ bản

bằng các toán tử như * (con trỏ), & (tham chiếu), [] (mảng), () hàm, hoặc được

định nghĩa bằng cơ chế struct hay class. Các kiểu dữ liệu được định nghĩa

bằng cơ chế struct hay class còn được gọi là các kiểu dữ liệu có cấu trúc hoặc

kiểu dữ liệu trừu tượng. Chi tiết về các kiểu dữ liệu dẫn xuất sẽ được trình bày

dần dần trong các chương sau.

2.2. Khai báo và sử dụng biến

Để bắt đầu sử dụng một biến, chúng ta phải tiến hành hai bước: đặt cho biến

một cái tên hợp lệ và khai báo biến.

2.2.1. Định danh và cách đặt tên biến

Định danh (identifier) là thuật ngữ trong ngôn ngữ lập trình khi nói đến tên (tên

biến, tên hàm, tên lớp…). Định danh là một chuỗi kí tự (bao gồm các chữ cái

a..z, A..Z, chữ số 0..9, dấu gạch chân „_‟) viết liền nhau. Định danh không được

bắt đầu bằng chữ số và không được trùng với các từ khóa (những từ mang ý

nghĩa đặc biệt) của ngôn ngữ lập trình. Lưu ý, C++ phân biệt chữ cái hoa và chữ

cái thường.

Cách đặt tên biến tuân thủ theo cách đặt tên định danh. Ví dụ về các tên biến:

_sinhvien, sinhvien_01, sinhVien_01 là các tên biến hợp lệ khác nhau

01sinhvien, sinhviên, "sinhvien" là các tên biến không hợp lệ

Tên biến nên dễ đọc, và gợi nhớ đến công dụng của biến hay kiểu dữ liệu mà

biến sẽ lưu trữ. Ví dụ, nếu cần dùng một biến để lưu số lượng quả táo, ta có thể

đặt tên là totalApples. Không nên sử dụng các tên biến chỉ gồm một kí tự và

không có ý nghĩa như a hay b.

Page 26: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

26

2.2.2. Khai báo biến

Các ngôn ngữ lập trình định kiểu mạnh, trong đó có C++, yêu cầu mỗi biến

trước khi dùng phải được khai báo và biến phải thuộc về một kiểu dữ liệu nào

đó. Có thể chia các biến thành hai loại:

các biến được khai báo ở ngoài tất cả các chương trình con là biến toàn cục,

có hiệu lực trên toàn bộ chương trình, chẳng hạn biến totalApples trong

Hình 2.1.

các biến được khai báo tại một chương trình con là biến địa phương, có

hiệu lực ở bên trong chương trình con đó, chẳng hạn numberOfBaskets và

applePerBasket trong Hình 2.1 là các biến địa phương của hàm main và chỉ

có hiệu lực ở bên trong hàm main.

Chương 4 sẽ nói kĩ hơn về hai loại biến trên.

Trong C++, biến có thể được khai báo gần như bất cứ đâu trong chương trình,

miễn là trước dòng đầu tiên sử dụng đến biến đó (xem Hình 2.1).

Một biến địa phương đã được khai báo nhưng chưa được gán một giá trị nào

được gọi là biến chưa được khởi tạo. Giá trị của biến chưa được khởi tạo thường

là không xác định. Để tránh tình trạng này, ta có thể khởi tạo giá trị của các biến

bằng cách gán giá trị ngay tại lệnh khai báo biến.

2.3. Hằng

Hằng là một loại biến đặc biệt mà giá trị của nó được xác định tại thời điểm

khai báo và không được thay đổi trong suốt chương trình. Để khai báo một

hằng, ta thêm từ khóa const vào phía trước lệnh khai báo biến. Ví dụ:

const float PI = 3.1415926535;

const float SCREEN_WIDTH = 317.24;

Hằng được dùng để đặt tên cho các giá trị không thay đổi được dùng trong

chương trình, chẳng hạn như độ rộng màn hình như trong ví dụ trên. Công dụng

của hằng là mỗi khi cần thay đổi giá trị đó, ta chỉ cần sửa lệnh khai báo hằng

thay vì tìm và sửa giá trị tương ứng tại tất cả các vị trí dùng đến nó. Ngoài ra,

một cái tên có ý nghĩa đặt cho một giá trị được dùng đi dùng lại sẽ giúp cho

chương trình dễ đọc và dễ hiểu hơn.

Page 27: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

27

2.4. Các phép toán cơ bản

2.4.1. Phép gán

Phép gán là cách gắn một giá trị cho một biến hoặc thay đổi giá trị của một

biến. Lệnh gán trong C++ là:

biến = biểu thức;

trong đó dấu bằng (“=”) được gọi là dấu gán hay toán tử gán. Lưu ý, dấu bằng

trong C++ không dùng để so sánh giá trị như trong một số ngôn ngữ lập trình

khác. Ví dụ

symbol = 'A';

là phép gán giá trị „A‟ cho biến symbol, không phải là biểu thức so sánh xem

giá trị của symbol có phải là 'A' hay không.

Công việc của phép gán là tính giá trị của biểu thức bên phải dấu gán rồi lưu giá

trị đó vào trong biến nằm bên trái dấu gán.

Biểu thức có thể là một số, một biến, hoặc một biểu thức phức tạp. Lưu ý rằng

một biến có thể xuất hiện ở cả hai bên của dấu gán. Ví dụ lệnh sau tăng giá trị

của biến apples thêm 2.

apples = apples + 2;

Điểm đặc biệt của C++ là bản thân phép gán cũng chính là một biểu thức với

giá trị trả về là kết quả của phép gán. Ví dụ, phép gán x = 3 là một biểu thức có

giá trị bằng 3.

2.4.2. Các phép toán số học

C++ hỗ trợ năm phép toán số học sau: + (cộng), - (trừ), * (nhân), / (chia), %

(modulo – lấy phần dư của phép chia). Phép chia được thực hiện cho hai giá trị

kiểu nguyên sẽ cho kết quả là thương nguyên. Ví dụ biểu thức 4 / 3 cho kết quả

bằng 1, còn 3 / 5 cho kết quả bằng 0.

Một số phép gán kèm theo biểu thức xuất hiện nhiều lần trong một chương

trình, vì vậy C++ cho phép viết các phép gán biểu thức đó một cách gắn ngọn

hơn, sử dụng các phép gán phức hợp (+=, -=, *=, /=, %=, >>=, <<=, &=, ^=, |=).

Cách sử dụng phép gán phức hợp += như sau:

biến += biểu thức; tương đương biến = biến + biểu thức;

Page 28: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

28

Ví dụ:

apples += 2; tương đương apples = apples + 2;

Các phép gán phức hợp khác được sử dụng tương tự.

C++ còn cung cấp các phép toán ++ (hay --) để tăng (giảm) giá trị của biến lên

một đơn vị. Ví dụ:

apples++ hay ++apple có tác dụng tăng apples thêm 1 đơn vị

apples-- hay --apple có tác dụng giảm apples đi 1 đơn vị

Khác biệt giữa việc viết phép tăng/giảm ở trước biến (tăng/giảm trước) và viết

phép tăng/giảm ở sau biến (tăng/giảm sau) là thời điểm thực hiện phép

tăng/giảm, thể hiện ở giá trị của biểu thức. Phép tăng/giảm trước được thực hiện

trước khi biểu thức được tính giá trị, còn phép tăng/giảm sau được thực hiện sau

khi biểu thức được tính giá trị. Ví dụ, nếu apples vốn có giá trị 1 thì các biểu

thức ++apples hay apples++ đều có hiệu ứng là apples được tăng từ 1 lên 2.

Tuy nhiên, ++apples là biểu thức có giá trị bằng 2 (tăng apples trước tính giá

trị), trong khi apples++ là biểu thức có giá trị bằng 1 (tăng apples sau khi tính

giá trị biểu thức). Nếu ta chỉ quan tâm đến hiệu ứng tăng hay giảm của các phép

++ hay – thì việc phép toán được đặt trước hay đặt sau không quan trọng. Đó

cũng là cách dùng phổ biến nhất của các phép toán này.

Lưu ý, cần hết sức cẩn thận khi sử dụng các phép tăng và giảm trong các biểu

thức. Việc này không được khuyến khích vì tuy nó có thể tiết kiệm được một

hai dòng lệnh nhưng lại làm giảm tính trong sáng của chương trình.

2.4.3. Các phép toán quan hệ

Các phép toán quan hệ được sử dụng để so sánh giá trị hai biểu thức. Các phép

toán này cho kết quả bằng 0 nếu đúng và khác 0 nếu sai. Ta sử dụng giá trị của

một biểu thức quan hệ như là một giá trị thuộc kiểu bool. Ví dụ:

bool enoughApples = (totalApples > 10);

Các phép toán quan hệ trong ngôn ngữ C++ được liệt kê trong Bảng 2.2.

Page 29: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

29

Ký hiệu toán học Toán tử của C++ Ví dụ Ý nghĩa

> > x > y x lớn hơn y

< < x < y x nhỏ hơn y

>= x >= y x lớn hơn hoặc bằng y

<= x <= y x nhỏ hơn hoặc bằng y

= == x == y x bằng y

!= x != y x khác y

Bảng 2.2: Các phép toán quan hệ.

Chú ý tránh nhầm lẫn giữa phép gán giá trị “=” và phép so sánh bằng “==”.

Những nhầm lẫn giữa các phép toán kiểu này thường dẫn đến những lỗi logic

rất khó phát hiện.

2.4.4. Các phép toán logic

Toán tử C++ Ý nghĩa Ví dụ Ý nghĩa của ví dụ

& and x && y Cho giá trị đúng khi cả x và y đúng,

ngược lại cho giá trị sai.

|| or x || y Cho giá trị đúng khi hoặc x đúng hoặc y

đúng, ngược lại cho giá trị sai

! not !x Phủ định của x. Cho giá trị đúng khi x sai;

cho giá trị sai khi x đúng

Bảng 2.3: Các phép toán logic.

Các phép toán logic dành cho các toán hạng là các biểu thức quan hệ hoặc các

giá trị bool. Kết quả của biểu thức logic là giá trị bool.

Ví dụ:

bool enoughApples = (apples > 3) && (apples < 10);

có kết quả là biến enoughApples nhận giá trị là câu trả lời của câu hỏi "biến

apples có giá trị lớn hơn 3 và nhỏ hơn 10 hay không?".

2.4.5. Độ ưu tiên của các phép toán

Dưới đây là mức độ ưu tiên của một số phép toán thường gặp. Thứ tự của chúng

như sau:

Page 30: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

30

Các phép toán nằm trong cặp dấu ngoặc ( ) có độ ưu tiên lớn nhất. Ví dụ:

2 * (1 + 3) cho kết quả bằng 8

Các phép toán *, /, +, -. Trong đó *, / có độ ưu tiên như nhau và cao hơn +, -.

Ví dụ:

2 * 1 + 3 cho kết quả là 5

Các phép toán so sánh <, >, <=, >=. Ví dụ:

3 + 4 < 2 + 6 cho kết quả đúng

Các phép toán logic có thứ thứ tự ưu tiên như sau: !, &&, ||. Ví dụ:

1 || 0 && 0 tương đương với 1 || (0 && 0) và cho kết quả 1

2.4.6. Tương thích giữa các kiểu

Về cơ bản, giá trị gán cho một biến nên cùng kiểu với biến đó. Khi một biến

được gán một giá trị không đúng với kiểu dữ liệu của biến đó, thì giá trị đó sẽ

được chuyển đổi sang kiểu của biến (type conversion). Một vài trường hợp

thường gặp trong việc chuyển kiểu là:

Chuyển đổi giữa số thực và số nguyên

int x = 2.5; // x nhận giá trị 2

double y = 3.5; // y nhận giá trị 3,5

x = y; // x nhận giá trị 3

Phép chia của số nguyên

int divisor = 4;

int dividend = 6;

int quo = dividend/divisor; // quo nhận giá trị 1.

Lưu ý: Phép chia một số nguyên cho một số nguyên sẽ cho kết quả là một số

nguyên. Muốn kết quả là một số thực thì ít nhất một trong hai số phải là số thực.

Page 31: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

31

Bài tập

1. Viết chương trình tính diện tích của một hình tròn. Bán kính là một số thực

và được nhập vào từ bàn phím. Diện tích hình tròn được hiện ra màn hình.

2. Nhập từ bàn phím hai số nguyên là chiều cao của Peter và Essen. Hãy tính

xem Peter cao gấp bao nhiêu lần Essen. Ví dụ, nếu chiều cao của Peter là

180, chiều cao của Essen là 150, thì hiện ra màn hình dòng chữ “Peter is 1.2

times as tall as Essen”.

3. Nhập từ bàn phím một số nguyên là nhiệt độ dưới dạng độ F (Fahrenheit),

hãy hiện ra màn hình nhiệt độ dưới dạng độ C (Celsius). Sinh viên tự tìm

hiểu công thức chuyển đổi.

4. Viết một chương trình trong đó có hai hằng số WIDTH với giá trị bằng

3.17654, và hằng số LENGTH với giá trị bằng 10.03212. Tính và hiện ra

màn hình diện tích của hình chữ nhật với hai cạnh là WIDTH và LENGTH.

5. Nhập từ bàn phím bốn số nguyên a, b, c, d. Hãy tính và hiện ra màn hình giá

trị của biểu thức:

(a + b) > (c + d) || (a - b) > (c - d)

6. Trình bày sự khác biệt giữa biến địa phương và biến toàn cục. Khi nào thì

nên dùng biến địa phương, khi nào thì nên dùng biến toàn cục. Nêu các lưu y

khi sử dụng biến toàn cục.

7. Viết một chương trình có chứa hai biến: biến địa phương bonus và biến toàn

cục score. Nhập giá trị hai biến từ bàn phím, tính và hiện ra màn hình tổng

của score và bonus.

8. Hãy tính (không dùng chương trình) giá trị của các biểu thức sau:

a) 1 + 3 < 2 * 4 – 1 && 1

b) 2 * 2 – 1 + 5 / 1 & 4 – 3

c) (2 – 3 * 1) && 0 / 5 * 2 + 1

9. Tính giá trị của biến c từ đoạn chương trình sau:

a) int a = 3; int b = 2; int c = a/b;

b) double a = 3.1; int b = 2; int c = a/b;

Page 32: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

32

c) int a = 3; int b = 2; double c = a/b;

10. Nhập hai số nguyên dương x và y từ bàn phím, hãy tính:

a) Thương của phép toán x chia cho y

b) Tích của phép toán x nhân y

c) Phần nguyên của x chia cho y

d) Phần dư của x chia cho y

11. Nhập từ bàn phím 3 số thực x, y, và z. Hãy tính

a) Trung bình cộng của ba số trên

b) Hiện ra màn hình số 1 nếu x là số lớn nhất trong ba số trên, ngược lại

hiện ra màn hình số 0.

c) Hiện ra màn hình số 1 nếu z là số nhỏ trong ba số trên, ngược lại hiện ra

màn hình số 0.

12. Tính giá trị (không dùng chương trình) của biểu thức so sánh dưới đây:

a) x = 1; y = 2; x++ < y

b) x = 2; y = 1; x-- < y++

c) x = 2; y = 2; x++ == y

Page 33: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

33

Chương 3. Các cấu trúc điều khiển

Như đã nói ở phần trên, chương trình máy tính mà chúng ta lập trình tạo ra thực

chất là một tập các lệnh. Các chương trình được viết bằng ngôn ngữ bậc cao

thường chứa một hàm chính (trong C++ là hàm main), nơi chương trình bắt đầu

thực hiện các lệnh.

Trong chương này chúng ta sẽ tìm hiểu thứ tự thực hiện các lệnh trong một

chương trình.

3.1. Luồng điều khiển

Luồng điều khiển của chương trình là thứ tự các lệnh (hành động) mà chương

trình thực hiện. Cho đến chương này, chúng ta mới gặp thứ tự đơn giản: thứ tự

tuần tự, nghĩa là các hành động được thực hiện đúng theo thứ tự mà chúng được

viết trong chương trình. Ví dụ, Hình 3.1 là sơ đồ minh họa một cấu trúc tuần tự

điển hình, trong đó hai hành động và các mũi tên biểu diễn thứ tự thực hiện các

hành động.

tính totalApples

in totalApples

Lệnh C++ tương ứng:

totalApples = numberOfBaskets * applePerBasket;

Lệnh C++ tương ứng:

cout << "Number of apples is " << totalApples;

Hình 3.1: Sơ đồ chuyển trạng thái của một đoạn lệnh có thứ tự thực thi tuần tự.

Trong chương này, ta sẽ làm quen với các luồng điều khiển phức tạp hơn. Hầu

hết các ngôn ngữ lập trình, trong đó có C++, cung cấp hai loại lệnh để kiểm soát

luồng điều khiển:

lệnh rẽ nhánh (branching) chọn một hành động từ danh sách gồm nhiều

hành động.

lệnh lặp (loop) thực hiện lặp đi lặp lại một hành động cho đến khi một

điều kiện dừng nào đó được thỏa mãn.

Page 34: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

34

Hai loại lệnh đó tạo thành các cấu trúc điều khiển (control structure) bên trong

chương trình.

3.2. Các cấu trúc rẽ nhánh

3.2.1. Lệnh if-else

Lệnh if-else (hay gọi tắt là lệnh if) cho phép rẽ nhánh bằng cách lựa chọn

thực hiện một trong hai hành động. Ví dụ, trong một chương trình xếp loại điểm

thi, nếu điểm của sinh viên nhỏ hơn 60, sinh viên đó được coi là trượt, nếu

không thì được coi là đỗ.

in "Failed" in "Passed"

[score < 60] [score >= 60]

Hình 3.2: Sơ đồ một lệnh if.

Thuật toán nhỏ này được thể hiện bằng sơ đồ trong Hình 3.2. Trong đó hình quả

trám biểu diễn điểm rẽ nhánh, nơi chương trình đưa ra quyết định xem nên rẽ

theo hướng nào. Từ điểm rẽ nhánh có các mũi tên đi ra, mỗi mũi tên kèm theo

một điều kiện. Luồng điều khiển sẽ đi theo mũi tên nào mà điều kiện của nó

được thỏa mãn.

Cũng có thể trình bày thuật toán bằng mã giả như sau:

If student’s score is less than 60

print “Failed”

else

print “Passed”

Thể hiện thuật toán trên bằng một lệnh if-else của C++, ta có đoạn mã:

Page 35: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

35

if (score < 60)

cout << "Failed";

else

cout << "Passed";

Khi chương trình chạy một lệnh if-else, đầu tiên nó kiểm tra biểu thức điều

kiện nằm trong cặp ngoặc đơn sau từ khóa if. Nếu biểu thức có giá trị bằng

true thì lệnh nằm sau từ khóa if sẽ được thực hiện. Ngược lại, lệnh nằm sau

else sẽ được thực hiện. Chú ý là biểu thức điều kiện phải được đặt trong một

cặp ngoặc đơn.

#include <iostream>

using namespace std;

int main(){ int score; cout << "Enter your score: "; cin >> score; //get the score from the keyboard

if (score < 60) cout << "Sorry. You’ve failed the course.\n"; else cout << "Congratulations! You’ve passed the course.\n"; return 0;}

Kết quả chạy chương trình

Enter your score: 20Sorry. You’ve failed the course.

Enter your score: 70Congratulations! You’ve passed the course.

Hình 3.3: Ví dụ về cấu trúc if-else.

Chương trình ví dụ trong Hình 3.3 yêu cầu người dùng nhập điểm rồi in ra các

thông báo khác nhau tùy theo điểm số đủ đỗ hoặc trượt. Để ý rằng các lệnh nằm

bên trong khối if-else cần được lùi đầu dòng một mức để chương trình trông

sáng sủa dễ đọc.

Page 36: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

36

Trong cấu trúc rẽ nhánh if-else, ta có thể bỏ phần else nếu không muốn

chương trình thực hiện hành động nào nếu điều kiện không thỏa mãn. Chẳng

hạn, nếu muốn thêm một lời khen đặc biệt cho điểm số xuất sắc từ 90 trở lên, ta

có thể thêm lệnh if sau vào trong chương trình tại Hình 3.3.

if (score >= 90)

cout << "Excellent!";

Ta có thể dùng các cấu trúc if-else lồng nhau để tạo ra điều kiện rẽ nhánh

phức tạp. Hình 3.3 là một ví dụ đơn giản với chỉ hai trường hợp và một lệnh if.

Xét một ví dụ phức tạp hơn: cho trước điểm số (lưu tại biến score kiểu int),

xác định xếp loại học lực A, B, C, D, F tùy theo điểm đó. Quy tắc xếp loại là:

nếu điểm từ 90 trở lên thì đạt loại A, điểm từ 80 tới dưới 90 đạt loại B, v.v.. Tại

đoạn mã xét các trường hợp của xếp loại điểm, ta có thể dùng cấu trúc if-else

lồng nhau như sau:

if (score >= 90)

grade = 'A';

else

if (score >= 80)

grade = 'B';

else

if (score >= 70)

grade = 'C';

else

if (score >= 60)

grade = 'D';

else

grade = 'F';

Để ý rằng mỗi khối lệnh if-else sau nằm trong phần else của khối lệnh if-

else liền trước. Nếu score không dưới 90, điều kiện của cả bốn lệnh if-else

đều cho kết quả true. Tuy nhiên, do điều kiện của lệnh if-else ngoài cùng

thỏa mãn, chỉ có lệnh gán đầu tiên grade = 'A' được thực thi, còn toàn bộ

phần else của lệnh if-else ngoài cùng, bao gồm ba lệnh if-else còn lại, bị

bỏ qua.

Với các khối lệnh if-else lồng nhau như ở trên, khi phải lùi đầu dòng quá xa

về bên phải, dòng còn lại quá ngắn khiến mỗi lệnh có thể phải trải trên vài dòng,

dẫn đến việc chương trình khó đọc. Do đó, đa số lập trình viên C++ thường

Page 37: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

37

dùng kiểu lùi đầu dòng như dưới đây. (Hai cách lùi đầu dòng này là như nhau

đối với trình biên dịch vì nó bỏ qua các kí tự trắng.)

if (score >= 90)

grade = 'A';

else if (score >= 80)

grade = 'B';

else if (score >= 70)

grade = 'C';

else if (score >= 60)

grade = 'D';

else

grade = 'F';

Hình 3.4 là chương trình hoàn chỉnh thực hiện nhiệm vụ yêu cầu người dùng

nhập điểm số sau đó tính và in ra xếp loại học lực tương ứng.

Page 38: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

38

#include <iostream>

using namespace std;

int main(){ int score; char grade;

cout << "Enter your score: "; cin >> score;

if (score >= 90) grade = 'A'; else if (score >= 80) grade = 'B'; else if (score >= 70) grade = 'C'; else if (score >= 60) grade = 'D'; else grade = 'F'; cout << "Grade = " << grade << endl;

return 0;}

Hình 3.4: Cấu trúc if-else lồng nhau.

Một điều cần đặc biệt lưu ý là nếu muốn thực hiện nhiều hơn một lệnh trong

mỗi trường hợp của lệnh if-else, ta cần dùng cặp ngoặc {} bọc tập lệnh đó

thành một khối chương trình. Ví dụ, phiên bản phức tạp hơn của lệnh if trong

Hình 3.3:

Page 39: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

39

if (score < 60) {

cout << "Failed" << endl;

cout << "You have to take this course again" << endl;

}

else {

cout << "Congratulations!!!" << endl;

cout << "You passed this course. ";

}

Để tránh lỗi quên cặp ngoặc, có một lời khuyên là tập thói quen dùng cặp ngoặc

ngay cả khi khối lệnh chỉ bao gồm đúng một lệnh. Chẳng hạn, xét khối lệnh

lồng nhau dưới đây:

if (mathGrade == 'A')

if (artGrade == 'A')

cout << "You have got two A's!" << endl;

else

cout << "You don't have grade A for math." << endl;

Thông báo trong phần else đáng ra cần được thực thi khi xếp hạng của môn

Toán (mathGrade) không đạt A. Tuy nhiên, trong thực tế, trình biên dịch lại

hiểu rằng phần else đó cùng cặp với lệnh if thứ hai – lệnh if gần nhất, nghĩa

là:

if (mathGrade == 'A')

if (artGrade == 'A')

cout << "You have got two A's!" << endl;

else

cout << "You don't have grade A for math." << endl;

Các lỗi logic dạng này thường khó phát hiện. Đối với C++, việc lùi đầu dòng

chỉ nhằm mục đích để lập trình viên dễ đọc chương trình chứ không có ý nghĩa

với trình biên dịch. Cách giải quyết là sử dụng cặp ngoặc {} để chỉ rõ cho trình

biên dịch rằng lệnh if thứ hai nằm trong lệnh if ngoài cùng và rằng phần else

cùng cặp với lệnh if ngoài cùng đó:

Page 40: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

40

if (mathGrade == 'A') {

if (artGrade == 'A')

cout << "You have got two A's!" << endl;

}

else

cout << "You don't have grade A for math." << endl;

3.2.2. Lệnh switch

Khi chúng ta muốn viết một cấu trúc rẽ nhánh có nhiều lựa chọn, ta có thể sử

dụng nhiều lệnh if-else lồng nhau. Tuy nhiên, trong trường hợp việc lựa chọn

rẽ nhánh phụ thuộc vào giá trị (kiểu số nguyên hoặc kí tự) của một biến hay

biểu thức, ta có thể sử dụng cấu trúc switch để chương trình dễ hiểu hơn. Lệnh

switch điển hình có dạng như sau:

switch (biểu_thức) {

case hằng_1:

tập_lệnh_1; break;

case hằng_2:

tập_lệnh_2; break;

...

default:

tập_lệnh_mặc_định;

}

Khi lệnh switch được chạy, biểu_thức được tính giá trị và so sánh với hằng_1.

Nếu bằng nhau, chuỗi lệnh kể từ tập_lệnh_1 được thực thi cho đến khi gặp lệnh

break đầu tiên, đến đây chương trình sẽ nhảy tới điểm kết thúc cấu trúc switch.

Nếu biểu_thức không có giá trị bằng hằng_1, nó sẽ được so sánh với hằng_2,

nếu bằng nhau, chương trình sẽ thực thi chuỗi lệnh kể từ tập_lệnh_2 tới khi gặp

lệnh break đầu tiên thì nhảy tới cuối cấu trúc switch. Quy trình cứ tiếp diễn

như vậy. Cuối cùng, nếu biểu_thức có giá trị khác với tất cả các giá trị đã được

liệt kê (hằng_1, hằng_2, ...), chương trình sẽ thực thi tập_lệnh_mặc_định nằm

sau nhãn default: nếu như có nhãn này (không bắt buộc).

Ví dụ, lệnh sau so sánh giá trị của biến grade với các hằng kí tự 'A', 'B', 'C' và

in ra các thông báo khác nhau cho từng trường hợp.

Page 41: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

41

switch (grade) {

case 'A':

cout << "Grade = A"; break;

case 'B':

cout << "Grade = B"; break;

case 'C':

cout << "Grade = C"; break;

default:

cout << "Grade is not A, B, or C.";

}

Nó tương đương với khối lệnh if-else lồng nhau sau:

if (grade == 'A')

cout << "Grade = A";

else if (grade == 'B')

cout << "Grade = B";

else if (grade == 'C')

cout << "Grade = C";

else

cout << "Grade is not A, B, or C.";

}

Lưu ý, các nhãn case trong cấu trúc switch phải là hằng chứ không thể là biến

hay biểu thức. Nếu cần so sánh với biến hay biểu thức, ta nên dùng khối lệnh

if-else lồng nhau.

Vấn đề đặc biệt của cấu trúc switch là các lệnh break. Nếu ta không tự gắn một

lệnh break vào cuối chuỗi lệnh cần thực hiện cho mỗi trường hợp, chương trình

sẽ chạy tiếp chuỗi lệnh của trường hợp sau chứ không tự động nhảy tới cuối cấu

trúc switch. Ví dụ, đoạn chương trình sau sẽ chạy lệnh in thứ nhất nếu grade

nhận một trong ba giá trị 'A', 'B', 'C' và chạy lệnh in thứ hai trong trường hợp

còn lại:

Page 42: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

42

switch (grade) {

case 'A':

case 'B':

case 'C':

cout << "Grade is A, B or C."; break;

default:

cout << "Grade is not A, B or C.";

}

Chương trình trong Hình 3.5 là một ví dụ hoàn chỉnh sử dụng cấu trúc switch

để in ra các thông báo khác nhau tùy theo xếp loại học lực (grade) mà người

dùng nhập từ bàn phím. Trong đó, case 'A' kết thúc với break sau chỉ một

lệnh, còn case 'B' chạy tiếp qua case 'C', 'D' rồi mới gặp break và thoát

khỏi lệnh switch. Nhãn default được dùng để xử lý trường hợp biến grade giữ

giá trị không hợp lệ đối với xếp loại học lực. Trong nhiều chương trình, phần

default thường được dùng để xử lý các trường hợp không mong đợi, chẳng

hạn như để bắt lỗi các kí hiệu học lực không hợp lệ mà người dùng có thể nhập

sai trong Hình 3.5.

Page 43: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

43

#include <iostream>using namespace std;

int main(){ char grade;

cout << "Enter your grade: "; cin >> grade;

switch (grade) { case 'A': cout << "Excellent!\n"; break; case 'B': cout << "Great!\n"; case 'C': case 'D': cout << "Well done!\n"; break; case 'F': cout << "Sorry, you failed.\n"; break; default: cout << "Error! Invalid grade."; }

return 0;

}

Kết quả chạy chương trình

Enter your grade: AExcellent!

Enter your grade: BGreat!Well done!

Enter your grade: DWell done!

Enter your grade: FSorry, you failed.

Hình 3.5: Ví dụ sử dụng cấu trúc switch.

Page 44: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

44

3.3. Các cấu trúc lặp

Các chương trình thường cần phải lặp đi lặp lại một hoạt động nào đó. Ví dụ,

một chương trình xếp loại học lực sẽ chứa các lệnh rẽ nhánh để gán xếp loại A,

B, C… cho một sinh viên tùy theo điểm số của sinh viên này. Để xếp loại cho

cả một lớp, chương trình sẽ phải lặp lại thao tác đó cho từng sinh viên trong lớp.

Phần chương trình lặp đi lặp lại một lệnh hoặc một khối lệnh được gọi là một

vòng lặp. Lệnh hoặc khối lệnh được lặp đi lặp lại được gọi là thân của vòng lặp.

Cấu trúc lặp cho phép lập trình viên chỉ thị cho chương trình lặp đi lặp lại một

hoạt động trong khi một điều kiện nào đó vẫn được thỏa mãn.

Khi thiết kế một vòng lặp, ta cần xác định thân vòng lặp thực hiện hành động gì.

Ngoài ra, ta còn cần một cơ chế để quyết định khi nào vòng lặp sẽ kết thúc.

Mục này sẽ giới thiệu về các lệnh lặp mà C++ cung cấp.

3.3.1. Vòng while

Vòng while lặp đi lặp lại chuỗi hành động, gọi là thân vòng lặp, nếu như điều

kiện lặp vẫn còn được thỏa mãn. Cú pháp của vòng lặp while như sau:

while (điều_kiện_lặp)

thân_vòng_lặp

Cấu trúc này bắt đầu bằng từ khóa while, tiếp theo là điều kiện lặp đặt trong

một cặp ngoặc đơn, cuối cùng là thân vòng lặp. Thân vòng lặp hay chứa nhiều

hơn một lệnh và khi đó thì phải được gói trong một cặp ngoặc {}.

Khi thực thi một cấu trúc while, đầu tiên chương trình kiểm tra giá trị của biểu

thức điều kiện, nếu biểu thức cho giá trị false (thực chất là bằng 0) thì nhảy

đến điểm kết thúc lệnh while, còn nếu điều kiện lặp có giá trị true (thực chất là

một giá trị nào đó khác 0) thì tiến hành thực hiện tập lệnh trong thân vòng lặp

rồi quay trở lại kiểm tra điều kiện lặp, nếu không thỏa mãn thì kết thúc, nếu thỏa

mãn thì lại thực thi thân vòng lặp rồi quay lại... Tập lệnh ở thân vòng lặp có thể

làm thay đổi giá trị của biểu thức điều kiện từ true sang false (thực chất là từ

khác 0 sang bằng 0) để dừng vòng lặp.

Ví dụ, xét một chương trình có nhiệm vụ đếm từ 1 đến một ngưỡng number cho

trước. Đoạn mã đếm từ 1 đến number có thể được viết như sau:

Page 45: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

45

count = 1;

while (count <= number)

{

cout << count << ", ";

count++;

}

Giả sử biến number có giá trị bằng 2, đoạn mã trên hoạt động như sau: Đầu tiên,

biến count được khởi tạo bằng 1. Vòng while bắt đầu bằng việc kiểm tra điều

kiện (count <= number), nghĩa là 1 ≤ 2, điều kiện thỏa mãn. Thân vòng lặp

được thực thi lần thứ nhất: giá trị 1 của count được in ra màn hình kèm theo

dấu phảy, sau đó count được tăng lên 2. Vòng lặp quay về điểm xuất phát: kiểm

tra điều kiện lặp, giờ là 2 ≤ 2, vẫn thỏa mãn. Thân vòng lặp được chạy lần thứ

hai (in giá trị 2 của count và tăng count lên 3) trước khi quay lại điểm xuất

phát của vòng lặp. Tại lần kiểm tra điều kiện lặp này, biểu thức 3 ≤ 2 cho giá trị

false, vòng lặp kết thúc do điều kiện lặp không còn được thỏa mãn, chương

trình chạy tiếp ở lệnh nằm sau cấu trúc while đang xét.

Cấu trúc while trong đoạn mã trên có thể được biểu diễn bằng sơ đồ trong Hình

3.6. Trong sơ đồ, mũi tên sau chuỗi hành động in biến count và tăng biến count

quay ngược trở lại điểm kiểm tra điều kiện rẽ nhánh, chuỗi hành động sẽ lặp lại

nếu điều kiện count <= number vẫn tiếp tục được thỏa mãn. Còn nếu count >

number, vòng while đi đến điểm kết thúc, trao điều khiển cho lệnh tiếp theo

trong chuỗi lệnh của chương trình.

in count tăng count thêm 1[count <= number]

[count > number]

Hình 3.6: Sơ đồ một vòng lặp while.

Chương trình hoàn chỉnh trong Hình 3.7 minh họa cách sử dụng vòng lặp while

để in ra các số nguyên (biến count) từ 1 cho đến một ngưỡng giá trị do người

dùng nhập vào từ bàn phím (lưu tại biến number). Kèm theo là kết quả của các

lần chạy khác nhau với các giá trị khác nhau của number. Đặc biệt, khi người

Page 46: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

46

dùng nhập giá trị 0 cho number, thân vòng while không chạy một lần nào, thể

hiện ở việc không một số nào được in ra màn hình. Lí do là vì nếu number bằng

0 thì biểu thức count <= number ngay từ đầu vòng while đã có giá trị false.

#include <iostream>using namespace std; int main(){ int count, number;

cout << "Enter a number: "; cin >> number;

count = 1; while (count <= number) { cout << count << ", "; count++; } cout << "BOOOOOM!" << endl;

return 0; }

Kết quả chạy chương trình

Enter a number: 21, 2, BOOOOOM!

Enter a number: 11, BOOOOOM!

Enter a number: 0BOOOOOM!

Hình 3.7: Ví dụ về vòng lặp while.

Vòng lặp vô tận

Một trong những lỗi chương trình thường gặp là một vòng lặp không thể kết

thúc, thân vòng lặp đó được lặp đi lặp lại mà không bao giờ ngừng. Một vòng

lặp như vậy được gọi là vòng lặp vô tận. Thông thường, một vài lệnh trong

thân một vòng while hay do-while sẽ làm thay đổi giá trị của một vài biến để

biểu thức điều kiện cho giá trị false (hoặc 0), chẳng hạn lệnh tăng biến đếm

Page 47: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

47

count trong Hình 3.7. Nếu (các) biến đó không được thay đổi giá trị một cách

thích hợp hoặc khi điều kiện lặp được viết không chính xác (có thể do thuật toán

sai hay nhầm lẫn khi viết mã chương trình), ta sẽ nhận được một vòng lặp vô

tận do điều kiện lặp không bao giờ nhận giá trị false.

Hình 3.8 minh họa một vòng lặp vô tận khi biến count tăng dần đến vô hạn

nhưng luôn nhận giá trị lẻ và không bao giờ có thể có giá trị bằng 12 để kết thúc

vòng lặp while. Đây có thể gọi là lỗi logic. Nếu đoạn chương trình có nhiệm

vụ tính tổng các số 1, 3, 5, 7, 9, 11 thì điều kiện lặp nên được sửa thành (count

< 12) để chương trình có thể chạy đúng.

#include <iostream>using namespace std;

int main(int argc, char *argv[]){ int sum; int count = 1;

while (count != 12) { sum = sum + count; count = count + 2; } return 0;

}

Hình 3.8: Ví dụ về vòng lặp vô tận.

3.3.2. Vòng do-while

Vòng do-while rất giống với vòng while, khác biệt chính là ở chỗ thân vòng

lặp sẽ được thực hiện trước, sau đó mới kiểm tra điều kiện lặp, nếu đúng thì

quay lại chạy thân vòng lặp, nếu sai thì dừng vòng lặp. Khác biệt đó có nghĩa

rằng thân của vòng do-while luôn được chạy ít nhất một lần, trong khi thân

vòng while có thể không được chạy lần nào.

Page 48: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

48

Công thức của vòng do-while tổng quát là:

do

thân_vòng_lặp

while (điều_kiện_lặp);

Tương tự như ở vòng while, thân_vòng_lặp của vòng do-while có thể chỉ gồm

một lệnh hoặc thường gặp hơn là một chuỗi lệnh được bọc trong một cặp ngoặc

{}. Lưu ý dấu chấm phảy đặt cuối toàn bộ khối do-while.

Công thức tổng quát của vòng do-while ở trên tương đương với công thức sau

nếu dùng vòng while:

thân_vòng_lặp

while (điều_kiện_lặp)

thân_vòng_lặp

Để minh họa hoạt động của hai cấu trúc lặp while và do-while, ta so sánh hai

đoạn mã dưới đây:

count = 1;

while (count <= number) {

cout << count << ", ";

count++;

}

count = 1;

do {

cout << count << ", ";

count++;

} while (count <= number);

Hai đoạn mã chỉ khác nhau ở chỗ một bên trái dùng vòng while, bên phải dùng

vòng do-while, còn lại, các phần thân vòng lặp, điều kiện, khởi tạo đều giống

hệt nhau. Đoạn bên trái được lấy từ ví dụ trong mục trước, nó in ra các số từ 1

đến number. Đoạn mã dùng vòng do-while bên phải cũng thực hiện công việc

giống hệt đoạn bên trái, ngoại trừ một điểm: khi number nhỏ hơn 1 thì nó vẫn

đếm 1 trước khi dừng vòng lặp – thân vòng lặp chạy 01 lần trước khi kiểm tra

điều kiện.

Để minh họa rõ hơn, ta xem xét Hình 3.9 là bản sửa đổi của ví dụ trong Hình

3.7 bằng cách thay cấu trúc while bằng cấu trúc do-while và giữ nguyên các

phần còn lại. Kết quả chạy chương trình cho thấy hoạt động của chương trình

trong Hình 3.9 không hoàn toàn như mong muốn. Cụ thể, khi number được nhập

giá trị 0, chương trình vẫn đếm một lần trong khi đáng ra không nên đếm lần

nào như chương trình nguyên gốc trong Hình 3.7. Đó là một minh họa về sự

khác biệt giữa hoạt động của hai cấu trúc while và do-while. Trong bài toán cụ

thể này, cấu trúc do-while không phải là lựa chọn tốt so với cấu trúc while.

Page 49: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

49

#include <iostream>using namespace std; int main(){ int count, number;

cout << "Enter a number: "; cin >> number;

count = 1; do { cout << count << ", "; count++; } while (count <= number);

cout << "BOOOOOM!" << endl;

return 0; }

Kết quả chạy chương trình

Enter a number: 21, 2, BOOOOOM!

Enter a number: 11, BOOOOOM!

Enter a number: 01, BOOOOOM!

Hình 3.9: Ví dụ về vòng lặp do-while.

Tận dụng đặc điểm chạy một lần trước khi kiểm tra điều kiện, chương trình

trong Hình 3.10 làm nhiệm vụ nhập từ bàn phím một chuỗi số kết thúc bằng số

0 và tính tổng của chúng. Công việc lặp đi lặp lại là nhập vào một số (input) rồi

cộng dồn giá trị của số đó vào tổng (sum). Điều kiện lặp là số vừa nhập phải

khác 0. Rõ ràng, không cần và không nên kiểm tra điều kiện lặp trước khi lần

đầu thực hiện thân vòng lặp. Trong trường hợp này, cấu trúc do-while là lựa

chọn tốt hơn so với cấu trúc while.

Chương trình trong Hình 3.10 còn là một ví dụ về một vòng lặp không dùng con

đếm để điều khiển vòng lặp mà dùng một biến canh hay cờ trạng thái để đánh

Page 50: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

50

dấu điểm kết thúc. Ở đây, 0 là giá trị canh đánh dấu phần tử dữ liệu cuối cùng

mà người dùng nhập vào chương trình.

#include <iostream>using namespace std;

int main (){ int total = 0; int input;

do { cout << "Enter a number (0 to stop) "; cin >> input; total = total + input; } while (input != 0);

cout << "The sum of the numbers you entered is " << total; return 0;}

Kết quả chạy chương trình

Enter a number (0 to stop) 2Enter a number (0 to stop) 4Enter a number (0 to stop) 1Enter a number (0 to stop) 0The sum of the numbers you entered is 7

Hình 3.10: Vòng lặp do-while dùng biến canh.

3.3.3. Vòng for

Vòng for là cấu trúc hỗ trợ việc viết các vòng lặp mà số lần lặp được kiểm soát

bằng biến đếm. Chẳng hạn, đoạn mã giả sau đây mô tả thuật toán in ra các số từ

1 đến number:

Page 51: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

51

Làm nhiệm vụ sau đây đối với mỗi giá trị của count từ 1 đến number:

In count ra màn hình

Đoạn mã giả đó có thể được viết bằng vòng for của C++ như sau:

for (count = 1; count <= number; count++)

cout << count << ", ";

Với number có giá trị bằng 3, đoạn trình trên cho kết quả in ra màn hình là:

1, 2, 3,

Cấu trúc tổng quát của vòng lặp for là:

for ( khởi_tạo; điều_kiện_lặp; cập_nhật)

thân_vòng_lặp

Trong đó, biểu thức khởi_tạo thường khởi tạo con đếm điều khiển vòng lặp,

điều_kiện_lặp xác định xem thân vòng lặp có nên chạy tiếp hay không (điều

kiện này thường chứa ngưỡng cuối cùng của con đếm), và biểu thức cập_nhật

làm tăng hay giảm con đếm. Cũng tương tự như ở các cấu trúc if, while..., nếu

thân_vòng_lặp có nhiều hơn một lệnh thì cần phải bọc nó trong một cặp ngoặc

{}. Lưu ý rằng cặp ngoặc đơn bao quanh bộ ba khởi_tạo, điều_kiện_lặp,

cập_nhật, cũng như hai dấu chấm phảy ngăn cách ba thành phần đó, là các

thành bắt buộc của cú pháp cấu trúc for. Ba thành phần đó cũng có thể là biểu

thức rỗng nếu cần thiết, nhưng kể cả khi đó vẫn phải có đủ hai dấu chấm phảy.

Trong hầu hết các trường hợp, cấu trúc for tổng quát ở trên tương đương với

cấu trúc lặp while sau (ngoại lệ được nói đến trong Mục 0):

khởi_tạo;

while (điều_kiện_lặp) {

thân_vòng_lặp

cập_nhật; }

Ta có thể khai báo biến ngay trong phần khởi_tạo của vòng for, chẳng hạn đối

với biến con đếm. Nhưng các biến được khai báo tại đó chỉ có hiệu lực ở bên

trong cấu trúc lặp (chi tiết về phạm vi của biến sẽ được nói đến trong Mục 4.4).

Ví dụ:

Page 52: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

52

for (int count = 1; count <= number; count++)

cout << count << ", ";

Hình 3.11 minh họa cách sử dụng vòng lặp for để tính điểm trung bình từ điểm

của 10 môn học (số môn học lưu trong biến subjects). Người dùng sẽ được

yêu cầu nhập từ bàn phím điểm số của 10 môn học trong khi chương trình cộng

dồn tổng của 10 điểm số này. Công việc mà chương trình cần lặp đi lặp lại 10

lần là: nhập điểm của một môn học, cộng dồn điểm đó vào tổng điểm. Đầu tiên

vòng for sẽ tiến hành bước khởi tạo với mục đích chính là khởi tạo biến đếm.

Việc khởi tạo chỉ được tiến hành duy nhất một lần. Trong ví dụ này, biến count

được khai báo ngay tại vòng for và khởi tạo giá trị bằng 0. Tiếp theo vòng for

sẽ tiến hành kiểm tra điều kiện lặp count < subjects. Nếu điều kiện sai, vòng

lặp for sẽ kết thúc. Nếu điều kiện đúng, thân vòng lặp for sẽ được thực hiện

(nhập một giá trị kiểu float rồi cộng dồn vào biến sum). Sau đó là bước cập

nhật với nhiệm vụ tăng biến đếm thêm 1. Kết quả là vòng lặp sẽ chạy 10 lần với

các giá trị count bằng 0, 1, .., 9 (khi count nhận giá trị 10 thì điều kiện lặp

không còn đúng và vòng lặp kết thúc).

#include <iostream>using namespace std; int main(){ float sum = 0; int subjects = 10;

cout << "Enter the marks for " << subjects << " subjects: "; for (int count = 0; count < subjects; count++) { float mark; cin >> mark; sum += mark; } cout << "Average mark = " << sum/subjects;

return 0; }

Hình 3.11: Ví dụ về vòng lặp for.

Xét một bài toán khác: Tính lãi gửi tiền tiết kiệm. Một người gửi một sổ tiết

kiệm kì hạn 12 tháng với số tiền ban đầu là 1.000.000 VNĐ, lãi suất 12% một

năm. Giả sử tiền lãi hàng năm không được rút ra mà gộp chung vào vốn. Số tiền

Page 53: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

53

nằm trong sổ tiết kiệm sau mỗi năm (kì hạn 12 tháng) được tính theo công thức

sau:

amount = principal * (1 + interest_rate)year

trong đó amount là số tiền trong sổ tiết kiệm, principal là vốn ban đầu,

interest_rate là lãi xuất hàng năm, và year là số năm đã gửi. Hãy tính và in ra số

tiền trong sổ tiết kiệm vào thời điểm cuối các kì 12 tháng hàng năm trong vòng

5 năm theo dạng

Year Amount in deposit

1 xxxx

2 xxxx

...

Thuật toán cho bài toán đó như sau:

Khởi tạo các biến amount, principal, interestRate

In đề mục hai cột Year và Amount in deposit ra màn hình

Với year chạy từ 1 đến 5, thực hiện các công việc sau:

tính amount sau year năm theo công thức

in amount theo cột

Chương trình hoàn chỉnh cho trong Hình 3.12. Trong đó có một số điểm cần

chú ý.

Thứ nhất, vấn đề định dạng các giá trị số khi in ra màn hình. Định dạng mặc

định khi in ra màn hình là định dạng khoa học, chẳng hạn số 1120000 ở hệ thập

phân sẽ có dạng 1.12e+006. Ta sẽ nhìn thấy kết quả này nếu xóa bỏ dòng sau

khỏi chương trình:

cout << fixed << setprecision( 2 ); // set number display

format

Dòng trên không hiển thị gì ra màn hình mà chỉ đặt cấu hình cho các lần in tiếp

theo. Cụ thể, fixed giúp quy định rằng các giá trị thực in ra theo dạng có dấu

chấm thập phân (hệ đo lường Anh-Mỹ, tương đương với dấu phảy thập phân

của hệ mét), còn setprecision( 2 ) quy định rằng các giá trị thực được in ra

với độ chính xác 2 chữ số sau dấu chấm. Đó là hai trong nhiều phép định dạng

luồng dữ liệu (stream manipulator). Các phép định dạng này nằm trong thư viện

iomanip của thư viện chuẩn. Do đó, để sử dụng chúng, ta phải có định hướng

tiền xử lý #include <iomanip> ở đầu tệp mã nguồn (xem phần đầu của chương

trình trong Hình 3.12).

Page 54: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

54

Thứ hai là sử dụng hàm thư viện toán học để tính lũy thừa. C++ không có phép

tính lũy thừa. Thay vào đó, thư viện <cmath> trong thư viện chuẩn C++ cung

cấp hàm pow. Hàm pow( x, y ) cho kết quả là giá trị của phép tính xy. Để

dùng hàm này, ta cũng cần có định hướng tiền xử lý #include <cmath> ở đầu

tệp mã nguồn.

#include <iostream>#include <iomanip>#include <cmath> // standard C++ math library

using namespace std;

int main(){ double amount; // amount at end of each year double principal = 1000000; // initial amount double interestRate = .12;

cout << "Year\tAmount on deposit" << endl; // display headers cout << fixed << setprecision( 2 ); // set number display format

// calculate amount on deposit for each of five years for ( int year = 1; year <= 5; year++) { amount = principal * pow( 1.0 + interestRate, year ); cout << year << "\t" << amount << endl; }

return 0; }

Kết quả chạy chương trình

Year Amount on deposit1 1120000.002 1254400.003 1404928.004 1573519.365 1762341.68

Hình 3.12: Tính tiền tiết kiệm hàng năm sử dụng vòng for.

Page 55: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

55

3.4. Các lệnh break và continue

Như đã giới thiệu ở các mục trước, các vòng lặp while, do-while, và for đều

kết thúc khi kiểm tra biểu thức điều kiện được giá trị false và chạy tiếp thân

vòng lặp trong trường hợp còn lại. Các lệnh break và continue là các lệnh

nhảy cho phép thay đổi luồng điều khiển đó.

Lệnh break khi được thực thi bên trong một cấu trúc lặp hay một cấu trúc

switch có tác dụng lập tức chấm dứt cấu trúc đó, chương trình sẽ chạy tiếp ở

lệnh nằm tiếp sau cấu trúc đó. Lệnh break thường được dùng để kết thúc sớm

vòng lặp (thay vì đợi đến lượt kiểm tra điều kiện lặp) hoặc để bỏ qua phần còn

lại của cấu trúc switch (như tại các ví dụ trong Mục 3.2.2).

Về ví dụ sử dụng lệnh break trong vòng lặp. Chẳng hạn, nếu ta sửa ví dụ trong

Hình 3.11 để vòng for ngừng lại khi người dùng nhập điểm số có giá trị âm, ta

có chương trình trong Hình 3.13. Với cài đặt này, khi người dùng nhập một

điểm số có giá trị âm, điều kiện (mark < 0) sẽ cho kết quả true, chương trình

thoát khỏi vòng for và chạy tiếp từ lệnh if nằm sau đó. Trong trường hợp đó,

biến count chưa kịp tăng đến ngưỡng subjects (điều kiện lặp của vòng for

chưa kịp bị phá vỡ). Do đó, biểu thức (count >= subjects) trong lệnh if sau

đó có nghĩa "vòng for có chạy đủ subjects lần hay không?" hoặc "vòng for

có bị ngắt giữa chừng bởi lệnh break hay không?", hay là "dữ liệu nhập vào có

thành công hay không?".

Page 56: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

56

#include <iostream>

using namespace std; int main(){ float sum = 0; int count, subjects = 10;

cout << "Enter the marks for " << subjects << " subjects: "; for (count = 0; count < subjects; count++) { float mark; cin >> mark; if (mark < 0) break; sum += mark; } if (count >= subjects) // successful cout << "Average mark = " << sum/subjects; else cout << "Error: Invalid mark!";

return 0; }

Hình 3.13: Ví dụ về lệnh break.

Lệnh continue nằm trong một vòng lặp có tác dụng kết thúc lần lặp hiện hành

của vòng lặp đó. Hình 3.14 là một bản sửa đổi khác của chương trình trong

Hình 3.11. Trong phiên bản này, chương trình không ghi nhận điểm số có giá trị

âm, cũng không kết thúc chương trình sau khi báo lỗi như bản trong Hình 3.13,

mà yêu cầu nhập lại cho đến khi nào thành công. Khi gặp điểm số âm được

nhập vào (biến mark), lệnh continue được thực thi có tác dụng bỏ qua đoạn

lệnh ghi nhận điểm ở nửa sau của thân vòng while (đoạn cộng dồn vào tổng

sum và tăng biến đếm count). Lần lặp được thực hiện sau đó sẽ yêu cầu nhập lại

điểm cho môn học đang nhập dở (xem kết quả chạy chương trình trong Hình

3.14).

Page 57: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

57

#include <iostream>

using namespace std; int main(){ float sum = 0; int count = 0, subjects = 3;

cout << "Enter the marks of " << subjects << " subjects.\n"; while (count < subjects) { float mark; cout << "#" << count + 1 << ": "; cin >> mark; if (mark < 0) { cout << mark << " ignored\n"; continue; } sum += mark; count++; } cout << "Average mark = " << sum/count;

return 0; }

Kết quả chạy chương trình

Enter the marks of 3 subjects.#1: 8.0#2: 7.2#3: -5-5 ignored#3: 10.0Average mark = 8.4

Hình 3.14: Ví dụ về lệnh continue.

Một điểm cần lưu ý là các lệnh break hay continue chỉ có tác dụng đối với

vòng lặp trong cùng chứa nó. Chẳng hạn, nếu có hai vòng lặp lồng nhau và lệnh

break nằm trong vòng lặp bên trong, thì khi được thực thi, lệnh break đó chỉ có

tác dụng kết thúc vòng lặp bên trong.

Một vòng lặp không có break hay continue có cấu trúc đơn giản và dễ hiểu vì

thời điểm kiểm tra điều kiện lặp là điểm duy nhất quyết định lặp hay dừng. Với

Page 58: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

58

một lệnh break kết thúc sớm vòng lặp hoặc một lệnh continue kết thúc sớm

lần lặp hiện hành, cấu trúc lặp sẽ khó hiểu. Vậy nên người ta khuyên nên tránh

hai lệnh này nếu có thể.

3.5. Biểu thức điều kiện trong các cấu trúc điều khiển

Hầu hết các cấu trúc điều khiển mà ta nói đến trong chương này đều dùng đến

một thành phần quan trọng: biểu thức điều kiện. Trong các ví dụ trước, ta mới

chỉ dùng đến các điều kiện đơn giản, chẳng hạn count <= number hay grade

== 'A', với duy nhất một phép so sánh. Khi cần viết những điều kiện phức tạp

hơn, cần đến nhiều điều kiện nhỏ, ta có thể kết hợp chúng bằng các phép toán

logic && (AND – và), || (OR – hoặc) và ! (NOT – phủ định). Ví dụ:

Khi kiểm tra điều kiện 80 ≤ score < 90, bất đẳng thức toán học này cần được

tách thành hai điều kiện đơn. Bất đẳng đúng khi cả hai điều kiện đơn đều thỏa

mãn. Đó là khi ta cần dùng phép toán logic && (AND).

if (score >= 80 && score < 90)

grade = 'B';

Khi một trong hai điều kiện xảy ra, hoặc tiền đã hết hoặc túi đã đầy, thì không

thể mua thêm hàng. Trường hợp này, ta cần dùng phép toán logic || (OR).

if (moneyLeft <= 0 || bagIsFull)

cout << "Can't buy anything more!";

Tiếp tục lặp trong khi dữ liệu vào chưa có giá trị bằng giá trị canh – đánh dấu

điểm cuối của chuỗi dữ liệu:

while ( !(input == 0))

...

Ở đây, ta có thể dùng phép phủ định. Hoặc nhiều người chọn cách đơn giản hơn

là dùng phép so sánh khác (!=):

while (input != 0)

...

Một điều cần đặc biệt lưu ý khi dùng toán tử so sánh bằng (==): tránh nhầm lẫn

với phép gán (=). Nhầm lẫn dạng này sẽ dẫn đến những lỗi logic rất khó phát

hiện. Chẳng hạn câu lệnh sẽ in ra lời khen "Excellent!" nếu xếp loại học lực

đạt loại A:

Page 59: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

59

if (grade == 'A')

cout << "Excellent!";

Nếu ta viết nhầm thành:

if (grade = 'A')

cout << "Excellent!";

thì khi kiểm tra điều kiện, câu lệnh này sẽ sửa giá trị của biến grade thành 'A'

thay vì so sánh. Biểu thức gán grade = 'A' có giá trị bằng giá trị được gán

('A'), giá trị này khác 0 nên được hiểu là true. Khi biểu thức điều kiện của lệnh

if này luôn có giá trị true, đoạn lệnh sẽ hiển thị lời khen cho mọi loại học lực.

Một gợi ý cách tránh lỗi này là nên tập thói quen viết các biểu thức so sánh theo

kiểu ('A' == grade) thay vì (grade == 'A') theo thói quen thông thường.

Với vị trí giá trị hằng ở bên trái, biến ở bên phải, thì khi ta chẳng may viết nhầm

== thành = thì trình biên dịch sẽ phát hiện lỗi biên dịch "sai cú pháp của phép

gán 'A' = grade" thay vì im lặng do không thể phát hiện lỗi logic.

Page 60: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

60

Bài tập

1. Nhập vào từ bàn phím một số nguyên nằm trong khoảng từ 0 đến 10 là

điểm của một sinh viên. Hãy sử dụng cấu trúc switch để phân loại sinh

viên đó theo quy tắc dưới đây. Hiện kết quả phân loại ra màn hình:

Điểm < 5: kém

5 ≤ Điểm ≤ 7: trung bình

8 ≤ Điểm ≤ 9: giỏi

Điểm = 10: xuất sắc

2. Nhập vào từ bàn phím hai số nguyên a và b. Nếu

a > b: hiện ra màn hình dòng chữ “a is greater than b”.

a < b: hiện ra màn hình dòng chữ “a is smaler than b”.

a = b: hiện ra màn hình dòng chữ “a is equal to b”.

3. Nhập vào từ bàn phím ba số a, b, c. Hãy kiểm tra xem ba số đó có thỏa

mãn là độ dài các cạnh của một tam giác hay không? Nếu có, hiện ra

màn hình thông báo về diện thích của tam giác đó. Nếu không, thông báo

ra màn hình ba số đó không phải là 3 cạnh của một tam giác.

4. Nhập từ bàn phím 3 số a, b, c. Hãy tìm và hiện kết quả ra màn hình.

a. Giá trị lớn nhất của ba số trên

b. Giá trị nhỏ nhất của ba số trên

5. Nhập từ bàn phím ba số nguyên a, b, c. Tính tổng của số nhỏ nhất và số

lớn nhất trong ba số trên. Hiện kết quả tính được ra màn hình.

6. Nhập vào từ bàn phím danh sách các số nguyên thể hiện cho điểm của

một lớp học. Quá trình nhập dừng lại khi số nhập vào là một số âm. Viết

chương trình giải quyết các bài toán sau (hiện kết quả tìm được ra màn

hình)

a. Tính số lượng sinh viên trong lớp

b. Tính điểm trung bình của lớp học vừa nhập.

c. Tìm sinh viên có điểm cao nhất

Page 61: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

61

d. Tìm sinh viên có điểm thấp nhất.

7. Nhập vào từ bàn phím một số nguyên dương, sử dụng cấu trúc vòng lặp

while hoặc do-while để tính tổng tất cả các số nguyên dương lẻ nhỏ hơn

số nhập từ bàn phím. Hiện kết quả tìm được ra màn hình.

8. Nhập từ bàn phím một số nguyên dương. Hãy kiểm tra xem số nguyên

dương đó có phải là số nguyên tố hay không, nếu không thì tìm tất cả các

ước số của số nguyên dương đó. Hiện kết quả tìm được ra màn hình.

9. Sử dụng cấu trúc vòng lặp for để tính tổng tất cả các số nguyên dương

chia hết cho 7 và nhỏ hơn một số n cho trước từ bàn phím. Hiện kết quả

ra màn hình.

10. Nhập vào từ bàn phím vào 4 số tự nhiên. Hãy kiểm tra xem có tồn tại hay

không một cặp số (a, b) trong bốn số trên mà a chia hết cho b. Nếu có,

hiện ra màn hình tất cả các cặp số (a, b) tìm được.

11. Công thức tính dãy số Fibonacci như sau:

f(0) = 0; f(1) = 1; f(n) = f(n - 1) + f(n - 2), với n > 1.

Nhập từ bàn phím một số nguyên không âm n, hãy tính giá trị f(n). Hiện

kết quả tính được ra màn hình.

12. Nhập vào từ bàn phím 3 số nguyên h (0 ≤ h ≤ 23), m (0 ≤ m ≤ 59), s

(0 ≤ s ≤ 59) cho biết giờ, phút, giây của thời điểm hiện tại. Hãy tính xem

còn bao nhiêu giây nữa thì đến thời điểm 0 giờ, 0 phút, 0 giây.

Ví dụ:

h = 23, m = 59, s = 58.

Kết quả: còn 2 giây nữa thì đến 00:00:00

Page 62: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

62

Chương 4. Hàm

Các chương trình ví dụ mà ta gặp từ đầu đến giờ đều ngắn với duy nhất một

hàm chính – hàm main. Khi cần giải quyết bài toán phức tạp hơn, hàm main sẽ

dài hơn, và phức tạp hơn, dẫn đến chương trình khó đọc, khó hiểu, lập trình viên

dễ mắc lỗi, và lỗi khó tìm.

Để giải quyết các bài toán lớn và phức tạp, người ta thường sử dụng chiến lược

chia để trị. Nghĩa là, chia bài toán thành một chuỗi các bài toán nhỏ hơn, dễ

hơn, và chúng có thể được giải một cách ít nhiều độc lập với nhau, sau đó kết

hợp lại để có một lời giải hoàn chỉnh cho bài toán ban đầu. Trong lập trình, chia

để trị được thể hiện bằng việc chia chương trình thành các chương trình con.

Đoạn mã chứa trong thân một chương trình con sẽ được thực thi mỗi khi

chương trình con đó được gọi (hay kích hoạt). Chương trình con chính là một

trong các cơ chế cho phép mô-đun hóa chương trình.

Bên cạnh chiến lược chia để trị, việc sử dụng chương trình con còn mang lại

cho lập trình viên khả năng tái sử dụng mã, sử dụng các chương trình con như

là các khối lắp ghép để xây dựng các chương trình mới. Chương trình con cũng

giúp tránh được các đoạn mã giống nhau lặp đi lặp lại trong một chương trình.

Người ta khuyên rằng độ dài mỗi chương trình con không nên vượt quá một

trang màn hình để lập trình viên có thể kiểm soát tốt hoạt động của chương trình

con đó.

Trong C++, tất cả các chương trình con đều được thể hiện bởi cấu trúc hàm.

Ngay cả chương trình chính (main) cũng là một hàm. Một hàm được kích hoạt

khi nó được gọi từ bên trong một hàm khác, chẳng hạn hàm pow được gọi từ

trong hàm main của chương trình trong Hình 3.12. Khi hàm được gọi thực hiện

xong công việc của mình, nó trả kết quả về cho nơi gọi nó hoặc đơn giản là trả

quyền điều khiển về cho nơi gọi nó. Có thể so sánh hàm được gọi như là một

người thợ, còn hàm gọi nó như là người chủ. Người chủ giao việc cho người thợ

A, người thợ A thực hiện công việc và khi làm xong thì báo cáo kết quả lại cho

người chủ. Người thợ A trong khi làm nhiệm vụ của mình cũng có thể yêu cầu

một người thợ khác (B) thực hiện một nhiệm vụ con trong đó. Đến đây quan hệ

giữa người thợ A và người thợ B cũng tương tự như quan hệ giữa người chủ và

người thợ B.

Page 63: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

63

Chương này sẽ nói về cách định nghĩa các chương trình con (hoặc các hàm) và

các vấn đề liên quan như biến địa phương, cách truyền dữ liệu vào trong hàm.

4.1. Các hàm có sẵn

Các ngôn ngữ lập trình bậc cao thường cung cấp rất nhiều các hàm có sẵn cho

người dùng sử dụng. Các hàm đó được chia thành các nhóm hay các thư viện

khác nhau. Khi muốn sử dụng hàm có sẵn đó, ta chỉ cần chỉ ra cho chương trình

biết là hàm đó nằm ở thư viện nào (sử dụng #include) và sau đó có thể sử dụng

hàm đó một cách bình thường. Ví dụ như tại chương trình trong Hình 3.12, ta đã

sử dụng hàm pow và khai báo include thư viện cmath chứa nó.

Một số thư viện của C++ hay được sử dụng:

iostream: cung cấp các hàm liên quan đến nhập và xuất dữ liệu

cmath: cung cấp các hàm liên quan đến toán học.

cstdlib: cung cấp các hàm cho các mục đích chung như: quản lý bộ nhớ

động, sinh số ngẫu nhiên, tìm kiếm-sắp xếp…

cstring: cung cấp các hàm xử lý chuỗi kí tự như: tính độ dài, sao chép

chuỗi…

Có thể tra cứu chi tiết về các thư viện này tại [4].

Ví dụ, Bảng 4.1 liệt kê danh sách một số hàm toán học thông dụng.

Bảng 4.1: Các hàm toán học trong thư viện cmath.

Hàm Miêu tả

ceil( x ) số nguyên nhỏ nhất không nhỏ hơn x

cos( x ) hàm lượng giác cos x (x đo bằng radian)

exp( x ) ex

fabs( x ) giá trị tuyệt đối của x

floor( x ) số nguyên lớn nhất không lớn hơn x

fmod( x, y ) phần dư của phép chia x/y (lấy giá trị kiểu thực)

Page 64: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

64

log( x ) loga cơ số e của x ( ln x )

log10( x ) loga cơ số 10 của x ( log x )

pow( x, y ) xy

sin( x ) hàm lượng giác sin x (x đo bằng radian)

sqrt( x ) căn bậc 2 của x (x là giá trị không âm)

tan( x ) hàm lượng giác tan x (x đo bằng radian)

4.2. Cấu trúc chung của hàm

Ngoài việc sử dụng các hàm có sẵn trong thư viện, lập trình viên còn tự xây

dựng các hàm của riêng mình. Sau khi được định nghĩa, một hàm có thể được

sử dụng nhiều lần. Hàm được định nghĩa theo công thức:

kiểu_trả_về tên_hàm (tham_số_1, tham_số_2,…) {

thân_hàm }

trong đó:

kiểu_trả_về là kiểu giá trị mà hàm sẽ trả về. Đây có thể là một trong các

kiểu dữ liệu có sẵn của C++ hoặc một kiểu dữ liệu người dùng tự định nghĩa.

Trong C++, nếu hàm không trả về giá trị nào, thì kiểu_trả_về được thay thế

bằng từ khóa void.

tên_hàm là định danh của hàm, cách đặt tên hàm tuân thủ theo cách đặt định

danh và giống cách đặt tên biến.

tham_số_1, tham_số_2,…là danh sách các tham số đầu vào của hàm. Tên

đầy đủ của chúng là các tham số hình thức (formal parameter)4. Đây là

phương tiện để truyền dữ liệu vào trong hàm.

thân_hàm là nơi chứa chuỗi các lệnh.

4 Còn được gọi tắt là "tham số".

Page 65: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

65

Hình 4.1 mô tả cách khai báo, cài đặt và sử dụng một hàm tính diện tích hình

chữ nhật (calculateArea) trong C++. Hàm calculateArea nhận hai tham số

đầu vào là rectangleLength và rectangleWidth, nó tính diện tích hình chữ

nhật (lưu tại biến area) và trả lại kết quả (lệnh return area;).

#include <iostream>using namespace std;

//Khai báo và định nghĩa một hàm mớiint calculateArea (int rectangleLength, int rectangleWidth){ int area = rectangleLength * rectangleWidth; return area; //hàm trả lại một giá trị }

//hàm chính của chương trìnhint main (){ int length = 3; int width = 2; int area = calculateArea (length, width); cout << "The area is: " << area; return 0;

}

Hình 4.1: Ví dụ về khai báo và sử dụng hàm.

Lưu ý:

Việc khai báo và định nghĩa (thân hàm) hàm có thể tách ra thành hai phần khác

nhau. Ta sẽ gặp ở những ví dụ ở các chương sau.

Biến được khai báo trong một hàm được gọi là biến địa phương của hàm đó,

nghĩa là nó chỉ có phạm vi là bên trong thân hàm đó. Giả sử trong chương trình

của ta có hai hàm, mỗi hàm khai báo một biến có tên giống nhau, ví dụ area. Ta

sẽ có hai biến khác nhau tình cờ có tên giống nhau, và việc sửa đổi biến area

trong hàm này sẽ không có ảnh hưởng gì đến biến area trong hàm kia. Tức là,

đối với máy tính, hai biến này không có liên quan gì đến nhau.

4.3. Cách sử dụng hàm

Để sử dụng một hàm, ta gọi tên hàm, theo sau là cặp ngoặc đơn chứa các giá trị

truyền vào cho hàm (xem cách gọi hàm calculateArea từ trong hàm main ở

Page 66: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

66

Hình 4.1). Các giá trị truyền vào cho hàm được gọi là các đối số (argument)5.

Như trong lời gọi hàm calculateArea(length, width) tại Hình 4.1, length

và width là các đối số. Một hàm có thể được gọi nhiều lần với các đối số khác

nhau.

Khi chương trình thực thi một lời gọi hàm, giá trị của các đối số được sao chép

vào các tham số hình thức tương ứng, nghĩa là giá trị của length được chép vào

rectangleLength và giá trị của width được chép vào rectangleWidth, rồi

đoạn mã định nghĩa hàm được thực thi. Cuối cùng, lệnh return trả về một giá

trị (là giá trị của biểu thức nằm ngay sau từ khóa return) cho đoạn mã chương

trình đã gọi hàm này. Đồng thời, luồng điều khiển của chương trình cũng quay

trở lại với nơi gọi hàm và các lệnh nối tiếp phía sau lời gọi hàm sẽ được thực

thi.

4.4. Biến toàn cục và biến địa phương

Chương 2 đã giới thiệu các khái niệm cơ bản về biến. Trong mục này, ta sẽ bàn

kĩ hơn về các loại biến, hiệu lực và phạm vi của biến.

Chúng ta đã biết rằng biến có các thuộc tính như: tên, kiểu, kích thước, giá trị.

Ngoài ra, mỗi biến còn có các đặc điểm khác, cụ thể là:

thời gian sống: biến tồn tại bao lâu trong bộ nhớ

phạm vi: biến có thể được sử dụng tại những nơi nào trong chương trình

4.4.1. Phạm vi của biến

Phạm vi (scope) của một biến là những nơi chúng ta có thể sử dụng biến đó.

Một biến có thể có phạm vi toàn cục hay phạm vi địa phương.

Biến toàn cục (global variable) là biến được khai báo bên ngoài tất cả các hàm

(biến totalApples trong Hình 2.1). Biến toàn cục có thể được sử dụng trong

toàn bộ chương trình, ngay cả bên trong các hàm, miễn là đoạn mã sử dụng biến

nằm sau dòng lệnh khai báo biến đó.

Biến địa phương (local variable) là biến được khai báo bên trong một hàm

hoặc một khối lệnh (trong C++, một khối lệnh được gói trong cặp ngoặc {}).

Biến địa phương chỉ có thể sử dụng bên trong khối lệnh mà nó được khai báo tại

5 Một số tài liệu dùng thuật ngữ "tham số thực sự" hay "tham đối" để chỉ đối số. Một số tài liệu khác dùng thuật

ngữ "tham số" để gọi chung cho cả tham số hình thức lẫn đối số.

Page 67: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

67

đó. Khối lệnh này có thể là thân của một hàm hoặc một khối cấu trúc điều khiển

(xem Chương 3). Chẳng hạn, tại chương trình trong Hình 4.2 có ba biến x khác

nhau với các phạm vi khác nhau: biến x được khai báo trong hàm foo có phạm

vi là hàm foo; biến x được khai báo ngay tại dòng đầu tiên của thân hàm main

có phạm vi là toàn bộ thân hàm main do cặp ngoặc {} gần nhất chính là cặp

ngoặc bao quanh thân hàm main; và biến x được khai báo bên trong thân vòng

for của hàm main chỉ có phạm vi bên trong vòng for. Đây là các biến hoàn

toàn độc lập. Sửa đổi đối với biến này hoàn toàn không có ảnh hưởng gì đối với

các biến kia (thể hiện ở các dòng output: con đếm x của vòng for thay đổi giá

trị trong khi hai biến x còn lại không có gì thay đổi).

#include<iostream>using namespace std;

void foo( void );

int main(){ int x = 5; // local variable to main foo();

for (int i = 1; i <= 3; i++) { int x = i * i; // local variable to for loop cout << "Local x in for loop is " << x << endl; } cout << "Local x in main is " << x << endl;}

void foo(){ int x = 0; // local variable to foo cout << "Local x in foo is " << x << endl;}

Kết quả chạy chương trình:

Local x in foo is 0Local x in for loop is 1Local x in for loop is 4Local x in for loop is 9Local x in main is 5

Hình 4.2: Ví dụ về phạm vi của biến.

Page 68: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

68

4.4.2. Thời gian sống của biến

Trong trường hợp mặc định với lệnh khai báo thông thường, một biến sẽ được

tạo ra khi chương trình chạy đến lệnh khai báo biến đó và tồn tại cho đến khi

chương trình ra khỏi phạm vi của biến đó. Cụ thể, các biến toàn cục có phạm vi

là toàn bộ chương trình, nên chúng được sinh ra và khởi tạo đúng một lần và

"sống" cho đến khi chương trình kết thúc.

Với các biến địa phương được khai báo theo kiểu thông thường, phạm vi của

chúng kéo từ lệnh khai báo chúng cho đến hết khối chương trình chứa lệnh khai

báo đó. Do đó, các biến này được tạo ra mỗi khi chương trình chạy đến lệnh

khai báo biến và bị hủy đi khi chương trình chạy đến hết khối. Người ta còn gọi

các biến địa phương này là biến tự động (automatic variable).

Trường hợp ngoại lệ trong C++ là các biến địa phương được khai báo với từ

khóa static. Các biến này tuy là biến địa phương nhưng "sống" cho đến khi

chương trình kết thúc mặc dù nó không thể được truy nhập tới trong khi chương

trình đang chạy ở ngoài phạm vi của nó. Các biến static này giữ nguyên giá trị

của mình giữa các lần gọi hàm. Người ta gọi các biến địa phương loại này là

biến tĩnh (static variable).

Chương trình trong Hình 4.3 là một ví dụ minh họa sự khác nhau giữa biến tĩnh

và biến thông thường. Trong hàm staticVariableTest có hai biến địa

phương: x là biến tự động và y là biến tĩnh. Cả hai đều được khai báo với giá trị

khởi tạo bằng 0, cùng được in ra màn hình trước khi tăng thêm 1.

staticVariableTest được gọi bốn lần từ trong hàm main, mỗi lần lại in ra

màn hình một dòng thông báo giá trị của x và y. Kết quả in ra màn hình cho

thấy x có giá trị bằng 0 trong tất cả các lời gọi hàm, trong khi biến tĩnh y mỗi

lần lại tăng thêm 1. Lần nào bước vào hàm staticVariableTest, x cũng nhận

giá trị khởi tạo (0). Trong khi đó, y chỉ được khởi tạo bằng 0 đúng một lần, nó

không bị hủy khi hàm kết thúc mà nó vẫn "sống" và giữ hiệu ứng của phép tăng

ở cuối hàm staticVariableTest từ lần gọi hàm trước cho đến lần gọi hàm sau.

Page 69: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

69

#include <iostream>

using namespace std;

void staticVariableTest(){ int x = 0; static int y = 0; cout << "x = " << x << ", y = " << y << endl; x++; y++;}

int main(){ staticVariableTest(); staticVariableTest(); staticVariableTest(); staticVariableTest();

return 0;}

Kết quả chạy chương trình:

x = 0, y = 0x = 0, y = 1x = 0, y = 2x = 0, y = 3

Hình 4.3: Ví dụ về biến tĩnh.

4.5. Tham số, đối số, và cơ chế truyền tham số cho hàm

Trong phần này chúng ta sẽ tìm hiểu kĩ về các cơ chế truyền tham số cho hàm.

Có hai cơ chế chính là truyền giá trị và truyền tham chiếu với điểm khác

nhau căn bản: một bên chỉ cho phép hàm được gọi thao tác trên bản sao dữ liệu

của nơi gọi hàm, bên kia cho phép hàm được gọi truy nhập trực tiếp.

4.5.1. Truyền giá trị

Trong cơ chế truyền giá trị hay truyền bằng giá trị (pass-by-value), tham số

được truyền bằng giá trị, nghĩa là bản sao của đối số được chép vào tham số

tương ứng trong hàm chứ bản thân đối số thì không được truyền vào hàm. Các

thay đổi đối với bản sao sẽ không có ảnh hưởng gì đối với bản gốc, cho nên, khi

hàm thực hiện các sửa đổi đối với một tham số thì các sửa đổi này không có ảnh

Page 70: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

70

hưởng gì tới đối số tương ứng được dùng cho lời gọi hàm. Tất cả các ví dụ trong

chương này cho đến đây đều dùng cơ chế truyền giá trị.

Xem thêm minh họa trong Hình 4.4. Việc tham số hình thức limit bị thay đổi

giá trị từ bên trong hàm sum không có ảnh hưởng gì đối với đối số upperLimit

(một biến của hàm main) được dùng khi gọi hàm này. Sau khi lời gọi hàm được

thực hiện xong, upperLimit vẫn giữ nguyên giá trị là 9.

#include <iostream>using namespace std;

int sum(int limit) { int total = 0; for ( ; limit > 0; limit--) total += limit; return total;}

int main (){ int upperLimit = 9; int total = sum (upperLimit); cout << "The sum of numbers from 1 to " << upperLimit << " is " << total << endl; return 0;}

Kết quả chạy chương trình:

The sum of numbers from 1 to 9 is 45

Hình 4.4: Ví dụ về truyền giá trị cho hàm.

Truyền giá trị được xem là lựa chọn an toàn do nó đảm bảo rằng hàm được gọi

sẽ không gây hiệu ứng phụ không mong muốn đối với dữ liệu của nơi gọi hàm.

Sử dụng cơ chế truyền giá trị là một trong các khía cạnh của phong cách lập

trình tốt. Tuy nhiên, cơ chế này có một nhược điểm: nếu dữ liệu được truyền

vào hàm có kích thước lớn thì sẽ tốn phí về thời gian chạy và bộ nhớ.

4.5.2. Truyền tham chiếu

Khác với truyền giá trị, cơ chế truyền tham chiếu hay truyền bằng tham

chiếu (pass-by-reference) cho phép hàm được gọi truy nhập trực tiếp tới dữ liệu

của nơi gọi hàm và sửa dữ liệu đó nếu muốn. So với truyền giá trị, cơ chế truyền

Page 71: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

71

tham chiếu giúp chương trình có hiệu năng cao hơn do tránh được chi phí cho

việc sao chép dữ liệu. Tuy nhiên, truyền tham chiếu làm cho chương trình kém

an toàn do nguy cơ hàm được gọi có thể làm rối loạn dữ liệu của hàm gọi. Mục

này nói về một cơ chế mà C++ cung cấp để truyền tham số bằng tham chiếu.

Ngoài ra, Chương 6 sẽ nói đến một dạng truyền tham chiếu khác đặc thù của

C/C++: con trỏ.

Trước tiên chúng ta tìm hiểu về khái niệm tham chiếu trong ngôn ngữ lập trình

C++. Tham chiếu (reference) của một biến về bản chất là một biệt danh hay

một cái tên khác của chính biến đó. Các thao tác trên tham chiếu của một biến

cũng có hiệu quả giống hệt như thao tác trên chính biến đó và ngược lại. Sau

khi khai báo, tham chiếu được sử dụng trong chương trình như các biến khác.

Ví dụ dòng:

int &numberStick = numberUSB;

khai báo numberStick là một tham chiếu tới một biến kiểu int có tên

numberUSB. Lưu ý rằng trong khai báo có dấu &. Ngoài ra, tham chiếu phải

được khởi tạo ngay khi khai báo, C++ không chấp nhận tình trạng một tham

chiếu không chiếu đi đâu cả.

Hình 4.5 minh họa cách khai báo và sử dụng tham chiếu. Biến numberStick

được khai báo (có dấu & vào đằng trước tên biến) là một tham chiếu tới biến

numberUSB (stick là một tên gọi khác của USB stick). Mọi tính toán và tác động

trên biến numberStick sẽ tương tự như trên biến numberUSB và ngược lại vì

thực chất hai biến này là một.

#include <iostream>using namespace std;

int main(){ int numberUSB = 10; int &numberStick = numberUSB;

numberStick ++; cout << numberUSB << " " << numberStick; return 0;

}

Hình 4.5: Ví dụ về khai báo và sử dụng tham chiếu.

Page 72: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

72

Trong nhiều trường hợp, chúng ta muốn việc thay đổi giá trị của một tham số

trong hàm sẽ làm thay đổi giá trị của đối số tương ứng. Ví dụ, khi ta viết một

hàm nhập giá trị cho các biến từ bàn phím.

Để làm như vậy, ta có thể sử dụng cơ chế truyền tham chiếu. Tức là, tham số

hình thức sẽ là một tham chiếu tới đối số tương ứng. Trong cơ chế này, các sửa

đổi đối với tham số hình thức ở bên trong hàm được thực hiện trên chính đối số

dùng cho lời gọi hàm.

Hình 4.6 minh họa cơ chế truyền tham chiếu với hàm getStudent. Hàm này

nhập hai số nguyên từ bàn phím và lưu chúng vào hai tham số id và mark (đã

được khai báo là tham chiếu). Việc lưu giá trị vào id và mark sẽ làm thay đổi

giá trị của đối số là các biến tương ứng của hàm main.

Khác biệt duy nhất về cú pháp giữa truyền giá trị và truyền tham chiếu là ở danh

sách khai báo tham số hình thức. Để quy định tham số nào sẽ được truyền bằng

tham chiếu, cần có dấu & đặt ngay sau tên kiểu dữ liệu tại khai báo tham số đó

trong định nghĩa hàm. Trong ví dụ hàm getStudent, có các dấu & đặt tại khai

báo của các tham số id và mark.

void getStudent (int &id, int &mark);

Lưu ý: Hàm getStudent không cần trả về giá trị gì nên nó đã được khai báo với

từ khóa void cho kiểu giá trị trả về và không cần có lệnh return kèm giá trị

trong thân hàm.

Page 73: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

73

#include <iostream>using namespace std;

void getStudent (int &id, int &mark){ cout << "Enter student’s id and mark: "; cin >> id >> mark; return;}

int main(){ int id1, mark1; getStudent (id1, mark1); cout << "Student " << id1 << " gets mark " << mark1 << endl; int id2, mark2; getStudent (id2, mark2); cout << "Student " << id2 << " gets mark " << mark2 << endl; cout << "Total mark: " << (mark1 + mark2); return 0;}

Kết quả chạy chương trình

Enter student's id and mark: 1 5Student 1 gets mark 5Enter student's id and mark: 2 6Student 2 gets mark 6Total mark: 11

Hình 4.6: Ví dụ về truyền tham biến cho hàm.

Như đã nói ở trên, việc cho phép hàm được gọi truy nhập trực tiếp tới dữ liệu

của nơi gọi hàm gây nguy cơ hàm được gọi phá rối dữ liệu của nơi gọi hàm.

Vậy có cách nào để giảm bớt hoặc ngăn chặn nguy cơ này? Đó là hằng tham

chiếu. Một khi một tham chiếu được khai báo là hằng, trình biên dịch sẽ đảm

bảo rằng tham chiếu đó sẽ chỉ được dùng để đọc chứ không thể sửa đổi biến mà

nó chiếu tới.

4.5.3. Tham số mặc định

Khi khai báo hàm, ta có thể đặt sẵn một giá trị mặc định cho các tham số. Các

tham số này phải đứng cuối danh sách. Để đặt giá trị mặc định, ta chỉ cần viết

dấu gán và giá trị mặt định cho tham số tại khai báo hàm. Nếu tham số đó

không được truyền giá trị khi gọi hàm, giá trị mặc định sẽ được dùng. Ngược

Page 74: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

74

lại, nếu một giá trị được truyền vào tham số thì giá trị mặc định sẽ bị bỏ qua để

dùng giá trị được truyền vào.

Hình 4.7 minh họa về cách khai báo và sử dụng tham số mặc định. Trong đó,

tham số hình thức upper của hàm sum có giá trị mặc định là 9. Lần gọi hàm sum

thứ nhất cung cấp đối số cho cả hai tham số (lower = 1 và upper = 5) và ta thu

được kết quả là 15. Lần gọi hàm sum thứ hai chỉ cung cấp đối số có giá bằng 1

cho tham số lower, còn tham số upper sẽ nhận giá trị mặc định (9), ta thu được

kết quả là 45.

#include <iostream>using namespace std;

int sum(int lower, int upper=9) { int total = 0; for ( ; lower <= upper; lower ++) total += lower; return total;}

int main (){ int lowerLimit = 1; int upperLimit = 5;

int total = sum (lowerLimit, upperLimit); cout << "Total is " << total << endl;

total = sum (lowerLimit); cout << "Total is " << total << endl; return 0; }

Kết quả chạy chương trình

Total is 15Total is 45

Hình 4.7: Ví dụ về tham số mặc định.

Về nguyên tắc, trình biên dịch sẽ duyệt danh sách các đối số truyền vào từ trái

qua phải, và lần lượt gắn các đối số cho các tham số hình thức tương ứng. Khi

kết thúc danh sách đối số, những tham số hình thức chưa được truyền giá trị sẽ

Page 75: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

75

nhận giá trị mặc định. Lưu ý: số đối số truyền vào phải nhiều hơn hoặc bằng số

tham số hình thức không được gắn giá trị mặc định.

4.6. Hàm trùng tên

Trong C++, hai hàm khác nhau có thể có tên trùng nhau (function overloading)

nếu danh sách tham số (chỉ quan tâm đến kiểu dữ liệu mà không quan tâm đến

tên của tham số) của hai hàm là khác nhau.

Ví dụ,

void print (int x);

void print (float x);

void print (int x, int y);

là ba hàm khác nhau trong C++. Mỗi khi cần dịch một lời gọi hàm có tên print,

trình biên dịch kiểm tra kiểu dữ liệu của đối số và số lượng các đối số trong lời

gọi hàm đó để xác định xem hàm được gọi là hàm nào trong ba hàm print đã

được định nghĩa.

Ví dụ trong Hình 4.8 minh họa việc khai báo và sử dụng các hàm trùng tên

print liệt kê ở trên. Cả ba hàm được gọi từ trong hàm main với danh sách tham

số khác nhau. Kết quả chạy chương trình chứng tỏ trình biên dịch đã gọi đúng

hàm cho các bộ đối số khác nhau.

Page 76: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

76

#include <iostream>using namespace std;

void print (int x){ cout << "int: " << x << endl;}

void print (double x){ cout << "double: " << x << endl;}

void print (int x, int y){ cout << "pair: " << x << " " << y << endl;}

int main (){ print(1); print(10.5); print(1,2); return 0; }

Kết quả chạy chương trình:

int: 1double: 10.5pair: 1 2

Hình 4.8: Ví dụ về hàm trùng tên.

Các hàm trùng tên được phân biệt bởi chữ kí của hàm. Chữ kí của một hàm bao

gồm tên của hàm đó và danh sách kiểu tham số theo thứ tự khai báo.

Theo quy tắc đó, ba lệnh khai báo hàm dưới đây có chữ kí trùng nhau nên

không được C++ chấp nhận làm các hàm trùng tên:

int print (int x);

float print (int x);

int print (int y);

Khi kết hợp sử dụng hàm trùng tên và tham số mặc định, cần lưu ý tránh trường

hợp một hàm khi bỏ qua các tham số mặc định lại có lời gọi giống hệt với một

Page 77: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

77

hàm trùng tên với nó. Ví dụ, khi ta có hai hàm trùng tên, một hàm có danh sách

tham số rỗng, hàm kia cho phép tất cả các tham số có thể lấy giá trị mặc định,

chẳng hạn int foo() và int foo(int i=1, float j=2.1). Tình trạng này sẽ

gây lỗi biên dịch do tình trạng nhập nhằng không thể xác định một lời gọi hàm

ứng với hàm nào.

C++ còn cho phép sử dụng cơ chế hàm trùng tên để định nghĩa các phiên bản

khác của các phép toán đã có sẵn để dùng cho các kiểu dữ liệu không cơ bản.

Chẳng hạn lập trình viên có thể định nghĩa kiểu dữ liệu số phức và viết phép

cộng (+) dành cho kiểu dữ liệu này. Nội dung chi tiết về chủ đề này nằm ngoài

phạm vi của cuốn sách này, người đọc có thể tìm hiểu tại [2].

4.7. Hàm đệ quy

Trong các ví dụ từ đầu cuốn sách, ta đã làm quen với việc một hàm gọi một hàm

khác. Đệ quy là dạng gọi hàm đặc biệt: hàm đệ quy là hàm chứa lời gọi tới

chính nó, trực tiếp hoặc gián tiếp (qua một hàm khác).

Dạng hàm này được dùng để biểu diễn các thuật toán đệ quy – đưa một bài toán

về một bài toán tương tự nhưng có kích thước nhỏ hơn. Đây là chủ đề phức tạp,

sẽ được nói đến một cách đầy đủ hơn trong các môn học cao hơn của ngành

Khoa học Máy tính. Phần này chỉ giới thiệu về đệ quy ở mức đơn giản nhất.

Ví dụ điển hình thường được dùng để minh họa cho hàm đệ quy là bài toán tính

giai thừa của một số. Giai thừa của n được tính theo công thức đệ quy:

n! = n * (n-1) * (n-2) * (n-3) ... * 1

tương đương với:

n! = n * (n-1)!

Nghĩa là, giả sử f(n) là giai thừa bậc n, f(n) được định nghĩa như sau:

f(0) = 1

f(n) = n * f(n-1)

Trong đó, dòng thứ nhất là trường hợp cơ bản, dòng thứ hai là công thức đệ

quy.

Hàm đệ quy viết bằng C++ về cơ bản là giống hệt với công thức đệ quy ở trên.

Hình 4.9 mô tả hàm đệ quy để tính giá trị của n! Hàm đệ quy factorial bao

gồm một khối lệnh if-else chia hai trường hợp cơ bản và tổng quát. Trường

hợp cơ bản xảy ra nếu n ≤ 1, khi đó hàm kết thúc tính toán và trả về giá trị 1 là

Page 78: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

78

giai thừa của n. Trường hợp còn lại (n > 1), hàm gọi đệ quy để tính giá trị của

(n - 1)! rồi trả về kết quả là tích của n và (n - 1)!.

Trong hai phần chính của hàm đệ quy, trường hợp cơ bản nhìn qua có vẻ đơn

giản nhưng thực ra lại có vai trò rất quan trọng trong hàm đệ quy. Nếu thiếu

hoặc có nhầm lẫn khi viết trường hợp này, hoặc sơ suất trong bước đệ quy làm

nó không thể hội tụ về trường hợp cơ bản thì chương trình có nguy cơ đệ quy vô

tận đến khi cạn kiệt bộ nhớ dành cho chương trình. Hiện tượng này tương tự

như vòng lặp vô tận.

#include <iostream>using namespace std;

unsigned long factorial (int n){ if (n <= 1) // test for base case return 1; // base case: n! = 1 else //recursion step return (n * factorial (n-1)); // calculate (n-1)! first}

int main (){ int n = 5; cout << n << "! = " << factorial (n); return 0;

}

Hình 4.9: Ví dụ về hàm đệ quy tính n!.

Một điểm cần nhỏ chú ý trong chương trình tính giai thừa là kiểu trả về cho hàm

tính giai thừa. Giá trị của giai thừa tăng rất nhanh so với n: 10! đã đạt đến giá trị

3628800; nếu dùng kiểu int (có kích thước 4 byte ở hầu hết các bản cài đặt

C++) thì giai thừa của 16 bắt đầu vượt ra ngoài khoảng giá trị của int (hãy thử

bằng cách sửa chương trình ví dụ trong Hình 4.9). Do đó, để có thể đáp ứng giá

trị lớn hơn của n, kiểu dữ liệu cho giá trị giai thừa phải có kích thước lớn. Tài

liệu C++ chuẩn quy định kiểu long int có kích thước lớn nhất trong các kiểu

nguyên và gồm ít nhất 4 byte. Kiểu unsigned long int (viết ngắn gọn là

unsigned long) chỉ gồm các giá trị không âm nên có thể lưu giá trị từ 0 tới ít

nhất 4294967295, ta nên chọn kiểu dữ liệu này cho kiểu trả về của hàm tính giai

thừa.

Page 79: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

79

Bài tập

1. Viết một hàm nhận tham số đầu vào là 5 số nguyên, và trả về kết quả là

trung bình cộng của 5 số nguyên đó. Hiện kết quả trả về của hàm ra màn

hình.

2. Hãy tìm những hàm trùng nhau trong danh sách các hàm sau:

a) int foo (int x)

b) int foo (float x)

c) float foo (int y)

d) float foo (int x, int y)

e) float foo (int x, float y)

f) int foo (int student, float teacher)

3. Khi nào thì nên truyền đối số cho hàm theo kiểu truyền giá trị, khi nào thì

nên truyền theo kiểm tham chiếu?

4. Viết một chương trình có một hàm với 5 tham số dạng tham chiếu để nhập

vào 5 số nguyên từ bàn phím. Tìm và hiện ra màn hình tất cả các cặp số có

tổng bằng 50.

5. Viết chương trình với một hàm get_min để tính giá trị nhỏ nhất của 3 số.

Hàm trên được viết dựa vào hàm get_min tính giá trị nhỏ nhất của hai số.

Lưu ý, hai hàm này trùng tên.

6. Nhập từ bán phím hai số nguyên x, y. Hãy tính và hiện ra màn hình giá trị

của các hàm sau đây:

a) căn bậc hai của x

b) ex

c) log (x)

d) x!

e) x!!

7. Tìm hiểu hàm rand để sinh ra các số ngẫu nhiên trong ngôn ngữ lập trình

C++. Hãy viết chương trình sinh ra 9 số nguyên ngẫu nhiên nằm trong phạm

Page 80: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

80

vi từ 2 đến 99. Hiện ra màn hình các cắp số nguyên được sinh ra có tổng

bằng 100.

8. Dãy số Fibonacci được tính như sau:

a) f (0) = 0; f (1) = 1; f(n) = f (n-1) + f(n-2) với n > 1.

b) Hãy viết chương trình với hàm đệ quy để tính f(k), với k là giá trị nhập từ

bàn phím. Hiện kết quả tính được ra màn hình.

9. Viết chương trình có một hàm đệ quy để tính tổng tất cả các số nguyên

dương chia hết cho 3 và nhỏ hơn một số nguyên cho trước nhập từ bàn phím.

Đưa kết quả tính được ra màn hình.

10. Viết một hàm nhận đầu vào là hai số x, y và trả ra 4 giá trị là kết quả của 4

phép toán sau đây:

a) Thương của phép toán x chia cho y

b) Tích của phép toán x nhân

c) Phần nguyên của x chia cho y

d) Phần dư của x chia cho y

11. Cho một dãy số gồm 5 số nguyên có giá trị đôi một khác nhau được nhập

vào từ bàn phím. Hãy viết hàm f(k) trả ra giá trị của phần tử lớn thứ k trong

dãy số trên. Giá trị k được nhập vào từ bàn phím. Hiện kết quả tìm được ra

màn hình.

Ví dụ cho dãy số: 14, 5, 11, 2, 23

f(1) = 23; f(3) = 11; f(5) = 2.

12. Nhập vào từ bàn phím 1 số nguyên x, viết một hàm tính tổng tất cả các chữ

số của số nguyên x. Hiện kết quả tìm được ra màn hình.

Ví dụ: x = 25701, thì tổng các chữ số của x là: 2+5+7+0+1 = 15.

Page 81: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

81

Chương 5. Mảng

Các kiểu dữ liệu cơ bản như đã giới thiệu trong Chương 2 không đủ để biểu

diễn các loại dữ liệu mà các bài toán đòi hỏi. Một ví dụ là khi chương trình cần

lưu và xử lý một chuỗi các phần tử dữ liệu cùng kiểu, chẳng hạn như danh sách

sinh viên trong trường hoặc danh sách điểm thi của một sinh viên, để có thể sắp

xếp, tìm kiếm, và tính toán các con số thống kê trên chuỗi dữ liệu đó. Đa số các

ngôn ngữ lập trình cung cấp các kiểu dữ liệu có cấu trúc để phục vụ các nhiệm

vụ này, trong đó, mảng là cấu trúc dữ liệu thông dụng nhất.

5.1. Mảng một chiều

Mảng một chiều là một chuỗi hữu hạn các phần tử dữ liệu thuộc cùng một kiểu

dữ liệu, đặt tại các ô nhớ liên tiếp trong bộ nhớ. Mỗi phần tử trong mảng có một

chỉ số khác nhau. Mảng cho phép định vị và truy nhập đến từng phần tử bằng

cách sử dụng chỉ số của phần tử đó.

Ví dụ, điểm số cho 7 môn thi của một sinh viên có thể được lưu trữ trong một

mảng có kích thước bằng 7 (nghĩa là có 7 ô nhớ) thay vì khai báo 7 biến khác

nhau cho điểm thi từng môn. Mảng score có thể được hình dung như sau:

score 30 85 76 90 72 80 88

0 1 2 3 4 5 6

trong đó, mỗi ô biểu diễn một phần tử của mảng – trong trường hợp này là một

giá trị thuộc kiểu int. Bảy phần tử của mảng được đánh số lần lượt từ 0 đến 6,

đó còn gọi là chỉ số của các phần tử trong mảng. Bảy phần tử của mảng có thể

được xem như 7 biến kiểu int với "tên" của chúng là score[0], score[1],

score[2], score[3], score[4], score[5], và score[6], trong đó chỉ số của

mỗi phần tử được đặt trong cặp ngoặc vuông. Khi truy nhập một phần tử mảng,

có thể dùng chỉ số là một giá trị nguyên hoặc một biểu thức có giá trị nguyên.

Ví dụ, với biến i có giá trị bằng 3, lệnh sau gán giá trị 100 cho score[5]:

score[i+2] = 100;

còn lệnh sau tính trung bình cộng của score[0] và score[1] rồi ghi kết quả

vào biến m:

Page 82: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

82

m = (score[0] + score[1]) / 2;

5.1.1. Khai báo và khởi tạo mảng

Cũng như một biến bình thường, mảng phải được khai báo trước khi sử dụng.

Cú pháp khai báo một mảng trong C++ có dạng:

kiểu_dữ_liệu tên_mảng [số_phần_tử];

trong đó, kiểu_dữ_liệu là một kiểu dữ liệu hợp lệ (chẳng hạn int, float, char,

bool…), tên_mảng là một định danh hợp lệ, và số_phần_tử (luôn đặt trong cặp

ngoặc vuông) quy định số lượng phần tử mà mảng cần chứa.

Ví dụ, mảng score trong ví dụ ở trên được khai báo như sau:

int score [7];

Lưu ý: giá trị số phần tử đặt trong cặp ngoặc vuông phải là một hằng số, do các

mảng khai báo kiểu này thuộc bộ nhớ tĩnh và phải có kích thước được xác định

trước khi chương trình thực thi. Mảng với kích thước động sẽ được nói đến

trong chương sau.

Hình 5.1 minh họa việc khai báo và sử dụng mảng một chiều. Chương trình

trong hình yêu cầu người dùng nhập điểm cho 7 môn học, lần lượt cộng dồn

tổng điểm rồi tính điểm trung bình. Để ý rằng chương trình sử dụng một hằng

biến (NUMBER_COURSES) để lưu trữ kích thước mảng, cũng là số môn học, thay vì

sử dụng giá trị 7 trong toàn bộ đoạn mã. Giá trị chỉ xuất hiện đúng một lần khi

khai báo hằng NUMBER_COURSES. Với cách sử dụng hằng như vậy, nếu ta muốn

thay đổi số môn học mà chương trình xử lý, ta chỉ cần sửa duy nhất một vị trí

trong chương trình là lệnh khai báo hằng NUMBER_COURSES thay vì phải làm một

công việc tốn thời gian và dễ gây lỗi là tìm và sửa tất cả các giá trị 7 có liên

quan (trong các chương trình phức tạp hơn, không phải giá trị 7 nào cũng liên

quan đến số môn học).

Page 83: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

83

#include <iostream>using namespace std;

const int NUMBER_COURSES = 7;

int main(){ int score[NUMBER_COURSES]; float sum = 0;

for (int course = 0; course < NUMBER_COURSES; count++) { cout << "Enter the score for course #" << course << ": "; cin >> score[course]; sum = sum + score[course]; } cout << "The average score is " << sum/NUMBER_COURSES;

return 0;}

Kết quả chạy chương trình

Enter the score for course #0: 30Enter the score for course #1: 85Enter the score for course #2: 76Enter the score for course #3: 90Enter the score for course #4: 72Enter the score for course #5: 80Enter the score for course #6: 88

The average score is 74.42857

Hình 5.1: Ví dụ về khai báo và sử dụng mảng.

Lưu ý sự khác nhau về ngữ nghĩa của giá trị bên trong cặp ngoặc vuông: tại lệnh

khai báo mảng thì nó là kích thước mảng, còn khi truy nhập phần tử của mảng

thì nó là chỉ số của phần tử mảng.

Ta có thể khai báo mảng chứa bất cứ loại dữ liệu nào ngoại trừ tham chiếu.

Chẳng hạn ta có thể khai báo một mảng kiểu char để chứa một xâu ký tự.

Khi khai báo một mảng, ta có thể khởi tạo giá trị cho các phần tử của mảng theo

cách sau, với lưu ý rằng số giá trị nằm trong danh sách khởi tạo không được

vượt quá kích thước được khai báo của mảng.

Page 84: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

84

int score [7] = {60, 70, 89, 75, 88, 34, 90};

Ta cũng có thể bỏ qua không khai báo kích thước của mảng để C++ tự xác định

thông qua độ dài của dãy khởi tạo. Ví dụ lệnh sau đây cho hiệu quả giống hệt

lệnh trên:

int score [] = {60, 70, 89, 75, 88, 34, 90};

Nếu không được khởi tạo, các phần tử của mảng sẽ có giá trị không xác định

cho đến khi ta gán cho chúng một giá trị nào đó. Việc sử dụng các giá trị không

xác định sẽ dẫn đến lỗi logic.

5.1.2. Ứng dụng của mảng

Mảng có thể được dùng để lưu trữ và tính toán nhiều dạng dữ liệu. Dễ thấy nhất

là dùng mảng để lưu trữ một chuỗi giá trị, chẳng hạn như dãy điểm số của các

môn học như ví dụ trong Hình 5.1. Ta có thể dùng mảng để biểu diễn tập hợp,

chẳng hạn mảng a = {1, 0, 1} đại diện cho tập hợp [0, 2], trong đó phần tử

a[i] có giá trị khác 0 mang ý nghĩa rằng giá trị i có mặt trong tập hợp.

Ngoài ra, ta có thể dùng các phần tử mảng làm các con đếm. Chương trình ví dụ

trong Hình 5.2 giả lập việc tung xúc xắc và thống kê số lần ra các mặt có giá trị

từ 1 đến 6. Chương trình sử dụng mảng counter chứa các con đếm, trong đó

counter[i] dùng để đếm số lần tung xúc xắc được mặt có giá trị i. Vòng for

thứ nhất trong chương trình thực hiện giả lập việc tung xúc 6.000.000 lần, sau

mỗi lần tung lại tăng con đếm tương ứng. Để giả lập tung xúc xắc, chương trình

dùng hàm sinh số ngẫu nhiên rand() thuộc thư viện cstdlib, vốn nằm trong bộ

thư viện chuẩn của ngôn ngữ C. Hàm này trả về một giá trị nguyên ngẫu nhiên

trong khoảng từ 0 đến RAND_MAX (một hằng số được định nghĩa trong cstdlib,

có giá trị phụ thuộc từng bộ cài đặt khác nhau của C++ nhưng được đảm bảo là

không nhỏ hơn 32767). Để có được một giá trị ngẫu nhiên trong một khoảng giá

trị, ta kết hợp rand(), phép lấy đồng dư và các phép tính số học khác. Ví dụ,

công thức (rand() % 6 + 1) như trong chương trình cho kết quả là một số

ngẫu nhiên trong đoạn từ 1 đến 6. Thực ra hàm rand() chỉ sinh ra các số giả

ngẫu nhiên (pseudocode). Chuỗi số mà rand() sinh ra khi được gọi lặp đi lặp

lại trông có vẻ ngẫu nhiên nhưng mỗi lần chạy chương trình lại cho ra cùng một

chuỗi. Để chuỗi số mà rand() sinh ra mỗi lần mỗi khác, ta cần dùng kèm hàm

srand() trước khi gọi rand() lần đầu trong chương trình. Hàm srand() lấy

một tham số kiểu int và lấy giá trị này làm hạt giống cho chuỗi ngẫu nhiên sẽ

được sinh. Các hạt giống khác nhau sẽ cho chuỗi khác nhau. Trong chương

Page 85: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

85

trình ví dụ, ta dùng thời gian hiện tại làm hạt giống (gọi hàm time() với đối số

bằng 0).

#include <iostream>#include <iomanip>#include <cstdlib>#include <ctime>

using namespace std;

int main(){

const int numberOfFaces = 6; int counter[numberOfFaces + 1] = { 0 }; //ignore counter[0]

srand( time(0) ); // initialize random number generator

// roll die 1,000,000 times;for ( int roll = 1; roll <= 6000000; roll++ ) {

int upFace = 1 + rand() % numberOfFaces;counter[upFace]++; // increment the face’s counter

}

cout << "Face\tCount" << endl;

// output the arrayfor ( int face = 1; face <= numberOfFaces; face++ )

cout << face << "\t" << counter[face] << endl;

return 0;}

Kết quả chạy chương trình

Face Count1 10009222 9995843 9992614 9989445 9999356 1001354

Hình 5.2: Sử dụng mảng để đếm.

Page 86: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

86

5.1.3. Trách nhiệm kiểm soát tính hợp lệ của chỉ số mảng

Đối với mảng được khai báo với kích thước n, chỉ số của các phần tử trong

mảng đó là các số nguyên từ 0 đến n–1. Ngoài ra, các giá trị khác đều không

hợp lệ. Việc truy nhập mảng bằng các chỉ số không hợp lệ, chẳng hạn khi truy

nhập đến score[-1] hay score[n], có thể dẫn đến các thay đổi không mong

muốn đối với dữ liệu ở vùng bộ nhớ bên ngoài mảng (có thể thuộc về các biến

khác), hậu quả là chương trình chạy sai hoặc gặp sự cố dừng đột ngột. Trong

nhiều ngôn ngữ lập trình, việc truy nhập mảng được kiểm soát tự động để tránh

trường hợp truy nhập với chỉ số không hợp lệ. Tuy nhiên, trong C++, việc truy

nhập đến các phần tử của mảng với chỉ số nhỏ hơn 0 hoặc lớn hơn n–1 không hề

phạm lỗi cú pháp, việc truy nhập ra ngoài mảng không gây lỗi khi dịch nhưng

có thể gây lỗi khi chạy. Lập trình viên có trách nhiệm kiểm soát các giá trị chỉ

số mảng để tránh trường hợp này.

5.1.4. Mảng làm tham số cho hàm

Có thể dùng mảng làm tham số cho hàm. Hình 5.3 là kết quả của việc sửa

chương trình trong Hình 5.1, đưa hai nhiệm vụ nhập dữ liệu cho mảng và tính

điểm trung bình vào hai hàm và truyền mảng score vào trong hai hàm đó.

Để ý rằng tuy các tham số mảng không được khai báo với kí tự "&" trong phần

khai báo và định nghĩa hàm, nhưng thực chất chúng là các tham chiếu (thay vì

giá trị). Nói cách khác, khi chương trình thực thi, các hàm không tạo các bản

sao riêng của các mảng được truyền làm tham số (việc tạo bản sao này có thể có

chi phí rất cao về bộ nhớ.)

Vậy làm thế nào khi ta cần quy định rằng một hàm không được sửa đổi một

tham số mảng nào đó? (ví dụ không nên cho hàm average quyền sửa mảng

score.) Giải pháp mà C++ cung cấp là từ khóa const. Ta sửa phần khai báo

tham số của hàm average từ

float average(int score[], int size);

thành

float average(const int score[], int size);

Kết quả là trình biên dịch sẽ không chấp nhận các dòng lệnh sửa giá trị của

tham số mảng score ở bên trong hàm average. Nên sử dụng từ khóa const

cho tất cả các tham số mà hàm không có nhu cầu thay đổi.

Page 87: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

87

#include <iostream>using namespace std;

const int NUMBER_COURSES = 7;

void loadScore(int score[], int size) { for (int course = 0 ; course < size ; course++) { cout << "Enter the score for course #" << course << ": "; cin >> score[course]; }}

float average(int score[], int size) { float sum = 0; for (int course = 0 ; course < size ; course++) sum = sum + score[course]; return sum/size; }

int main(){ int score[NUMBER_COURSES];

loadScore(score, NUMBER_COURSES);

float averageScore = average(score, NUMBER_COURSES); cout << "The average score is " << averageScore;

return 0;}

Hình 5.3: Ví dụ về mảng làm tham số của hàm.

5.2. Mảng nhiều chiều

Cấu trúc mảng có thể có nhiều hơn một chiều. Mảng nhiều chiều có thể được

miêu tả là "mảng của các mảng". Chẳng hạn, có thể hình dung mảng hai chiều

là một bảng hai chiều gồm các dòng và các cột, mỗi ô lưu một giá trị thuộc cùng

một kiểu. Ví dụ, mảng anArray dưới đây là một mảng kích thước 3x4 gồm các

phần tử kiểu nguyên. anArray[2][1] là biểu thức truy nhập tới phần tử tại ô

mầu xám (dòng 2, cột 1).

Page 88: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

88

0 1 2 3 0 1 2 3 4 anArray 1 8 7 6 5 2 9 10 11 12

Một ví dụ khác về mảng hai chiều: để lưu một bàn cờ ca-rô, ta có thể dùng một

mảng hai chiều chứa các kí tự. Trong đó, kí tự '.' biểu diễn một ô trống trên bàn

cờ, các kí tự 'x' và 'o' lần lượt biểu diễn các kí hiệu trong trò chơi cờ ca rô.

Khai báo mảng hai chiều như sau:

const int BOARD_HEIGHT = 22;

const int BOARD_WIDTH = 26;

char board[BOARD_HEIGHT][BOARD_WIDTH];

Ta dùng hai chỉ số hàng và cột để truy nhập từng phần tử trong mảng, ví dụ:

board[10][2] = 'x';

Đoạn chương trình sau có thể được dùng để vẽ bàn cờ ra màn hình.

for (int row = 0, row < BOARD_HEIGHT, row++) {

for (int column = 0, column < BOARD_WIDTH, column ++)

cout << board[row][column];

cout << endl;

}

Cũng như mảng một chiều, mảng nhiều chiều cũng có thể dùng làm tham số cho

hàm.

Lưu ý, khi khai báo mảng nhiều chiều như là tham số của hàm, chúng ta phải

khai báo kích thước tất cả các chiều của mảng, ngoại trừ chiều đầu tiên có thể

bỏ trống.

void clearBoard(char board[][SCREEN_WIDTH]);

là một khai báo hợp lệ, còn

void clearBoard(char board[][]);

là một khai báo không hợp lệ.

Đối với các mảng có số chiều nhiều hơn 2, cú pháp cũng tương tự. Ví dụ

Page 89: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

89

void foo(int myArray[][3][4]);

5.3. Mảng và xâu kí tự

Chúng ta đã dùng đến các xâu kí tự, chẳng hạn "The average score is" trong

Hình 5.3. Các ngôn ngữ lập trình đều có cấu trúc xâu kí tự (string), trong đó

quy định cách tham chiếu tới từng phần tử trong xâu. Kèm theo đó là một bộ

các hàm và thủ tục để thực hiện các phép toán trên dữ liệu xâu, chẳng hạn như

xác định độ dài xâu, so sánh nội dung xâu, ghép xâu.

Bộ thư viện chuẩn C++ có thư viện string khá mạnh với kiểu dữ liệu string

dùng để xử lý và thao tác với các xâu kí tự. Tuy nhiên, về bản chất, xâu kí tự là

chuỗi các kí tự, nên ta có thể biểu diễn xâu kí tự bằng một mảng một chiều gồm

các phần tử kiểu char. Nhiều tiện ích trong thư viện chuẩn C++ sử dụng xâu kí

tự kiểu mảng, các chương trình C++ truyền thống cũng như nhiều chương trình

hiện đại vẫn sử dụng xâu kí tự dạng mảng. Do đó, ta vẫn cần học sử dụng thành

thạo kiểu dữ liệu này.

Ví dụ, mảng char sau đây gồm 20 phần tử kiểu char.

char greeting [20];

Về lý thuyết, mảng trên có thể chứa một chuỗi các ký tự với độ dài không quá

20. Nhưng ta còn có thể lưu tại đó các xâu kí tự có độ dài ngắn hơn, chẳng hạn

"Merry Christmas" hay "Hello". Xâu "The average score is" mà ta đã gặp

cũng có cấu trúc tương tự, điểm khác biệt chỉ là đây là một hằng xâu kí tự. Hằng

xâu kí tự có thể dùng làm giá trị khởi tạo tại lệnh khai báo mảng char. Ví dụ:

char greeting [20] = "Hello";

Do mảng char có thể lưu xâu kí tự có độ dài nhỏ hơn kích thước của mảng, ta

cần có cơ chế xác định điểm cuối hay độ dài của xâu. C++ sử dụng kí tự null

('\0') để đánh dấu kết thúc của xâu kí tự. Ví dụ, mảng greeting có thể dùng để

lưu xâu "Merry Christmas" hay "Hello" như trong Hình 5.4.

Page 90: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

90

greeting

M e r r y C h r i s t m a s \0

H e l l o \0

Hình 5.4: Xâu ký tự lưu trong mảng.

Lưu ý: các kí tự '\0' nằm ở cuối xâu để đánh dấu kết thúc xâu; còn các ô màu

xám kí hiệu các phần tử mảng nằm ngoài xâu (tuy vẫn nằm trong mảng) và có

giá trị không xác định. Khi khai báo mảng để lưu trữ một xâu kí tự, ta cần chú ý

khai báo kích thước mảng đủ lớn để chứa cả kí tự null nằm cuối xâu.

#include <iostream>using namespace std;

const int MAX_NAME_LENGTH = 100;

int main(){ char name[MAX_NAME_LENGTH]; cout << "What is your first name? "; cin >> name; cout << "Hi " << name << "!"; return 0;}

Kết quả chạy chương trình

What is your first name? EllenHi Ellen!

Hình 5.5: Ví dụ về xâu kí tự.

Hình 5.5 minh họa một chương trình ví dụ nhập một xâu kí tự và ghi ra màn

hình. Ta có thể thấy rằng mảng kí tự (hay xâu) name được sử dụng như là một

biến bình thường đối với các lệnh vào ra dữ liệu, cin cho phép đọc các xâu kí tự

không chứa kí tự trắng. Nếu muốn đảm bảo xâu nhập vào sẽ không dài quá kích

thước của mảng name, ta có thể dùng hàm setw để chặn số kí tự sẽ được đọc

vào. Ví dụ lệnh sau giới hạn số kí tự được đọc vào không vượt quá 99 và dành

vị trí thứ 100 trong mảng cho kí tự null đánh dấu cuối xâu.

Page 91: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

91

cin >> setw(100) >> name;

Nếu ta muốn nhập cả một dòng, trong đó có cả các kí tự trắng (tên đầy đủ

thường chứa dấu trắng phân cách giữa họ, đệm, và tên), ta có thể dùng lệnh

cin.getline() cho công việc này. Ví dụ, để đọc vào mảng name một dòng text

có độ dài tối đa 100, kết thúc bằng kí tự xuống dòng, ta dùng lệnh:

cin.getline( name, 100, '\n' );

cin.getline() đòi hỏi tham số thứ nhất là mảng để lưu xâu kí tự input, tham

số thứ hai là độ dài tối đa của input, và tham số thứ ba là kí tự đánh dấu cuối

input (kí tự này sẽ không được ghi vào mảng đích).

5.3.1. Khởi tạo giá trị cho xâu kí tự

Có hai cách khởi tạo giá trị cho xâu kí tự ngay tại lệnh khai báo:

1. Khởi tạo xâu theo cách khởi tạo mảng:

char greeting[20] = {'H', 'e', 'l', 'l', 'o', '\0'};

2. Dùng hằng giá trị để khởi tạo xâu:

char greeting[20] = "Hello";

Đối với cách thứ nhất, ta phải tự điền kí tự null ở cuối xâu. Còn cách thứ hai,

các biểu diễn giá trị xâu có dạng dùng cặp dấu nháy kép được tự động kèm

thêm kí tự null.

Lưu ý rằng chúng ta đang nói về công đoạn khởi tạo giá trị cho xâu chứ không

nói về lệnh gán giá trị cho cả xâu. Cũng như các loại mảng nói chung, đối với

mảng char (hay xâu ký tự), không được phép dùng một lệnh để gán trị hàng

loạt cho các phần tử trong mảng. Các lệnh sau sẽ gây lỗi khi dịch:

greeting = {'H', 'e', 'l', 'l', 'o', '\0'};

greeting = "Merry Christmas";

5.3.2. Thư viện xử lý xâu kí tự

Thư viện cstring của C++ (vốn là thư viện string.h của C) cung cấp một loạt

các hàm tiện ích cho việc xử lý xâu kí tự, chẳng hạn như strlen trả về độ dài

xâu, strcpy sao chép xâu, và strcmp so sánh xâu. Xem thêm chi tiết về thư

viện này trong Phụ lục C.

Page 92: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

92

5.4. Tìm kiếm và sắp xếp dữ liệu trong mảng

Tìm kiếm dữ liệu trong mảng là xác định xem một giá trị nào đó (được gọi là

khóa) có mặt trong mảng hay không, nếu có thì nó nằm ở vị trí nào. Sắp xếp

mảng là sắp đặt lại vị trí của các phần tử mảng theo một trong hai thứ tự: giá trị

khóa tăng dần hoặc giảm dần. Ví dụ, một danh sách từ có thể được sắp xếp theo

thứ tự từ điển, danh sách sinh viên được sắp xếp theo số sinh viên. Mục này giới

thiệu một số thuật toán cơ bản cho việc tìm kiếm và sắp xếp dữ liệu trong mảng.

5.4.1. Tìm kiếm tuyến tính

Thuật toán tìm kiếm tuyến tính (linear search) duyệt tuần tự từng phần tử

trong một mảng, so sánh khóa với mỗi phần tử. Khi gặp một phần tử khớp với

khóa, thuật toán kết thúc và trả về chỉ số của phần tử đó. Nếu duyệt đến hết

mảng mà vẫn không tìm thấy phần tử nào khớp với khóa, thuật toán kết luận là

khóa cần tìm không có trong mảng. Ví dụ, với mảng {3, 18, 2, 3, 10, 1}, nếu ta

thực hiện tìm kiếm với khóa bằng 10, thuật toán tìm kiếm tuyến tính sẽ duyệt

lần lượt từ phần tử đầu tiên (3) cho tới khi gặp phần tử thứ năm (10) và trả về

kết quả là chỉ số mảng của phần tử đó (với mảng C++ đánh số từ 0, kết quả của

thuật toán trong trường hợp này sẽ là 4).

Page 93: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

93

#include <iostream>using namespace std; int linearSearch(int a[], int size, int key) { for (int i = 0 ; i <= size ; i++) if (a[i] == key) return i; return -1;} int main(){ int array[100] = {10, 3, 4, 7, 2, 15}; int arraySize = 6; int searchKey; cout << "Enter the search key: "; cin >> searchKey; int pos = linearSearch(array, arraySize, searchKey); if (pos >= 0) cout << searchKey << " is found at entry " << pos; else cout << searchKey << " is not found"; return 0;}

Kết quả chạy chương trình:

Enter the search key: 33 is found at entry 1Enter the search key: 4545 is not found

Hình 5.6: Ví dụ về tìm kiếm tuyến tính.

Chương trình trong Hình 5.6 minh họa thuật toán tìm kiếm tuyến tính trong

mảng. Trong đó, thuật toán tìm kiếm được cài trong hàm linearSearch trả về

chỉ số của phần tử mảng khớp với khóa tìm kiếm hoặc trả về -1 nếu không tìm

thấy.

5.4.2. Tìm kiếm nhị phân

Thuật toán tìm kiếm tuyến tính được giới thiệu ở mục trước tuy đơn giản nhưng

có độ phức tạp O(n) nên cho hiệu quả không cao đối với dữ liệu lớn. Tìm kiếm

Page 94: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

94

nhị phân (binary search) là một thuật toán tìm kiếm phức tạp hơn nhưng cho

hiệu quả cao hơn. Tuy nhiên, nó còn đòi hỏi mảng đầu vào đã được sắp xếp.

Giả sử mảng đã được sắp xếp tăng dần, thuật toán tìm kiếm nhị phân hoạt động

như sau: Ở lần lặp đầu tiên, lấy phần tử nằm giữa mảng. Nếu phần tử đó khớp

với khóa thì thuật toán kết thúc. Nếu nó có giá trị lớn hơn khóa thì chắc chắn

nửa sau của mảng chứa toàn các phần tử lớn hơn khóa, và do đó không cần tìm

tại phần này. Nghĩa là vùng tìm kiếm nay đã được giới hạn chỉ còn là nửa đầu

của mảng. Còn nếu phần tử ở giữa có giá trị nhỏ hơn khóa, với lập luận tương tự

như trên, ta có giới hạn cần tìm kiếm thu hẹp lại chỉ còn là nửa sau của mảng.

Như vậy, tại mỗi lần lặp, thuật toán so sánh khóa với phần tử nằm giữa phần

mảng cần tìm kiếm để hoặc là thấy phần tử đó khớp với khóa hoặc là loại bỏ

một nửa mảng ra khỏi phạm vi cần tìm kiếm. Thuật toán kết thúc khi tìm thấy

một phần tử có giá trị bằng khóa hoặc khi phạm vi cần tìm kiếm bị giảm xuống

thành một mảng con có kích thước bằng 0.

Ví dụ, cho mảng {1, 3, 4, 7, 10, 12, 15}. Để tìm phần tử có giá trị 10, đầu tiên

thuật toán lấy phần tử nằm giữa mảng là 7 và so sánh với 10. Vì 7 nhỏ hơn 10,

thuật toán bỏ qua nửa đầu của mảng, phạm vi tìm kiếm thu hẹp thành đoạn

mảng {10, 12, 15}. Tại lần lặp tiếp theo, thuật toán lại lấy phần tử nằm giữa là

12 và so sánh với khóa 10. Vì 12 lớn hơn 10 nên nửa sau của đoạn {10, 12, 15}

bị bỏ qua, phạm vi tìm kiếm được thu hẹp lại chỉ còn đoạn mảng gồm một phần

tử {10}. Lần lặp thứ ba, phần tử nằm giữa mảng (phần tử duy nhất còn lại trong

đoạn cần tìm) có giá trị trùng với khóa nên thuật toán kết luận đã tìm thấy phần

tử có giá trị 10.

Thuật toán được minh họa bằng chương trình trong Hình 5.7.

Page 95: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

95

#include <iostream>using namespace std;

int binarySearch(int a[], int size, int key) { int start = 0, end = size; while (end > start) { cout << end << " " << start << endl; int middle = (end + start) / 2; if (a[middle] == key) return middle; else if (a[middle] > key) end = middle; else start = middle + 1; } return -1; }

int main(){ int array[100] = {1, 3, 4, 7, 10, 12, 15}; int arraySize = 7; int searchKey;

cout << "Enter the search key: "; cin >> searchKey;

int pos = binarySearch(array, arraySize, searchKey); if (pos >= 0) cout << searchKey << " is found at entry " << pos; else cout << searchKey << " is not found";

return 0;

}

Hình 5.7: Ví dụ về tìm kiếm nhị phân.

5.4.3. Sắp xếp chọn

Sắp xếp chọn (selection sort) là một trong những thuật toán sắp xếp đơn giản

nhất, nhưng lại có hiệu quả không cao đối với dữ liệu có kích thước lớn (mảng

có số phần tử lớn). Hoạt động của thuật toán này rất đơn giản. Ở lần lặp thứ

nhất, ta tìm phần tử nhỏ nhất của toàn mảng rồi tráo vị trí của nó với phần tử

đầu tiên (giả thiết rằng ta đang cần sắp xếp mảng theo thứ tự tăng dần). Lần lặp

thứ 2, ta tìm phần tử nhỏ thứ nhì trong toàn mảng, nghĩa là phần tử nhỏ nhất của

đoạn mảng tính từ phần tử thứ hai trở đi, rồi tráo vị trí của nó với phần tử thứ

Page 96: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

96

hai trong mảng. Thuật toán tiếp tục cho đến lần lặp cuối cùng – khi phần tử lớn

nhì mảng được chọn ra và tráo với phần tử có chỉ số sát cuối, dẫn đến việc phần

tử lớn nhất mảng nằm tại vị trí cuối mảng. Tổng quát, kết quả của lần lặp thứ i

là phần tử nhỏ thứ i của mảng được xếp vào vị trí thứ i trong mảng.

Hình 5.8 minh họa thuật toán sắp xếp chọn.

#include <iostream>using namespace std;

void selectionSort(int a[], int size) { for (int i = 0; i < size - 1; i++) { // find the smallest entry in a[i],...a[size-1] int smallest = i; for (int j = i + 1; j < size; j++) if (a[j] < a[smallest]) smallest = j; // swap a[i] with that smallest entry int temp = a[i]; a[i] = a[smallest]; a[smallest] = temp; }}

int main(){ int array[100], arraySize; cout << "Enter array size: "; cin >> arraySize; cout << "Enter the array: "; for (int i = 0; i < arraySize; i++) cin >> array[i];

selectionSort(array, arraySize); cout << "Sorted array: \n"; for (int i = 0; i < arraySize; i++) cout << array[i] << " ";

return 0;}

Hình 5.8: Sắp xếp chọn.

Page 97: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

97

Bài tập

1. Nhập vào từ bàn phím một danh sách các số X1, X2,…, X2n-1, X2n. Hãy

viết một hàm tính giá trị của biểu thức sau đây (hiện kết quả tìm được ra

màn hình):

( X1 + X2n) * (X2 + X2n-1) *…* (Xn + Xn+1)

2. Nhập vào từ bàn phím danh sách tên các bạn sinh viên trong lớp. Hãy tìm

hiểu và cài đặt thuật toán sắp xếp nổi bọt (bubble sort) để sắp xếp danh

sách tên theo thứ tự tăng dần. Hiện danh sách sau khi đã sắp xếp ra màn

hình.

3. Nhập vào từ bàn phím một danh sách các số nguyên. Hãy hiện ra màn

hình tất cả các bộ số (a, b, c) mà tổng của chúng bằng 25.

4. Một quả đồi được chia thành một lưới ô vuông có kích thước m*n ô

vuông. Mỗi ô vuông chứa một số nguyên đại diện cho độ cao tại ô vuông

đó. Hãy tìm và hiện ra màn hình:

Ô vuông có độ cao lớn nhất

Ô vuông có độ cao nhỏ nhất

Tất cả các hình vuông có kích thước 3*3, mà ô vuông ở giữa cao hơn

các ô vuông xung quanh

Tất cả các hình vuông có kích thước 3*3, mà ô vuông ở giữa thấp hơn

các ô vuông xung quanh

5. Nhập vào từ bàn phím một xâu kí tự. Sử dụng kiểu dữ liệu char[] để ghi

ra màn hình các kết quả sau:

Độ dài của xâu kí tự vừa nhập

Xâu kí tự sau khi đã loại bỏ tất cả các dấu „?‟, „!‟

Xâu kí tự sau khi chèn vào giữa xâu từ “C++”.

Xâu kí tự sau khi đã xóa bỏ 5 kí tự nằm ở giữa

6. Nhập vào từ bàn phím một danh sách tên các bạn sinh viên trong lớp, hãy

đếm xem có bao nhiêu bạn có tên với chữ cái bắt đầu là „T‟, „C‟, „V‟, „A‟,

Page 98: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

98

„Q‟. Tìm tên có số bạn trùng nhau là nhiều nhất. Hiện kết quả tìm được

ra màn hình.

7. Nhập vào từ bàn phím một xâu ký tự là danh sách tên các sinh viên. Hai

tên đứng liền nhau được cách nhau bởi ít nhất một dấu cách. Hãy tính

xem có bao nhiêu sinh viên trong danh sách. Ví dụ:

Dữ liệu vào Kết quả tương ứng

Vinh tuan vinh blah blah 5

8. Nhập một xâu ký tự từ bàn phím. Hãy chuẩn hoá xâu bằng cách loại bỏ

các ký tự không phải chữ cái, dấu cách, dấu phảy, dấu chấm. Các từ cách

nhau bởi đúng một dấu cách và chỉ viết hoa chữ cái đầu tiên. Ví dụ:

Dữ liệu vào Kết quả tương ứng

tOi te1N !LA ng2uyen v3an aN;. Toi Ten La Nguyen Van An.

9. Nhập vào từ bàn phím 1 dãy kí tự gồm các kí từ 0..9. Hãy tính số lượng

kí tự 0, số lượng kí tự 1,…, số lượng kí tự 9 trong xâu vừa nhập vào. Hiện

kết quả ra màn hình.

10. Nhập từ bàn phím 2 số nguyên lớn (số lượng chữ số lớn hơn 100). Hãy

viết chương trình tính và hiện kết quả ra màn hình:

Tổng của hai số trên

Hiệu của hai số trên

Tích của hai số trên

11. Nhập từ bàn phím 1 dãy các chữ số từ 0 đến 9. Hãy viết chương trình đảo

chỗ các chữ số để thu được (hiện kết quả ra màn hình):

Số nguyên có giá trị lớn nhất

Số nguyên có giá trị nhỏ nhất

Ví dụ: Dãy đầu vào: 1312013

Số nguyên có giá trị lớn nhất: 3321110

Số nguyên có giá trị nhỏ nhất: 0111233

Page 99: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

99

Chương 6. Con trỏ và bộ nhớ

Con trỏ là một công cụ mà một số ngôn ngữ lập trình bậc cao, đặc biệt là C và

C++, cung cấp để cho phép chương trình quản lý và tương tác trực tiếp với bộ

nhớ. Nó giúp chương trình linh động và hiệu quả. Tuy nhiên, sử dụng con trỏ

cũng dễ dẫn đến sai sót và khó phát hiện ra lỗi.

6.1. Bộ nhớ máy tính

Để hiểu về con trỏ, trước tiên ta sẽ tìm hiểu về bộ nhớ máy tính. Ta có thể hình

dung bộ nhớ máy tính là một loạt các ô nhớ nối tiếp nhau, mỗi ô nhớ có kích

thước nhỏ nhất là một byte. Các ô nhớ đơn này được đánh số liên tục, ô nhớ sau

có số thứ tự hơn số thứ tự của ô nhớ liền trước là 1. Tức là ô nhớ 1505 sẽ đứng

sau ô nhớ 1504 và đứng trước ô nhớ 1506 (xem Hình 6.1). Số thứ tự của ô nhớ

có thể được gọi là địa chỉ của ô nhớ đó.

Hình 6.1: Biến greeting– xâu kí tự "Hello"– trong bộ nhớ.

6.2. Biến và địa chỉ của biến

Biến trong một chương trình là một vùng bộ nhớ được dùng để lưu dữ liệu. Việc

truy nhập dữ liệu tại các ô nhớ đó được thực hiện thông qua tên biến. Tức là, ta

không phải quan tâm đến vị trí vật lý của ô bộ nhớ.

Khi một biến được khai báo, phần bộ nhớ cần thiết sẽ được cấp phát cho nó tại

một vị trí cụ thể trong bộ nhớ, vị trí đó được gọi là địa chỉ bộ nhớ của biến đó.

Ví dụ, trong Hình 6.1, biến greeting có địa chỉ là 1504. Thông thường, hệ điều

hành sẽ quyết định vị trí của biến trong bộ nhớ khi chương trình chạy.

Địa chỉ

bộ nhớ

Nội dung

ô nhớ cỡ 1 byte

Tên biến

1503 1504 'H' greeting

1505 'e' 1506 'l' 1507 'l' 1508 'o' 1509 '\0' 1510 .. .. ..

Page 100: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

100

Để lấy địa chỉ của một biến, trong C++ ta sử dụng toán tử tham chiếu địa chỉ

(&). Khi đặt toán tử tham chiếu trước tên của một biến, ta có một biểu thức có

giá trị là địa chỉ của biến đó trong bộ nhớ. Ví dụ:

short apples = 9; //khai báo biến apples có giá trị 9

cout << apples; //hiện ra số 9 là giá trị của apples

cout << &apples; //hiện ra địa chỉ của biến apples

Trong thực tế, trước khi chương trình chạy, chúng ta không thể biết được địa chỉ

của biến apples.

6.3. Biến con trỏ

Biến con trỏ là một loại biến đặc biệt, được dùng để lưu giữ địa chỉ của các

vùng nhớ. Tức là, giá trị của một biến con trỏ là địa chỉ của một ô nhớ trong bộ

nhớ. Hay nói một cách hình tượng, biến con trỏ chỉ đến một ô nhớ trong bộ

nhớ, nó được dùng để gián tiếp truy nhập đến ô nhớ đó.

Ví dụ, giả sử ta có biến apples kiểu int nằm tại địa chỉ 0x2a52ec, biến con trỏ

ptrApples đang chỉ tới biến apples. Điều đó có nghĩa biến ptrApples đang có

giá trị 0x2a52ec và điều quan trọng là có thể dùng ptrApples để gián tiếp truy

nhập tới biến apples bằng cách dùng biểu thức (*ptrApples), xem Hình 6.2.

Hình 6.2. Con trỏ ptrApples chỉ tới biến apples, gián tiếp truy nhập ô nhớ apples

Cũng như các loại biến khác, biến con trỏ cần được khai báo trước khi sử dụng.

Với các biến apples và ptrApples nói trên, có thể khai báo bằng lệnh sau:

int *ptrApples, apples;

trong đó, biến ptrApples được khai báo thuộc kiểu int * (con trỏ tới một giá

trị int), còn biến apples được khai báo thuộc kiểu int. Lưu ý là dấu * kí hiệu

kiểu con trỏ chỉ áp dụng duy nhất cho biến nằm ngay sau nó chứ không áp dụng

cho tất cả các biến nằm sau, mỗi biến con trỏ trong lệnh khai báo cần có một

dấu * đặt trước nó. Ví dụ, lệnh sau khai báo hai biến con trỏ:

ptrApples apples

Page 101: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

101

int *ptrApples, *ptrPineapples;

Con trỏ cần được khởi tạo bằng giá trị 0 (hay NULL) hoặc một địa chỉ ngay tại

lệnh khai báo hoặc bằng một lệnh gán. Một con trỏ có giá trị 0 hay NULL (một

hằng số tương đương với 0 được định nghĩa trong tệp thư viện iostream) không

chỉ tới ô nhớ nào và được gọi là con trỏ null. Một con trỏ chưa được khởi tạo

chỉ đến một vùng bộ nhớ không xác định hoặc chưa được khởi tạo, việc vô tình

dùng con trỏ chưa được khởi tạo để truy nhập bộ nhớ là một lỗi lập trình khó

phát hiện, có thể dẫn đến dữ liệu chương trình bị sửa làm chương trình chạy sai

hoặc gây sự cố làm sập chương trình. Đây là một trong những đặc điểm không

an toàn của ngôn ngữ C/C++. Do đó, lập trình viên nên chú ý khởi tạo con trỏ

ngay sau khi khai báo.

Để truy nhập dữ liệu tại ô nhớ mà biến con trỏ chỉ đến ta sử dụng toán tử *. Ví

dụ, ta dùng biểu thức *ptrApples để truy nhập biến apples mà ptrApples

đang trỏ tới. Có thể dùng biểu thức *ptrApples ở bên trái cũng như bên phải

của phép gán, nghĩa là để đọc cũng như để ghi giá trị vào ô nhớ apples. Nếu

dùng phép toán truy nhập * cho con trỏ null, chương trình sẽ gặp lỗi run-time và

dừng lập tức.

Hình 6.4 minh họa về việc khai báo và sử dụng biến con trỏ. Trong ví dụ, ta

khai báo một biến con trỏ ptrApples dùng để lưu giữ địa chỉ ô nhớ của biến

apples. Lệnh

ptrApples = &apples;

sẽ gán địa chỉ của biến apples cho biến con trỏ ptrApples và tạo hiệu ứng

tương tự như trong Hình 6.2 và Hình 6.3

Hình 6.3: Biến con trỏ ptrApples có giá trị là địa chỉ của biến apples.

Lưu ý, các tác động trên ô nhớ biến con trỏ ptrApples chỉ đến cũng chính là

các tác động được thực hiện trên biến apples và ngược lại. Cụ thể là lệnh

Địa chỉ

bộ nhớ

Nội dung

ô nhớ cỡ 4 byte Tên biến

.. 0x2a52dc .. 0x2a52e0 0x2a52ec ptrApples

0x2a52e4 .. 0x2a52e8 .. 0x2a52ec 0x000009 apples ..

0x2a52f0 ..

Page 102: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

102

apples++;

sẽ tăng giá trị của biến apples lên một, đồng nghĩa với việc tăng giá trị ở ô nhớ

do biến con trỏ ptrApples chỉ đến lên 1.

Tương tự, lệnh

*ptrApples += 1;

sẽ tăng giá trị tại ô nhớ mà biến con trỏ ptrApples thêm 1, đồng nghĩa với việc

tăng giá trị của biến apples thêm 1.

Chương trình trong Hình 6.4 còn minh họa việc in giá trị của con trỏ ra màn

hình. Có thể dùng cout để in giá trị của con trỏ như đối với nhiều kiểu dữ liệu

khác. Tuy nhiên, định dạng của giá trị in ra phụ thuộc vào từng trình biên dịch,

một số hệ thống in ra các giá trị con trỏ ở dạng hệ cơ số 16 (như trong hình), các

hệ thống khác sử dụng định dạng hệ thập phân.

Page 103: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

103

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int apples = 9; cout << "apples: " << apples << endl; cout << "Address of apples: " << &apples << endl; int *ptrApples; ptrApples = &apples; cout << "ptrApples: " << ptrApples << endl; cout << "Value at ptrApples: " << *ptrApples << endl;

apples ++; cout << "apples: " << apples << endl; cout << "Value at ptrApples: " << *ptrApples << endl; *ptrApples += 1; cout << "apples: " << apples << endl; cout << "Value at ptrApples: " << *ptrApples << endl;

return 0;}

Kết quả chạy chương trình

apples: 9Address of apples: 0x2a52ecptrApples: 0x2a52ecValue at ptrApples: 9apples: 10Value at ptrApples: 10apples: 11Value at ptrApples: 11

Hình 6.4: Khai báo và sử dụng biến con trỏ.

Một trong những ứng dụng thường của con trỏ là làm con chạy trên chuỗi các ô

nhớ liên tiếp, chẳng hạn như mảng. Chương trình trong Hình 6.5 minh họa việc

đó. Trong chương trình là một vòng lặp có nhiệm vụ duyệt và in ra toàn bộ các

phần tử của mảng score. Con chạy trong vòng lặp là con trỏ ptr với giá trị ban

đầu là địa chỉ của phần tử đầu tiên mảng score. Tại mỗi lần lặp, phần tử mảng

Page 104: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

104

hiện được ptr trỏ tới được đọc bằng biểu thức *ptr rồi in ra màn hình, sau đó

ptr được tăng thêm 1 (ptr ++) với hiệu quả là nó sẽ trỏ tới phần tử tiếp theo

trong mảng. Vòng lặp dừng khi ptr bắt đầu trỏ vượt ra xa hơn vị trí phần tử

cuối mảng (điều kiện lặp là ptr <= &score[6]).

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int score[7] = {1,2,3,4,5,6,7};

for (int *ptr = &score[0]; ptr <= &score[6]; ptr ++) cout << *ptr << " ";

return 0;}

Hình 6.5: Dùng con trỏ làm con chạy duyệt mảng.

Hình 6.5 còn minh họa các phép toán cộng và trừ con trỏ với một số nguyên và

các phép so sánh con trỏ.

Có thể dùng các phép so sánh bằng/khác và lớn hơn/nhỏ hơn cho con trỏ. Các

phép toán này so sánh các địa chỉ lưu trong các con trỏ. Một phép so sánh

thường gặp là so sánh một con trỏ với 0 để kiểm tra xem đó có phải là con trỏ

null hay không. Các phép so sánh lớn hơn/nhỏ hơn đối với các con trỏ là vô

nghĩa trừ khi chúng đang trỏ tới các phần tử của cùng một mảng. Trong trường

hợp đó, kết quả của phép so sánh cho thấy con trỏ này trỏ tới phần tử mảng có

chỉ số cao hơn con trỏ kia. Trong Hình 6.5, con trỏ ptr được so sánh với địa chỉ

của phần tử cuối cùng xem nó đã chạy đến vị trí đó hay chưa.

Các phép toán cộng trừ đối với con trỏ không có nghĩa cộng hay trừ chính số

nguyên đó vào giá trị lưu trong biến con trỏ. Thực tế, giá trị được cộng vào hay

trừ đi là số nguyên đó nhân với kích thước của kiểu dữ liệu mà con trỏ trỏ tới.

Ví dụ, giả sử con trỏ ptr kiểu int đang trỏ tới phần tử mảng score[2] và kiểu

dữ liệu int có kích thước 4 byte, lệnh ptr += 3; sẽ gán cho ptr giá trị 4020

(nghĩa là 4008 + 3 * 4) làm nó trỏ tới score[5]. Xem minh họa tại Hình 6.6.

Page 105: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

105

score[0] score[6]score[5]score[4]score[3]score[2]score[1]

ptr

4004 40204016401240084000 40284024

địa chỉ bộ nhớ

Hình 6.6. Phép cộng 3 vào con trỏ ptr.

6.4. Mảng và con trỏ

Trong C++, mảng và con trỏ có quan hệ chặt chẽ với nhau và gần như có thể

dùng thay thế cho nhau. Một cái tên mảng có thể được coi như một hằng con

trỏ, có thể dùng biến con trỏ trong bất cứ cú pháp nào của mảng.

Giả sử, khi khai báo một mảng

int score[7];

ta đã khai báo một vùng nhớ liên tiếp gồm 7 ô, mỗi ô chứa một số nguyên.

Trong C++, mảng được cài đặt bằng con trỏ. Cụ thể là, tên mảng (score) có thể

được coi là một hằng con trỏ kiểu int trỏ tới ô nhớ đầu tiên trong mảng

(score[0]).

Để truy cập đến phần tử có chỉ số i trong mảng score, ta có thể sử dụng

score[i] hoặc *(score + i). Ví dụ, *score và score[0] đều có ý nghĩa là

phần tử đầu tiên trong mảng.

Cách dùng kiểu con trỏ để thao tác với mảng được minh họa trong Hình 6.7.

Trong đó vòng lặp lần lượt in ra các phần tử trong mảng, với biểu thức *(score

+ i) được dùng để truy nhập phần tử thứ i trong mảng.

Page 106: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

106

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int score[7] = {1,2,3,4,5,6,7};

for (int count = 0; count < 7; count ++) cout << *(score+count) << " ";

return 0;

}

Hình 6.7: Sử dụng tên mảng như con trỏ.

Ngược lại, nếu ta có ptrScore là một con trỏ kiểu int, lệnh sau gán giá trị của

score cho ptrScore làm nó trỏ tới vị trí đầu tiên trong mảng score.

ptrScore = score;

Lệnh trên tương đương với

ptrScore = &score[0];

Sau đó ta có thể dùng biến con trỏ ptrScore để thao tác mảng score như thể

ptrScore là một tên mảng.

Hình 6.8 minh họa việc đó, đây chỉ là sửa đổi nhỏ từ chương trình trong Hình

6.7 để thực hiện cùng một nhiệm vụ nhưng lại dùng ptrScore với cú pháp

mảng thay vì dùng tên mảng score với cú pháp con trỏ.

Page 107: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

107

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int score[7] = {1,2,3,4,5,6,7}; int *ptrScore = score;

for (int count = 0; count < 7; count ++) cout << ptrScore[count] << “ ”;

return 0;

}

Hình 6.8: Sử dụng con trỏ như tên mảng.

6.5. Bộ nhớ động

Trong tất cả các chương trình ví dụ trước, ta mới chỉ dùng bộ nhớ trong phạm vi

các biến được khai báo trong mã nguồn với kích thước được xác định trước khi

chương trình chạy. Với C++, tất cả các biến được khai báo trong chương trình

đều nằm trong bộ nhớ tĩnh và có kích thước xác định trước. Nếu chúng ta cần

đến lượng bộ nhớ mà kích thước chỉ có thể biết được khi chương trình chạy thì

sao? Chẳng hạn khi chương trình của ta cần làm việc với một mảng mà kích

thước của nó không xác định được khi chương trình chưa chạy, và ta cũng

không thể ước lượng độ dài tối đa mà cần đọc dữ liệu vào để xác định độ dài

cần thiết.

Giải pháp là sử dụng bộ nhớ động. Không như các biến thông thường chỉ cần

khai báo, các biến nằm trong vùng bộ nhớ động cần được cấp phát khi muốn sử

dụng và giải phóng một cách tường minh khi không còn được dùng đến.

Trong C++, con trỏ cùng với các toán tử new và delete là công cụ để xin cấp

phát và giải phóng dữ liệu nằm trong bộ nhớ động.

6.5.1. Cấp phát bộ nhớ động

Khi một biến con trỏ được khai báo, ví dụ

int *ptrApples;

giá trị của biến con trỏ ptrApples chưa được xác định, nghĩa là nó chưa trỏ vào

một vùng nhớ xác định trước nào. Chúng ta cũng có thể xin cấp phát một vùng

Page 108: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

108

nhớ động, và vùng nhớ này được quản lý (trỏ tới) bởi biến con trỏ ptrApples

bằng cách sử dụng toán tử cấp phát bộ nhớ động new như sau:

ptrApples = new int;

Lênh này sẽ cấp phát một vùng nhớ đủ cho một giá trị kiểu int, địa chỉ vùng nhớ

này được ghi vào biến ptrApples. Quá trình cấp phát bộ nhớ này được thực

hiện trong quá trình chạy chương trình và được gọi là cấp phát bộ nhớ động.

Sau lệnh cấp phát trên, ta có thể tiến hành lưu giữ, cập nhật giá trị ở vùng nhớ

này thông qua địa chỉ của nó (hiện lưu tại biến ptrApples). Nếu lệnh cấp phát

không thành công (chẳng hạn vì không còn đủ bộ nhớ), ptrApples sẽ nhận giá

trị 0. Lập trình viên cần chú ý đề phòng những tình huống này.

6.5.2. Giải phóng bộ nhớ động

Do lượng bộ nhớ động là có hạn, dữ liệu đặt trong bộ nhớ động nên được giải

phóng khi dùng xong để dành bộ nhớ cho các yêu cầu cấp phát tiếp theo.

Việc giải phóng vùng nhớ được thực hiện bằng toán tử delete. Ví dụ, lệnh sau

giải phóng ô nhớ kiểu int mà con trỏ ptrApples hiện đang trỏ tới.

delete ptrApples;

Sau khi được giải phóng, vùng nhớ đó có thể được tái sử dụng cho một lần cấp

phát bộ nhớ động khác.

Hình 6.9 minh họa về việc cấp phát, sử dụng, và giải phóng bộ nhớ động.

Page 109: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

109

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int *ptrApples; ptrApples = new int; *ptrApples = 5; cout << "value at ptrApples: " << *ptrApples << endl; *ptrApples += 2; cout << "value at ptrApples: " << *ptrApples << endl;

delete ptrApples;

return 0;

}

Hình 6.9: Cấp phát, sử dụng và giải phóng bộ nhớ động.

Lưu ý: nếu vì sơ xuất của người lập trình hay vì lý do nào đó mà tại một thời

điểm nào đó chương trình không còn lưu hoặc có cách nào tính ra địa chỉ của

một vùng nhớ được cấp phát động, thì vùng bộ nhớ này được xem là bị "mất".

Nghĩa là chương trình không có cách gì truy nhập hay giải phóng vùng bộ nhớ

này nữa, sau khi chương trình kết thúc nó mới được hệ điều hành thu hồi. Ví dụ,

đoạn chương trình sau làm thất thoát phần bộ nhớ được cấp phát do lệnh new

thứ nhất, do con trỏ duy nhất giữ địa chỉ của ô nhớ đó đã bị gán giá trị mới, ta

không còn có thể lấy lại được địa chỉ của vùng nhớ đó để sử dụng hoặc giải

phóng.

ptr1 = new int;

ptr2 = new int;

ptr1 = ptr2;

6.6. Mảng động và con trỏ

Trong các chương trình ví dụ từ trước đến giờ, kích thước của mảng phải được

xác định trước khi khai báo. Tức là, kích thước của mảng là một hằng số cho

trước. Trong nhiều trường hợp, chúng ta khó có thể xác định trước được kích

thước của mảng ngay từ khi lập trình, mà kích thước này phụ thuộc vào dữ liệu

đầu vào cũng như quá trình chạy chương trình.

Page 110: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

110

Để khắc phục tình trạng trên, C++ cho phép chúng ta sử dụng con trỏ để tạo

mảng có kích thước động nằm trong bộ nhớ động. Những mảng được tạo theo

cách đó được gọi là mảng động, bởi kích thước mảng và việc cấp phát bộ nhớ

cho mảng được xác định và tiến hành trong quá trình chạy chương trình. Việc

khai báo mảng động được tiến hành thành hai bước:

1. Khai báo con trỏ mảng: Trước tiên, khai báo một biến con trỏ:

data_type *array_name;

trong đó data_type là kiểu dữ liệu của mảng, array_name là tên mảng.

2. Xin cấp phát bộ nhớ: Sau khi khai báo, chúng ta xin cấp phát bộ nhớ cho

biến con trỏ bằng toán tử new [] theo cấu trúc sau:

array_name = new data_type [size];

hệ thống sẽ cấp cho chương trình một vùng nhớ liên tiếp có thể chứa được size

phần tử có kiểu data_type. Địa chỉ của phần tử đầu tiên được chỉ đến bởi biến

array_name. Lưu ý, size có thể là một hằng số, hoặc là một biến.

Sau khi đã cấp phát bộ nhớ thành công, ta có thể đối xử với biến con trỏ

array_name như một mảng thông thường với size phần tử. Tức là, để truy cập

đến phần tử thứ i trong mảng, ta có thể sử dụng array_name[i] hoặc

*(array_name + i).

Giải phóng bộ nhớ: Khi chúng ta không sử dụng đến mảng động nữa, chúng ta

phải tiến hành giải phóng bộ nhớ đã xin cấp phát bằng cách sử dụng toán tử

delete [] như sau:

delete [] array_name;

Lưu ý khi giải phóng bộ nhớ động cho một mảng, nếu ta sử dụng lệnh

delete array_name;

thì chỉ có ô nhớ array_name[0] được giải phóng, còn các ô nhớ tiếp theo của

mảng không được giải phóng.

Hình 6.10 minh họa việc sử dụng mảng động để tính tổng điểm của một danh

sách sinh viên nhập vào từ bàn phím. Số lượng sinh viên không được xác định

trước mà được người dùng nhập vào từ bàn phím.

Page 111: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

111

#include <iostream>

using namespace std;

int main(int argc, char *argv[]){ int numberCourses; cout << "Enter the number of courses: "; cin >> numberCourses;

int *courses; courses = new int[numberCourses];

for (int count = 0; count < numberCourses; count++) { cout << "Enter the mark of course #" << count << ": "; cin >> courses[count]; }

int totalMark = 0; for (int count = 0; count < numberCourses; count++) totalMark += courses[count]; cout << "Total mark: " << totalMark << endl;

delete [] courses;

return 0;

}

Hình 6.10: Mảng động và con trỏ.

6.7. Truyền tham số là con trỏ

Như đã được giới thiệu trong Mục 4.5, C++ có hai phương pháp truyền dữ liệu

vào trong hàm: truyền bằng giá trị và truyền bằng tham chiếu. Trong phương

pháp truyền bằng tham chiếu, Mục 4.5 đã nói về cách dùng đối số là tham

chiếu. Trong mục này, chúng tôi sẽ giới thiệu kiểu truyền bằng tham chiếu còn

lại: dùng đối số là con trỏ.

Còn nhớ rằng các đối số là biến tham chiếu cho phép hàm được gọi sửa giá trị

dữ liệu của hàm gọi. Các đối số tham chiếu còn cho phép chương trình truyền

lượng dữ liệu lớn vào hàm mà không phải chịu chi phí cho việc sao chép dữ liệu

như khi truyền bằng giá trị. Cũng như vậy, các đối số là con trỏ có thể được sử

Page 112: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

112

dụng để sửa đổi các biến của hàm gọi hoặc để truyền các đối tượng dữ liệu lớn

trong khi tránh được việc sao chép dữ liệu.

Trong C++, con trỏ được dùng kèm với toán tử * để thực hiện cơ chế truyền

bằng tham chiếu. Khi gọi một hàm, các đối số cần sửa giá trị được truyền vào

trong hàm bằng địa chỉ của chúng (sử dụng toán tử địa chỉ &).

#include <iostream>

using namespace std;

void swap (int *x, int *y) { int tmp = *x; *x = *y; *y = tmp;}

int main(int argc, char *argv[]){ int a, b; cout << "Enter the numbers a and b: "; cin >> a >> b;

cout << "Before swapping, a = " << a << ", b = " << b << endl; swap (&a, &b); cout << "After swapping, a = " << a << ", b = " << b << endl;

return 0; }

Kết quả chạy chương trình

Enter the numbers a and b: 3 5Before swapping, a = 3, b = 5After swapping, a = 5, b = 3

Hình 6.11: Tham số của hàm là con trỏ.

Chương trình trong Hình 6.11 minh họa cách sử dụng con trỏ để truyền dữ liệu

vào hàm qua tham chiếu. Hàm swap có nhiệm vụ tráo đổi giá trị của hai biến.

Nó nhận các tham số x và y là con trỏ tới các biến cần đổi giá trị, rồi thông qua

x và y truy nhập gián tiếp tới các biến đó để sửa giá trị. Còn tại hàm main, khi

Page 113: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

113

muốn đổi giá trị của hai biến a và b, địa chỉ của hai biến này được truyền vào

khi gọi hàm swap.

Trong Hình 6.12, biến con trỏ courses được truyền vào hàm getMark và

calculateMark. Dẫn đến tham số hình thức của hàm getMark và

calculateMark chính là con trỏ tới mảng courses trong hàm main.

#include <iostream>

using namespace std;

void getMark (int *courses, int numberCourses) { for (int count = 0; count < numberCourses; count++) { cout << "Enter the mark of course # " << count << ": "; cin >> courses[count]; }}

int calculateMark (const int *courses, int numberCourses) { int totalMark = 0; for (int count = 0; count < numberCourses; count++) totalMark += courses[count]; return totalMark;}

int main(int argc, char *argv[]){ int numberCourses; cout << "Enter the number of courses: "; cin >> numberCourses; int *courses; courses = new int[numberCourses];

getMark (courses, numberCourses); int totalMark = calculateMark (courses, numberCourses); cout << "Total mark: " << totalMark << endl; delete courses;

return 0; }

Hình 6.12: Tham số của hàm là con trỏ.

Page 114: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

114

Trong những trường hợp ta muốn dùng con trỏ để truyền lượng lớn dữ liệu vào

hàm nhưng lại không muốn cho hàm sửa đổi dữ liệu, ta có thể dùng từ khóa

const để thiết lập quyền của hàm đối với tham số. Xem ví dụ hàm

calculateMark ở Hình 6.12, trong đó, từ khóa const ở phía trước tham số hình

thức courses

int calculateMark (const int *courses, int numberCourses)

quy định rằng hàm calculate phải coi dữ liệu int mà courses trỏ tới là hằng

và không được phép sửa đổi.

Về cách sử dụng từ khóa const khi khai báo một biến con trỏ, ta có 4 lựa chọn:

1. không quy định con trỏ hay dữ liệu được trỏ tới là hằng

float * ptr;

2. quy định dữ liệu được trỏ tới là hằng, con trỏ thì được sửa đổi.

const float * const ptr;

3. quy định dữ liệu trỏ tới không phải là hằng, nhưng biến con trỏ thì là

hằng

float * const ptr;

4. quy định cả con trỏ lẫn dữ liệu nó trỏ tới đều là hằng.

const float * const ptr;

Sử dụng const cho những hoàn cảnh thích hợp là một phần của phong cách lập

trình tốt. Nó giúp giảm quyền hạn của hàm xuống tới mức vừa đủ, chẳng hạn

một hàm chỉ có nhiệm vụ đọc dữ liệu và tính toán thì không nên có quyền sửa

dữ liệu, từ đó giảm nguy cơ của hiệu ứng phụ không mong muốn. Đây chính là

một trong các cách thi hành nguyên tắc quyền ưu tiên tối thiểu (the principle

of least privilege), chỉ cấp cho hàm quyền truy nhập dữ liệu vừa đủ để thực hiện

nhiệm vụ của mình.

Một điểm quan trọng khác cần lưu ý là tình trạng con trỏ có giá trị null hoặc

không xác định do chưa gán bằng địa chỉ của biến nào. Nếu con trỏ ptr có giá

trị null hoặc không xác định thì việc truy nhập *ptr sẽ dẫn đến lỗi run-time

hoặc lỗi logic cho chương trình. Ta cần chú ý khởi tạo giá trị của các biến con

trỏ và kiểm tra giá trị trước khi truy nhập. Ngoài ra, còn có một lời khuyên là

nên sử dụng biến tham chiếu thay cho con trỏ bất cứ khi nào có thể để tránh

trường hợp con trỏ null hoặc có giá trị không xác định.

Page 115: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

115

Bài tập

1. Trình bày sự khác biệt, ưu điểm, nhược điểm giữa biến tĩnh và biến động.

2. Viết một chương trình với hai biến con trỏ x, y. Sử dụng hai biến x,y để

lưu hai số thực được nhập từ bàn phím. Tính tổng của x, và y và hiện kết

quả ra màn hình.

3. Tính giá trị của apples, *ptrApp, *ptrApp2 của đoạn mã sau:

int apples;

int *ptrApp = &apples;

int *ptrApp2 = ptrApp;

apples += 2;

*ptrApp --;

*ptrApp2 += 3;

4. Xác định kết quả của đoạn mã sau:

int *p1 = new int;

int *p2;

*p1 = 5;

p2 = p1;

cout << "*p1 = " << *p1 << endl;

cout << "*p2 = " << *p2 << endl;

*p2 = 10;

cout << "*p1 = " << *p1 << endl;

cout << "*p2 = " << *p2 << endl;

p1 = new int;

*p1 = 20;

cout << "*p1 = " << *p1 << endl;

cout << "*p2 = " << *p2 << endl;

5. Hãy nêu các tình huống có thể xảy ra đối với đoạn chương trình sau:

Page 116: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

116

int *p1 = new int;

int *p2;

*p1 = 5;

p2 = p1;

cout << *p2;

delete p1;

cout << *p2;

*p2 = 10;

6. Hãy nêu các tình huống có thể xảy ra đối với đoạn chương trình sau:

int *p1 = new int;

int *p2 = new int[10];

*p1 = 5;

p2 = p1;

cout << *p2;

7. Hãy nêu các tình huống có thể xảy ra đối với đoạn chương trình sau:

int *a = new int[10];

for (int i = 0; i <= 10; i ++) {

print a[i];

a[i] = i;

}

8. Nhập từ bàn phím vào một danh sách các số nguyên. Hãy sử dụng cấu

trúc mảng động để lưu giữ dãy số nguyên này và tìm số lượng số nguyên

chia hết cho số nguyên đầu tiên trong dãy. Hiện kết quả ra màn hình.

9. Phân tích sự khác biệt, nhược điểm, ưu điểm của việc sử dụng mảng động

và mảng tĩnh. Những lưu ý khi khai báo, xin cấp phát và giải phóng bộ

nhớ đối với mảng động.

10. Sử dụng cấu trúc mảng động để lưu giữ một danh sách tên các sinh viên

nhập vào từ bàn phím. Hãy sắp xếp danh sách tên sinh viên tăng dần theo

độ dài của tên. Hiện ra màn hình danh sách sau khi đã sắp xếp.

11. Một bàn cờ có kích thước m*n ô vuông. Trạng thái trên mỗi ô vuông

được biểu diễn bởi một kí tự in hoa từ „A‟ đến „Z‟. Sử dụng mảng động

hai chiều để lưu giữ trạng thái của bàn cờ nhập từ bàn phím. Tìm và hiện

ra màn hình:

Các hàng thỏa mãn điều kiện tất cả các ô cùng một trạng thái

Page 117: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

117

Các cột thỏa mãn điều kiện tất cả các ô cùng một trạng thái

Các đường chéo thỏa mãn điều kiện tất cả các ô cùng một trạng thái

Page 118: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

118

Chương 7. Các kiểu dữ liệu trừu tượng

Các kiểu dữ liệu có sẵn của một ngôn ngữ lập trình, đặc biệt là các kiểu dữ liệu

cơ bản, đôi khi không đủ để biểu diễn dữ liệu của bài toán cần giải quyết.

Thông thường, ta cần gộp một vài thành phần dữ liệu có liên quan với nhau lại

để biểu diễn một phần tử dữ liệu phức hợp mới. Chẳng hạn:

Dữ liệu cần thiết để mô tả một ngày (Date) gồm 3 thành phần: ngày, tháng,

năm. Ví dụ, dữ liệu về ngày Quốc khánh của Việt Nam gồm 3 thành phần: 2

(ngày), 9 (tháng), 1945 (năm).

Dữ liệu cần thiết để mô tả Student bao gồm ba thành phần: studentNumber

thuộc kiểu int, birthday thuộc kiểu Date, name thuộc kiểu char [50].

Kiểu dữ liệu trừu tượng là kiểu dữ liệu phức hợp được cấu tạo từ các thành

phần dữ liệu thuộc các kiểu dữ liệu khác nhau (có thể là kiểu có sẵn hoặc kiểu

dữ liệu mà chúng ta tự định nghĩa). Đây là cơ chế cho phép chúng ta tự định

nghĩa các kiểu dữ liệu mới, chẳng hạn như Date và Student như miêu tả ở trên.

Đối với mỗi một kiểu dữ liệu trừu tượng, ta cần định nghĩa một loạt các thao tác

xử lý dữ liệu cho nó. Ví dụ, với kiểu dữ liệu Date có thể cần đến các thao tác

xử lý dữ liệu như in ra màn hình, và các phép tính đối với dữ liệu ngày tháng

như: tính ngày hôm qua, tính ngày mai, tính khoảng cách giữa hai ngày, …

Các ngôn ngữ lập trình bậc cao có thể chia thành hai loại: ngôn ngữ lập trình

hướng thủ tục, và ngôn ngữ lập trình hướng đối tượng. Kiểu dữ liệu trừu tượng

ở hai loại ngôn ngữ này có sự khác biệt như sau:

Đối với các ngôn ngữ lập trình hướng thủ tục, các kiểu dữ liệu có cấu trúc

chỉ dừng lại ở việc đóng gói các thành phần dữ liệu có liên quan lại với nhau.

Phần xử lý dữ liệu được đặt tại các hàm và thủ tục độc lập.

Các ngôn ngữ lập trình hướng đối tượng đi xa hơn một bước: cho phép

đóng gói cả các hàm xử lý dữ liệu vào trong kiểu dữ liệu, coi chúng như là

một phần không thể tách rời của kiểu dữ liệu. Trong các ngôn ngữ này, kiểu

dữ liệu trừu tượng được gọi là các lớp đối tượng (class).

7.1. Định nghĩa kiểu dữ liệu trừu tượng bằng cấu trúc struct

Các ngôn ngữ lập trình bậc cao sử dụng các cấu trúc khá tương tự nhau để mô tả

dữ liệu trừu tượng. Một trong các cấu trúc mà C++ cung cấp cho công việc này

Page 119: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

119

là cấu trúc struct. Ví dụ, kiểu dữ liệu trừu tượng Time có thể được định nghĩa

như sau trong C++:

struct Time {

int hour;

int minute;

int second;

};

Trong đó, hour, minute, second là các trường hay các thành viên dữ liệu của

cấu trúc Time.

Chú ý rằng các trường thuộc cùng một cấu trúc phải có tên khác nhau tuy rằng

có thể trùng tên với các trường của một cấu trúc khác. Ngoài ra, phần định

nghĩa một cấu trúc phải được kết thúc bằng một dấu chấm phảy.

Sau khi đã định nghĩa kiểu dữ liệu Time, ta có thể sử dụng nó y như các kiểu dữ

liệu khác. Ta có thể khai báo biến, mảng, con trỏ, tham chiếu kiểu Time, ví dụ:

Time dinnerTime;

Time appointment[10];

Time *timePtr = &dinnerTime;

Time &timeRef = dinnerTime;

Lưu ý: mặc dù cùng dùng từ khóa struct, nhưng struct của C++ không tương

đương với struct của ngôn ngữ lập trình C.

Trong C++, biến thuộc kiểu dữ liệu cấu trúc cũng được đối xử như các kiểu dữ

liệu thông thường. Ta có thể thực hiện phép gán giá trị của biến Time này cho

biến Time khác, ví dụ:

appointment[1] = dinnerTime;

Kết quả là giá trị của các thành viên dữ liệu của dinnerTime được sao chép vào

các thành viên dữ liệu tương ứng của appointment[1]. Lưu ý rằng phép gán

mặc định của C++ là phép gán nông, nghĩa là chỉ có giá trị của các thành viên

được sao chép. Do đó nếu một trong các thành viên dữ liệu là con trỏ tới một

vùng nhớ thì chỉ có giá trị của con trỏ được sao chép chứ nội dung của vùng nhớ

thì không.

Để truy cập các thành viên dữ liệu của cấu trúc, ta có hai toán tử dấu chấm (.)

và mũi tên (->). Toán tử mũi tên (->) dùng để truy nhập các thành viên qua một

Page 120: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

120

con trỏ đến đối tượng. Toán tử dấu chấm (.) được dùng trong các trường hợp

còn lại. Ví dụ, để in thành viên hour của biến dinnerTime ra màn hình:

cout << dinnerTime.hour;

hoặc

cout << timeRef.hour;

hoặc thông qua biến con trỏ timePtr như sau

cout << timePtr->hour;

Trong đó timePtr đang chứa địa chỉ của biến dinnerTime, còn timeRef là một

tham chiếu của biến dinnerTime.

Lưu ý rằng biểu thức (*timePtr) cho ta chính biến dinnerTime, do đó ba biểu

thức (*timePtr).hour, dinnerTime.hour, và timePtr->hour tương đương

với nhau. Ngoài ra, cặp ngoặc trong biểu thức (*timePtr).hour là cần thiết do

toán tử * không được ưu tiên bằng toán tử ->.

Hình 7.1 minh họa cách khai báo và sử dụng struct để khai báo cấu trúc dữ

liệu Time. Cũng như các kiểu dữ liệu khác, kiểu dữ liệu có cấu trúc cũng có thể

được truyền vào trong các hàm dưới dưới dạng tham số (xem Hình 7.2). Trong

ví dụ đó, phần mã chương trình có nhiệm vụ in một giá trị kiểu Time ra màn

hình được chuyển thành hàm print với tham số là một tham chiếu tới biến Time

cần in. Trong ví dụ này, tham số của hàm print được quy định là hằng tham

chiếu (từ khóa const) để hàm này không có quyền sửa dữ liệu nằm trong biến

kiểu Time được truyền vào.

Khi sử dụng cơ chế truyền bằng giá trị, chẳng hạn print(Time), một bản sao

của đối số kiểu Time sẽ được truyền vào hàm tương tự như đối với tham số

thuộc các kiểu dữ liệu khác. Tuy nhiên, để tránh việc phải tốn chi phí tính toán

và bộ nhớ cho việc sao chép các cấu trúc mà không phải cấp quyền sửa một

cách không cần thiết, người ta thường dùng tham số là hằng tham chiếu như tại

hàm print(const Time & t) trong Hình 7.2 thay vì dùng kiểu truyền giá trị.

Một lựa chọn khác là dùng tham số là con trỏ, chẳng hạn

void print(Time * ptrTime)

Và nếu không muốn cho print quyền sửa dữ liệu của biến Time được truyền cho

nó, ta quy định tham số là con trỏ tới hằng kiểu Time, nghĩa là:

Page 121: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

121

void print (const Time * ptrTime)

Để ý rằng cấu trúc Time trong Hình 7.2 chỉ bao gồm dữ liệu. Phần chương trình

xử lý dữ liệu của Time được đặt ở bên ngoài cấu trúc dữ liệu Time, tại các hàm

có chức năng thao tác dữ liệu Time. Đây chính là đặc điểm của phong cách lập

trình hướng thủ tục: dữ liệu được khai báo một nơi và xử lý dữ liệu một nơi. Cụ

thể, trong ví dụ của ta, các hàm print, setTime và main tuy nằm ngoài và độc

lập với Time nhưng lại có toàn quyền thao tác dữ liệu nằm trong các biến kiểu

Time, chẳng hạn như gán cho một biến Time giá trị không hợp lệ 3:100:-1. Đây

là giới hạn của các ngôn ngữ lập trình thủ tục. Các ngôn ngữ lập trình hướng đối

tượng đi xa hơn và cung cấp cơ chế tự bảo vệ cho các kiểu dữ liệu. Ta có thể

sửa cấu trúc Time trong Hình 7.2 để sử dụng cơ chế đó, mục tiếp theo sẽ nói về

vấn đề này.

Page 122: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

122

// define struct Time and test it.#include <iostream>

using namespace std;

// Time structure definition struct Time { int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 }; // end struct Time

int main(){ Time dinnerTime, midnight; // variables of new type Time dinnerTime.hour = 18; // set hour member of dinnerTime dinnerTime.minute = 30; // set minute member of dinnerTime dinnerTime.second = 0; // set second member of dinnerTime

midnight.hour = 0; // set hour member of midnight midnight.minute = 0; // set minute member of midnight midnight.second = 0; // set second member of midnight cout << "Dinner will be held at " << dinnerTime.hour << ":" << dinnerTime.minute << ":" << dinnerTime.second << endl;

cout << "Lights will be switched off at " << midnight.hour << ":" << midnight.minute << ":" << midnight.second << endl;

return 0; }

Kết quả chạy chương trình

Dinner will be held at 18:30:0Lights will be switched off at 0:0:0

Hình 7.1: Ví dụ về khai báo và sử dụng cấu trúc dữ liệu struct.

Page 123: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

123

// define struct Time and test it.#include <iostream>#include <iomanip>

using namespace std; // Time structure definition struct Time { int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 }; // end struct Time

// print time to the screenvoid print (const Time &t){ cout << t.hour << ":" << t.minute << ":" << t.second;}

// set hour, minute, and second of a Time structurevoid setTime (Time &t, int hour, int minute, int second){ t.hour = hour; t.minute = minute; t.second = second;}

int main(){ Time dinnerTime, midnight; // variables of the type Time

setTime(dinnerTime, 18, 30, 0); setTime(midnight, 0, 0, 0); cout << "Dinner will be held at "; print(dinnerTime); cout << endl; cout << "Lights will be switched off at "; print(midnight);

return 0; }

Hình 7.2: Ví dụ về dùng dữ liệu cấu trúc làm tham số cho hàm.

Page 124: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

124

7.2. Định nghĩa kiểu dữ liệu trừu tượng bằng cấu trúc class

Như đã nói ở trên, ta có thể đóng gói các hàm xử lý dữ liệu vào bên trong kiểu

dữ liệu trừu tượng. Ví dụ, các hàm print và setTime trong Hình 7.2 chính là

một tiện ích cho kiểu dữ liệu Time và nên được đóng gói vào bên trong kiểu dữ

liệu này.

Cấu trúc class cho phép ta thực hiện việc đóng gói đó. Khi thực hiện công việc

này, ta bắt đầu bước từ lập trình hướng thủ tục sang lập trình hướng đối tượng.

Hình 7.3 minh họa cách khai báo và cài đặt lớp đối tượng Time sử dụng cấu trúc

class. Một biến khi khai báo thuộc lớp Time thì biến đó được gọi là một đối

tượng Time.

So với struct Time trong Hình 7.2, class Time khác ở hai điểm: (1) bao gồm

cả các hàm setTime và print; (2) có thêm nhãn quyền truy nhập public. Các

hàm setTime và print của lớp đối tượng Time trong Hình 7.3 hoạt động giống

như các hàm tương ứng trong Hình 7.2, chỉ khác ở chỗ ta không cần truyền

tham số là một đối tượng Time cho các hàm này (chi tiết sẽ được giải thích

trong mục sau).

Về mặt hoạt động, cài đặt của class Time trong Hình 7.3 hoàn toàn tương

đương với struct Time trong Hình 7.2. Cụ thể, hàm main hay một hàm nào

khác vẫn có thể tạo các đối tượng Time với dữ liệu không hợp lệ. Nói cách khác,

so với struct Time trong Hình 7.2, class Time trong Hình 7.3 mới chỉ tiến

một bước là đóng gói dữ liệu với phần xử lý.

Cải tiến trong Hình 7.4 là bước tiếp theo, thực hiện được nhiệm vụ kiểm soát dữ

liệu. Trong cài đặt này, các thành viên dữ liệu được giới hạn là chỉ được truy

nhập từ bên trong class Time, còn hàm setTime đảm bảo dữ liệu được gán cho

các thành viên dữ liệu phải có giá trị hợp lệ. Chi tiết sẽ được giải thích trong các

mục sau.

Lưu ý rằng ta hoàn toàn có thể dùng từ khóa struct thay vì class trong tất cả

các trường hợp, nhưng theo thông lệ, struct thường được dùng cho các lớp đối

tượng chỉ có dữ liệu còn class dùng cho các lớp gồm cả dữ liệu và hàm, nên ta

chuyển sang dùng cấu trúc class kể từ ví dụ này.

Page 125: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

125

// define class Time and test it.#include <iostream>using namespace std;

// class Time definition class Time { public: void print () { cout << hour << ":" << minute << ":" << second; } void setTime (int h, int m, int s) { hour = h; minute = m; second = s; }

int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 }; // end class Time

int main(){ Time dinnerTime, midnight; // variables of the type Time

setTime(dinnerTime, 18, 30, 0); setTime(midnight, 0, 0, 0); cout << "Dinner will be held at "; print(dinnerTime); cout << endl; cout << "Lights will be switched off at "; print(midnight);

return 0; }

Hình 7.3: Đóng gói các hàm xử lý dữ liệu vào trong class Time.

Page 126: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

126

// define class Time and test it.#include <iostream>using namespace std;

// class Time definition class Time { public: void print () { cout << hour << ":" << minute << ":" << second; } bool setTime (int h, int m, int s);

private: int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 bool isValid (int h, int m, int s) { return (h >= 0 && h < 24) && (m >= 0 && m < 60) && (s >= 0 && s < 60); } }; // end class Time

bool Time::setTime (int h, int m, int s){ if (! isValid(h,m,s)) return false; hour = h; minute = m; second = s; return true;}

int main(){ Time dinnerTime, midnight; // variables of the type Time

setTime(dinnerTime, 18, 30, 0); setTime(midnight, 0, 0, 0); cout << "Dinner will be held at "; print(dinnerTime); cout << endl; cout << "Lights will be switched off at "; print(midnight);

return 0; }

Hình 7.4: Bổ sung cho class Time khả năng tự kiểm soát tính hợp lệ của dữ liệu.

Page 127: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

127

7.2.1. Quyền truy nhập

Khi khai báo một lớp đối tượng, chúng ta mong muốn phân quyền truy nhập và

sử dụng dữ liệu cũng như các hàm của lớp đó. Tức là, một số dữ liệu hay hàm

được truy nhập và sử dụng rộng rãi bởi tất cả các hàm thuộc hay không thuộc

lớp đối tượng đó. Bên cạnh đó, chúng ta cũng mong muốn một số dữ liệu hay

hàm được bảo vệ khỏi việc bị truy nhập và sử dụng từ bên ngoài. Tức là, chỉ các

hàm thuộc lớp đó được phép truy nhập và sử dụng, các hàm bên ngoài không

thuộc lớp đối tượng đó thì không được phép truy nhập và sử dụng.

Các ngôn ngữ lập trình hướng đối tượng cung cấp cho chúng ta cơ chế để phân

quyền như vậy.

Nhãn quyền truy nhập (member access specifier) quy định quyền truy nhập

đến các thành viên của lớp. Trong số đó có hai loại thường được sử dụng là:

public: Dữ liệu hay hàm thuộc loại công cộng (được khai báo dưới nhãn

public) có thể được truy nhập và sử dụng rộng rãi bởi tất cả các hàm thuộc

hay không thuộc lớp đối tượng đó. Trong Hình 7.4, hàm print của lớp đối

tượng Time là hàm public, và ta có thể gọi nó thông qua các biến midnight

(midnight.print) hay dinnerTime (dinnerTime.print).

private: Dữ liệu hay hàm thuộc loại riêng tư (được khai báo dưới nhãn

private) chỉ được truy nhập và sử dụng bởi các hàm thành viên thuộc lớp

đó. Các hàm bên ngoài không thuộc lớp đối tượng đó thì không được phép

truy nhập và sử dụng. Trong Hình 7.4, các biến hour, minute, second, và

hàm isValid thuộc diện private và chỉ có các hàm thành viên print và

setTime là có thể truy nhập trực tiếp, chẳng hạn để đọc/ghi giá trị của hour.

Trong khi, đó tại hàm main, nghĩa là ở ngoài lớp Time, ta không thể truy

nhập vào các biến hour, minute, second hay gọi hàm isValid. Các lệnh sau

đây không hợp lệ và sẽ gây lỗi khi biên dịch:

dinnerTime.hour = 18;

dinnerTime.isValid(10,3,4);

Loại nhãn private này thường được dùng cho các hàm tiện ích chỉ cần dùng

đến ở bên trong phạm vi lớp bởi các hàm thành viên khác.

Bên cạnh hai nhãn phổ biến trên, ta còn các loại nhãn phân quyền khác như

protected, friend (ta sẽ không nói đến trong phạm vi khóa học này).

Page 128: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

128

Nếu không dùng các từ khóa trên để quy định quyền truy nhập một các tường

minh, thì quyền truy nhập mặc định của các thành viên của class là private,

còn của struct là public. Các loại quyền truy nhập ngoài mặc định phải được

quy định một cách tường minh.

Tuy các thành viên private của một lớp không thể được truy nhập trực tiếp từ

ngoài nhưng chúng có thể được truy nhập gián tiếp thông qua các hàm mà lớp

dành riêng cho công việc này. Chẳng hạn với các đối tượng thuộc lớp Time,

hàm print cho phép đọc và hàm setTime cho phép ghi dữ liệu private. Ngoài

ra, setTime còn có một chức năng quan trọng là kiểm tra tính hợp lệ của dữ liệu

mới trước khi gán trị cho các thành viên dữ liệu (nó gọi hàm isValid). Nếu cần

thiết, ta có thể bổ sung các hàm getHour, setHour, getMinute, setMinute,

getSecond, setSecond để truy cập đến các thành viên dữ liệu private. Cùng với

setTime, chúng được gọi là các hàm truy cập (accessor) của lớp Time, chúng

kiểm soát dữ liệu được ghi vào đối tượng và quản lý định dạng của kết quả đọc

dữ liệu của đối tượng.

7.2.2. Toán tử phạm vi và định nghĩa các hàm thành viên

Việc khai báo và định nghĩa một hàm có thể thực hiện đồng thời bên trong một

lớp đối tượng (chẳng hạn hàm print trong Hình 7.4). Nhưng ta cũng có thể

tách phần khai báo và định nghĩa một hàm ra hai nơi khác nhau: phần khai báo

nằm bên trong khai báo lớp, phần định nghĩa nằm bên ngoài khai báo lớp

(chẳng hạn hàm setTime trong Hình 7.4). Nếu đặt định nghĩa hàm ở bên ngoài

này, ta phải dùng toán tử phạm vi (::) để chỉ rõ rằng ta đang định nghĩa một

hàm thành viên của lớp Time chứ không phải một hàm thông thường.

Toán tử phạm vi chỉ rõ lớp đối tượng mà một thành viên thuộc về, cho phép

hàm thành viên đó có được tính chất phạm vi như thể định nghĩa của nó nằm

bên trong khối định nghĩa của lớp đối tượng chủ của nó. Ví dụ, phần thân hàm

setTime được xem là nằm bên trong phạm vi của lớp Time, do đó, từ đây vẫn

có thể truy nhập trực tiếp các biến thành viên loại private thuộc lớp Time.

Lưu ý, cũng như các thành viên dữ liệu, do đã nằm bên trong phạm vi của lớp

nên các hàm thành viên của các lớp khác nhau có thể trùng tên.

Để ý các lời gọi hàm setTime từ hai đối tượng khác nhau dinnerTime và

midnight trong Hình 7.3 hay Hình 7.4, lời gọi hàm từ đối tượng nào thì hàm

hoạt động trên các thành viên dữ liệu của đối tượng đó. Tuy nhiên, ta không

phải truyền đối tượng vào hàm như trong Hình 7.2 – khi hàm print là hàm

Page 129: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

129

thông thường chứ không phải hàm thành viên. Lí do là vì khi gọi hàm thành

viên từ một đối tượng, đối tượng đó được tự động truyền vào hàm như là một

tham số ẩn.

7.2.3. Hàm khởi tạo và hàm hủy

Hình 7.5 là phiên bản mở rộng của cài đặt lớp Time trong Hình 7.4 (các phần

<...> trong Hình 7.5 có nội dung giống như đoạn tương ứng trong Hình 7.4).

Ngoài thành viên dữ liệu note, trong Hình 7.5 còn có thêm các hàm đặc biệt là

Time(),Time(int h, int m, int s) và ~Time(). Các hàm có tên Time được

gọi là hàm khởi tạo (constructor), còn ~Time được gọi là hàm hủy (destructor).

Hàm khởi tạo là hàm thành viên đặc biệt có chức năng khởi tạo các thành viên

dữ liệu của đối tượng. Nó được gọi một cách tự động khi đối tượng được tạo, ví

dụ khi một biến thuộc lớp đối tượng, chẳng hạn hàm khởi tạo Time() được gọi

khi lệnh khai báo midnightTime được thực thi. Hàm khởi tạo phải trùng tên với

tên lớp, không có giá trị trả về, và không có kiểu giá trị trả về. Một lớp có thể có

vài hàm khởi tạo hoạt động theo nguyên tắc hàm trùng tên. Khi một đối tượng

được tạo, trình biên dịch sẽ chọn gọi hàm nào có danh sách tham số khớp với

các đối số được cho tại lệnh khai báo đối tượng. Trong Hình 7.5, đối tượng

dinnerTime được khai báo với 3 đối số, cho nên hàm khởi tạo Time (int h,

int m, int s) sẽ được tự động gọi khi chạy lệnh khao báo biến dinnerTime.

Trong khi đó, biến midnight được khai báo không có đối số, nên hàm khởi tạo

Time() không yêu cầu tham số sẽ được tự động gọi khi chạy lệnh khao báo biến

midnight.

Hàm hủy, ví dụ ~Time(), thực hiện chức năng ngược lại với hàm khởi tạo. Nó

được gọi tự động khi một đối tượng bị hủy khi thời gian sống của nó đã kết thúc

hoặc khi nó là một đối tượng được cấp phát bộ nhớ động và đang được giải

phóng khỏi bộ nhớ bằng lệnh delete. Hàm hủy phải trùng tên với tên lớp

nhưng có thêm dấu ngã (~) đặt trước. Hàm hủy không được có kết quả trả về.

Đối với một đối tượng có thành viên dữ liệu là bộ nhớ động được cấp phát trong

thời gian sống của đối tượng, hàm hủy là nơi thích hợp để viết các lệnh giải

phóng phần bộ nhớ được cấp phát đó, mà đoạn lệnh này sẽ được thực hiện tại

thời điểm mà đối tượng bị hủy. Trong Hình 7.5, hàm hủy ~Time chứa một lệnh

giải phóng bộ nhớ động mà thành viên dữ liệu note trỏ tới. Công việc này sẽ

được thực hiện đối với từng đối tượng midnight và dinnerTime khi chương

trình chạy hết thời gian sống của chúng.

Page 130: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

130

Cả hai loại hàm hủy và hàm tạo đều không bắt buộc. Nếu không được định

nghĩa, trình biên dịch sẽ tự tạo các hàm tạo và hủy mặc định có nội dung rỗng.

Page 131: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

131

#include <cstring>#include <iostream>using namespace std; class Time { public: Time () { //default constructor hour = minute = second = 0; note = 0; } Time (int h, int m, int s, const char* n) { setTime(h, m, s); note = new char [strlen(n) + 1]; strcpy (note, n); } ~Time () { delete [] note; } //destructor bool setTime (int h, int m, int s) { <...> } void print () { cout << hour << ":" << minute << ":" << second; if (note != 0) cout << " (" << note << ")"; }

private: int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 char* note; bool isValid (int h, int m, int s) { <...> } };

int main(){ Time dinnerTime (19, 30, 0, "first date"); Time midnight;

cout << "Dinner will be held at "; dinnerTime.print(); cout << endl << "Light will be switched off at "; midnight.print();

return 0; }

Hình 7.5: Hàm tạo và hàm hủy.

Page 132: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

132

7.3. Lợi ích của lập trình hướng đối tượng

Tại Hình 7.4, các nhãn quyền truy nhập public và private quy định rằng các

thành viên dữ liệu (hour, minute, second) chỉ được phép truy nhập từ bên trong

lớp Time (hàm main nằm ngoài phạm vi này), còn các hàm setTime và print

có thể được gọi từ bất cứ đâu trong chương trình. Trong khi đó, tại Hình 7.2 các

thành viên dữ liệu đó có thể được truy nhập từ bất cứ đâu (chẳng hạn hàm main)

do có quyền truy nhập mặc định là public.

Điều đó có nghĩa là dữ liệu của struct Time có thể bị đọc và sửa đổi tùy ý tại

bất cứ đoạn chương trình nào, trong khi đó, từ bên ngoài class Time, dữ liệu

chỉ có thể được đọc bằng cách gọi hàm print và được ghi bằng cách gọi hàm

thành viên setTime – nơi giá trị mới được kiểm tra tính hợp lệ (gọi hàm

isValid) trước khi cập nhật.

Kết quả của các khác biệt trên là:

Để làm việc với dữ liệu kiểu dữ liệu Time trong Hình 7.2, các đoạn chương trình

bên ngoài phải biết chi tiết cấu tạo của Time. Còn đối với kiểu Time trong Hình

7.4, các đoạn mã khác khỉ cần biết cách sử dụng các hàm print và setTime là

đủ - đây chính là giao diện (interface) của Time.

Các đoạn chương trình bên ngoài cấu trúc Time ở Hình 7.2 có toàn quyền thao

túng dữ liệu của các đối tượng Time, còn cấu trúc Time ở Hình 7.4 có thể tự đảm

bảo được tính hợp lệ dữ liệu của mình bằng việc cho phép truy nhập có kiểm

soát qua các hàm thành viên của chính nó.

Sự khác biệt này không có nhiều ý nghĩa đối với một chương trình nhỏ chỉ do

một người viết, tuy nhiên, nó mang lại ích lợi quan trọng trong quá trình phát

triển các phần mềm lớn hơn với nhiều mô đun và với sự tham gia của nhiều lập

trình viên, trong đó có việc tăng tính mô đun và giảm lỗi lập trình.

Tóm lại, mỗi lớp đối tượng là khuôn mẫu hay mô hình cho việc tạo các đối

tượng thuộc một kiểu nhất định. Lớp đối tượng định nghĩa hai loại thành viên và

mỗi đối tượng thuộc lớp đó đều có:

Các thành viên dữ liệu mô tả các thuộc tính của đối tượng. Ví dụ là hour,

minute, và second của cấu trúc Time.

Các hàm thành viên, hay hàm, mô tả hành vi của đối tượng (các hành động

mà đối tượng có thể thực hiện). Ví dụ là các hàm print, setTime của cấu

trúc Time.

Page 133: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

133

Lập trình hướng đối tượng cho phép đơn giản hóa việc lập trình. Các mô đun

chương trình có thể kết nối với nhau mà chỉ cần biết giao diện của nhau. Các chi

tiết cài đặt được che dấu bên trong các mô-đun và bên ngoài không cần biết đến.

Lập trình hướng đối tượng còn giúp tăng tính tái sử dụng và khả năng tích hợp

các mô đun phần mềm, thể hiện ở hai điểm: các thành viên của một lớp có thể là

đối tượng thuộc lớp khác, và các lớp mới được tạo từ lớp cũ bằng quan hệ thừa

kế (chủ đề này nằm ngoài phạm vi khóa học này).

7.4. Biên dịch riêng rẽ

C++ cho phép chia chương trình thành nhiều phần lưu tại các tệp khác nhau,

Các tệp này có thể được biên dịch riêng rẽ, rồi liên kết (link) với nhau trước khi

chạy chương trình.

Cách phân chia thông dụng nhất là đặt định nghĩa của một lớp cùng với định

nghĩa các hàm thành viên của nó vào các tệp riêng chứ không đặt cùng chương

trình sử dụng class đó. Với cách này, ta có thể xây dựng một thư viện các lớp

đối tượng sao cho nhiều chương trình có thể dùng chung một lớp. Ta có thể biên

dịch lớp đó đúng một lần và dùng nó trong nhiều chương trình khác nhau, tương

tự như ta vẫn dùng các thư viện quen thuộc iostream và cstring.

Với mỗi lớp, ta còn có thể viết định nghĩa của nó tại hai tệp để tách giao diện

của lớp (khối định nghĩa lớp gói trong cặp ngoặc {}) khỏi chi tiết cài đặt của lớp

(định nghĩa các hàm thành viên). Khi đó, nếu ta thay đổi cài đặt của một lớp mà

vẫn giữ nguyên giao diện của lớp đó thì ta chỉ phải dịch lại tệp chứa cài đặt của

lớp đó. Các tệp khác, trong đó có các tệp chứa các chương trình sử dụng lớp đó,

không cần phải sửa đổi gì và không cần biên dịch lại. Hình 7.6, Hình 7.7 và

Hình 7.8 là kết quả của việc tách chương trình trong Hình 7.5 thành ba tệp riêng

rẽ.

Thông lệ cho việc tách tệp như sau:

Tệp header chứa định nghĩa lớp và các nguyên mẫu hàm thành viên, nghĩa là

phần nằm trong cặp ngoặc {}, được đặt tên có phần mở rộng là .h (tệp time.h

trong Hình 7.7). Tệp này phải được include trong tất cả các tệp sử dụng lớp

Time (các tệp time.cpp và main.cpp).

Tệp mã nguồn chứa định nghĩa của các hàm thành viên của lớp, thường

trùng tên với tên tệp header tương ứng (ví dụ time.cpp) và có phần mở rộng

là .cpp hay .cc.

Page 134: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

134

Các tệp này được biên dịch riêng rẽ và liên kết với tệp chương trình chính

(main.cpp) trước khi chạy chương trình.

Để ý mã nguồn của tệp time.h, ta thấy phần nội dung tệp được gói trong bộ định

hướng tiền xử lý dưới đây để tránh việc nội dung tệp được include nhiều lần

trong một chương trình.

#ifndef __TIME_H__

#define __TIME_H__

#endif

Đoạn tiền xử lý đó có nghĩa: nếu nhãn __TIME_H__ chưa được định nghĩa thì

định nghĩa __TIME_H__, và đoạn mã sau đó cho đến trước #endif mới được

trình biên dịch xét đến. Còn nếu như __TIME_H__ đã được định nghĩa khi trình

biên dịch duyệt đến dòng #ifndef __TIME_H__, thì trình biên dịch sẽ bỏ qua

phần mã tiếp theo cho đến hết #endif. Dòng #define __TIME_H__ nằm trong

tệp time.h có tác dụng đánh dấu trạng thái rằng time.h đã được include vào

chương trình. Cái tên __TIME_H__ là được đặt theo thông lệ để tương ứng với

tên tệp time.h.

#include <iostream>#include "time.h"

using namespace std; int main(){ Time dinnerTime (19, 30, 0, "first date"); Time midnight;

cout << "Dinner will be held at "; dinnerTime.print(); cout << endl << "Light will be switched off at "; midnight.print();

return 0; }

Hình 7.6: Tệp main.cpp chứa chương trình chính.

Page 135: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

135

#ifndef TIME_H#define TIME_H class Time { public: Time () { //default constructor hour = minute = second = 0; note = 0; } Time (int h, int m, int s, const char* n); ~Time () { delete [] note; } //destructor bool setTime (int h, int m, int s) ; void print ();

private: int hour; // 0-23 (24-hour clock format) int minute; // 0-59 int second; // 0-59 char* note; bool isValid (int h, int m, int s); };

#endif

Hình 7.7: Tệp time.h chứa định nghĩa của lớp Time.

Page 136: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

136

#include <cstring>#include <iostream>#include "time.h"

using namespace std;

Time::Time (int h, int m, int s, const char* n) { setTime(h, m, s); note = new char [strlen(n) + 1]; strcpy (note, n);}

bool Time::setTime (int h, int m, int s){ if (! isValid(h,m,s)) return false; hour = h; minute = m; second = s; return true;}

void Time::print() { cout << hour << ":" << minute << ":" << second; if (note != 0) cout << " (" << note << ")";}

bool Time::isValid (int h, int m, int s) { return (h >= 0 && h < 24) && (m >= 0 && m < 60) && (s >= 0 && s < 60);}

Hình 7.8: Tệp time.cpp chứa cài đặt lớp Time.

Page 137: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

137

Bài tập

1. Tìm hiểu và trình bày về lập trình hướng đối tượng, các ngôn ngữ lập trình

hướng đối tượng mà bạn biết. Sự khác biệt, ưu điểm, nhược điểm của lập

trình hướng đối tượng so với lập trình hướng thủ tục.

2. Trình bày các sự giống nhau và khác biệt giữa cấu trúc dữ liệu struct, và cấu

trúc dữ liệu class. Cho hai ví dụ về sử dựng cấu trúc dữ liệu struct và cấu

trúc dữ liệu class cho cùng một kiểu dữ liệu.

3. Trình bày sự khác biệt giữa dữ liệu và phương thức thuộc loại private và

thuộc loại public. Khi nào thì nên khai báo biến và phương thức thuộc loại

private. Cho hai ví dụ minh họa về cách sử dụng dữ liệu và phương thức

thuộc private và thuộc public.

4. Viết một cấu trúc dữ liệu struct chứa thông tin về ba cạnh của một tam giác:

Nhập thông tin các cạnh của tam giác từ bàn phím

Viết hàm kiểm tra xem ba cạnh đó có thỏa mãn là ba cạnh của tam giác

hay không. Hiện kết quả ra màn hình.

Viết hàm tính diện tích tam giác. Hiện kết quả ra màn hình.

5. Viết một class chứa thông tin về ba cạnh của một tam giác và các phương

thức:

Nhập thông tin các cạnh của tam giác từ bàn phím.

Kiểm tra xem là tam giác thường, tam giác cân, tam giác đều, hay không

là tam giác. Hiện kết quả ra màn hình.

6. Nhập từ bàn phím thông tin về một ngày (một ngày gồm ba thành phần là

ngày, tháng, năm), sử dụng cấu trúc struct và cấu trúc class để tính toán và

hiện ra màn hình:

Ngày hôm trước.

Ngày hôm sau.

Còn bao nhiêu ngày nữa thì đến ngày 1/1/2020.

7. Viết một class Student bao gồm 3 thông tin cơ bản là name, age, university.

Lớp Student bao gồm:

Page 138: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

138

Một hàm khởi tạo gán thông tin cho sinh viên:

name = “NO NAME”; age = -1; university = “No information”;

Hàm hủy với mục đích hiện ra dòng chữ:

“Good bye” + name.

Hàm assign nhận và gán thông tin name, age, university cho một sinh

viên.

Hàm print hiện thông tin của một sinh viên ra màn hình.

8. Viết một class lưu trữ thông tin về một sinh viên (tên tuổi, ngày tháng năm

sinh, quê quán, lớp học, lực học từ 0,0 đến 10,0) và các phương thức tính

toán:

Sinh viên đó bao nhiêu tuổi tính đến ngày hôm nay.

Phân loại học lực của sinh viên đó (kém, trung bình, giỏi, xuất sắc).

Quê quán thuộc có thuộc ba thành thành phố lớn (Hà Nội, Hồ Chí

Minh, Đà Nẵng) hay không.

9. Nhập vào từ bàn phím một danh sách sinh viên. Hãy tính và hiện ra màn

hình:

Thông tin về tất cả các bạn tên là Vinh

Thông tin về tất cả các bạn quê ở Hà Nội

Cho biết tổng số bạn có học lực kém (<4), học lực trung bình (≥4 và

<8), học lực giỏi (≥8).

10. Sử dụng cấu trúc dữ liệu class và mảng động để lưu giữ danh sách sinh viên

một lớp học. Mỗi sinh viên bao gồm các thông tin (tên tuổi, ngày tháng năm

sinh, quê quán, lớp học, lực học từ 0,0 đến 10,0). Viết chương trình thực

hiện các công việc sau đây:

Nhập thông tin các sinh viên từ bàn phím

Hãy kiểm tra xem có hai bạn sinh viên nào trùng nhau tất cả các thông

tin.

Hiện kết quả tìm được ra màn hình.

Page 139: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

139

11. Tìm hiểu về tính thừa kế (inheritance) trong trong lập trình hướng đối tượng.

Hãy viết một chương trình gồm 2 lớp sau:

12. Lớp people gồm 2 thuộc tính (name, age) và phương thức talk (đầu vào là

một xâu kí tự, hiện xâu kí tự ra màn hình).

13. Lớp student gồm 4 thuộc tính (name, age, university, major) và hai phương

thức talk, study (hiện ra màn hình thông tin về university và major).

Page 140: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

140

Chương 8. Vào ra dữ liệu

Tất cả các chương trình mà ta đã gặp trong cuốn sách này đều lấy dữ liệu vào từ

bàn phím và in ra màn hình. Nếu chỉ dùng bàn phím và màn hình là các thiết bị

vào ra dữ liệu thì chương trình của ta khó có thể xử lý được khối lượng lớn dữ

liệu, và kết quả chạy chương trình sẽ bị mất ngay khi ta đóng cửa sổ màn hình

output hoặc tắt máy. Để cải thiện tình trạng này, ta có thể lưu dữ liệu tại các

thiết bị lưu trữ thứ cấp mà thông dụng nhất thường là ổ đĩa cứng. Khi đó dữ liệu

tạo bởi một chương trình có thể được lưu lại để sau này được sử dụng bởi chính

nó hoặc các chương trình khác. Dữ liệu lưu trữ như vậy được đóng gói tại các

thiết bị lưu trữ thành các cấu trúc dữ liệu gọi là tệp (file).

Chương này sẽ giới thiệu về cách viết các chương trình lấy dữ liệu vào (từ bàn

phím hoặc từ một tệp) và ghi dữ liệu ra (ra màn hình hoặc một tệp).

8.1. Khái niệm dòng dữ liệu

Trong một số ngôn ngữ lập trình như C++ và Java, dữ liệu vào ra từ tệp, cũng

như từ bàn phím và màn hình, đều được vận hành thông qua các dòng dữ liệu

(stream). Ta có thể coi dòng dữ liệu là một kênh hoặc mạch dẫn mà dữ liệu

được truyền qua đó để chuyển từ nơi gửi đến nơi nhận.

Dữ liệu được truyền từ chương trình ra ngoài theo một dòng ra (output stream).

Đó có thể là dòng ra chuẩn nối và đưa dữ liệu ra màn hình, hoặc dòng ra nối với

một tệp và đẩy dữ liệu ra tệp đó.

Chương trình nhận dữ liệu vào qua một dòng vào (input stream). Dòng vào có

thể là dòng vào chuẩn nối và đưa dữ liệu vào từ màn hình, hoặc dòng vào nối

với một tệp và nhận dữ liệu vào từ tệp đó.

Dữ liệu vào và ra có thể là các kí tự, số, hoặc các byte chứa các chữ số nhị phân.

Trong C++, các dòng vào ra được cài đặt bằng các đối tượng của các lớp dòng

vào ra đặc biệt. Ví dụ, cout mà ta vẫn dùng để ghi ra màn hình chính là dòng ra

chuẩn, còn cin là dòng vào chuẩn nối với bàn phím. Cả hai đều là các đối tượng

dòng dữ liệu (khái niệm "đối tượng" này có liên quan đến tính năng hướng đối

tượng của C++, khi nói về các dòng vào/ra của C++, ta sẽ phải đề cập nhiều đến

tính năng này).

Page 141: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

141

8.2. Tệp văn bản và tệp nhị phân

Về bản chất, tất cả dữ liệu trong các tệp đều được lưu trữ dưới dạng một chuỗi

các bit nhị phân 0 và 1. Tuy nhiên, trong một số hoàn cảnh, ta không coi nội

dung của một tệp là một chuỗi 0 và 1 mà coi tệp đó là một chuỗi các kí tự. Một

số tệp được xem như là các chuỗi kí tự và được xử lý bằng các dòng và hàm cho

phép chương trình và hệ soạn thảo văn bản của bạn nhìn các chuỗi nhị phân như

là các chuỗi kí tự. Chúng được gọi là các tệp văn bản (text file). Những tệp

không phải tệp văn bản là tệp nhị phân (binary file). Mỗi loại tệp được xử lý

bởi các dòng và hàm riêng.

Chương trình C++ của bạn được lưu trữ trong tệp văn bản. Các tệp ảnh và nhạc

là các tệp nhị phân. Do tệp văn bản là chuỗi kí tự, chúng thường trông giống

nhau tại các máy khác nhau, nên ta có thể chép chúng từ máy này sang máy

khác mà không gặp hoặc gặp phải rất ít rắc rối. Nội dung của các tệp nhị phân

thường lấy cơ sở là các giá trị số, nên việc sao chép chúng giữa các máy có thể

gặp rắc rối do các máy khác nhau có thể dùng các quy cách lưu trữ số không

giống nhau. Cấu trúc của một số dạng tệp nhị phân đã được chuẩn hóa để chúng

có thể được sử dụng thống nhất tại các platform khác nhau. Nhiều dạng tệp ảnh

và âm thanh thuộc diện này.

Mỗi kí tự trong một tệp văn bản được biểu diễn bằng 1 hoặc 2 byte, tùy theo đó

là kí tự ASCII hay Unicode. Khi một chương trình viết một giá trị vào một tệp

văn bản, các kí tự được ghi ra tệp giống hệt như khi chúng được ghi ra màn hình

bằng cách sử dụng cout. Ví dụ, hành động viết số 1 vào một tệp sẽ dẫn đến kết

quả là 1 kí tự được ghi vào tệp, còn với số 1039582 là 7 kí tự được ghi vào tệp.

Các tệp nhị phân lưu tất cả các giá trị thuộc một kiểu dữ liệu cơ bản theo cùng

một cách, giống như cách dữ liệu được lưu trong bộ nhớ máy tính. Ví dụ, mỗi

giá trị int bất kì, 1 hay 1039582 đều chiếm một chuỗi 4 byte.

8.3. Vào ra tệp

C++ cung cấp các lớp sau để thực hiện nhập và xuất dữ liệu đối với tệp:

ofstream: lớp dành cho các dòng ghi dữ liệu ra tệp

ifstream: lớp dành cho các dòng đọc dữ liệu từ tệp

fstream: lớp dành cho các dòng vừa đọc vừa ghi dữ liệu ra tệp.

Page 142: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

142

Đối tượng thuộc các lớp này do quan hệ thừa kế nên cách sử dụng chúng khá

giống với cin và cout – các đối tượng thuộc lớp istream và ostream – mà

chúng ta đã dùng. Khác biệt chỉ là ở chỗ ta phải nối các dòng đó với các tệp.

#include <iostream>#include <fstream>

using namespace std;

int main () { ofstream myfile; // declare an output file stream object

myfile.open ("hello.txt"); // open a file to write myfile << "Hello!\n"; // write a string to the file myfile.close(); // close the file

return 0;

}

Hình 8.1: Các thao tác cơ bản với tệp văn bản.

Chương trình trong Hình 8.1 tạo một tệp có tên hello.txt và ghi vào đó một câu

"Hello!" theo cách mà ta thường làm đối với cout, chỉ khác ở chỗ thay cout

bằng đối tượng dòng myfile đã được nối với một tệp. Sau đây là các bước thao

tác với tệp.

8.3.1. Mở tệp

Việc đầu tiên là nối đối tượng dòng với một tệp, hay nói cách khác là mở một

tệp. Kết quả là đối tượng dòng sẽ đại diện cho tệp, bất kì hoạt động đọc và ghi

đối với đối tượng đó sẽ được thực hiện đối với tệp mà nó đại diện. Để mở một

tệp từ một đối tượng dòng, ta dùng hàm open của nó:

open (fileName, mode);

Trong đó, fileName là một xâu kí tự thuộc loại const char * với kết thúc là kí

tự null (hằng xâu kí tự cũng thuộc dạng này), là tên của tệp cần mở, và mode là

tham số không bắt buộc và là một tổ hợp của các cờ sau:

Page 143: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

143

ios::in mở để đọc

ios::out mở để ghi

ios::binary mở ở dạng tệp nhị phân

ios::ate đặt ví trí bắt đầu đọc/ghi tại cuối tệp. Nếu cờ này không

được đặt giá trị gì, vị trí khởi đầu sẽ là đầu tệp.

ios::app mở để ghi tiếp vào cuối tệp. Cờ này chỉ được dùng cho dòng

mở tệp chỉ để ghi.

ios::trunc nếu tệp được mở để ghi đã có từ trước, nội dung cũ sẽ bị xóa

để ghi nội dung mới.

Các cờ trên có thể được kết hợp với nhau bằng toán tử bit OR (|). Ví dụ, nếu ta

muốn mở tệp people.dat theo dạng nhị phân để ghi bổ sung dữ liệu vào cuối tệp,

ta dùng lời gọi hàm sau:

ofstream myfile;

myfile.open ("people.dat",

ios::out | ios::app | ios::binary);

Trong trường hợp lời gọi hàm open không cung cấp tham số mode, chẳng hạn

Hình 8.1, chế độ mặc định cho dòng loại ostream là ios::out, cho dòng loại

istream là ios::in, và cho dòng loại fstream là ios::in | ios::out.

Cách thứ hai để nối một dòng với một tệp là khai báo tên tệp và kiểu mở tệp

ngay khi khai báo dòng, hàm open sẽ được gọi với các đối số tương ứng. Ví dụ:

ofstream myfile ("hello.txt",

ios::out | ios::app | ios::binary);

Để kiểm tra xem một tệp có được mở thành công hay không, ta dùng hàm thành

viên is_open(), hàm này không yêu cầu đối số và trả về một giá trị kiểu bool

bằng true nếu thành công và bằng false nếu xảy ra trường hợp ngược lại

if (myfile.is_open()) { /* file now open and ready */ }

8.3.2. Đóng tệp

Khi ta hoàn thành các công việc đọc dữ liệu và ghi kết quả, ta cần đóng tệp để

tài nguyên của nó trở về trạng thái sẵn sàng được sử dụng. Hàm thành viên này

Page 144: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

144

không có tham số, công việc của nó là xả các vùng bộ nhớ có liên quan và đóng

tệp:

myfile.close();

Sau khi tệp được đóng, ta lại có thể dùng dòng myfile để mở tệp khác, còn tệp

vừa đóng lại có thể được mở bởi các tiến trình khác.

Hàm close cũng được gọi tự động khi một đối tượng dòng bị hủy trong khi nó

đang nối với một tệp.

8.3.3. Xử lý tệp văn bản

Chế độ dòng tệp văn bản được thiết lập nếu ta không dùng cờ ios::binary khi

mở tệp. Các thao tác xuất và nhập dữ liệu đối với tệp văn bản được thực hiện

tương tự như cách ta làm với cout và cin.

#include <iostream>#include <fstream>

using namespace std;

int main () { ofstream courseFile ("courses.txt"); if (courseFile.is_open()) { courseFile << "1 Introduction to Programming\n"; courseFile << "2 Mathematics for Computer Science\n"; courseFile.close(); } else cout << "Error: Cannot open file"; return 0;}

Kết quả chạy chương trình [tệp courses.txt]

1 Introduction to Programming2 Mathematics for Computer Science

Hình 8.2: Ghi dữ liệu ra tệp văn bản.

Page 145: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

145

#include <iostream>#include <fstream>#include <string>

using namespace std;

int main () { ifstream file ("courses.txt"); if (file.is_open()) { while (! file.eof()) { string line; getline (file,line); cout << line << endl; } file.close(); } else cout << "Error! Cannot open file";

return 0;}

Kết quả chạy chương trình

1 Introduction to Programming2 Mathematics for Computer Science

Hình 8.3: Đọc dữ liệu từ tệp văn bản.

Chương trình ví dụ trong Hình 8.2 ghi hai dòng văn bản vào một tệp. Chương

trình trong Hình 8.3 đọc nội dung tệp đó và ghi ra màn hình. Để ý rằng trong

chương trình thứ hai, ta dùng một vòng lặp để đọc cho đến cuối tệp. Trong đó,

myfile.eof() là hàm trả về giá trị true khi chạm đến cuối tệp, giá trị true mà

myfile.eof() trả về đã được dùng làm điều kiện kết thúc vòng lặp đọc tệp.

Kiểm tra trạng thái của dòng

Bên cạnh hàm eof() có nhiệm vụ kiểm tra cuối tệp, còn có các hàm thành viên

khác dùng để kiểm tra trạng thái của dòng:

Page 146: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

146

bad()

trả về true nếu một thao tác đọc hoặc ghi bị thất bại. Ví dụ khi ta

cố viết vào một tệp không được mở để ghi hoặc khi thiết bị lưu

trữ không còn chỗ trống để ghi.

fail()

trả về true trong những trường hợp bad() trả về true và khi có

lỗi định dạng, chẳng hạn như khi ta đang định đọc một số nguyên

nhưng lại gặp phải dữ liệu là các chữ cái.

eof() trả về true nếu chạm đến cuối tệp

good() trả về false nếu xảy tình huống mà một trong các hàm trên nếu

được gọi thì sẽ trả về true.

Để đặt lại cờ trạng thái mà một hàm thành viên nào đó đã đánh dấu trước đó, ta

dùng hàm thành viên clear.

Con trỏ get và put của dòng

Mỗi đối tượng dòng vào ra có ít nhất một con trỏ nội bộ. Con trỏ nội bộ của

ifstream hay istream được gọi là con trỏ get hay con trỏ đọc. Nó chỉ tới vị

trí mà thao tác đọc tiếp theo sẽ được thực hiện tại đó. Con trỏ nội bộ của

ofstream hay ostream được gọi là con trỏ put hay con trỏ ghi. Nó chỉ tới vị

trí mà thao tác ghi tiếp theo sẽ được thực hiện tại đó. Cuối cùng, fstream có cả

con trỏ get và con trỏ put.

Để định vị vị trí hiện tại của các con trỏ get và put, ta có các hàm thành viên

tellg và tellp. Các hàm này trả về một giá trị thuộc kiểu pos_type, là kiểu dữ

liệu số nguyên biểu diễn vị trí hiện tại (tính từ đầu tệp) của con trỏ get của dòng

(nếu gọi hàm tellg) hoặc con trỏ put của dòng (nếu gọi hàm tellp).

Để đặt lại vị trí của các con trỏ get và put, ta có các hàm seekg(offset,

direction) và seekp(offset, direction) có công dụng di chuyển các con

trỏ get và put tới vị trí offset. Trong đó, offset được tính từ đầu tệp nếu

direction là ios::beg (giá trị mặc định của direction), từ cuối tệp nếu

direction là ios::end, và từ vị trí hiện tại nếu direction là ios::cur.

Hình 8.4 minh họa cách sử dụng các con trỏ get và put để tính kích thước của

một tệp văn bản.

Page 147: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

147

#include <iostream>#include <fstream>

using namespace std;

int main () { ifstream file ("courses.txt"); long begin = file.tellg(); file.seekg (0, ios::end); long end = file.tellg(); file.close(); cout << "The size is " << (end - begin) << " bytes.\n";

return 0;}

Kết quả chạy chương trình

The size is 65 bytes.

Hình 8.4: Dùng con trỏ dòng để xác định kích thước tệp.

8.3.4. Xử lý tệp nhị phân

Để đọc và ghi dữ liệu với tệp nhị phân, ta không thể dùng các toán tử <<, >> và

các hàm như getline do dữ liệu có định dạng khác và các kí tự trắng không

được dùng để tách giữa các phần tử dữ liệu. Thay vào đó, ta dùng hai hàm thành

viên được thiết kế riêng cho việc đọc và ghi dữ liệu nhị phân một cách tuần tự là

write và read. Cách dùng như sau:

output_stream.write(memory_block, size);

input_stream.read(memory_block, size);

Trong đó memory_block thuộc loại "con trỏ tới char" (char*), nó đại diện cho

địa chỉ của một mảng byte lưu trữ các phần tử dữ liệu đọc được hoặc các phần

tử cần được ghi ra dòng. Tham số size là một giá trị nguyên xác định số kí tự

cần đọc hoặc ghi vào mảng đó.

Các chương trình trong Hình 8.5, Hình 8.6 và Hình 8.7 minh họa việc đọc và

ghi dữ liệu kiểu người dùng tự định nghĩa từ tệp nhị phân.

Page 148: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

148

#ifndef STUDENT_H#define STUDENT_H

#include <iostream>using namespace std;

#define MAX_NAME_LENGTH 20

struct Student { char name [MAX_NAME_LENGTH + 1]; float score;

Student () { name[0] = '\0'; score = 0; } Student (const char*, float); void println() { cout << name << "\t" << score << endl; }};

Student::Student (const char* n,float s) { int length = strlen(n); if (length > MAX_NAME_LENGTH) length = MAX_NAME_LENGTH; strncpy(name, n, length); name[length] = '\0'; // mark the end of the string

score = s; }

#endif

Hình 8.5: Kiểu bản ghi đơn giản Student.

Page 149: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

149

#include <iostream>#include <fstream>#include <cstring>

#include "student.h"

using namespace std;

int main () { ofstream myfile ("scores.dat", ios::binary | ios::out);

if (myfile.is_open()) { Student anne("anne", 10); Student julia("julia", 9); Student bob("bob", 3);

myfile.write((char *)(&anne), sizeof(Student)); myfile.write((char *)(&julia), sizeof(Student)); myfile.write((char *)(&bob), sizeof(Student));

myfile.close(); } else cout << "Error: Cannot open file";

return 0;}

Hình 8.6: Ghi dữ liệu ra tệp nhị phân.

Page 150: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

150

#include <iostream>#include <fstream>#include <cstring>

#include "student.h"

using namespace std;

int main () { ifstream myfile ("scores.dat", ios::binary); if (myfile.is_open()) { while (! myfile.eof() ) { Student student; myfile.read((char *)(&student), sizeof(student)); if (myfile.good()) student.println(); } myfile.close(); } else cout << "Error! Cannot open file";

return 0;}

Hình 8.7: Đọc dữ liệu từ tệp nhị phân.

Page 151: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

151

Bài tập

1. Nhập vào từ bàn phím một danh sách sinh viên. Mỗi sinh viên gồm có các

thông tin sau đây: tên tuổi, ngày tháng năm sinh, nơi sinh, quê quán, lớp, học

lực (từ 0 đến 9). Hãy ghi thông tin về danh sách sinh viên đó ra tệp văn bản

student.txt

2. Sau khi thực hiện bài 1, hãy viết chương trình nhập danh sách sinh viên từ

tệp văn bản student.txt rồi hiển thị ra màn hình:

Thông tin về tất cả các bạn tên là Vinh ra tệp văn bản vinh.txt

Thông tin tất cả các bạn quê ở Hà Nội ra tệp văn bản hanoi.txt

Tổng số bạn có học lực kém (<4), học lực trung bình (≥4 và <8), học lực

giỏi (≥8) ra tệp văn bản hocluc.txt.

3. Sau khi thực hiện bài 2, hãy viết chương trình cho biết kích thước của tệp

văn bản student.txt, vinh.txt, hanoi.txt, hocluc.txt. Kết quả ghi ra tệp văn

bản all.txt.

4. Viết chương trình kiểm tra xem tệp văn bản student.txt có tồn tại hay không?

Nếu tồn tại thì hiện ra màn hình các thông tin sau:

Số lượng sinh viên trong tệp

Số lượng dòng trong tệp

Ghi vào cuối tệp văn bản dòng chữ “CHECKED”

Nếu không tồn tại, thì hiện ra màn hình dòng chữ “NOT EXISTED”.

5. Tệp văn bản numbers.txt gồm nhiều dòng, mỗi dòng chứa một danh sách các

số nguyên hoặc thực. Hai số đứng liền nhau cách nhau ít nhất một dấu cách.

Hãy viết chương trình tổng hợp các thông tin sau và ghi vào tệp văn bản

info.txt những thông tin sau:

Số lượng số trong tệp

Số lượng các số nguyên

Số lượng các số thực

Lưu ý: Test chương trình với cả trường hợp tệp văn bản number.txt chứa một

hay nhiều dòng trắng ở cuối tệp.

Page 152: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

152

6. Trình bày sự khác nhau, ưu điểm, nhược điểm giữa tệp văn bản và tệp văn

bản nhị phân. Khi nào thì nên dùng tệp văn bản nhị phân.

7. Cho file văn bản numbers.txt chứa các số nguyên hoặc thực. Hãy viết một

chương trình đọc các số từ file numbers.txt và ghi ra file nhị phân

numbers.bin các số nguyên nằm trong file numbers.txt.

8. Sau khi thực hiện bài 7, hãy viết một chương trình đọc và tính tổng của tất cả

các số nguyên ở file nhị phân numbers.bin. Hiện ra màn hình kết quả thu

được.

9. Sau khi thực hiện bài 7, hãy viết một chương trình đọc các số nguyên ở file

nhị phân numbers.bin. Ghi các số nằm ở vị trí chẵn (số đầu tiên trong file

được tính ở vị trí số 0) trong file nhị phân numbers.bin vào cuối file

numbers.txt.

10. Cho hai file văn bản num1.txt và num2.txt, mỗi file chứa 1 dãy số đã được

sắp không giảm. Số lượng số trong mỗi file không quá 109. Hãy viết

chương trình đọc và ghi ra file văn bản num12.txt các số trong hai file

num1.txt và num2.txt thỏa mãn điều kiện các số trong file num12.txt cũng

được sắp xếp không giảm.

11. File văn bản document.txt chứa một văn bản tiếng anh. Các câu trong văn

bản được phân cách nhau bởi dấu „.‟ hoặc „!‟. Hãy ghi ra file văn bản

sentences.txt nội dung của văn bản document.txt, mỗi câu được viết trên một

dòng.

Ví dụ:

document.txt sentences.txt

this is a good

house! However, too expensive.

this is a good house!

However, too expensive.

12. File văn bản document.txt chứa một văn bản có lẫn cả các câu tiếng anh và

các câu tiếng Việt. Các câu trong văn bản được phân cách nhau bởi dấu „.‟

hoặc „! ‟. Hãy ghi ra file văn bản english.txt (viet.txt) các cấu tiếng Anh

(Việt) trong văn bản document.txt.

Page 153: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

153

Chương 9. Viết chồng toán tử

9.1. Hàm friends

9.1.1. Hàm friend

Hoàn toàn có thể để một hàm không phải là thành viên của lớp có thể truy cập

đến cá thành viên private của lớp bằng cách sử dụng friend. Một hàm friend có

thể truy cập tới tất cả các thành viên private và protected của lớp được định

nghĩa friend. Để định nghĩa một hàm friend, bao gồm khuôn dạng của nó bên

trong lớp, và đặt trước nó là từ khóa friend. Hãy để ý chương trình:

Ví dụ 10.1:

#include <iostream>

using namespace std;

class myclass {

int a, b;

public:

friend int sum(myclass x);

void set_ab(int i, int j);

};

void myclass::set_ab(int i, int j)

{

a = i;

b = j;

}

// Note: sum() is not a member function of any class.

int sum(myclass x)

{

/* Because sum() is a friend of myclass, it can

directly access a and b. */

return x.a + x.b;

Page 154: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

154

}

int main(){

myclass n;n.set_ab(3, 4);cout<< sum(n);return 0;

}

Trong ví dụ này, hàm sum() không phải là thành viên của myclass. Tuy nhiên,

hàm đó vẫn có toàn quyền truy xuất tới các thành viên private. Bên cạnh đó, chú

ý rằng hàm sum() được gọi mà không sử dụng toán tử dot. Bởi vì, nó không

phải thành viên của lớp, nó không cần phải (thự tế, nó có thể không) phụ thuộc

vào một đối tượng nào cả.

Mặc dù vậy, không có lợi ích gì khi xây dựng sum() là hàm friend hơn là một

hàm thành viên của myclass, một số trường hợp mà hàm friend tỏ ra rất có giá

trị. Đầu tiên, hàm bạn có thể hữu ích khi bạn muốn nạp chồng một loại toán tử

nào đó (…). Thứ hai, hàm friend có thể tạo ra các loại hàm I/O dễ dàng.

Nguyên nhân thứ ba là hàm friend có thể hữu hiệu trong một số trường hợp, hai

hay nhiều lớp có thể chứa các thành viên mà chúng liên hệ tương quan với

những phần khác trong chương trình của ban.Chúng ta hãy xem ví dụ sau để

hiểu rõ trường hợp thứ ba này.

Đầu tiên, chúng ta có hai lớp khác nhau, mỗi lớp sẽ thông báo một thông điệp ra

mành hình hiển thị khi có lỗi xảy ra. Các phần khác trong chương trình của bạn

muốn biết được nếu một thông báo đang được hiển thị hay không trước khi hiển

thị thông tin trên màn hình nhằm đảm bảo không có hiện tượng ghi đè lên nhau

của các thông điệp. Mặc dù bạn có thể tạo ra các hàm thành viên của mỗi lớp để

trả về giá trị xác định khi nào thông điệp đang được hiển thị, điều đó có nghĩa là

thêm các trường hợp khi mà điều kiện được kiểm tra (hai hàm được gọi tới chứ

không phải một).Nếu điều kiện được kiểm tra một cách đều đặn thì việc lặp lại

này không được chấp nhận.Tuy nhiên, nếu một hàm được gọi là friend của lớp,

ta hoàn toàn có thể kiểm tra trạng thái của mỗi đối tượng chỉ với một hàm. Do

đó, trong trường hợp này, hàm friendcho phép bạn tạo ra mã nguồn hiệu quả.

Đoạn mã nguồn sau thể hiện tư tưởng đó:

Ví dụ 10.2:

#include <iostream>

using namespace std;

const int IDLE = 0;const int INUSE = 1;

class C2; // forward declaration

Page 155: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

155

class C1 {

int status; // IDLE if off, INUSE if on screen

// ...

public:

void set_status(int state);

friend int idle(C1 a, C2 b);

};

class C2 {

int status; // IDLE if off, INUSE if on screen

// ...

public:

void set_status(int state);

friend int idle(C1 a, C2 b);

};

void C1::set_status(int state)

{

status = state;

}

void C2::set_status(int state)

{

status = state;

}

int idle(C1 a, C2 b)

{

if(a.status || b.status) return 0;

else return 1;

}

int main()

{

C1 x;

C2 y;

Page 156: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

156

x.set_status(IDLE);y.set_status(IDLE);

if(idle(x, y)) cout << "Screen can be used.\n";

else cout << "In use.\n";

x.set_status(INUSE);

if(idle(x, y)) cout << "Screen can be used.\n";

else cout << "In use.\n";

return 0;

}

Chú ý rằng, chương trình này sử dụng forward declaration (cũng được gọi là

forward reference) cho lớp C2. Điều này là cần thiết bởi vì định nghĩa

idle()trong lớp C1 tham chiếu tới lớp C2 trước khi lớp này được định nghĩa. Để

tạo ra forward declaration đối với một lớp, đơn giản hãy sử dụng như được thấy

trong chương trình trên.

Một friend của một lớp có thể là thành viên của lớp khác. Ví dụ, ở đây chương

trình ở trên được viết lại với hàm idle() là thành viên của lớp C1:

Ví dụ 10.3:

#include <iostream>

using namespace std;

const int IDLE = 0;

const int INUSE = 1;

class C2; // forward declaration

class C1 {

int status; // IDLE if off, INUSE if on screen

// ...

public:

void set_status(int state);

int idle(C2 b); // now a member of C1

};

class C2 {

int status; // IDLE if off, INUSE if on screen

// ...

Page 157: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

157

public:

void set_status(int state);

friend int C1::idle(C2 b);

};

void C1::set_status(int state)

{

status = state;

}

void C2::set_status(int state)

{

status = state;

}

// idle() is member of C1, but friend of C2int C1::idle(C2 b){

if(status || b.status) return 0;else return 1;

}

int main(){

C1 x;C2 y;x.set_status(IDLE);y.set_status(IDLE);

if(x.idle(y)) cout << "Screen can be used.\n";else cout

<< "In use.\n";

x.set_status(INUSE);

if(x.idle(y)) cout << "Screen can be used.\n";else cout

<< "In use.\n";

return 0;}

Bởi vì idle() là thành viên của C1, nó có thể truy cập trực tiếp đến biến status

cả đối tượng kiểu C1. Do đó, chỉ những đối tượng kiểu C2 mới có thể gọi được

hàm idle().

Có hai giới hạn quan trọng khi áp dụng hàm friend.Đầu tiên, dẫn xuất của một

lớp không thừa kế hàm friend.Thứ hai, hàm friend có thể không có lớp nào

chứa nó. Do đó chúng có thể không được định nghĩa như là static hoặc extern.

Page 158: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

158

9.1.2. Lớp Friend (Friend Class)

Một lớp có thể là friend của một lớp khác.Trong trường hợp này, lớp friend và

tất cả các hàm thành viên của lớp đó có khả năng truy cập đến thành viên

private được định nghĩa trong các lớp khác. Ví dụ:

Ví dụ 10.4:

// Using a friend class.

#include <iostream>

using namespace std;

class TwoValues {

int a;int b;

public:

TwoValues(int i, int j) { a = i; b = j; }

friend class Min;};

class Min {public:

int min(TwoValues x);};

int Min::min(TwoValues x){

return x.a < x.b ? x.a : x.b;}

int main(){ TwoValues ob(10, 20); Min m;

cout<< m.min(ob);

return 0;}

Trong ví dụ này, lớp Min có thể truy cập đến các biến private a và b được định

nghĩa trong lớp TwoValues.

Một điều cần được hiểu rõ ràng: khi một lớp là bạn của lớp khác, nó chỉ có thể

truy cập đến biến bên trong của lớp khác. Nó không thừa kế lớp khác.Đặc biệt,

thành viên của lớp đầu tiên không trờ thành thành viên của lớp friend.

Lớp bạn rất hiếm khi được sử dụng.Chúng được hỗ trợ để cho phép các trường

hợp đặc biệt cần được điều khiển.

9.2. Khai báo viết chồng toán từ

Một hàm toán tử thường có dạng sau:

Page 159: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

159

ret-type class-name::operator#(arg-list)

{

// operations

}

Bình thường, hàm toán tử thường trả về đối tượng của lớp mà chúng đang tính

toán, nhưng ret-type có thể là bất kỳ kiểu biến nào mà C++ cho phép.Ký tự #

thể hiện vị trí có thể thay thế được.Khi bạn tạo ra một hàm toán tử, hãy thay thế

# bằng toán tử mà bạn đang muốn cài đặt. Ví dụ, nếu bạn muốn nạp chồng hàm

toán tử /, sử dụng operator/. Khi bạn đang nạp chồng một toán tử một ngôi,

arg-list là một danh sách rỗng.Khi bạn nạp chồng toán tử hai ngôi, arg-list sẽ

chứa một tham số.(Nguyên nhân cho trường hợp đặc biệt sẽ được mô tả trong

phần sau).

9.3. Viết chồng hàm một số toán tử toán học

Sau đây là ví dụ về nạp chồng toán tử. Chương trình tạo ra một lớp gọi là loc,

chứa kinh độ và vĩ độ. Nó nạp chồng toán tử + cho lớp này. Hãy xem đoạn mã

sau cẩn thận và chú ý đến định nghĩa của toán tử operator+():

Ví dụ 10.5:

#include <iostream>

using namespace std;

class loc {

int longitude, latitude;public:

loc() {}loc(int lg, int lt) {

longitude = lg;latitude = lt;

}

void show() {

cout<< longitude << " ";

cout<< latitude << "\n";}

loc operator+(loc op2);

};

// Overload + for loc.loc loc::operator+(loc op2){

Page 160: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

160

loc temp;temp.longitude = op2.longitude +

longitude;temp.latitude = op2.latitude + latitude;return

temp;

}

int main(){

loc ob1(10, 20), ob2( 5, 30);ob1.show(); // displays 10

20ob2.show(); // displays 5 30

ob1 = ob1 + ob2;ob1.show(); // displays 15 50

return 0;}

Như bạn đã thấy, operator+() có một tham số mặc dù nó nạp chồng toán tử hai

ngôi. (Bạn có thể trông chờ hai tham số thể hiện hai toán hạng của toán tử hai

ngôi). Nguyên nhân mà toán tử operator+() chỉ nhận một tham số là toán hạng

ở phía bên trái của + được truyền không tường minh cho hàm thông qua con trỏ

this. Toán hạng phía bên phải được truyền là tham số op2. Thực tế, toán hạng

phía bên trái được tuyền bằng cách sử dụng con trỏ thí đảm bảo điều quan trọng

sau: Khi toán tử hai ngôi được nạp chồng, đối tượng bên trái sẽ tự động gọi đến

hàm toán tử.

Như đã đề cập ở trên, đó là điều thông thường cho nạp chồng hàm toán tử khi

trả về đối tượng của lớp mà toán tử đang được tinh toán.Bằng cách đó, nó cho

phép toán tử được sử dụng trong biểu thức phức tạp hơn. Ví dụ, nếu toán tử

operator+() trả về kiểu khác, biểu thức sau sẽ không còn đúng nữa:

ob1 = ob1 + ob2;

Để tính tổng của ob1 và ob2 để gán cho ob1, giá trị trả về của toán tử phải là

đối tượng có kiểu loc.

Ngoài ra, operator+() trả về một đối tượng kiểu loc cũng cho phép biểu thức

sau được thực hiện

(ob1 + ob2).show();

Trong trường hợp này ob1 + ob2 sẽ tạo ra một đối tượng tạm thời và bị hủy đi

ngay sau khi hàm show() kết thức.

Điều đó rất cần thiết để hiểu rằng hàm toán tử có thể trả về bất cứ kiểu nào và

kiểu trả về hoàn toàn phụ thuộc vào những đặc tả trong chương trình của

bạn.Thông thường, hàm toán tử sẽ trả về đối tượng của lớp mà nó đang xử lý.

Page 161: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

161

Điều cuối cùng về toán tử operator+(): nó không thay đổi toán hạng. Bởi vì

theo truyền thồng khi sử dụng toán tử cộng, chúng ta không thay đổi giá trị của

toán hạng, điều đó hoàn toàn có ý nghĩa khi nạp chồng hàm không thay đổi toán

hạng. (Ví dụ, 5 + 7 = 12, nhưng cả 5 và 7 đều không thay đổi). Mặc dù vậy, bạn

hoàn toàn tự do khi thực thi bất cứ toán tử nào bên trong hàm toán tử, thông

thường chúng ta nên đặt vào trong ngữ cảnh sử dụng của toán tử.

Chương trình kế tiếp thêm ba toán tử được nạp chồng cho lớp loc: -, toán tử một

ngôi ++.

Ví dụ 10.6:

#include <iostream>

using namespace std;

class loc {

int longitude, latitude;public:

loc() {} // needed to construct temporariesloc(int lg,

int lt) {

longitude = lg;latitude = lt;

}void show() {

cout<< longitude << " ";cout<< latitude << "\n";

}

loc operator+(loc op2);loc operator-(loc op2);loc

operator++();

};

// Overload + for loc.loc loc::operator+(loc op2){

loc temp;temp.longitude = op2.longitude +

longitude;temp.latitude = op2.latitude + latitude;return

temp;

}

// Overload - for loc.loc loc::operator-(loc op2){

loc temp;// notice order of operandstemp.longitude =

longitude - op2.longitude;temp.latitude = latitude -

op2.latitude;return temp;

}

// Overload prefix ++ for loc.loc loc::operator++(){

longitude++;latitude++;

Page 162: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

162

return *this;}

int main(){

loc ob1(10, 20), ob2( 5, 30), ob3(90,

90);ob1.show();ob2.show();++ob1;ob1.show(); // displays

11 21

ob2 = ++ob1;ob1.show(); // displays 12 22ob2.show(); //

displays 12 22

return 0;}

Đầu tiên chúng ta sẽ xem hàm operator-().Chú ý đến thứ tự của toán hạng

trong phép trừ.Để đảm bảo ý nghĩa của phép trừ, toán hạng phía bên trái ký tự

trừ được từ từ toán hạng bên trái. Do đó, nếu đối tượng phía bên trái gọi tới hàm

operator-(), dữ liệu của op2 cần được trừ đi từ dữ liệu của con trỏ this. Điều

quan trọng nhất là nắm được đối tượng gọi hàm toán tử.

Bây giờ, chúng ta cùng xem xét định nghĩa của operator++(). Như bạn thấy,

không có tham số nào được đưa vào định nghĩa.Vì ++ là toán tử một ngôi, nó

chi có một toán hạng được truyền không tường minh thông qua con trỏ this.

Chú ý rằng toán tử operator++() có thể thay thế giá trị của toán hạng. Ở đây,

toán hạng tăng giá trị lên của đối tượng có kiểu loc. Như trong những phần

trước, mặc dù bạn tự do trong việc tạo ra những toán tử, nhưng thông thường

các toán tử mới nên nhất quán về mặt ý nghĩa với toán tử gốc.

9.4. Viết chồng hàm toán tử gán

Trong phần này, chúng ta sẽ thực hiện việc nạp chồng toán tử gán, điều này cho

phép chúng ta thực hiện những biểu thức phức tạp hơn.Đoạn mã sau đây, chúng

ta sẽ áp dụng toán tử gán cho đối tượng có kiểu loc. Bạn nên xem xét khai báo

đối với toán tử gán.

Ví dụ 10.7

#include <iostream>

using namespace std;class loc {

int longitude, latitude;public:

loc() {} // needed to construct temporariesloc(int lg,

int lt) {

longitude = lg;latitude = lt;

}

Page 163: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

163

void show() {

cout<< longitude << " ";cout<< latitude << "\n";

}

loc operator=(loc op2);};

// Overload asignment for loc.loc loc::operator=(loc op2){

longitude = op2.longitude;latitude = op2.latitude;return

*this; // i.e., return object that generated call

}

int main(){

loc ob1(10, 20), ob2( 5, 30), ob3(90,

90);ob1.show();ob2.show();

ob1 = ob2 = ob3; // multiple assignmentob1.show(); //

displays 90 90ob2.show(); // displays 90 90

return 0;}

Trong C++, nếu toán tử = không được nạp chồng, toán tử gán mặc định sẽ được

tạo ra tự động cho mỗi lớp mà bạn định nghĩa. Toán tử gán mặc định này thực

hiện việc sao chép đơn giản từng thành viên của lớp. Bằng việc nạp chồng toán

tử =, bạn sẽ định nghĩa một cách tường minh hành vi toán tử gán của bạn thực

hiện giữa các lớp với nhau. Trong ví dụ trên, toán tử gán đối với lớp loc thực

hiện công việc tương tự như toán tử gán mặc định. Chú ý rằng, operator=()trả

về giá trị của con trỏ this, thể hiện đối tượng đã sinh ra lời gọi toán tử gán. Điều

này rất quan trọng nếu bạn muốn thực hiện phép gán đồng thời nhiều biến như

dưới đây:

ob1 = ob2 = ob3; // multiple assignment

Chú ý rằng, cũng giống như toán tử ++định nghĩa ở ví dụ trên, toán tử gán cũng

làm thay đổi giá trị của các toán hạng (toán hạng bên trái ký hiệu dấu bằng nhận

giá trị mới dựa vào toán hạng bên phải, giá trị mới hoàn toàn phụ thuộc vào

chiến lược bạn cài đặt toán tử gán mới).

9.5. Viết chồng hàm toán tử so sánh

Để thực hiện việc so sánh hai đối tượng chúng ta có thể nạp chồng các toán tử

so sánh. Ở đây, các toán tử so sánh là ==, !=, >, <, >= và <=. Đoạn mã sau đây

là ví dụ về nạp chồng toán tử so sánh == cho đối tượng có kiểu loc. (Để đơn

Page 164: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

164

giản, hai đối tượng kiểu loc được gọi là bằng nhau nếu thành viên là giống hệt

nhau).

Ví dụ 10.8:

#include <iostream>

using namespace std;

class loc {

private:

int longtitude, latitude;

public:

loc (){}

loc (int lg, int lt){

longtitude = lg;

latitude = lt;

}

void show (){

cout<< longtitude << " ";

cout<< latitude << endl;

}

bool operator==(const loc &op2);

};

bool loc::operator==(const loc &op2){

return this->longtitude == op2.longtitude && this-

>latitude == op2.latitude;

}

int main (int argc, char *argv[]){

Page 165: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

165

loc ob1 (10, 30), ob2(10, 30), ob3(4, 5);

if (ob1 == ob2){

cout<< "Ob1 and Ob2 are equals." <<endl;

} else {

cout<< "Ob1 and Ob2 are not equals." <<endl;

}

if (ob1 == ob3){

cout<< "Ob1 and Ob3 are equals." <<endl;

} else {

cout<< "Ob1 and Ob3 are not equals." <<endl;

}

return 0;

}

Trong ví dụ trên, chúng ta để ý rằng tham số op2 là toán hạng thứ 2 bên phải

dấu ==, vì toán tử so sánh về mặt ý nghĩa có nhiệm vụ đánh giá tương quan giữa

hai đối tượng, nên toán tử so sánh không làm thay đổi giá trị của các toán hạng.

Vì vậy, việc sử dụng khóa const giúp đảm bảo việc thay đổi giá trị toán hạng

op2 là bị cấm.Tương tự như toán hạng số học, ta cũng có thể cài đặt các toán tử

so sánh ở mức độ toàn cục, không phải là thành viên của lớp.

9.6. Viết chồng toán tử << và >>

Như bạn đã biết, toán tử << và >> có thể được nạp chồng trong C++ để thực thi

toán tử I/O trong các kiểu xây dựng sẵn của C++. Bạn cũng có thể nạp chồng

những toán tử để thực thi trên kiểu dữ liệu do bạn định nghĩa.

Trong ngôn ngữ C++, toán tử đầu ra << được tham chiếu đến như là intersection

operator, bởi vì nó thêm các ký tự vào luồng.Tương tự như vậy toán tử >> được

gọi là extraction operator bởi vì nó tách các ký tự ra khỏi luồng. Hàm được nạp

chồng toán tử thêm và trích xuất được gọi là inserters và extractors.

Page 166: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

166

9.6.1. Tạo riêng toán tử <<

Việc tạo ra một inserter cho lớp bạn tự định nghĩa rất đơn giản. Tất cả các hàm

inserter đều có dạng sau:

ostream&operator<< (ostream &stream, class_type obj )

{

// body of inserter

returnstream;

}

Chú ý rằng, đối tượng trả về của hàm này tham chiếu đến kiểu ostream.(Nhờ

rằng, ostream là lớp dẫn xuất từ ios hỗ trợ đầu ra).Hơn nữa, tham số đầu tiên

của hàm tham chiếu đến luồng ra.Tham số thứ hai là đối tượng cần được thêm

vào.(Tham số thứ hai cũng có thể là đối tượng đang được thêm vào).Cuối cùng,

hàm inserter cần trả về luồng trước khi kết thúc hàm.Điều này cho phép hàm

inserter có thể sử dụng các biểu thức I/O phức tạp hơn.

Bên trong hàm inserter, bạn có thể thêm bất kỳ hàm hoặc toán tử mà bạn

muốn.Điều này cho phép hàm inserter hoàn toàn phụ thuộc vào bạn.Tuy nhiên,

để đảm bảo hàm inserter hiệu quả trong thực tế, bạn nên hạm chế các toán tử để

đưa ra các thông tin vào luồng. Ví dụ, có một inserter để tính pi với 30 ký tự sau

dấu phẩy như là hiệu ứng trượt với toán tử insertion có thể là một ý tưởng

không tốt.

Để thể hiện một inserter tùy biến, chúng tôi tạo một đối tượng kiểu phonebook,

như sau:

Ví dụ 10.9:

#include <iostream>

#include <cstring>

using namespace std;

class phonebook {

public:

char name[80];int areacode;int prefix;int num;

phonebook(char *n, int a, int p, int nm){

strcpy(name, n);

areacode = a;

prefix = p;

Page 167: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

167

num = nm;}

};

Lớp này quản lý tên người vào số điện thoại. Đây là một cách để tạo ra hàm

inserter cho đối tượng có kiểu phonebook

Ví dụ 10.10:

// Display name and phone number.

ostream&operator<<(ostream &stream, phonebook o)

{

stream<< o.name << " ";stream<< "(" << o.areacode << ")

";stream<< o.prefix << "-" << o.num << "\n";return

stream; // must return stream

}

Sau đây là chương trình thể hiện hàm phonebook inserter:

Ví dụ 10.11:

#include <iostream>

#include <cstring>

using namespace std;

class phonebook {

public:

char name[80];int areacode;int prefix;int

num;phonebook(char *n, int a, int p, int nm){

strcpy(name, n);areacode = a;prefix = p;num = nm;

}};

// Display name and phone number.ostream&operator<<(ostream

&stream, phonebook o){

stream<< o.name << " ";

stream<< "(" << o.areacode << ") ";

stream<< o.prefix << "-" << o.num << "\n";

return stream; // must return stream

}

int main()

{

Page 168: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

168

phonebook a("Ted", 111, 555, 1234);

phonebook b("Alice", 312, 555, 5768);

phonebook c("Tom", 212, 555, 9991);

cout<< a << b << c;

return 0;

}

Chương trình sẽ hiển thị:

Ted (111) 555-1234

Alice (312) 555-5768

Tom (212) 555-9991

Trong những ví dụ ở trên, chú ý rằng phonebook inserter không phải là thành

viên của phonebook.Mặc dù vậy, vấn đề dường như hơi khó hiểu, nhưng

nguyên nhân rất đơn giản để hiểu.Khi một hàm toán tử của bất kỳ loại là thành

viên của lớp, toán tử bên trái (được gọi tới thông qua toán tử this) là đối tượng

tạo ra lời gọi tới hàm toán tử.Ngoài ra, đối tượng này là kiểu đối tượng của lớp

mà hàm toán tử là thành viên.Không có cách nào thay đổi chúng cả.Nếu hàm

toán tử được nạp chồng là thành viên của lớp, toán hạng phía trái phải là đối

tượng của lớp đó.Tuy nhiên, khi bạn nạp chồng hàm inserters, toán hạng phía

trái là một luồng và toán hạng phía phải là đối tượng của lớp đó. Vì vậy, nạp

chồng inserters không thể là thành viên của lớp mà chúng đang được nạp chồng

hàm. Các biến name, areacode, prefix, và num là các biến public trong

chương trình ở trên do đó chúng có thể truy cập bởi hàm inserter.

Thực tế các inserter không thể là thành viên của lớp mà chúng được định nghĩa

dường như là thất bại trong C++ Nếu nạp chồng hàm inserters không phải là

thành viên, làm thế nào để chúng có thể truy cập các thành viên private của lớp?

Trong các ví dụ trên, tất cả các thành viên đều là public.Tuy nhiên, đóng gói là

một thành phần chủ yếu của lập trình hướng đối tượng.Nó yêu cầu tất cả dữ liệu

sẽ được public sẽ xung đột với nguyên lý này. May mắn thay, chúng ta có giải

pháp cho tình huống này: định nghĩa inserters là friend của lớp. Điều đó đảm

bảo tham số đầu tiên đối với hàm nạp chồng inserter là luồng và cho phép hàm

có thể truy cập tới các thành viên private của lớp mà nó nạp chồng. Sau đây là

ví dụ định nghĩa inserter trở thành hàm friend:

Ví dụ 10.12:

Page 169: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

169

#include <iostream>

#include <cstring>

using namespace std;

class phonebook {

// now privatechar name[80];int areacode;int prefix;int

num;public:phonebook(char *n, int a, int p, int nm){

strcpy(name, n);areacode = a;prefix = p;num = nm;

}friend ostream &operator<<(ostream &stream, phonebook

o);

};

// Display name and phone number.ostream&operator<<(ostream

&stream, phonebook o){{

stream<< o.name << " ";stream<< "(" << o.areacode << ")

";stream<< o.prefix << "-" << o.num << "\n";return

stream; // must return stream}

int main(){

phonebook a("Ted", 111, 555, 1234);phonebook b("Alice",

312, 555, 5768);phonebook c("Tom", 212, 555, 9991);cout<<

a << b << c;

return 0;}

Khi bạn định nghĩa thân hàm inserter, nên đảm bảo nó càng tổng quát càng

tốt.Ví dụ, hàm inserter được chỉ ra trong ví dụ trước đó có thể sử dụng với bất

kỳ luồng nào bởi thân hàm trực tiếp thêm nó vào luồng, mà luồng đó được

truyền vào hàm inserter. Trong khi đó, nó sẽ sai để viết

stream<< o.name << " ";

cout<< o.name << " ";

Điều này sẽ ảnh hưởng tới các đoạn mã nguồn cố định cout là luồng ra.Phiên

bản chính sẽ làm việc với bất kỳ luồng nào, bao gồm những liên kết đến tệp tin

trên đĩa.Mặc dù, trong một số trường hợp, đặc biệt thiết bị đầu ra được sử dụng,

bạn sẽ muốn hard-code luồng ra, trong nhiều trường hợp bạn sẽ không làm vậy.

Trong trường hợp chung, hàm inserter càng uyển chuyển, chúng càng có giá trị.

Chú ý: hàm inserter của lớp phonebook làm việc tốt trừ phi giá trị của

numgiống như 0034, trong trường hợp các chữ zero có trước sẽ không được

Page 170: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

170

hiển thị. Để sửa chúng, bạn có thể chuyển num thành xâu hoặc bạn có thể gán

hàm lấp đày ký tự zero và sử dụng width() hàm định dạng để sinh ra dạng có

chữ không. Đáp án để lại cho người đọc như là bài tập.

Trước khi chuyển sang hàm trích xuất, chúng ta xem xét những ví dụ của hàm

inserter. Một hàm inserter cần được giới hạn để làm việc với xâu ký tự.Một hàm

inserter có thể sử dụng để hiển thị dữ liệu ở bất kỳ dạng nào.Ví dụ, hàm inserter

đối với lớp là thành phần của hệ thống CAD có thể hiển thị các chỉ thị.Hàm

inserter có thể sinh ra các ảnh đò họa.Hàm inserter cho chương trình dạng

Windows có thể hiển thị các hộp hội thoại.Dưới đây là ví dụ hiển thị các hộp

thoại trên màn hình thay vì các xâu ký tự. (Bởi vì C++ không định nghĩa thư

viện đồ họa, chương trình sử dụng ký tự để vẽ hộp hội thoại, tương tự như

chương trình hỗ trợ đồ họa).

Ví dụ 10.13

#include <iostream>

using namespace std;

class box {

int x, y;public:

box(int i, int j) { x=i; y=j; }friend ostream

&operator<<(ostream &stream, box o);

};

// Output a box.ostream&operator<<(ostream &stream, box o){

register int i, j;

for(i=0; i<o.x; i++)

stream<< "*";

stream<< "\n";

for(j=1; j<o.y-1; j++) {

for(i=0; i<o.x; i++)

if(i==0 || i==o.x-1) stream << "*";else stream

<< " ";

stream<< "\n"; }

for(i=0; i<o.x; i++)

stream<< "*";stream<< "\n";

Page 171: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

171

return stream;}

int main(){

box a(14, 6), b(30, 7), c(40, 5);

cout<< "Here are some boxes:\n";cout<< a << b << c;

return 0;}

9.6.2. Tạo riêng toán tử >>

Tách dòng (extractor) là toán tử ngược lại với toán tử chèn. Dạng tổng quát của

toán tử extractor được định nghĩa như sau:

istream&operator>> (istream &stream, class_type &obj )

{

// body of extractor

returnstream;

}

Extractors sẽ trả về tham chiếu tới luồng của kiểu istream, là luồng vào. Tham

số đầu tiên cần phải là tham chiếu tới luồng có kiểu là istream. Chú ý rằng,

tham số thứ hai cần phải là tham số tới đối tượng của lớp mà extractor được nạp

chồng.Nó cũng là đối tượng có thể thay đổi bởi toán tử đầu vào (extractor).

Extractor trả về tham chiếu đến luồng kiểu istram, như là luồng vào.Tham số

thứ hai cần được tham chiếu tới đối tượng của lớp mà extractor được nạp

chồng.Đây cũng là đối tượng có thể thay đổi bởi toàn tử đầu vào.

Tiếp tục với lớp phonebook, sau đây là một cách để xây dựng hàm extractor

Ví dụ 10.14:

istream&operator>>(istream &stream, phonebook &o)

{

cout<< "Enter name: ";

stream>> o.name;

cout<< "Enter area code: ";

stream>> o.areacode;

cout<< "Enter prefix: ";

stream>> o.prefix;

Page 172: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

172

cout<< "Enter number: ";

stream>> o.num;

cout<< "\n";

return stream;

}

Chú ý mặc dù đây là hàm đầu vào, nó thực hiện hiện thị thông tin để thông báo

cho người dùng.Điểm chính là mặc dùn mục đích chính của một extractor là

nhập vào, nó có thể thực thi bất kỳ toán tử cần thiết nào để đạt mục đích cuối.

Tuy nhiên, cũng như inserters, tốt nhất là giữ hành động thực thi bởi trực tiếp

extractor đối với input. Nếu bạn không làm, bạn có thể chạy với rủi ro của mất

một vài thành phần của cấu trúc.

Ví dụ 10.15:

#include <iostream>

#include <cstring>

using namespace std;

class phonebook {

char name[80];

int areacode;

int prefix;

int num;

public:

phonebook() { };

phonebook(char *n, int a, int p, int nm)

{

strcpy(name, n);

areacode = a;

prefix = p;

num = nm;

}

friend ostream &operator<<(ostream &stream, phonebook o);

Page 173: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

173

friend istream &operator>>(istream &stream, phonebook

&o);

};// Display name and phone number.ostream&operator<<(ostream

&stream, phonebook o){

stream<< o.name << " ";stream<< "(" << o.areacode << ")

";stream<< o.prefix << "-" << o.num << "\n";return

stream; // must return stream

}

// Input name and telephone number.istream&operator>>(istream

&stream, phonebook &o){

cout<< "Enter name: ";stream>> o.name;cout<< "Enter area

code: ";stream>> o.areacode;cout<< "Enter prefix:

";stream>> o.prefix;cout<< "Enter number: ";stream>>

o.num;cout<< "\n";return stream;

}

int main(){

phonebook a;cin>> a;cout<< a;return 0;

}

Thực sự, extractor của phonebook là kém hiệu quả bởi vì biểu thức cout cần

thiết nếu đầu vào được kết nối với thiết bị đang rỗi như là màn hình (khi mà

luồng vào là cin).Nếu extractor thường được sử dụng trong luồng kết nối thiết bị

tới đĩa cứng, như vậy biểu thức count không thực sự tốt.Một điều thú vị, bạn có

thể muôn kiểm tra xem luồng vào có liên hệ với cin hay không?Như ví dụ dưới

đây.

if(stream == cin) cout << "Enter name: ";

9.7. Nạp chồng new và delete

9.7.1. Nạp chồng new và delete

Chúng ta có thể nạp chồng new và delete. Bạn có thể lựa chọn việc thực hiện

nó nếu bạn muốn sử dụng các phương thức cấp phát đặc biệt.Ví dụ, bạn muốn

cập phát bộ nhớ cho dãy mà tự động sử dụng tệp tin trên đĩa cứng như là bộ nhớ

ảo khi head đã đầy. Thật là đơn giản khi chúng ta nạp chồng toán tử.

Mã giả cho hàm nạp chồng new và delete được đưa ra dưới đây:

// Allocate an object.

void *operator new(size_t size)

Page 174: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

174

{

/* Perform allocation. Throw bad_alloc on failure.

Constructor called automatically. */

return pointer_to_memory;

}

// Delete an object.

void operator delete(void *p)

{

/* Free memory pointed to by p.

Destructor called automatically. */

}

Kiểu size_t là kiểu được định nghĩa thể hiện số lượng các ô nhớ nhiều nhất mà

có thể được khởi tạo. (size_t về bản chất là kiểu số nguyên không dấu). Tham

số size là một số thể hiện số lượng bytes cần thiết để lưu trữ đối tượng đang

được khởi tạo. Đây là khối bộ nhớ mà phiên bản của hàm new do bạn viết cần

khởi tạo. Nạp chồng hàm new cần trả về con trỏ tới bộ nhớ được khởi tạo, hoặc

ném ra ngoại lệ bad_alloc nếu có lỗi xảy ra trong quá trình cấp phát bộ nhớ.

Sau những ràng buộc này, nạp chồng hàm new có thể làm bất cứ điều gì nữa mà

bạn muốn. Khi bạn khởi tạo một đối tượng sử dụng new (phiên bản của bạn

hoặc mặc định), hàm kiến tạo của đối tượng được gọi đến một cách tự động.

Hàm delete nhận một con trỏ tới miền của bộ nhớ cần được giải phóng. Nó sẽ

trả lại phần bộ nhớ được gán trước đây lại cho hệ thống.Khi một đối tượng bị

xóa, hàm hủy được tự động gọi tới.

Toán tử new và delete có thể được nạp chồng toàn cục, do đó khi sử dụng

những toán tử này, phiên bản chỉnh sửa của bạn sẽ được ưu tiên gọi đến. Chúng

cũng có thể nạp chồng liên quan tới một hoặc nhiều lớp.Chúng ta bắt đầu với ví

dụ về nạp chồng liên quan đến các lớp. Để làm vấn đề đơn giản, chúng ta sẽ

không sử dụng mô hình khởi tạo mới. Thay vào đó, nạp chồng toán tử đơn giản

chỉ gọi đến hàm malloc() và free() trong thư viện chuẩn. (Trong chương trình

của bạn, bạn có thể đưa ra mô hình cấp phát riêng của mình).

Để nạp chồng toán tử new và delete cho một lớp, phương pháp đơn giản là nạp

chồng toán tử như là một hàm thành viên của lớp đó. Ví dụ sau đây thể hiện nạp

chồng toán tử new và delete cho một lớp:

Page 175: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

175

Ví dụ 10.16:

#include <iostream>#include <cstdlib>#include <new>

using namespace std;

class loc {

int longitude, latitude;

public:

loc() {}loc(int lg, int lt) {

longitude = lg;latitude = lt;

}

void show() {

cout<< longitude << " ";cout<< latitude << "\n";

}

void *operator new(size_t size);void operator delete(void

*p);

};

// new overloaded relative to loc.void *loc::operator

new(size_t size){

void *p;cout<< "In overloaded new.\n";

p = malloc(size);

if(!p) {

bad_alloc ba;throw ba;

}return p;

}

// delete overloaded relative to loc.void loc::operator

delete(void *p){

cout<< "In overloaded delete.\n";free(p);

}int main(){

loc *p1, *p2;

try {

p1 = new loc (10, 20);

} catch (bad_alloc xa) {

cout<< "Allocation error for p1.\n";return 1;

Page 176: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

176

}

try {

p2 = new loc (-10, -20);} catch (bad_alloc xa) {

cout<< "Allocation error for p2.\n";return 1;;

}

p1->show();p2->show();

delete p1;delete p2;

return 0;}

Kết quả của chương trình như sau:

In overloaded new.

In overloaded new.

10 20

-10 -20

In overloaded delete.

In overloaded delete.

Khi new và delete được đặc tả cho một lớp, cách sử dụng những toán tử này đối

với các dữ liệu khác là nguyên nhân khiến các toán tử gốc của new và

deleteđược sử dụng. Nạp chồng toán tử chỉ được áp dụng cho kiểu mà chúng

được định nghĩa. Điều đó có nghĩa là nếu bạn thêm dòng sau vào hàm main(),

toán tử mặc định new sẽ được thực thi:

int *f = new float; // uses default new

Bạn có thể nạp chồng toán tử new và delete tổng quát bằng cách nạp chồng

chúng ở ngoài các định nghĩa lớp. Khi toán tử new và delete được nạp chồng

tổng quát, hàm new và delete mặc định của C++ sẽ bị loại bỏ và phiên bản mới

của toán tử này sẽ được sử dụng với bất cứ yêu cầu khởi tạo nào. Tất nhiên, nếu

bạn định nghĩa bất kỳ một phiên bản mới nào của toán tử new và delete liên

quan tới một hoặc nhiều lớp, thì phiên bản của toán tử new và delete đặc tả mới

này sẽ được sử dụng khi khởi tạo các đối tượng của lớp mà chúng được định

nghĩa.Ngoài ra, khi toán tử new và delete được sử dụng, trình biên dịch đầu tiên

sẽ kiểm tra xem chúng có được định nghĩa liên quan đến lớp nào đó hay

không.Nếu có, toán tử được đặc tả sẽ được sử dụng.Nếu không, C++ sẽ sử dụng

Page 177: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

177

toán tử new và delete tổng quát.Nếu chúng được nạp chồng thì phiên bản nạp

chồng sẽ được sử dụng.

Ta có ví dụ sau:

Ví dụ 10.17:

#include <iostream>#include <cstdlib>#include <new>

using namespace std;

class loc {

int longitude, latitude;public:

loc() {}

loc(int lg, int lt) {

longitude = lg;latitude = lt;

}

void show() {

cout<< longitude << " ";cout<< latitude << "\n";

}};

// Global newvoid *operator new(size_t size){

void *p;p = malloc(size);if(!p) {

bad_alloc ba;

throw ba;}return p;

}

// Global deletevoid operator delete(void *p){

free(p);}

int main(){

loc *p1, *p2;float *f;

try {

p1 = new loc (10, 20);} catch (bad_alloc xa) {

cout<< "Allocation error for p1.\n";return 1;;

}

try {

p2 = new loc (-10, -20);} catch (bad_alloc xa) {

Page 178: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

178

cout<< "Allocation error for p2.\n";

return 1;;}try { f = new float; // uses

overloaded new, too} catch (bad_alloc xa) {

cout<< "Allocation error for f.\n";return 1;;

}

*f = 10.10F;cout<< *f << "\n";

p1->show();p2->show();

delete p1;delete p2;delete f;

return 0;}

Chạy chương trình sau để kiểm chứng lại rằng toán tử new và delete được xây

dựng sẵn đã bị nạp chồng.

9.7.2. Nạp chồng new và delete cho mảng.

Nếu bạn muốn cấp phát bộ nhớ cho mảng các đối tượng sử dụng phương pháp

cấp phát của bạn, bạn sẽ cần nạp chồng new và delete một lần nữa. Để cấp phát

và giải phóng mảng, bạn cần sử dụng khai báo sau của new và delete.

// Allocate an array of objects.

void *operator new[](size_t size) { /* Perform allocation.

Throw bad_alloc on failure. Constructor for each element

called automatically. */

return pointer_to_memory;}

// Delete an array of objects.void operator delete[](void *p){

/* Free memory pointed to by p. Destructor for each

element called automatically. */}

Khi bạn khởi tạo một mảng, hàm khởi tạo của mỗi đối tượng trong mảng sẽ

được gọi tự động.Khi giải phóng bộ nhớ của mảng, mỗi hàm hủy của đối tượng

sẽ được tự động gọi tới Bạn sẽ không cần thực thi mã nguồn để làm việc này.

Đoạn chương trình sau cung cấp ví dụ cho việc cấp phát bộ nhớ cho mảng đối

tượng kiểu loc.

Ví dụ 10.18:

#include <iostream>#include <cstdlib>#include <new>

using namespace std;

class loc {

int longitude, latitude;public:

Page 179: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

179

loc() {longitude = latitude = 0;}

loc(int lg, int lt) {

longitude = lg;

latitude = lt;}

void show() {

cout<< longitude << " ";cout<< latitude << "\n";

}

void *operator new(size_t size);void operator delete(void

*p);

void *operator new[](size_t size);void operator

delete[](void *p);

};

// new overloaded relative to loc.void *loc::operator

new(size_t size){

void *p;cout<< "In overloaded new.\n";p =

malloc(size);if(!p) {

bad_alloc ba;

throw ba;}return p;

}

// delete overloaded relative to loc.void loc::operator

delete(void *p){

cout<< "In overloaded delete.\n";free(p);

}

// new overloaded for loc arrays.void *loc::operator

new[](size_t size){

void *p;cout<< "Using overload new[].\n";p =

malloc(size);if(!p) {

bad_alloc ba;throw ba;

}return p;

}

// delete overloaded for loc arrays.void loc::operator

delete[](void *p){

cout<< "Freeing array using overloaded

delete[]\n";free(p);

Page 180: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

180

}

int main(){

loc *p1, *p2;int i;

try { p1 = new loc (10, 20); // allocate an

object} catch (bad_alloc xa) {

cout<< "Allocation error for p1.\n";return 1;;

}

try {

p2 = new loc [10]; // allocate an array} catch

(bad_alloc xa) {

cout<< "Allocation error for p2.\n";return 1;

}p1->show();

for(i=0; i<10; i++)

p2[i].show();

delete p1; // free an objectdelete [] p2; // free an

array

return 0;}

9.7.3. Nạp chồng new và delete không có throw

Bạn cũng có thể nạp chồng phiên bản không sử dụng throw cho new và delete

(không có ngoại lệ được ném ra khi có lỗi). Bạn có thể sử dụng mô hình sau:

Ví dụ 10.19:

// Nothrow version of new.void *operator new(size_t size,

const nothrow_t &n){

// Perform allocation.

if(success) return pointer_to_memory;else return 0;

}

// Nothrow version of new for arrays.void *operator

new[](size_t size, const nothrow_t &n){

// Perform allocation. if(success) return

pointer_to_memory;else return 0;

}

Page 181: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

181

void operator delete(void *p, const nothrow_t &n){ // free

memory}void operator delete[](void *p, const nothrow_t &n){

// free memory}

Kiểu nothrow_t được định nghĩa trong header new. Đây là một dạng đối tượng

nothrow. Tham số nothrow_t không được sử dụng.

Page 182: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

182

Chương 10. Tính chất của Lớp

10.1. Tính đóng gói và quyền truy cập

Tính đóng gói là cơ chế liên kết mã và dữ liệu mà nó thao tác và giữ cho cả hai

được an toàn khỏi sự can thiệp từ bên ngoài và do sử dụng sai. Trong ngôn ngữ

lập trình hướng đối tượng, mã và dữ liệu liên kết với nhau để tạo thành một

“hộp đen” độc lập.Trong hộp này là tất cả mã và dữ liệu cần thiết.Khi mã Trong

hộp này là tất cả mã và dữ liệu cần thiết.Khi mã và dữ liệu liên kết với nhau như

thế, một đối tượng được tạo ra.Nói cách khác, đối tượng là công cụ hỗ trợ cho

việc đóng kín.

Trong một đối tượng, mã, dữ liệu hoặc cả hai có thể là riêng của đối tượng hoặc

là chung. Mã hoặc dữ liệu riêng là thuộc về đối tượng đó và chỉ được truy cập

bở bộ phận của đối tượng.Nghĩa là mã hoặc dữ liệu riêng không thể truy cập bởi

các phần khác nhau của chương trình tồn tại ngoài đối tượng. Khi dữ liệu là

chung, các modul khác của chương trình có thể truy cập nó mặc dù nó được

định nghĩa trong một đối tượng. Các phần chung của một đối tượng được dùng

được dùng để cung cấp một giao diện cho điều khiển cho các phần riêng của đối

tượng.

10.2. Thừa kế

Kế thừa là một trong những tính chất của lập trình hướng đối tượng và là đặc

điểm quan trọng của của C++. Sử dụng kế thừa, bạn có thể tạo ra lớp tổng quát

mà định nghĩa

Khi lớp kế thừa từ lớp khác, các thành viên của lớp cơ sở trở thành thành viên

của lớp kế thừa. Việc kế thừa lớp được minh họa bằng dạng tổng quát như sau:

class tên_lớp_kế thừa : quyền_truy_cập tên_lớp_cơ_ sở {

// thân của lớp

};

Page 183: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

183

Việc truy cập các thành viên của lớp cơ sở bên trong lớp kế thừa được xác định

bởi quyền truy cập. Chỉ rõ quyền truy cập lớp cơ sở được thể hiện bởi các từ

khóa: public, private hoặc protected. Nếu không chỉ rõ quyền truy cập thì

quyền truy cập mặc định là private với lớp kế thừa.Nếu lớp kế thừa là cấu trúc

(struct) thì quyền truy cập mặc định là public.

Khi chỉ rõ quyền truy cập đối với lớp cơ sở là public, tất cả các thành viên

chung (public) của lớp cơ sở trở thành các thành viên chung của lớp kế thừa và

tất cả các thành viên bảo vệ (protected) của lớp cơ sở trở thành các thành viên

bảo vệ của lớp kế thừa. Ngược lại, các thành viên riêng của lớp cơ sở vần là

thành viên riêng của nó và không thể truy cập bởi lớp kế thừa. Ví dụ chương

trình dưới đây minh họa đối tượng kiểu base (cơ sở) được kế thừa bởi đối tượng

derived với quyền truy cập là public:

Ví dụ 11.1:

#include <iostream>

using namespace std;

class base {

int i, j;

public:

void set(int a, int b) { i=a; j=b; }

void show() { cout << i << " " << j << "\n"; }

};

class derived : public base {

int k;

public:

derived(int x) { k=x; }

void showk() { cout << k << "\n"; }

};

int main()

{

derived ob(3);

Page 184: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

184

ob.set(1, 2); // truy cập thành viên lớp cơ sở

ob.show(); // truy cập thành viên lớp cơ sở

ob.showk(); // sử dụng thành viên lớp kế thừa

return 0;

}

Như chương trình minh họa, vì lớp base được kế thừa như một lớp chung nên

các hàm thành viên chung của base là set và showx() trở thành hàm thành viên

chung của derived và do đó có thể được truy cập bởi bất kỳ phần nào của

chương trình. Đặc biệt, chúng được gọi một cách hợp lệ trong hàm main().

Khi lớp cơ sở được kế thừa bởi quyền truy cập private, tất cả các thành viên

bảo vệ và chung của lớp cơ sở trở thành thành viên riêng của lớp dẫn xuất.

Chương trình tiếp theo sẽ bị lỗi bởi vì cả hai hàm set() và show() đều là thành

viên riêng của lớp derived.

Ví dụ 11.2:

// This program won't compile.

#include <iostream>

using namespace std;

class base {

int i, j;

public:

void set(int a, int b) { i=a; j=b; }

void show() { cout << i << " " << j << "\n";}

};

// Public elements of base are private in derived.

class derived : private base {

int k;

public:

derived(int x) { k=x; }

Page 185: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

185

void showk() { cout << k << "\n"; }

};

int main()

{

derived ob(3);

ob.set(1, 2); // error, can't access set()

ob.show(); // error, can't access show()

return 0;

}

Thành viên bảo vệ (protectd members)

Từ khóa bảo vệ (protected), C++ cung cấp để tạo sự linh hoạt hơn trong việc kế

thừa. Như chúng ta biết trên đây, một lớp dẫn xuất không truy cập được các

thành viên riêng của lớp cơ sở. Nghĩa là nếu lớp dẫn xuất muốn truy cập một

thành viên nào đó của lớp cơ sở thì thành viên đó phải là chung. Tuy nhiên sẽ có

những lúc chúng ta muốn một thành viên của lớp cơ sở vẫn là riêng nhưng vẫn

cho phép lớp dẫn xuất truy cập tới nó. Để thực hiện mục đích này, chúng ta sử

dụng từ khóa protected.

Chỉ rõ truy cập protected tương đương với chỉ rõ private với một ngoại lệ duy

nhất là các thành viên được bảo vệ (protected) của lớp cơ sở có thể được truy

cập đối với các thành viên của một lớp dẫn xuất kế thừa từ lớp cơ sở đó. Bên

ngoài lớp cơ sở hoặc lớp dẫn xuất, các thành viên được bảo vệ không thể truy

cập. Dưới đây là chương trình minh họa:

Ví dụ 11.3:

#include <iostream>

using namespace std;

class base {

protected:

Page 186: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

186

int i, j; // private to base, but accessible by derived

public:

void set(int a, int b) { i=a; j=b; }

void show() { cout << i << " " << j << "\n"; }

};

class derived : public base {

int k;

public:

// derived may access base's i and j

void setk() { k=i*j; }

void showk() { cout << k << "\n"; }

};

int main()

{

derived ob;

ob.set(2, 3); // OK, known to derived

ob.show(); // OK, known to derived

ob.setk();

ob.showk();

return 0;

}

Trong ví dụ này, lớp base được kế thừa bởi lớp derived với quyền truy cập

public và biến i và j được khai báo với quyền truy cập protected. Vì vậy, hàm

setk() của lớp derived có thể truy cập chúng. Nếu i và j được khai báo với

Page 187: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

187

quyền truy cập private thì lớp derived không thể truy cập chúng và chương

trình bị lỗi.

Nếu lớp dẫn xuất được kế thừa vời quyền truy cập private thì tất cả thành viên

bảo vệ của lớp cơ sở sẽ trở thành thành viên riêng của lớp dẫn xuất. Chương

trình sau đây minh họa:

Ví dụ 11.4:

// This program won't compile.

#include <iostream>

using namespace std;

class base {

protected:

int i, j;

public:

void set(int a, int b) { i=a; j=b; }

void show() { cout << i << " " << j << "\n"; }

};

// Now, all elements of base are private in derived1.

class derived1 : private base {

int k;

public:

// this is legal because i and j are private to derived1

void setk() { k = i*j; } // OK

void showk() { cout << k << "\n"; }

};

// Access to i, j, set(), and show() not inherited.

class derived2 : public derived1 {

int m;

Page 188: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

188

public:

// illegal because i and j are private to derived1

void setm() { m = i-j; } // Error

void showm() { cout << m << "\n"; }

};

int main()

{

derived1 ob1;

derived2 ob2;

ob1.set(1, 2); // error, can't use set()

ob1.show(); // error, can't use show()

ob2.set(3, 4); // error, can't use set()

ob2.show(); // error, can't use show()

return 0;

}

Xem xét chương trinh trên, chúng ta thấy biến i và j trở thành biến riêng của lớp

derived1, vì vậy không thể truy cập được bởi lớp derived2. Do đó chương trình

bị lỗi.

Nếu lớp cơ sở có thể được kế thừa với quyền truy nhập protected bởi lớp dẫn

xuất thì các thành viên chung và bảo vệ của lớp cơ sở trở thành các thành viên

bảo vệ của lớp dẫn xuất. Dưới đây là chương trình minh họa:

Ví dụ 11.5:

#include <iostream>

using namespace std;

class base {

Page 189: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

189

protected:

int i, j; // private to base, but accessible by derived

public:

void setij(int a, int b) { i=a; j=b; }

void showij() { cout << i << " " << j << "\n"; }

};

// Inherit base as protected.

class derived : protected base{

int k;

public:

// derived may access base's i and j and setij().

void setk() { setij(10, 12); k = i*j; }

// may access showij() here

void showall() { cout << k << " "; showij(); }

};

int main()

{

derived ob;

// ob.setij(2, 3); // illegal, setij() is

// protected member of derived

ob.setk(); // OK, public member of derived

ob.showall(); // OK, public member of derived

// ob.showij(); // illegal, showij() is protected

// member of derived

Page 190: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

190

return 0;

}

Đa kế thừa

Có thể lớp dẫn xuất kế thừa từ hai hoặc nhiều lớp cơ sở. Ví dụ 11.5 minh họa

lớp derived được kế thừa từ 2 lớp base1 và base2:

Ví dụ 11.6:

#include <iostream>

using namespace std;

class base1 {

protected:

int x;

public:

void showx() { cout << x << "\n"; }

};

class base2 {

protected:

int y;

public:

void showy() {cout << y << "\n";}

};

// Inherit multiple base classes.

class derived: public base1, public base2 {

public:

void set(int i, int j) { x=i; y=j; }

Page 191: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

191

};

int main()

{

derived ob;

ob.set(10, 20); // provided by derived

ob.showx(); // from base1

ob.showy(); // from base2

return 0;

}

#include <iostream>

using namespace std;

class base {

public:

base() { cout << "Constructing base\n"; }

~base() { cout << "Destructing base\n"; }

};

Ví dụ 11.7:

class derived: public base {

public:

derived() { cout << "Constructing derived\n"; }

~derived() { cout << "Destructing derived\n"; }

};

int main()

{

derived ob;

Page 192: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

192

// do nothing but construct and destruct ob

return 0;

}

Như ví dụ minh họa, kế thừa có thể nhiều hơn một lớp, sử dụng dấu phẩy để

phân biệt các lớp cơ sở.Hơn nữa, cũng phải chỉ rõ quyền truy nhập đối với mỗi

lớp cơ sở.

10.3. Kế thừa tạo tử và hủy tử

Có hai câu hỏi chính xuất hiện liên quan đến tạo tử và hủy tử trong khi kế

thừa.Đầu tiên, khi nào hàm tạo tử và hủy tử của lớp cơ sở và lớp kế thừa được

gọi?Thứ hai, tham số của hàm tạo tử của lớp cơ sở được truyền như thế nào khi

kế thừa?Trong phần này chúng ta xem xét những vấn đế trên.

10.3.1. Khi hàm khởi tử và hủy tử được thi hành

Hàm tạo tử và hủy tử có thể có trong lớp cơ sở, lớp kế thừa hoặc có trong cả 2 lớp này.Điều

quan trọng là phải hiểu được thứ tự của các hàm được thi hành khi đối tượng của lớp kế thừa

tồn tại hoặc kết thúc (không tồn tại).

Ví dụ 11.7:

#include <iostream>

using namespace std;

class base {

public:

base() { cout << "Constructing base\n"; }

~base() { cout << "Destructing base\n"; }

};

class derived: public base {

public:

derived() { cout << "Constructing derived\n"; }

~derived() { cout << "Destructing derived\n"; }

Page 193: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

193

};

int main()

{

derived ob;

// do nothing but construct and destruct ob

return 0;

}

Chương trình trên chỉ đơn giản thực hiện hàm tạo tử và hủy tử khi gọi đối tượng

kế thừa ob. Khi thi hành, kết quả chạy chương trình như sau:

Constructing base

Constructing derived

Destructing derived

Destructing base

Như các bạn đã thấy, hàm tạo tử của lớp cở (base) được được thi hành sau đó là

hàm tạo tử của lớp kế thừa (derived). Tiếp theo khi đối tượng ob kết thúc, hàm

hủy tử của lớp kế thừa (derived) được gọi và sau đó là hàm hủy tử của lớp cơ

sở.

Kêt quả của chương trình trên có thể tổng quát hóa.Hàm tạo tử của lớp cơ sở

được thi hành trước hàm tạo tử của lớp kế thừa.Ngược lại, hàm hủy tử của lớp

kế thừa được thi hành trước hàm hủy tử của lớp cơ sở.

Trong trường hợp kế thừa nhiều mức (lớp kế thừa trở thành lớp cơ sở của lớp

khác), qui tắc tổng quát sau được áp dung: Hàm tạo tử được gọi theo thứ tự của

lớp kế thừa, hàm hủy tử được gọi theo thứ tự ngược lại. Ví dụ minh họa chương

trình sau:

Ví dụ 11.8:

Page 194: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

194

#include <iostream>

using namespace std;

class base {

public:

base() { cout << "Constructing base\n"; }

~base() { cout << "Destructing base\n"; }

};

class derived1 : public base {

public:

derived1() { cout << "Constructing derived1\n"; }

~derived1() { cout << "Destructing derived1\n"; }

};

class derived2: public derived1 {

public:

derived2() { cout << "Constructing derived2\n"; }

~derived2() { cout << "Destructing derived2\n"; }

};

int main()

{

derived2 ob;

// construct and destruct ob

return 0;

}

Kết quả chạy chương trình:

Page 195: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

195

Constructing base

Constructing derived1

Constructing derived2

Destructing derived2

Destructing derived1

Destructing base

Cũng với qui tắc tổng quát trên áp dụng cho trường hợp đa kế thừa. Ví dụ trong

chương trình sau:

Ví dụ 11.9:

#include <iostream>

using namespace std;

class base1 {

public:

base1() { cout << "Constructing base1\n"; }

~base1() { cout << "Destructing base1\n"; }

};

class base2 {

public:

base2() { cout << "Constructing base2\n"; }

~base2() { cout << "Destructing base2\n"; }

};

class derived: public base1, public base2 {

public:

derived() { cout << "Constructing derived\n"; }

~derived() { cout << "Destructing derived\n"; }

};

Page 196: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

196

int main()

{

derived ob;

// construct and destruct ob

return 0;

}

Kết quả chương trình như sau:

Constructing base1

Constructing base2

Constructing derived

Destructing derived

Destructing base2

Destructing base1

Như chúng ta thấy ở chương trình trên, hàm tạo tử được gọi theo thứ tự kế thừa,

từ trái qua phải như danh sách kế thừa base1, base2 của lớp derived. Trong khí

đó, hàm hủy tử được gọi theo thứ tự ngược lại từ phải qua trái.

10.3.2. Truyền tham số cho hàm tạo tử của lớp cơ sở

Không có ví dụ nào trên đây truyền tham số cho hoặc hàm tạo của lớp dẫn xuất

hoặc hàm tạo của lớp cơ sở.Tuy nhiên, có thể thực hiện được điều này. Khi chỉ

có lớp dẫn xuất thực hiện sự khởi đầu, tham số được truyền cho hàm tạo các lớp

dẫn xuất theo cách bình thường. Tuy nhiên, nếu cần truyền tham số cho hàm tạo

của lớp cơ sở thì phải thực hiện thay đổi một chút.Để làm điều này thì cần lập

một chuỗi truyền tham số.Đầu tiên, tất cả các tham số cần thiết cho cả lớp cơ sở

lẫn lớp dẫn xuất được truyền cho hàm tạo tử của lớp dẫn xuất, các tham số thích

hợp sẽ được truyền cho lớp cơ sở. Cú pháp để truyền tham số từ lớp dẫn xuất

đến lớp cơ sở như sau:

derived-constructor(arg-list) : base1(arg-list),

Page 197: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

197

base2(arg-list),

// ...

baseN(arg-list)

{

// body of derived constructor

}

Ở đây, base1, base2, …, baseN là tên các lớp cơ sở bị kế thừa bởi lớp dẫn xuất.

Chú ý rằng dấu hai (:) chấm phân biệt việc khai báo hàm tạo tử của lớp dân xuất

từ các lớp cơ sở được khai báo và các lớp cơ sở khai báo đó được phân biệt với

nhau bởi dấu phẩy (,) trong trường hợp đa kế thừa. Xem xét chương trình sau:

Ví dụ 11.10:

#include <iostream>

using namespace std;

class base {

protected:

int i;

public:

base(int x) { i=x; cout << "Constructing base\n"; }

~base() { cout << "Destructing base\n"; }

};

class derived: public base {

int j;

public:

// derived uses x; y is passed along to base.

derived(int x, int y): base(y)

{ j=x; cout << "Constructing derived\n"; }

~derived() { cout << "Destructing derived\n"; }

Page 198: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

198

void show() { cout << i << " " << j << "\n"; }

};

int main()

{

derived ob(3, 4);

ob.show(); // displays 4 3

return 0;

}

Ở đây, hàm tạo tử của lớp derived được khai báo với hai tham số x và y. Tuy

nhiên, hàm tạo tử derived() chỉ sử dụng x; y được truyền qua hàm tạo tử của

lớp cơ sở base().

10.4. Phương thức ảo

Chúng ta khảo sát một tính chất quan trọng nữa của C++ là phương thức ảo vì

hảm ảo hỗ trợ tính đa hình (được đề cập trong phần 11.5) trong lúc thực thi

chương trình.

Phương thức ảo là một hàm thành phần quan trọng của một lớp, nó được khai

báo ở lớp cơ sở và được định nghĩa trong lớp dẫn xuất.Để tạo ra phương thức

ảo, phần khai báo phương thức được bắt đầu bằng từ khóa virtual.Khi một lớp

có chứa phương thức ảo được thừa kế, lớp dẫn xuất sẽ định nghĩa lại hàm ảo đó

cho chính mình.Các phương thức ảo triển khai tư tưởng chủ đạo của đa hình

“một giao diện cho nhiều phương thức”.Phương thức ảo bên trong một lớp cơ

sở định nghĩa hình thức giao tiếp đối với phương thức đó.Việc định nghĩa lại

của phương thức ảo ở lớp dẫn xuất là thi hành các tác vụ của phương thức liên

quan đến chính lớp dẫn xuất đó.Nói cách khác, việc định nghĩa lại các phương

thức ảo là tạo ra phương thức cụ thể.Trong phần định nghĩa lại phương thức ảo

của lớp dẫn xuất, chúng ta không cần sử dụng từ khóa virtual.

Tuy nhiên, điều mà tạo cho hàm ảo quan trọng và có khả năng hỗ trợ tính đa

hình là chúng hoạt động như thế nào khi truy cập thông qua con trỏ.Như thảo

luận trong chương trước, con trỏ của lớp cơ sở có thể được sử dụng để trỏ tới

đối tượng của bất kỳ lớp dẫn xuất kế thừa từ lớp cở sở. Khi con trỏ lớp cơ sở trỏ

Page 199: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

199

tới đối tượng dẫn suất mà chứa phương thức ảo, C++ sẽ xác định phiên bản nào

của phương thức đó để gọi dựa vào kiểu của đối tượng được kế thừa từ lớp cơ

sở. Và với việc xác định này sẽ được tạo trong thời gian chạy.Vì vậy khi những

đối tượng khác nhau được trỏ tới, phiên bản khác nhau của hàm ảo được thực

thi.Hiệu quả như nhau khi áp dụng cho tham chiếu của lớp cơ sở. Bắt đầu với

việc khảo sát chương trình dưới đây:

Ví dụ 11.11:

#include <iostream>

using namespace std;

class base {

public:

virtual void vfunc() {

cout << "This is base's vfunc().\n";

}

};

class derived1 : public base {

public:

void vfunc() {

cout << "This is derived1's vfunc().\n";

}

};

class derived2 : public base {

public:

void vfunc() {

cout << "This is derived2's vfunc().\n";

}

};

Page 200: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

200

int main()

{

base *p, b;

derived1 d1;

derived2 d2;

// point to base

p = &b;

p->vfunc(); // access base's vfunc()

// point to derived1

p = &d1;

p->vfunc(); // access derived1's vfunc()

// point to derived2

p = &d2;

p->vfunc(); // access derived2's vfunc()

return 0;

}

Kết quả chạy chương trình:

This is base's vfunc().

This is derived1's vfunc().

This is derived2's vfunc().

10.5. Tính đa hình

Đa hình là một quá trình áp dụng một giao diện cho hai hay nhiều trường hợp

tương tự nhau (nhưng khác nhau về mặt kỹ thuật), nó triển khai tư tưởng “một

giao diện cho nhiều phương thức”. Đa hình rất quan trọng vì nó đơn giản hóa

các hệ thống phức tạp. Một giao diện đẹp và đơn giản được dùng để truy cập

Page 201: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

201

đến một số hoạt động có liên hệ với nhau nhưng khác nhau và làm mất sự phức

tạp giả tạo của hệ thống các hoạt động này. Đa hình làm cho mối quan hệ luận

lý giữa các hoạt động tương tự nhau được trở lên rõ ràng hơn, do đó giúp cho

người sử dụng dễ dàng hơn trong việc đọc hiểu và bảo trì chương trình. Một khi

mà các hoạt động có liên quan với nhau được truy cập bằng duy nhất một giao

diện, chúng ta sẽ dễ nhớ và quản lý hơn.

Có 2 khái niệm liên quan đến lập trình hướng đối tượng nói chung và C++ nói

riêng là khái niệm về liên kết sớm (early blinding) và liên kết muộn (late

blinding).

Liên kết sớm gắn với những trường hợp có thể xác định được ở thời điểm biên

dịch chương trình.Đặc biệt liên kết sớm liên quan đến gọi các hàm được xử lý

trong lúc biên dịch, bao gồm các hàm thông thường, các hàm quá tải, các hàm

thành phần không phải là hàm ảo, các hàm friend. Tất cả các thông tin về địa

chỉ cần thiết cho việc gọi các hàm trên được xác định rõ ràng trong lúc biên

dịch. Gọi các hàm liên kết sớm là kiểu gọi hàm nhanh nhất.Nhược điểm chính

của cách gọi này là thiếu tính linh hoạt.

Liên kết muộn gắn với những trường hợp xuất hiện trong lúc thực thi chương

trình.Khi gọi liên kết muộn, địa chỉ của hàm được gọi chỉ biết được khi thực thi

chương trình. Trong C++, phương thức ảo là một đối tượng liên kết muộn. Chỉ

khi thực thi chương trình, phương thức ảo được truy cập bằng con trỏ của lớp cơ

sở, chương trình mới xác định được kiểu của đối tượng bị trỏ và biết được phiên

bản nào của phương thức ảo được thực thi.Ưu điểm lớn nhất của liên kết muộn

là tính linh hoạt của ở thời gian thực thi chương trình chương trình, điều này

giúp cho chương trình gọn gàng vì không có những đoạn chương trình xử lý

trường hợp thừa trong khi thực thi. Do phải qua nhiều giai đoạn trung gian kèm

theo việc gọi thực thi một hàm liên kết muộn, nên nhược điểm chính của liên

kết muộn là chậm hơn so với liên kết sớm.

Vì mỗi loại liên kết đều có những ưu nhược điểm riêng của nó. Vì vậy, khi sử

dụng chúng ta cần cân nhắc để quyết định tình huống thích hợp để sử dụng hai

loại liên kết trên.

Page 202: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

202

Chương 11. Một số vấn đề khác

11.1. Các chỉ thị tiền xử lý

Trong C/C++ có một thành phần khá quan trọng đó là các câu lệnh tiền xử lý,

nếu không có các câu lệnh thật khó mà có được một chương trình C/C++ hoàn

hảo.

11.1.1. Lệnh chỉ thị bao hàm tệp #include

Có 2 cách để viết câu lệnh này:

Cách 1:#include<[đường dẫn]tên tệp>

Cách 2: #include"[đường dẫn]tên tệp"

Hai cách trên chỉ có một sự khác nhau duy nhất đó là:

-Cách 1: chỉ tìm tệp trong thư mục include

-Cách 2: tìm tệp trong thư mục hiện hành trước sau đó mới tìm trong thư mục

include

11.1.2. Lệnh chỉ định nghĩa #define

Cú pháp chung của câu lệnh này là:

#define tên_định_nghĩa phần_cần_định_nghĩa

Câu lệnh #define không chỉ định nghĩa được một tham số đơn giản như

#define max 100

hay một biểu thức

#define length (100+100)

mà thậm chí còn có thể định nghĩa được cả một số câu lệnh đơn giản(được gọi

là macro). Sau đây là một số macro điển hình

#define max(A,B) (A)>(B)?(A):(B)

hoặc

#define tich(A,B) (A)*(B)

Page 203: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

203

Chú ý: Sở dĩ phải có các dấu ngoặc ở trong các biến như (A) và (B) đó là do khi

thực hiện câu lệnh ví dụ như tich(a+c,b) thì chương trình sẽ bê nguyên cả a+c

và gán là A còn b là B. Nếu không có dấu ngoặc thì chương trình sẽ thực hiện

thành a+c*b chứ không phải là (a+c)*b như có dấu ngoặc.

11.1.3. Chỉ thị biên dịch có điều kiện #if, #else

Cú pháp câu lệnh như sau:

#if điều_kiện đoạn lệnh #endif

hoặc

#if điều_kiện đoạn lệnh 1 #else đoạn lệnh 2 #endif

hoặc

#if điều_kiên_1 đoạn lệnh 1 #elif điều_kiên_2 \\elif=else if đoạn lệnh 2 #else đoạn lệnh 3 #endif

Tác dụng của câu lệnh này cũng tương tự như if, nhưng điều kiện trong câu lệnh

#if không cần có ngoặc bao quanh và phải có giá trị xác định. Đối số của điều

kiện câu lệnh #if thường chỉ là các hằng đã định nghĩa từ trước.

Ví dụ 12.1:

#define max 100

#if max>100

printf("Max>100\n");

#else

printf("Max<=100\n");

#endif

11.1.4. Chỉ thị biên dịch có điều kiện #ifdef và #ifndef

#ifdef có thể dịch ra là nếu đã định nghĩa còn #ifndef thì có thể dịch là nếu chưa định nghĩa. Câu lệnh #ifdef có cú pháp như sau:

Page 204: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

204

#ifdef tên_định_nghĩa đoạn lệnh #endif

hoặc

#ifdef tên_định_nghĩa đoạn lệnh 1 #else đoạn lệnh 2 #endif

Cú pháp của câu lệnh #ifndef có cú pháp hoàn toàn tương tự với #ifdef

Tác dụng của câu lệnh này cũng tương tự như #if . Ngoài ra câu lệnh này còn có

một ứng dụng quan trọng. Ứng dụng này được tạo ra là do có sự khác nhau

trong một số câu lệnh của windows và linux mà người lập trình lại muốn

chương trình của mình chạy được trên cả 2 hệ điều hành. Vì vậy họ áp dụng cú

pháp sau với những chỗ có đoạn mã sai khác giữ 2 hệ điều hành

#ifdef UNIX đoạn mã lệnh cho Linux #else đoạn mã lệnh cho windows #endif

Như vậy chương trình có thể hoạt động trơn tru trên cả 2 hệ điều hành

Ngoài ra câu lệnh #ifdef vẫn còn một ứng dụng quan trọng không kém. Câu

lệnh #ifdef còn thường được sử dụng trong việc xây dựng các thư viện, để tránh

thư viện đó bị gọi nhiều lần, bởi mỗi lần gọi là toàn bộ code của thư viện lại

được trình biên dịch chèn vào trong chương trình. Vì vậy nếu một thư viện bị

dịch nhiều lần có thể dẫn đến lỗi cho chương trình. Do đó người lập trình

thường bắt đầu thư viện của mình bằng một đoạn sau đây:

#ifndef _DEFS_H_ #define _DEFS_H_ [giá_trị_bất_kỳ] {Nội dung thực sự của thư viện} #endif

như vậy sẽ đảm bảo được dù bị gọi bao nhiêu lần thì cũng chỉ có một lần đoạn

mã của thư viện bị chèn vào và biên dịch khi chương trình chạy

11.1.5. Chỉ thỉ thông báo lỗi #error

Đúng như tên gọi, câu lệnh #error dùng để thông báo khi một lỗi nào đó xảy ra.

Câu lệnh này có cú pháp như sau

#error thông_báo

Page 205: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

205

Chỉ thị #error thường được đi kèm với chỉ thị #if hoặc #ifdef hay câu lệnh if

11.2. Kỹ thuật xử lý ngoại lệ và bắt lỗi

C++ cung cấp sẵn một cơ chế xử lý lỗi gọi là điều khiển ngoại lệ. Qua đó, bạn

có thể khống chế và đáp ứng lại các lỗi xuất hiện lúc thực thi chương trình. Điều

khiển ngoại lệ của C++ gồm ba từ khóa: try, catch và throw. Nói một cách

tổng quát, các mệnh đề dùng để giám sát các ngoại lệ được đặt bên trong một

khối try. Khi một ngoại lệ (tức là một lỗi) xuất hiện bên trong khối try, nó sẽ

được ném ra (bằng từ khóa throw). Tiếp theo ngoại lệ này sẽ bị bắt để xử lý

(bằng từ khóa catch).

Bất kỳ một mệnh đề nào thực hiện việc ném ra một ngoại lệ phải đặt ở bên trong

khối try (hoặc bên trong một hàm được gọi đến từ một mệnh đề bên trong khối

try). Tất cả các ngoại lệ phải bị bắt lại bởi một mệnh đề catch đặt ngay sau khối

try mà từ đó nó được ném ra. Dạng tổng quát của try và catch là như sau:

try {

// try block

}

catch (type1 arg) {

// catch block

}

catch (type2 arg) {

// catch block

}

catch (type3 arg) {

// catch block

}

..

.

catch (typeN arg) {

// catch block

}

Page 206: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

206

Khối try phải chứa đoạn chương trình mà chúng ta cần giám sát lỗi. Nó có thể

ngắn gọn và một vài mệnh đề bên trong một hàm hay là phức tạp hơn khi nó

bao gồm toàn bộ hàm main() của chương trình (điều này làm cho toàn bộ

chương trình đều được giám sát lỗi).

Khi một ngoại lệ được ném ra, nó sẽ bị bắt lại và xử lý bởi một mệnh đề catch

tương ứng. Kiểu dữ liệu của ngoại lệ chính là cơ sở để quyết định xem ngoại lệ

đó được xử lý bằng mệnh để catch nào. Có nghĩa là, nếu kiểu dữ liệu xác định

trong một catch nó đó trùng hợp với kiểu dữ liệu của ngoại lệ, thì mệnh đề

catch đó được thực thi (tất cả các mệnh đề khác được bỏ qua). Khi một ngoại lệ

bị bắt lại, arg sẽ nhận giá trị của ngoại lệ này. Giá trị ngoại lệ có thể là một kiểu

dữ liệu bất kỳ, kể cả kiểu dữ liệu mới do bạn tạo ra.

Dạng tổng quát của mệnh đề throw là như sau:

throwexception;

Mệnh đề throw phải được đặt trong khối try, hay là trong một hàm mà hàm này

được gọi (trực tiếp hay gián tiếp) từ trong khối try. Giá trị exception sẽ được

ném ra.

Chương trình ngắn sau đây minh họa cách biểu diễn ngoại lệ của C++.

Ví dụ 12.2:

// A simple exception handling example.

#include <iostream>

using namespace std;

int main()

{

cout << "Start\n";

Page 207: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

207

try { // start a try block

cout << "Inside try block\n";

throw 100; // throw an error

cout << "This will not execute";

}

catch (int i) { // catch an error

cout << "Caught an exception -- value is: ";

cout << i << "\n";

}

cout << "End";

return 0;

}

Kết quả chạy của chương trình:

Start

Inside try block

Caught an exception -- value is: 100

End

Hãy đọc kỹ chương trình ví dụ trên, bạn sẽ thấy một khối try gồm ba mệnh đề

và một mệnh đề catch(int i) để điều khiển ngoại lệ kiểu nguyên. Bên trong khối

try, chỉ có hai mệnh đề đầu là được thực thi. Khi ngoại lệ được ném ra, điều

khiển của chương trình sẽ chuyển đến biểu thức của catch và kết thúc việc thực

thi khối try. Điều này có nghĩa là catch không được gọi như gọi hàm bình

thường mà điều khiển của chương trình được chuyển đến catch. Chính vì vậy

mà mệnh đề để cout theo sau mệnh đề throw sẽ không bao giờ được thực thi.

11.3. Lập trình trên nhiều file

Trong chương trước, chúng tôi đã cung cấp những hướng dẫn cơ bản nhất giúp

bạn có thể làm việc với C++, hầu hết các ví dụ đều được đưa ra trong cùng một

tệp tin. Cách tiếp cận này có nhược điểm: nếu chương trình của bạn trở nên lớn

Page 208: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

208

hơn thì mã nguồn trở nên rất lớn, gây khó khăn cho việc quản lý, chỉnh sửa

cũng như phân phối mã nguồn. Ngoài ra, cách tiếp cận này còn gây khó khăn

cho việc sử dụng lại mã nguồn.Trong mục này, chúng tôi hướng tới mục đích tổ

chức mã nguồn thành các module, hoặc tệp tin có ý nghĩa khác nhau.Trong

C++, chúng ta có hai loại tệp tin chính là header (phần mở rộng của tệp tin là

.hxx, cung cấp các định nghĩa, prototype của hàm), source file (phần mở rộng

của tệp tin là .cxx, cung cấp thực thi các định nghĩa, trong tệp tin prototype).

Sau đây là ví dụ về việc tổ chức lại mã nguồn:

Ví dụ 12.3:

//main.cpp

#include <iostream>

#include "Loc.h"

using namespace std;

int main (){

Loc ob1 (4, 5);

ob1.show();

return 0;

}

//loc.h

#ifndef LOC_H_

#define LOC_H_

classLoc {

private:

intlongtitude, latitude;

public:

Page 209: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

209

Loc();

Loc(intlg, intlt);

void show();

};

#endif /* LOC_H_ */

//loc.cpp

#include <iostream>

using namespace std;

#include "Loc.h"

Loc::Loc(){}

Loc::Loc(intlg, intlt){

longtitude = lg;

latitude = lt;

}

voidLoc::show(){

cout<< longtitude << " " << latitude <<endl;

}

11.4. Mẫu (Template) và thư viện STL

11.4.1. Mẫu (Tempate)

Bằng cách sử dụng mẫu, chúng ta có thể tạo ra các hàm chung và các lớp chung.

Trong một hàm chung hoặc một lớp chung, kiểu của dữ liệu được dùng có thể

được xác định bằng tham số. Do đó một hàm hay một lớp có thể được áp dụng

Page 210: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

210

cho nhiều dữ liệu có kiểu khác nhau mà không cần viết lại từng phiên bản cho

mỗi kiểu dữ liệu. Hàm chung và lớp chung sẽ được đề cập đến dưới đây.

11.4.2. Hàm khái quát (Generic function)

Một hàm khái quát định nghĩa một tập hợp các chức năng đối các kiểu dữ liệu

khác nhau. Kiểu dữ liệu mà hàm sẽ xử lý sẽ được truyền vào như là tham số.

Thông qua các generic function, một quy trình xử lý chung sẽ được áp dụng cho

nhiều loại dữ liệu. Như bạn đã biệt, nhiều thuật toán có những bước xử lý logic

giống nhau mà không phân biệt kiểu dữ liệu truyền vào là gì. Ví dụ như thuật

toán sắp xếp QuickSort là giống nhau áp dụng cho mảng các số nguyên hay số

thực. Sự khác biệt ở đây chỉ là kiểu dữ liệu được lưu trữ.Bằng việc sử dụng

generic function, bạn có thể định nghĩa một cách tự nhiên các thuật toán, độc

lập với dữ liệu. Khi bạn thực hiện chúng, trình biên dịch sẽ tự động sinh ra các

đoạn mã chính xác theo kiểu dữ liệu mà chương trình bạn đang làm việc. Như

vậy, khi bạn định nghĩa một generic function, tức là bạn đang tạo ra một hàm có

khả năng tự động nạp chồng lại chính nó.

Để tạo ra hàm khái quát, bạn có thể sử dụng từ khóa template.Ý nghĩa của từ

khóa này được thể hiện thông qua việc nó được sử dụng trong ngôn ngữ

C++.Nó được sử dụng để tạo ra các mẫu (framework) mà mô tả các hàm thực

thi công việc và trình biên dịch sẽ tự động sinh ra mã nguồn tương ứng khi cần

thiết. Đoạn mã sau mô tả trường hợp tổng quát để tạo ra generic function:

template<class Type> ret-type func-name(parameter list)

{

//thân hàm

}

Ở đây Type thể hiện kiểu dữ liệu bạn muốn sử dụng trong hàm.Nó có thể được

sử dụng trong định nghĩa hàm. Tuy nhiên, trình biên dịch sẽ tự động thay thế

kiểu dữ liệu chính xác khi nó mô tả rõ ràng hàm này. Mặc dù vậy, việc sử dụng

từ khóa class để đặc đả kiểu trong định nghĩa khuôn mẫu mang tính truyền

thống, bạn có thể thay nó bằng từ khóa typename.

Ví dụ sau đây sẽ tạo ra một generic function có nhiệm vụ hoán đổi giá trị của

hai biến khi nó được gọi tới..Vì việc hoán đổi giữa hai giá trị là tông quát và

không phụ thuộc vào kiểu dữ liệu, nên việc tạo ra hàm khái quát là ví dụ tốt.

Ví dụ 12.4:

// Function template example.

Page 211: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

211

#include <iostream>

using namespace std;

// This is a function template.template<class X> void

swapargs(X &a, X &b){ X temp;temp = a; a = b; b = temp;}

int main(){

int i=10, j=20;double x=10.1, y=23.3;char a='x',

b='z';cout<< "Original i, j: " << i << ' ' << j <<

'\n';cout<< "Original x, y: " << x << ' ' << y <<

'\n';cout<< "Original a, b: " << a << ' ' << b << '\n';

swapargs(i, j); // swap integersswapargs(x, y); // swap

floatsswapargs(a, b); // swap chars

cout<< "Swapped i, j: " << i << ' ' << j << '\n';cout<<

"Swapped x, y: " << x << ' ' << y << '\n';cout<< "Swapped

a, b: " << a << ' ' << b << '\n';

return 0;}

Hãy xem dòng mã nguồn sau đây:

template<class X> void swapargs(X &a, X &b)

Dòng mã nguồn này thông báo cho trình biên dịch hai điều: đây là mẫu được tạo

ra và định nghĩa tổng quát được sử dụng. Ở đây, X là kiểu chung, là vị trí có thể

thay thế được. Sau đó, hàm swapargs() được định nghĩa, với X là kiểu dữ liệu

của hai giá trị sẽ được hoán đổi. Trong hàm main(), hàm swapargs() được gọi

tới với ba kiểu dữ liệu đầu vào khác nhau: ints, doubles, và chars. Bởi vì hàm

swapargs() là generic function, trình biên dịch tự động tạo ra ba phiên bản khác

nhau: hoán đổi giá trị hai biến nguyên, hoán đổi giá trị hai biến double, và hoán

đổi giá trị hai ký tự.

Ở đây có một số thuật ngữ liên quan đến khuôn mẫu.Đầu tiên, một generic

function (ở đây, định nghĩa đưa ra bởi các câu lệnh template) cũng có thể gọi là

template function.Khi trình biên dịch tạo ra một phiên bản cụ thể, người ta nó

rằng hàm được tham chiếu như là instantiating.Như vậy hàm được sinh ra là

một thể hiện đặc tả template function.

Bạn có thể viết từ khóa template cùng hoặc khác dòng với từ khóa function vì

C++ không phân biệt được ký tự kết thúc dòng trong câu lệnh template.

template<class X>

void swapargs(X &a, X &b)

{

Page 212: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

212

X temp;

temp = a;a = b;b = temp;

}

Tuy nhiên, bạn cần chú ý rằng không được thêm bất cứ câu lệnh nào ở giữa biểu

thức statement và tên của hàm. Đoạn mã sau sẽ không được biên dịch

// This will not compile.template<class X>int i; // this

is an errorvoid swapargs(X &a, X &b){ X temp; temp =

a; a = b; b = temp;}

Chú ý rằng bạn có thể định nghĩa hàm khái quát với nhiều kiểu khác nhau trong

biểu thức template hoặc tham số là các kiểu biến có sẵn trong ngôn ngữ C++

11.4.3. Lớp khái quát (Generic class)

Mở rộng hơn so với generic function, bạn có thể định nghĩa một generic class.

Thực hiện như vậy, bạn có thể áp dụng nhiều thuật toán khác nhau đối với một

lớp này, tuy nhiên, kiểu thực tế của dữ liệu đang được xử lý cần được mô tả như

là tham số khi đối tượng được khởi tạo.

Lớp khái quát có lợi trong những trường hợp mà hành vi của đối tượng mang

tính tổng quát hóa cao. Ví dụ, cùng với thuật toán quản lý hàng đợi các số

nguyên cũng làm việc tốt với hàng đợi các ký tự, và cùng cơ chế với quản lý

danh sách liên kết danh sách liên lạc cũng làm việc vợi danh sách liên kết của

các bộ phân tự động. Khi bạn xây dựng một lớp khái quát, bạn có thể thực thi

định nghĩa như là quản lý hàng đợi hoặc danh sách liên kết cho các kiểu dữ liệu

khác nhau. Trình biên dịch sẽ tự động sinh ra các mã nguồn đặc tả đúng kiểu dữ

liệu mà bạn đã mô tả, khi bạn khởi tạo đối tượng. Dạng tổng quát được định

nghĩa như sau:

template<class Type> class class-name {};

Trong đó, bạn có thể thay thể kiểu dữ liệu bạn cần làm việc thay cho Type khi

đối tượng được khởi tạo.Trong trường hợp cần thiết, bạn có thể định nghĩa

nhiều hơn một loại dữ liệu bằng cách sử dụng dấu chấm phẩy.

Khi đã có generic class bạn có thể định nghĩa một đối tượng của lớp đó như sau:

class-name<Type> ob;

Trong đó type là tên của kiểu dữ liệu và lớp đó sẽ thực thi các thuật toán với

kiểu dữ liệu đó.Sau đây là một ví dụ cài đặt lớp ngăn xếp như là một generic

class.

Page 213: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

213

Ví dụ 12.5:

// This function demonstrates a generic stack.#include

<iostream>using namespace std;

const int SIZE = 10;

// Create a generic stack classtemplate<class StackType> class

stack {

StackType stck[SIZE]; // holds the stackint tos; // index

of top-of-stack

public:

stack() { tos = 0; } // initialize stack

void push(StackType ob); // push object on stack

StackType pop(); // pop object from stack

};

// Push an object.template<class StackType> void

stack<StackType>::push(StackType ob) {

if(tos==SIZE) {

cout<< "Stack is full.\n";

return;}stck[tos] = ob;tos++;

}

// Pop an object.template<class StackType> StackType

stack<StackType>::pop(){

if(tos==0) {

cout<< "Stack is empty.\n";

return 0; // return null on empty stack }tos--;

return stck[tos];

}

int main(){

// Demonstrate character stacks.stack<char> s1, s2; //

create two character stacksint

i;s1.push('a');s2.push('x');s1.push('b');

s2.push('y');s1.push('c');s2.push('z');

for(i=0; i<3; i++) cout << "Pop s1: " << s1.pop() <<

"\n";for(i=0; i<3; i++) cout << "Pop s2: " << s2.pop() <<

"\n";// demonstrate double stacksstack<double> ds1, ds2;

// create two double stacks

Page 214: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

214

ds1.push(1.1);ds2.push(2.2);ds1.push(3.3);ds2.push(4.4);d

s1.push(5.5);ds2.push(6.6);

for(i=0; i<3; i++) cout << "Pop ds1: " << ds1.pop() <<

"\n";for(i=0; i<3; i++) cout << "Pop ds2: " << ds2.pop()

<< "\n";

return 0;}

Như bạn thấy, định nghĩa generic class cũng tương tự như định nghĩa generic

function.Kiểu dữ liệu thực dùng để lưu trữ bởi ngăn xếp được mô tả một cách

tổng quát.Nó chỉ được xác định cụ thể khi một đối tượng được định nghĩa. Khi

bạn định nghĩa một đối tượng stack, trình biên dịch sẽ tự động sinh ra tất cả các

hàm và biến cần thiết để quản lý dữ liệu thực. Sau đây là kiểu ngăn xếp: ngăn

xếp các ký tự và ngăn xếp các số thực.

stack<char> s1, s2; // create two character stacks

stack<double> ds1, ds2; // create two double stacks

Để sử dụng một kiểu dữ liệu mong muốn, bạn chỉ cần thay đổi kiểu dữ liệu bên

trong cặp dấu <> khi đối tượng được khởi tạo. Ta có thể định nghĩa ngăn xếp

con trỏ ký tự như sau:

stack<char *> chrptrQ;

Ta có thể thay bất kỳ kiểu dữ liệu phù hợp (ngôn ngữ C++ hỗ trợ hoặc do bạn

định nghĩa). Nếu bạn muốn xây dựng ngăn xếp các thông tin cá nhân mà bạn

lưu trữ:

struct addr {

char name[40];char street[40];

char city[30];char state[3];char zip[12];

};

sau đó, khởi tạo đối tượng với kiểu dữ liệu như ta đã định nghĩa:

stack<addr> obj;

Với mỗi đối tượng và kiểu dữ liệu được đặc tả khi khởi tạo, trình biên dịch sẽ tự

động sinh ra các đoạn mã tương ứng với kiểu dữ liệu đó.

Page 215: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

215

11.4.4. Giới thiệu thư viện chuẩn STL

C++ được đánh giá là ngôn ngữ mạnh vì tính mềm dẻo, gần gũi với ngôn ngữ

máy. Ngoài ra, với khả năng lập trình theo mẫu (template), C++ đã điều khiển

ngôn ngữ lập trình trở thành khái quát, không cụ thể và chi tiết như nhiều ngôn

ngữ khác. Sức mạnh của C++ đến từ STL, viết tắt của Standard Template

Libarary – một thư viện template cho C++ với cấu trúc dữ liệu cũng giải thuật

được xây dựng tổng quát mà vẫn tận dụng được hiệu quả và tốc độ của C. Với

khái niệm template, những người lập trình đã đề ra khái niệm lập trình khái quát

(generic programming), C++ được cung cấp kèm với bộ thư viện chuẩn STL.

STL gồm các thành phần chính sau:

Container (bộ chứa dữ liệu) là các cấu trúc dữ liệu phổ biến đã template

hóa dùng để lưu trữ các kiểu dữ liệu khác nhau. Các container chia làm 2

loại:

o Sequential container (các bộ chứa tuần tự) bao gồm list, vector và

deque

o Asociative container (các bộ chứa liên kết) bao gồm map,

multimap, set và multiset

Interator (biến lặp) giống như con trỏ, tích hợp bên trong container

Algorithm (các thuật toán) là các hàm phổ biến để làm việc với các bộ vật

chứa như thêm, xóa, sửa, truy xuất, tìm kiếm, sắp xếp, …

Function object (functor): Một kiểu đối tượng có thể gọi như 1 hàm, đúng

ra đây là 1 kỹ thuật trong STL nó được nâng cao và kết hớp với các

algorithm.

Các adapter (bộ tương thích), chia làm 3 loại:

o Container adapter (các bộ tương thích lưu trữ) bao gồm stack,

queue và priority_queue

o Iterator adapter (các bộ tương thích con trỏ)

o Function adapter (các bộ tương thích hàm)

Page 216: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

216

Những thành phần này làm việc chung với các thành phần khác để cung cấp các

giải pháp cho các vấn đề khác nhau của chương trình.

Bộ thư viện này thực hiện toàn bộ các công việc vào ra dữ liệu (iostream), quản

lý mảng (vector), thực hiện hầu hết các tính năng của các cấu trúc dữ liệu cơ

bản (stack, queue, map, set, …). Ngoài ra, STL còn bao gồm các thuật toán cơ

bản: tìm min, max, tính tổng, sắp xếp (với nhiều thuật toán khác nhau), thay thế

các phần tử, tìm kiếm (tìm kiếm thường và tìm kiếm nhị phân), trộn. Toàn bộ

các tính năng nêu trên đều được cung cấp dưới dạng template nên việc lập trình

luôn thể hiện tính khái quát hóa cao. Nhờ vậy STL làm cho ngôn ngữ C++ trở

lên trong sáng hơn và hiệu quả hơn.

Đặc điểm thự viện STL là được hỗ trợ trên các trình biên dịch ở cả hai mội

trường WINDOWS và UNIX, vì vậy khi sử dụng thư viện này trong xử lý thuận

tiện cho việc chia sẽ mã nguồn với cộng đồng phát triển.

Vì thư viện chuẩn STL được thiết kế bởi những chuyên gia hàng đầu và đã được

chưng minh tính hiệu quả trong lịch sử tồn tại của nó, các thành phần của thư

viện này được khuyến cáo sử dụng thay vì dùng những phần viết tay bên ngoài

hay những phương tiện cấp thấp khác. Thí dụ, dùng std::vector hay std::string

thay vì dùng kiểu mảng đơn thuần là một cách hữu hiệu để viết phần mềm được

an toàn và linh hoạt hơn.

Các chức năng của thư viện chuẩn C++ được khai báo trong namespace std;

Page 217: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

217

Phụ lục A. Phong cách lập trình

Phần này giới thiệu những điểm cơ bản mà lập trình viên nên làm theo để

chương trình dễ đọc, dễ hiểu, và đạt được hiệu quả cao. Về cơ bản, chương trình

cần viết tường minh, đơn giản, dễ hiểu. Không nên sử dụng các cấu trúc lệnh

phức tạp dễ dẫn đến nhầm lẫn và khó tìm lỗi.

A.1. Chú thích

Trước và trong khi lập trình cần phải ghi chú thích cho các đoạn mã trong

chương trình. Việc chú thích giúp chúng ta hiểu một cách rõ ràng và tường

minh hơn về công việc chúng ta cần làm. Quan trọng hơn, chúng sẽ giúp chúng

ta dễ dàng hiểu khi chúng ta quay lại kiểm tra hoặc tiếp tục làm việc với chương

trình. Đặc biệt quan trọng là giúp chúng ta có thể chia sẻ và cùng phát triển

chương trình theo nhóm trong một thời gian dài.

Cụ thể là, đối với mỗi hàm, đặc biệt là các hàm quan trọng, chúng ta cần xác

định và ghi chú thích về những vấn đề cơ bản sau:

Mục đích của hàm là gì?

Biến đầu vào của hàm (tham biến) là gì?

Các điều kiện rằng buộc của các biến đầu vào nếu có?

Kết quả trả về của hàm là gì?

Các rằng buộc của kết quả trả ra nếu có.

Việc chú thích sẽ giúp chúng hiểu rõ ràng về yêu cầu của hàm.

Ví dụ:

Page 218: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

218

// the function calculate the sum of two digits

// input: two integer numbers smaller than 10

// output: an integer number

getSum (int x, int y)

{

int sum = x + y;

return sum;

}

A.2. Chia nhỏ chương trình

Trong lập trình, người ta thường sử dụng chiến lược chia để trị, tức là chương

trình được chia nhỏ ra thành các chương trình con. Việc chia nhỏ ra thành các

chương trình con làm tăng tính môđun của chương trình và mang lại cho lập

trình viên khả năng tái sử dụng mã.

Người ta khuyên rằng độ dài mỗi chương trình con không nên vượt quá một

trang màn hình để lập trình viên có thể kiểm soát tốt hoạt động của chương trình

con đó.

A.3. Biến toàn cục

Xu hướng chung là nên hạn chế sử dụng biến toàn cục. Khi nhiều hàm cũng sử

dụng một biến toàn cục, việc thay đổi giá trị biến toàn cục của một hàm nào đó

có thể dẫn đến những thay đổi không mong muốn ở các hàm khác. Biến toàn

cục sẽ làm cho các hàm trong chương trình không độc lập với nhau.

A.4. Cách đặt tên biến

Tên biến nên dễ đọc, và gợi nhớ đến công dụng của biến hay kiểu dữ liệu mà

biến sẽ lưu trữ. Đối với những biến gồm nhiều từ, thì các từ nên viết liền nhau

và chữ cái đầu tiên của các từ phía sau nên được viết hoa. Ví dụ

numberStudents, savingAccount

Các biến hằng nên viết hoa. Nếu biến hằng gồm nhiều từ, thì các từ nên chia

cách bằng dấu gạch chân. Ví dụ

Page 219: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

219

PI, NUMBER_ITERATION

A.5. Cách đặt tên hàm

Tên hàm nên dễ đọc, và gợi nhớ đến mục đích của hàm. Tên hàm nên bắt đầu

bằng một động từ. Đối với những hàm gồm nhiều từ, thì các từ nên viết liền

nhau và chữ cái đầu tiên của các từ phía sau nên được viết hoa. Ví dụ

getSum, getMin, calculateScores

A.6. Biểu thức

Các biểu thức cần viết đơn giản, gắn gọn và dễ hiểu. Các biểu thức dài có thể

tách nhỏ ra sử dụng các biến trung gian. Nên sử dụng các cặp dấu ngoặc để

trách sự nhập nhằng và nhầm lẫn về thứ tự thực hiện các phép toán trong biểu

thức. Ví dụ:

sum = a * b + a * b * c – d/e

Có thể viết thành

ab = a * b;

sum = ab * (c + 1) – (d/e);

Lưu ý: Biểu thức điều kiện cũng nên tuân thủ theo nguyên tắc trên.

A.7. Vòng lặp

Không nên sử dụng nhiều các lệnh nhảy như break, hay continue để thoát ra

khỏi vòng lặp. Với mỗi vòng lặp, nên xác định rõ ràng điều kiện để vòng lặp kết

thúc.

A.8. Khối chương trình

Các đoạn chương trình cần được trình bày theo thành từng khối (sử dụng các

dấu cách). Việc trình bày theo khối sẽ giúp chúng ta dễ dàng hiểu được cấu trúc

và thứ tự thực hiện các lệnh. Ví dụ:

Page 220: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

220

while (!done)

{

doSomething ();

done = check ();

}

A.9. Lớp

Mỗi lớp (class) nên tách ra thành hai tệp riêng biệt. Tệp header (.h) chứa khai

báo về lớp, còn tệp nguồn (.cpp) chứa định nghĩa về các phương thức của lớp.

Việc tách này sẽ dễ dàng cho chúng ta trong việc theo dõi, tìm hiểu và phát triển

chương trình. Ví dụ:

time.h, time.cpp

Page 221: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

221

Phụ lục B. Dịch chương trình C++ bằng GNU C++

B.1. Dịch và liên kết

Quá trình tạo tệp thực thi được từ các tệp mã nguồn có hai bước.

Bước 1: biên dịch các tệp mã nguồn thành các tệp mã object. Các tệp có mở

rộng .cpp hoặc .cc được dịch thành các tệp có mở rộng .o.

Bước 2: liên kết các tệp mã object thành một tệp thực thi được. Các tệp có mở

rộng .o được liên kết với nhau thành một tập thực thi được (đuôi .exe trong môi

trường DOS/Windows).

Nếu chương trình chỉ gồm một tệp mã nguồn, ví dụ count.cpp, ta chỉ cần chạy

lệnh

g++ -o count.exe count.cpp

Kết quả là một tệp thực thi được có tên count.exe. Hoặc ta chỉ dùng lệnh

g++ count.cpp

Kết quả là một tệp thực thi được có tên mặc định (a.out trong môi trường

Unix/Linux).

Nếu chương trình bao gồm nhiều tệp mã nguồn, chẳng hạn time_test.cpp (chứa

hàm main), time.cpp, time.h, ta dịch từng tệp có đuôi cpp hoặc cc bằng lệnh sau

g++ -c file.cpp

Sau đó liên kết lại bằng lệnh

g++ time_test.o time.o -o time_test.exe

B.2. Tiện ích make của GNU

Ta có biên dịch bằng cách mỗi lần lại gõ từng lệnh như hướng dẫn ở trên. Tuy

nhiên, với những chương trình lớn bao gồm nhiều tệp mã nguồn, dùng đến

nhiều thư viện, việc gõ từng lệnh bằng tay mỗi khi cần dịch chương trình không

phải là cách làm hiệu quả. Tiện ích make trong bộ công cụ GNU cho phép tự

động hóa quy trình dịch và liên kết chương trình. make là một hệ thống được

thiết kế để xây dựng các chương trình từ các cây mã nguồn lớn. Từ đặc tả của

lập trình viên về cây mã nguồn, tiện ích make sẽ gọi trình biên dịch và liên kết

Page 222: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

222

một cách hiệu quả nhất để xây dựng chương trình thực thi được. Mục này là

một hướng dẫn tối giản cho việc sử dụng tiện ích make.

Để dùng tiện ích make cho một chương trình, lập trình viên phải tạo một tệp có

tên Makefile nằm trong cùng thư mục với các tệp mã nguồn. Tệp này chứa các

lệnh hướng dẫn make làm gì và làm thế nào để xây dựng được chương trình.

Makefile gồm các bộ đặc tả, mỗi bộ quản lý việc cập nhật một tệp.

Mỗi bộ đặc tả của một tệp Makefile gồm 3 phần: đích (target), các quan hệ phụ

thuộc (dependency), và các chỉ thị (instruction). Công thức như sau:

TargetFile: DependencyFile1 DependencyFile2 ... DependencyFilen

trong đó, TargetFile là tệp cần cập nhật và mỗi DependencyFilei là một tệp

mà TargetFile phụ thuộc vào nó. Dòng thứ hai của bộ đặc tả là một lệnh dịch

TargetFile, lệnh này phải có một kí tự TAB đứng trước và kết thúc bằng kí tự

xuống dòng. Ví dụ, bộ đặc tả đầu tiên trong Makefile của chúng ta như sau:

time_test.exe: time.o time_test.o g++ time.o time_test.o -o time_test.exe

trong đó dòng đầu chỉ ra rằng time_test.exe phụ thuộc hai tệp time.o và

time_test.o, dòng thứ hai là lệnh dùng g++ dịch ra tệp time_test.exe.

Tiếp theo, time.o không có sẵn ngay khi biên dịch lần đầu, cho nên ta phải viết

bộ đặc tả cho tệp này:

time.o: time.cc time.h g++ -c time.cc

Tương tự là bộ đặc tả cho time_test.o

time_test.o: time.cc time.h g++ -c time_test.cc

Do đó, Makefile sẽ có dạng:

time_test: time.o time_test.o g++ time.o time_test.o -o time_test time.o: time.cc time.h g++ -c time.cc time_test.o: time.cc time.h g++ -c time_test.cc

Và khi ta gõ từ dấu nhắc dòng lệnh

Page 223: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

223

make

chương trình make sẽ đọc Makefile và thực hiện các công việc sau:

1. thấy rằng time_test.exe phụ thuộc vào time.o, và:

a) Kiểm tra time.o, thấy nó phụ thuộc time.cc và time.h;

b) Xác định xem time.o đã lỗi thời chưa (cũ hơn bản mới nhất của các

tệp time.cc và time.h)

c) nếu đã lỗi thời thì chạy lệnh g++ -c time.cc để tạo time.o

2. thấy rằng time_test.exe cũng phụ thuộc vào time_test.o, và

a) Kiểm tra time_test.o, thấy nó phụ thuộc time_test.cc và time.h;

b) Xác định xem time_test.o đã lỗi thời chưa (chưa được dịch từ bản

mới nhất của các tệp time_test.cc và time.h)

c) nếu đã lỗi thời thì chạy lệnh g++ -c time_test.cc để tạo

time_test.o

3. thấy rằng tất cả các tệp mà time_test.exe phụ thuộc đều đã được cập nhật,

nên nó chạy lệnh g++ time.o time_test.o -o time_test để tạo

chương trình time_test.exe.

Page 224: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

224

Phụ lục C. Xử lý xâu bằng thư viện cstring

C.1. Một số hàm xử lý xâu thông dụng

char *strcpy( char *d, const char *s );

Sao chép xâu s vào mảng char d. Trả về địa chỉ của xâu d.

char *strncpy( char *d, const char *s, size_t n );

Chép tối đa n kí tự từ xâu s vào mảng char d. Trả về địa chỉ mảng d.

char *strcat( char *d, const char *s );

Chép xâu s vào cuối xâu d. Kí tự đầu tiên của xâu s sẽ đè lên kí tự null

cuối xâu d. Trả về địa chỉ của xâu d.

char *strncat( char *d, const char *s, size_t n );

Chép tối đa n kí tự của xâu s vào cuối xâu d. Kí tự đầu tiên của xâu s sẽ

đè lên kí tự null cuối xâu d. Trả về địa chỉ của xâu d.

int strcmp( const char *s1, const char *s2 );

So sánh xâu s1 và xâu s2. Trả về một giá trị bằng 0 nếu s1 bằng s2, nhỏ

hơn 0 (thường là -1) nếu s1 nhỏ hơn s2, hoặc lớn hơn 0 (thường là 1) nếu

s1 lớn hơn s2 theo thứ tự từ điển.

int strncmp( const char *s1, const char *s2, size_t n );

Tương tự hàm strcmp() nhưng chỉ so sánh n kí tự đầu tiên của xâu s1 với

n kí tự đầu tiên của xâu s2.

char *strtok( char *s1, const char *s2 );

Một chuỗi các lời gọi hàm strtok có tác dụng tác xâu s1 thành các mảnh

("token"), chẳng hạn các từ trong một dòng văn bản. Xâu kí tự được tách

dựa theo các kí tự chứa trong xâu s2. Ví dụ, nếu ta cần tách xâu "Hello,

how are you?" thành các từ, với ' ', '?', ',' là các kí tự ngăn cách giữa các

từ, kết quả sẽ là các token "Hello", "how", "are", "you". Tuy nhiên, mỗi

lần gọi hàm strtok chỉ tách được một token và trả về địa chỉ của token đó.

Khi không còn tìm thấy token nào, hàm sẽ trả về giá trị NULL. Lần gọi

đầu tiên trong quá trình phân tách xâu s1 sẽ cần tham số đầu tiên là s1,

các lần gọi sau sẽ tiếp tục tách s1 nếu tham số đầu tiên là NULL.

size_t strlen( const char *s );

Trả về độ dài của xâu s – là số kí tự đứng trước kí tự null.

Page 225: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

225

C.2. Khuyến cáo về việc sử dụng thư viện cstring

Một số hàm trong thư viện cstring nếu sử dụng không cẩn thận có thể dẫn đến

việc truy nhập ra ngoài không gian đã được khai báo của mảng hoặc tình trạng

xâu kí tự không được kết thúc đúng cách bằng kí tự null. Đối với những hàm

này, lập trình viên phải sử dụng một cách cẩn trọng để đảm bảo không gây ra lỗi

dữ liệu. Mục này đưa ra một số khuyến cáo về việc sử dụng các hàm này.

Các hàm strcpy và strncpy sao chép toàn bộ xâu kí tự là tham số thứ hai vào

mảng char đích mà không quan tâm không gian khai báo của mảng đích có đủ

chỗ chứa hay không. Lập trình viên phải tự đảm bảo rằng mảng đích có đủ chỗ

cho xâu nguồn cũng như kí tự null kết thúc xâu đó. Một lời khuyên là nên dùng

strncpy thay cho strcpy, vì strncpy cho phép kiểm soát được số lượng kí tự

sẽ được chép vào mảng đích.

Tương tự, các hàm strcat và strncat chép xâu kí tự là tham số thứ hai vào

cuối xâu kí tự là tham số thứ nhất mà cũng không quan tâm không gian khai báo

của mảng đích có đủ chỗ chứa cả hai xâu hay không. Lập trình viên phải tự đảm

bảo rằng mảng đích có đủ chỗ cho cả hai xâu cũng như kí tự null kết thúc xâu

thứ hai. strncat cũng được khuyên dùng hơn vì nó cho phép kiểm soát số

lượng kí tự sẽ được chép vào mảng đích.

Khi sử dụng strncpy, nếu số kí tự cần chép (tham số thứ ba) không lớn hơn độ

dài của xâu nguồn (tham số thứ hai), thì kí tự null sẽ không được chép vào

mảng đích. strncpy cũng không tự chèn thêm một kí tự null vào cuối đoạn vừa

chép. Trong trường hợp này, nếu lập trình viên không tự gắn kí tự null đánh dấu

kết thúc xâu kết quả, xâu này có thể trở thành xâu có độ dài không xác định với

nguy cơ dẫn đến lỗi nghiêm trọng khi chạy chương trình. Tình trạng tương tự

cũng xảy ra đối với hàm strncat.

Page 226: Mục lục - uet.vnu.edu.vnuet.vnu.edu.vn/~tqlong/2016ltnc/Bai-giang-LTNC.pdf · Phương thức ảo ... Giáo trình này trang bị cho sinh viên những kiến thức cơ bản

226

Tài liệu tham khảo

[1]. Bjarne Stroustrup, The C++ Programming Language, 3rd

edition,

Addison-Wesley, 1997.

[2]. Deitel & Deitel, C++ How to Program, 5th edition, Prentice Hall, 2005.

[3]. ISO/IEC JTC1/SC22/WG21. Stable release, ISO/IEC 14882:2003 (2003)

[4]. Juan Soulie, C++ Language Tutorial, http://www.cplusplus.com.