Bài 11.815 phút đọc
strchr
Sau bài này bạn sẽ làm được
- Dùng strchr và strrchr đúng cách
- Xử lý trường hợp tìm ký tự null
- Tách phần mở rộng của tên file bằng strrchr
- Đếm số lần xuất hiện của một ký tự
Nhóm hàm tìm kiếm trong string.h đều trả về con trỏ chứ không phải chỉ số. Cách thiết kế đó ban đầu gây khó chịu, nhưng nó khiến việc tách chuỗi trở nên rất gọn, và nó là chìa khóa để đọc mã C của người khác.
#strchr và strrchr
#include <string.h>
char *strchr(const char *s, int c); /* tìm từ đầu */
char *strrchr(const char *s, int c); /* tìm từ cuối, chữ r là reverse */Trả về con trỏ tới lần xuất hiện đầu tiên hoặc cuối cùng của ký tự c trong chuỗi, hoặc NULL nếu không có.
strchr.c
#include <stdio.h>
#include <string.h>
int main(void)
{
const char *s = "a/b/c.txt";
char *dau = strchr(s, '/');
char *cuoi = strrchr(s, '/');
if (dau != NULL)
printf("dau tai chi so %td: [%s]\n", dau - s, dau);
if (cuoi != NULL)
printf("cuoi tai chi so %td: [%s]\n", cuoi - s, cuoi);
if (strchr(s, 'z') == NULL)
printf("khong co ky tu z\n");
return 0;
}terminal
./strchr
dau tai chi so 1: [/b/c.txt] cuoi tai chi so 3: [/c.txt] khong co ky tu z
#Tìm chính byte kết thúc
const char *s = "hello";
char *p = strchr(s, '\0'); /* trả về địa chỉ byte kết thúc, KHÔNG phải NULL */
printf("%td\n", p - s); /* 5, đúng bằng strlen(s) */#Tách chuỗi bằng con trỏ
Tách phần mở rộng của tên file
phan-mo-rong.c
/* Trả về con trỏ tới phần mở rộng, không kể dấu chấm.
Trả về NULL nếu không có phần mở rộng. */
const char *phan_mo_rong(const char *ten_file)
{
const char *cham = strrchr(ten_file, '.');
if (cham == NULL) return NULL; /* không có dấu chấm nào */
if (cham == ten_file) return NULL; /* tên bắt đầu bằng dấu chấm, file ẩn */
if (cham[1] == '\0') return NULL; /* dấu chấm ở cuối, không có đuôi */
return cham + 1;
}terminal
./phan-mo-rong
a/b/c.txt -> txt anh.tar.gz -> gz khong_co_duoi -> (khong co) .gitignore -> (khong co) cuoi. -> (khong co)
Ba dòng kiểm tra ở đầu hàm chính là phần khó của bài toán. Ai cũng viết được dòng strrchr, nhưng chỉ khi liệt kê đủ trường hợp biên thì hàm mới dùng được trong thực tế.
Tách tên file khỏi đường dẫn
const char *ten_file(const char *duong_dan)
{
const char *xien = strrchr(duong_dan, '/');
return xien != NULL ? xien + 1 : duong_dan;
}
ten_file("/home/an/bai.c"); /* "bai.c" */
ten_file("bai.c"); /* "bai.c" */
ten_file("/home/an/"); /* "" */Tách cặp khóa và giá trị
tach-cap.c
/* Tách dòng "khoa=gia_tri" thành hai phần.
Sửa ngay trên chuỗi gốc nên chuỗi phải ghi được. */
int tach_cap(char *dong, char **khoa, char **gia_tri)
{
char *bang = strchr(dong, '=');
if (bang == NULL) return -1;
*bang = '\0'; /* cắt chuỗi làm đôi ngay tại dấu bằng */
*khoa = dong;
*gia_tri = bang + 1;
return 0;
}
char dong[] = "ten=Nguyen Van A";
char *k, *v;
if (tach_cap(dong, &k, &v) == 0)
printf("[%s] = [%s]\n", k, v); /* [ten] = [Nguyen Van A] */#Ba hàm họ hàng
| Hàm | Tìm gì | Trả về |
|---|---|---|
| strchr(s, c) | Ký tự c, lần đầu tiên | Con trỏ tới nó, hoặc NULL |
| strrchr(s, c) | Ký tự c, lần cuối cùng | Con trỏ tới nó, hoặc NULL |
| strpbrk(s, tap) | Ký tự đầu tiên thuộc tập cho trước | Con trỏ tới nó, hoặc NULL |
| strspn(s, tap) | Độ dài đoạn đầu chỉ gồm ký tự trong tập | Số ký tự |
| strcspn(s, tap) | Độ dài đoạn đầu KHÔNG có ký tự nào trong tập | Số ký tự |
ho-hang.c
const char *s = " \t42abc";
/* Bỏ qua khoảng trắng đầu chuỗi */
size_t bo_qua = strspn(s, " \t\n");
printf("[%s]\n", s + bo_qua); /* [42abc] */
/* Tìm ký tự đầu tiên không phải chữ số */
size_t so_chu_so = strspn(s + bo_qua, "0123456789");
printf("%zu chu so\n", so_chu_so); /* 2 */
/* Tìm dấu toán tử đầu tiên */
char *toan_tu = strpbrk("a + b * c", "+-*/");
printf("[%s]\n", toan_tu); /* [+ b * c] */
/* strcspn dùng để xóa ký tự xuống dòng, như Bài 11.3 */
char dong[] = "xin chao\n";
dong[strcspn(dong, "\n")] = '\0';#Tự cài lại
my-strchr.c
char *my_strchr(const char *s, int c)
{
char ch = (char)c;
for (;;) {
if (*s == ch)
return (char *)s; /* tìm thấy, kể cả khi ch là byte 0 */
if (*s == '\0')
return NULL; /* hết chuỗi mà không thấy */
++s;
}
}
char *my_strrchr(const char *s, int c)
{
char ch = (char)c;
const char *tim_thay = NULL;
for (;;) {
if (*s == ch)
tim_thay = s; /* ghi nhớ, nhưng vẫn đi tiếp */
if (*s == '\0')
break;
++s;
}
return (char *)tim_thay;
}test.c
#include <assert.h>
#include <string.h>
int main(void)
{
const char *s = "hello";
assert(my_strchr(s, 'h') == strchr(s, 'h'));
assert(my_strchr(s, 'l') == strchr(s, 'l'));
assert(my_strchr(s, 'z') == strchr(s, 'z')); /* cả hai đều NULL */
assert(my_strchr(s, '\0') == strchr(s, '\0'));
assert(my_strrchr(s, 'l') == strrchr(s, 'l'));
assert(my_strrchr(s, 'h') == strrchr(s, 'h'));
assert(my_strrchr(s, 'z') == strrchr(s, 'z'));
assert(my_strrchr(s, '\0') == strrchr(s, '\0'));
assert(my_strchr("", 'a') == strchr("", 'a'));
assert(my_strchr("", '\0') == strchr("", '\0'));
puts("Tat ca kiem thu deu qua");
return 0;
}So sánh trực tiếp hai con trỏ được vì cả hai hàm đều trả về địa chỉ trong cùng một chuỗi. Đây là cách kiểm thử chặt nhất: không chỉ đúng giá trị mà đúng cả vị trí.
Tự làm thử
- Dùng
strchrvàstrrchrtìm ký tự trong một đường dẫn, in cả con trỏ lẫn chỉ số. - Cài
phan_mo_rongvới đủ ba trường hợp biên, thử với năm tên file khác nhau. - Viết hàm đếm số lần xuất hiện của một ký tự, xử lý đúng trường hợp ký tự cần tìm là byte 0.
- Cài
tach_captách dòng cấu hình, thử với dòng không có dấu bằng và dòng có nhiều dấu bằng. - Viết trình tách từ bằng
strspnvàstrcspn, in mỗi từ trên một dòng bằng%.*s. - Cài
my_strchrvàmy_strrchr, chạy bộ kiểm thử so con trỏ với bản chuẩ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
strchrtìm từ đầu,strrchrtìm từ cuối, cả hai trả về con trỏ hoặcNULL.- Đổi con trỏ thành chỉ số bằng phép trừ
p - s, kiểu kết quả làptrdiff_tin bằng%td. - Tìm byte kết thúc luôn thành công, nên kiểm tra
NULLkhông đủ khi ký tự cần tìm có thể bằng 0. - Ghi byte 0 đè lên dấu phân cách là cách tách chuỗi rất hiệu quả, nhưng nó sửa chuỗi gốc.
- Cặp
strspnvàstrcspnghép lại thành một trình phân tích chuỗi đơn giản mà không cần cấp phát gì.