Download as pdf or txt
Download as pdf or txt
You are on page 1of 233

BỘ THÔNG TIN VÀ TRUYỀN THÔNG

HỌC VIỆN CÔNG NGHỆ BƯU CHÍNH VIỄN THÔNG

BÀI GIẢNG
Kĩ thuật lập trình
Mã môn học: TEL1340
Ngành đào tạo Kỹ thuật Điện tử Viễn thông
(Lưu hành nội bộ)

Biên soạn: TS. Nguyễn Trọng Trung Anh


KS. Khuất Văn Đức

Hà nội - 11/2022
MỤC LỤC

DANH MỤC HÌNH VẼ .................................................................................. iii


DANH MỤC BẢNG BIỂU .............................................................................. v
LỜI NÓI ĐẦU ................................................................................................ vi
CHƯƠNG 1. GIỚI THIỆU CHUNG ............................................................ 1
1.1. Giới thiệu .................................................................................................. 1
1.1.1. Kĩ thuật lập trình .................................................................................... 1
1.1.2. Nội dung và phạm vi môn học................................................................. 2
1.2. Các khái niệm cơ bản ................................................................................ 2
1.2.1. Máy tính và bộ nhớ ................................................................................. 2
1.2.2. Bài toán và giải bài toán trên máy tính ................................................... 4
1.2.3. Chương trình máy tính............................................................................ 4
1.3. Hoạt động của chương trình máy tính và ngôn ngữ lập trình ..................... 6
1.3.1. Các thành phần cơ bản ........................................................................... 7
1.3.2. Chương trình dịch .................................................................................. 7
1.3.3. Mã nguồn ............................................................................................... 8
1.4. Các mô thức lập trình ................................................................................ 9
1.5. Chu trình phát triển tổng quát .................................................................. 10
1.6. Bài tập chương ........................................................................................ 12
CHƯƠNG 2. LẬP TRÌNH CƠ BẢN ........................................................... 13
2.1. Giới thiệu về C++ .................................................................................... 13
2.1.1 Định nghĩa và khái niệm chung ............................................................. 13
2.2.1. Bộ kí tự và từ khóa................................................................................ 19
2.2.2. Kiểu dữ liệu .......................................................................................... 20
2.2.3. Tên, biến và hằng ................................................................................. 23
2.2.3. Biểu thức .............................................................................................. 27
2.2.4. Mảng và con trỏ ................................................................................... 30
2.2.4.1 Con trỏ ............................................................................................... 30
2.2.4.2 Mảng .................................................................................................. 32
2.2.4.3 Mối liên hệ giữa mảng và con trỏ ....................................................... 35
2.2. Hàm và kiểu dữ liệu có cấu trúc .............................................................. 40
2.2.1 Hàm....................................................................................................... 40
2.2.2 Kiểu dữ liệu có cấu trúc C++ ................................................................ 48
2.3. Luồng điểu khiển chương trình ................................................................ 54
2.3.1 Cấu trúc rẽ nhánh.................................................................................. 55
2.3.2 Vòng lặp ................................................................................................ 66
2.4. Vào ra file theo luồng .............................................................................. 71
2.5. Luồng fstream , ofstream và các tham số ................................................. 74
2.6 Bài tập Chương 2...................................................................................... 77
CHƯƠNG 3. LỚP VÀ ĐỐI TƯỢNG .......................................................... 79
3.1. Khái niệm hướng đối tượng ..................................................................... 79
3.2. Lớp và đối tượng ..................................................................................... 81
3.3 Mảng đối tượng C++ ................................................................................ 84

I
3.4 Hàm chúng ta, lớp chúng ta và các thư viện lớp trong C++ ...................... 88
3.5. Các đặc tính của hướng đối tượng (Kế thừa, đa hình, đóng gói, trừu tượng)
..................................................................................................................... 102
3.5.1 Tính kế thừa......................................................................................... 102
3.5.2 Tính đa hình ........................................................................................ 122
3.5.3 Tính đóng gói ...................................................................................... 132
3.5.4 Tính trừ tượng hóa .............................................................................. 135
3.6. Bài tập Chương 3................................................................................... 138
CHƯƠNG 4. CẤU TRÚC DỮ LIỆU......................................................... 141
4.1. Danh sách liên kết ................................................................................. 141
4.1.1 Danh sách liên kết đơn ........................................................................ 143
4.1.2. Danh sách liên kết đôi ........................................................................ 160
4.2. Ngăn xếp và hàng đợi ............................................................................ 172
4.2.1. Ngăn xếp ............................................................................................ 172
4.2.2. Hàng đợi............................................................................................. 181
4.2.3. Map và Set .......................................................................................... 192
4.3. Thư viện và các cấu trúc dữ liệu cơ bản ................................................. 195
4.4. Bài tập Chương 4................................................................................... 196
CHƯƠNG 5. CẢI THIỆN MÃ NGUỒN ................................................... 200
5.1. Hợp tác .................................................................................................. 200
5.2. kiểm thử ................................................................................................ 201
5.3. Gỡ lỗi .................................................................................................... 211
5.4. Case Study............................................................................................. 224
5.5. Bài tập chương ...................................................................................... 224
TÀI LIỆU THAM KHẢO ............................................................................ 225

II
DANH MỤC HÌNH VẼ
Hình 1. 1 Kiến trúc máy tính .............................................................................. 4
Hình 1. 2 Luồng thực hiện lệnh .......................................................................... 5
Hình 1. 3 Hoạt động của nhận lệnh .................................................................... 6
Hình 1. 4 Trình biên dịch (Compiler) ................................................................. 8
Hình 1. 5 Trình thông dịch (Interpreter) ............................................................. 8
Hình 1. 6 Chu trình phát triển phần mềm ......................................................... 10

Hình 2. 1 Minh họa chương trình c++ ............................................................. 13


Hình 2. 2 Lịch sử phát triển của C++ .............................................................. 16
Hình 2. 3 Cú pháp khai báo một hàm ............................................................... 41
Hình 2. 4 Khai báo hàm ................................................................................... 43
Hình 2. 5 Các loại hàm khia báo trong C++ .................................................... 44
Hình 2. 6 Hình minh họa các kiểu truyền tham số trong hàm ........................... 45
Hình 2. 7 Cấu trúc trong C++ ......................................................................... 49
Hình 2. 8 Sơ đồ hình họa việc ra quyết định ..................................................... 55
Hình 2. 9 Sơ đồ minh họa câu lệnh if................................................................ 57
Hình 2. 10 Sơ đồ minh họa quá trình hoạt động của câu lệnh if-else ................ 59
Hình 2. 11 Sơ đồ minh họa luồng hoạt động của nested – if ............................. 61
Hình 2. 12 Sơ đồ minh họa luồng hoạt động của if -else- if .............................. 63
Hình 2. 13 Các vòng lặp trong C++ ................................................................ 68

Hình 3. 1 Các đặc điểm của lập trình hướng đối tượng .................................... 79
Hình 3. 2 Cú pháp khai báo 1 lớp..................................................................... 82
Hình 3. 3 Hàm khởi tạo trong C++ .................................................................. 96
Hình 3. 4 Minh họa kế thừa ............................................................................ 103
Hình 3. 5 Lớp kế thừa từ lớp phương tiện ....................................................... 103
Hình 3. 6 ba chế độ trên và hiển thị thông số truy cập của các thành viên của
lớp cơ sở trong lớp con .................................................................................. 112
Hình 3. 7 đơn kế thừa ..................................................................................... 113
Hình 3. 8 Đa kế thừa ...................................................................................... 115
Hình 3. 9 Kế thừa đa mức............................................................................... 117
Hình 3. 10 Kế thừa phân cấp .......................................................................... 119
Hình 3. 11 Kế thừa lai .................................................................................... 121
Hình 3. 12 Mô hình đa hình ............................................................................ 123

III
Hình 3. 13 Minh họa đa hình ghi đè hàm ....................................................... 128
Hình 3. 14 Minh họa đóng gói ........................................................................ 133
Hình 3. 15 Mô hình trừu tượng hóa ................................................................ 136

Hình 4. 1 Cấu trúc cơ bản của 1 danh sách liên kết ....................................... 141
Hình 4. 2 Độ phức tập về thời gian thực thi của các thao tác trên danh sách liên
kết đơn ........................................................................................................... 146
Hình 4. 3 Thêm một nút vào phần trước ......................................................... 147
Hình 4. 4 Thêm một nút vào sau nút đã cho.................................................... 148
Hình 4. 5 Thêm một nút ở cuối ....................................................................... 150
Hình 4. 6 Cấu trúc của 1 danh sách liên kết đôi ............................................. 160
Hình 4. 7 Thêm một nút ở phía trước trong danh sách liên kết đôi ................. 162
Hình 4. 8 Thêm một nút ở phía sau một nút đã cho trong danh sách liên kết đôi
....................................................................................................................... 163
Hình 4. 9 Thêm một nút ở cuối trong danh sách liên kết đôi ........................... 164
Hình 4. 10 Thêm một nút trước một nút đã cho trong danh sách liên kết đôi .. 167
Hình 4. 11 Cấu trúc của một ngăn xếp ........................................................... 172
Hình 4. 12 Các thao tác trên Stack ................................................................. 173
Hình 4. 13 Độ phúc tạp của các thao tác trên stack ....................................... 175
Hình 4. 14 Cấu trúc của queue ....................................................................... 182
Hình 4. 15 Minh họa FIFO ............................................................................. 182
Hình 4. 16 Enqueue ........................................................................................ 186
Hình 4. 17 Dequeue........................................................................................ 187

Hình 5. 1 Hệ thống phân cấp kế thừa lớp điển hình........................................ 208

IV
DANH MỤC BẢNG BIỂU

Bảng 2. 1 Các kiểu dữ liệu cơ bản trong C++ .................................................. 21


Bảng 2. 2 Kiểu dữ liệu số thực ......................................................................... 22
Bảng 2. 3 Các kiểu biến và mô tả ..................................................................... 24
Bảng 2. 4 Các toán tử trong C++ .................................................................... 27
Bảng 2. 5 Các toán tử so sánh C++ ................................................................. 28
Bảng 2. 6 Các toán tử logic trong C++ ............................................................ 29
Bảng 2. 7 Sự khác biệt giữa cuộc gọi hàm theo giá trị và gọi hàm theo tham
chiếu trong C++ .............................................................................................. 47

V
LỜI NÓI ĐẦU

Hiện nay, với sự phát triển mạnh mẽ của công nghệ thông tin và truyền
thông (ICT), việc sử dung máy tính kết hợp với các kỹ thuật lập trình đã và đang
trở thành một trong các kiến thức quan trọng và thiết yếu cho không chỉ các kỹ
sư ngành ICT mà còn đối với nhiều người dùng phổ thông khác. Với vai trò là
thành phần quan trọng trong việc thao tác máy tính, kiến thức và kỹ năng về các
kỹ thuật lập trình là hành trang không thể thiếu đối với mỗi kỹ sư và chuyên gia
công nghệ thông tin và các ngành liên quan.
Nội dung tài liệu này hướng đến đối tượng là sinh viên ngành kỹ thuật
điện tử viễn thông tại Học viện Công nghệ Bưu chính Viễn thông nhằm trang bị
cho các em các kiến thức và kỹ năng cơ bản liên quan liên quan đến các kỹ thuật
lập trình để sinh viên có thể thao tác qua mã code và cài đặt một chương trình
thành công. Bài giảng này được bố cục thành 05 chương với các nội dung chính
như sau:
Chương 1-Giới thiệu chung: cung cấp các kiến thức tổng quan về kĩ thuật
lập trình các khái niệm cơ bản liên quan đến máy tính, các hoạt động chương
trình máy tính. Ngoài ra, trong chương này, nội dung về các mô thức lập trình,
chu trình phát triển tổng quát cũng cũng được đề cập.
Chương 2-Lập trình cơ bản: trình bày chi tiết về ngôn ngữ lập trình C++.
Những nội dung chính của chương bao gồm: Các kiến thức liên quan đến biến,
kiểu dữ liệu, biểu thức, con trỏ, mảng. Ngoài ra chương học còn đề cập đến hàm
và các kiểu dữ liệu có cấu trúc, luồng điều khiển chương trình cũng như việc
vào ra file theo luồng.
Chương 3-Lớp và đối tượng: trình bày về các khái niệm cơ bản liên quan
đến đối tượng, lớp, mảng đối tượng, hàm chúng ta, lớp chúng ta, các thư viện
lớp ngoài ra chương học cũng đề cập đến các tính chất cơ bản của hướng đối
tượng.
Chương 4-Cấu trúc dữ liệu: tập trung giới thiệu về các vấn liên quan đến
danh sách liên kết, ngăn xếp và hàm đợi cũng như các thư viện và cấu trúc dữ
liệu căn bản.
Chương 5- Cải thiện mã nguồn: giới thiệu một số cách cải thiện mã nguồn
liên quan đến việc hợp tác, kiểm thử, gỡ lỗi, case study.
Kĩ thuật lập trình là mảng kiến thức rộng và có sự cập nhật thường xuyên,
liên tục, hội tụ nhiều công nghệ cốt lõi tiên tiến trong đó. Hiện nay, lĩnh vực này
đang thu hút rất nhiều sự quan tâm nghiên cứu phát triển của các nhà khoa học

VI
và các công ty, tập đoàn công nghệ. Do đó, kiến thức liên quan tới lĩnh vực này
luôn cần được bổ sung, cập nhật. Trong quá trình biên soạn giáo trình, mặc dù
nhóm tác giả đã có nhiều cố gắng cập nhật các kiến thức mới bên cạnh những
kiến thức cơ bản song không thể tránh khỏi những thiếu sót. Nhóm tác giả rất
mong nhận được các ý kiến phản hồi, góp ý cho các thiếu sót cũng như các ý
kiến về việc cập nhật, hoàn thiện nội dung của bài giảng.
Hà nội, 11/2022
Nhóm tác giả

VII
Chương 1. Giới thiệu chung

CHƯƠNG 1. GIỚI THIỆU CHUNG


1.1. Giới thiệu
Với sự phát triển của công nghệ thông tin và truyển thông trong những năm
qua, chúng ta càng ngày càng hiểu rõ được tầm quan trọng của ngành công
nghiệp phần mềm. Một giải pháp phần mềm được triển khai cần được thực thi
bởi những kĩ thuật dựa trên nền tảng một phương pháp luận phù hợp có tính
khoa học. Học phần kĩ thuật lập trình là môn học cung cấp cho sinh viên những
kĩ thuật đó. Mục đích của môn học nhằm giúp sinh viên nắm được các khái niệm
cơ bản về lập trình, ngôn ngữ lập trình, các kĩ thuật lập trình, phương pháp lập
trình hướng cấu trúc và hướng đối tượng. Trong bài giảng này, những kĩ thuật
cơ bản trong C++ như sử dụng con trỏ, mảng, xâu kí tự, sử sụng kiểu dữ liệu cấu
trúc, vào/ra tệp, tiếp cận lớp và đối tượng cùng các đặc điểm của lập trình hướng
đối tượng. Song song với tiếp cận lý thuyết, sinh viên cũng sẽ sử dụng ngôn ngữ
lập trình C++ để giải quyết các bài toán lập trình thực tế.
1.1.1. Kĩ thuật lập trình
Khái niệm Kĩ thuật lập trình có thể được trình bày như sau:
“Kĩ thuật lập trình là kĩ thuật thực thi một giải pháp phần mềm (cấu trúc dữ
liệu và giải thuật) dựa trên nền tảng một phương pháp luận (methodology) và
một hoặc nhiều ngôn ngữ lập trình phù hợp với yêu cầu đặc thù của ứng dụng”.
Như vậy trong định nghĩa chúng ta có thể nhận ra sự liên quan của kĩ thuật
lập trình và các giải pháp phần mềm. Thực tế cho thấy, kết quả cuối cùng của
công việc lập trình là tạo ra những phần mềm, và để tạo ra những phần mềm tốt,
chúng ta cần những kĩ thuật từ tư tưởng thiết kế, cấu trúc dữ liệu, thuật toán cho
đến ngôn ngữ lập trình và hoạt động lập trình. Khái niệm về lập trình có thể xem
như quá trình chúng ta giải một bài toán. Với mỗi bài toán đặt ra, ta cần thiết kế
giải thuật để giải quyết. Sự khác nhau duy nhất là thay vì chúng ta giải một bài
toán thông thường, giải thuật được cài đặt và chạy trong một chương trình máy
tính. Chi tiết về chương trình máy tính sẽ được giải thích trong phần sau của
Chương 1.
Có nhiều yếu tố để quyết định như thế nào là lập trình tốt. Một số tiêu chí
có thể kể đến như:

1
Chương 1. Giới thiệu chung

 Đúng / Chính xác : Thỏa mãn các nhiệm vụ, được khách hàng chấp nhận
 Ổn định: Ít lỗi hoặc lỗi nhẹ có thể chấp nhận được
 Khả năng nâng cấp: Dễ dàng chỉnh sửa, dễ dàng nâng cấp trong điều kiện
bài toán thay đổi
 Tái sử dụng: Sử dụng lại được hoặc kế thừa cho bài toán khác.
 Tương thích: Thích ứng tốt ở các môi trường khác nhau
 Hiệu quả: Thời gian lập trình ngắn, khả năng bảo trì dễ dàng, giá trị sử
dụng lại lớn, sử dụng đơn giản, thân thiện, nhiều chức năng tiện ích
 Hiệu suất: Chương trình nhỏ gọn, ít tốn bộ nhớ, tốc độ nhanh, sử dụng ít
CPU.
1.1.2. Nội dung và phạm vi môn học
Kĩ thuật lập trình trong học phần này thực hiện minh họa trên ngôn ngữ
lập trình C++ do tính bao quát của ngôn ngữ đối với những nội dung môn học đề
cập đến. Ngoài ra, môn học đề cao kiến thức cơ bản và nền tảng, thiên về tư duy,
phương pháp lập trình, tạo khả năng dễ thích ứng với các ứng dụng và ngôn ngữ
khác nhau, xây dựng cái nhìn tổng quan về kĩ thuật lập trình nói chung. Những
nội dung chính của học phần bao gồm 3 phần chính:
 Tổng quan về Kĩ thuật lập trình
 Lập trình và ngôn ngữ lập trình: Ngôn ngữ C++
 Cải thiện mã nguồn
và được chia làm 5 chương trong bài giảng.
1.2. Các khái niệm cơ bản
Những khái niệm cơ bản trong kĩ thuật lập trình liên quan đến phần cứng
và phần mềm cùng với hoạt động tương tác giữa chúng. Ba thành phần phần
cứng quan trọng nhất trong máy tính phải kể đến đó là: Bộ xử lý trung tâm
(CPU), bộ nhớ máy tính (RAM) và hệ thống truyền dẫn (BUS). Đối với phần
mềm, những khái niệm về hoạt động của một chương trình máy tính và ngôn
ngữ lập trình sẽ được trình bày rõ.
1.2.1. Máy tính và bộ nhớ
Hiểu rõ về bộ nhớ là một trong những yêu cầu quan trọng của học phần
Kĩ thuật lập trình. Bộ nhớ máy tính đề cập trong bài giảng là bộ nhớ truy cập

2
Chương 1. Giới thiệu chung

ngẫu nhiên (RAM). Tuy nhiên, nên chú ý rằng tất cả các thành phần phần cứng
được gọi là bộ nhớ trong máy tính đều liên quan trực tiếp đến một thứ đó là dữ
liệu. Dữ liệu được lưu trữ trong bộ nhớ. Chúng ta có thể đọc dữ liệu từ bộ nhớ
(như đọc một file được lưu trên ổ đĩa cứng) và chúng ta cũng có thể ghi dữ liệu
lên bộ nhớ (bộ nhớ ROM chỉ đọc chứ không ghi). Bộ nhớ máy tính hiện đại bao
gồm hàng tỉ bit nhớ, mỗi bit nhớ có thể có hai trạng thái 1 hoặc 0, tương ứng với
có điện hoặc không có điện. Nhiều bit nhớ gộp lại giúp chúng ta có thể lưu trữ
nhiều kiểu dữ liệu khác nhau, từ số tự nhiên, số thực, kí tự, chữ cái cho đến xâu
kí tự, mảng nhiều phần tử hay lớn hơn là ảnh, video và dữ liệu phức tạp khác.
Hình 1.1 minh họa ba thành phần quan trọng của máy tính là bộ xử lý
CPU, bộ nhớ truy cập ngẫu nhiên RAM và hệ thống BUS.

3
Chương 1. Giới thiệu chung

Hình 1. 1 Kiến trúc máy tính

1.2.2. Bài toán và giải bài toán trên máy tính


Trong cuộc sống thường ngày, mọi cá nhân trong chúng ta đều phải giải
quyết nhiều bài toán. Một bài toán bất kì có thể được diễn đạt theo cách: Cho
biết giả thiết, điều kiện ban đầu và mục đích là tìm ra kết quả, mục tiêu cần đạt
được hoặc kết luận từ những cái đã có. Tương tự như vậy, một bài toán trên máy
tính được giải quyết bằng cách xác định đầu vào (Input), từ đó xây dựng giải
pháp thực thi để sinh ra kết quả là đầu ra (Output). Giải pháp sau đó được cài đặt
và chạy trên máy tính. Việc xác định một bài toàn trên máy tính thường gặp khó
khăn như: Thông tin về đầu vào và đầu ra thường không rõ ràng và đầy đủ, hay
thông báo về điều kiện đặt ra cho cách giải quyết thường không được nêu ra một
cách minh bạch. Chính vì vậy, việc xác định bài toán là rất quan trọng, ảnh
hưởng tới cách thức và chất lượng của việc giải quyết bài toán
1.2.3. Chương trình máy tính
Trong cuộc sống hiện đại, chúng ta sử dụng chương trình máy tính một
cách thường xuyên ngay cả khi không nhận thức được việc đó. Một ứng dụng
trên điện thoại cũng là một chương trình máy tính, một phần mềm soạn thảo văn
bản, trình duyệt web trên máy tính cá nhân, v.v. chính là những ví dụ của
chương trình máy tính. Hoạt động của chương trình máy tính liên quan mật thiết
với bộ nhớ. Chương trình được nạp vào bộ nhớ (primary memory) như là một

4
Chương 1. Giới thiệu chung

tập các lệnh viết bằng ngôn ngữ mà máy tính hiểu được, hay nói cách khác là
một dãy tuần tự các số nhị phân (binary digits). Tại bất cứ một thời điểm nào,
máy tính sẽ ở một trạng thái nào đó. Đặc điểm cơ bản của trạng thái là con trỏ
lệnh, trỏ tới lệnh tiếp theo để thực hiện, và thứ tự thực hiện các nhóm lệnh được
gọi là luồng điều khiển. Hình 1.2 mô tả luồng thực hiện lệnh trong máy tính của
chương trình.

Hình 1. 2 Luồng thực hiện lệnh

Chương trình máy tính bắt đầu hoạt động theo một chu kì nhận lệnh và
thực hiện lệnh. Chu trình lệnh cứ thế được thực hiện trong máy tính qua các giai
đoạn, áp dụng từ những bộ vi xử lý 8-bit đầu tiên đến những bộ xử lý phức tạp
nhất hiện nay. Trong giai đoạn nhận lệnh, hay tìm nạp lệnh, lệnh được lấy từ
RAM, sao chép vào bên trong bộ xử lý. Giai đoạn thực hiện lệnh sẽ giải mã và
thực thi lệnh, và kết quả được ghi trong thanh ghi bên trong của bộ xử lý hoặc
trong địa chỉ bộ nhớ của RAM. Một giai đoạn thứ tư nữa cũng có thể được thể
hiện đó là khi các đơn vị thực thi lệnh ghi kết quả (write-back). Giai đoạn này
thường được tính trong giai đoạn thực thi của chu trình.

5
Chương 1. Giới thiệu chung

Hình 1.3 minh họa hoạt động nhận lệnh. Bắt đầu mỗi chu trình lệnh, CPU
nhận lệnh từ bộ nhớ chính. Thanh ghi PC (Program Counter) lưu trữ địa chỉ của
lệnh sẽ được nhận. Nội dung của lệnh sẽ được nạp vào thanh ghi lệnh IR
(Instruction Register). Sau khi lệnh được nhận vào, nội dung PC tự động tăng để
trỏ sang lệnh kế tiếp. Ở đây chúng ta có thể thấy sự xuất hiện của khái niệm địa
chỉ và con trỏ. Về cơ bản. các ô nhớ trong bộ nhớ đều có địa chỉ nhất định, và
con trỏ dùng để lưu địa chỉ đó. Các tín hiệu điều kiển của máy tính có thể ghi giá
trị vào các ô nhớ hoặc đọc giá trị lưu trong một ô nhớ có địa chỉ nhất định.

Hình 1. 3 Hoạt động của nhận lệnh

1.3. Hoạt động của chương trình máy tính và ngôn ngữ lập trình
Các chương trình máy tính được viết trên những ngôn ngữ lập trình khác
nhau. Về mặt khái niệm, ngôn ngữ lập trình là một hệ thống các kí hiệu dùng để
liên lạc, trao đổi với máy tính nhằm thực thi một nhiệm vụ tính toán. Hay nói
cách khác, ngôn ngữ lập trình là một ngôn ngữ dùng để viết chương trình cho
máy tính. Ta có thể chia ngôn ngữ lập trình thành các loại sau: ngôn ngữ máy,
hợp ngữ và ngôn ngữ lập trình bậc cao.
 Ngôn ngữ máy (Machine language): Là các chỉ thị dưới dạng nhị phân,
can thiệp trực tiếp vào trong các mạch điện tử. Chương trình được viết
bằng ngôn ngữ máy thì có thể được thực hiện ngay không cần qua bước

6
Chương 1. Giới thiệu chung

trung gian nào. Tuy nhiên chương trình dễ sai sót, khó đọc vì mã máy chỉ
bao gồm các bit nhị phân.
 Hợp ngữ (Assembly language): Hợp ngữ bao gồm tên các câu lệnh và quy
tắc viết các câu lệnh đó. Lệnh bao gồm phần mã lệnh chỉ nội dung thực
hiện, và địa chỉ chứa toán hạng để thực hiện lệnh đó. Để máy tính thực
hiện được một chương trình viết bằng hợp ngữ thì chương trình đó phải
được dịch sang mã máy với Assembler.
 Ngôn ngữ bậc cao (High-level programming language): Thay vì dựa trên
phần cứng (machine-oriented), các ngôn ngữ bậc cao được phát triển dựa
trên giải quyết vấn đề (problem-oriented) để tạo ra chương trình. Ngôn
ngữ bậc cao gần gũi với ngôn ngữ tự nhiên hơn và thường sử dụng các từ
khóa tiếng Anh.
1.3.1. Các thành phần cơ bản
Các thành phần cơ bản của một ngôn ngữ lập trình bao gồm:
 Mô thức lập trình (Programming paradigm): như một nguyên tắc
chung cơ bản của một ngôn ngữ.
 Cú pháp (Syntax): bao gồm những thành phần hợp lệ để tạo nên
đoạn mã như từ khóa, kí tự, v.v.
 Ngữ nghĩa (Semantic): là ngữ nghĩa của ngôn ngữ, hay sự kết hợp
các kí hiệu, từ khóa, v.v trong cú pháp thành các câu lệnh.
1.3.2. Chương trình dịch
Mã nguồn chương trình muốn thực thi và chạy được trên máy thì cần có
chương trình dịch. Thông thường, mỗi một ngôn ngữ lập trình bậc cao đều có
một chương trình dịch riêng, và chung quy lại thì có hai cách dịch mã nguồn là
biên dịch và thông dịch:
 Trình biên dịch (Compiler): Chương trình thực hiện biên dịch toàn bộ
chương trình nguồn thành mã máy trước khi thực hiện. Các ngôn ngữ như
C/C++ sử dụng trình biên dịch để chạy code.
 Trình thông dịch (Interpreter): Chương trình dịch và thực hiện từng dòng
lệnh của chương trình cùng lúc, không tạo ra chương trình dạng mã máy.

7
Chương 1. Giới thiệu chung

Hình 1. 4 Trình biên dịch (Compiler)

Hình 1. 5 Trình thông dịch (Interpreter)

1.3.3. Mã nguồn
Mã nguồn là thành phần cơ bản của một chương trình máy tính chứa các
mã lệnh thực thi và được tạo ra bởi các lập trình viên. Giải thích một cách khác
thì mã nguồn là những kí tự được con người nhập vào máy tính dưới dạng một
văn bản thuần túy. Con người bình thường có thể đọc và hiểu được mã nguồn,
khi lập trình viên sử dụng ngôn ngữ lập trình để viết ra những câu lệnh. Những
câu lệnh được viết ra và lưu lại trong một tệp nào đó như tệp “.cpp” đối với
ngôn ngữ C++ chẳng hạn, nó sẽ được gọi là tệp chứa mã nguồn. Lập trình viên
có thể sử dụng phần mềm gõ văn bản thông thường hoặc một bộ công cụ trực
quan chuyên cho code, một môi trường phát triển tích hợp IDE (Integrated
Development Environment), và cũng có thể là một bộ phát triển phần mềm SDK
(Software Development Kit) để phát triển mã nguồn. Mã nguồn chương trình có

8
Chương 1. Giới thiệu chung

thể được công khai và chia sẻ miễn phí hoặc cũng có thể không được chia sẻ vì
liên quan đến vấn đề bản quyền và bảo mật.
Mục đích chính của mã nguồn là làm nền tảng để tạo ra các phần mềm.
Ngoài ra mã nguồn còn có nhiều mục đích khác như: hạn chế cho những người
có kĩ năng mới có thể truy cập, những người có quyền hạn với mã nguồn mới có
thể truy cập, điều chỉnh và cài đặt phần mềm. Một mục đích khác nữa là giúp
các nhà phát triển, lập trình viên khác có thể tiếp tục xây dựng chương trình
tương tự trên các hệ điều hành khác, hoặc nâng cấp phiên bản hiện tại lên. Cần
phân biệt giữa các khái niệm liên quan đến mã nguồn và chương trình máy tính.
Ví dụ điển hình như hệ điều hành Window, Mac OS. Mã nguồn của những hệ
điều hành này người sử dụng không thể thấy được vì hai hệ điều hành này là
những chương trình được thương mại hóa. Tuy nhiên, cũng có những hệ điều
hành mã nguồn mở như Linux, Android. Với mã nguồn mở, cộng đồng có thể
tham gia vào chỉnh sửa, đóng góp sao cho phần mềm ứng dụng đó tốt hơn, hoặc
tùy chỉnh phù hợp với mục đích sử dụng.
1.4. Các mô thức lập trình
Như đã giới thiệu từ trước, một ngôn ngữ lập trình gồm có ba thành phần
chính. Một trong số đó là mô thức lập trình của ngôn ngữ đó. Các mô thức lập
trình phổ biển có thể kể đến như:
 Lập trình hướng mệnh lệnh: Mô thức lập trình sử dụng câu lệnh để thay
đổi trạng thái của chương trình. Một chương trình mệnh lệnh bao gồm các
lệnh cho máy tính thực hiện. Mô thức tập trung vào miêu tả cách một
chương trình hoạt động. Các thành phần chính của mô thức gồm các lệnh
khai báo, lệnh gán, lệnh điều khiển cấu trúc chương trình và các mô-đun
chia chương trình thành các chương trình con
Ví dụ: Mệnh lệnh: đầu tiên thực hiện lệnh 1, sau đó thực hiện lệnh 2
 Mô thức hướng chức năng: Đặc trưng cơ bản của mô thức hướng chức
năng là mô-đun hóa chương trình. Chức năng được xây dựng theo các
hàm, trong hướng chức năng, giá trị đầu ra của hàm phụ thuộc vào các đối
số được truyền cho hàm. Các thành phần chính như tập hợp các cấu trúc
dữ liệu và các hàm liên quan, tập hợp các hàm cơ sở và tập hợp các toán
tử.
Ví dụ: Hàm tính n! và factorial(int n)

9
Chương 1. Giới thiệu chung

 Mô thức hướng logic: Dựa trên các tiên đề, các quy luật suy diên và truy
vấn. Chương trình thực hiện từ việc tìm kiếm có hệ thống trong một tập
các sự kiện, sử dụng một tập các luật để đưa ra kết luận.
 Mô thức hướng đối tượng: Dựa trên khái niệm về lớp và đối tượng. Trong
hướng đối tượng, các chương trình được thiết kế theo hướng bao gồm các
đối tượng tương tác với nhau. Những ngôn ngữ hướng đối tượng thường
dựa trên khái niệm lớp, các đối tượng là thể hiện của các lớp. Dữ liệu
cũng như các thao tác trên dữ liệu được bao gói trong các đối tượng, cơ
chế che giấu thông tin được sử dụng. Đối tượng đại diện cho thể hiện,
trong cùng một lớp có chung các thuộc tính. Lớp đại diện cho khái niệm,
có tính thừa kế, có thể mở rộng hay chuyên biệt hóa.
Ví dụ: Lớp User, Đối tượng là user1, user2, v.v.
1.5. Chu trình phát triển tổng quát
Phần mềm, chương trình hay ứng dụng đều là các sản phẩm của việc lập
trình. Chính vì vậy, việc nắm bắt cách thức hoạt động, quy trình phát triển tổng
quát của một phần mềm trong kĩ thuật lập trình là cần thiết. Quy trình, chu trình
hay vòng đời phát triển của phần mềm đề cập đến cách thức hoạt động và các
hoạt động trong phát triển phần mềm như cài đặt, phân tích yêu cầu, thiết kế
thực thi, kiểm thử, triển khai, bảo trì phần mềm. Hình 1.xxxx mô tả các hoạt
động trong vòng đời phát triển của phần mềm tiêu chuẩn.

Hình 1. 6 Chu trình phát triển phần mềm

Sáu giai đoạn chính trong chu trình bao gồm:

10
Chương 1. Giới thiệu chung

 Lập kế hoạch (Planning / Needs Identification): Giai đoạn nghiên cứu


thị trường, lên kế hoạch phát triển, xác định nhu cầu. Trước khi xây
dựng phần mềm, công ty cần thực hiện nghiên cứu rõ ràng thị trường để
xác định tính khả thi của sản phẩm. Nhà phát triển cũng thảo luận về
điểm mạnh, điểm yếu và cơ hội của sản phẩm phần mềm nhằm thỏa
mãn những thông số nhất định trước khi các hoạt động khác bắt đầu.

 Phân tích yêu cầu (Requirements Analysis): Là giai đoạn thực hiện
khảo sát chi tiết yêu cầu, mong muốn của khách hàng. Sau đó, thông tin
sẽ được tổng hợp vào tài liệu đặc tả yêu cầu (Requirements and
Specifications). Tài liệu đặc tả phải đầy đủ các yêu cầu về chức năng,
phi chức năng và giao diện. Ngoài ra, tài liệu còn cung cấp một bản
phác thảo chi tiết về thành phần, phạm vi, nhiệm vụ của nhà phát triển
và các thông số thử nghiệm để tạo ra sản phẩm chất lượng. Phân tích
yêu cầu cũng là giai đoạn mà các nhà phát triển lựa chọn cách tiếp cận
phát triển phần mềm như mô hình và cách thức triển khai.

 Thiết kế (Design): Sau khi đã xác định và phân tích kỹ lưỡng về yêu
cầu, chúng ta chuyển sang giai đoạn thiết kế, giai đoạn nắm vai trò quan
trọng trong phát triển phần mềm. Tại đây, các kiến trúc sư phần mềm và
nhà phát triển phần mềm sẽ đưa ra các thông số kỹ thuật tiên tiến mà họ
cần để tạo ra sản phẩm theo yêu cầu. Vấn đề cần được thảo luận thêm
giữa các bên bao gồm: Mức độ rủi ro, thành phần nhóm, công nghệ áp
dụng, thời gian, ngân sách, giới hạn của dự án, phương pháp và thiết kế
kiến trúc. Tài liệu đặc điểm kỹ thuật thiết kế sẽ là kết quả cuối cùng của
giai đoạn. Tài liệu chỉ định thiết kế kiến trúc, thành phần, giao tiếp, đại
diện front-end và luồng người dùng của sản phẩm, v.v.

 Phát triển (Development / Implementation): Tại giai đoạn này, nhà phát
triển sẽ bắt tay vào lập trình và triển khai thông số thiết kế. Lập trình
viên sẽ coding dựa trên các thông số kỹ thuật và yêu cầu của sản phẩm
đã được thống nhất trong các giai đoạn trước. Sau khi coding hoàn tất,
developers sẽ deploy sản phẩm trong môi trường phát triển
(development environment). Lập trình viên sẽ thử nghiệm phiên bản đã
tạo ra và điều chỉnh lại cho phù hợp với yêu cầ

11
Chương 1. Giới thiệu chung

 Kiểm thử (Testing): Sau khi nhà phát triển đã hoàn thành giai đoạn lập
trình, tester sẽ tiếp nhận sản phẩm và tiến hành kiểm thử. Các kịch bản
kiểm thử sẽ được tạo dựa trên tài liệu ở giai đoạn trước, và quá trình
kiểm tra bắt đầu. Tester sẽ cập nhật kết quả vào công cụ quản lý và
thông báo lỗi đến nhà phát triển. Hai bên sẽ cùng nhau phối hợp xử lý
các lỗi và cập nhật trên hệ thống quản lý. Trong thực tế, tùy theo mô
hình phát triển phần mềm mà hoạt động phát triển và kiểm thử có thể
diễn ra song song hoặc tiến hành lần lượt.

 Bảo trì (Maintenance): Tại giai đoạn này khi lỗi đã được xử lý xong,
nhà phát triển phần mềm sẽ cung cấp sản phẩm hoàn chỉnh đến tay
khách hàng. Kiểm thử vẫn được diễn ra ở giai đoạn triển khai để đảm
bảo sản phẩm luôn có mức độ hoàn hảo cao. Sau khi phát hành, công ty
sẽ tạo ra một nhóm bảo trì để quản lý các vấn đề mà khách hàng gặp
phải khi sử dụng sản phẩm. Bảo trì giúp khắc phục nhanh các vấn đề
nhỏ xảy ra trong quá trình sử dụng sản phẩm.

