Phần trước: [C++ Cơ bản] Phần 18: Cấu trúc dữ liệu - struct và class
Học hành là phải vừa “học” lại vừa “hành”. Trong bài viết này và bài viết sau, chúng ta sẽ tổng hợp lại các kiến thức đã học vào trong một bài viết thực hành lớn - phần mềm quản lý học sinh. Sau khi hoàn thành xong bài thực hành này, bạn có thể đem sản phẩm vào sử dụng trong thực tế - vẫn còn xa lắm mới tới được một chương trình với giao diện đẹp đẽ, nhưng cũng khá tốt rồi.
Yêu cầu của bài thực hành
Trong bài viết này, chúng ta sẽ viết một chương trình quản lý học sinh có các khả năng sau:
- Đọc và lưu trữ dữ liệu của học sinh từ một file text. Thông tin của học sinh bao gồm tên, lớp, địa chỉ và điểm số 3 môn Toán, Văn, Anh.
- Cho phép thực hiện các hành động thêm học sinh mới và chỉnh sửa hoặc xóa học sinh cũ.
- Cho phép liệt kê tất cả học sinh, hoặc lọc học sinh theo tên, lớp, địa chỉ hoặc điểm số.
Khi ta chạy chương trình quản lý học sinh, phần mềm sẽ yêu cầu ta lựa chọn mở một file cũ, hoặc tạo một file mới, hoặc kết thúc chương trình bằng cách nhập lệnh vào màn hình console.
Khi ta đã mở file hoặc tạo file, chương trình sẽ cho phép người dùng in ra toàn bộ học sinh, lọc lấy một số học sinh, thêm, chỉnh sửa, xóa thông tin học sinh, hoặc kết thúc công việc và đóng file.
Khi lọc học sinh theo thông tin dạng chữ, ta sẽ lấy các học sinh có thông tin trùng với giá trị nhập vào. Khi lọc theo thông tin dạng số, ta có thể lựa chọn lấy các giá trị bằng, lớn hơn, nhỏ hơn,…
Các bạn có thể download chương trình đã hoàn thành tại đây để chạy thử.
Ok, với các thông tin đã có, chúng ta sẽ bắt tay vào thực hành!
Cấu trúc dữ liệu Student
Để có thể lưu trữ dữ liệu của học sinh, ta cần có cấu trúc dữ liệu để biểu diễn học sinh. Ta sẽ sử dụng struct
để tạo ra kiểu dữ liệu Student
bao gồm các biến
string name
- Tên của học sinhstring inClass
- Lớp của học sinh. Ta sử dụnginClass
thay vìclass
, doclass
là từ khóa của C++.string address
- Địa chỉdouble math
,literature
,english
- Điểm số 3 bộ môn Toán, Văn, Anh Hãy nhớ khai báo sử dụng thư việnstring
trước khi sử dụng classstring
.
Sau khi ta đã có cấu trúc dữ liệu Student
, ta có thể khai báo một mảng Student
để lưu trữ các học sinh đang được xử lý. Ở đây ta gọi mảng Student
là allStudent[]
với \( 10 ^ 5 \) phần tử.
Ta cũng cần một biến int number
, là số lượng học sinh đang được xử lý. Các học sinh được đọc ra từ file sẽ được lưu trữ trong các phần tử mảng allStudent[]
, từ 1 tới number
.
Cấu trúc của file đầu vào.
Ta cần phải quy định bố cục nội dung của file đầu vào, để chương trình có thể xử lý được file. Dưới đây là bố cục được sử dụng trong bài viết.
- Dòng đầu tiên chứa một số kiểu
int
, là số lượng học sinh được lưu trữ trong file. - Các nhóm dòng tiếp theo lưu trữ thông tin về học sinh. Trong mỗi nhóm dòng, mỗi dòng lần lượt sẽ là giá trị của các biến
name
,inClass
,address
,math
,literature
vàenglish
của học sinh. - Do khi ta đọc vào một
string
, C++ lấy các kí tự dấu cách làm ngắt từ, nên các string kí tự có nhiều từ sẽ bị cắt ra làm nhiềustring
. Để giải quyết vần đề này, ta sẽ lưu lại vào file các dấu cách trong các giá trị string bằng kí tự$
(Ví dụTrần$Minh$Hiếu
thay vìTrần Minh Hiếu
).
Ví dụ về một file đúng chuẩn
1
Trần$Minh$Hiếu
KT22
The$Internet
8.0
5.0
7.0
Biểu diễn bố cục của chương trình trong hàm main
Đầu tiên, vì chương trình chạy liên tục cho tới khi ta ra lệnh dừng, nên ta sẽ đặt tất cả nội dung của chương trình trong một vòng lặp vô hạn, chỉ được thoát ra khi ta ra lệnh.
Chương trình của chúng ta có 2 trạng thái - khi chưa mở file nào ra, và khi đã mở file để chỉnh sửa. Ta sẽ quy định trạng thái của chương trình bằng biến bool
global isEditing
- true
nếu chương trình đang chỉnh sửa file, false
nếu ngược lại.
Khi chưa mở file nào ra, ta có 3 lựa chọn là tạo file mới, mở file cũ, hoặc kết thúc chương trình. Ta sẽ yêu cầu người dùng nhập vào số 1, 2 hoặc 0, tương ứng với lệnh cần thực hiện.
Vấn đề nảy sinh ra: Làm sao để chắc chắn người dùng sẽ nhập vào đúng 3 giá trị này? Ta sẽ viết một hàm getIntRange()
, bắt người dùng phải nhập vào một giá trị int cho tới khi giá trị này nằm đúng trong khoảng đã cho. Vòng lặp vô hạn sẽ có tác dụng ở đây:
Khi đó ta sẽ biểu diễn trạng thái lúc chưa chỉnh sửa file nào như sau:
Các chương trình con newFile()
và openFile()
sẽ được chúng ta thêm vào về sau.
Khi chương trình đang mở file để chỉnh sửa, ta cũng sẽ sử dụng cấu trúc tương tự để rẽ hướng chương trình:
Toàn bộ chương trình con main()
sẽ có bố cục như sau
Tạo file mới
Như trong các chương trình soạn thảo văn bản hay vẽ tranh, chương trình đều cung cấp cho chúng ta khả năng lưu vào file đang mở, hoặc lưu vào một file mới. Ta sẽ cần phải lưu lại tên của file đang mở, để tiện dùng sau này.
Do hàm ofstream.open()
chỉ chấp nhận kiểu biểu diễn string bằng mảng char
mà không chấp nhận class string
, nên ta sẽ tạo một biến global kiểu mảng char
currentFile[100]
để lưu tên file.
Khi ta ra lệnh tạo file mới, thực chất ta chỉ cần làm ba việc: gán giá trị number = 0
(tức là chưa có một học sinh nào trong danh sách cả), gán currentFile[]
thành string rỗng (dữ liệu chưa được lưu vào file nào) và chuyển trạng thái isEditing
thành true
.
Đọc dữ liệu từ file
Do dữ liệu trong file mã hóa các dấu cách, nên ta cần phải giải mã chúng khi đọc file.
Ta sẽ viết hai hàm decode()
và encode()
để giải mã và mã hóa string - chỉ cần duyệt qua toàn bộ string, và thay các kí tự $
bằng dấu cách.
Ok, có mã hóa và giải mã rồi, giờ ta sẽ bắt tay vào viết hàm void openFile()
.
Sau khi đã mở file ra, ta sẽ đọc file theo đúng bố cục đã trình bày ở trên.
Cuối cùng, ta đóng stream input lại, và chuyển trạng thái isEditing
thành true
.
Toàn bộ nội dung của chương trình con openFile()
như sau:
Thêm học sinh và chỉnh sửa thông tin
Quá trình thêm học sinh bao gồm việc nhập thông tin học sinh mới vào danh sách.
Quá trình chỉnh sửa thông tin học sinh bao gồm việc chỉ định học sinh nào sẽ bị chỉnh sửa, và nhập thông tin mới trong học sinh.
Vậy nên chả có lý do gì ta lại không dùng chung một chương trình con để nhập dữ liệu chung cho hai thao tác này cả. Việc thêm học sinh thì cũng chỉ là việc chỉnh sửa học sinh thứ number + 1
thôi mà :)) Cụ thể ở đây ta sẽ viết một hàm inputStudentInfo(int id)
, cho phép nhập dữ liệu để lưu trữ vào phần tử vị trí id
của mảng allStudent[]
.
Ở trên chúng ta được biết rằng C++ sử dụng kí tự dấu cách để phân cách string
, nên ta phải tránh việc lưu tên học sinh có dấu cách bằng việc mã hóa. Nhưng khi đưa tới người dùng sử dụng, ta lại nhất thiết cần phải có dấu cách để có thể nhập thông tin trực quan.
Để giải quyết vấn đề này, ta sẽ sử dụng hàm getline()
của thư viện fstream
. Hàm này nhận hai tham số, một luồng input để lấy dữ liệu và một biến kiểu class string
, và hàm này sẽ đọc nốt tất cả thông tin còn lại trên dòng của input để gán vào string. Lệnh
có nghĩa là ta sẽ nhập hết dữ liệu còn lại trên dòng hiện tại ở cin
, và gán vào biến s
.
Tại sao ta lại cần phải đọc vào biến temp
trước? Vì ở trên dòng input cuối cùng, trước dòng có tên học sinh, vẫn còn một kí hiệu xuống dòng '\n'
nữa. Ta phải thêm một dòng getline()
vào temp
để quét nốt dòng này, trước khi xuống tới tên. Đối với lớp và địa chỉ thì lại không cần nữa, vì các dòng getline()
đã lấy hết dòng trước hộ rồi.
Cuối cùng, ta nhập vào điểm 3 môn như bình thường.
Toàn bộ nội dung hàm inputStudentInfo()
như sau:
Sau khi đã có hàm này, ta chỉ cần viết hai hàm addNewStudent()
và editStudent()
rất ngắn gọn. Chú ý editStudent()
còn có một đoạn kiểm tra xem mã số học sinh nhập vào có hợp lệ hay không.
Xóa thông tin học sinh
Hàm xóa thông tin học sinh ta viết trong main()
là deleteStudent()
. Để xóa thông tin học sinh, ta cần phải nhập vào mã số học sinh cần xóa. Đoạn này giống như với editStudent()
.
Khi đã có mã số học sinh cần xóa hợp lệ, ta sẽ xóa thông tin của học sinh đó đi, bằng cách dồn các học sinh ở sau về phía trước, gán đè lên giá trị của người bị xóa.
Toàn bộ nội dung của hàm deleteStudent()
như sau
Ok, chúng ta đã hoàn thành xong việc thêm, sửa và xóa thông tin học sinh. Bài thực hành sẽ tiếp tục ở phần sau, với việc tìm kiếm học sinh theo thông tin, và lưu lại dữ liệu lên file.
Phần sau: [C++ Cơ bản] Phần 20: Bài thực hành tổng hợp - Chương trình quản lý học sinh (tiếp)