Bỏ qua điều hướng, tới nội dung chính
Học C
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àmTìm gìTrả về
strchr(s, c)Ký tự c, lần đầu tiênCon trỏ tới nó, hoặc NULL
strrchr(s, c)Ký tự c, lần cuối cùngCon trỏ tới nó, hoặc NULL
strpbrk(s, tap)Ký tự đầu tiên thuộc tập cho trướcCon trỏ tới nó, hoặc NULL
strspn(s, tap)Độ dài đoạn đầu chỉ gồm ký tự trong tậpSố ký tự
strcspn(s, tap)Độ dài đoạn đầu KHÔNG có ký tự nào trong tậpSố 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ử

  1. Dùng strchrstrrchr tìm ký tự trong một đường dẫn, in cả con trỏ lẫn chỉ số.
  2. Cài phan_mo_rong với đủ ba trường hợp biên, thử với năm tên file khác nhau.
  3. 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.
  4. Cài tach_cap tá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.
  5. Viết trình tách từ bằng strspnstrcspn, in mỗi từ trên một dòng bằng %.*s.
  6. Cài my_strchrmy_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

  • strchr tìm từ đầu, strrchr tìm từ cuối, cả hai trả về con trỏ hoặc NULL.
  • Đổi con trỏ thành chỉ số bằng phép trừ p - s, kiểu kết quả là ptrdiff_t in bằng %td.
  • Tìm byte kết thúc luôn thành công, nên kiểm tra NULL khô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 strspnstrcspn ghé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ì.