1.6. Bài tập chương


Bài 1. Tìm hiểu về nguyên lý hoạt động của bộ nhớ máy tính, bộ vi xử lý trung
tâm và hệ thống bus?
Bài 2. Những ngôn ngữ lập trình phổ biến trong thực tế? Cài đặt, làm quen với
một IDE mới để sử dụng ngôn ngữ lập trình C++. Ví dụ Dev-C++, Visual
Studio Code, v.v.
Bài 3. Sử dụng C, C++ và Python để viết một chương trình nhập vào một số
nguyên dương n và tính tổng của n số tự nhiên đầu tiên.
Bài 4. Tìm hiểu những bài toán cần đến lập trình trong thực tế, mô tả thuật toán
để giải quyết vấn đề đó.
Bài 5. Tìm hiểu tương lai của ngôn ngữ lập trình, các vấn đề liên quan đến học
máy, trí tuệ nhân tạo.

12
Chương 2. Lập trình cơ bản

CHƯƠNG 2. LẬP TRÌNH CƠ BẢN


2.1. Giới thiệu về C++

2.1.1 Định nghĩa và khái niệm chung


C++ là ngôn ngữ lập trình có mục đích chung được phát triển như là một
cải tiến của ngôn ngữ C để bao gồm mô hình hướng đối tượng. Nó là một mệnh
lệnh và một ngôn ngữ được biên dịch

Hình 2. 1 Minh họa chương trình c++

C ++ là ngôn ngữ cấp trung khiến nó có lợi thế khi lập trình ứng dụng cấp
thấp (trình điều khiển, nhân) và thậm chí cấp cao hơn (trò chơi, GUI, ứng dụng
dành cho máy tính để bàn, v.v.). Cú pháp cơ bản và cấu trúc mã của cả C và
C++ đều giống nhau.
Một số tính năng & điểm chính cần lưu ý về ngôn ngữ lập trình như sau:
 Đơn giản : Đó là một ngôn ngữ đơn giản theo nghĩa là các chương
trình có thể được chia thành các đơn vị logic và các phần, có hỗ trợ
thư viện phong phú và nhiều loại dữ liệu.
 Độc lập với máy nhưng phụ thuộc vào nền tảng : Tệp thực thi C++
không độc lập với nền tảng (các chương trình được biên dịch trên
Linux sẽ không chạy trên Windows), tuy nhiên chúng độc lập với
máy.
 Ngôn ngữ cấp trung : Đây là ngôn ngữ cấp trung vì chúng ta có thể
thực hiện cả lập trình hệ thống (trình điều khiển, hạt nhân, mạng,
v.v.) và xây dựng các ứng dụng người dùng quy mô lớn (Trình phát
đa phương tiện, Photoshop, Công cụ trò chơi, v.v.)

13
Chương 2. Lập trình cơ bản

 Hỗ trợ thư viện phong phú : Có hỗ trợ thư viện phong phú (Cả tiêu
chuẩn ~ cấu trúc dữ liệu tích hợp sẵn, thuật toán, v.v.) cũng như các
thư viện của bên thứ 3 (ví dụ: thư viện Boost) để phát triển nhanh
chóng và nhanh chóng.
 Tốc độ thực thi : Các chương trình C++ vượt trội về tốc độ thực thi.
Vì, nó là một ngôn ngữ được biên dịch và cũng có rất nhiều thủ tục.
Các ngôn ngữ mới hơn có thêm các tính năng mặc định tích hợp sẵn
như thu gom rác, gõ động, v.v. làm chậm quá trình thực thi chương
trình nói chung. Vì không có chi phí xử lý bổ sung như thế này trong
C++, nên nó rất nhanh.
 Con trỏ và truy cập bộ nhớ trực tiếp : C++ cung cấp hỗ trợ con trỏ
giúp người dùng thao tác trực tiếp với địa chỉ lưu trữ. Điều này giúp
thực hiện lập trình cấp thấp (nơi người ta có thể cần có quyền kiểm
soát rõ ràng đối với việc lưu trữ các biến).
 Hướng đối tượng : Một trong những điểm mạnh nhất của ngôn ngữ
khiến nó khác biệt với C. Hỗ trợ hướng đối tượng giúp C++ tạo ra
các chương trình có thể bảo trì và mở rộng. tức là có thể xây dựng
các ứng dụng quy mô lớn. Mã thủ tục trở nên khó duy trì khi kích
thước mã tăng lên.
 Ngôn ngữ biên dịch : C++ là ngôn ngữ được biên dịch, góp phần vào
tốc độ của nó.
Các ứng dụng của C++
C++ tìm thấy cách sử dụng đa dạng trong các ứng dụng như:
 Hệ điều hành & Lập trình hệ thống. ví dụ: HĐH dựa trên Linux (Ubuntu,
v.v.)
 Trình duyệt (Chrome & Firefox)
 Công cụ đồ họa & trò chơi (Photoshop, Blender, Unreal-Engine)
 Công cụ cơ sở dữ liệu (MySQL, MongoDB, Redis, v.v.)
 Đám mây/Hệ thống phân tán
Một số sự thật thú vị về C++
Đây là một số sự thật thú vị về C++ mà chúng ta có thể quan tâm:

14
Chương 2. Lập trình cơ bản

 Tên của C++ biểu thị bản chất tiến hóa của những thay đổi từ C. “++” là
toán tử gia số C.
 C ++ là một trong những ngôn ngữ chiếm ưu thế để phát triển tất cả các
loại phần mềm kỹ thuật và thương mại.
 C++ giới thiệu Lập trình hướng đối tượng, không có trong C. Giống như
những thứ khác, C++ hỗ trợ bốn tính năng chính của OOP: đóng gói, đa
hình, trừu tượng hóa và kế thừa.
 C++ có các tính năng OOP từ ngôn ngữ Lập trình Simula67.
 Một hàm là yêu cầu tối thiểu để chương trình C++ chạy được. (ít nhất là
hàm main())
b) Lịch sử phát triển của C++
Ngôn ngữ C++ được Bjarne Stroustrup phát triển từ ngôn ngữ C từ cuối
thập niên 1970. C++ được coi như là ngôn ngữ bậc trung (middle-level), kết hợp
các đặc điểm và tính năng của ngôn ngữ bậc cao và bậc thấp. Trước C++, ngôn
ngữ lập trình C được phát triển trong năm 1972 bởi Dennis Ritchie tại phòng thí
nghiệm Bell Telephone, C chủ yếu là một ngôn ngữ lập trình hệ thống, một
ngôn ngữ để viết ra hệ điều hành. Hệ điều hành nổi tiếng Windows cũng được
viết bằng C/C++. Từ thập niên 1990, C++ đã trở thành một trong những ngôn
ngữ thương mại ưa thích và phổ biến của lập trình viên. C++ là một phiên bản
mở rộng của ngôn ngữ lập trình C. Những bản cập nhật gần đây nhất là C++ 14
và C++ 17, và C++ 20 , đã và đang mang đến những tính năng hỗ trợ rất lớn
cho lập trình viên C++.

15
Chương 2. Lập trình cơ bản

Hình 2. 2 Lịch sử phát triển của C++

c) Thiết lập môi trường phát triển C++


C ++ là ngôn ngữ lập trình đa năng và được sử dụng rộng rãi ngày nay để
lập trình cạnh tranh. Nó có các tính năng lập trình bắt buộc, hướng đối tượng và
chung chung. C++ chạy trên rất nhiều nền tảng như Windows, Linux, Unix,
Mac, v.v. Trước khi chúng ta bắt đầu lập trình với C++. Chúng ta sẽ cần một
môi trường được thiết lập trên máy tính cục bộ của mình để biên dịch và chạy
thành công các chương trình C++ của chúng tôi. Nếu không muốn thiết lập một
môi trường cục bộ, thì cũng có thể sử dụng các IDE trực tuyến để biên dịch
chương trình.

Sử dụng IDE trực tuyến: IDE là viết tắt của môi trường phát triển tích
hợp. IDE là một ứng dụng phần mềm cung cấp phương tiện cho một lập trình
viên máy tính để phát triển phần mềm. Có rất nhiều IDE trực tuyến có sẵn mà
chúng ta có thể sử dụng để biên dịch và chạy các chương trình của mình một
cách dễ dàng mà không cần thiết lập môi trường phát triển cục bộ.

*Thiết lập môi trường cục bộ


Để thiết lập môi trường phát triển cá nhân của riêng chúng ta trên máy cục
bộ, chúng ta cần cài đặt hai phần mềm quan trọng:
 Trình soạn thảo văn bản : Trình soạn thảo văn bản là loại chương trình
được sử dụng để chỉnh sửa hoặc viết văn bản. Chúng ta sẽ sử dụng trình
soạn thảo văn bản để nhập các chương trình C++. Phần mở rộng thông
thường của tệp văn bản là (.txt) nhưng tệp văn bản chứa chương trình

16
Chương 2. Lập trình cơ bản

C++ phải được lưu với phần mở rộng '.CPP' hoặc '.C'. Các tệp kết thúc
bằng phần mở rộng '.CPP' và '.C' được gọi là tệp mã nguồn và chúng
được cho là chứa mã nguồn được viết bằng ngôn ngữ lập trình C++.
Phần mở rộng này giúp trình biên dịch xác định rằng tệp chứa chương
trình C++. Trước khi bắt đầu lập trình với C++, người ta phải cài đặt
trình soạn thảo văn bản để viết chương trình.
 Trình biên dịch C++: Sau khi đã cài đặt trình soạn thảo văn bản, nhập
và lưu chương trình của mình trong một tệp có phần mở rộng '.CPP',
chúng ta sẽ cần một trình biên dịch C++ để biên dịch tệp này. Trình
biên dịch là một chương trình máy tính chuyển đổi ngôn ngữ cấp cao
thành ngôn ngữ cấp thấp mà máy có thể hiểu được. Nói cách khác,
chúng ta có thể nói rằng nó chuyển đổi mã nguồn được viết bằng ngôn
ngữ lập trình sang ngôn ngữ máy tính khác mà máy tính hiểu được. Để
biên dịch một chương trình C++, chúng ta sẽ cần một trình biên dịch
C++ để chuyển đổi mã nguồn được viết bằng C++ thành mã máy. Dưới
đây là chi tiết về cách thiết lập trình biên dịch trên các nền tảng khác
nhau.
d) Cài đặt
*Cài đặt Linux: Chúng ta sẽ cài đặt trình biên dịch GNU GCC trên Linux.
Để cài đặt và làm việc với trình biên dịch GCC trên máy Linux của chúng ta,
hãy tiến hành theo các bước dưới đây: Trước tiên, chúng ta phải chạy hai lệnh
dưới đây từ cửa sổ đầu cuối Linux của mình:

sudo apt-get cập nhật


Sudo apt-get cài đặt gcc
sudo apt-get cài đặt g ++

 Lệnh này sẽ cài đặt trình biên dịch GCC trên hệ thống của chúng ta. Chúng
ta cũng có thể chạy lệnh dưới đây:
Sudo apt-get cài đặt bản dựng cần thiết
 Lệnh này sẽ cài đặt tất cả các thư viện cần thiết để biên dịch và chạy
chương trình C++.

17
Chương 2. Lập trình cơ bản

 Sau khi hoàn thành bước trên, nên kiểm tra xem trình biên dịch GCC đã
được cài đặt chính xác trong hệ thống hay chưa. Để làm điều này, chúng ta
phải chạy lệnh dưới đây từ thiết bị đầu cuối Linux:
g++ --version
 Nếu đã hoàn thành hai bước trên mà không có bất kỳ lỗi nào, thì môi
trường Linux đã được thiết lập và sẵn sàng sử dụng để biên dịch các
chương trình C++. Trong các bước tiếp theo, chúng ta sẽ tìm hiểu cách biên
dịch và chạy chương trình C++ trên Linux bằng trình biên dịch GCC.
 Viết chương trình trong một tệp văn bản và lưu nó với bất kỳ tên tệp nào và
phần mở rộng .CPP. Chúng ta đã viết một chương trình để hiển thị “Xin
chào thế giới” và lưu nó trong một tệp có tên tệp “helloworld.cpp” trên máy
tính để bàn.
 Bây giờ phải mở thiết bị đầu cuối Linux và di chuyển đến thư mục mà
chúng ta đã lưu tệp của mình. Sau đó, chạy lệnh dưới đây để biên dịch tệp
đó:
g++ filename.cpp -o any-name
 filename.cpp là tên của tệp mã nguồn. Trong trường hợp của chúng ta, tên
là “helloworld.cpp” và any-name có thể là bất kỳ tên nào được chọn. Tên
này sẽ được gán cho tệp thực thi được tạo bởi trình biên dịch sau khi biên
dịch. Trong trường hợp của chúng ta nên chọn bất kỳ tên nào là “hello”.
 Chúng ta sẽ chạy lệnh trên dưới dạng:
g++ helloworld.cpp -o hello
 Sau khi thực hiện lệnh trên, chúng ta sẽ thấy một tệp mới được tạo tự động
trong cùng thư mục mà chúng đã lưu tệp nguồn và tên của tệp này là tên
chúng ta đã chọn là any-name .
 Bây giờ để chạy chương trình chúng ta phải chạy lệnh dưới đây:
./hello
 Lệnh này sẽ chạy chương trình của chúng ta trong cửa sổ đầu cuối.

* Cài đặt trên Windows


Có rất nhiều IDE dành cho hệ điều hành windows mà chúng có thể sử
dụng để làm việc dễ dàng với ngôn ngữ lập trình C++. Một trong những IDE
phổ biến là Code::Blocks . Để tải xuống Code::Blocks, chúng ta có thể truy cập
liên kết này . Khi đã tải xuống tệp cài đặt Code::Blocks từ liên kết đã cho, hãy

18
Chương 2. Lập trình cơ bản

mở tệp đó và làm theo hướng dẫn để cài đặt. Sau khi cài đặt thành công
Code::Blocks, chúng ta vào menu File -> Chọn New và tạo một file Empty . Bây
giờ hãy viết chương trình C++ vào tệp trống này và lưu tệp với phần mở rộng
'.cpp'. Sau khi lưu tệp với phần mở rộng '.cpp', hãy chuyển đến menu Build và
chọn tùy chọn Build and Run .

*Cài đặt Mac OS X


Nếu chúng ta là người dùng Mac thì phải tải xuống Xcode. Để tải xuống
Xcode, chúng ta phải truy cập trang web của apple hoặc chúng ta có thể tìm
kiếm nó trên cửa hàng ứng dụng của apple. Chúng ta có thể theo liên kết
developer.apple.com/technologies/tools/ để tải xuống Xcode. Chúng ta sẽ tìm
thấy tất cả các hướng dẫn cài đặt cần thiết ở đó. Sau khi cài đặt Xcode thành
công, hãy mở ứng dụng Xcode.

Để tạo một dự án mới. Vào menu File -> chọn New -> chọn Project. Điều
này sẽ tạo ra một dự án mới cho chúng ta. Bây giờ trong cửa sổ tiếp theo, chúng
ta phải chọn một mẫu cho dự án của mình. Để chọn mẫu C++, hãy chọn tùy
chọn Ứng dụng nằm trong phần OS X trên thanh bên trái. Bây giờ, chọn các
công cụ dòng lệnh từ các tùy chọn có sẵn và nhấn nút Tiếp theo trên cửa sổ tiếp
theo, cung cấp tất cả các chi tiết cần thiết như 'tên tổ chức', 'Tên sản phẩm', v.v.
Nhưng hãy đảm bảo chọn ngôn ngữ là C++ . Sau khi điền các chi tiết, nhấn nút
tiếp theo để tiến hành các bước tiếp theo. Chọn vị trí mà chúng ta muốn lưu dự
án của mình. Sau đó, chọn tệp main.cpp từ danh sách thư mục trên thanh bên
trái.

Bây giờ sau khi mở tệp main.cpp, chúng ta sẽ thấy một chương trình c ++
được viết sẵn hoặc mẫu được cung cấp. Chúng ta có thể thay đổi chương trình
này theo yêu cầu của chúng ta. Để chạy chương trình C++, chúng ta phải vào
menu Sản phẩm và chọn tùy chọn Chạy từ trình đơn thả xuống. Một IDE khác
rất dễ sử dụng và phổ biến nhất hiện nay là VSC (Visual Studio Code), cho cả
Windows và Mac OS.
2.2.1. Bộ kí tự và từ khóa
Bộ kí tự:

19
Chương 2. Lập trình cơ bản

Bộ chữ viết trong ngôn ngữ C/C++ bao gồm những ký tự, ký hiệu sau:
(phân biệt chữ in hoa và in thường):
 26 chữ cái Latinh lớn: A,B,C...Z
 26 chữ cái Latinh nhỏ: a,b,c ...z
 10 chữ số thập phân: 0,1,2...9
 Các ký hiệu toán học: +, -, *, /, =, <, >, (, )
 Các ký hiệu đặc biệt: . : ; " ' _ @ # $ ! ^ [ ] { } ...
 Dấu cách hay khoảng trống
Từ khóa:
Từ khóa của một ngôn ngữ lập trình là các từ dành riêng cho ngôn ngữ đó
(reserved words), giống như từ vựng của một ngôn ngữ nói thông thường. Trong
C/C++, từ khóa với mục đích đã được xác định trước được sử dụng để khai báo
biến, xây dựng câu lệnh, điều khiển chương chình, v.v. Một lưu ý là từ khóa
không thể được dùng để đặt tên một biến, hàm hay một đối tượng. Một số ví dụ
về từ khóa của C++ như:
 Từ khóa char dùng để khai báo kiểu dữ liệu kí tự.
 Từ khóa if, else dùng để xây dựng câu lệnh điều kiện.
 Từ khóa for dùng để xây dựng câu lệnh cấu trúc vòng lặp.

Hình 2. 3 Các bộ ký tự và từ khóa trong C++

2.2.2. Kiểu dữ liệu


Kiểu dữ liệu xác định giá trị mà một biến có thể nhận hay giá trị mà một
hàm có thể trả về. Kiểu dữ liệu xác định kích thước theo byte của dữ liệu đó.

20
Chương 2. Lập trình cơ bản

Trong C/C++ có bốn kiểu dữ liệu chính: Kiểu dữ liệu cơ bản, kiểu dữ liệu enum,
kiểu void và kiểu dữ liệu nâng cao.
Kiểu dữ liệu cơ bản:
Kiểu dữ liệu cơ bản là kiểu dữ liệu số học, có thể là số nguyên (integer)
hoặc số thực (float). Với kiểu dữ liệu số nguyên ta có các loại sau:

Kiểu Kích thước Vùng giá trị


Char 1 byte -128 tới 127 hoặc 0 tới 255
unsigned char 1 byte 0 tới 255
signed char 1 byte -128 tới 127
Int 2 hoặc 4 bytes -32,768 tới 32,767 hoặc -2,147,483,648
tới 2,147,483,647
unsigned int 2 hoặc 4 bytes 0 tới 65,535 hoặc 0 tới 4,294,967,295
short 2 bytes -32,768 tới 32,767
unsigned short 2 bytes 0 tới 65,535
Long 4 bytes -2,147,483,648 tới 2,147,483,647
unsigned long 4 bytes 0 tới 4,294,967,295

Bảng 2. 1 Các kiểu dữ liệu cơ bản trong C++

Cùng là dữ liệu kiểu số học nhưng chúng ta có kiểu dữ liệu khác nhau. Điều
này giúp tiết kiệm bộ nhớ tùy thuộc vào vấn đề cần giải quyết. Ví dụ như nếu
muốn lưu tuổi của một người ta chỉ cần dùng kiểu unsigned char, nhưng nếu dữ
liệu lớn hơn như số lượng người trong một thành phố, ta cần sử dụng kiểu
unsigned int. Tương tự với kiểu dữ liệu số thực (dấu phảy động) ta cũng có các
loại sau:
Kiểu Kích thước Vùng giá trị Độ chính xác
Float 4 bytes xấp xỉ 10-38 đến 1038 6 vị trí thập phân
double 8 bytes xấp xỉ 10-308 đến 10308 15 vị trí thập phân

21
Chương 2. Lập trình cơ bản

long 10 bytes xấp xỉ 10-4932 đến 19 vị trí thập phân


double 104932

Bảng 2. 2 Kiểu dữ liệu số thực

Sau đây là ví dụ sẽ tạo ra kích thước chính xác của các loại dữ liệu khác
nhau trên máy tính:

include <iostream>
using namespace std;

int main() {
cout << "Size of char : " << sizeof(char) << endl;
cout << "Size of int : " << sizeof(int) << endl;
cout << "Size of short int : " << sizeof(short int) << endl;
cout << "Size of long int : " << sizeof(long int) << endl;
cout << "Size of float : " << sizeof(float) << endl;
cout << "Size of double : " << sizeof(double) << endl;
cout << "Size of wchar_t : " << sizeof(wchar_t) << endl;
return 0;
}
Ví dụ này sử dụng endl , sẽ chèn một ký tự dòng mới sau mỗi dòng và
toán tử << đang được sử dụng để chuyển nhiều giá trị ra màn hình. Việc sử dụng
toán tử sizeof() để lấy kích thước của các loại dữ liệu khác nhau. Khi đoạn mã
trên được biên dịch và thực thi, nó tạo ra kết quả sau đây có thể khác nhau giữa
các máy:

Size of char : 1
Size of int : 4

22
Chương 2. Lập trình cơ bản

Size of short int : 2


Size of long int : 4
Size of float : 4
Size of double : 8
Size of wchar_t : 4

2.2.3. Tên, biến và hằng


Một biến cung cấp bộ lưu trữ được đặt tên mà các chương trình người
dùng có thể thao tác. Mỗi biến trong C++ có một kiểu xác định, xác định kích
thước và cách bố trí bộ nhớ của biến phạm vi giá trị có thể được lưu trữ trong bộ
nhớ đó và tập hợp các hoạt động có thể được áp dụng cho biến.

STT Kiểu biến và mô tả

1 Bool

Lưu trữ 2 giá trị 0 hoặc 1 ( tương đương với false và true)

2 Char
Điển hình là một octet đơn (một byte). Đây là một kiểu số nguyên.

3 Int

Kiểu số nguyên

4 Float

Số thực

5 double
Một giá trị dấu phẩy động có độ chính xác kép.

23
Chương 2. Lập trình cơ bản

6 void

Đại diện cho hàm không có kiểu dữ liệu trả về.

7 wchar_t
Kiểu dữ liệu ký tự có phạm vi rộng

Bảng 2. 3 Các kiểu biến và mô tả

Định nghĩa biến trong C++


Một định nghĩa biến cho trình biên dịch biết vị trí và dung lượng lưu trữ
cần tạo cho biến. Một định nghĩa biến chỉ định một kiểu dữ liệu và chứa danh
sách một hoặc nhiều biến của kiểu đó như sau:
type variable_list;
Tên hay còn gọi là danh biểu (identifier) được dùng để đặt cho chương
trình, hằng, kiểu, biến, chương trình con... Tên có hai loại là tên chuẩn và tên do
người lập trình đặt.Tên chuẩn là tên do C++ đặt sẵn như tên kiểu: int, char,
float,…; tên hàm: sizeof, sort, find… Tên do người lập trình tự đặt để dùng
trong chương trình của mình. Sử dụng bộ chữ cái, chữ số và dấu gạch dưới (_)
để đặt tên, nhưng phải tuân thủ quy tắc:
 Bắt đầu bằng một chữ cái hoặc dấu gạch dưới
 Không có khoảng trống ở giữa tên
 Không được trùng với từ khóa
 Độ dài tối đa của tên là không giới hạn, tuy nhiên chỉ có 31 ký tự
đầu tiên là có ý nghĩa
 Không cấm việc đặt tên trùng với tên chuẩn nhưng khi đó ý nghĩa
của tên chuẩn không còn giá trị nữa.
Ví dụ: Khai báo các biến age, number_of_student, _total, average,
_time_waiting.

int main(){
//khai báo biến số nguyên
int age, number_of_student, _total;

24
Chương 2. Lập trình cơ bản

//khai báo biến số thực


double average, _time_waiting;

return 0;
}

Biến là một đại lượng được người lập trình định nghĩa và được đặt tên
thông qua việc khai báo biến. Biến dùng để chứa dữ liệu trong quá trình thực
hiện chương trình và giá trị của biến có thể bị thay đổi trong quá trình này. Mỗi
biến phải thuộc về một kiểu dữ liệu xác định và có miền giá trị thuộc kiểu đó.

Vị trí khai báo biến trong C++


Trong ngôn ngữ lập trình C++, ta phải khai báo biến đúng vị trí. Nếu khai
báo (đặt các biến) không đúng vị trí sẽ dẫn đến những sai sót ngoài ý muốn mà
người lập trình không lường trước. Chúng ta có 2 cách đặt vị trí của biến như
sau:
 Khai báo biến ngoài: Các biến này được đặt bên ngoài tất cả các hàm và
nó có tác dụng hay ảnh hưởng đến toàn bộ chương trình (còn gọi là biến
toàn cục),
 Khai báo biến trong: Các biến được đặt ở bên trong hàm, chương trình
chính hay một khối lệnh. Các biến này chỉ có tác dụng hay ảnh hưởng đến
hàm, chương trình hay khối lệnh chứa nó. Khi khai báo biến, phải đặt các
biến này ở đầu của khối lệnh, trước các lệnh gán.

Ví dụ:

#include <iostream>
using namespace std;

//khai báo biến số thực f bên ngoài hàm main()


float f = 0.5;

int main(){

25
Chương 2. Lập trình cơ bản

//khai báo biến số nguyên tên _total bên trong hàm main()
int _total = 10;

f = 0.25;

//in ra giá trị f = 0.25


cout << f << endl;

return 0;
}

Hằng là đại lượng không đổi trong suốt quá trình thực thi của chương
trình. Cú pháp: const <Kiểu dữ liệu> <Tên_hằng> = Giá_trị;

Ví dụ: Hằng số pi kiểu số thực được khai báo

#include <iostream>
using namespace std;

int main(){
//khai báo hằng pi
const double pi = 3.14159;

return 0;
}

Bằng việc thêm từ khóa const khi khai báo biến, ta có thể khai báo các
loại biến hằng có kiểu khác nhau như hằng số thực, hằng số nguyên, hằng kí tự
hay hằng chuỗi kí tự. Số thực bao gồm các giá trị kiểu float, double, long
double, có thể được thể hiện theo cách viết thông thường là sử dụng dấu thập
phân (dấu .) hay sử dụng cách viết theo số khoa học. Một số thực được tách làm
hai phần: phần giá trị và phần mũ, cách nhau bằng kí tự e hoặc E. Hằng kí tự là
một kí tự riêng biệt viết trong cặp dấu nháy đơn (dấu ‘’), và cũng được xem như
trị số nguyên.

26
Chương 2. Lập trình cơ bản

2.2.3. Biểu thức


Biểu thức là một sự kết hợp giữa các toán tử (operator) và toán hạng
(operand) theo một trật tự nhất định. Mỗi toán hạng có thể là một hằng, một biến
hoặc một biểu thức khác. Trong ngôn ngữ C++, các toán tử cộng (+), trừ (-),
nhân (*), chia (/) làm việc tương tự như trong các ngôn ngữ khác. Bảng 2.xx
trình bày các toán tử số học.

Toán tử Ý nghĩa
+ Cộng

- Trừ

* Nhân

/ Chia

% Chia lấy phần dư

-- Giảm 1 đơn vị

++ Tăng 1 đơn vị

Bảng 2. 4 Các toán tử trong C++

Các toán tử quan hệ và toán tử logic được sử dụng trong các biểu thức mà
kết quả là đúng hoặc sai. Trong C++, mọi giá trị khác 0 được gọi là đúng, và 0 là
sai. Bảng 2.5, 2.6 đưa ra các toán tử quan hệ so sánh và toán tử logic theo toán
học và trong ngôn ngữ C++.

Theo toán học Toán tử Ý nghĩa


= == Bằng

27
Chương 2. Lập trình cơ bản

≠ != Khác

< < Nhỏ hơn

≤ <= Nhỏ hơn hoặc bằng

≥ >= Lớn hơn hoặc bằng

> > Lớn hơn

Bảng 2. 5 Các toán tử so sánh C++

28
Chương 2. Lập trình cơ bản

Toán tử logic Ý nghĩa


&& AND

|| OR

! NOT

Bảng 2. 6 Các toán tử logic trong C++

Toán tử ? và :
Trong C/C++ có thể sử dụng hai toán tử này để thay thế cho các câu lệnh
if-then-else. Cú pháp của câu lệnh được mô tả như sau:
Biểu_thức_1 ? Biểu_thức_2 : Biểu_thức_3
Câu lệnh có thể được giải thích là nếu biểu thức 1 đúng, giá trị bằng biểu
thức 2, ngược lại giá trị bằng biểu thức 3. Ví dụ:

#include <iostream>
using namespace std;

int main(){
//khai báo và khởi tạo x, y
int x = 10;

int y;
//cú pháp với hai toán tử ? và :
y = x > 20 ? 1 : 0;

//in ra giá trị y = 0


cout << y << endl;

29
Chương 2. Lập trình cơ bản

return 0;
}

Trong trường hợp này, biểu thức 1 là x > 20, có giá trị sai. Vì vậy, giá trị
của y sẽ bằng biểu thức 3 (bằng 0). Nếu giả sử biểu thức 1 là x >5 (trả về kết quả
đúng) thì giá trị của y lúc này sẽ bằng 1.
Thứ tự ưu tiên của các toán tử:
Một điểm quan trọng trong biểu thức đó là thứ tự ưu tiên của các toán tử.
Thông thường trong một biểu thức chúng ta có thể sử dụng dấu ngoặc đơn ( ) để
thể hiện độ ưu tiên của một phép tính. Trong ngôn ngữ lập trình C++ nói riêng
và các ngôn ngữ lập trình khác nói chung, thứ tự ưu tiên của các toán tử xác
định mức độ ưu tiên thực hiện trước của các toán tử. Thứ tự ưu tiên trong C++
được thể hiện trong hình:

Hình 2. 4 Thứ tự ưu tiên của các toán tử

2.2.4. Mảng và con trỏ


2.2.4.1 Con trỏ
Con trỏ là biểu diễn tượng trưng của địa chỉ. Chúng cho phép các chương
trình mô phỏng cuộc gọi theo tham chiếu cũng như tạo và thao tác các cấu trúc
dữ liệu động. Lặp lại các phần tử trong mảng hoặc các cấu trúc dữ liệu khác là

30
Chương 2. Lập trình cơ bản

một trong những cách sử dụng chính của con trỏ. Địa chỉ của biến bạn đang làm
việc được gán cho biến con trỏ trỏ đến cùng kiểu dữ liệu (chẳng hạn như int
hoặc string).
Cú pháp:
datatype *var_name;
int *ptr; // ptr can point to an address which holds int data

Hình 2. 5 Cách mà con trỏ làm việc trong C++

Làm thế nào để sử dụng một con trỏ?


 Định nghĩa một biến con trỏ
 Gán địa chỉ của một biến cho một con trỏ bằng cách sử dụng toán tử một
ngôi (&) trả về địa chỉ của biến đó.
 Truy cập giá trị được lưu trữ trong địa chỉ bằng toán tử một ngôi (*) trả về
giá trị của biến nằm ở địa chỉ được chỉ định bởi toán hạng của nó.
Lý do chúng ta kết hợp kiểu dữ liệu với một con trỏ là vì nó biết dữ liệu
được lưu trữ trong bao nhiêu byte . Khi chúng ta tăng một con trỏ, chúng ta tăng
con trỏ theo kích thước của kiểu dữ liệu mà nó trỏ tới.
Ví dụ:
// C++ program to illustrate Pointers

#include <bits/stdc++.h>

31
Chương 2. Lập trình cơ bản

using namespace std;


void geeks()
{
int var = 20;

// declare pointer variable


int* ptr;

// note that data type of ptr and var must be same


ptr = &var;

// assign the address of a variable to a pointer


cout << "Value at ptr = " << ptr << "\n";
cout << "Value at var = " << var << "\n";
cout << "Value at *ptr = " << *ptr << "\n";
}
// Driver program
int main()
{
geeks();
return 0;
}
Chạy đoạn chương trình trên ta sẽ thu được kết quả sau:
Giá trị tại ptr = 0x7ffe454c08cc
Giá trị tại var = 20
Giá trị tại *ptr = 20

2.2.4.2 Mảng
Mảng được định nghĩa như sau:
 Nó là một nhóm các biến có kiểu dữ liệu tương tự được tham chiếu
bởi một phần tử duy nhất.

32
Chương 2. Lập trình cơ bản

 Các phần tử của nó được lưu trữ trong một vị trí bộ nhớ liền kề.
 Kích thước của mảng nên được đề cập trong khi khai báo nó.
 Các phần tử của mảng luôn được tính từ số không (0) trở đi.
 Các phần tử của mảng có thể được truy cập bằng cách sử dụng vị trí
của phần tử trong mảng.
 Mảng có thể có một hoặc nhiều chiều.
Một mảng trong C++ hoặc trong bất kỳ ngôn ngữ lập trình nào là một tập
hợp các mục dữ liệu tương tự được lưu trữ tại các vị trí bộ nhớ liền kề và các
phần tử có thể được truy cập ngẫu nhiên bằng cách sử dụng các chỉ số của một
mảng. Chúng có thể được sử dụng để lưu trữ tập hợp các kiểu dữ liệu nguyên
thủy như int, float, double, char, v.v. của bất kỳ kiểu cụ thể nào. Ngoài ra, một
mảng trong C++ có thể lưu trữ các kiểu dữ liệu dẫn xuất như cấu trúc, con trỏ,
v.v. Dưới đây là hình ảnh đại diện của một mảng.

Hình 2. 6 Minh họa một mảng C++

Tại sao chúng ta cần mảng?


Chúng ta có thể sử dụng các biến thông thường (v1, v2, v3, ..) khi chúng
ta có số lượng đối tượng nhỏ, nhưng nếu chúng ta muốn lưu trữ một số lượng
lớn các thể hiện, việc quản lý chúng bằng các biến thông thường trở nên khó
khăn. Ý tưởng của một mảng là biểu diễn nhiều trường hợp trong một biến.
Thuận lợi:
 Tối ưu hóa mã: chúng tôi có thể truy xuất hoặc sắp xếp dữ liệu một
cách hiệu quả.
 Truy cập ngẫu nhiên: Chúng tôi có thể lấy bất kỳ dữ liệu nào ở vị
trí chỉ mục.
Nhược điểm:

33
Chương 2. Lập trình cơ bản

 Giới hạn kích thước: Chúng ta chỉ có thể lưu trữ kích thước cố định
của các phần tử trong mảng. Nó không tăng kích thước của nó
trong thời gian chạy.
Ví dụ:

#include <iostream>
using namespace std;

int main()
{
// array declaration by specifying size
int arr1[10];

// With recent C/C++ versions, we can also


// declare an array of user specified size
int n = 10;
int arr2[n];

return 0;
}

Ưu điểm của Mảng trong C/C++:


 Truy cập ngẫu nhiên các phần tử bằng cách sử dụng chỉ số mảng.
 Sử dụng ít dòng mã hơn vì nó tạo ra một mảng gồm nhiều phần tử.
 Dễ dàng truy cập vào tất cả các yếu tố.
 Việc duyệt qua mảng trở nên dễ dàng bằng cách sử dụng một vòng lặp.
 Việc sắp xếp trở nên dễ dàng vì nó có thể được thực hiện bằng cách viết ít
dòng mã hơn.
Nhược điểm của Mảng trong C/C++:
 Cho phép nhập một số phần tử cố định được quyết định tại thời điểm khai
báo. Không giống như danh sách liên kết, mảng trong C không động.
 Chèn và xóa các phần tử có thể tốn kém vì các phần tử cần được quản lý
theo cấp phát bộ nhớ mới.

34
Chương 2. Lập trình cơ bản

2.2.4.3 Mối liên hệ giữa mảng và con trỏ


Giữa mảng và con trỏ có một sự liên hệ hết sức chặt chẽ. Những phần tử
của mảng có thể được xác đinh bằng chỉ số trong mảng, bên cạnh đó chúng cũng
có thể được xác lập qua biến con trỏ. Giả sử ta có một mảng các số nguyên arr
gồm 10 phần tử. Khi đó, &arr[0] là địa chỉ của phần tử đầu tiên của mảng. arr
là một hằng địa chỉ và không thể dùng trong câu lệnh gán hay toán tử tăng như
arr++.
Ví dụ:
#include <iostream>
using namespace std;

int main(){
//con trỏ ptr trỏ đến địa chỉ đầu tiên của mảng
int arr[10] {}, *ptr {arr};

//in ra địa chỉ phần tử đầu tiên của mảng


cout << arr << endl;

//in ra địa chỉ phần tử thứ 3(arr[2]) của mảng


cout << arr + 2 << endl;

//in ra giá trị arr[2] của mảng


cout << *(ptr + 2) << endl;

return 0;
}

Trong ví dụ này, một mảng arr gồm 10 phần tử được khởi tạo với các giá
trị ban đầu bằng 0, một con trỏ ptr được khởi tạo trỏ đến phần tử đầu tiên của

35
Chương 2. Lập trình cơ bản

mảng (tương tự ptr = &arr[0]). Con trỏ ptr + i sẽ trỏ vào phần tử thứ (i+1) của
mảng arr và nội dung của arr[i] sẽ là *(ptr +i).
Mảng các con trỏ:
Ta có thể tạo một mảng mà các phần tử của mảng là con trỏ theo cú pháp:
Kiểu_dữ_liệu * Tên_mảng_con_trỏ [số_phần_tử];
Cẩn phân biệt mảng con trỏ và mảng nhiều chiều. Mảng nhiều chiều có đủ
vùng nhớ dành sẵn cho các phần tử, còn mảng con trỏ chỉ dành vùng nhớ để lưu
một số lượng các con trỏ nhất định.
Câu hỏi:
 Hãy khai báo một mảng gồm 100 con trỏ char?
 Khai báo một mảng hai chiều, mỗi chiều gồm 10 con trỏ char?
