scanf
Sau bài này bạn sẽ làm được
- Luôn kiểm tra giá trị trả về của scanf
- Giới hạn độ rộng khi đọc chuỗi để không tràn bộ đệm
- Phân biệt %f và %lf giữa printf và scanf
- Dùng tập ký tự để đọc cả dòng có khoảng trắng
scanf là hàm nhập liệu mà mọi giáo trình C đều dạy đầu tiên, và cũng là hàm gây ra nhiều bug nhất cho người mới. Bài này dạy dùng nó cho đúng, đồng thời giải thích vì sao từ bài sau trở đi khóa học sẽ chuyển sang cách khác.
#Cách dùng và giá trị trả về
int scanf(const char *format, ...);scanf trả về số mục đã gán thành công, hoặc hằng số EOF nếu gặp cuối luồng hay lỗi trước khi gán được mục nào.
#include <stdio.h>
int main(void)
{
int tuoi;
printf("Nhập tuổi: ");
if (scanf("%d", &tuoi) != 1) { /* LUÔN kiểm tra */
fprintf(stderr, "Dữ liệu không hợp lệ\n");
return 1;
}
printf("Tuổi của bạn là %d\n", tuoi);
return 0;
}Cú pháp đặc tả của scanf
%[*][độ rộng tối đa][bổ từ]ký_tự_kiểuDấu * nghĩa là đọc rồi bỏ đi, không gán vào đối số nào. Hữu ích khi bạn muốn bỏ qua một trường trong dữ liệu.
/* Dữ liệu dạng: 15/03/2003 nhưng chỉ cần năm */
int nam;
scanf("%*d/%*d/%d", &nam);#Bốn sai lầm chết người
Sai lầm 1: quên dấu lấy địa chỉ
int n;
scanf("%d", n); /* SAI: truyền GIÁ TRỊ của n, thường là rác */
scanf("%d", &n); /* đúng: truyền ĐỊA CHỈ để scanf ghi vào */scanf cần biết ghi kết quả vào đâu, nên phải nhận địa chỉ. Quên dấu & thì nó lấy giá trị rác trong n coi như một địa chỉ và ghi vào đó. Kết quả gần như luôn là chương trình dừng đột ngột.
Sai lầm 2: không giới hạn độ rộng khi đọc chuỗi
char ten[16];
scanf("%s", ten);
/* Người dùng nhập 100 ký tự thì ghi đè 84 byte
ra ngoài vùng nhớ của ten. Đây là tràn bộ đệm. */char ten[16];
scanf("%15s", ten);
/* Tối đa 15 ký tự, chừa một chỗ cho ký tự kết thúc.
Độ rộng KHÔNG tính ký tự kết thúc. */Sai lầm 3: quên rằng %s dừng ở khoảng trắng
char ten[64];
printf("Nhập họ tên: ");
scanf("%63s", ten);
/* Người dùng nhập: Nguyễn Văn A
ten chỉ nhận được: Nguyễn
Phần còn lại vẫn nằm trong bộ đệm và sẽ phá lần nhập tiếp theo. */Sai lầm 4: không xử lý khi nhập sai
int n;
while (1) {
printf("Nhập số: ");
if (scanf("%d", &n) == 1) {
break;
}
printf("Sai rồi\n");
/* VÒNG LẶP VÔ HẠN.
scanf thất bại nhưng KHÔNG tiêu thụ ký tự gây lỗi,
nên lần sau nó lại gặp đúng ký tự đó và lại thất bại. */
}Cách sửa là tự dọn phần dữ liệu hỏng ra khỏi bộ đệm. Chi tiết ở Bài 3.5.
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* nuốt hết phần còn lại của dòng */
}#Khoảng trắng trong định dạng
Quy tắc xử lý khoảng trắng của scanf khá tinh tế và là nguồn của nhiều hiểu lầm.
| Đặc tả | Có tự bỏ qua khoảng trắng đầu? |
|---|---|
| %d %i %u %f %g %x | Có |
| %s | Có |
| %c | KHÔNG |
| %[...] | KHÔNG |
int tuoi;
char gioi_tinh;
printf("Nhập tuổi: ");
scanf("%d", &tuoi); /* người dùng gõ 20 rồi Enter.
Số 20 được đọc, ký tự xuống dòng CÒN LẠI. */
printf("Giới tính (M/F): ");
scanf("%c", &gioi_tinh); /* đọc ngay ký tự xuống dòng, KHÔNG chờ người dùng */
/* Sửa: */
scanf(" %c", &gioi_tinh); /* dấu cách nuốt ký tự xuống dòng còn sót */#Khác biệt với printf
Nhiều người tưởng đặc tả của scanf giống hệt printf. Có một khác biệt quan trọng ở kiểu số thực.
| Kiểu | printf | scanf |
|---|---|---|
| float | %f | %f |
| double | %f | %lf |
| long double | %Lf | %Lf |
| int | %d | %d |
| size_t | %zu | %zu |
#Tập ký tự
Đặc tả %[...] cho phép chỉ định chính xác những ký tự nào được chấp nhận. Dấu mũ ở đầu nghĩa là phủ định.
char dong[100];
char ten[32];
scanf("%99[^\n]", dong); /* đọc tới hết dòng, không lấy ký tự xuống dòng */
scanf("%31[a-zA-Z ]", ten); /* chỉ nhận chữ cái và dấu cách */
scanf("%*[^\n]"); /* đọc rồi bỏ hết phần còn lại của dòng */
scanf("%19[0-9]", so); /* chỉ nhận chữ số */Đây là cách duy nhất để scanf đọc được một chuỗi có khoảng trắng, vì %s luôn dừng ở khoảng trắng đầu tiên.
#sscanf và cách dùng an toàn
sscanf giống hệt scanf nhưng đọc từ một chuỗi đã có sẵn trong bộ nhớ thay vì từ bàn phím. Đây chính là mảnh ghép giúp giải quyết mọi vấn đề của scanf.
int sscanf(const char *s, const char *format, ...);int a, b;
if (scanf("%d %d", &a, &b) != 2) {
/* Bộ đệm đang ở trạng thái nào?
Còn bao nhiêu ký tự hỏng chưa đọc?
Rất khó khôi phục. */
}char dong[256];
int a, b;
if (fgets(dong, sizeof dong, stdin)) {
if (sscanf(dong, "%d %d", &a, &b) == 2) {
/* thành công */
} else {
/* thất bại, nhưng cả dòng đã được tiêu thụ gọn gàng.
Chỉ cần báo lỗi rồi đọc dòng tiếp theo. */
}
}Sai rồi Sai rồi Sai rồi ... vòng lặp vô hạn
Dữ liệu không hợp lệ, nhập lại:
Tự làm thử
- Nhập ba số nguyên trên một dòng, tính tổng, kiểm tra
scanftrả về đúng 3. - Nhập ngày theo dạng
dd/mm/yyyybằng đúng một lời gọiscanf, rồi kiểm tra tính hợp lệ của ngày. - Viết chương trình đọc dòng dạng
ten:tuoi:diembằng tập ký tự, xử lý được cả trường hợp tên có khoảng trắng. - Tái hiện lỗi bỏ qua lượt nhập: nhập một số rồi nhập một ký tự bằng
%ckhông có dấu cách. Sau đó sửa bằng cách thêm dấu cách. - Tái hiện vòng lặp vô hạn khi nhập chữ vào
%d, rồi sửa lại bằng cách dọn bộ đệm. - Viết lại bài trên theo cách
fgetskèmsscanfvà so sánh độ phức tạp của hai bản.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- scanf trả về số mục gán thành công, và luôn phải kiểm tra giá trị này.
- Quên
&, không giới hạn độ rộng, quên%sdừng ở khoảng trắng, và không xử lý lỗi là bốn sai lầm phổ biến nhất. %cvà%[không tự bỏ qua khoảng trắng, nên cần viết" %c"có dấu cách phía trước.- Kiểu
doubledùng%ftrongprintfnhưng%lftrongscanf. - Tập ký tự là cách duy nhất để scanf đọc chuỗi có khoảng trắng.
- Cách an toàn là đọc cả dòng bằng fgets rồi phân tích bằng sscanf.