Professional Documents
Culture Documents
BG Kĩ Thuật Lập Trình 2022
BG Kĩ Thuật Lập Trình 2022
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ộ)
Hà nội - 11/2022
MỤC LỤC
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 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
IV
DANH MỤC BẢNG BIỂU
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
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
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.
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.
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
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.
10
Chương 1. Giới thiệu chung
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.
12
Chương 2. Lập trình cơ bản
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
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ộ.
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:
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.
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 .
Để 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.
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:
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
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
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
7 wchar_t
Kiểu dữ liệu ký tự có phạm vi rộng
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
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í dụ:
#include <iostream>
using namespace std;
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;
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ị;
#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
Toán tử Ý nghĩa
+ Cộng
- Trừ
* Nhân
/ Chia
-- Giảm 1 đơn vị
++ Tăng 1 đơn vị
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++.
27
Chương 2. Lập trình cơ bản
≠ != Khác
28
Chương 2. Lập trình cơ bản
|| OR
! NOT
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;
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:
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
#include <bits/stdc++.h>
31
Chương 2. Lập trình cơ bản
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.
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];
return 0;
}
34
Chương 2. Lập trình cơ bản
int main(){
//con trỏ ptr trỏ đến địa chỉ đầu tiên của mảng
int arr[10] {}, *ptr {arr};
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;
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}};
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
int main()
{
int n;
int* arr;
cout << "The number of integers is: " << endl;
cin >> n;
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
Ví dụ về hàm:
41
Chương 2. Lập trình cơ bản
42
Chương 2. Lập trình cơ bản
43
Chương 2. Lập trình cơ bản
*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.
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
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ụ:
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;
}
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++
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
Đố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++
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:
Để 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
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.
50
Chương 2. Lập trình cơ bản
{
int x, y;
} p1; // The variable p1 is declared with 'Point'
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
#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;
#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 };
return 0;
}
#include <iostream>
using namespace std;
struct Point {
int x, y;
};
int main()
{
// Create an array of structures
struct Point arr[10];
53
Chương 2. Lập trình cơ bản
return 0;
}
#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;
54
Chương 2. Lập trình cơ bản
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
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
Ví dụ minh họa:
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");
}
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:
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;
}
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
Ví dụ minh họa:
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;
}
if (condition)
statement;
else if (condition)
statement;
.
.
else
statement;
* Sơ đồ
62
Chương 2. Lập trình cơ bản
Ví dụ minh họa:
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";
}
Ví du:
64
Chương 2. Lập trình cơ bản
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
#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;
}
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.
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
68
Chương 2. Lập trình cơ bản
Ví dụ:
initialization expression;
while (test_expression)
{
// statements
update_expression;
}
Ví dụ:
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;
}
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
int main()
{
int i = 2; // Initialization expression
do {
// loop body
cout << "Hello World\n";
// update expression
i++;
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:
int main()
72
Chương 2. Lập trình cơ bản
{
fstream file;
file.open("cout.txt", ios::out);
string line;
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ụ:
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ụ:
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
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
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
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
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
83
Chương 3. Lớp và đố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>
85
Chương 3. Lớp và đối tượng
86
Chương 3. Lớp và đối tượng
int main() {
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) {};
88
Chương 3. Lớp và đối tượng
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;
}
#include <bits/stdc++.h>
using namespace std;
class Geeks
{
public:
string geekname;
int id;
91
Chương 3. Lớp và đối tượng
};
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
#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
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;
}
#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;
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).
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.
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 main()
{
// Constructor called
Point p1(10, 15);
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;
}
// Illustration
#include <iostream>
using namespace std;
class point {
private:
double x, y;
public:
99
Chương 3. Lớp và đối tượng
int main(void)
{
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>(){}
100
Chương 3. Lớp và đối tượng
#include <iostream>
using namespace std;
class Test {
public:
Test() { cout << "\n Constructor executed"; }
return 0;
}
#include <iostream>
using namespace std;
class Test {
public:
Test() { cout << "\n Constructor executed"; }
main()
{
Test t, t1, t2, t3;
return 0;
}
101
Chương 3. Lớp và đối tượng
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:
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:
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 :
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
#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
};
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
#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;
}
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
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::display_p()
{
cout<<endl<<id<<"\t"<<name;
}
109
Chương 3. Lớp và đối tượng
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 đủ:
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
};
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
112
Chương 3. Lớp và đối tượng
OR
class A
{
... .. ...
};
class B: public A
{
... .. ...
};
Ví dụ về đơn kế thừa:
113
Chương 3. Lớp và đối tượng
// base class
class Vehicle {
public:
Vehicle()
{
cout << "This is a Vehicle\n";
}
};
};
// 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 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:
115
Chương 3. Lớp và đối tượng
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};
// 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
Cú pháp:
class C
{
... .. ...
};
class B:public C
{
... .. ...
};
class A: public B
{
... ... ...
};
Ví dụ về kế thừa đa mức:
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"; }
};
// 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.
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
// base class
class Vehicle {
public:
Vehicle() { cout << "This is a Vehicle\n"; }
};
// 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:
Ví dụ kế thừa lai:
#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
// main function
int main()
{
// Creating object of sub class will
// invoke the constructor of base class.
Bus obj2;
return 0;
}
122
Chương 3. Lớp và đối tượng
123
Chương 3. Lớp và đối tượng
// Driver code
int main()
{
Geeks obj1;
124
Chương 3. Lớp và đối tượng
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++.
class Complex {
private:
125
Chương 3. Lớp và đối tượng
public:
Complex(int r = 0,
int i = 0)
{
real = r;
imag = i;
}
// Driver code
int main()
{
Complex c1(10, 5), c2(2, 4);
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.
127
Chương 3. Lớp và đối tượng
Dưới đây là chương trình C++ để chứng minh chức năng ghi đè:
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;
}
};
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;
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:
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";
}
};
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
GFG_Child child;
base = &child;
132
Chương 3. Lớp và đối tượng
Đó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:
#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;
}
// 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à.
135
Chương 3. Lớp và đối tượng
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.
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
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ã.
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:
#include <bits/stdc++.h>
using namespace std;
class Node {
public:
int data;
Node* 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;
// Function call
printList(head);
return 0;
}
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.
146
Chương 4. Cấu trúc dữ liệu
Ví dụ:
// 1. allocate node
Node* new_node = new Node();
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.
Ví dụ:
148
Chương 4. Cấu trúc dữ liệu
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.
Ví dụ:
// 1. allocate node
Node* new_node = new Node();
// Used in step 5
Node *last = *head_ref;
150
Chương 4. Cấu trúc dữ liệu
new_node->next = NULL;
151
Chương 4. Cấu trúc dữ liệu
#include <bits/stdc++.h>
using namespace std;
// 1. allocate node
Node* new_node = new Node();
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;
}
// 1. allocate node
Node* new_node = new Node();
//used in step 5
153
Chương 4. Cấu trúc dữ liệu
154
Chương 4. Cấu trúc dữ liệu
{
cout<<" "<<node->data;
node = node->next;
}
}
// Driver code
int main()
{
155
Chương 4. Cấu trúc dữ liệu
return 0;
}
Trỏ đầu tới nút tiếp theo tức là nút thứ hai
temp = head
head = head->next
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;
156
Chương 4. Cấu trúc dữ liệu
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;
}
}
}
#include <bits/stdc++.h>
using namespace std;
157
Chương 4. Cấu trúc dữ liệu
struct Node {
int number;
Node* next;
};
158
Chương 4. Cấu trúc dữ liệu
// 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
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ụ:
162
Chương 4. Cấu trúc dữ liệu
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:
163
Chương 4. Cấu trúc dữ liệu
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:
165
Chương 4. Cấu trúc dữ liệu
return;
}
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:
167
Chương 4. Cấu trúc dữ liệu
new_node->data = new_data;
168
Chương 4. Cấu trúc dữ liệu
169
Chương 4. Cấu trúc dữ liệu
return;
}
// Driver code
170
Chương 4. Cấu trúc dữ liệu
int main()
{
/* Start with the empty list */
Node* head = NULL;
return 0;
}
171
Chương 4. Cấu trúc dữ liệu
Đó 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
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
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
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:
class Stack {
int top;
public:
int a[MAX]; // Maximum size of Stack
176
Chương 4. Cấu trúc dữ liệu
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
bool Stack::isEmpty()
{
return (top < 0);
}
178
Chương 4. Cấu trúc dữ liệu
return 0;
}
179
Chương 4. Cấu trúc dữ liệu
return popped;
}
// 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);
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).
182
Chương 4. Cấu trúc dữ liệu
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
struct QNode {
int data;
QNode* next;
QNode(int d)
{
data = d;
next = NULL;
}
};
struct Queue {
QNode *front, *rear;
Queue() { front = rear = NULL; }
};
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ụ:
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
// 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ụ:
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ụ:
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ụ:
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ụ:
189
Chương 4. Cấu trúc dữ liệu
class Queue {
public:
int front, rear, size;
unsigned cap;
int* arr;
};
queue->rear = cap - 1;
queue->arr = new int[(queue->cap * sizeof(int))];
return queue;
}
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ụ:
#include <bits/stdc++.h>
using namespace std;
// Key is integer
cout << it->first << " => ";
193
Chương 4. Cấu trúc dữ liệu
set<string> st = it->second;
// 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;
194
Chương 4. Cấu trúc dữ liệu
show(mapOfSet);
return 0;
}
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
Để 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.
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
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.
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.
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();
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á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.
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.
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.
207
Chương 5. Cải thiện mã nguồn
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ể.
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.
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ể.
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ó?
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.
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.
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.
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!
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>
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() };
int z{ add(x, 5) };
printResult(z);
return 0;
}
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>
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';
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;
}
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>
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';
int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
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
[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