Con trỏ trỏ tới con trỏ:
Con trỏ trỏ tới con trỏ đề cập đến việc một con trỏ mà giá trị của con trỏ
đó là địa chỉ của một con trỏ khác. Khai báo một con trỏ như vậy như sau:
Kiểu_dữ_liệu ** Tên_con_trỏ;
Ta có thể mô tả một con trỏ trỏ tới con trỏ như trong ví dụ sau:
int main()
{
int x = 10;

// con trỏ ptr trỏ đến x


int* ptr = &x;

// con trỏ pointer_to_ptr trỏ đến ptr


int** pointer_to_ptr = &ptr;

return 0;
}
Trong ví dụ trên, một con trỏ hai cấp được khai báo theo cú pháp đã đưa
ra. Trong trường hợp này, con trỏ hai cấp mang giá trị là địa chỉ của con trỏ ptr.
Toán tử lấy giá trị cũng có thể được sử dụng với con trỏ đã khai báo.
*pointer_to_ptr là ptr, và *pointer_to_ptr là x. Để Ta cũng có thể mô tả một

36
Chương 2. Lập trình cơ bản

mảng hai chiều qua con trỏ trỏ tới con trỏ như trong ví dụ sau: Ma trận vuông
arr, mỗi phần tử tăng lên một đơn vị và được in ra màn hình.

#include <iostream>
using namespace std;

int main()
{
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

for (int i = 0; i < 3; i++)


for (int j = 0; j < 3; j++)
{
(*(*(arr + i) + j))++;
cout << arr[i][j] << endl;
}

return 0;
}
Quản lý bộ nhớ:
Khi chúng ta khai báo, khởi tạo một biến hay đối tượng trong chương
trình, bộ nhớ được cấp phát cho biến đó. Trong thực tế nhiều khi không thể xác
định trước được kích thước bộ nhớ cần thiết cho chương trình làm việc, đánh đổi
bằng việc khai báo dự trữ bộ nhớ quá lớn. Việc dùng bộ nhớ động cho phép xác
định bộ nhớ cần thiết trong quá trình thực hiện của chương trình, đồng thời giải
phóng chúng khi không cần đến. Bộ nhớ khi đó có thể được dùng cho tác vụ
khác.
 Trong C, chúng ta dùng các hàm malloc, calloc, realloc và free để cấp
phát, tái cấp phát và giải phóng bộ nhớ
 Trong C++, ta dùng new và delete
Để cấp phát bộ nhớ ta dùng:

37
Chương 2. Lập trình cơ bản

<biến_trỏ> = new <kiểu_dữ_liệu>;


<biến_trỏ> = new <kiểu_dữ_liệu>[số_phần tử]; //cho mảng
Để giải phóng bộ nhớ ta dùng:
delete <biến_trỏ> ;
delete [ ] <biến_trỏ> ; //cho mảng
Ví dụ:
#include <iostream>
using namespace std;

int main()
{
int n;
int* arr;
cout << "The number of integers is: " << endl;
cin >> n;

//cấp phát động n phần tử


arr = new int [n];
if (arr==NULL) exit(1);

for(int i=0; i<n; i++){


cout << "Number "<< i+1 << " is: ";
cin >> arr[i];
cout << "arr at position " <<i<< " is: " <<arr[i]<< endl;
}

//giải phóng bộ nhớ đã cấp phát cho mảng arr


delete [] arr;

return 0;

38
Chương 2. Lập trình cơ bản

Với mảng nhiều chiều, để cấp phát động ta sử dụng con trỏ đa cấp. Ví dụ
với trường hợp khởi tạo ma trận HxC như sau:
Ví dụ a:
#include <iostream>
using namespace std;

#define H 2
#define C 3

int main()
{
//cấp phát
float** matran = new float* [H];
for (int i=0; i<H; i++)
matran[i] = new float [C];

//giải phóng
for (int i=0; i<H; i++)
delete [] matran[i];
delete [] matran;

return 0;
}
Ví dụ b:
#include <iostream>
using namespace std;

39
Chương 2. Lập trình cơ bản

#define H 2
#define C 3

int main()
{
//cấp phát
float** matran = new float* [H];
for (int i=0; i<H; i++)
matran[i] = new float [C];

//giải phóng
for (int i=0; i<H; i++)
delete [] matran[i];
delete [] matran;

return 0;
}
Ngoài ra chúng ta cũng có thể dùng mảng một chiều để biểu diễn mảng
hai chiều và cấp phát theo cách cấp phát cho mảng một chiều thông thường.
2.2. Hàm và kiểu dữ liệu có cấu trúc
2.2.1 Hàm
a) Khái niệm và khai báo hàm
Hàm là một tập hợp các câu lệnh nhận đầu vào, thực hiện một số tính toán
cụ thể và tạo đầu ra. Ý tưởng là đặt một số tác vụ thường được thực hiện hoặc
lặp đi lặp lại với nhau và tạo một hàm sao cho thay vì viết đi viết lại cùng một
mã cho các đầu vào khác nhau, chúng ta có thể gọi hàm. Nói một cách đơn giản,
một hàm là một khối mã chỉ chạy khi nó được gọi.

40
Chương 2. Lập trình cơ bản

Hình 2. 7 Cú pháp khai báo một hàm

Ví dụ về hàm:

// C++ Program to demonstrate working of a function


#include <iostream>
using namespace std;

// Following function that takes two parameters 'x' and 'y'


// as input and returns max of two input numbers
int max(int x, int y)
{
if (x > y)
return x;
else
return y;
}

// main function that doesn't receive any parameter and


// returns integer
int main()
{
int a = 10, b = 20;

// Calling above function to find max of 'a' and 'b'


int m = max(a, b);

41
Chương 2. Lập trình cơ bản

cout << "m is " << m;


return 0;
}

Tại sao chúng ta cần hàm?


Các hàm giúp chúng ta giảm dư thừa mã. Nếu hàm được thực hiện ở
nhiều nơi trong phần mềm, thì thay vì viết đi viết lại cùng một mã, chúng ta tạo
một hàm và gọi nó ở mọi nơi. Điều này cũng giúp bảo trì bới vì khi chúng ta
thay đổi tại một vị trí thì việc thực hiện các thay đổi trong tương lai đối các chức
năng này ở vị trí khác cũng được thực hiện. Hàm làm cho mã mô-đun . Hãy xem
xét một tệp lớn có nhiều dòng mã. Việc đọc và sử dụng mã trở nên thực sự đơn
giản nếu mã được chia thành các hàm. Các hàm cung cấp sự trừu tượng . Ví dụ,
chúng ta có thể sử dụng các hàm thư viện mà không phải lo lắng về công việc
bên trong của chúng.

Khai báo hàm?


Một khai báo hàm cho trình biên dịch biết về số lượng tham số, hàm nhận
kiểu dữ liệu của tham số và trả về kiểu hàm. Đặt tên tham số trong khai báo hàm
là tùy chọn khi khai báo hàm. Dưới đây là một ví dụ về khai báo hàm. (tên tham
số không có trong phần khai báo bên dưới)

42
Chương 2. Lập trình cơ bản

Hình 2. 8 Khai báo hàm

Ví dụ khai báo hàm:

// C++ Program to show function that takes


// two integers as parameters and returns
// an integer
int max(int, int);

// A function that takes an int


// pointer and an int variable
// as parameters and returns
// a pointer of type int
int* swap(int*, int);

// A function that takes


// a char as parameter and
// returns a reference variable
char* call(char b);

// A function that takes a


// char and an int as parameters
// and returns an integer

43
Chương 2. Lập trình cơ bản

int fun(char, int);

a) Các kiểu khai báo hàm

Hình 2. 9 Các loại hàm khia báo trong C++

*Hàm do người dùng xác định: Các hàm do người dùng xác định là các khối mã
do người dùng/khách hàng xác định được tùy chỉnh đặc biệt để giảm độ phức
tạp của các chương trình lớn. Chúng còn thường được gọi là “ hàm tùy chỉnh ”
được xây dựng chỉ để đáp ứng điều kiện mà người dùng đang gặp phải vấn đề
đồng thời giảm độ phức tạp của toàn bộ chương trình.

*Hàm thư viện: Các hàm thư viện còn được gọi là “ Hàm dựng sẵn “. Các chức
năng này là một phần của gói trình biên dịch đã được xác định và bao gồm một
chức năng đặc biệt với các ý nghĩa đặc biệt và khác nhau. Hàm dựng sẵn mang
lại cho lợi ích rất lớn vì chúng ta có thể trực tiếp sử dụng chúng mà không cần
xác định chúng trong khi ở hàm do người dùng định nghĩa, chúng ta phải khai
báo và xác định hàm trước khi sử dụng chúng. Ví dụ: sqrt(), setw(), strcat(), v.v.

b) Truyền tham số trong hàm


Các tham số được truyền cho hàm được gọi là tham số thực . Ví dụ,
trong chương trình bên dưới, 5 và 10 là các tham số thực tế. Các tham số mà
hàm nhận được gọi là các tham số hình thức . Ví dụ, trong chương trình trên
x và y là các tham số hình thức.

44
Chương 2. Lập trình cơ bản

Hình 2. 10 Hình minh họa các kiểu truyền tham số trong hàm

Có hai cách phổ biến nhất để truyền tham số:


 Truyền theo giá trị: Trong phương thức truyền tham số này, các giá
trị của tham số thực tế được sao chép sang tham số chính thức của
hàm và hai loại tham số được lưu trữ ở các vị trí bộ nhớ khác nhau.
Vì vậy, bất kỳ thay đổi nào được thực hiện bên trong các chức năng
không được phản ánh trong các tham số thực tế của người gọi.
 Chuyển qua tham chiếu: Cả tham số thực tế và chính thức đều đề
cập đến cùng một vị trí, do đó, bất kỳ thay đổi nào được thực hiện
bên trong hàm đều thực sự được phản ánh trong tham số thực tế của
trình gọi.

Chuyển qua tham chiếu được sử dụng khi giá trị của x không bị sửa đổi
bằng cách sử dụng hàm fun().

Ví dụ:

// C++ Program to demonstrate function definition


#include <iostream>
using namespace std;

45
Chương 2. Lập trình cơ bản

void fun(int x)
{
// definition of
// function
x = 30;
}

int main()
{
int x = 20;
fun(x);
cout << "x = " << x;
return 0;
}

*Các hàm sử dụng con trỏ


Hàm fun() mong đợi một con trỏ ptr tới một số nguyên (hoặc một địa
chỉ của một số nguyên). Nó sửa đổi giá trị tại địa chỉ ptr. Toán tử dereference
* được sử dụng để truy cập giá trị tại một địa chỉ. Trong câu lệnh '*ptr = 30',
giá trị tại địa chỉ ptr được thay đổi thành 30. Toán tử địa chỉ & được sử dụng
để lấy địa chỉ của một biến thuộc bất kỳ loại dữ liệu nào. Trong câu lệnh gọi
hàm 'fun(&x)', địa chỉ của x được chuyển để có thể sửa đổi x bằng địa chỉ
của nó.
Ví dụ:

// C++ Program to demonstrate working of


// function using pointers
#include <iostream>
using namespace std;

void fun(int* ptr) { *ptr = 30; }

int main()
{

46
Chương 2. Lập trình cơ bản

int x = 20;
fun(&x);
cout << "x = " << x;

return 0;
}

*Sự khác biệt giữa cuộc gọi hàm theo giá trị và gọi hàm theo tham chiếu trong
C++

Gọi theo giá trị Gọi theo tham chiếu

Một bản sao của giá trị được Một địa chỉ của giá trị được chuyển đến
chuyển đến hàm chức năng

Các thay đổi được thực hiện bên


trong chức năng không Các thay đổi được thực hiện bên trong
được phản ánh trên các chức năng chức năng cũng được phản ánh
khác bên ngoài chức năng

Đối số thực tế và chính thức sẽ được Đối số thực tế và chính thức sẽ được tạo
tạo ở vị trí bộ nhớ khác nhau trong cùng một vị trí bộ nhớ.

Bảng 2. 7 Sự khác biệt giữa cuộc gọi hàm theo giá trị và gọi hàm theo tham chiếu
trong C++

Những điểm cần nhớ về hàm trong C++:


 Hầu hết các chương trình C++ đều có một hàm gọi là main() được gọi bởi
hệ điều hành khi người dùng chạy chương trình.
 Mọi hàm đều có kiểu trả về. Nếu một hàm không trả về bất kỳ giá trị nào,
thì void được sử dụng làm kiểu trả về. Ngoài ra, nếu kiểu trả về của hàm
là void, chúng ta vẫn có thể sử dụng câu lệnh return trong phần thân của
định nghĩa hàm bằng cách không chỉ định bất kỳ hằng, biến, v.v. nào với

47
Chương 2. Lập trình cơ bản

nó, bằng cách chỉ nhắc đến 'return;' tuyên bố sẽ tượng trưng cho việc
chấm dứt chức năng như sau:

void function name(int a)


{
....... // Function Body
return; // Function execution would get terminated
}

 Để khai báo một hàm chỉ có thể được gọi mà không có bất kỳ tham số
nào, chúng ta nên sử dụng “ void fun(void) “. Như một lưu ý phụ, trong
C++, một danh sách trống có nghĩa là một hàm chỉ có thể được gọi mà
không có bất kỳ tham số nào. Trong C++, cả void fun() và void fun(void)
đều giống nhau.
2.2.2 Kiểu dữ liệu có cấu trúc C++
a) Khái niệm và khai báo
Chúng ta thường gặp phải các tình huống cần lưu trữ một nhóm dữ liệu
cho dù là kiểu dữ liệu giống nhau hay kiểu dữ liệu không giống nhau. Chúng ta
đã thấy Mảng trong C++ được sử dụng để lưu trữ tập hợp dữ liệu có kiểu dữ liệu
tương tự tại các vị trí bộ nhớ liền kề. Không giống như Mảng, Cấu trúc trong
C++ là kiểu dữ liệu do người dùng xác định được sử dụng để lưu trữ nhóm các
mục có kiểu dữ liệu không giống nhau.
Cấu trúc là gì?
Cấu trúc là kiểu dữ liệu do người dùng định nghĩa trong C/C++. Một cấu
trúc tạo ra một kiểu dữ liệu có thể được sử dụng để nhóm các mục thuộc các
kiểu có thể khác nhau thành một kiểu duy nhất.

48
Chương 2. Lập trình cơ bản

Hình 2. 11 Cấu trúc trong C++

Từ khóa 'struct' được sử dụng để tạo cấu trúc. Cú pháp chung để tạo một
cấu trúc như sau:

struct structureName{

member1;

member2;

member3;

memberN;

};

49
Chương 2. Lập trình cơ bản

Các cấu trúc trong C++ có thể chứa hai loại thành viên:
 Thành viên dữ liệu : Các thành viên này là các biến C++ bình
thường. Chúng ta có thể tạo một cấu trúc với các biến thuộc các
kiểu dữ liệu khác nhau trong C++.
 Các hàm thành viên : Các thành viên này là các hàm C++ bình
thường. Cùng với các biến, chúng ta cũng có thể bao gồm các hàm
bên trong một khai báo cấu trúc.
Ví dụ:

// Data Members
int roll;
int age;
int marks;

// Member Functions
void printDetails()
{
cout<<"Roll = "<<roll<<"\n";
cout<<"Age = "<<age<<"\n";
cout<<"Marks = "<<marks;
}

Trong cấu trúc trên, các thành viên dữ liệu là ba biến số nguyên để lưu
trữ số điểm, tuổi và điểm của bất kỳ học sinh nào và hàm thành viên là
printDetails() sẽ in tất cả các chi tiết trên của bất kỳ học sinh nào.

Cách khai báo biến cấu trúc?


Một biến cấu trúc có thể được khai báo bằng khai báo cấu trúc hoặc
dưới dạng một khai báo riêng như các kiểu cơ bản. Ví dụ:

// A variable declaration with structure declaration.


struct Point

50
Chương 2. Lập trình cơ bản

{
int x, y;
} p1; // The variable p1 is declared with 'Point'

// A variable declaration like basic data types


struct Point
{
int x, y;
};

int main()
{
struct Point p1; // The variable p1 is declared like a normal variable
}

c) Làm thế nào để khởi tạo các thành viên cấu trúc?
Các thành viên cấu trúc không thể được khởi tạo bằng khai báo. Ví dụ,
chương trình C sau đây bị lỗi trong quá trình biên dịch. Nhưng được coi là đúng
trong C++ 11 trở lên. Ví dụ:

struct Point
{
int x = 0; // COMPILER ERROR: cannot initialize members here
int y = 0; // COMPILER ERROR: cannot initialize members here
};

Lý do của lỗi trên rất đơn giản, khi một kiểu dữ liệu được khai báo, không
có bộ nhớ nào được cấp phát cho nó. Bộ nhớ chỉ được cấp phát khi các biến
được tạo. Các thành viên cấu trúc có thể được khởi tạo bằng cách khai báo trong
C++. Ví dụ, chương trình C++ sau đây Thực thi thành công mà không gây ra bất
kỳ lỗi nào.

51
Chương 2. Lập trình cơ bản

// In C++ We can Initialize the Variables with Declaration in Structure.

#include <iostream>
using namespace std;

struct Point {
int x = 0; // It is Considered as Default Arguments and no Error is Raised
int y = 1;
};

int main()
{
struct Point p1;

// Accessing members of point p1


// No value is Initialized then the default value is considered. ie x=0 and
y=1;
cout << "x = " << p1.x << ", y = " << p1.y<<endl;

// Initializing the value of y = 20;


p1.y = 20;
cout << "x = " << p1.x << ", y = " << p1.y;
return 0;
}

d) Làm cách nào để truy cập các phần tử cấu trúc?


Các thành viên cấu trúc được truy cập bằng toán tử dấu chấm (.). Ví dụ:

#include <iostream>
using namespace std;

struct Point {
int x, y;

52
Chương 2. Lập trình cơ bản

};

int main()
{
struct Point p1 = { 0, 1 };

// Accessing members of point p1


p1.x = 20;
cout << "x = " << p1.x << ", y = " << p1.y;

return 0;
}

e) Mảng cấu trúc là gì?


Giống như các kiểu dữ liệu nguyên thủy khác, chúng ta có thể tạo một
mảng cấu trúc. Ví dụ:

#include <iostream>
using namespace std;

struct Point {
int x, y;
};

int main()
{
// Create an array of structures
struct Point arr[10];

// Access array members


arr[0].x = 10;
arr[0].y = 20;

cout << arr[0].x << " " << arr[0].y;

53
Chương 2. Lập trình cơ bản

return 0;
}

f) Con trỏ cấu trúc là gì?


Giống như các kiểu nguyên thủy, chúng ta có thể có con trỏ tới một cấu
trúc. Nếu chúng ta có một con trỏ tới cấu trúc, các thành viên được truy cập
bằng toán tử mũi tên ( -> ) thay vì toán tử dấu chấm (.).

#include <iostream>
using namespace std;

struct Point {
int x, y;
};

int main()
{
struct Point p1 = { 1, 2 };

// p2 is a pointer to structure p1
struct Point* p2 = &p1;

// Accessing structure members using


// structure pointer
cout << p2->x << " " << p2->y;
return 0;
}
2.3. Luồng điểu khiển chương trình
Luồng điều khiển đề cập đến thứ tự các câu lệnh, tập lệnh, lời gọi hàm,
v.v trong chương trình được thực thi và là một trong những khái niệm cơ bản về
lập trình. Hầu hết các thuật toán đều có thể được triển khai bằng một trong ba
cấu trúc điều khiển cơ bản: Cấu trúc tuần tự, Cấu trúc rẽ nhánh và cấu trúc lặp.
Trong phần này, chúng ta thảo luận về một số cấu trúc điều khiển và vòng lặp cơ
bản trong ngôn ngữ lập trình C++.

54
Chương 2. Lập trình cơ bản

2.3.1 Cấu trúc rẽ nhánh


a) Đặt vấn đề
Ra quyết định trong C/C++ (if , if..else, Nested if, if-else-if ). Có những
tình huống trong cuộc sống thực khi chúng ta cần đưa ra một số quyết định và
dựa trên những quyết định này, chúng ta quyết định mình nên làm gì tiếp theo.
Các tình huống tương tự cũng phát sinh trong lập trình khi chúng ta cần đưa ra
một số quyết định và dựa trên những quyết định này, chúng ta sẽ thực thi khối
mã tiếp theo.
Ví dụ: trong C nếu x xảy ra thì thực hiện y, ngược lại thực hiện z. Cũng có
thể có nhiều điều kiện như trong C nếu x xảy ra thì thực hiện p, ngược lại nếu
điều kiện y xảy ra thì thực hiện q, ngược lại thực hiện r. Điều kiện này của C
else-if là một trong nhiều cách nhập nhiều điều kiện.

Hình 2. 12 Sơ đồ hình họa việc ra quyết định

Các câu lệnh ra quyết định trong các ngôn ngữ lập trình quyết định hướng
của luồng thực thi chương trình. Các câu lệnh ra quyết định có sẵn trong C hoặc
C++ là:
 câu lệnh if
 câu lệnh if-else
 câu lệnh netsed if

55
Chương 2. Lập trình cơ bản

 câu lệnh if-else-if


 câu lệnh switch
 câu lệnh nhảy:
 break
 continue
 goto
 return
b) Câu lệnh if trong C/C++
Câu lệnh if là câu lệnh ra quyết định đơn giản nhất. Nó được sử dụng để
quyết định xem một câu lệnh hoặc khối câu lệnh nào đó có được thực thi hay
không, tức là nếu một điều kiện nào đó đúng thì khối câu lệnh sẽ được thực
hiện, ngược lại thì không.
Cú pháp :

if(điều kiện)
{
// Các câu lệnh sẽ thực hiện nếu
// điều kiện đúng
}

Ở đây, điều kiện sau khi đánh giá sẽ là đúng hoặc sai. Câu lệnh if của C
chấp nhận các giá trị boolean – nếu giá trị là true thì nó sẽ thực thi khối câu lệnh
bên dưới nó, nếu không thì không. Nếu chúng ta không cung cấp dấu ngoặc
nhọn '{' và '}' sau if(condition) thì theo mặc định, câu lệnh if sẽ coi câu lệnh đầu
tiên ngay bên dưới nằm trong khối của nó.

*Sơ đồ

56
Chương 2. Lập trình cơ bản

Hình 2. 13 Sơ đồ minh họa câu lệnh if

Ví dụ minh họa:

// C program to illustrate If statement


#include <stdio.h>

int main()
{
int i = 10;

57
Chương 2. Lập trình cơ bản

if (i > 15) {
printf("10 is greater than 15");
}

printf("I am Not in if");


}

c) if-else trong C/C++


Chỉ riêng câu lệnh if đã cho chúng ta biết rằng nếu một điều kiện là đúng
thì nó sẽ thực thi một khối câu lệnh và nếu điều kiện là sai thì sẽ không. Nhưng
nếu chúng ta muốn làm gì khác nếu điều kiện là sai. Đây là câu lệnh C khác.
Chúng ta có thể sử dụng câu lệnh khác với câu lệnh if để thực thi một khối mã
khi điều kiện sai.
*Cú pháp

if (điều kiện)
{
// Thực thi khối này nếu
// điều kiện đúng
}
else
{
// Thực thi khối này nếu
// điều kiện sai
}

* Sơ đồ

58
Chương 2. Lập trình cơ bản

Hình 2. 14 Sơ đồ minh họa quá trình hoạt động của câu lệnh if-else

Ví dụ minh họa:

// C++ program to illustrate if-else statement


#include <iostream>
using namespace std;

int main()
{
int i = 20;

59
Chương 2. Lập trình cơ bản

if (i < 15)
cout << "i is smaller than 15";
else
cout << "i is greater than 15";

return 0;
}

d) Nested-if trong C/C++


Một if lồng nhau trong C là một câu lệnh if là đích của một câu lệnh if
khác. Các câu lệnh if lồng nhau có nghĩa là một câu lệnh if bên trong một câu
lệnh if khác. Có, cả C và C++ đều cho phép chúng ta lồng các câu lệnh if bên
trong các câu lệnh if, tức là chúng ta có thể đặt một câu lệnh if bên trong một
câu lệnh if khác.
*Cú pháp

if (điều kiện 1)
{
// Thực hiện khi điều kiện 1 đúng
if (điều kiện 2)
{
// Thực hiện khi điều kiện 2 đúng
}
}

* Sơ đồ

60
Chương 2. Lập trình cơ bản

Hình 2. 15 Sơ đồ minh họa luồng hoạt động của nested – if

Ví dụ minh họa:

// C++ program to illustrate nested-if statement


#include <iostream>
using namespace std;

int main()
{
int i = 10;

if (i == 10) {
// First if statement
if (i < 15)
cout << "i is smaller than 15\n";

61
Chương 2. Lập trình cơ bản

// Nested - if statement
// Will only be executed if
// statement above is true
if (i < 12)
cout << "i is smaller than 12 too\n";
else
cout << "i is greater than 15";
}

return 0;
}

e) if-else-if trong C/C++


Tại đây, người dùng có thể quyết định trong số nhiều tùy chọn. Các câu
lệnh if trong C được thực thi từ trên xuống. Ngay khi một trong các điều kiện
kiểm soát if là đúng, câu lệnh liên quan đến if đó sẽ được thực thi và phần còn
lại của bậc thang C else-if bị bỏ qua. Nếu không có điều kiện nào là đúng, thì
câu lệnh khác cuối cùng sẽ được thực thi.
*Cú pháp

if (condition)
statement;
else if (condition)
statement;
.
.

else
statement;

* Sơ đồ

62
Chương 2. Lập trình cơ bản

Hình 2. 16 Sơ đồ minh họa luồng hoạt động của if -else- if

Ví dụ minh họa:

// C++ program to illustrate if-else-if ladder


#include <iostream>
using namespace std;

63
Chương 2. Lập trình cơ bản

int main()
{
int i = 20;

if (i == 10)
cout << "i is 10";
else if (i == 15)
cout << "i is 15";
else if (i == 20)
cout << "i is 20";
else
cout << "i is not present";
}

g) Các câu lệnh nhảy trong C/C++


Các câu lệnh này được sử dụng trong C hoặc C++ cho luồng điều khiển
vô điều kiện xuyên suốt các chức năng trong chương trình. Chúng hỗ trợ bốn
loại câu lệnh nhảy:
 *Break: Câu lệnh điều khiển vòng lặp này được sử dụng để kết thúc vòng
lặp. Ngay khi bắt gặp câu lệnh break từ bên trong một vòng lặp, các lần
lặp lại của vòng lặp sẽ dừng lại ở đó và quyền điều khiển sẽ ngay lập tức
quay trở lại câu lệnh đầu tiên sau vòng lặp từ vòng lặp đó.
 Cú pháp: Break;

Ví du:

// C++ program to illustrate


// to show usage of break
// statement
#include <iostream>
using namespace std;

void findElement(int arr[], int size, int key)


{

64
Chương 2. Lập trình cơ bản

// loop to traverse array and search for key


for (int i = 0; i < size; i++) {
if (arr[i] == key) {
cout << "Element found at position: "
<< (i + 1);
break;
}
}
}

// Driver program to test above function


int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6 };
int n = 6; // no of elements
int key = 3; // key to be searched

// Calling function to find the key


findElement(arr, n, key);

return 0;
}

 *Continue: Câu lệnh điều khiển vòng lặp này cũng giống như câu lệnh
ngắt . Câu lệnh continue ngược lại với câu lệnh break , thay vì kết thúc
vòng lặp, nó buộc phải thực hiện lần lặp tiếp theo của vòng lặp. Đúng
như tên gọi, câu lệnh continue buộc vòng lặp phải tiếp tục hoặc thực hiện
lần lặp tiếp theo. Khi câu lệnh continue được thực thi trong vòng lặp, mã
bên trong vòng lặp theo sau câu lệnh continue sẽ bị bỏ qua và lần lặp tiếp
theo của vòng lặp sẽ bắt đầu.
 Cú pháp: Continue

Ví dụ:

65
Chương 2. Lập trình cơ bản

// C++ program to explain the use


// of continue statement

#include <iostream>
using namespace std;

int main()
{
// loop from 1 to 10
for (int i = 1; i <= 10; i++) {

// If i is equals to 6,
// continue to next iteration
// without printing
if (i == 6)
continue;

else
// otherwise print the value of i
cout << i << " ";
}

return 0;
}

2.3.2 Vòng lặp


a) Đặt vấn đề
Trong Lập trình, đôi khi cần phải thực hiện một số thao tác nhiều lần hoặc
(giả sử) n lần. Các vòng lặp được sử dụng khi chúng ta cần thực hiện lặp đi lặp
lại một khối câu lệnh. Ví dụ : Giả sử chúng ta muốn in “Hello World” 10 lần.
Điều này có thể được thực hiện theo hai cách như hình dưới đây:
 Phương pháp thủ công (Phương pháp lặp): Theo cách thủ công,
chúng ta phải viết cout cho câu lệnh C++ 10 lần. Giả sử chúng ta phải
viết nó 20 lần (chắc chắn sẽ mất nhiều thời gian hơn để viết 20 câu)

66
Chương 2. Lập trình cơ bản

bây giờ hãy tưởng tượng chúng ta phải viết nó 100 lần, sẽ thực sự rất
bận rộn khi phải viết đi viết lại cùng một câu. Vì vậy, ở đây các vòng
lặp có vai trò của chúng.

Ví dụ theo cách thủ công:

// C++ program to Demonstrate the need of loops


#include <iostream>
using namespace std;

int main()
{
cout << "Hello World\n";
cout << "Hello World\n";
cout << "Hello World\n";
cout << "Hello World\n";
cout << "Hello World\n";
return 0;
}

b) Vòng lặp
Trong Loop, câu lệnh chỉ cần viết một lần và vòng lặp sẽ được thực hiện
10 lần như hình bên dưới. Trong lập trình máy tính, vòng lặp là một chuỗi các
hướng dẫn được lặp lại cho đến khi đạt được một điều kiện nhất định. Chủ yếu
có hai loại vòng lặp:
 Entry Controlled loops : Trong loại vòng lặp này, điều kiện kiểm
tra được kiểm tra trước khi vào thân vòng lặp. For Loop và While
Loop là các vòng lặp được kiểm soát mục nhập.
 Exit Controlled Loops: Trong loại vòng lặp này, điều kiện kiểm tra
được kiểm tra hoặc đánh giá ở phần cuối của thân vòng lặp. Do đó,
thân vòng lặp sẽ thực thi ít nhất một lần, bất kể điều kiện kiểm tra
là đúng hay sai. vòng lặp do- while là vòng lặp được kiểm soát
thoát.

67
Chương 2. Lập trình cơ bản

Hình 2. 17 Các vòng lặp trong C++

*Vòng lặp For


Vòng lặp For là một cấu trúc điều khiển lặp lại cho phép chúng ta viết
một vòng lặp được thực hiện trong một số lần cụ thể. Vòng lặp cho phép chúng
ta thực hiện n bước cùng nhau trên một dòng.
Cú pháp:

for (initialization expr; test expr; update expr)


{
// body of the loop
// statements we want to execute
}

Giải thích cú pháp:


 Initialization statement: Câu lệnh này chỉ được thực hiện một lần, ở đầu
vòng lặp for. Chúng ta có thể nhập khai báo nhiều biến cùng loại, chẳng
hạn int x=0, a=1, b=2. Các biến này chỉ có giá trị trong phạm vi của vòng
lặp. Biến được xác định trước vòng lặp có cùng tên sẽ bị ẩn trong quá
trình thực hiện vòng lặp.
 Condition: Câu lệnh này được đánh giá trước mỗi lần thực hiện phần thân
vòng lặp và hủy bỏ việc thực thi nếu điều kiện đã cho sai.
 Iteration execution: Câu lệnh này được thực thi sau phần thân vòng lặp,
trước điều kiện tiếp theo được đánh giá, trừ khi vòng lặp for bị hủy bỏ
trong phần thân (do break, goto, return hoặc một ngoại lệ được đưa ra.)

68
Chương 2. Lập trình cơ bản

Ví dụ:

for(int i = 0; i < n; i++)


{
// BODY
}

*Vòng lặp while


Trong khi nghiên cứu vòng lặp for, chúng ta đã thấy rằng số lần lặp được
biết trước, tức là số lần thân vòng lặp cần được thực hiện đã được biết trước.
vòng lặp while được sử dụng trong trường hợp chúng ta không biết chính xác số
lần lặp của vòng lặp trước đó. Việc thực hiện vòng lặp được kết thúc trên cơ sở
các điều kiện kiểm tra. Chúng ta đã nói rằng một vòng lặp chủ yếu bao gồm ba
câu lệnh – biểu thức khởi tạo, biểu thức kiểm tra và biểu thức cập nhật. Cú pháp
của ba vòng lặp – For, while và do while chủ yếu khác nhau ở vị trí của ba câu
lệnh này.
Cú pháp :

initialization expression;
while (test_expression)
{
// statements

update_expression;
}

Ví dụ:

// C++ program to Demonstrate while loop


#include <iostream>
using namespace std;

int main()
{

69
Chương 2. Lập trình cơ bản

// initialization expression
int i = 1;

// test expression
while (i < 6) {
cout << "Hello World\n";

// update expression
i++;
}

return 0;
}

*Vòng lặp do-while


Trong các vòng lặp Do-while, việc thực hiện vòng lặp cũng được kết thúc
trên cơ sở các điều kiện kiểm tra. Sự khác biệt chính giữa vòng lặp do-while và
vòng lặp while là trong vòng lặp do-while, điều kiện được kiểm tra ở phần cuối
của thân vòng lặp, tức là vòng lặp do-while được kiểm soát thoát ra trong khi hai
vòng lặp còn lại là vòng lặp kiểm soát vào.
Lưu ý: Trong vòng lặp do-while, thân vòng lặp sẽ thực thi ít nhất một lần
bất kể điều kiện kiểm tra.

Cú pháp:

initialization expression;
do
{
// statements

update_expression;
} while (test_expression);

Ví dụ:

70
Chương 2. Lập trình cơ bản

// C++ program to Demonstrate do-while loop


#include <iostream>
using namespace std;

int main()
{
int i = 2; // Initialization expression

do {
// loop body
cout << "Hello World\n";

// update expression
i++;

} while (i < 1); // test expression

return 0;
}
2.4. Vào ra file theo luồng
a) Chuyển hướng vào ra trong C++
C++ là ngôn ngữ lập trình hướng đối tượng, cho chúng ta khả năng không
chỉ định nghĩa các luồng của riêng mình mà còn chuyển hướng các luồng tiêu
chuẩn. Do đó, trong C++, luồng là một đối tượng có hành vi được định nghĩa
bởi một lớp. Do đó, bất cứ thứ gì hoạt động như một luồng cũng là một luồng.
Luồng các đối tượng trong C++ chủ yếu có ba loại:

 istream : Đối tượng luồng loại này chỉ có thể thực hiện các thao tác
nhập liệu từ luồng
 ostream : Những đối tượng này chỉ có thể được sử dụng cho các
hoạt động đầu ra.
 iostream : Có thể được sử dụng cho cả hoạt động đầu vào và đầu ra

71
Chương 2. Lập trình cơ bản

Tất cả các lớp này, cũng như các lớp luồng tệp, bắt nguồn từ các lớp: ios
và streambuf. Do đó, các đối tượng luồng tệp và luồng IO hoạt động tương tự
nhau.

Tất cả các đối tượng luồng cũng có một thành viên dữ liệu liên quan của
lớp streambuf. Nói một cách đơn giản, đối tượng streambuf là bộ đệm cho
luồng. Khi chúng tôi đọc dữ liệu từ một luồng, chúng tôi không đọc trực tiếp từ
nguồn mà thay vào đó, chúng ta đọc nó từ bộ đệm được liên kết với nguồn.
Tương tự, các hoạt động đầu ra được thực hiện đầu tiên trên bộ đệm, sau đó bộ
đệm được xóa (ghi vào thiết bị vật lý) khi cần.C ++ cho phép chúng tôi đặt bộ
đệm luồng cho bất kỳ luồng nào, vì vậy nhiệm vụ chuyển hướng luồng chỉ đơn
giản là thay đổi bộ đệm luồng được liên kết với luồng. Do đó, để chuyển hướng
Luồng A sang Luồng B, chúng ta cần thực hiện:
 Lấy bộ đệm luồng của A và lưu trữ ở đâu đó
 Đặt bộ đệm luồng của A thành bộ đệm luồng của B
 Nếu cần đặt lại bộ đệm luồng của A về bộ đệm luồng trước đó

Chúng ta có thể sử dụng hàm ios::rdbuf() để thực hiện hai thao tác bên
dưới:
1) stream_object.rdbuf(): Trả về con trỏ tới bộ đệm luồng của
stream_object
2) stream_object.rdbuf(streambuf * p): Đặt bộ đệm luồng cho đối tượng
được trỏ bởi p.

Dưới đây là một chương trình ví dụ dưới đây để hiển thị các bước:

// Cpp program to redirect cout to a file


#include <fstream>
#include <iostream>
#include <string>

using namespace std;

int main()

72
Chương 2. Lập trình cơ bản

{
fstream file;
file.open("cout.txt", ios::out);
string line;

// Backup streambuffers of cout


streambuf* stream_buffer_cout = cout.rdbuf();
streambuf* stream_buffer_cin = cin.rdbuf();

// Get the streambuffer of the file


streambuf* stream_buffer_file = file.rdbuf();

// Redirect cout to file


cout.rdbuf(stream_buffer_file);

cout << "This line written to file" << endl;

// Redirect cout back to screen


cout.rdbuf(stream_buffer_cout);
cout << "This line is written to screen" << endl;

file.close();
return 0;
}

b) Nhập xuất
C ++ đi kèm với các thư viện cung cấp cho chúng ta nhiều cách để thực
hiện đầu vào và đầu ra. Trong C++, đầu vào và đầu ra được thực hiện dưới dạng
một chuỗi byte hoặc thường được gọi là luồng.
 Luồng đầu vào: Nếu hướng của luồng byte là từ thiết bị (ví dụ: Bàn
phím) đến bộ nhớ chính thì quá trình này được gọi là đầu vào.
 Luồng đầu ra: Nếu hướng của luồng byte ngược lại, tức là từ bộ
