Nhập chuỗi
Sau bài này bạn sẽ làm được
- Nhập chuỗi an toàn bằng fgets
- Xóa ký tự xuống dòng còn sót đúng cách
- Xử lý trường hợp dòng nhập dài hơn bộ đệm
- Biết giới hạn của scanf với định dạng chuỗi
Nhập chuỗi là nơi C nguy hiểm nhất. Một hàm của thư viện chuẩn nguy hiểm tới mức chuẩn C11 đã xóa hẳn nó khỏi ngôn ngữ, chuyện chưa từng xảy ra với hàm nào khác. Bài này chỉ ra cách nhập chuỗi đúng, và đủ sâu để bạn xử lý được mọi trường hợp biên.
#gets đã bị xóa khỏi chuẩn
char s[10];
gets(s); /* KHÔNG BAO GIỜ DÙNG. Đã bị xóa khỏi C11. */#fgets, cách đúng
char *fgets(char *s, int n, FILE *f);| Tham số | Ý nghĩa |
|---|---|
| char *s | Bộ đệm nhận dữ liệu |
| int n | Kích thước bộ đệm, tính cả byte kết thúc |
| FILE *f | Luồng đọc, dùng stdin cho bàn phím |
| Hành vi | Chi tiết |
|---|---|
| Đọc nhiều nhất | n trừ 1 ký tự, chừa một ô cho byte kết thúc |
| Dừng khi | Gặp ký tự xuống dòng, hoặc đọc đủ n trừ 1 ký tự, hoặc hết dữ liệu |
| Giữ ký tự xuống dòng | Có. Nó nằm trong bộ đệm nếu còn chỗ |
| Luôn thêm byte kết thúc | Có, khi hàm thành công |
| Trả về | Con trỏ s khi thành công, NULL khi hết dữ liệu hoặc lỗi |
#include <stdio.h>
#include <string.h>
int main(void)
{
char ten[64];
printf("Nhap ten: ");
if (fgets(ten, sizeof ten, stdin) == NULL) {
fprintf(stderr, "Khong doc duoc\n");
return 1;
}
ten[strcspn(ten, "\n")] = '\0'; /* xóa ký tự xuống dòng */
printf("Xin chao [%s], do dai %zu\n", ten, strlen(ten));
return 0;
}Nhap ten: Nguyen Van A Xin chao [Nguyen Van A], do dai 12
#Xóa ký tự xuống dòng
Hàm fgets giữ lại ký tự xuống dòng, và nó gần như luôn gây phiền toái. So sánh chuỗi sẽ thất bại, in ra bị xuống dòng thừa, độ dài lớn hơn một so với mong đợi.
| Cách xóa | Viết thế nào | Đánh giá |
|---|---|---|
| Dùng strcspn | s[strcspn(s, "\n")] = '\0'; | Gọn nhất, an toàn cả khi không có ký tự xuống dòng |
| Kiểm tra ô cuối | if (len > 0 && s[len-1] == '\n') s[len-1] = 0; | Rõ ràng, cần gọi strlen trước |
| Dùng strchr | char *p = strchr(s, '\n'); if (p) *p = 0; | Cũng tốt, hai dòng |
| Xóa vô điều kiện | s[strlen(s)-1] = '\0'; | SAI, hỏng khi chuỗi rỗng hoặc khi dòng bị cắt |
fgets(s, sizeof s, stdin);
s[strlen(s) - 1] = '\0';
/* Hai vấn đề:
1. Nếu s rỗng thì strlen(s) - 1 với size_t cho ra số khổng lồ.
2. Nếu dòng nhập dài hơn bộ đệm, ô cuối là ký tự thật, bị xóa oan. */if (fgets(s, sizeof s, stdin) == NULL) {
/* xử lý hết dữ liệu */
}
s[strcspn(s, "\n")] = '\0';
/* strcspn trả về vị trí ký tự đầu tiên thuộc tập cho trước,
hoặc độ dài chuỗi nếu không tìm thấy. Cả hai trường hợp đều đúng. */#Khi dòng nhập dài hơn bộ đệm
Nếu người dùng gõ nhiều ký tự hơn bộ đệm chứa được, fgets lấy phần đầu và để phần còn lại nằm nguyên trong luồng. Lần gọi tiếp theo sẽ đọc tiếp phần đó, chứ không phải dòng mới.
#include <stdio.h>
#include <string.h>
int main(void)
{
char a[8], b[8];
fgets(a, sizeof a, stdin);
fgets(b, sizeof b, stdin);
printf("a = [%s]\n", a);
printf("b = [%s]\n", b);
return 0;
}a = [abcdefg] b = [hijklmn]
Bộ đệm thứ hai nhận phần dư của dòng thứ nhất, còn dòng xyz chưa được đọc lần nào. Với chương trình có menu, hiện tượng này gây ra chuỗi phản ứng khó hiểu.
/* Phát hiện dòng bị cắt và bỏ phần dư. */
int doc_dong(char *s, size_t co, FILE *f)
{
if (fgets(s, (int)co, f) == NULL)
return -1; /* hết dữ liệu hoặc lỗi */
size_t len = strlen(s);
if (len > 0 && s[len - 1] == '\n') {
s[len - 1] = '\0';
return 0; /* đọc trọn một dòng */
}
/* Không có ký tự xuống dòng nghĩa là dòng còn dài hơn bộ đệm.
Bỏ phần dư để lần gọi sau bắt đầu ở dòng mới. */
int c;
while ((c = fgetc(f)) != '\n' && c != EOF)
;
return 1; /* báo cho người gọi biết đã bị cắt */
}#scanf với chuỗi
char s[10];
scanf("%s", s); /* NGUY HIỂM: không giới hạn độ dài, giống gets */
scanf("%9s", s); /* an toàn: đọc nhiều nhất 9 ký tự cộng byte kết thúc */| scanf với %s | fgets | |
|---|---|---|
| Đọc tới đâu | Tới khoảng trắng đầu tiên | Tới hết dòng |
| Đọc được chuỗi có dấu cách | Không | Có |
| Giới hạn độ dài | Phải viết cứng vào chuỗi định dạng | Truyền bằng tham số |
| Giữ ký tự xuống dòng | Không, nhưng để lại trong luồng | Có, trong bộ đệm |
| Bỏ qua khoảng trắng đầu | Có | Không |
#Một hàm đọc dòng dùng được
#include <stdio.h>
#include <string.h>
/* Đọc một dòng vào s, bỏ ký tự xuống dòng, bỏ phần dư nếu dòng quá dài.
Trả về:
0 đọc trọn một dòng
1 dòng dài hơn bộ đệm, phần dư đã bị bỏ
-1 hết dữ liệu hoặc lỗi */
int doc_dong(char *s, size_t co, FILE *f)
{
if (s == NULL || co == 0 || f == NULL) return -1;
if (fgets(s, (int)co, f) == NULL) {
s[0] = '\0';
return -1;
}
size_t len = strlen(s);
if (len > 0 && s[len - 1] == '\n') {
s[len - 1] = '\0';
return 0;
}
int c, bi_cat = 0;
while ((c = fgetc(f)) != '\n' && c != EOF)
bi_cat = 1;
return bi_cat ? 1 : 0;
}
int main(void)
{
char dong[16];
int kq;
while ((kq = doc_dong(dong, sizeof dong, stdin)) != -1) {
printf("[%s]%s\n", dong, kq == 1 ? " (bi cat)" : "");
}
return 0;
}[ngan] [mot dong rat da] (bi cat) []
Trên Linux có getline
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
char *dong = NULL;
size_t co = 0;
ssize_t n;
while ((n = getline(&dong, &co, stdin)) != -1) {
if (n > 0 && dong[n - 1] == '\n') dong[n - 1] = '\0';
printf("[%s]\n", dong);
}
free(dong);Hàm getline tự cấp phát bộ đệm và tự nới rộng khi dòng dài, nên không bao giờ bị cắt. Nhược điểm là nó thuộc chuẩn POSIX chứ không phải chuẩn C, nên không có trên Windows nếu không dùng thư viện tương thích. Chương 14 sẽ giải thích phần cấp phát động của nó.
Tự làm thử
- Viết chương trình đọc tên bằng
fgets, xóa ký tự xuống dòng và in độ dài. Thử với tên có dấu cách. - Nhập một dòng dài hơn bộ đệm, gọi
fgetshai lần liên tiếp và quan sát bộ đệm thứ hai nhận gì. - Cài
doc_dongđầy đủ như trong bài, thử với cả năm trường hợp biên trong danh sách. - Viết chương trình dùng
scanfđọc số rồifgetsđọc chuỗi để tái hiện lỗi trộn hai hàm, sau đó sửa bằng cách chỉ dùngfgets. - So sánh
scanf("%s", s)vàfgets(s, sizeof s, stdin)khi nhập chuỗi có dấu cách. - Viết chương trình đọc toàn bộ dữ liệu vào theo từng dòng, đếm số dòng, số từ và số ký tự, giống lệnh
wc.
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
- Không bao giờ dùng
gets. Nó đã bị xóa khỏi chuẩn C11 vì không thể dùng an toàn. fgetsnhận kích thước bộ đệm đầy đủ, luôn thêm byte kết thúc, và giữ lại ký tự xuống dòng.- Xóa ký tự xuống dòng bằng
s[strcspn(s, "\n")] = 0;, không bao giờ bằngs[strlen(s) - 1] = 0;. - Dòng dài hơn bộ đệm để lại phần dư trong luồng, phải chủ động bỏ đi bằng vòng lặp có kiểm tra hết dữ liệu.
- Với
scanf, độ rộng trường là số ký tự nên phải viết%9scho mảng mười ô. Tốt hơn là dùngfgetsrồisscanf.