nhớ chính đến thiết bị (màn hình hiển thị) thì quá trình này được
gọi là đầu ra.

73
Chương 2. Lập trình cơ bản

Các tệp tiêu đề có sẵn trong C++ cho các thao tác Nhập/Xuất là:
 iostream : iostream là viết tắt của luồng đầu vào-đầu ra tiêu chuẩn.
Tệp tiêu đề này chứa định nghĩa của các đối tượng như cin, cout,
cerr, v.v.
 iomanip : iomanip là viết tắt của bộ điều khiển đầu vào-đầu ra. Các
phương thức được khai báo trong các tệp này được sử dụng để thao
tác các luồng. Tệp này chứa các định nghĩa về setw, setprecision,
v.v.
 fstream : Tệp tiêu đề này chủ yếu mô tả luồng tệp. Tệp tiêu đề này
được sử dụng để xử lý dữ liệu được đọc từ tệp dưới dạng đầu vào
hoặc dữ liệu được ghi vào tệp dưới dạng đầu ra.
 bits/stdc++: Tệp tiêu đề này bao gồm mọi thư viện chuẩn. Trong
các cuộc thi lập trình, sử dụng tệp này là một ý tưởng hay, khi
chúng ta muốn giảm thời gian lãng phí khi làm việc nhà; đặc biệt là
khi thứ hạng của chúng ta nhạy cảm với thời gian.
Trong C++ sau các tệp tiêu đề, chúng ta thường sử dụng ' using
namespace std; '. Lý do đằng sau nó là tất cả các định nghĩa thư viện tiêu chuẩn
đều nằm trong không gian tên std. Vì các chức năng thư viện không được xác
định ở phạm vi toàn cầu, nên để sử dụng chúng, chúng tôi sử dụng không gian
tên std. Vì vậy, chúng ta không cần viết STD:: ở mọi dòng (ví dụ: STD::cout,
v.v.). Để biết thêm tham khảo bài viết này.

Hai trường hợp cout trong C++ và cin trong C++ của lớp iostream được
sử dụng rất thường xuyên để in đầu ra và lấy đầu vào tương ứng. Đây là hai
phương thức cơ bản nhất để lấy đầu vào và in đầu ra trong C++. Để sử dụng cin
và cout trong C++, người ta phải đưa tệp tiêu đề iostream vào chương trình.
2.5. Luồng fstream , ofstream và các tham số
Ở đây, chúng ta sẽ xây dựng một chương trình C++ để nối thêm một
chuỗi vào một tệp hiện có bằng 2 cách tiếp cận, tức là:
 Sử dụng ofstream
 Sử dụng fstream
Ngôn ngữ lập trình C++ cung cấp một thư viện gọi là fstream bao gồm
các loại lớp khác nhau để xử lý các tệp trong khi làm việc với chúng. Các lớp

74
Chương 2. Lập trình cơ bản

hiện diện trong fstream là ofstream, ifstream và fstream. Tệp chúng tôi đang
xem xét các ví dụ bên dưới bao gồm văn bản “A for A “.
a) Sử dụng “ ofstream ”
Trong đoạn mã dưới đây, chúng tôi đã thêm một chuỗi vào tệp “A for A
.txt” và in dữ liệu trong tệp sau khi nối thêm văn bản. ofstream đã tạo “ofstream
of” chỉ định tệp sẽ được mở ở chế độ ghi và “ ios::app “ trong phương thức open
chỉ định chế độ chắp thêm.
Ví dụ:

// C++ program to demonstrate appending of


// a string using ofstream
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main()
{
ofstream of;
fstream f;

// opening file using ofstream


of.open("A for A.txt", ios::app);
if (!of)
cout << "No such file found";
else {
of << " String";
cout << "Data appended successfully\n";
of.close();
string word;

// opening file using fstream


f.open("A for A.txt");
while (f >> word) {
cout << word << " ";

75
Chương 2. Lập trình cơ bản

}
f.close();
}
return 0;
}

b) Sử dụng “ fstream ”
Trong đoạn mã dưới đây, chúng tôi đã thêm một chuỗi vào tệp A for A.txt
” và in dữ liệu trong tệp sau khi nối thêm văn bản. Dòng fstream “fstream f” đã
tạo chỉ định tệp sẽ được mở ở chế độ đọc và ghi và “ ios::app “ trong phương
thức mở chỉ định chế độ chắp thêm.

Ví dụ:

// C++ program to demonstrate appending of


// a string using fstream
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream f;
f.open("A for A.txt", ios::app);
if (!f)
cout << "No such file found";
else {
f << " String_fstream";
cout << "Data appended successfully\n";
f.close();
string word;
f.open("A for A.txt");
while (f >> word) {
cout << word << " ";
}

76
Chương 2. Lập trình cơ bản

f.close();
}
return 0;
}
2.6 Bài tập Chương 2
Bài 1. Viết chương trình C++ để mở một file có tên là two.txt, ghi nội dung vào
file và sau đó chuyển đổi thành dạng chữ hoa.
Bài 2. Viết chương trình C++ đơn giản để đọc nội dung của file. Chương trình
của chúng ta cần kiểm tra xem file có tồn tại hay không và kiểm tra điều kiện
EOF (end of file).
Bài 3. Viết chương trình C++ để tìm tổng, hiệu, tích và thương của hai số nguyên
và in kết quả ra màn hình.
Bài 4. Viết chương trình C++ để nhập hai số nguyên từ bàn phím và sau đó in ra
màn hình tổng và trung bình của hai số chúng ta vừa nhập.
Bài 5. Viết chương trình C++ để nhập tuổi và in ra kết quả nếu tuổi học sinh đó không đủ
điều kiện vào học lớp 10.
Bài 6. Viết chương trình C++ để nhập một số nguyên bất kỳ từ bàn phím và in kết quả ra
màn hình để nói cho người dùng biết số đó là lớn hay nhỏ hơn 100.
Bài 7. Viết một chương trình C++ để nhắc người dùng nhập 3 số nguyên và tìm giá trị lớn
nhất.
Bài 8. Viết chương trình C++ để nhập nhập một số nguyên, tìm bội số của số đó với các số
từ 1-15 , sau đó in kết quả ra màn hình.
Bài 9. Viết chương trình C++ để nhập một câu, đếm số từ và ký tự trong câu đó, và in kết
quả ra màn hình.
Bài 10. Viết chương trình C++ để nhập một số nguyên và in kết quả ra màn hình dưới dạng
số đảo ngược (về thứ tự) của số nguyên vừa nhập đó.
Bài 11. Viết chương trình C++ để nhập một số nguyên x và tính giá trị của x - 1/3!x3 +
1/5!x5 - 1/7!x5 + 1/9!x9.
Bài 12. Viết chương trình C++ để tìm số nguyên tố bởi sử dụng vòng lặp FOR.
Bài 13. Viết chương trình C++ để in dãy Fibonacci bất kỳ
Bài 14. Viết chương trình C++ để tính giai thừa của một số nguyên dương bởi sử dụng vòng
lặp FOR trong C++.
Bài 15. Viết chương trình C++ để vẽ tam giác sao.

77
Chương 2. Lập trình cơ bản

Bài 16. Viết chương trình C++ để nhập n số dương. Chương trình sẽ kết thúc nếu một trong
các số đó là số âm.
Bài 17. Viết một chương trình C++ để xử lý tình huống khi người dùng lựa chọn một tùy
chọn nào thì chương trình của chúng ta sẽ in một dòng thông báo về tùy chọn đó.
Bài 18. Viết một chương trình C++ để tính số tiền lãi ngân hàng phải trả cho khách hàng.

78
Chương 3. Lớp và đối tượng

CHƯƠNG 3. LỚP VÀ ĐỐI TƯỢNG


3.1. Khái niệm hướng đối tượng
Lập trình hướng đối tượng – Như tên gọi nó sử dụng các đối tượng trong
lập trình. Lập trình hướng đối tượng nhằm mục đích triển khai các thực thể
trong thế giới thực như kế thừa, ẩn, đa hình, v.v. trong lập trình. Mục đích chính
của OOP là liên kết dữ liệu và các chức năng hoạt động trên chúng với nhau để
không phần nào khác của mã có thể truy cập dữ liệu này ngoại trừ chức năng đó.

Đặc điểm của lập trình hướng đối tượng:

Hình 3. 1 Các đặc điểm của lập trình hướng đối tượng

Lớp: Khối xây dựng của C++ dẫn đến lập trình hướng đối tượng là Lớp.
Nó là kiểu dữ liệu do người dùng định nghĩa, chứa các thành viên dữ liệu và

79
Chương 3. Lớp và đối tượng

hàm thành viên riêng, có thể được truy cập và sử dụng bằng cách tạo một thể
hiện của lớp đó. Một lớp giống như một bản thiết kế cho một đối tượng.
Ví dụ: Hãy xem xét loại Ô tô. Có thể có nhiều ô tô với tên gọi và nhãn
hiệu khác nhau nhưng tất cả chúng sẽ có chung một số đặc điểm như tất cả
chúng sẽ có 4 bánh, giới hạn tốc độ, quãng đường đi được, v.v. Vì vậy, ở đây, ô
tô là phân loại và bánh xe, giới hạn tốc độ, quãng đường đi được là tài sản của
họ.
Lớp là kiểu dữ liệu do người dùng định nghĩa có các thành viên dữ liệu và
các hàm thành viên. Các thành viên dữ liệu là các biến dữ liệu và các hàm thành
viên là các hàm được sử dụng để thao tác với các biến này và cùng với các thành
viên dữ liệu này và các hàm thành viên xác định các thuộc tính và hành vi của
các đối tượng trong Lớp.
Trong ví dụ trên về lớp Car, thành viên dữ liệu sẽ là giới hạn tốc độ, số
dặm, v.v. và các chức năng thành viên có thể áp dụng phanh, tăng tốc độ, v.v.
Chúng ta có thể nói rằng Lớp trong C++ là một bản in màu xanh đại diện cho
một nhóm đối tượng chia sẻ một số thuộc tính và hành vi chung.
Đối tượng: Đối tượng là một thực thể có thể nhận dạng với một số đặc
điểm và hành vi. Một đối tượng là một thể hiện của một lớp. Khi một lớp được
định nghĩa, không có bộ nhớ nào được cấp phát nhưng khi nó được khởi tạo (tức
là một đối tượng được tạo) thì bộ nhớ được cấp phát.
Ví dụ xây dựng một lớp có tên person và khởi tạo đối tượng P1

class person
{
char name[20];
int id;
public:
void getdetails(){}
};

int main()
{
person p1; // p1 is a object
}

80
Chương 3. Lớp và đối tượng

3.2. Lớp và đối tượng


Nhớ lại rằng một đối tượng có cùng mối quan hệ với một lớp mà một biến
có kiểu dữ liệu. Một đối tượng được cho là một thể hiện của một lớp, giống như
cách chiếc Chevrolet 1954 là một thể hiện của một chiếc xe. Trong
SMALLOBJ, lớp có tên là smallobj được định nghĩa trong phần đầu tiên của
chương trình. Sau đó, trong hàm main(), chúng ta định nghĩa hai đối tượng s1 và
s2 là các thể hiện của lớp đó. Mỗi đối tượng trong số hai đối tượng được cung
cấp một giá trị và mỗi đối tượng hiển thị giá trị của nó. Đây là đầu ra của
chương trình: Dữ liệu là 1066 ←đối tượng s1 hiển thị dữ liệu này Dữ liệu là
1776 ← đối tượng s2 hiển thị cái này Chúng ta sẽ bắt đầu bằng cách xem chi tiết
phần đầu tiên của chương trình định nghĩa của lớp smallobj. Sau đó, chúng ta sẽ
tập trung vào những gì main() thực hiện với các đối tượng của lớp này.
Lớp: Một lớp trong C++ là khối xây dựng dẫn đến lập trình Hướng đối
tượng. Nó là kiểu dữ liệu do người dùng định nghĩa, chứa các thành viên dữ liệu
và hàm thành viên riêng, có thể được truy cập và sử dụng bằng cách tạo một thể
hiện của lớp đó. Một lớp C++ giống như một bản thiết kế cho một đối tượng. Ví
dụ: Hãy xem xét loại Ô tô . Có thể có nhiều ô tô với các tên và nhãn hiệu khác
nhau nhưng tất cả chúng sẽ chia sẻ một số thuộc tính chung như tất cả chúng sẽ
có 4 bánh , Giới hạn tốc độ , Phạm vi quãng đường, v.v. Vì vậy, ở đây, Ô tô là
lớp và bánh xe, giới hạn tốc độ, quãng đường là tài sản của họ.
 Lớp là kiểu dữ liệu do người dùng định nghĩa có các thành viên dữ liệu và
các hàm thành viên.
 Các thành viên dữ liệu là các biến dữ liệu và các hàm thành viên là các
hàm được sử dụng để thao tác với các biến này và cùng với các thành viên
dữ liệu này và các hàm thành viên xác định các thuộc tính và hành vi của
các đối tượng trong một Lớp.
 Trong ví dụ trên về lớp Car , thành viên dữ liệu sẽ là giới hạn tốc độ , số
dặm , v.v. và các chức năng thành viên có thể là áp dụng phanh , tăng tốc
độ , v.v.

Một đối tượng là một thể hiện của một lớp. Khi một lớp được định nghĩa,
không có bộ nhớ nào được cấp phát nhưng khi nó được khởi tạo (tức là một đối
tượng được tạo) thì bộ nhớ được cấp phát.

81
Chương 3. Lớp và đối tượng

Một lớp được định nghĩa trong C++ bằng cách sử dụng từ khóa lớp theo
sau là tên của lớp. Phần thân của lớp được định nghĩa bên trong dấu ngoặc nhọn
và được kết thúc bằng dấu chấm phẩy ở cuối

Hình 3. 2 Cú pháp khai báo 1 lớp

Khai báo đối tượng: Khi một lớp được định nghĩa, chỉ đặc tả cho đối
tượng được xác định; không có bộ nhớ hoặc lưu trữ được phân bổ. Để sử dụng
các hàm truy cập và dữ liệu được định nghĩa trong lớp, chúng ta cần tạo các đối
tượng.
Cú pháp: ClassName ObjectName;
Mức độ truy cập
Thông thường, mức độ truy cập (access-modifiers) của một lớp là public.
Ngoài ra các thành phần của lớp cũng có mức độ truy cập riêng. Mức độ truy
cập của một thành phần cho biết loại phương thức nào được phép truy cập đến
nó, hay nói cách khác nó mô tả phạm vi mà thành phần đó được nhìn thấy, ví
dụ: Chương trình C++ để chứng minh việc truy cập các thành viên dữ liệu

// C++ program to demonstrate accessing of data members


#include <bits/stdc++.h>
using namespace std;
class Geeks {
// Access specifier
public:

82
Chương 3. Lớp và đối tượng

// Data Members
string geekname;
// Member Functions()
void printname() { cout << "Geekname is:" << geekname; }
};
int main()
{
// Declare an object of class geeks
Geeks obj1;
// accessing data member
obj1.geekname = "Abhi";
// accessing member function
obj1.printname();
return 0;
}

Phạm vi truy cập là cách mà người lập trình quy định về quyền được
truy xuất đến các thàh phần của lớp.Trong C++ có 3 loại phạm vi chính
là: private, protected, public

Phạm Vi Truy Cập Ý Nghĩa

Không hạn chế. Thành phần có thuộc


tính này có thể được truy cập ở bất kì
Public
vị trí nào.

Thành phần có thuộc tính này sẽ chỉ


được truy cập từ bên trong lớp. Bên
Private
ngoài lớp hay trong lớp dẫn xuất sẽ
không thể truy cập được.

Mở rộng hơn private một chút. Thành


phần có thuộc tính này sẽ có thể truy
Protected
cập ở trong nội bộ lớp và trong lớp dẫn
xuất

83
Chương 3. Lớp và đối tượng

( lớp dẫn xuất sẽ được trình bày trong


bài Tính Kế Thừa )

3.3 Mảng đối tượng C++


Mảng các đối tượng cũng có thể được khai báo và sử dụng như mảng của
các biến có kiểu thông thường.
 Khai báo mảng tĩnh các đối tượng
Mảng các đối tượng được khai báo theo cú pháp:
<Tên lớp> <Tên biến mảng>[<Số lượng đối tượng>];
Ví dụ: Car cars[10];
Là khai báo một mảng có 10 đối tượng có cùng kiểu lớp Car.
Lưu ý:
 Có thể khai báo mảng tĩnh các đối tượng mà chưa cần khai báo độ dài
mảng, cách này thường dùng khi chưa biết chính xác độ dài mảng: Car
cars[];
 Muốn khai báo được mảng tĩnh các đối tượng, lớp tương ứng phải có hàm
khởi tạo không có tham số. Vì khi khai báo mảng, tương đương với khai
báo một dãy các đối tượng với hàm khởi tạo không có tham số.

 Khai báo mảng động với con trỏ


Một mảng các đối tượng cũng có thể được khai báo và cấp phát động
thông qua con trỏ đối tượng như sau:
<Tên lớp> *<Tên biến mảng động> = new <Tên lớp>[<Độ dài mảng>];
Ví dụ: Car *cars = new Car[10];
Sau khi được sử dụng, mảng động các đối tượng cũng cần phải giải phóng bộ
nhớ:
delete [] <Tên biến mảng động>;
Ví dụ:
Car *cars = new Car[10];// Khai báo và cấp phát động
… // Sử dụng biến mảng động
delete [] cars; // Giải phóng bộ nhớ của mảng động

 Sử dụng mảng đối tượng

84
Chương 3. Lớp và đối tượng

Khi truy nhập vào các thành phần của một đối tượng có chỉ số xác định
trong mảng đã khai báo, ta có thể sử dụng cú pháp:
<Tên biến mảng>[<Chỉ số đối tượng>].<Tên thành phần>([<Các đối số>]);
Ví dụ: Car cars[10]; cars[5].show(); sẽ thực hiện phương thức show() của đối
tượng có chỉ số thứ 5 (tính từ chỉ số 0) trong mảng cars.
Chương trình sau sẽ cài đặt một chương trình, trong đó nhập vào độ dài
mảng, sau đó yêu cầu người dùng nhập thông tin về mảng các xe. Cuối cùng,
chương trình sẽ tìm kiếm và hiển thị thông tin về chiếc xe có giá đắt nhất trong
mảng.

#include<iostream>
#include<string>

using namespace std;


/* Định nghĩa lớp */
class Car {
private:
int speed; // Tốc độ
string mark; // Nhãn hiệu
float price; // Giá xe
public:
void setSpeed(int); // Gán tốc độ cho xe
int getSpeed(); // Lấy tốc độ xe
void setMark(string); // Gán nhãn cho xe
string getMark(); // Lấy nhãn xe
void setPrice(float); // Gán giá cho xe
float getPrice(); // Lấy giá xe
void init(int, string, float);// Khởi tạo thông tin về xe
void show(); // Hiển thị thông tin về xe
};

/* Khai báo phương thức bên ngoài lớp */


void Car::setSpeed(int speedIn) { // Gán tốc độ cho xe
speed = speedIn;

85
Chương 3. Lớp và đối tượng

int Car::getSpeed() { // Lấy tốc độ xe


return speed;
}

void Car::setMark(string markIn) { // Gán nhãn cho xe


mark = markIn;
}

string Car::getMark() { // Lấy nhãn xe


return mark;
}
void Car::setPrice(float priceIn) { // Gán giá cho xe
price = priceIn;
}
float Car::getPrice() { // Lấy giá xe
return price;
}
void Car::init(int speedIn, string markIn, float priceIn) {
speed = speedIn;
mark = markIn;
price = priceIn;
return;
}

void Car::show() { // Phương thức hiển thị xe


cout << "This is a " << mark
<< " having a speed of " << speed << "km/h and its price is $" << price <<
endl;
return;
}

// Hàm main, chuong trình chính

86
Chương 3. Lớp và đối tượng

int main() {

int length; // Chiều dài mảng


float maxPrice = 0; // Giá đắt nhất
int index = 0; // Chỉ số của xe đắt nhất
Car *cars; // Khai báo mảng đối tượng
// Nhập số lượng xe, tức là chiều dài mảng
cout << "So luong xe: ";
cin >> length;
// Cấp phát bộ nhớ động cho mảng
cars = new Car[length];
// Khởi tạo các đối tượng trong mảng
for(int i=0;i<length; i++){
int speed; // (Biến tạm) tốc độ
string mark; // (Biến tạm) nhãn hiệu
float price; // (Biến tạm) giá xe
cout << "Xe thu " << i << ": " <<endl;
cout << "Toc do (km/h): ";
cin >> speed;
cars[i].setSpeed(speed); // Nhập tốc độ
cout << "Nhan hieu : ";
cin >> mark;
cars[i].setMark(mark); // Nhập nhãn xe
cout << "Gia ($): ";
cin >> price;
cars[i].setPrice(price); // Nhập giá xe
if (maxPrice < price) {
maxPrice = price;
index = i;
}
}

// Tìm xe đắt nhất


for(int i=0; i<length; i++)

87
Chương 3. Lớp và đối tượng

if(i == index){
cars[i].show(); // Giới thiệu xe đắt nhất
break;
}
// Giải phóng bộ nhớ của mảng
delete [] cars;
system("pause");
return 0;
}

3.4 Hàm chúng ta, lớp chúng ta và các thư viện lớp trong C++
a) Hàm chúng ta
Hàm chúng ta của một lớp không phải là hàm thành viên nên nó không
phụ thuộc vào lớp và có thể định nghĩa ở trong hoặc ngoài lớp. Hàm chúng ta có
thể truy cập trực tiếp các thành viên private và một lớp có thể có nhiều hàm
chúng ta. Cú pháp khai báo hàm chúng ta đơn giản là thêm từ khóa friend trước
hàm bình thường ta vẫn sử dụng. Ví dụ hàm chúng ta:

#include <bits/stdc++.h>
using namespace std;

class TamGiac{
private:
int a,b,c;
public:
TamGiac()
{
a = 1;
b = 1;
c = 1;
}
TamGiac(int a, int b, int c): a(a), b(b), c(c) {};

friend bool kiemTraTG(TamGiac tg);

88
Chương 3. Lớp và đối tượng

friend ostream& operator << (ostream &os ,TamGiac &tg)


{
os<<"a = "<<tg.a<<" b = "<<tg.b<<" c = "<<tg.c<<endl;
return os;
}
};
bool kiemTraTG(TamGiac tg)
{
if(tg.a>0 && tg.b>0 && tg.c>0 && (tg.a+tg.b>tg.c) &&
(tg.a+tg.c>tg.b) && (tg.b+tg.c>tg.a))
return true;
return false;
}

int main()
{
TamGiac t(3,4,5);
cout<<kiemTraTG(t);
cout<<t;
return 0;
}

Ví dụ trên mình minh họa lớp tam giác có các thuộc tính là a,b,c và các
phương thức là hàm tạo mạc định, hàm tạo 3 tham số, hàm chúng ta nạp chồng
toán tử xuất, hàm chúng ta kiểm tra tam giác. Và các hàm chúng ta mình đã
minh họa cho các chúng ta thấy rằng hàm chúng ta hoàn toàn có thể định nghĩa
được ở trong class như các phương thức của lớp và khi định nghĩa ở ngoài class
nó hoàn toàn được định nghĩa như các hàm thông thường. Các chúng ta nên
dùng hàm chúng ta để nạp chồng toán tử vì:
 Toán tử nạp chồng là hàm bình thường, không phải là hàm thành viên
(phương thức) nên chúng truy cập dữ liệu thông qua hàm truy cập và hàm
biến đổi nên nó rất kém hiệu quả (do mất phụ phí lời gọi)

89
Chương 3. Lớp và đối tượng

 Hàm chúng ta có thể truy cập trực tiếp dữ liệu từ biến private nên nó
không mất phụ phí -> Cải thiện hiệu quả thực hiện, tránh gọi hàm thành
viên truy cập || biến đổi.
h) Lớp chúng ta (Friend Class)
Lớp chúng ta cũng giống như hàm chúng ta là nó có thể truy cập các biến
thành viên private của lớp kia. Ta có class A là chúng ta của class B nên tất cả
hàm thành viên của lớp B là chúng ta của lớp A và chiều ngược lại thì không
đúng. Cú pháp: ta thêm từ khóa friend trước class. Ví dụ lớp chúng ta:

#include <bits/stdc++.h>
using namespace std;

class B{
private:
int b;
public:
B()
{
b b = 10;
}
friend class A;
};

class A{
public:
void print(B arg)
{
cout<<arg.b;
}
};
int main()
{
B b;
A a;

90
Chương 3. Lớp và đối tượng

a.print(b);
return 0;
}

Ví dụ trên ta có class A là chúng ta của class B nên mặc dù b là biến trong


phạm vi private nhưng ta vẫn truy cập trực tiếp nó được và kết quả sau khi chạy
chương trình ra 10.
i) Hàm thành viên trong lớp
Có 2 cách để định nghĩa một hàm thành viên:
 Định nghĩa bên trong lớp
 Định nghĩa lớp bên ngoài
Để định nghĩa một hàm thành viên bên ngoài định nghĩa lớp, chúng ta phải sử
dụng toán tử phân giải phạm vi :: cùng với tên lớp và tên hàm. Ví dụ minh họa
chương trình C++ để chứng minh chức năng khai báo bên ngoài lớp như sau:

// C++ program to demonstrate function


// declaration outside class

#include <bits/stdc++.h>
using namespace std;
class Geeks
{
public:
string geekname;
int id;

// printname is not defined inside class definition


void printname();

// printid is defined inside class definition


void printid()
{
cout <<"Geek id is: "<<id;
}

91
Chương 3. Lớp và đối tượng

};

// Definition of printname using scope resolution operator ::


void Geeks::printname()
{
cout <<"Geekname is: "<<geekname;
}
int main() {

Geeks obj1;
obj1.geekname = "xyz";
obj1.id=15;

// call printname()
obj1.printname();
cout << endl;

// call printid()
obj1.printid();
return 0;
}

Lưu ý rằng tất cả các hàm thành viên được xác định bên trong định nghĩa
lớp theo mặc định là nội tuyến , nhưng chúng ta cũng có thể đặt bất kỳ hàm
không thuộc lớp nào thành nội tuyến bằng cách sử dụng từ khóa nội tuyến với
chúng. Các hàm nội tuyến là các hàm thực tế, được sao chép ở mọi nơi trong
quá trình biên dịch, giống như macro tiền xử lý, do đó, chi phí gọi hàm được
giảm thiểu. Lưu ý: Khai báo chức năng kết chúng ta là một cách để cấp quyền
truy cập riêng tư cho chức năng không phải thành viên.
j) Hàm khởi tạo
Hàm khởi tạo là các thành viên lớp đặc biệt được trình biên dịch gọi mỗi
khi một đối tượng của lớp đó được khởi tạo. Constructor có cùng tên với lớp và
có thể được định nghĩa bên trong hoặc bên ngoài định nghĩa lớp. Có 3 loại
constructor:

92
Chương 3. Lớp và đối tượng

 Hàm khởi tạo mặc định


 Các hàm tạo được tham số hóa
 Sao chép hàm tạo
Constructor trong C++ là một phương thức đặc biệt được gọi tự động tại
thời điểm tạo đối tượng. Nó được sử dụng để khởi tạo các thành viên dữ liệu của
các đối tượng mới nói chung. Hàm tạo trong C++ có cùng tên với lớp hoặc cấu
trúc. Trình xây dựng được gọi tại thời điểm tạo đối tượng. Nó xây dựng các giá
trị, tức là cung cấp dữ liệu cho đối tượng, đó là lý do tại sao nó được gọi là hàm
tạo. Trình xây dựng không có giá trị trả về, do đó chúng không có kiểu trả về.
Nguyên mẫu của Constructor như sau:
<tên lớp> (danh sách tham số);
Các hàm tạo có thể được định nghĩa bên trong hoặc bên ngoài khai báo
lớp:
 Cú pháp để xác định hàm tạo trong lớp:
<tên lớp> (danh sách tham số) { // định nghĩa hàm tạo }
 Cú pháp để xác định hàm tạo bên ngoài lớp:
<tên lớp>: :<tên lớp> (danh sách tham số){ // định nghĩa hàm tạo}
Ví dụ định nghĩa hàm tạo bên trong lớp:

// defining the constructor within the class

#include <iostream>
using namespace std;

class student {
int rno;
char name[10];
double fee;

public:
student()
{
cout << "Enter the RollNo:";
cin >> rno;

93
Chương 3. Lớp và đối tượng

cout << "Enter the Name:";


cin >> name;
cout << "Enter the Fee:";
cin >> fee;
}

void display()
{
cout << endl << rno << "\t" << name << "\t" << fee;
}
};

int main()
{
student s; // constructor gets called automatically when
// we create the object of the class
s.display();

return 0;
}

Ví dụ định nghĩa hàm tạo bên ngoài lớp:

// defining the constructor outside the class

#include <iostream>
using namespace std;
class student {
int rno;
char name[50];
double fee;

public:
student();

94
Chương 3. Lớp và đối tượng

void display();
};

student::student()
{
cout << "Enter the RollNo:";
cin >> rno;

cout << "Enter the Name:";


cin >> name;

cout << "Enter the Fee:";


cin >> fee;
}

void student::display()
{
cout << endl << rno << "\t" << name << "\t" << fee;
}

int main()
{
student s;
s.display();

return 0;
}

Hàm tạo khác với hàm thông thường ở các điểm sau:
 Hàm khởi tạo có cùng tên với chính lớp đó
 Hàm khởi tạo mặc định không có đối số đầu vào, tuy nhiên, Trình xây
dựng sao chép và tham số hóa có đối số đầu vào
 Constructor không có kiểu trả về
 Hàm khởi tạo được gọi tự động khi một đối tượng được tạo.

95
Chương 3. Lớp và đối tượng

 Nó phải được đặt trong phần công cộng của lớp học.
 Nếu chúng ta không chỉ định một hàm tạo, trình biên dịch C++ sẽ tạo một
hàm tạo mặc định cho đối tượng (không có tham số và có phần thân
trống).

Hình 3. 3 Hàm khởi tạo trong C++

Đặc điểm của hàm tạo:


 Tên của hàm tạo giống như tên lớp của nó.
 Các hàm tạo hầu hết được khai báo trong phần chung của lớp mặc dù nó
có thể được khai báo trong phần riêng của lớp.
 Constructor không trả về giá trị; do đó chúng không có kiểu trả về.
 Hàm tạo được gọi tự động khi chúng ta tạo đối tượng của lớp.
 Constructor có thể bị quá tải.
 Trình xây dựng không thể được khai báo là ảo.
 Trình xây dựng không thể được kế thừa.
 Địa chỉ của Constructor không thể được giới thiệu.
 Trình xây dựng thực hiện các cuộc gọi ngầm đến các toán tử mới và xóa
trong quá trình cấp phát bộ nhớ.

Các loại Constructor:


 Hàm tạo mặc định: Hàm tạo mặc định là hàm tạo không nhận bất kỳ đối
số nào. Nó không có tham số. Nó còn được gọi là hàm tạo không đối số.

Ví dụ hàm khởi tạo mặc định:

// Cpp program to illustrate the


// concept of Constructors

96
Chương 3. Lớp và đối tượng

#include <iostream>
using namespace std;

class construct {
public:
int a, b;

// Default Constructor
construct()
{
a = 10;
b = 20;
}
};

int main()
{
// Default constructor called automatically
// when the object is created
construct c;
cout << "a: " << c.a << endl << "b: " << c.b;
return 1;
}

 Các hàm tạo được tham số hóa: Có thể truyền đối số cho các hàm tạo.
Thông thường, các đối số này giúp khởi tạo một đối tượng khi nó được
tạo. Để tạo một hàm tạo được tham số hóa, chỉ cần thêm các tham số vào
nó theo cách chúng ta làm với bất kỳ hàm nào khác. Khi chúng ta xác
định phần thân của hàm tạo, hãy sử dụng các tham số để khởi tạo đối
tượng.

Ví dụ hàm khởi tạo có tham số:

// CPP program to illustrate

97
Chương 3. Lớp và đối tượng

// parameterized constructors
#include <iostream>
using namespace std;

class Point {
private:
int x, y;

public:
// Parameterized Constructor
Point(int x1, int y1)
{
x = x1;
y = y1;
}

int getX() { return x; }


int getY() { return y; }
};

int main()
{
// Constructor called
Point p1(10, 15);

// Access values assigned by constructor


cout << "p1.x = " << p1.getX()
<< ", p1.y = " << p1.getY();

return 0;
}

Khi một đối tượng được khai báo trong hàm tạo được tham số hóa, các giá
trị ban đầu phải được truyền dưới dạng đối số cho hàm tạo. Cách khai báo đối

98
Chương 3. Lớp và đối tượng

tượng thông thường có thể không hoạt động. Các hàm tạo có thể được gọi rõ
ràng hoặc ngầm định.
Công dụng của hàm tạo được tham số hóa:
 Nó được sử dụng để khởi tạo các thành phần dữ liệu khác nhau của
các đối tượng khác nhau với các giá trị khác nhau khi chúng được tạo.
 Nó được sử dụng để quá tải các hàm tạo.
Sao chép hàm khởi tạo:
Trình tạo bản sao là một hàm thành viên khởi tạo một đối tượng bằng
cách sử dụng một đối tượng khác trong cùng một lớp. Một bài viết chi tiết về
Copy Constructor. Bất cứ khi nào chúng ta định nghĩa một hoặc nhiều hàm tạo
không mặc định (có tham số) cho một lớp, hàm tạo mặc định (không có tham số)
cũng nên được định nghĩa rõ ràng vì trình biên dịch sẽ không cung cấp hàm tạo
mặc định trong trường hợp này. Tuy nhiên, không cần thiết nhưng nó được coi
là cách tốt nhất để luôn xác định một hàm tạo mặc định. Hàm tạo bản sao lấy
một tham chiếu đến một đối tượng của cùng một lớp làm đối số. Cú pháp sao
chép hàm khởi tạo:

Sample(Sample &t)
{
id=t.id;
}

Ví dụ sao chép hàm khởi tạo:

// Illustration
#include <iostream>
using namespace std;

class point {
private:
double x, y;

public:

99
Chương 3. Lớp và đối tượng

// Non-default Constructor &


// default Constructor
point(double px, double py) { x = px, y = py; }
};

int main(void)
{

// Define an array of size


// 10 & of type point
// This line will cause error
point a[10];

// Remove above line and program


// will compile without error
point b = point(5, 6);
}

k) Hàm hủy:
Hàm hủy cũng là một hàm thành viên đặc biệt với tư cách là hàm tạo.
Destructor hủy các đối tượng lớp được tạo bởi hàm tạo. Destructor có cùng tên
với tên lớp của chúng trước ký hiệu dấu ngã (~). Không thể định nghĩa nhiều
hơn một hàm hủy. Hàm hủy chỉ là một cách để hủy đối tượng được tạo bởi hàm
tạo. Do đó hàm hủy không thể bị quá tải. Hàm hủy không yêu cầu bất kỳ đối số
nào cũng như không trả về bất kỳ giá trị nào. Nó được gọi tự động khi đối tượng
vượt quá phạm vi. Trình hủy giải phóng không gian bộ nhớ bị chiếm bởi các đối
tượng được tạo bởi hàm tạo. Trong hàm hủy, các đối tượng bị hủy ngược lại với
quá trình tạo đối tượng.
 Cú pháp xác định hàm hủy trong lớp:
~ <tên lớp>(){}

 Cú pháp xác định hàm hủy bên ngoài lớp

100
Chương 3. Lớp và đối tượng

<tên lớp>: : ~ <tên lớp>(){}


Ví dụ hàm hủy trong lập trình hướng đối tượng:

#include <iostream>
using namespace std;

class Test {
public:
Test() { cout << "\n Constructor executed"; }

~Test() { cout << "\n Destructor executed"; }


};
main()
{
Test t;

return 0;
}

#include <iostream>
using namespace std;
class Test {
public:
Test() { cout << "\n Constructor executed"; }

~Test() { cout << "\n Destructor executed"; }


};

main()
{
Test t, t1, t2, t3;
return 0;
}

101
Chương 3. Lớp và đối tượng

Đặc điểm của hàm hủy:


 Hàm hủy được trình biên dịch gọi tự động khi hàm tạo tương ứng của nó
vượt quá phạm vi và giải phóng không gian bộ nhớ không còn được
chương trình yêu cầu.
 Hàm hủy không yêu cầu bất kỳ đối số nào cũng như không trả về bất kỳ
giá trị nào do đó nó không thể bị quá tải.
 Hàm hủy không thể được khai báo là tĩnh và const;
 Destructor nên được khai báo trong phần public của chương trình.
 Hàm hủy được gọi theo thứ tự ngược lại với lệnh gọi hàm tạo của nó.

3.5. Các đặc tính của hướng đối tượng (Kế thừa, đa hình, đóng gói, trừu
tượng)
3.5.1 Tính kế thừa
Khả năng của một lớp lấy được các thuộc tính và đặc điểm từ một lớp
khác được gọi là Kế thừa . Kế thừa là một trong những tính năng quan trọng
nhất của Lập trình hướng đối tượng. Kế thừa là một tính năng hoặc một quá
trình trong đó, các lớp mới được tạo từ các lớp hiện có. Lớp mới được tạo được
gọi là “lớp dẫn xuất” hoặc “lớp con” và lớp hiện có được gọi là “lớp cơ sở” hoặc
“lớp cha”. Bây giờ lớp dẫn xuất được cho là kế thừa từ lớp cơ sở.
Khi chúng ta nói lớp dẫn xuất kế thừa lớp cơ sở, điều đó có nghĩa là lớp
dẫn xuất kế thừa tất cả các thuộc tính của lớp cơ sở, không làm thay đổi các
thuộc tính của lớp cơ sở và có thể bổ sung thêm các tính năng mới cho lớp cơ
sở. Những tính năng mới này trong lớp dẫn xuất sẽ không ảnh hưởng đến lớp cơ
sở. Lớp dẫn xuất là lớp chuyên biệt cho lớp cơ sở.
 Lớp con: Lớp kế thừa các thuộc tính từ một lớp khác được gọi là lớp con
hoặc Lớp dẫn xuất.
 Lớp siêu cấp: Lớp có các thuộc tính được kế thừa bởi một lớp con được
gọi là Lớp cơ sở hoặc lớp siêu cấp.
Bài toán đặt ra:
 Tại sao và khi nào nên sử dụng thừa kế?
 Phương thức kế thừa
 Các loại thừa kế

102
Chương 3. Lớp và đối tượng

Hãy xem xét một nhóm các phương tiện. Chúng ta cần tạo các lớp cho
Bus, Car và Truck. Các phương thức fuelAmount(), capacity(), applyBrakes() sẽ
giống nhau cho cả ba lớp. Nếu chúng ta tạo các lớp này để tránh kế thừa thì
chúng ta phải viết tất cả các hàm này trong mỗi lớp trong ba lớp như hình bên
dưới:

Hình 3. 4 Minh họa kế thừa

Chúng ta có thể thấy rõ rằng quy trình trên dẫn đến việc lặp lại cùng một
đoạn mã 3 lần. Điều này làm tăng khả năng xảy ra lỗi và dư thừa dữ liệu. Để
tránh loại tình huống này, kế thừa được sử dụng. Nếu chúng ta tạo một lớp Xe
và viết ba chức năng này trong đó và kế thừa các lớp còn lại từ lớp xe, thì chúng
ta có thể tránh trùng lặp dữ liệu và tăng khả năng sử dụng lại. Nhìn vào sơ đồ
bên dưới, trong đó ba lớp được kế thừa từ lớp phương tiện:

Hình 3. 5 Lớp kế thừa từ lớp phương tiện

103
Chương 3. Lớp và đối tượng

Sử dụng tính kế thừa, chúng ta chỉ phải viết các hàm một lần thay vì ba
lần như chúng ta đã kế thừa phần còn lại của ba lớp từ lớp cơ sở (Vehicle).
 Thực hiện kế thừa trong C++ : Để tạo một lớp con được kế thừa từ lớp cơ
sở, chúng ta phải tuân theo cú pháp.
 Các lớp dẫn xuất: Một lớp dẫn xuất được định nghĩa là lớp dẫn xuất từ lớp
cơ sở.
Cú pháp :

class <derived_class_name> : <access-specifier> <base_class_name>


{
//body
}

Trong đó:
 Class: từ khóa để tạo một lớp mới dẫn
 derived_class_name: tên của lớp mới, lớp này sẽ kế thừa trình xác định
truy cập lớp cơ sở có thể là riêng tư, công khai hoặc được bảo vệ. Nếu
không được chỉ định được lấy làm
 base_class_name: tên của lớp cơ sở
 Lưu ý : Một lớp dẫn xuất không kế thừa quyền truy cập vào các thành
viên dữ liệu riêng tư. Tuy nhiên, nó kế thừa một đối tượng cha đầy đủ,
chứa bất kỳ thành viên riêng nào mà lớp đó khai báo.

Ví dụ:
 lớp ABC: XYZ riêng tư // dẫn xuất riêng tư { }
 lớp ABC: công khai XYZ // dẫn xuất công khai{ }
 lớp ABC: XYZ được bảo vệ // dẫn xuất được bảo vệ{ }
 lớp ABC: XYZ // riêng tư dẫn xuất theo mặc định{ }

Ghi chú:
 Khi một lớp cơ sở được kế thừa private bởi lớp dẫn xuất, các thành viên
public của lớp cơ sở trở thành các thành viên private của lớp dẫn xuất và
do đó, các thành viên public của lớp cơ sở chỉ có thể được truy cập bởi

104
Chương 3. Lớp và đối tượng

các hàm thành viên của lớp dẫn xuất. Chúng không thể truy cập được đối
với các đối tượng của lớp dẫn xuất.
 Mặt khác, khi lớp cơ sở được kế thừa public bởi lớp dẫn xuất, các thành
viên public của lớp cơ sở cũng trở thành các thành viên public của lớp dẫn
xuất. Do đó, các thành viên public của lớp cơ sở có thể truy cập được bởi
các đối tượng của lớp dẫn xuất cũng như bởi các hàm thành viên của lớp
dẫn xuất.

Ví dụ: Định nghĩa hàm thành viên không có đối số trong lớp

// Example: define member function without argument within the class

#include<iostream>
using namespace std;

class Person
{
int id;
char name[100];

public:
void set_p()
{
cout<<"Enter the Id:";
cin>>id;
fflush(stdin);
cout<<"Enter the Name:";
cin.get(name,100);
}

void display_p()
{
cout<<endl<<id<<"\t"<<name<<"\t";
}

105
Chương 3. Lớp và đối tượng

};

class Student: private Person


{
char course[50];
int fee;

public:
void set_s()
{
set_p();
cout<<"Enter the Course Name:";
fflush(stdin);
cin.getline(course,50);
cout<<"Enter the Course Fee:";
cin>>fee;
}

void display_s()
{
display_p();
cout<<course<<"\t"<<fee<<endl;
}
};

main()
{
Student s;
s.set_s();
s.display_s();
return 0;
}

106
Chương 3. Lớp và đối tượng

Ví dụ: định nghĩa hàm thành viên không có đối số ngoài lớp

// Example: define member function without argument outside the class

#include<iostream>
using namespace std;

class Person
{
int id;
char name[100];

public:
void set_p();
void display_p();
};

void Person::set_p()
{
cout<<"Enter the Id:";
cin>>id;
fflush(stdin);
cout<<"Enter the Name:";
cin.get(name,100);
}

void Person::display_p()
{
cout<<endl<<id<<"\t"<<name;
}

class Student: private Person


{
char course[50];

107
Chương 3. Lớp và đối tượng

int fee;

public:
void set_s();
void display_s();
};

void Student::set_s()
{
set_p();
cout<<"Enter the Course Name:";
fflush(stdin);
cin.getline(course,50);
cout<<"Enter the Course Fee:";
cin>>fee;
}

void Student::display_s()
{
display_p();
cout<<"\t"<<course<<"\t"<<fee;
}

main()
{
Student s;
s.set_s();
s.display_s();
return 0;
}

Ví dụ: định nghĩa hàm thành viên với đối số bên ngoài lớp

// Example: define member function with argument outside the class

108
Chương 3. Lớp và đối tượng

#include<iostream>
#include<string.h>
using namespace std;

class Person
{
int id;
char name[100];

public:
void set_p(int,char[]);
void display_p();
};

void Person::set_p(int id,char n[])


{
this->id=id;
strcpy(this->name,n);
}

void Person::display_p()
{
cout<<endl<<id<<"\t"<<name;
}

class Student: private Person


{
char course[50];
int fee;
public:
void set_s(int,char[],char[],int);
void display_s();
};

109
Chương 3. Lớp và đối tượng

void Student::set_s(int id,char n[],char c[],int f)


{
set_p(id,n);
strcpy(course,c);
fee=f;
}

void Student::display_s()
{
display_p();
cout<<"t"<<course<<"\t"<<fee;
}

main()
{
Student s;
s.set_s(1001,"Ram","B.Tech",2000);
s.display_s();
return 0;
}

Trong chương trình trên, lớp 'Con' được kế thừa công khai từ lớp 'Cha', vì
vậy các thành viên dữ liệu công khai của lớp 'Cha' cũng sẽ được lớp 'Con' kế
thừa.
Phương thức kế thừa: Có 3 phương thức thừa kế.
 Public: Nếu chúng ta lấy một lớp con từ một lớp cơ sở công khai.
Sau đó, thành viên public của lớp cơ sở sẽ trở thành public trong
lớp dẫn xuất và thành viên protected của lớp cơ sở sẽ trở thành
protected trong lớp dẫn xuất.
 Protected: Nếu chúng ta lấy được một lớp con từ lớp cơ sở được
bảo vệ. Sau đó, cả thành viên public và protected của lớp cơ sở sẽ
trở thành protected trong lớp dẫn xuất.

110
Chương 3. Lớp và đối tượng

 Private: Nếu chúng ta lấy được một lớp con từ một lớp cơ sở riêng
tư. Sau đó, cả thành viên công khai và thành viên được bảo vệ của
lớp cơ sở sẽ trở thành riêng tư trong lớp dẫn xuất.

Lưu ý: Các thành viên private trong lớp cơ sở không thể truy cập trực tiếp
trong lớp dẫn xuất, trong khi các thành viên protected có thể được truy cập trực
tiếp. Ví dụ: Các lớp B, C và D đều chứa các biến x, y và z trong ví dụ bên dưới.
Nó chỉ là một câu hỏi về quyền truy cập.

Ví dụ triển khai C++ để chỉ ra rằng một lớp dẫn xuất, không kế thừa
quyền truy cập vào các thành viên dữ liệu riêng tư. Tuy nhiên, nó kế thừa một
đối tượng cha đầy đủ:

// C++ Implementation to show that a derived class


// doesn’t inherit access to private data members.
// However, it does inherit a full parent object.
class A {
public:
int x;

protected:
int y;

private:
int z;
};

class B : public A {
// x is public
// y is protected
// z is not accessible from B
};

class C : protected A {

111
Chương 3. Lớp và đối tượng

// x is protected
// y is protected
// z is not accessible from C
};

class D : private A // 'private' is default for classes


{
// x is private
// y is private
// z is not accessible from D
};

Bảng dưới đây tóm tắt ba chế độ trên và hiển thị thông số truy cập của các
thành viên của lớp cơ sở trong lớp con khi được dẫn xuất ở chế độ công khai,
được bảo vệ và riêng tư:

Hình 3. 6 ba chế độ trên và hiển thị thông số truy cập của các thành viên của
lớp cơ sở trong lớp con

Các kiểu kế thừa trong C++


 Kế thừa đơn: Trong kế thừa đơn, một lớp chỉ được phép kế thừa từ một
lớp duy nhất. tức là một lớp con chỉ được kế thừa bởi một lớp cơ sở.

112
Chương 3. Lớp và đối tượng

Hình 3. 7 đơn kế thừa

Cú pháp đơn kế thừa:

class subclass_name : access_mode base_class


{
// body of subclass
};

OR

class A
{
... .. ...
};

class B: public A
{
... .. ...
};

Ví dụ về đơn kế thừa:

// C++ program to explain


// Single inheritance
#include<iostream>
using namespace std;

113
Chương 3. Lớp và đối tượng

// base class
class Vehicle {
public:
Vehicle()
{
cout << "This is a Vehicle\n";
}
};

// sub class derived from a single base classes


class Car : public Vehicle {

};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes
Car obj;
return 0;
}

 Đa kế thừa: Đa kế thừa là một tính năng của C++ trong đó một lớp có thể
kế thừa từ nhiều lớp. tức là một lớp con được kế thừa từ nhiều hơn một
lớp cơ sở .

114
Chương 3. Lớp và đối tượng

Hình 3. 8 Đa kế thừa

Cú pháp:

class subclass_name : access_mode base_class1, access_mode base_class2, ....


{
// body of subclass
};

class B
{
... .. ...
};
class C
{
... .. ...
};
class A: public B, public C
{
... ... ...
};

Tại đây, số lớp cơ sở sẽ được phân tách bằng dấu phẩy (', ') và chế độ truy
cập cho mỗi lớp cơ sở phải được chỉ định.
Ví dụ về đa kế thừa:

// C++ program to explain


// multiple inheritance
#include <iostream>
using namespace std;

// first base class


class Vehicle {

115
Chương 3. Lớp và đối tượng

public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// second base class


class FourWheeler {
public:
FourWheeler()
{
cout << "This is a 4 wheeler Vehicle\n";
}
};

// sub class derived from two base classes


class Car : public Vehicle, public FourWheeler {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes.
Car obj;
return 0;
}

 Kế thừa đa mức: Trong kiểu kế thừa này, một lớp dẫn xuất được tạo từ
một lớp dẫn xuất khác.

116
Chương 3. Lớp và đối tượng

Hình 3. 9 Kế thừa đa mức

Cú pháp:

class C
{
... .. ...
};
class B:public C
{
... .. ...
};
class A: public B
{
... ... ...
};

Ví dụ về kế thừa đa mức:

// C++ program to implement

117
Chương 3. Lớp và đối tượng

// Multilevel Inheritance
#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// first sub_class derived from class vehicle


class fourWheeler : public Vehicle {
public:
fourWheeler()
{
cout << "Objects with 4 wheels are vehicles\n";
}
};
// sub class derived from the derived base class fourWheeler
class Car : public fourWheeler {
public:
Car() { cout << "Car has 4 Wheels\n"; }
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base classes.
Car obj;
return 0;
}

118
Chương 3. Lớp và đối tượng

 Kế thừa phân cấp: Trong kiểu thừa kế này, nhiều hơn một lớp con được
kế thừa từ một lớp cơ sở duy nhất. tức là có nhiều hơn một lớp dẫn xuất
được tạo ra từ một lớp cơ sở duy nhất.

Hình 3. 10 Kế thừa phân cấp

Cú pháp:

class A
{
// body of the class A.
}
class B : public A
{
// body of class B.
}
class C : public A
{
// body of class C.
}
class D : public A
{
// body of class D.
}

119
Chương 3. Lớp và đối tượng

Ví dụ về kế thừa phân cấp:

// C++ program to implement


// Hierarchical Inheritance
#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// first sub class


class Car : public Vehicle {
};

// second sub class


class Bus : public Vehicle {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base class.
Car obj1;
Bus obj2;
return 0;
}

120
Chương 3. Lớp và đối tượng

Kế thừa lai (Ảo) : Kế thừa lai được thực hiện bằng cách kết hợp nhiều loại kế
thừa. Ví dụ: Kết hợp kế thừa Hierarchical và Multiple Inheritance. Hình ảnh
dưới đây cho thấy sự kết hợp của nhiều kế thừa và phân cấp:

Hình 3. 11 Kế thừa lai

Ví dụ kế thừa lai:

// C++ program for Hybrid Inheritance

#include <iostream>
using namespace std;

// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};

// base class
class Fare {
public:

121
Chương 3. Lớp và đối tượng

Fare() { cout << "Fare of Vehicle\n"; }


};

// first sub class


class Car : public Vehicle {
};

// second sub class


class Bus : public Vehicle, public Fare {
};

// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base class.
Bus obj2;
return 0;
}

3.5.2 Tính đa hình


Từ "đa hình" có nghĩa là có nhiều dạng. Nói một cách đơn giản, chúng
ta có thể định nghĩa tính đa hình là khả năng một thông báo được hiển thị ở
nhiều dạng. Một ví dụ thực tế về tính đa hình là một người đồng thời có thể có
những đặc điểm khác nhau. Một người đàn ông đồng thời là một người cha,
một người chồng và một nhân viên. Vì vậy, cùng một người thể hiện hành vi
khác nhau trong các tình huống khác nhau. Điều này được gọi là đa hình. Tính
đa hình được coi là một trong những tính năng quan trọng của Lập trình hướng
đối tượng.
Các loại đa hình:
 Đa hình thời gian biên dịch.
 Đa hình thời gian chạy.

122
Chương 3. Lớp và đối tượng

Hình 3. 12 Mô hình đa hình

a) Đa hình thời gian biên dịch


Loại đa hình này đạt được bằng nạp chồng hàm hoặc nạp chồng toán tử.

Nạp chồng hàm:


Khi có nhiều hàm có cùng tên nhưng khác tham số, thì các hàm đó được
cho là bị quá tải, do đó điều này được gọi là Quá tải hàm. Hàm có thể được nạp
chồng bằng cách thay đổi số lượng đối số hoặc/và thay đổi loại đối số . Nói một
cách đơn giản, đây là một tính năng của lập trình hướng đối tượng cung cấp
nhiều hàm có cùng tên nhưng các tham số khác nhau khi nhiều tác vụ được liệt
kê dưới một tên hàm. Có một số quy tắc về quá tải chức năng nên được tuân
theo trong khi quá tải một chức năng. Dưới đây là chương trình C++ để hiển thị
nạp chồng hàm hoặc đa hình thời gian biên dịch:

// C++ program to demonstrate


// function overloading or
// Compile-time Polymorphism
#include <bits/stdc++.h>

using namespace std;


class Geeks {
public:

123
Chương 3. Lớp và đối tượng

// Function with 1 int parameter


void func(int x)
{
cout << "value of x is " <<
x << endl;
}

// Function with same name but


// 1 double parameter
void func(double x)
{
cout << "value of x is " <<
x << endl;
}

// Function with same name and


// 2 int parameters
void func(int x, int y)
{
cout << "value of x and y is " <<
x << ", " << y << endl;
}
};

// Driver code
int main()
{
Geeks obj1;

// Function being called depends


// on the parameters passed
// func() is called with int value
obj1.func(7);

124
Chương 3. Lớp và đối tượng

// func() is called with double value


obj1.func(9.132);

// func() is called with 2 int values


obj1.func(85, 64);
return 0;
}

Giải thích: Trong ví dụ trên, một hàm duy nhất có tên hàm func() hoạt
động khác nhau trong ba tình huống khác nhau, đây là một thuộc tính của tính
đa hình. Nạp chồng hàm là một tính năng của lập trình hướng đối tượng trong đó
hai hay nhiều hàm có thể có cùng tên nhưng khác tham số. Khi một tên hàm bị
quá tải với các công việc khác nhau, nó được gọi là Quá tải hàm. Trong quá tải
chức năng, tên "Hàm" phải giống nhau và các đối số phải khác nhau. Nạp chồng
hàm có thể coi là một ví dụ về tính năng đa hình trong C++.

Quá tải toán tử:


C++ có khả năng cung cấp cho các toán tử một ý nghĩa đặc biệt cho một
kiểu dữ liệu, khả năng này được gọi là nạp chồng toán tử. Ví dụ: chúng ta có thể
sử dụng toán tử cộng (+) cho lớp chuỗi để nối hai chuỗi. Chúng ta biết rằng
nhiệm vụ của toán tử này là cộng hai toán hạng. Vì vậy, một toán tử đơn '+', khi
được đặt giữa các toán hạng số nguyên, sẽ cộng chúng lại và khi được đặt giữa
các toán hạng chuỗi, sẽ nối chúng lại. Dưới đây là chương trình C++ để chứng
minh nạp chồng toán tử:

// C++ program to demonstrate


// Operator Overloading or
// Compile-Time Polymorphism
#include <iostream>
using namespace std;

class Complex {
private:

125
Chương 3. Lớp và đối tượng

int real, imag;

public:
Complex(int r = 0,
int i = 0)
{
real = r;
imag = i;
}

// This is automatically called


// when '+' is used with between
// two Complex objects
Complex operator+(Complex const& obj)
{
Complex res;
res.real = real + obj.real;
res.imag = imag + obj.imag;
return res;
}
void print()
{
cout << real << " + i" <<
imag << endl;
}
};

// Driver code
int main()
{
Complex c1(10, 5), c2(2, 4);

// An example call to "operator+"


Complex c3 = c1 + c2;

126
Chương 3. Lớp và đối tượng

c3.print();
}

Giải thích: Trong ví dụ trên, toán tử '+' bị quá tải. Thông thường, toán tử
này được sử dụng để cộng hai số (số nguyên hoặc số dấu phẩy động), nhưng ở
đây toán tử này được tạo để thực hiện phép cộng hai số ảo hoặc số phức.

b) Đa hình thời gian chạy


Loại đa hình này đạt được bằng Ghi đè chức năng . Liên kết muộn và đa
hình động là những tên gọi khác của đa hình thời gian chạy. Cuộc gọi chức năng
được giải quyết trong thời gian chạy trong đa hình thời gian chạy . Ngược lại,
với tính đa hình thời gian biên dịch, trình biên dịch xác định lệnh gọi hàm nào sẽ
liên kết với đối tượng sau khi suy diễn nó khi chạy.

Ghi đè chức năng:


Ghi đè hàm xảy ra khi một lớp dẫn xuất có một định nghĩa cho một trong
các hàm thành viên của lớp cơ sở. Chức năng cơ sở đó được cho là bị ghi đè.

127
Chương 3. Lớp và đối tượng

Hình 3. 13 Minh họa đa hình ghi đè hàm

Dưới đây là chương trình C++ để chứng minh chức năng ghi đè:

// C++ program for function overriding


#include <bits/stdc++.h>
using namespace std;

class base {
public:
virtual void print()

128
Chương 3. Lớp và đối tượng

{
cout << "print base class" <<
endl;
}

void show()
{
cout << "show base class" <<
endl;
}
};

class derived : public base {


public:

// print () is already virtual function in


// derived class, we could also declared as
// virtual void print () explicitly
void print()
{
cout << "print derived class" <<
endl;
}

void show()
{
cout << "show derived class" <<
endl;
}
};

// Driver code
int main()
{

129
Chương 3. Lớp và đối tượng

base* bptr;
derived d;
bptr = &d;

// Virtual function, binded at


// runtime (Runtime polymorphism)
bptr->print();

// Non-virtual function, binded


// at compile time
bptr->show();

return 0;
}

c) Hàm ảo
Hàm ảo là một hàm thành viên được khai báo trong lớp cơ sở sử dụng từ
khóa virtual và được định nghĩa lại (Ghi đè) trong lớp dẫn xuất.
Một số điểm chính về chức năng ảo:
 Các hàm ảo có bản chất là Động.
 Chúng được định nghĩa bằng cách chèn từ khóa “ virtual ” bên trong lớp
cơ sở và luôn được khai báo với lớp cơ sở và được ghi đè trong lớp con
 Một chức năng ảo được gọi trong thời gian chạy
Dưới đây là chương trình C++ để chứng minh chức năng ảo:

// C++ Program to demonstrate


// the Virtual Function
#include <iostream>
using namespace std;

// Declaring a Base class


class GFG_Base {

public:

130
Chương 3. Lớp và đối tượng

// virtual function
virtual void display()
{
cout << "Called virtual Base Class function" <<
"\n\n";
}

void print()
{
cout << "Called GFG_Base print function" <<
"\n\n";
}
};

// Declaring a Child Class


class GFG_Child : public GFG_Base {

public:
void display()
{
cout << "Called GFG_Child Display Function" <<
"\n\n";
}

void print()
{
cout << "Called GFG_Child print Function" <<
"\n\n";
}
};

// Driver code
int main()
{

131
Chương 3. Lớp và đối tượng

// Create a reference of class bird


GFG_Base* base;

GFG_Child child;

base = &child;

// This will call the virtual function


base->GFG_Base::display();

// this will call the non-virtual function


base->print();
}
3.5.3 Tính đóng gói
Theo thuật ngữ thông thường được định nghĩa là gói gọn dữ liệu và thông
tin dưới một đơn vị. Trong Lập trình hướng đối tượng, Đóng gói được định
nghĩa là liên kết dữ liệu và các hàm thao tác với chúng. Xem xét một ví dụ thực
tế về đóng gói, trong một công ty có các phần khác nhau như phần tài khoản,
phần tài chính, phần bán hàng, v.v. Phần tài chính xử lý tất cả các giao dịch tài
chính và lưu giữ hồ sơ của tất cả dữ liệu liên quan đến tài chính. Tương tự, bộ
phận bán hàng xử lý tất cả các hoạt động liên quan đến bán hàng và lưu giữ hồ
sơ của tất cả các lần bán hàng. Bây giờ có thể nảy sinh tình huống khi vì lý do
nào đó, một quan chức từ bộ phận tài chính cần tất cả dữ liệu về doanh số bán
hàng trong một tháng cụ thể. Trong trường hợp này, anh ta không được phép
truy cập trực tiếp vào dữ liệu của phần bán hàng. Trước tiên, anh ta sẽ phải liên
hệ với một số nhân viên khác trong bộ phận bán hàng và sau đó yêu cầu anh ta
cung cấp dữ liệu cụ thể. Đây là những gì đóng gói là. Ở đây, dữ liệu của bộ phận
bán hàng và những nhân viên có thể thao tác với chúng được gói gọn dưới một
tên duy nhất “bộ phận bán hàng”.
Chúng ta không thể truy cập trực tiếp bất kỳ chức năng nào từ lớp. Khi
cần một đối tượng để truy cập hàm đang sử dụng biến thành viên của lớp đó.
Hàm mà chúng ta đang tạo bên trong lớp, nó phải sử dụng tất cả các biến thành
viên thì chỉ có nó mới được gọi là đóng gói. Nếu chúng ta không tạo hàm bên
trong lớp đang sử dụng biến thành viên của lớp thì sẽ không gọi đó là đóng gói.

132
Chương 3. Lớp và đối tượng

Hình 3. 14 Minh họa đóng gói

Đóng gói cũng dẫn đến trừu tượng hóa hoặc ẩn dữ liệu. Khi sử dụng đóng
gói cũng ẩn dữ liệu. Trong ví dụ trên, dữ liệu của bất kỳ phần nào như bán hàng,
tài chính hoặc tài khoản bị ẩn khỏi bất kỳ phần nào khác. Trong C++ đóng gói
có thể được thực hiện bằng cách sử dụng lớp và truy cập sửa đổi . Nhìn vào
chương trình dưới đây:

// c++ program to explain


// Encapsulation

#include<iostream>
using namespace std;

class Encapsulation
{
private:
// data hidden from outside world
int x;

public:
// function to set value of

133
Chương 3. Lớp và đối tượng

// variable x
void set(int a)
{
x =a;
}

// function to return value of


// variable x
int get()
{
return x;
}
};

// main function
int main()
{
Encapsulation obj;

obj.set(5);

cout<<obj.get();
return 0;
}

Trong chương trình trên, biến x được đặt ở chế độ riêng tư. Biến này chỉ
có thể được truy cập và thao tác bằng cách sử dụng các hàm get() và set() có
trong lớp. Vì vậy, chúng ta có thể nói rằng ở đây, biến x và các hàm get() và
set() được liên kết với nhau, không có gì khác ngoài sự đóng gói.

Vai trò của các chỉ định truy cập trong đóng gói:
Như chúng ta đã thấy trong ví dụ trên, các chỉ định truy cập đóng một vai
trò quan trọng trong việc thực hiện đóng gói trong C++. Quá trình thực hiện
đóng gói có thể được chia thành hai bước:

134
Chương 3. Lớp và đối tượng

 Các thành viên dữ liệu phải được gắn nhãn là riêng tư bằng cách sử
dụng các chỉ định truy cập riêng tư
 Hàm thành viên thao tác với các thành viên dữ liệu phải được gắn
nhãn là công khai bằng cách sử dụng công cụ xác định quyền truy
cập công khai
3.5.4 Tính trừ tượng hóa
Trừu tượng hóa dữ liệu là một trong những tính năng cần thiết và quan
trọng nhất của lập trình hướng đối tượng trong C++. Trừu tượng có nghĩa là chỉ
hiển thị thông tin cần thiết và ẩn các chi tiết. Trừu tượng hóa dữ liệu đề cập đến
việc chỉ cung cấp thông tin cần thiết về dữ liệu cho thế giới bên ngoài, ẩn các chi
tiết cơ bản hoặc triển khai. Hãy xem xét một ví dụ thực tế về một người đàn ông
đang lái ô tô . Người đàn ông chỉ biết rằng nhấn ga sẽ tăng tốc độ của xe hoặc
đạp phanh sẽ dừng xe nhưng anh ta không biết khi nhấn ga tốc độ thực sự tăng
lên như thế nào, anh ta không biết về cơ chế bên trong của xe hay việc thực hiện
chân ga, phanh, v.v. trên ô tô. Đây là những gì trừu tượng là.

Các loại trừu tượng:


 Trừu tượng hóa dữ liệu – Loại này chỉ hiển thị thông tin cần thiết về dữ
liệu và ẩn dữ liệu không cần thiết.
 Trừu tượng hóa điều khiển – Loại này chỉ hiển thị thông tin cần thiết về
việc triển khai và ẩn thông tin không cần thiết.

135
Chương 3. Lớp và đối tượng

Hình 3. 15 Mô hình trừu tượng hóa

Trừu tượng hóa bằng cách sử dụng các Lớp:


Chúng ta có thể triển khai Trừu tượng hóa trong C++ bằng cách sử dụng
các lớp. Lớp này giúp chúng ta nhóm các thành viên dữ liệu và các hàm thành
viên bằng cách sử dụng các chỉ định truy cập có sẵn. Một Lớp có thể quyết định
thành viên dữ liệu nào sẽ hiển thị với thế giới bên ngoài và thành viên nào
không.

Trừu tượng hóa trong tệp Tiêu đề:


Một kiểu trừu tượng nữa trong C++ có thể là tệp tiêu đề. Ví dụ: hãy xem
xét phương thức pow() có trong tệp tiêu đề math.h. Bất cứ khi nào chúng ta cần
tính lũy thừa của một số, chúng ta chỉ cần gọi hàm pow() có trong tệp tiêu đề
math.h và chuyển các số đó làm đối số mà không cần biết thuật toán cơ bản theo
đó hàm thực sự tính toán lũy thừa của các số .

Trừu tượng hóa bằng Access Specifiers


Các chỉ định truy cập là trụ cột chính của việc thực hiện trừu tượng hóa
trong C++. Chúng ta có thể sử dụng các chỉ định truy cập để thực thi các hạn
chế đối với các thành viên của lớp.
Ví dụ:
 Các thành viên được khai báo là công khai trong một lớp có thể được truy
cập từ bất kỳ đâu trong chương trình.

136
Chương 3. Lớp và đối tượng

 Các thành viên được khai báo là riêng tư trong một lớp, chỉ có thể được
truy cập từ bên trong lớp. Chúng không được phép truy cập từ bất kỳ phần
nào của mã bên ngoài lớp.
Chúng ta có thể dễ dàng thực hiện trừu tượng hóa bằng cách sử dụng hai
tính năng trên được cung cấp bởi các chỉ định truy cập. Giả sử, các thành viên
xác định việc triển khai nội bộ có thể được đánh dấu là riêng tư trong một lớp.
Và thông tin quan trọng cần cung cấp cho thế giới bên ngoài có thể được đánh
dấu là công khai. Và những thành viên công cộng này có thể truy cập các thành
viên riêng tư khi họ ở trong lớp.

Ví dụ về trừu tượng hóa C++:

// C++ Program to Demonstrate the


// working of Abstraction
#include <iostream>
using namespace std;

class implementAbstraction {
private:
int a, b;

public:
// method to set values of
// private members
void set(int x, int y)
{
a = x;
b = y;
}

void display()
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;

137
Chương 3. Lớp và đối tượng

}
};

int main()
{
implementAbstraction obj;
obj.set(10, 20);
obj.display();
return 0;
}

Chúng ta có thể thấy trong chương trình trên chúng ta không được phép
truy cập trực tiếp vào biến a và b, tuy nhiên ta có thể gọi hàm set() để thiết lập
giá trị cho a và b và hàm display() để hiển thị giá trị của a và b.
Ưu điểm của trừu tượng hóa dữ liệu
 Giúp người dùng tránh viết mã cấp thấp
 Tránh trùng lặp mã và tăng khả năng sử dụng lại.
 Có thể thay đổi cài đặt nội bộ của lớp một cách độc lập mà không ảnh
hưởng đến người dùng.
 Giúp tăng tính bảo mật của ứng dụng hoặc chương trình vì chỉ những chi
tiết quan trọng mới được cung cấp cho người dùng.
 Nó làm giảm độ phức tạp cũng như sự dư thừa của mã, do đó làm tăng
khả năng đọc.
3.6. Bài tập Chương 3
Bài 1. Xây dựng một lớp đường tròn lưu giữ bán kính và tâm của đường tròn.
Tạo các phương thức để tính chu vi, diện tích của đường tròn.
Bài 2. Thêm thuộc tính BanKinh vào lớp được tạo ra từ bài tập 1.
Bài 3. Viết lớp giải phương trình bậc hai. Lớp này có các thuộc tính a, b, c và
nghiệm x1, x2. Lớp cho phép bên ngoài xem được các nghiệm của phương trình
và cho phép thiết lập hay xem các giá trị a, b, c.
Bài 4. Xây dựng lớp đa thức với các toán tử +, -, *, / và chỉ mục để truy cập đến
hệ số thứ i của đa thức.
Bài 5. Xây dựng lớp ma trận với các phép toán +, -, *, / và chỉ mục để truy cập
đến phần tử bất kỳ của ma trận.

138
Chương 3. Lớp và đối tượng

Bài 6. Xây dựng lớp NguoiThueBao (số điện thọai, họ tên), từ đó xây dựng lớp
DanhBa (danh bạ điện thọai) với các phương thức như nhập danh bạ điện thọai,
xuất danh bạ điện thọai, tìm số điện thọai theo tên (chỉ mục ), tìm tên theo số
điện thoại (chỉ mục ).
Bài 7. Xây dựng lớp Person gồm các thông tin: Họ và tên, Ngày sinh, Quê quán.
Sau đó, xây dựng lớp dẫn xuất “Kỹ sư” ngoài các thông tin của lớp Person, lớp
kỹ sư còn có các thông tin về: Ngành học, Năm tốt nghiệp (int) và các phương
thức: Phương thức nhập: nhập các thông tin của kỹ sư. Phương thức xuất: xuất
các thông tin lên màn hình. Xây dựng chương trình chính nhập vào một danh
sách các kỹ sư. In danh sách của các kỹ sư lên màn hình và thông tin của các kỹ
sư tốt nghiệp gần đây nhất (năm tốt nghiệp lớn nhất).
Bài 8. Xây dựng lớp Máy in gồm các thông tin: Trọng lượng máy, năm sản xuất,
hãng sản xuất. Sau đó, xây dựng lớp dẫn xuất: Máy in kim, ngoài các thuộc tính
của máy in ra còn có thêm thuộc tính : số kim (int), tốc độ in (trang/ phút - int).
Xây dựng lớp Máy in Laser ngoài các thuộc tính của máy in còn có thêm các
thuộc tính: Độ phân giải (int), tốc độ in (int). Hai lớp dẫn xuất này có các
phương thức: Nhập: nhập các thông tin của máy in, Xuất: xuất các thông tin của
máy in ra màn hình. Xây dựng chương trình chính nhập vào thông tin của n máy
in kim và m máy in Laser. Xuất các thông tin đó lên màn hình.
Bài 9. Xây dựng lớp PERSON gồm các thông tin sau: Hoten (char[50]),
Ngaysinh (char[12]), Quequan (char[100]) và xây dựng lớp DIEM gồm:
Diểmtoan (int), Diemly (int), Điểmhoá (int). Xây dựng lớp HOCSINH kế thừa
từ 2 lớp trên có thêm dữ liệu: Lop (char [30]), Tongdiem (int) và các phương
thức nhập dữ liệu từ bàn phím và xuất dữ liệu ra màn hình. Yêu cầu cả 3 lớp trên
đều có phương thức thiết lập để khởi tạo các dữ liệu là số thì giá trị = 0, dữ liệu
là xâu thì giá trị = “”. Phải viết chương trình chính để minh hoạ sử dụng lớp vừa
xâu dựng.
Bài 10. Cài đặt lớp theo sơ đồ sau
(với input và output là các phương thức nhập, xuất thông tin của các thuộc tính
của lớp). Viết chương trình chính nhập vào danh sách n máy tính. In ra thông tin
của các máy tính của nhà sản xuất IBM. Sắp xếp danh sách các máy tính theo
chiều tăng dần của giá thành và in danh sách đã sắp ra màn hình. Xoá mọi máy
tính của hãng Intel sản xuất và in danh sách kết quả ra màn hình.

139
Chương 3. Lớp và đối tượng

140
Chương 4. Cấu trúc dữ liệu

CHƯƠNG 4. CẤU TRÚC DỮ LIỆU


4.1. Danh sách liên kết
Như các cấu trúc dữ liệu trên, danh sách liên kết cũng là một cấu trúc dữ
liệu tuyến tính. Nhưng danh sách liên kết khác với Array trong cấu hình của nó.
Nó không được phân bổ cho các vị trí bộ nhớ liền kề. Thay vào đó, mỗi nút của
danh sách liên kết được phân bổ cho một số không gian bộ nhớ ngẫu nhiên và
nút trước đó duy trì một con trỏ trỏ tới nút này. Vì vậy, không thể truy cập bộ
nhớ trực tiếp của bất kỳ nút nào và nó cũng là động, tức là kích thước của danh
sách được liên kết có thể được điều chỉnh bất cứ lúc nào.

Hình 4. 1 Cấu trúc cơ bản của 1 danh sách liên kết

Tại sao cần danh sách liên kết?


Mảng có thể được sử dụng để lưu trữ dữ liệu tuyến tính có kiểu tương tự, nhưng
mảng có những hạn chế sau:
 Kích thước của mảng là cố định : Vì vậy, chúng ta phải biết trước giới hạn
trên của số lượng phần tử. Ngoài ra, nói chung, bộ nhớ được phân bổ bằng
với giới hạn trên bất kể mức sử dụng.
 Chèn phần tử mới / Xóa phần tử hiện có trong một mảng các phần tử rất
tốn kém: Phải tạo không gian cho các phần tử mới và để tạo không gian,
các phần tử hiện có phải được dịch chuyển nhưng trong Danh sách liên
kết nếu chúng ta có nút đầu thì chúng ta có thể đi qua bất kỳ nút nào
thông qua nút đó và chèn nút mới vào vị trí cần thiết.
Ví dụ:
 Trong một hệ thống, nếu chúng ta duy trì một danh sách ID được sắp xếp
trong một mảng id[] = [1000, 1010, 1050, 2000, 2040]. Nếu muốn chèn

141
Chương 4. Cấu trúc dữ liệu

ID mới 1005 thì để giữ nguyên thứ tự đã sắp xếp, chúng ta phải di chuyển
tất cả các phần tử sau 1000 (không bao gồm 1000).
 Việc xóa cũng tốn kém với các mảng trừ khi một số kỹ thuật đặc biệt
được sử dụng. Ví dụ: để xóa 1010 trong id[], mọi thứ sau 1010 phải được
di chuyển do có quá nhiều công việc đang được thực hiện ảnh hưởng đến
hiệu quả của mã.

Ưu điểm của Danh sách được liên kết so với mảng:


 Mảng động.
 Dễ dàng Chèn/Xóa.
Hạn chế của danh sách liên kết:
 Truy cập ngẫu nhiên không được phép. Chúng ta phải truy cập các phần
tử một cách tuần tự bắt đầu từ nút đầu tiên (nút đầu). Vì vậy, không thể
thực hiện tìm kiếm nhị phân với các danh sách được liên kết một cách
hiệu quả với cách triển khai mặc định của nó.
 Không gian bộ nhớ bổ sung cho một con trỏ được yêu cầu với mỗi phần
tử của danh sách.
 Không thân thiện với bộ đệm. Vì các phần tử mảng là các vị trí liền kề,
nên có vị trí tham chiếu không có trong trường hợp danh sách được liên
kết.
 Phải mất rất nhiều thời gian để duyệt và thay đổi con trỏ.
 Không thể duyệt ngược trong danh sách liên kết đơn.
 Sẽ rất khó hiểu khi chúng ta làm việc với con trỏ.
 Không thể truy cập trực tiếp vào một phần tử trong danh sách được liên
kết như trong một mảng theo chỉ mục.
 Tìm kiếm một phần tử rất tốn kém và yêu cầu độ phức tạp thời gian O(n).
 Việc sắp xếp danh sách liên kết rất phức tạp và tốn kém.
Các loại danh sách liên kết:
 Danh sách được liên kết đơ – Trong loại danh sách liên kết này, người ta
có thể di chuyển hoặc duyệt qua danh sách được liên kết chỉ theo một
hướng. trong đó con trỏ tiếp theo của mỗi nút trỏ đến các nút khác nhưng
con trỏ tiếp theo của nút cuối cùng trỏ đến NULL. Nó còn được gọi là “
Danh sách liên kết đơn lẻ” .

142
Chương 4. Cấu trúc dữ liệu

 Danh sách liên kết kép – Trong loại danh sách được liên kết này, người ta
có thể di chuyển hoặc duyệt qua danh sách được liên kết theo cả hai
hướng (Tiến và Lùi)
 Danh sách liên kết vòng – Trong loại danh sách được liên kết này, nút
cuối cùng của danh sách được liên kết chứa liên kết của nút đầu tiên/nút
đầu của danh sách được liên kết trong con trỏ tiếp theo của nó.
 Danh sách liên kết vòng đôi – Danh sách liên kết vòng đôi hoặc danh sách
liên kết hai chiều vòng là một loại danh sách liên kết phức tạp hơn có
chứa một con trỏ tới nút tiếp theo cũng như nút trước đó trong chuỗi. Sự
khác nhau giữa danh sách kép liên kết kép và danh sách kép vòng cũng
giống như sự khác biệt giữa danh sách liên kết đơn và danh sách liên kết
vòng. Danh sách liên kết kép vòng không chứa null trong trường trước đó
của nút đầu tiên.
 Danh sách liên kết tiêu đề – Danh sách liên kết tiêu đề là một loại danh
sách liên kết đặc biệt có chứa nút tiêu đề ở đầu danh sách.
Các thao tác cơ bản trên danh sách liên kết:
 xóa
 Chèn
 Tìm kiếm
 Hiển thị
4.1.1 Danh sách liên kết đơn
a) Định nghĩa danh sách đơn
Biểu diễn danh sách liên kết đơn:
Một danh sách được liên kết được biểu diễn bằng một con trỏ tới nút đầu
tiên của danh sách được liên kết. Nút đầu tiên được gọi là nút đầu của danh sách
liên kết. Nếu danh sách liên kết rỗng thì giá trị của phần đầu trỏ tới NULL. Mỗi
nút trong danh sách bao gồm ít nhất hai phần:
 Một mục dữ liệu (chúng ta có thể lưu trữ số nguyên, chuỗi hoặc bất kỳ
loại dữ liệu nào).
 Con trỏ (Hoặc Tham chiếu) tới nút tiếp theo (kết nối nút này với nút khác)
hoặc địa chỉ của nút khác

Cú pháp:

143
Chương 4. Cấu trúc dữ liệu

class Node {
public:
int data;
Node* next;
};

Xây dựng danh sách liên kết đơn giản có 3 nút: Duyệt qua một danh sách
được liên kết. Để truyền tải, chúng ta hãy viết một hàm đa năng printList() để in
bất kỳ danh sách đã cho nào. Ví dụ sau cho thấy việc duyệt qua một danh sách
được liên kết:

// A simple C++ program for


// traversal of a linked list

#include <bits/stdc++.h>
using namespace std;

class Node {
public:
int data;
Node* next;
};

// This function prints contents of linked list


// starting from the given node
void printList(Node* n)
{
while (n != NULL) {
cout << n->data << " ";
n = n->next;
}
}

// Driver's code

144
Chương 4. Cấu trúc dữ liệu

int main()
{
Node* head = NULL;
Node* second = NULL;
Node* third = NULL;

// allocate 3 nodes in the heap


head = new Node();
second = new Node();
third = new Node();

head->data = 1; // assign data in first node


head->next = second; // Link first node with second

second->data = 2; // assign data to second node


second->next = third;

third->data = 3; // assign data to third node


third->next = NULL;

// Function call
printList(head);

return 0;
}

Thời gian thực thi:

145
Chương 4. Cấu trúc dữ liệu

Hình 4. 2 Độ phức tạp về thời gian thực thi của các thao tác trên danh sách liên
kết đơn

 Tìm kiếm là O(n) vì dữ liệu không được lưu trữ trong các vị trí bộ nhớ
liền kề nên chúng ta phải duyệt qua từng phần tử một.
 Chèn và xóa là O(1) bởi vì chúng ta phải liên kết các nút mới để Chèn với
nút trước đó và nút tiếp theo đổng thời hủy liên kết các nút tồn tại để xóa
khỏi các nút trước đó sau đó sẽ không có bất kỳ chuyển đổi nào.
b) Chèn một nút vào danh sách liên kết đơn
Trong bài đăng này, các phương pháp để chèn một nút mới trong danh
sách được liên kết sẽ được thảo luận. Một nút có thể được thêm vào theo ba cách
:
 Đứng đầu danh sách liên kết
 Sau một nút nhất định.
 Ở cuối danh sách liên kết.

 Thêm một nút ở phía trước: (quy trình 4 bước)


Cách tiếp cận: Nút mới luôn được thêm vào trước phần đầu của Danh
sách được liên kết đã cho. Và nút mới được thêm vào sẽ trở thành nút đầu mới
của Danh sách được liên kết. Ví dụ: Nếu Danh sách Liên kết đã cho là 10->15-
>20->25 và chúng ta thêm mục 5 ở phía trước, thì Danh sách Liên kết sẽ trở
thành 5->10->15->20->25. Chúng ta hãy gọi hàm thêm vào đầu danh sách là
push(). Push() phải nhận một con trỏ tới con trỏ đầu vì đẩy phải thay đổi con trỏ
đầu để trỏ tới nút mới.

146
Chương 4. Cấu trúc dữ liệu

Hình 4. 3 Thêm một nút vào phần trước

Ví dụ:

// Given a reference (pointer to pointer)


// to the head of a list and an int,
// inserts a new node on the front of
// the list.
void push(Node** head_ref, int new_data)
{

// 1. allocate node
Node* new_node = new Node();

// 2. put in the data


new_node->data = new_data;

// 3. Make next of new node as head


new_node->next = (*head_ref);

// 4. Move the head to point to


// the new node
(*head_ref) = new_node;
}

Phân tích độ phức tạp:

147
Chương 4. Cấu trúc dữ liệu

Độ phức tạp về thời gian: O(1), Chúng ta có một con trỏ tới đầu và chúng
ta có thể trực tiếp đính kèm một nút để thay đổi con trỏ. Vì vậy, độ phức tạp
Thời gian của việc chèn một nút ở vị trí đầu là O(1) vì nó thực hiện một lượng
công việc không đổi.
Không gian phụ trợ: O(1).

 Thêm một nút sau một nút đã cho: (quy trình 5 bước)
Cách tiếp cận: Chúng tôi được cung cấp một con trỏ tới một nút và nút
mới được chèn vào sau nút đã cho.
Làm theo các bước để thêm một nút sau một nút đã cho:

 Đầu tiên, hãy kiểm tra xem nút đã cho trước đó có phải là NULL hay
không.
 Sau đó, phân bổ một nút mới và
 Gán dữ liệu cho nút mới
 Và sau đó tạo nút tiếp theo của nút mới làm nút tiếp theo của nút trước đó.
 Cuối cùng, di chuyển nút tiếp theo của nút trước đó thành một nút mới.

Hình 4. 4 Thêm một nút vào sau nút đã cho

Ví dụ:

// Given a node prev_node, insert a


// new node after the given
// prev_node
void insertAfter(Node* prev_node, int new_data)
{

148
Chương 4. Cấu trúc dữ liệu

// 1. Check if the given prev_node is NULL


if (prev_node == NULL) {
cout << "The given previous node cannot be NULL";
return;
}

// 2. Allocate new node


Node* new_node = new Node();

// 3. Put in the data


new_node->data = new_data;

// 4. Make next of new node as


// next of prev_node
new_node->next = prev_node->next;

// 5. move the next of prev_node


// as new_node
prev_node->next = new_node;
}

Phân tích độ phức tạp:


 Độ phức tạp về thời gian: O(1), vì prev_node đã được đưa ra làm đối số
trong một phương thức, không cần phải lặp lại danh sách để tìm
prev_node
 Không gian phụ trợ: O(1) vì sử dụng không gian cố định để sửa đổi con
trỏ.

 Thêm một nút ở cuối: (quy trình 6 bước)


 Nút mới luôn được thêm vào sau nút cuối cùng của Danh sách liên kết đã
cho. Ví dụ: nếu Danh sách Liên kết đã cho là 5->10->15->20->25 và

149
Chương 4. Cấu trúc dữ liệu

chúng ta thêm một mục 30 vào cuối, thì Danh sách Liên kết sẽ trở thành
5->10->15->20->25- >30.
 Vì một Danh sách Liên kết thường được đại diện bởi phần đầu của nó,
nên chúng ta phải duyệt qua danh sách cho đến hết và sau đó thay đổi nút
tiếp theo thành nút cuối cùng thành một nút mới.

Hình 4. 5 Thêm một nút ở cuối

Ví dụ:

// Given a reference (pointer to pointer) to the head


// of a list and an int, appends a new node at the end
void append(Node** head_ref, int new_data)
{

// 1. allocate node
Node* new_node = new Node();

// Used in step 5
Node *last = *head_ref;

// 2. Put in the data


new_node->data = new_data;

// 3. This new node is going to be


// the last node, so make next of
// it as NULL

150
Chương 4. Cấu trúc dữ liệu

new_node->next = NULL;

// 4. If the Linked List is empty,


// then make the new node as head
if (*head_ref == NULL)
{
*head_ref = new_node;
return;
}

// 5. Else traverse till the last node


while (last->next != NULL)
{
last = last->next;
}

// 6. Change the next of last node


last->next = new_node;
return;
}

Phân tích độ phức tạp:


 Độ phức tạp về thời gian: O(N), trong đó N là số nút trong danh sách liên
kết. Vì có một vòng lặp từ đầu đến cuối nên hàm O(n) hoạt động.
 Phương pháp này cũng có thể được tối ưu hóa để hoạt động trong O(1)
bằng cách giữ thêm một con trỏ tới đuôi của danh sách được liên kết/
 Không gian phụ trợ: O(1)
Sau đây là một chương trình hoàn chỉnh sử dụng tất cả các phương pháp
trên để tạo danh sách liên kết:

// A complete working C++ program to


// demonstrate all insertion methods
// on Linked List

151
Chương 4. Cấu trúc dữ liệu

#include <bits/stdc++.h>
using namespace std;

// A linked list node


class Node
{
public:
int data;
Node *next;
};

// Given a reference (pointer to pointer)


// to the head of a list and an int, inserts
// a new node on the front of the list.
void push(Node** head_ref, int new_data)
{

// 1. allocate node
Node* new_node = new Node();

// 2. put in the data


new_node->data = new_data;

// 3. Make next of new node as head


new_node->next = (*head_ref);

// 4. move the head to point


// to the new node
(*head_ref) = new_node;
}

// Given a node prev_node, insert a new


// node after the given prev_node
void insertAfter(Node* prev_node, int new_data)

152
Chương 4. Cấu trúc dữ liệu

{
// 1. check if the given prev_node
// is NULL
if (prev_node == NULL)
{
cout<<"The given previous node cannot be NULL";
return;
}

// 2. allocate new node


Node* new_node = new Node();

// 3. put in the data


new_node->data = new_data;

// 4. Make next of new node


// as next of prev_node
new_node->next = prev_node->next;

// 5. move the next of prev_node


// as new_node
prev_node->next = new_node;
}

// Given a reference (pointer to pointer)


// to the head of a list and an int,
// appends a new node at the end
void append(Node** head_ref, int new_data)
{

// 1. allocate node
Node* new_node = new Node();

//used in step 5

153
Chương 4. Cấu trúc dữ liệu

Node *last = *head_ref;

// 2. put in the data


new_node->data = new_data;

/* 3. This new node is going to be


the last node, so make next of
it as NULL*/
new_node->next = NULL;

/* 4. If the Linked List is empty,


then make the new node as head */
if (*head_ref == NULL)
{
*head_ref = new_node;
return;
}

/* 5. Else traverse till the last node */


while (last->next != NULL)
{
last = last->next;
}

/* 6. Change the next of last node */


last->next = new_node;
return;
}

// This function prints contents of


// linked list starting from head
void printList(Node *node)
{
while (node != NULL)

154
Chương 4. Cấu trúc dữ liệu

{
cout<<" "<<node->data;
node = node->next;
}
}

// Driver code
int main()
{

// Start with the empty list


Node* head = NULL;

// Insert 6. So linked list becomes 6->NULL


append(&head, 6);

// Insert 7 at the beginning.


// So linked list becomes 7->6->NULL
push(&head, 7);

// Insert 1 at the beginning.


// So linked list becomes 1->7->6->NULL
push(&head, 1);

// Insert 4 at the end. So


// linked list becomes 1->7->6->4->NULL
append(&head, 4);

// Insert 8, after 7. So linked


// list becomes 1->7->8->6->4->NULL
insertAfter(head->next, 8);

cout<<"Created Linked list is: ";


printList(head);

155
Chương 4. Cấu trúc dữ liệu

return 0;
}

C. Xóa phần tử khỏi Danh sách được liên kết


Chúng ta có thể xóa một phần tử trong danh sách từ:
 bắt đầu
 giữa
 cuối
 Xóa ở vị trí bắt đầu
Cú pháp:

Trỏ đầu tới nút tiếp theo tức là nút thứ hai
temp = head
head = head->next

Đảm bảo giải phóng bộ nhớ không sử dụng


free(temp); hoặc xóa tạm thời;

 Xóa ở vị trí Kết thúc:


Cú pháp:

Trỏ đầu tới phần tử trước tức là phần tử thứ hai cuối cùng
Thay đổi con trỏ tiếp theo thành
struct nodel*end = head;
struct nodel * pre = NULL;
while(end->next)
{
prev = end;
end= end-> next;
}
pre->next = NULL;

Đảm bảo giải phóng bộ nhớ không sử dụng

156
Chương 4. Cấu trúc dữ liệu

free(end); hoặc xóa kết thúc;

 Xóa ở vị trí giữa


Cú pháp

Theo dõi con trỏ trước nút để xóa và con trỏ tới nút để xóa
temp = head;
pre = head;
for(int i = 0; i < position; i++)
{
if(i == 0 && position == 1)
head = head->next;
free(temp) other
{
if
(i == position - 1 && temp)
{
prev->next = temp->next;
free (temp);
}
else
{
pre = temp;
if(prev == NULL) // vị trí lớn hơn số node trong list
break;
temp = temp->next;
}
}
}

Ví dụ xóa một phần tử:

#include <bits/stdc++.h>
using namespace std;

157
Chương 4. Cấu trúc dữ liệu

struct Node {
int number;
Node* next;
};

void Push(Node** head, int A)


{
Node* n = (Node*)malloc(sizeof(Node));
n->number = A;
n->next = *head;
*head = n;
}

void deleteN(Node** head, int position)


{
Node* temp;
Node* prev;
temp = *head;
prev = *head;
for (int i = 0; i < position; i++) {
if (i == 0 && position == 1) {
*head = (*head)->next;
free(temp);
}
else {
if (i == position - 1 && temp) {
prev->next = temp->next;
free(temp);
}
else {
prev = temp;

// Position was greater than

158
Chương 4. Cấu trúc dữ liệu

// number of nodes in the list


if (prev == NULL)
break;
temp = temp->next;
}
}
}
}

void printList(Node* head)


{
while (head) {
if(head->next == NULL)
cout << "[" << head->number << "] " << "[" << head << "]->" <<
"(nil)" << endl;
else
cout << "[" << head->number << "] " << "[" << head << "]->" <<
head->next << endl;
head = head->next;
}
cout << endl << endl;
}

// Driver code
int main()
{
Node* list = (Node*)malloc(sizeof(Node));
list->next = NULL;
Push(&list, 1);
Push(&list, 2);
Push(&list, 3);

printList(list);

159
Chương 4. Cấu trúc dữ liệu

// Delete any position from list


deleteN(&list, 1);
printList(list);
return 0;
}

4.1.2. Danh sách liên kết đôi


a) Khái niệm
Danh sách liên kết kép (DLL) chứa một con trỏ bổ sung, thường được gọi
là con trỏ trước, cùng với con trỏ tiếp theo và dữ liệu có trong danh sách liên kết
đơn.

Hình 4. 6 Cấu trúc của 1 danh sách liên kết đôi

Sau đây là một đại diện của một nút DLL:

// Node of a doubly linked list


class Node {
public:
int data;

// Pointer to next node in DLL


Node* next;

// Pointer to previous node in DLL


Node* prev;
};

Ưu điểm của DLL so với danh sách liên kết đơn:

160
Chương 4. Cấu trúc dữ liệu

 Một DLL có thể được duyệt theo cả hai hướng tiến và lùi.
 Thao tác xóa trong DLL sẽ hiệu quả hơn nếu đưa ra một con trỏ tới nút
cần xóa.
 Chúng ta có thể nhanh chóng chèn một nút mới trước một nút đã cho.
 Trong danh sách liên kết đơn, để xóa một nút, cần có một con trỏ tới nút
trước đó. Để có được nút trước đó, đôi khi danh sách được duyệt qua.
Trong DLL, chúng ta có thể lấy nút trước đó bằng cách sử dụng con trỏ
trước đó.
Nhược điểm của DLL so với danh sách liên kết đơn:
 Mỗi nút của DLL Yêu cầu thêm không gian cho một con trỏ trước đó.
Tuy nhiên, có thể triển khai DLL bằng một con trỏ.
 Tất cả các hoạt động yêu cầu một con trỏ bổ sung trước đó để được duy
trì. Ví dụ, trong phép chèn, chúng ta cần sửa đổi các con trỏ trước đó cùng
với các con trỏ tiếp theo. Ví dụ trong các hàm sau để chèn vào các vị trí
khác nhau, chúng ta cần thêm 1 hoặc 2 bước để đặt con trỏ trước đó.
b) Chèn vào DLL:
Một nút có thể được thêm vào theo bốn cách:
 Ở phía trước của DLL
 Sau một nút nhất định.
 Ở cuối DLL
 Trước một nút nhất định.
 Thêm một nút ở phía trước:
Nút mới luôn được thêm vào trước phần đầu của Danh sách được liên kết
đã cho. Và nút mới được thêm vào sẽ trở thành phần đầu mới của DLL. Ví dụ:
nếu Danh sách được liên kết đã cho là 1->0->1->5 và chúng ta thêm một mục 5
ở phía trước, thì Danh sách được liên kết sẽ trở thành 5->1->0->1->5 . Chúng ta
sẽ gọi hàm thêm vào phía trước danh sách push(). Push() phải nhận một con trỏ
tới con trỏ đầu vì đẩy phải thay đổi con trỏ đầu để trỏ tới nút mới.

161
Chương 4. Cấu trúc dữ liệu

Hình 4. 7 Thêm một nút ở phía trước trong danh sách liên kết đôi

Ví dụ:

/* Given a reference (pointer to pointer)


to the head of a list
and an int, inserts a new node on the
front of the list. */
void push(Node** head_ref, int new_data)
{
/* 1. allocate node */
Node* new_node = new Node();

/* 2. put in the data */


new_node->data = new_data;

/* 3. Make next of new node as head


and previous as NULL */
new_node->next = (*head_ref);
new_node->prev = NULL;

/* 4. change prev of head node to new node */


if ((*head_ref) != NULL)
(*head_ref)->prev = new_node;

/* 5. move the head to point to the new node */


(*head_ref) = new_node;
}

162
Chương 4. Cấu trúc dữ liệu

 Độ phức tạp thời gian: O(1)


 Không gian phụ trợ: O(1)

Thêm một nút sau một nút đã cho:
Chúng ta được cung cấp một con trỏ tới một nút là prev_node và nút mới
được chèn sau nút đã cho.

Hình 4. 8 Thêm một nút ở phía sau một nút đã cho trong danh sách liên kết đôi

Dưới đây là cách thực hiện 7 bước để chèn một nút vào sau một nút đã
cho trong danh sách liên kết:

/* Given a node as prev_node, insert


a new node after the given node */
void insertAfter(Node* prev_node, int new_data)
{
/*1. check if the given prev_node is NULL */
if (prev_node == NULL) {
cout << "the given previous node cannot be NULL";
return;
}

/* 2. allocate new node */


Node* new_node = new Node();

/* 3. put in the data */


new_node->data = new_data;

163
Chương 4. Cấu trúc dữ liệu

/* 4. Make next of new node as next of prev_node */


new_node->next = prev_node->next;

/* 5. Make the next of prev_node as new_node */


prev_node->next = new_node;

/* 6. Make prev_node as previous of new_node */


new_node->prev = prev_node;

/* 7. Change previous of new_node's next node */


if (new_node->next != NULL)
new_node->next->prev = new_node;
}

 Độ phức tạp thời gian: O(1)


 Không gian phụ trợ: O(1)

Thêm một nút ở cuối:


Nút mới luôn được thêm vào sau nút cuối cùng của Danh sách liên kết đã
cho. Ví dụ: nếu DLL đã cho là 5->1->0->1->5->2 và chúng ta thêm mục 30 vào
cuối, thì DLL sẽ trở thành 5->1->0->1->5 ->2->30 . Vì một Danh sách Liên kết
thường được đại diện bởi phần đầu của nó, nên chúng ta phải duyệt qua danh
sách cho đến hết và sau đó thay đổi nút tiếp theo của nút cuối cùng thành nút
mới.

Hình 4. 9 Thêm một nút ở cuối trong danh sách liên kết đôi

164
Chương 4. Cấu trúc dữ liệu

Dưới đây là cách thực hiện 7 bước để chèn một nút vào cuối danh sách
liên kết:

/* Given a reference (pointer to pointer) to the head


of a DLL and an int, appends a new node at the end */
void append(Node** head_ref, int new_data)
{
/* 1. allocate node */
Node* new_node = new Node();

Node* last = *head_ref; /* used in step 5*/

/* 2. put in the data */


new_node->data = new_data;

/* 3. This new node is going to be the last node, so


make next of it as NULL*/
new_node->next = NULL;

/* 4. If the Linked List is empty, then make the new


node as head */
if (*head_ref == NULL) {
new_node->prev = NULL;
*head_ref = new_node;
return;
}

/* 5. Else traverse till the last node */


while (last->next != NULL)
last = last->next;

/* 6. Change the next of last node */


last->next = new_node;

165
Chương 4. Cấu trúc dữ liệu

/* 7. Make last node as previous of new node */


new_node->prev = last;

return;
}

 Độ phức tạp thời gian: O(n)


 Không gian phụ trợ: O(1)

Thêm một nút trước một nút đã cho:


Thực hiện theo các bước dưới đây để giải quyết vấn đề:
 Đặt con trỏ tới nút đã cho này là next_node và dữ liệu của nút mới được
thêm vào dưới dạng new_data.
 Kiểm tra xem next_node có phải là NULL hay không. Nếu là NULL, trả
về từ hàm vì không thể thêm bất kỳ nút mới nào trước NULL
 Cấp phát bộ nhớ cho nút mới, đặt tên là new_node
 Đặt new_node->data = new_data
 Đặt con trỏ trước đó của new_node này làm nút trước đó của next_node,
new_node->prev = next_node->prev
 Đặt con trỏ trước đó của next_node là new_node, next_node->prev =
new_node
 Đặt con trỏ tiếp theo của new_node này làm next_node, new_node->next
= next_node;
 Nếu nút trước đó của new_node không phải là NULL, thì đặt con trỏ tiếp
theo của nút trước đó là new_node, new_node->prev->next = new_node
 Mặt khác, nếu phần trước của new_node là NULL, thì đó sẽ là nút đầu
mới. Vì vậy, hãy tạo (*head_ref) = new_node.

166
Chương 4. Cấu trúc dữ liệu

Hình 4. 10 Thêm một nút trước một nút đã cho trong danh sách liên kết đôi

Sau đây là chương trình hoàn chỉnh để kiểm tra các chức năng trên:

// A complete working C++ program to


// demonstrate all insertion methods
#include <bits/stdc++.h>
using namespace std;

// A linked list node


class Node {
public:
int data;
Node* next;
Node* prev;
};

/* Given a reference (pointer to pointer)


to the head of a list
and an int, inserts a new node on the
front of the list. */
void push(Node** head_ref, int new_data)
{
/* 1. allocate node */
Node* new_node = new Node();

/* 2. put in the data */

167
Chương 4. Cấu trúc dữ liệu

new_node->data = new_data;

/* 3. Make next of new node as head


and previous as NULL */
new_node->next = (*head_ref);
new_node->prev = NULL;

/* 4. change prev of head node to new node */


if ((*head_ref) != NULL)
(*head_ref)->prev = new_node;

/* 5. move the head to point to the new node */


(*head_ref) = new_node;
}

/* Given a node as prev_node, insert


a new node after the given node */
void insertAfter(Node* prev_node, int new_data)
{
/*1. check if the given prev_node is NULL */
if (prev_node == NULL) {
cout << "the given previous node cannot be NULL";
return;
}

/* 2. allocate new node */


Node* new_node = new Node();

/* 3. put in the data */


new_node->data = new_data;

/* 4. Make next of new node as next of prev_node */


new_node->next = prev_node->next;

168
Chương 4. Cấu trúc dữ liệu

/* 5. Make the next of prev_node as new_node */


prev_node->next = new_node;

/* 6. Make prev_node as previous of new_node */


new_node->prev = prev_node;

/* 7. Change previous of new_node's next node */


if (new_node->next != NULL)
new_node->next->prev = new_node;
}

/* Given a reference (pointer to pointer) to the head


of a DLL and an int, appends a new node at the end */
void append(Node** head_ref, int new_data)
{
/* 1. allocate node */
Node* new_node = new Node();

Node* last = *head_ref; /* used in step 5*/

/* 2. put in the data */


new_node->data = new_data;

/* 3. This new node is going to be the last node, so


make next of it as NULL*/
new_node->next = NULL;

/* 4. If the Linked List is empty, then make the new


node as head */
if (*head_ref == NULL) {
new_node->prev = NULL;
*head_ref = new_node;
return;
}

169
Chương 4. Cấu trúc dữ liệu

/* 5. Else traverse till the last node */


while (last->next != NULL)
last = last->next;

/* 6. Change the next of last node */


last->next = new_node;

/* 7. Make last node as previous of new node */


new_node->prev = last;

return;
}

// This function prints contents of


// linked list starting from the given node
void printList(Node* node)
{
Node* last;
cout << "\nTraversal in forward direction \n";
while (node != NULL) {
cout << node->data << " ";
last = node;
node = node->next;
}

cout << "\nTraversal in reverse direction \n";


while (last != NULL) {
cout << last->data << " ";
last = last->prev;
}
}

// Driver code

170
Chương 4. Cấu trúc dữ liệu

int main()
{
/* Start with the empty list */
Node* head = NULL;

// Insert 6. So linked list becomes 6->NULL


append(&head, 6);

// Insert 7 at the beginning. So


// linked list becomes 7->6->NULL
push(&head, 7);

// Insert 1 at the beginning. So


// linked list becomes 1->7->6->NULL
push(&head, 1);

// Insert 4 at the end. So linked


// list becomes 1->7->6->4->NULL
append(&head, 4);

// Insert 8, after 7. So linked


// list becomes 1->7->8->6->4->NULL
insertAfter(head->next, 8);

cout << "Created DLL is: ";


printList(head);

return 0;
}

 Độ phức tạp thời gian: O(n)


 Không gian phụ trợ: O(1)

171
Chương 4. Cấu trúc dữ liệu

4.2. Ngăn xếp và hàng đợi


4.2.1. Ngăn xếp
a) Khái niệm
Ngăn xếp là một cấu trúc dữ liệu tuyến tính tuân theo một thứ tự cụ thể
trong đó các thao tác được thực hiện. Thứ tự có thể là LIFO(Last In First Out)
hoặc FILO(First In Last Out) .

Hình 4. 11 Cấu trúc của một ngăn xếp

Đó là một cấu trúc dữ liệu tuyến tính tuân theo một thứ tự cụ thể trong đó
các hoạt động được thực hiện. Để triển khai ngăn xếp, cần duy trì con trỏ tới
đỉnh ngăn xếp , đây là phần tử cuối cùng được chèn vào vì chúng ta chỉ có thể
truy cập các phần tử trên đỉnh ngăn xếp.
LIFO (Vào sau ra trước): Chiến lược này nói rằng phần tử được chèn vào
cuối cùng sẽ xuất hiện trước. Chúng ta có thể lấy một đống đĩa chồng lên nhau
làm ví dụ thực tế. Cái đĩa mà chúng ta đặt cuối cùng ở trên cùng và vì chúng ta
lấy cái đĩa ở trên cùng ra, nên chúng ta có thể nói rằng cái đĩa được đặt cuối
cùng sẽ xuất hiện trước.
b) Các thao tác cơ bản trên Stack
Để thực hiện các thao tác trong ngăn xếp, có một số thao tác nhất định
được cung cấp cho chúng tôi:
push() để chèn một phần tử vào ngăn xếp
pop() để xóa một phần tử khỏi ngăn xếp
top() Trả về phần tử trên cùng của ngăn xếp.
isEmpty() trả về true nếu ngăn xếp trống, ngược lại trả về false.
size() trả về kích thước của ngăn xếp.

172
Chương 4. Cấu trúc dữ liệu

Hình 4. 12 Các thao tác trên Stack

Push: Thêm một mục vào ngăn xếp. Nếu ngăn xếp đầy, thì nó được gọi là tình
trạng tràn. Thuật toán push:

bắt đầu
nếu ngăn xếp đầy
trở lại
phần cuối
khác
tăng hàng đầu
stack[top] gán giá trị
kết thúc khác
thủ tục kết thúc

Pop: Xóa một mục khỏi ngăn xếp. Các mục được bật ra theo thứ tự đảo ngược
mà chúng được đẩy. Nếu ngăn xếp trống, thì nó được gọi là tình trạng
Underflow .Thuật toán pop:

bắt đầu

173
Chương 4. Cấu trúc dữ liệu

nếu ngăn xếp trống


trở lại
phần cuối
khác
lưu trữ giá trị của ngăn xếp[top]
giảm hàng đầu
giá trị trả về
kết thúc khác
thủ tục kết thúc

Top: Trả về phần tử trên cùng của ngăn xếp. Thuật toán top:

bắt đầu
trả về ngăn xếp[top]
thủ tục kết thúc

Empty: Trả về true nếu ngăn xếp trống, ngược lại trả về false. Thuật toán:

bắt đầu
nếu đầu < 1
trả về đúng
khác
trả về sai
thủ tục kết thúc

Độ phức tạp:

174
Chương 4. Cấu trúc dữ liệu

Hình 4. 13 Độ phúc tạp của các thao tác trên stack

c) Các loại ngăn xếp:


 Ngăn xếp thanh ghi: Loại ngăn xếp này cũng là một thành phần bộ nhớ có
trong đơn vị bộ nhớ và chỉ có thể xử lý một lượng nhỏ dữ liệu. Chiều cao
của ngăn xếp thanh ghi luôn bị giới hạn vì kích thước của ngăn xếp thanh
ghi rất nhỏ so với bộ nhớ.
 Ngăn xếp bộ nhớ: Loại ngăn xếp này có thể xử lý một lượng lớn dữ liệu
bộ nhớ. Chiều cao của ngăn xếp bộ nhớ linh hoạt vì nó chiếm một lượng
lớn dữ liệu bộ nhớ.
d) Các ứng dụng của ngăn xếp:
 Chuyển đổi từ Infix sang Postfix /Prefix
 Tính năng redo-undo ở nhiều nơi như editor, photoshop.
 Các tính năng chuyển tiếp và lùi trong trình duyệt web
 Được sử dụng trong nhiều thuật toán như Tower of Hanoi, tree traversals,
stock span problems, và histogram problems.
 Quay lui là một trong những kỹ thuật thiết kế thuật toán. Một số ví dụ về
quay lui là bài toán Knight-Tour, bài toán N-Queen, tìm đường đi qua mê
cung và trò chơi giống như cờ vua hoặc cờ đam trong tất cả những bài
toán này chúng ta sẽ đi sâu vào một lúc nào đó nếu cách đó không hiệu
quả, chúng ta quay lại bài trước state và đi vào một số con đường khác.
Để quay lại từ trạng thái hiện tại, chúng ta cần lưu trữ trạng thái trước đó
cho mục đích đó, chúng ta cần một ngăn xếp.

175
Chương 4. Cấu trúc dữ liệu

 Trong các thuật toán đồ thị như sắp xếp tô pô và các thành phần được kết
nối mạnh mẽ
 Trong quản lý bộ nhớ, bất kỳ máy tính hiện đại nào cũng sử dụng ngăn
xếp làm quản lý chính cho mục đích chạy. Mỗi chương trình đang chạy
trong hệ thống máy tính có phân bổ bộ nhớ riêng
 Đảo chuỗi cũng là một ứng dụng khác của ngăn xếp. Tại đây, từng ký tự
một được chèn vào ngăn xếp. Vì vậy, ký tự đầu tiên của chuỗi nằm ở dưới
cùng của ngăn xếp và phần tử cuối cùng của chuỗi nằm ở trên cùng của
ngăn xếp. Sau khi thực hiện các thao tác bật lên trên ngăn xếp, chúng tôi
nhận được một chuỗi theo thứ tự ngược lại.
 Stack cũng giúp thực hiện lời gọi hàm trong máy tính. Hàm được gọi cuối
cùng luôn được hoàn thành trước.
 Ngăn xếp cũng được sử dụng để thực hiện thao tác hoàn tác/làm lại trong
trình soạn thảo văn bản.
e)Thực hiện ngăn xếp
Có hai cách để thực hiện một ngăn xếp:
 Sử dụng mảng
 Sử dụng danh sách liên kết
Triển khai Stack bằng mảng:

/* C++ program to implement basic stack


operations */
#include <bits/stdc++.h>

using namespace std;

#define MAX 1000

class Stack {
int top;

public:
int a[MAX]; // Maximum size of Stack

176
Chương 4. Cấu trúc dữ liệu

Stack() { top = -1; }


bool push(int x);
int pop();
int peek();
bool isEmpty();
};

bool Stack::push(int x)
{
if (top >= (MAX - 1)) {
cout << "Stack Overflow";
return false;
}
else {
a[++top] = x;
cout << x << " pushed into stack\n";
return true;
}
}

int Stack::pop()
{
if (top < 0) {
cout << "Stack Underflow";
return 0;
}
else {
int x = a[top--];
return x;
}
}
int Stack::peek()
{
if (top < 0) {

177
Chương 4. Cấu trúc dữ liệu

cout << "Stack is Empty";


return 0;
}
else {
int x = a[top];
return x;
}
}

bool Stack::isEmpty()
{
return (top < 0);
}

// Driver program to test above functions


int main()
{
class Stack s;
s.push(10);
s.push(20);
s.push(30);
cout << s.pop() << " Popped from stack\n";

//print top element of stack after popping


cout << "Top element is : " << s.peek() << endl;

//print all elements in stack :


cout <<"Elements present in stack : ";
while(!s.isEmpty())
{
// print top element in stack
cout << s.peek() <<" ";
// remove top element in stack
s.pop();

178
Chương 4. Cấu trúc dữ liệu

return 0;
}

Ưu điểm của việc thực hiện mảng:


 Dễ để thực hiện.
 Bộ nhớ được lưu dưới dạng con trỏ không liên quan.
Nhược điểm của việc thực hiện mảng:
 Nó không động, nghĩa là nó không phát triển và thu nhỏ tùy theo nhu cầu
khi chạy. [Nhưng trong trường hợp các mảng có kích thước động như
vector trong C++, list trong Python, ArrayList trong Java, ngăn xếp cũng
có thể tăng và giảm khi triển khai mảng].
 Tổng kích thước của ngăn xếp phải được xác định trước.
Triển khai Stack bằng danh sách liên kết:

// C++ program for linked list implementation of stack


#include <bits/stdc++.h>
using namespace std;

// A structure to represent a stack


class StackNode {
public:
int data;
StackNode* next;
};

StackNode* newNode(int data)


{
StackNode* stackNode = new StackNode();
stackNode->data = data;
stackNode->next = NULL;
return stackNode;
}

179
Chương 4. Cấu trúc dữ liệu

int isEmpty(StackNode* root)


{
return !root;
}

void push(StackNode** root, int data)


{
StackNode* stackNode = newNode(data);
stackNode->next = *root;
*root = stackNode;
cout << data << " pushed to stack\n";
}

int pop(StackNode** root)


{
if (isEmpty(*root))
return INT_MIN;
StackNode* temp = *root;
*root = (*root)->next;
int popped = temp->data;
free(temp);

return popped;
}

int peek(StackNode* root)


{
if (isEmpty(root))
return INT_MIN;
return root->data;
}

// Driver code

180
Chương 4. Cấu trúc dữ liệu

int main()
{
StackNode* root = NULL;

push(&root, 10);
push(&root, 20);
push(&root, 30);

cout << pop(&root) << " popped from stack\n";

cout << "Top element is " << peek(root) << endl;

cout <<"Elements present in stack : ";


//print all elements in stack :
while(!isEmpty(root))
{
// print top element in stack
cout << peek(root) <<" ";
// remove top element in stack
pop(&root);
}
return 0;
}

Ưu điểm của việc triển khai danh sách liên kết:


 Việc triển khai danh sách được liên kết của một ngăn xếp có thể tăng và
giảm theo nhu cầu khi chạy.
 Nó được sử dụng trong nhiều máy ảo như JVM.
Nhược điểm của việc triển khai Danh sách liên kết:
 Yêu cầu thêm bộ nhớ do có sự tham gia của con trỏ.
 Truy cập ngẫu nhiên là không thể trong ngăn xếp.
4.2.2. Hàng đợi
a) khái niệm

181
Chương 4. Cấu trúc dữ liệu

Hàng đợi được định nghĩa là cấu trúc dữ liệu tuyến tính mở ở cả hai đầu
và các thao tác được thực hiện theo thứ tự Nhập trước Xuất trước (FIFO).

Hình 4. 14 Cấu trúc của queue

Nguyên tắc hàng đợi FIFO:


 Hàng đợi giống như một hàng chờ mua vé, trong đó người đầu tiên xếp
hàng là người đầu tiên được phục vụ. (tức là đến trước phục vụ trước).
 Vị trí của mục nhập trong hàng đợi đã sẵn sàng để được phục vụ, nghĩa là
mục nhập đầu tiên sẽ bị xóa khỏi hàng đợi, được gọi là phần đầu của hàng
đợi(đôi khi, đầu hàng đợi), tương tự, vị trí của mục nhập cuối cùng trong
hàng đợi, nghĩa là cái được thêm vào gần đây nhất, được gọi là phía sau
(hoặc đuôi ) của hàng đợi. Xem hình bên dưới.

Hình 4. 15 Minh họa FIFO

b) Đặc điểm của hàng đợi:


 Hàng đợi có thể xử lý nhiều dữ liệu.
 Chúng tôi có thể truy cập cả hai đầu.

182
Chương 4. Cấu trúc dữ liệu

 Chúng nhanh và linh hoạt.


c)Biểu diễn hàm đợi
Hàng đợi cũng có thể được biểu diễn trong một mảng: Trong cách biểu
diễn này, Hàng đợi được triển khai bằng cách sử dụng mảng. Các biến được sử
dụng trong trường hợp này là:
 Queue: tên mảng lưu trữ các phần tử của queue.
 Front : chỉ mục nơi phần tử đầu tiên được lưu trữ trong mảng đại diện cho
hàng đợi.
 Rear: chỉ mục nơi phần tử cuối cùng được lưu trữ trong một mảng đại
diện cho hàng đợi.
Ví dụ tạo ra một hàm đợi bằng mảng:

// Creating an empty queue

// A structure to represent a queue


class Queue {
public:
int front, rear, size;
unsigned cap;
int* arr;
};

// Function to create a queue of given capacity


// It initializes size of queue as 0
Queue* createQueue(unsigned cap)
{
Queue* queue = new Queue();
queue->cap = cap;
queue->front = queue->size = 0;

queue->rear = cap - 1;
queue->arr = new int[(queue->cap * sizeof(int))];
return queue;
}

183
Chương 4. Cấu trúc dữ liệu

Biểu diễn danh sách liên kết của hàng đợi:


Một hàng đợi cũng có thể được biểu diễn bằng các thực thể sau:
 Danh sách liên kết,
 Con trỏ
 Cấu trúc.
Cú pháp:

struct QNode {
int data;
QNode* next;
QNode(int d)
{
data = d;
next = NULL;
}
};

struct Queue {
QNode *front, *rear;
Queue() { front = rear = NULL; }
};

d) Các loại hàng đợi:


Có nhiều loại hàng đợi khác nhau:
 Hàng đợi hạn chế đầu vào: Đây là một hàng đợi đơn giản. Trong loại hàng
đợi này, đầu vào chỉ có thể được lấy từ một đầu nhưng việc xóa có thể
được thực hiện từ bất kỳ đầu nào.
 Hàng đợi giới hạn đầu ra: Đây cũng là một hàng đợi đơn giản. Trong loại
hàng đợi này, đầu vào có thể được lấy từ cả hai đầu nhưng việc xóa chỉ có
thể được thực hiện từ một đầu.
 Hàng đợi tròn: Đây là một loại hàng đợi đặc biệt trong đó vị trí cuối cùng
được kết nối trở lại vị trí đầu tiên. Ở đây các thao tác cũng được thực hiện
theo thứ tự FIFO.

184
Chương 4. Cấu trúc dữ liệu

 Hàng đợi hai đầu (Dequeue): Trong hàng đợi hai đầu, thao tác thêm và
xóa có thể được thực hiện từ cả hai đầu.
 Hàng đợi ưu tiên: Hàng đợi ưu tiên là hàng đợi đặc biệt trong đó các phần
tử được truy cập dựa trên mức độ ưu tiên được gán cho chúng. Để biết
thêm tham khảo này.
e) Các thao tác cơ bản cho hàng đợi trong cấu trúc dữ liệu:
Một số thao tác cơ bản cho Hàng đợi trong Cấu trúc dữ liệu là:
 Enqueue() – Thêm (hoặc lưu trữ) một phần tử vào cuối hàng đợi..
 Dequeue() – Loại bỏ các phần tử khỏi hàng đợi.
 Peek() hoặc front()- Lấy phần tử dữ liệu có sẵn ở nút phía trước của hàng
đợi mà không xóa nó.
 rear() – Thao tác này trả về phần tử ở phía sau mà không xóa phần tử đó.
 isFull() – Xác thực nếu hàng đợi đầy.
 isNull() – Kiểm tra xem hàng đợi có trống không.

Enqueue(): Thao tác Enqueue() trong Hàng đợi thêm (hoặc lưu trữ) một phần tử
vào cuối hàng đợi .Cần thực hiện các bước sau để liệt kê (chèn) dữ liệu vào hàng
đợi:
 Bước 1: Kiểm tra xem hàng đợi đã đầy chưa.
 Bước 2: Nếu queue đầy trả về lỗi tràn và thoát.
 Bước 3: Nếu hàng đợi không đầy, hãy tăng con trỏ phía sau để trỏ đến
khoảng trống tiếp theo.
 Bước 4: Thêm phần tử dữ liệu vào vị trí hàng đợi, nơi phía sau đang chỉ.
 Bước 5: trả về thành công.

185
Chương 4. Cấu trúc dữ liệu

Hình 4. 16 Enqueue

Ví dụ:

void queueEnqueue(int data)


{
// Check queue is full or not
if (capacity == rear) {
printf("\nQueue is full\n");
return;
}

// Insert element at the rear


else {
queue[rear] = data;
rear++;
}
return;
}

Dequeue(): Xóa (hoặc truy cập) phần tử đầu tiên khỏi hàng đợi.

186
Chương 4. Cấu trúc dữ liệu

Các bước sau đây được thực hiện để thực hiện thao tác dequeue:
 Bước 1: Kiểm tra xem hàng đợi có trống không.
 Bước 2: Nếu hàng đợi trống, trả về lỗi tràn và thoát.
 Bước 3: Nếu hàng đợi không trống, hãy truy cập dữ liệu mà mặt trước
đang trỏ tới.
 Bước 4: Tăng con trỏ phía trước để trỏ đến phần tử dữ liệu có sẵn tiếp
theo.
 Bước 5: Return thành công.

Hình 4. 17 Dequeue

Ví dụ:

void queueDequeue()
{
// If queue is empty
if (front == rear) {
printf("\nQueue is empty\n");
return;
}

187
Chương 4. Cấu trúc dữ liệu

// Shift all the elements from index 2


// till rear to the left by one
else {
for (int i = 0; i < rear - 1; i++) {
queue[i] = queue[i + 1];
}

// decrement rear
rear--;
}
return;
}

Front(): Thao tác này trả về phần tử ở giao diện người dùng mà không xóa phần
tử đó.
Ví dụ:

// Function to get front of queue


int front(Queue* queue)
{
if (isempty(queue))
return INT_MIN;
return queue->arr[queue->front];
}

Rear(): Thao tác này trả về phần tử ở cuối phía sau mà không xóa phần tử đó.
Ví dụ:

// Function to get rear of queue


int rear(Queue* queue)
{
if (isEmpty(queue))
return INT_MIN;
return queue->arr[queue->rear];

188
Chương 4. Cấu trúc dữ liệu

isEmpty(): Thao tác này trả về một giá trị boolean cho biết hàng đợi có trống
hay không.
Ví dụ:

// This function will check whether


// the queue is empty or not:
bool isEmpty()
{
if (front == -1)
return true;
else
return false;
}

isFull(): Thao tác này trả về một giá trị boolean cho biết hàng đợi đã đầy hay
chưa.
Ví dụ:

// This function will check


// whether the queue is full or not.
bool isFull()
{
if (front == 0 && rear == MAX_SIZE - 1) {
return true;
}
return false;
}

f) Triển khai hàng đợi:


Hàng đợi có thể được triển khai bằng các cấu trúc dữ liệu sau:
 Triển khai Hàng đợi bằng Cấu trúc trong C/C++
 Triển khai Hàng đợi bằng Mảng

189
Chương 4. Cấu trúc dữ liệu

 Triển khai hàng đợi bằng danh sách liên kết


Ví dụ triển khai hàm đợi:

// Implementation of queue(enqueue, dequeue).


#include <bits/stdc++.h>
using namespace std;

class Queue {
public:
int front, rear, size;
unsigned cap;
int* arr;
};

Queue* createQueue(unsigned cap)


{
Queue* queue = new Queue();
queue->cap = cap;
queue->front = queue->size = 0;

queue->rear = cap - 1;
queue->arr = new int[(queue->cap * sizeof(int))];
return queue;
}

int isFull(Queue* queue)


{
return (queue->size == queue->cap);
}

int isempty(Queue* queue) { return (queue->size == 0); }


// Function to add an item to the queue.
// It changes rear and size.
void enqueue(Queue* queue, int item)

190
Chương 4. Cấu trúc dữ liệu

{
if (isFull(queue))
return;
queue->rear = (queue->rear + 1) % queue->cap;
queue->arr[queue->rear] = item;
queue->size = queue->size + 1;
cout << item << " enqueued to queue\n";
}
// Function to remove an item from queue.
// It changes front and size
int dequeue(Queue* queue)
{
if (isempty(queue))
return INT_MIN;
int item = queue->arr[queue->front];
queue->front = (queue->front + 1) % queue->cap;
queue->size = queue->size - 1;
return item;
}
int front(Queue* queue)
{
if (isempty(queue))
return INT_MIN;
return queue->arr[queue->front];
}
int rear(Queue* queue)
{
if (isempty(queue))
return INT_MIN;
return queue->arr[queue->rear];
}

// Driver code
int main()

191
Chương 4. Cấu trúc dữ liệu

{
Queue* queue = createQueue(1000);
enqueue(queue, 10);
enqueue(queue, 20);
enqueue(queue, 30);
enqueue(queue, 40);
cout << dequeue(queue);
cout << " dequeued from queue\n";
cout << "Front item is " << front(queue) << endl;
cout << "Rear item is " << rear(queue);
return 0;
}

 Độ phức tạp về thời gian: Tất cả các hoạt động có độ phức tạp về thời
gian O(1).
 Không gian phụ trợ: O(N)
g) Các ứng dụng của hàng đợi:
 Ứng dụng của hàng đợi là phổ biến. Trong một hệ thống máy tính, có thể
có hàng đợi các tác vụ đang chờ máy in, để truy cập vào bộ lưu trữ đĩa
hoặc thậm chí trong hệ thống chia sẻ thời gian, để sử dụng CPU. Trong
một chương trình, có thể có nhiều yêu cầu được giữ trong hàng đợi hoặc
một tác vụ có thể tạo ra các tác vụ khác, các tác vụ này phải được thực
hiện lần lượt bằng cách giữ chúng trong hàng đợi.
 Nó có một tài nguyên duy nhất và nhiều người tiêu dùng.
 Nó đồng bộ hóa giữa các thiết bị chậm và nhanh.
 Trong mạng, hàng đợi được sử dụng trong các thiết bị như bộ định
tuyến/bộ chuyển mạch và hàng đợi thư.
 Các biến thể: hàng đợi, hàng đợi ưu tiên và hàng đợi ưu tiên hai đầu.
4.2.3. Map và Set
Map là các thùng chứa liên kết lưu trữ các phần tử theo kiểu được ánh xạ.
Mỗi phần tử có một giá trị khóa và một giá trị được ánh xạ . Không có hai giá trị
được ánh xạ nào có thể có cùng giá trị khóa.
Set là một loại bộ chứa kết hợp trong đó mỗi phần tử phải là duy nhất vì
giá trị của phần tử xác định nó. Giá trị của phần tử không thể được sửa đổi sau

192
Chương 4. Cấu trúc dữ liệu

khi nó được thêm vào tập hợp, mặc dù có thể xóa và thêm giá trị đã sửa đổi của
phần tử đó.
Map các tập hợp trong STL: Bản đồ các tập hợp có thể rất hữu ích trong
việc thiết kế các thuật toán và cấu trúc dữ liệu phức tạp .
Cú pháp:

map<datatype, set<datatype>> map_of_set : lưu trữ một tập hợp các số tương
ứng với một số

hoặc

map<set<datatype>, datatype> map_of_set : lưu trữ một số tương ứng với một
bộ số

Ví dụ:

// C++ program to demonstrate use of map of set

#include <bits/stdc++.h>
using namespace std;

void show(map<int, set<string> >& mapOfSet)


{
// Using iterator to access
// key, value pairs present
// inside the mapOfSet
for (auto it = mapOfSet.begin();
it != mapOfSet.end();
it++) {

// Key is integer
cout << it->first << " => ";

// Value is a set of string

193
Chương 4. Cấu trúc dữ liệu

set<string> st = it->second;

// Strings will be printed


// in sorted order as set
// maintains the order
for (auto it = st.begin();
it != st.end(); it++) {
cout << (*it) << ' ';
}
cout << '\n';
}
}

// Driver code
int main()
{
// Declaring a map whose key
// is of integer type and
// value is a set of string
map<int, set<string> > mapOfSet;

// Inserting values in the


// set mapped with key 1
mapOfSet[1].insert("Geeks");
mapOfSet[1].insert("For");

// Set stores unique or


// distinct elements only
mapOfSet[1].insert("Geeks");

// Inserting values in the


// set mapped with key 2
mapOfSet[2].insert("Is");
mapOfSet[2].insert("The");

194
Chương 4. Cấu trúc dữ liệu

// Inserting values in the


// set mapped with key 3
mapOfSet[3].insert("Great");
mapOfSet[3].insert("Learning");

// Inserting values in the


// set mapped with key 4
mapOfSet[4].insert("Platform");

show(mapOfSet);

return 0;
}

4.3. Thư viện và các cấu trúc dữ liệu cơ bản


Standard Template Library - thư viện Template chuẩn của C++ có lẽ là một
trong những thứ mà các chúng ta học lập trình C++ được nghe tới rất nhiều. Nếu
như chúng ta đọc còn nhớ, ở các bài trước mình đã giới thiệu về khái
niệm Template - khuôn mẫu hàm. STL chính là một thư viện chứa những
template của cấu trúc dữ liệu cũng như thuật toán được xây dựng một cách tổng
quát nhất, nhằm hỗ trợ cho người dùng trong quá trình lập trình.
Thư viện STL vô cùng rộng lớn, gồm rất nhiều các template khác nhau.
Nhưng ta có thể chia STL làm 4 phần chính:

 Containers Library: Thư viện chứa các cấu trúc dữ liệu mẫu
như vector, stack, queue, deque, set, map,...
 Algorithm Library: Chứa các thuật toán viết sẵn để thao tác với dữ liệu.
 Iterator Library: Là các biến lặp, sử dụng để truy cập, duyệt các phần tử
dữ liệu của các containers. Về cơ bản, nó giống như các biến chạy trên dữ
liệu nhưng truy cập vào địa chỉ của dữ liệu. Dạng dễ hình dung nhất của
iterator là con trỏ, nhưng chúng ta đã bỏ qua con trỏ nên iterator sẽ được
đề cập trong từng containers.

195
Chương 4. Cấu trúc dữ liệu

 Numeric Library: Chứa các hàm toán học.

Để sử dụng thư viện STL, chúng ta cần khai báo không gian tên là using
namespace std;, sau đó khai báo thư viện cần dùng bằng cú pháp #include
<{Tên_thư_viện}>. Qua mỗi phiên bản của C++, lại có thêm những template
mới được bổ sung vào STL.

4.4. Bài tập Chương 4


Bài 1. Hãy cài đặt lớp Stack bằng cách sử dụng lớp Dlist (hoặc lớp Llist) làm
lớp cơ sở với dạng thừ kế private.
Bài 2. Cho ngăn xếp S chứa các phần tử. Sử dụng ngăn xếp T rỗng, hãy đưa ra
thuật toán (chỉ được sử dụng các phép toán ngăn xếp) thực hiện các nhiệm vụ
sau:
a. Đếm số phần tử trong ngăn xếp S, ngăn xếp S không thay đổi.
b. Loại bỏ tất cả các phần tử trong ngăn xếp S bằng một phần tử cho trước, thứ
tự các phần tử còn lại trong S không thay đổi.
Bài 3. Giả sử biểu thức dấu ngoặc chứa ba loại dấu ngoặc ( ), [ ], { }. Biểu thức [
( ) ( ) ] { } được xem là cân xứng, còn biểu thức {[( ] ) ( ) là không cân xứng.
Hãy đưa ra định nghĩa biểu thức dấu ngoặc cân xứng và đưa ra thuật toán cho
biết biểu thức dấu ngoặc có cân xứng hay không.
Bài 4. Áp dụng thuật toán chuyển biểu thức dạng infix thành biểu thức dạng
postfix ,hãy chuyển các biểu thức infix sau thành biểu thức postfix, cần chỉ ra
nội dung của ngăn xếp sau mỗi bước của thuật toán.
a. A / B /C – (D + E) * F
b. A – ( B + C * D) / E
c. A * (B / C / D) + E
Bài 5. Thiết kế thuật toán đoán nhận các xâu ký tự có dạng w $ w’, trong đó w’
là đảo ngược của xâu w, chẳng hạn nếu w = a c d b thì w’ = b d c a.
Bài 6. Cho đỉnh A (đỉnh xuất phát) và đỉnh B (đỉnh đích) trong đồ thị định
hướng G (đồ thị có thể có chu trình). Sử dụng ngăn xếp, hãy thiết kế thuật toán
tìm đường đi từ A đến B. (Sử dụng ngăn xếp để lưu vết của đường đi từ A đến
B).
Bài 7. Sử dụng các ngăn xếp, hãy đưa ra thuật toán không đệ quy cho bài
toán tháp Hà Nội.

196
Chương 4. Cấu trúc dữ liệu

Bài 8 Để định nghĩa một cấu trúc sinh viên có tên là Sinhvien, gồm có tên và
tuổi sinh viên. Định nghĩa nào sau đây là đúng:
a. struct Sinhvien{
char name[20];
int age;
};
b. struct {
char name[20];
int age;
} Sinh vien;
c. typedef struct Sinhvien{
char name[20];
int age;
};
Bài 9 Một cấu trúc được định nghĩa như sau:
struct Employee{
char name[20];
int age;
};
Khi đó, cách khai báo biến nào sau đây là đúng:
a. struct Employee myEmployee;
b. struct employee myEmployee;
c. Employee myEmployee;
d. employee myEmployee;
Bài 10 Một cấu trúc được định nghĩa như sau:
typedef struct employee{
char name[20];
int age;
} Employee;
Khi đó, cách khai báo biến nào sau đây là đúng:
a. Employee myEmployee;
b. employee myEmployee;
c. struct Employee myEmployee;
d. struct employee myEmployee;

197
Chương 4. Cấu trúc dữ liệu

Bài 11 Với cấu trúc được định nghĩa như trong bài 3. Khi đó, cách khởi tạo biến
nào sau đây là đúng:
a. Employee myEmployee = {‘A’, 27};
b. Employee myEmployee = {“A”, 27};
c. Employee myEmployee = (‘A’, 27);
d. Employee myEmployee = (“A”, 27);
Bài 12 Với cấu trúc được định nghĩa như trong bài 3. Khi đó, các cách cấp phát
bộ nhớ cho biến
con trỏ nào sau đây là đúng:
a. Employee *myEmployee = new Employee;
b. Employee *myEmployee = new Employee();
c. Employee *myEmployee = new Employee(10);
d. Employee *myEmployee = new Employee[10];
Bài 13 Định nghĩa một cấu trúc về môn học của một học sinh có tên Subject,
bao gồm các thông tin:
• Tên môn học, kiểu char[];
• Điểm tổng kết môn học, kiểu float;
Bài 14 Định nghĩa cấu trúc về học sinh tên là Student bao gồm các thông tin sau:
• Tên học sinh, kiểu char[];
• Tuổi học sinh, kiểu int;
• Lớp học sinh, kiểu char[];
• Danh sách điểm các môn học của học sinh, kiểu là một mảng các cấu
trúc Subject
đã được định nghĩa trong bài tập 6.
• Xếp loại học lực, kiểu char[];
Bài 15. Khai báo một biến có cấu trúc là Student đã định nghĩa trong bài 7. Sau
đó, thực hiện tính điểm trung bình của tất cả các môn học của học sinh đó, và
viết một thủ tục xếp loại học sinh dựa vào điểm trung bình các môn học:
• Nếu điểm tb nhỏ hơn 5.0, xếp loại kém
• Nếu điểm tb từ 5.0 đến dưới 6.5, xếp loại trung bình.
• Nếu điểm tb từ 6.5 đến dưới 8.0, xếp loại khá
• Nếu điểm tb từ 8.0 trở lên, xếp loại giỏi.

198
Chương 4. Cấu trúc dữ liệu

Bài 16. Viết một chương trình quản lí các học sinh của một lớp, là một dãy các
cấu trúc có kiểu Stupid định nghĩa trong bài 7. Sử dụng thủ tục đã cài đặt trong
bài 8 để thực hiện các thao tác sau:
• Khởi tạo danh sách và điểm của các học sinh trong lớp.
• Tính điểm trung bình và xếp loại cho tất cả các học sinh.
• Tìm tất cả các học sinh theo một loại nhất định
Bài 17. Sử dụng cấu trúc ngăn xếp đã định nghĩa trong bài để đổi một số từ kiểu
thập phân sang kiểu nhị phân: Chi số nguyên cho 2, mãi cho đến khi thương <2,
lưu các số dư váo ngăn xếp. Sau đó, đọc các giá trị dư từ ngăn xếp ra, ta sẽ thu
được chuỗi nhị phân tương ứng.
Bài 18. Mở rộng cấu trúc hàng đợi đã định nghĩa trong bài để trở thành hàng đợi
có độ ưu tiên:
• Cho mỗi node thêm một thuộc tính là độ ưu tiên của node đó
• Khi thêm một node vào hàng đợi, thay vì thêm vào cuối hàng đợi như
thông
thường, ta tìm vị trí có độ ưu tiên phù hợp để chèn node vào, sao cho dãy
các node trong hàng đợi là một danh sách có độ ưu tiên của các node là
giảm dần.
• Việc lấy ra là không thay đổi: lấy ra phần tử ở đầu hàng đợi, chính là
phần tử có độ ưu tiên cao nhất.
Bài 19. Áp dụng hàng đợi có độ ưu tiên trong bài 11 để xây dựng chương trình
quản lí tiến trình có độ ưu tiên của hệ điều hành, mở rộng ứng dụng trong bài
ngăn xếp.
Bài 20. Mở rộng cấu trúc danh sách liên kết đơn trong bài thành danh sách liên
kết kép:
• Mỗi node có thêm một con trỏ prev để trỏ đến node trước nó
• Đối với node header, cũng cần 2 con trỏ: trỏ đến node đầu tiên và node
cuối cùng của danh sách
. Riêng với node đầu tiên (front) của danh sách, con trỏ prev của nó sẽ trỏ
đến
NULL. Giống như con trỏ next của node rear.

199
Chương 5. Cải thiện mã nguồn

CHƯƠNG 5. CẢI THIỆN MÃ NGUỒN


5.1. Hợp tác
Phong cách lập trình (Programming Style) được hiểu là tư tưởng xuyên
suốt quá trình phát triển phần mềm của người lập trình viên. Họ sẽ viết lên
những mã nguồn cho các chương trình máy tính. Programming Style được gọi là
tốt khi người lập trình viên có thể viết mã tốt, dễ đọc. Không chỉ vậy, muốn thực
thi chương trình thành công, người viết mã phải đảm bảo mã nguồn dễ đọc, dễ
sửa, người lập trình khác cũng có thể hiểu.
Phong cách của người lập trình ảnh hưởng mật thiết đến sự dễ hiểu của
chương trình gốc. Khi xây dựng và phát triển phần mềm, người lập trình phải
đảm bảo chương trình dễ hiểu. Lý do là bởi các chương trình luôn cần nâng cấp
và sửa đổi. Khi chương trình được đơn giản hóa, việc bảo trì sẽ đỡ mất nhiều
thời gian, tiết kiệm chi phí hơn.

Phong cách viết mã chương trình tốt là phong cách có thể khắc phục được
những nhược điểm của ngôn ngữ lập trình khác. Ở những dự án nhỏ, phong cách
của người lập trình thường không được quan tâm. Tuy nhiên khi thực hiện
những dự án quy mô lớn và có độ phức tạp cao, muốn phát triển chương trình
nhanh và thành công đòi hỏi người lập trình phải có phong cách và kỹ thuật lập
trình tốt.

Khi xây dựng Programming Style, các lập trình viên nên cố gắng tạo sự dễ
hiểu. Chương trình viết ra phải dễ đọc, khi lập trình viên khác nhìn vào cũng có
thể hiểu mới là một chương trình có tính khả thi cao, giúp tiết kiệm chi phí khi
cần bảo trì và nâng cấp.

Để viết lên những chương trình dễ đọc dễ hiểu, lập trình viên phải trang bị
những kiến thức cơ bản về phong cách cũng như kỹ thuật lập trình. Một số kỹ
thuật và phong cách đơn giản phải kể đến là:

 Cách đặt tên cho biến hàm: Ưu tiên đặt tên thể hiện bao quát ý nghĩa.
Tên phải ngắn gọn và giàu tính mô tả và có thể phát âm được. Có như vậy
developer mới dễ dàng debug. Đây là khâu cơ bản nhưng đóng vai trò cực
kỳ quan trọng.

200
Chương 5. Cải thiện mã nguồn

 Phong cách viết mã nguồn: Nên dùng kích thước chung là 4 và 8, giúp
chương trình có bố cục hợp lý, dễ theo dõi. Chúng ta có thể tham khảo các
phong cách khác từ những người có kinh nghiệm để rút ra cách viết mã
nguồn chuẩn.
 Viết câu lệnh: Ưu tiên câu lệnh có mô tả cấu trúc. Cố gắng làm đơn giản
hóa các lệnh, hạn chế các cấu trúc phức tạp. Như vậy, việc sửa lại lệnh khi
cần sẽ đơn giản hơn rất nhiều.
 Hàm biến và cục bộ: Nên chia nhỏ chương trình thành các hàm. Khi viết
hàm không nên viết quá dài.
 Khâu xử lý lỗi: Phải có tính nhất quán trong cách xử lý lỗi, thống nhất
cách phân loại lỗi và định dạng thông báo.

5.2. kiểm thử


Kiểm thử quá trình kiểm tra chương trình có đáp ứng các chức năng theo
yêu cầu ban đầu đưa ra không.

Quá trình kiểm thử như sau:


 Kiểm thử (Testing): chỉ ra các vấn đề làm chương trình không chạy
 Kiểm tra theo cấu trúc của chương trình: Kiểm tra việc thực hiện các
nhiệm vụ đặt ra cho từng phần chương trình
 Nếu chương trình không có tham số đầu vào, mà chỉ thực thi nhiệm vụ và
sinh ra kết quả thì không cần kiểm tra nhiều.
 Hầu hết chương trình đều không như vậy

Cấu trúc kiểm thử:


 Kiểm tra bất biến - Testing invariants
 Kiểm tra các thuộc tính lưu trữ -Verifying conservation properties
 Kiểm tra các giá trị trả về - Checking function return values
 Tạm thay đổi code - Changing code temporarily
 Giữ nguyên mã thử nghiệm - Leaving testing code intact

*Kiểm thử lớp:


Kiểm thử đơn vị là mức kiểm thử thấp nhất được thực hiện trong quá trình
phát triển phần mềm. Kiểm thử đơn vị tận tâm sẽ phát hiện nhiều vấn đề ở giai

201
Chương 5. Cải thiện mã nguồn

đoạn phát triển nơi chúng có thể được sửa chữa một cách kinh tế. Đối với phần
mềm được phát triển bằng C++, lớp là đơn vị nhỏ nhất để kiểm thử đơn vị. Một
lớp được thiết kế tốt sẽ cung cấp một sự trừu tượng, với các chi tiết triển khai
được ẩn trong lớp. Các đối tượng của các lớp như vậy có thể khó kiểm tra đơn vị
kỹ lưỡng. Bài báo này thảo luận về các vấn đề liên quan đến các lớp C++ kiểm
thử đơn vị, trình bày các chiến lược giải quyết các vấn đề này. Một ví dụ chi tiết
về bài kiểm tra đơn vị cho lớp C++, sử dụng công cụ kiểm tra Cantata của IPL,
được bao gồm trong đĩa.

Kiểm thử đơn vị là mức kiểm tra thấp nhất được thực hiện trong quá trình
phát triển phần mềm. Các đơn vị phần mềm riêng lẻ được kiểm tra tách biệt với
các phần khác của chương trình. Kiểm thử đơn vị thường nhằm mục đích đạt
được phạm vi quyết định 100% của mã trong một đơn vị (mặc dù các biện pháp
bảo hiểm khác cũng có thể được sử dụng). Trong phần này, thuật ngữ chung
"phạm vi bảo hiểm" được sử dụng để chỉ bất kỳ biện pháp bảo hiểm nào (phạm
vi quyết định hoặc khác) được thông qua.

Kinh nghiệm cho thấy rằng một cách tiếp cận tận tâm đối với kiểm thử
đơn vị sẽ phát hiện ra nhiều vấn đề ở giai đoạn phát triển phần mềm, nơi chúng
có thể được sửa chữa một cách kinh tế. Trong các giai đoạn sau của quá trình
phát triển phần mềm, việc phát hiện và sửa chữa các vấn đề khó khăn hơn, tốn
nhiều thời gian và chi phí hơn. Trong một ngôn ngữ lập trình có cấu trúc thông
thường, chẳng hạn như C, đơn vị được kiểm tra theo truyền thống là hàm hoặc
thủ tục con. Để kiểm tra một đơn vị như vậy trong sự cô lập, các đơn vị chương
trình bên ngoài được gọi bởi đơn vị được kiểm tra và dữ liệu bên ngoài được sử
dụng bởi đơn vị được kiểm tra phải được mô phỏng. Lớp này là tính năng mới
chính của ngôn ngữ C++ so với C. Mục đích chính của nó là cung cấp khả năng
đóng gói trong một đối tượng. Nói chung, một lớp C++ phức tạp hơn một hàm C
và có nhiều phạm vi đa dạng hơn. Do đó, chiến lược thử nghiệm được thông qua
nên linh hoạt hơn. Nó nên nhằm mục đích thiết lập các hướng dẫn chứ không
phải các quy tắc.

Một lớp chứa cả hàm thành viên và dữ liệu, thể hiện mức độ liên kết nội
bộ mạnh mẽ. Các thành viên chỉ liên quan đến việc triển khai nội bộ của lớp

202
Chương 5. Cải thiện mã nguồn

được gọi là riêng tư. Các thành viên như vậy được đóng gói hoàn toàn trong lớp
và không hiển thị đối với các phần chương trình bên ngoài. Mức độ liên kết cao
được thể hiện bởi các thành viên của một lớp làm cho việc tách các chức năng
thành viên khỏi một lớp để kiểm tra từng chức năng thành viên một cách riêng
biệt là không thực tế. Do đó, lớp là thành phần cơ bản được kiểm tra khi phần
mềm dựa trên đối tượng thử nghiệm đơn vị được viết bằng C++. Các vấn đề kết
quả rơi vào hai nhóm. Đầu tiên, việc kiểm tra một lớp phải kỹ lưỡng, nhưng việc
thiếu khả năng hiển thị của phần riêng tư của một lớp có nghĩa là khó có thể đạt
được mức độ bao quát đầy đủ của một số chức năng thành viên, đặc biệt là các
chức năng thành viên riêng tư. Khu vực vấn đề thứ hai bắt nguồn từ mục đích
kiểm tra đơn vị một lớp tách biệt với các phần khác của chương trình. Để cô lập
một lớp để thử nghiệm, các lớp khác phải được mô phỏng. Vấn đề này, và các
giải pháp.

Kiểm tra đơn vị của một lớp không phải là hoạt động một lần. Các bài
kiểm tra đơn vị phải được lặp lại khi lớp được sửa đổi hoặc được sử dụng trong
một môi trường mới. Sau khi kiểm tra đơn vị, các lớp phải được tích hợp để
cung cấp hệ thống phần mềm tổng thể. Cấu trúc của một lớp và các mối quan hệ
qua lại giữa các thành viên của một lớp dẫn đến một lớp là một đơn vị phức tạp
hơn nhiều so với một chức năng hoặc thủ tục con đơn lẻ. Đạt được phạm vi
kiểm tra đầy đủ của một số thành viên trong lớp có thể khó khăn. Đặc biệt khó
đạt được phạm vi kiểm tra đầy đủ của các hàm thành viên riêng của một lớp vì
chúng không thể nhìn thấy hoặc truy cập trực tiếp bên ngoài lớp.

b) Phương pháp kiểm thử


Một giải pháp là sửa đổi cấu trúc một lớp để thử nghiệm bằng cách làm
cho tất cả các thành viên của nó có thể truy cập được từ bên ngoài, nhưng bất kỳ
sửa đổi tạm thời nào cho mục đích thử nghiệm đều là điều không mong muốn.
Một sửa đổi có thể che khuất các vấn đề lẽ ra phải được phát hiện bởi bài kiểm
tra đơn vị và cũng có thể gây ra các vấn đề mới. Việc sửa đổi tạm thời (hoặc
vĩnh viễn) một lớp để tạo điều kiện kiểm tra sẽ thay đổi hiệu quả toàn bộ bối
cảnh của lớp, đánh bại mục đích thiết kế ban đầu là đóng gói các chi tiết trong
lớp. Tương tự như các phương pháp thiết kế khác, khi sử dụng các kỹ thuật thiết

203
Chương 5. Cải thiện mã nguồn

kế hướng đối tượng, một kỹ sư phần mềm nên tính đến việc kiểm tra một lớp
trong thiết kế của nó. Khi sử dụng C++, điều này có nghĩa là định nghĩa của một
lớp phải bao gồm các phương tiện mà lớp đó sẽ được kiểm tra. Trong thực tế,
việc khai báo một hàm thành viên hiển thị bên ngoài trong một định nghĩa lớp
có thể được sử dụng để tạo điều kiện kiểm tra đơn vị của lớp.
Ví dụ sau cho thấy cách một phương thức thử nghiệm có thể được kết hợp
vào một lớp C++.

class example_class
{
class example_class
{
// Public member functions public:
int add (); // sums x and y
int subtract(); // subtracts y from x
void PrintLargest(); // Prints greatest of x or y

// Test Method
friend void test();

// Private member functions


private:
int largest(); // Returns largest of x or y

// Private member data


int x;
int y;
};

Phương thức kiểm thử có thể là một hàm thành viên công khai của lớp.
Tuy nhiên, tốt hơn nên khai báo nó như một hàm chúng ta của lớp (như trong ví
dụ 1), bởi vì phương thức này thực sự khởi tạo các đối tượng của lớp hơn là tác
động lên một đối tượng cụ thể. Tất cả các lớp có thể có cùng một phương thức
kiểm tra như một hàm chúng ta bè, do đó, quyền truy cập vào các thành viên

204
Chương 5. Cải thiện mã nguồn

riêng của bất kỳ lớp nào đều có sẵn từ phương thức kiểm tra. Điều này có thể
hữu ích trong quá trình thử nghiệm và tích hợp ở cấp độ cao hơn. Ví dụ 1- Các
thành viên riêng và chức năng kiểm thử.

c)Kịch bản thử nghiệm


Để kiểm tra một lớp, một tệp được gọi là tập lệnh kiểm tra được tạo, chứa
mã cho phương thức kiểm tra được khai báo trong khai báo lớp. Các trường hợp
thử nghiệm trong phương thức thử nghiệm phải khởi tạo các đối tượng thuộc
loại lớp, cung cấp dữ liệu để đạt được đường dẫn mong muốn thông qua các
hàm thành viên và kiểm tra dữ liệu so với các giá trị dự kiến ở cuối mỗi lần thực
hiện trường hợp thử nghiệm.

Các trường hợp kiểm thử nên được thiết kế để đạt được phạm vi bao quát
đầy đủ của tất cả các chức năng trong lớp. Các trường hợp thử nghiệm ban đầu
nên sử dụng các hàm thành viên công khai, bao gồm các hàm tạo. Mục tiêu là để
đạt được phạm vi kiểm tra càng nhiều (cả cấu trúc và chức năng) của tất cả các
thành viên của lớp càng tốt, mà không cần truy cập trực tiếp vào các chức năng
thành viên riêng. Nếu cần, các trường hợp thử nghiệm tiếp theo có thể truy cập
trực tiếp vào các chức năng thành viên riêng để cho phép hoàn thành phạm vi
thử nghiệm. Mỗi trường hợp thử nghiệm nên được thiết kế để hoạt động độc lập
với các trường hợp thử nghiệm khác. Một ca kiểm thử thông thường sẽ bắt đầu
bằng cách khởi tạo một đối tượng thuộc loại lớp và kết thúc bằng cách xóa nó.
Phương thức được kiểm tra sẽ được gọi sau khi dữ liệu thành viên lớp đã được
thiết lập. Điều này có thể được thực hiện trực tiếp từ tập lệnh thử nghiệm hoặc
gián tiếp bằng cách gọi các phương thức đã thử nghiệm trước đó. Tất cả dữ liệu
lớp nên được kiểm tra trước khi xóa đối tượng.

Điều này sẽ đòi hỏi nhiều công việc hơn một chút và sẽ yêu cầu các tập
lệnh thử nghiệm dài hơn so với phương án cho phép các trường hợp thử nghiệm
dựa trên dữ liệu từ các trường hợp thử nghiệm trước đó. Tuy nhiên, nó cung cấp
một số lợi thế:
 Các trường hợp thử nghiệm riêng lẻ mạnh mẽ hơn. Lỗi trong một
trường hợp thử nghiệm sẽ không dẫn đến lỗi thứ cấp trong các trường
hợp tiếp theo.

205
Chương 5. Cải thiện mã nguồn

 Các kịch bản thử nghiệm dễ bảo trì hơn. Các ca kiểm thử có thể dễ
dàng được chèn, di chuyển, thay đổi hoặc xóa mà không ảnh hưởng đến
các ca kiểm thử xung quanh.
 Mỗi chức năng thành viên được kiểm tra hiệu quả trong sự cô lập,
nhưng trong môi trường tổng thể của lớp.
 Đạt được phạm vi bảo hiểm cấu trúc hoàn chỉnh đơn giản hơn.

Kiểm thử chức năng của một lớp có thể đạt được với ít công việc hơn
bằng cách thiết kế các trường hợp kiểm thử dựa trên kết quả của các trường hợp
kiểm thử trước đó. Tuy nhiên, tất cả những lợi thế trên sẽ sớm bị mất đi. Có thể
thực tế hơn để đạt được các mục tiêu kiểm tra chức năng và cấu trúc bằng cách
gọi nhiều hơn một phương thức trong một trường hợp thử nghiệm. Tuy nhiên,
dữ liệu thành viên vẫn nên được kiểm tra sau mỗi cuộc gọi.

d) Rò rỉ đống
Một vấn đề quá phổ biến trong các hệ thống C++ hướng đối tượng là rò rỉ
đống. Hiện tượng này xảy ra khi bộ nhớ được phân bổ để sử dụng, nhưng không
được giải phóng khi không còn cần thiết. Dần dần, ngày càng nhiều không gian
heap bị tiêu tốn do sự 'rò rỉ' này cho đến khi hệ thống gặp sự cố nghiêm trọng.
Thực hành thiết kế tốt rõ ràng sẽ bảo vệ chống rò rỉ đống và thử nghiệm đơn vị
có thể được sử dụng để củng cố điều này. Bài kiểm tra đơn vị cho một lớp phải
đảm bảo rằng không có không gian heap nào được phân bổ ở cuối mỗi bài kiểm
tra.

e) Kiểm thử một lớp trong sự cô lập


Để kiểm thử một đơn vị trong sự cô lập, các đơn vị xung quanh phải được
mô phỏng bằng thử nghiệm. Đối với các lớp C++, điều này có nghĩa là môi
trường thử nghiệm sẽ phải mô phỏng toàn bộ các lớp.Một câu hỏi rõ ràng là "Tại
sao đơn vị kiểm tra một lớp trong sự cô lập, trong khi nó sẽ phải được tích hợp
với các đơn vị khác? Tại sao không sử dụng các lớp thực?". Một hệ thống hướng
đối tượng được thiết kế tốt nên có một hệ thống phân cấp lớp có cấu trúc tốt. Do
đó, có thể lập kế hoạch kiểm tra đơn vị để kiểm tra các lớp trừu tượng nhất trước
tiên, sau đó tiến hành thông qua hệ thống phân cấp, đảm bảo rằng mỗi lớp mới
được kiểm tra chỉ yêu cầu các lớp đã được kiểm tra đầy đủ. Hình thức kiểm tra

206
Chương 5. Cải thiện mã nguồn

đơn vị đơn giản này loại bỏ mọi nhu cầu mô phỏng (sơ khai) các lớp bên ngoài
lớp được kiểm tra.

f) Kiểm thử lớp phân cấp


Để khám phá thêm khả năng kiểm tra lớp phân cấp, hãy xem xét hệ thống
phân cấp kế thừa lớp. Giả sử các lớp này đã được kiểm tra bằng cách sử dụng
kiểm tra lớp phân cấp. Một kế hoạch thử nghiệm sẽ phải xem xét những điều sau
đây:
 Để kiểm tra lớp b202, các lớp bl0l và b0 cần phải được kiểm tra.
 Để kiểm tra lớp a301, chúng ta cần tính đến thực tế là lớp al02, từ đó
lớp a301 được dẫn xuất, chứa một đối tượng của lớp b201.
 Do đó, để kiểm tra lớp a301, các lớp a201, al0l, a0, b201, bl0l và b0
cần phải được kiểm tra.

Ngăn chặn xảy ra khi một thành viên của một lớp là một đối tượng của
một lớp khác. Trong ví dụ 2, lớp a102 chứa một đối tượng của lớp b201. Việc sử
dụng hoặc sử dụng sai chức năng ngăn chặn, cùng với tính kế thừa, là một vấn
đề thiết kế và nằm ngoài phạm vi của báo cáo này. Tuy nhiên, việc sử dụng ngăn
chặn hợp pháp dẫn đến một lớp từ một nhánh của hệ thống phân cấp thừa kế có
chứa một đối tượng từ một nhánh khác. Sự ngăn chặn thường làm phức tạp thêm
các mối quan hệ giữa các lớp.

Ví dụ 2 - Hệ thống phân cấp kế thừa lớp điển hình:

207
Chương 5. Cải thiện mã nguồn

Hình 5. 1 Hệ thống phân cấp kế thừa lớp điển hình

g) Các lớp kiểm thử cách ly


Trong kiểm thử cô lập, các lớp được kiểm thử độc lập với nhau. Điều này
có nghĩa là tất cả các lớp được yêu cầu bởi một lớp được kiểm tra, do tính kế
thừa, ngăn chặn hoặc là kết quả của một đối tượng toàn cục được sử dụng trong
một hàm thành viên, phải được mô phỏng. Cách thực tế để quản lý việc kiểm tra
các lớp một cách cô lập là tạo và duy trì một bộ các lớp sơ khai cùng với các
triển khai thực của chúng. Mỗi sơ khai lớp chỉ định một sơ khai cho từng chức
năng thành viên của lớp, cho phép các chức năng thành viên được mô phỏng từ
tập lệnh kiểm tra:
 Để xác định rằng hàm thành viên đã được gọi tại một điểm dự kiến.
 Để cho phép kiểm tra các tham số cho hàm thành viên dựa trên các giá
trị dự kiến.
 Để trả về các giá trị được yêu cầu bởi từng trường hợp thử nghiệm.

Bộ sơ khai lớp này được gọi là "thư viện sơ khai". Trước khi kiểm tra một
lớp, sơ khai lớp cho mỗi lớp yêu cầu mô phỏng được sao chép từ thư viện sơ
khai vào tệp sơ khai kiểm tra. Đối với một lớp có tính dẫn xuất cao, nhiều lớp sẽ
yêu cầu mô phỏng và do đó tệp sơ khai kiểm tra sẽ lớn. Tuy nhiên, vì mỗi tệp sơ

208
Chương 5. Cải thiện mã nguồn

khai kiểm tra được tạo bằng cách sao chép sơ khai từ thư viện sơ khai, nên việc
tạo ra nó không phải là một nhiệm vụ đặc biệt khó khăn hoặc tốn thời gian.

Trong thực tế, ranh giới giữa kiểm thử cô lập các lớp và kiểm thử lớp
phân cấp đôi khi bị vượt qua do sự thuận tiện trong thực tế. Một số lớp cơ bản
tương đối đơn giản và độc lập (ví dụ: một lớp triển khai các chuỗi). Các lớp cơ
sở như vậy có thể dự đoán được ở chỗ chức năng của chúng dễ dàng được nhận
ra và không có khả năng thay đổi (các lớp này có thể được coi là tương tự với
các loại C++ được xác định trước). Khi đã kiểm tra đơn vị, có thể thuận lợi khi
sử dụng các lớp đó trực tiếp trong kiểm tra đơn vị của các lớp khác, thay vì mô
phỏng chúng. Một cách tiếp cận tương tự có thể được áp dụng cho các thói quen
thư viện, mô phỏng hoặc sử dụng các thói quen đã được thử nghiệm trước đó
nếu có thể.

l) Bảo trì kiểm tra đơn vị


Thử nghiệm cấp độ đơn vị không chỉ dành cho mục đích sử dụng phát
triển một lần, để hỗ trợ mã hóa không có lỗi. Các bài kiểm tra đơn vị nên được
lặp lại bất cứ khi nào một lớp được sửa đổi hoặc được sử dụng trong một môi
trường mới. Do đó, tất cả các bài kiểm tra đơn vị phải được duy trì trong suốt
vòng đời của phần mềm. Có thể phân tách một hệ thống thành các đơn vị cơ bản
của nó và lặp lại tất cả các bài kiểm tra đơn vị mà không có lỗi tại bất kỳ thời
điểm nào trong vòng đời của hệ thống. Việc duy trì các bài kiểm tra đặc biệt
quan trọng đối với phần mềm hướng đối tượng, mục đích cụ thể là khuyến khích
việc sử dụng lại phần mềm giữa các dự án khác nhau. Để một dự án mới sử
dụng lại một lớp, nó phải tin tưởng rằng lớp đó sẽ hoạt động trong môi trường
của dự án mới. Chúng ta có thể đạt được sự tự tin này bằng cách chạy lại bài
kiểm tra cho lớp.

Cam kết bảo trì thực hiện một chi phí chung. Khi phát triển phần mềm
bằng C, nếu giao diện của một đơn vị bị thay đổi, thì đơn vị đó và tất cả các đơn
vị gọi đơn vị đó phải lặp lại các thử nghiệm của chúng. Tuy nhiên, trong phát
triển C++ dựa trên đối tượng, đơn vị cho mục đích thử nghiệm là lớp và nếu
giao diện của một lớp bị thay đổi thì tất cả các thử nghiệm trên các lớp yêu cầu
lớp đã sửa đổi phải được lặp lại. Điều này thường thể hiện khối lượng kiểm tra

209
Chương 5. Cải thiện mã nguồn

lại lớn hơn đáng kể. Trong phần này đã cho thấy rằng mối quan hệ giữa các lớp
có thể hơi gián tiếp. Nếu một thay đổi được thực hiện đối với giao diện của một
lớp cơ sở, thì lượng kiểm tra lại cần thiết có thể là đáng kể. (Tuy nhiên, không
thể chấp nhận 'bodge' một lớp để tránh thay đổi giao diện của nó). Do đó, điều
rất quan trọng khi phát triển một hệ thống dựa trên đối tượng là có được giao
diện đúng giữa các lớp. Việc giảm thiểu nhu cầu thay đổi giao diện sẽ là kết quả
của thiết kế tốt, điều này sẽ thực thi số lượng đóng gói tối đa, hạn chế các thành
viên có thể nhìn thấy bên ngoài của một lớp ở mức tối thiểu.

Thiết kế hướng đối tượng là một quá trình lặp đi lặp lại nhiều hơn so với
các kỹ thuật thông thường. Chọn đúng đối tượng sẽ dẫn đến giao diện ổn định.
Chọn các đối tượng không phù hợp sẽ dẫn đến giao diện không ổn định và nỗ
lực bảo trì quá mức. Đây là nguyên nhân chính gây lo ngại cho các nhà phát
triển phần mềm khi xem xét hướng đối tượng, ủng hộ niềm tin rằng giai đoạn
thiết kế của phát triển hướng đối tượng nên chiếm tỷ lệ lớn hơn trong tổng nỗ
lực phát triển so với trường hợp thông thường.

i)Kiểm thử tích hợp phần mềm hướng đối tượng


Phần này đã xác định một chiến lược thử nghiệm mức đơn vị cho các phát
triển C++ dựa trên đối tượng dựa trên lớp C++ như là đơn vị thử nghiệm cơ bản.
Tuy nhiên, kiểm tra ở cấp độ cao hơn các đơn vị (các lớp cách ly) cũng rất cần
thiết. Với phần mềm hướng đối tượng, không có lý do gì khiến điều này phải
khác biệt đáng kể so với các phương pháp thử nghiệm cấp cao hơn được áp
dụng khi sử dụng các phương pháp phát triển phần mềm khác. Thử nghiệm như
vậy về cơ bản ở dạng chứng minh mối quan hệ giữa các đối tượng và chức năng
hệ thống tổng thể.

Trong một hệ thống hướng đối tượng lớn, phần mềm thường được chia
thành các tập hợp các lớp liên quan đến chức năng, thường được gọi là các loại
lớp. Các danh mục lớp có thể được kiểm tra chức năng tách biệt với nhau, sử
dụng sơ khai lớp được tạo để kiểm tra cấp đơn vị khi mô phỏng được yêu cầu.
Đối với một hệ thống chỉ bao gồm một vài danh mục lớp, việc tích hợp hệ thống
có thể được thực hiện trong một giai đoạn 'vụ nổ lớn' duy nhất - tích hợp tất cả
các danh mục lớp trong một lần, khi tất cả chúng đã được kiểm tra đầy đủ. Tuy

210
Chương 5. Cải thiện mã nguồn

nhiên, đối với các hệ thống phức tạp hơn, cách tiếp cận dần dần để tích hợp sẽ
thực tế hơn. Bản dựng tích hợp ban đầu của hệ thống có thể bao gồm một số loại
lớp được kiểm tra đầy đủ, với phần còn lại của hệ thống được mô phỏng. Các
thử nghiệm chức năng trên phần này của hệ thống sau đó có thể được thực hiện
và giải quyết các vấn đề. Quá trình tích hợp tiến triển với các bản dựng tiếp theo
thay thế ngày càng nhiều các lớp mô phỏng bằng các triển khai thực tế của
chúng.Cách tiếp cận tích hợp này rất linh hoạt về khoảng thời gian, cho phép bắt
đầu tích hợp trước khi tất cả các danh mục lớp (hoặc thậm chí cả các lớp) được
kiểm tra đầy đủ.
5.3. Gỡ lỗi
Gỡ lỗi là quá trình tìm lỗi và sửa lỗi trong chương trình. Quá trình gỡ lỗi thường
yêu cầu các bước sau:
 Nhận dạng lỗi Các lập trình viên, người kiểm thử và người dùng
cuối báo cáo lỗi mà họ phát hiện ra trong khi kiểm thử hoặc sử dụng
phần mềm. Các lập trình viên xác định vị trí chính xác dòng mã hoặc
mô-đun mã gây ra lỗi. Công việc này có thể rất nhàm chán và tốn
thời gian.
 Phân tích lỗi Người viết mã phân tích lỗi bằng cách ghi lại tất cả các
thay đổi trạng thái chương trình và giá trị dữ liệu. Họ cũng ưu tiên
sửa lỗi dựa trên tác động của lỗi đối với chức năng phần mềm. Đội
ngũ xây dựng phần mềm cũng xác định một mốc thời gian để sửa lỗi
tùy thuộc vào mục tiêu và yêu cầu phát triển.
 Sửa lỗi và xác thực Các lập trình viên sửa lỗi và chạy thử nghiệm để
đảm bảo phần mềm tiếp tục hoạt động như mong đợi. Họ có thể viết
các bài kiểm thử mới để kiểm tra xem lỗi có tái diễn trong tương lai
hay không.
Khi gỡ lỗi một chương trình, trong hầu hết các trường hợp, phần lớn thời
gian của chúng ta sẽ được dành để cố gắng tìm ra lỗi thực sự ở đâu. Sau khi tìm
thấy sự cố, các bước còn lại (khắc phục sự cố và xác thực rằng sự cố đã được
khắc phục) thường không đáng kể.

a) Các cách tìm kiếm lỗi


*Tìm kiếm vấn đề thông qua kiểm tra code

211
Chương 5. Cải thiện mã nguồn

Giả sử chúng ta đã nhận thấy một vấn đề và chúng ta muốn theo dõi
nguyên nhân của vấn đề cụ thể đó. Trong nhiều trường hợp (đặc biệt là trong các
chương trình nhỏ hơn), chúng ta có thể nhanh chóng tìm hiểu về vấn đề. Hãy
xem xét đoạn chương trình sau:

int main()
{
getNames(); // ask user to enter a bunch of names
sortNames(); // sort them in alphabetical order
printNames(); // print the sorted list of names

return 0;
}

Nếu chúng ta mong đợi chương trình này sẽ in các tên theo thứ tự bảng chữ
cái, nhưng thay vào đó, nó đã in chúng theo thứ tự ngược lại, vấn đề có thể nằm
ở hàm sortNames. Trong trường hợp chúng ta có thể thu hẹp vấn đề xuống một
hàm cụ thể, chúng ta có thể phát hiện ra vấn đề chỉ bằng cách xem code. Tuy
nhiên, khi các chương trình trở nên phức tạp hơn, việc tìm kiếm các vấn đề bằng
cách kiểm tra code cũng trở nên phức tạp hơn. Đầu tiên, có rất nhiều mã để xem
xét. Nhìn vào mỗi dòng code trong một chương trình dài hàng nghìn dòng có thể
mất một thời gian rất dài (chưa kể nó vô cùng nhàm chán). Thứ hai, bản thân
code có xu hướng phức tạp hơn, với nhiều nơi có thể xảy ra sự cố. Thứ ba, hành
vi của code có thể không cung cấp cho chúng ta nhiều manh mối về việc mọi
thứ sẽ sai ở đâu. Nếu chúng ta đã viết một chương trình để đưa ra các khuyến
nghị về lỗi và nó thực sự không tạo ra gì cả, có lẽ chúng ta sẽ không có nhiều cơ
hội để bắt đầu tìm kiếm vấn đề. Cuối cùng, lỗi có thể được gây ra bằng cách đưa
ra các giả định xấu. Hầu như không thể phát hiện ra một lỗi trực quan gây ra bởi
một giả định xấu, bởi vì chúng ta có thể đưa ra giả định tồi tệ tương tự khi kiểm
tra code và không nhận thấy lỗi. Vì vậy, nếu chúng ta có một vấn đề mà chúng
ta không thể tìm thấy thông qua kiểm tra code, làm thế nào để chúng ta tìm thấy
nó?

*Tìm kiếm sự cố bằng cách chạy chương trình

212
Chương 5. Cải thiện mã nguồn

May mắn thay, nếu chúng ta không thể tìm thấy sự cố thông qua kiểm tra
code, có một cách khác chúng ta có thể thực hiện: chúng ta có thể xem hành vi
của chương trình khi chương trình chạy và cố gắng chẩn đoán sự cố từ đó. Cách
tiếp cận này có thể được khái quát như:
 Tìm hiểu làm thế nào để tái tạo vấn đề
 Chạy chương trình và thu thập thông tin để thu hẹp nơi xảy ra sự cố
 Lặp lại bước trước cho đến khi chúng ta tìm thấy vấn đề
Đối với phần còn lại của chương này, chúng ta sẽ thảo luận về các kỹ thuật
để tạo thuận lợi cho phương pháp này.

*Tái tạo vấn đề


Bước đầu tiên và quan trọng nhất trong việc tìm ra vấn đề là có thể tái tạo
vấn đề. Lý do rất đơn giản: cực kỳ khó tìm ra vấn đề trừ khi chúng ta có thể
quan sát nó xảy ra. Quay lại với sự tương tự của máy làm đá của chúng ta – giả
sử một ngày nào đó chúng ta của chúng ta nói với chúng ta rằng máy làm đá của
chúng ta không hoạt động. Chúng ta đi để xem xét nó, và nó hoạt động tốt. Làm
thế nào chúng ta sẽ chẩn đoán vấn đề? Nó sẽ rất khó khăn. Tuy nhiên, nếu chúng
ta thực sự có thể thấy vấn đề của máy làm đá không hoạt động, thì chúng ta có
thể bắt đầu chẩn đoán tại sao nó không hoạt động hiệu quả.

Nếu một sự cố phần mềm là trắng trợn (ví dụ: chương trình gặp sự cố ở
cùng một nơi mỗi khi chúng ta chạy nó) thì việc tái tạo vấn đề có thể là chuyện
nhỏ. Tuy nhiên, đôi khi tái tạo một vấn đề có thể khó khăn hơn rất nhiều. Sự cố
chỉ có thể xảy ra trên một số máy tính nhất định hoặc trong một số trường hợp
cụ thể (ví dụ: khi người dùng nhập một số đầu vào nhất định). Trong những
trường hợp như vậy, việc tạo ra một tập hợp các bước tái tạo có thể hữu ích. Các
bước sinh sản là một danh sách các bước rõ ràng và chính xác có thể được theo
dõi để khiến vấn đề tái diễn với mức độ dự đoán cao. Mục tiêu là có thể khiến
vấn đề tái diễn càng nhiều càng tốt, vì vậy chúng ta có thể chạy chương trình
của mình nhiều lần và tìm kiếm manh mối để xác định nguyên nhân gây ra sự
cố. Nếu vấn đề có thể được sao chép 100% thời gian, đó là lý tưởng, nhưng khả
năng tái tạo dưới 100% có thể ổn. Một vấn đề chỉ xảy ra 50% thời gian có nghĩa
là sẽ mất gấp đôi thời gian để chẩn đoán sự cố, vì một nửa thời gian chương

213
Chương 5. Cải thiện mã nguồn

trình sẽ không xuất hiện vấn đề và do đó không đóng góp bất kỳ thông tin chẩn
đoán hữu ích nào.

*Tìm hiểu về các vấn đề


Một khi chúng ta có thể tái tạo vấn đề một cách hợp lý, bước tiếp theo là
tìm ra vấn đề ở đâu trong code. Dựa trên bản chất của vấn đề, điều này có thể dễ
hoặc khó. Vì lợi ích của ví dụ, giả sử chúng ta không có nhiều ý tưởng về vấn đề
thực sự là gì. Làm thế nào để chúng ta tìm thấy nó? Một sự tương tự sẽ phục vụ
chúng ta tốt ở đây. Hãy chơi một trò chơi hi-lo. Việc yêu cầu chúng ta đoán một
số trong khoảng từ 1 đến 10. Với mỗi lần đoán chúng ta thực hiện, tôi sẽ cho
chúng ta biết liệu mỗi lần đoán quá cao, quá thấp hay đúng. Một ví dụ của trò
chơi này có thể trông như thế này:

You: 5
Me: Too low
You: 8
Me: Too high
You: 6
Me: Too low
You: 7
Me: Correct

Trong trò chơi trên, chúng ta không cần phải đoán mọi số để tìm số tôi
đang nghĩ đến. Thông qua quá trình đoán và xem xét thông tin chúng ta học
được từ mỗi lần đoán, chúng ta có thể về nhà trong số đúng với chỉ một vài lần
đoán (nếu chúng ta sử dụng một chiến lược tối ưu, chúng ta luôn có thể tìm thấy
số tôi nghĩ đến trong 4 lần đoán hoặc ít hơn).

Chúng ta có thể sử dụng một quy trình tương tự để gỡ lỗi chương trình.
Trong trường hợp xấu nhất, chúng ta có thể không biết lỗi ở đâu. Tuy nhiên,
chúng ta biết rằng vấn đề phải nằm ở đâu đó trong đoạn code thực thi giữa lúc
bắt đầu chương trình và điểm mà chương trình biểu hiện triệu chứng không
chính xác đầu tiên mà chúng ta có thể quan sát được. Điều đó ít nhất loại trừ các
phần của chương trình thực thi sau triệu chứng quan sát đầu tiên. Nhưng điều đó

214
Chương 5. Cải thiện mã nguồn

vẫn có khả năng để lại rất nhiều code. Để chẩn đoán vấn đề, chúng ta sẽ đưa ra
một số phỏng đoán đã học về vấn đề đang ở đâu, với mục tiêu nhanh chóng giải
quyết vấn đề. Thông thường, bất cứ điều gì đã khiến chúng ta nhận thấy vấn đề
sẽ cho chúng ta một dự đoán ban đầu gần với vấn đề thực sự. Ví dụ: nếu chương
trình không ghi dữ liệu vào một tệp khi cần, thì vấn đề có thể nằm ở đâu đó
trong code xử lý ghi vào file (duh!). Sau đó, chúng ta có thể sử dụng chiến lược
giống như hi-lo để thử và cách ly vấn đề thực sự ở đâu.

Ví dụ: Nếu tại một thời điểm nào đó trong chương trình của chúng ta,
chúng ta có thể chứng minh rằng sự cố chưa xảy ra, thì điều này tương tự với
việc nhận được kết quả hi-lo quá thấp – chúng ta biết vấn đề phải ở đâu đó trong
chương trình. Ví dụ: nếu chương trình của chúng ta bị sập ở cùng một nơi mọi
lúc và chúng ta có thể chứng minh rằng chương trình không bị sập tại một thời
điểm cụ thể trong quá trình thực thi chương trình, thì sự cố phải nằm sau code.
Nếu tại một thời điểm nào đó trong chương trình của chúng ta, chúng ta có thể
quan sát hành vi không chính xác liên quan đến vấn đề, thì điều này tương tự với
việc nhận kết quả hi-lo quá cao và chúng ta biết vấn đề phải ở đâu đó sớm hơn
trong chương trình. Ví dụ: giả sử một chương trình in giá trị của một số biến x .
Chúng ta đang mong đợi nó in giá trị 2 , nhưng nó đã in 8 thay thế. Biến x phải
có giá trị sai. Nếu tại một thời điểm nào đó trong quá trình thực thi chương trình
của chúng ta, chúng ta có thể thấy biến x đã có giá trị 8 , thì chúng ta biết vấn đề
phải xảy ra trước thời điểm đó.

Sự tương tự hi-lo không hoàn hảo – đôi khi chúng ta cũng có thể loại bỏ
toàn bộ các phần của code của chúng ta khỏi sự cân nhắc mà không thu được bất
kỳ thông tin nào về vấn đề thực tế là trước hay sau thời điểm đó. Cuối cùng, với
đủ dự đoán và một số kỹ thuật tốt, chúng ta có thể tìm ra dòng chính xác gây ra
vấn đề!.
Chiến lược đoán nào chúng ta muốn sử dụng tùy thuộc vào chúng ta –
chiến lược tốt nhất phụ thuộc vào loại lỗi đó, vì vậy chúng ta có thể muốn thử
nhiều cách tiếp cận khác nhau để thu hẹp vấn đề. Khi chúng ta có được kinh
nghiệm trong việc gỡ lỗi, trực giác của chúng ta sẽ giúp hướng dẫn chúng ta.
Vậy làm thế nào để chúng ta làm ra những trò đoán Có nhiều cách để làm như
vậy. Chúng ta sẽ bắt đầu với một số cách tiếp cận đơn giản trong chương tiếp

215
Chương 5. Cải thiện mã nguồn

theo, và sau đó chúng ta sẽ xây dựng những điều này và khám phá những cách
khác trong các chương.

b) Các chiến thuật sửa lỗi trong C++


*Chiến thuật gỡ lỗi 1: Comment code.
Hãy bắt đầu với một điều dễ dàng. Nếu chương trình thể hiện hành vi sai
lầm, một cách để giảm số lượng code chúng ta phải tìm kiếm là comment một số
code và xem vấn đề còn tồn tại không. Nếu vấn đề vẫn còn, code comment sẽ
không gây ra lỗi. Hãy xem xét các code sau đây:

int main()
{
getNames(); // ask user to enter a bunch of names
doMaintenance(); // do some random stuff
sortNames(); // sort them in alphabetical order
printNames(); // print the sorted list of names

return 0;
}

Giả sử chương trình này được cho là in tên người dùng nhập theo thứ tự
bảng chữ cái, nhưng chương trình này in chúng theo thứ tự bảng chữ cái ngược.
Vấn đề ở đâu? Là getNames nhập tên không chính xác? Là sortNames sắp xếp
chúng ngược? Là printNames in chúng ngược? Nó có thể là bất cứ thứ gì trong
số đó. Nhưng chúng ta có thể nghi ngờ doMaintenance() không liên quan đến
vấn đề này, vì vậy hãy comment nó lại.

int main()
{
getNames(); // ask user to enter a bunch of names
// doMaintenance(); // do some random stuff
sortNames(); // sort them in alphabetical order
printNames(); // print the sorted list of names

216
Chương 5. Cải thiện mã nguồn

return 0;
}

Nếu vấn đề biến mất, thì doMaintenance phải gây ra vấn đề và chúng ta
nên tập trung chú ý vào đó. Tuy nhiên, nếu sự cố vẫn còn (có nhiều khả năng),
thì chúng ta biết doMaintenance không có lỗi và chúng ta có thể loại trừ toàn bộ
hàm khỏi tìm kiếm của chúng ta. Điều này không giúp chúng ta hiểu được vấn
đề thực sự là trước hay sau cuộc gọi đến doMaintenance , nhưng nó làm giảm số
lượng code mà chúng ta phải xem qua sau đó. Đừng quên những hàm chúng ta
đã comment để chúng ta có thể bỏ qua chúng sau này!

*Chiến thuật gỡ lỗi 2: Xác thực luồng code


Một vấn đề phổ biến khác trong các chương trình phức tạp hơn là chương
trình đang gọi một hàm quá nhiều hoặc quá ít lần (bao gồm cả không). Trong
các trường hợp như vậy, có thể hữu ích khi đặt các câu lệnh ở đầu các hàm của
chúng ta để in tên của hàm. Theo cách đó, khi chương trình chạy, chúng ta có
thể thấy các hàm nào đang được gọi.

Khi in thông tin cho mục đích gỡ lỗi, hãy sử dụng std :: cerr thay vì std ::
cout. Một lý do cho điều này là std :: cout có thể được đệm, điều đó có nghĩa là
có thể có một khoảng dừng giữa khi chúng ta hỏi std :: cout để xuất thông tin và
khi nào nó thực sự xảy ra. Nếu chúng ta xuất bằng std :: cout và sau đó chương
trình của chúng ta gặp sự cố ngay lập tức, std :: cout có thể có hoặc chưa thực sự
xuất ra. Điều này có thể đánh lừa chúng ta về vấn đề là ở đâu. Mặt khác, std ::
cerr không có bộ đệm, có nghĩa là bất cứ điều gì chúng ta gửi đến nó sẽ xuất ra
ngay lập tức. Điều này giúp đảm bảo tất cả đầu ra gỡ lỗi xuất hiện càng sớm
càng tốt (với chi phí của một số hiệu suất, điều mà chúng ta thường không quan
tâm khi gỡ lỗi).

Hãy xem xét chương trình đơn giản sau đây không hoạt động chính xác:

#include <iostream>

int getValue()

217
Chương 5. Cải thiện mã nguồn

{
return 4;
}

int main()
{
std::cout << getValue;

return 0;
}

Mặc dù chúng ta hy vọng chương trình này sẽ in giá trị 4, nhưng nó thực
sự sẽ in các giá trị khác nhau trên các máy khác nhau. Trên máy của tác giả, nó
được in: 00101424. Hãy thêm một số câu lệnh gỡ lỗi vào các hàm này:

#include <iostream>

int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}

int main()
{
std::cerr << "main() called\n";
std::cout << getValue;

return 0;
}

Khi thêm các câu lệnh gỡ lỗi tạm thời, có thể hữu ích để không thụt lề
chúng. Điều này làm cho chúng dễ dàng hơn để tìm cách loại bỏ sau này. Bây
giờ khi các hàm này thực thi, chúng sẽ xuất tên của chúng, biểu thị rằng chúng

218
Chương 5. Cải thiện mã nguồn

được gọi: main() called 00101424. Nhận thấy rằng hàm getValue không bao giờ
được gọi. Phải có một số vấn đề với code gọi hàm. Chúng ta hãy xem xét kỹ hơn
về dòng đó:
std::cout << getValue;

#include <iostream>

int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}

int main()
{
std::cerr << "main() called\n";
std::cout << getValue(); // added parenthesis here

return 0;
}

Điều này bây giờ sẽ tạo ra đầu ra chính xác: main() called, getValue()
called, 4. Và chúng ta có thể loại bỏ các câu lệnh gỡ lỗi tạm thời. Với một số
loại lỗi, chương trình có thể tính toán hoặc chuyển sai giá trị. Chúng ta cũng có
thể xuất giá trị của các biến (bao gồm các tham số) hoặc biểu thức để đảm bảo
rằng chúng là chính xác. Hãy xem xét chương trình sau đây được cho là thêm
hai số nhưng không hoạt động chính xác:

#include <iostream>

int add(int x, int y)


{
return x + y;
}

219
Chương 5. Cải thiện mã nguồn

void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}

int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
int x{ getUserInput() };
int y{ getUserInput() };

std::cout << x << " + " << y << '\n';

int z{ add(x, 5) };
printResult(z);

return 0;
}

Đây là một số đầu ra từ chương trình này:

Enter a number: 4
Enter a number: 3
4+3
The answer is: 9

220
Chương 5. Cải thiện mã nguồn

Điều đó không đúng. Liệu có thấy lỗi không? Ngay cả trong chương trình
ngắn này, có thể khó phát hiện ra. Hãy thêm một số code để gỡ lỗi các giá trị:

#include <iostream>

int add(int x, int y)


{
return x + y;
}

void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}

int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';

std::cout << x << " + " << y << '\n';

221
Chương 5. Cải thiện mã nguồn

int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
printResult(z);

return 0;
}

Đây là đầu ra trên:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
4+3
main::z = 9
The answer is: 9

Các biến x và y đang nhận được các giá trị đúng, nhưng biến z thì không.
Vấn đề phải nằm giữa hai điểm đó, điều này làm cho hàm add là một nghi ngờ
chính. Hãy sửa đổi chức năng thêm:

#include <iostream>

int add(int x, int y)


{
std::cerr << "add() called (x=" << x <<", y=" << y << ")" << '\n';
return x + y;
}

void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}

222
Chương 5. Cải thiện mã nguồn

int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';

std::cout << x << " + " << y << '\n';

int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
printResult(z);

return 0;
}

Bây giờ chúng ta sẽ nhận được đầu ra:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
add() called (x=4, y=5)
main::z = 9
The answer is: 9

223
Chương 5. Cải thiện mã nguồn

Biến y có giá trị 3, nhưng bằng cách nào đó, hàm add của chúng ta có giá
trị 5 cho tham số y. Chúng ta phải thông qua lập luận sai. Đảm bảo đủ: int z{
add(x, 5) }; Nó đây rồi Chúng ta đã lấy 5 thay vì giá trị của biến y làm đối số.
Đó là một sửa chữa dễ dàng, và sau đó chúng ta có thể loại bỏ các câu lệnh gỡ
lỗi.
5.4. Case Study
Case study đề cập đến việc kiểm tra chuyên sâu về một sự kiện hoặc cá
nhân hoặc một nhóm cá nhân cụ thể. Nó giống một phương pháp nghiên cứu
định tính hơn, trong đó nó hiểu các vấn đề phức tạp bằng cách quan sát và phân
tích sâu sắc sự kiện hoặc tình huống bằng cách thu thập và báo cáo dữ liệu liên
quan đến sự kiện hoặc tình huống đó. Nghiên cứu trường hợp nghiên cứu hướng
tới mô tả hơn là tìm ra nguyên nhân và kết quả ngay lập tức. Case study được
phân loại thành ba cách tức là khám phá, giải thích và mô tả dựa trên phương
pháp nghiên cứu liên quan đến cả dữ liệu định lượng và định tính. Loại nghiên
cứu này có thể được sử dụng để giải quyết các vấn đề dựa vào cộng đồng như
mù chữ, thất nghiệp, nghèo đói và nghiện ma túy.
5.5. Bài tập chương
Bài 1. Kiểm thử là gì? Nên các thao tác trong kiểm thử chương trình.
Bài 2. Trình bày các phương pháp kiểm thử. So sánh ưu và nhược điểm của từng
nhóm phương pháp
Bài 3. Gỡ lỗi là gì? Qúa trình gỡ lỗi thường trải qua mấy yêu cầu? Trình bày nội
dung của từng nhóm yêu cầu.
Bài 4. Trình bày các cánh tìm kiếm lỗi và sửa lỗi trong một chương trình?
Bài 5. Có bao nhiêu chiến thuật sửa lỗi trong C++. Trình bày nội dung của từng
chiến thuật.
Bài 6. Trình bày nội dung của case study?

224
Tài liệu tham khảo

TÀI LIỆU THAM KHẢO

[1] A.Kosowski, M. Malafiejski, T. Noinski “Application of an Online Judge


& Contester System in Academic Tuition”, 2008.
[2] S. Wasik, M. Antczak, J. Badura, A. Laskowski, T. Sternal, “A Survey on
Online Judge Systems and Their Applications”, ACM Comp. Survey, 2016.
[3] “Computer Programming - TF”, [Online]. Available:
http://www.laptrinh.ptit.vn [Accessed 21 Nov 2022]
[4] S. Meyers. Effective C++. Addison - Wesley, Reeding, Mass., 2d
ed.,1998.

[5] E. M. Reingole, J.Nievergelt and N. Deo. Combinatorial Algorithms:


Theory and Practice. Prentice – Hall, Englewood Cliffs, NJ, 1977.

[6] H.Samet. The Design and Analysis of Spatial Data Structures.


Addison - Wesley, Reeding, Mass., 1989.R.Sedgewick. Algorithms. Addison -
Wesley, Reeding, Mass., 2d ed.,1988.

[7] R.Sedgewick. Algorithms in C++. Addison - Wesley, Reeding, Mass.,


1992.
[8] Đinh Mạnh Tường. Cấu Trúc Dữ Liệu và Thuật Toán. Nhà xuất bản
Khoa Học và Kỹ Thuật, Hà nội, in lần thứ 3, 2003.

[9] M.A. Weiss. Data Structures and Algorithm Analysis in C ++. Addison -
Wesley, Reeding, Mass., 2d ed., 1999.

[10] M.A. Weiss. Data Structures and Problem Solving using C ++.Addison -
Wesley, Reeding, Mass., 2d ed., 2000.

225

You might also like