Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 11.1026 phút đọc

Tự cài lại thư viện chuỗi

Sau bài này bạn sẽ làm được

  • Tự cài lại strlen, strcpy, strcat, strcmp, strchr, strstr
  • Viết bộ kiểm thử so kết quả với thư viện chuẩn
  • Xử lý đúng mọi trường hợp biên gồm chuỗi rỗng
  • Hiểu vì sao bản chuẩn nhanh hơn bản tự cài rất nhiều

Bài cuối của Phần 4 là một dự án nhỏ: viết lại tám hàm của string.h kèm bộ kiểm thử tự động so với bản chuẩn. Đây là cách chắc chắn nhất để biết bạn đã thật sự hiểu chuỗi C, và là mô hình làm việc bạn sẽ dùng lại suốt phần còn lại của khóa học.

#Mục tiêu bài này

  • Cài lại tám hàm, mỗi hàm đặt tên với tiền tố my_ để không đụng tên hàm chuẩn.
  • Viết bộ kiểm thử gọi cả bản của bạn lẫn bản chuẩn trên cùng dữ liệu và khẳng định kết quả giống nhau.
  • Liệt kê đủ trường hợp biên, đặc biệt là chuỗi rỗng và chuỗi vừa khít bộ đệm.
  • Đo chênh lệch tốc độ giữa bản của bạn và bản thư viện, và giải thích nó.

#Tám hàm cần cài

mystring.h
#ifndef MYSTRING_H
#define MYSTRING_H

#include <stddef.h>

size_t my_strlen(const char *s);
char  *my_strcpy(char *dich, const char *nguon);
char  *my_strcat(char *dich, const char *nguon);
int    my_strcmp(const char *a, const char *b);
int    my_strncmp(const char *a, const char *b, size_t n);
char  *my_strchr(const char *s, int c);
char  *my_strrchr(const char *s, int c);
char  *my_strstr(const char *lon, const char *nho);

#endif
mystring.c
#include "mystring.h"

size_t my_strlen(const char *s)
{
    const char *p = s;

    while (*p != '\0')
        ++p;

    return (size_t)(p - s);
}

char *my_strcpy(char *dich, const char *nguon)
{
    size_t i = 0;

    while (nguon[i] != '\0') {
        dich[i] = nguon[i];
        ++i;
    }

    dich[i] = '\0';

    return dich;
}

char *my_strcat(char *dich, const char *nguon)
{
    size_t d = my_strlen(dich);
    size_t i = 0;

    while (nguon[i] != '\0') {
        dich[d + i] = nguon[i];
        ++i;
    }

    dich[d + i] = '\0';

    return dich;
}

int my_strcmp(const char *a, const char *b)
{
    while (*a != '\0' && *a == *b) {
        ++a;
        ++b;
    }

    return (int)(unsigned char)*a - (int)(unsigned char)*b;
}

int my_strncmp(const char *a, const char *b, size_t n)
{
    for (size_t i = 0; i < n; ++i) {
        unsigned char x = (unsigned char)a[i];
        unsigned char y = (unsigned char)b[i];

        if (x != y)   return (int)x - (int)y;
        if (x == '\0') return 0;      /* cả hai cùng kết thúc */
    }

    return 0;
}

char *my_strchr(const char *s, int c)
{
    char ch = (char)c;

    for (;;) {
        if (*s == ch)   return (char *)s;
        if (*s == '\0') return NULL;

        ++s;
    }
}

char *my_strrchr(const char *s, int c)
{
    char        ch = (char)c;
    const char *kq = NULL;

    for (;;) {
        if (*s == ch)   kq = s;
        if (*s == '\0') break;

        ++s;
    }

    return (char *)kq;
}

char *my_strstr(const char *lon, const char *nho)
{
    if (nho[0] == '\0')
        return (char *)lon;

    for (; *lon != '\0'; ++lon) {
        const char *a = lon;
        const char *b = nho;

        while (*a == *b && *b != '\0') {
            ++a;
            ++b;
        }

        if (*b == '\0')
            return (char *)lon;
    }

    return NULL;
}

#Bộ kiểm thử tự động

test.c
#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "mystring.h"

static int so_luot = 0;

#define KT(bieu_thuc)                                          \
    do {                                                       \
        ++so_luot;                                             \
        if (!(bieu_thuc)) {                                    \
            printf("HONG tai dong %d: %s\n", __LINE__, #bieu_thuc); \
            return 1;                                          \
        }                                                      \
    } while (0)

/* So sánh dấu chứ không so giá trị, vì chuẩn chỉ bảo đảm dấu. */
static int cung_dau(int a, int b)
{
    return (a < 0 && b < 0) || (a == 0 && b == 0) || (a > 0 && b > 0);
}

static int kiem_thu_len(void)
{
    const char *mau[] = { "", "a", "hello", "a b c", "\t\n", "xyz123" };

    for (size_t i = 0; i < sizeof mau / sizeof mau[0]; ++i)
        KT(my_strlen(mau[i]) == strlen(mau[i]));

    return 0;
}

static int kiem_thu_cmp(void)
{
    const char *mau[] = { "", "a", "ab", "abc", "abd", "B", "b", "\x80" };
    size_t n = sizeof mau / sizeof mau[0];

    for (size_t i = 0; i < n; ++i)
        for (size_t j = 0; j < n; ++j)
            KT(cung_dau(my_strcmp(mau[i], mau[j]),
                        strcmp(mau[i], mau[j])));

    for (size_t i = 0; i < n; ++i)
        for (size_t j = 0; j < n; ++j)
            for (size_t k = 0; k <= 4; ++k)
                KT(cung_dau(my_strncmp(mau[i], mau[j], k),
                            strncmp(mau[i], mau[j], k)));

    return 0;
}

static int kiem_thu_cpy_cat(void)
{
    char a[64], b[64];

    const char *mau[] = { "", "a", "hello", "xin chao the gioi" };
    size_t n = sizeof mau / sizeof mau[0];

    for (size_t i = 0; i < n; ++i) {
        my_strcpy(a, mau[i]);
        strcpy(b, mau[i]);
        KT(strcmp(a, b) == 0);

        for (size_t j = 0; j < n; ++j) {
            my_strcpy(a, mau[i]);
            strcpy(b, mau[i]);

            my_strcat(a, mau[j]);
            strcat(b, mau[j]);

            KT(strcmp(a, b) == 0);
        }
    }

    return 0;
}

static int kiem_thu_tim(void)
{
    const char *mau[] = { "", "a", "hello", "aaaa", "abcabcabd", "a/b/c.txt" };
    const char  ky_tu[] = { 'a', 'l', 'z', '/', '\0' };

    for (size_t i = 0; i < sizeof mau / sizeof mau[0]; ++i) {
        for (size_t j = 0; j < sizeof ky_tu / sizeof ky_tu[0]; ++j) {
            KT(my_strchr(mau[i], ky_tu[j])  == strchr(mau[i], ky_tu[j]));
            KT(my_strrchr(mau[i], ky_tu[j]) == strrchr(mau[i], ky_tu[j]));
        }

        for (size_t j = 0; j < sizeof mau / sizeof mau[0]; ++j)
            KT(my_strstr(mau[i], mau[j]) == strstr(mau[i], mau[j]));
    }

    return 0;
}

int main(void)
{
    if (kiem_thu_len())     return 1;
    if (kiem_thu_cmp())     return 1;
    if (kiem_thu_cpy_cat()) return 1;
    if (kiem_thu_tim())     return 1;

    printf("Tat ca %d luot kiem thu deu qua\n", so_luot);

    return 0;
}
terminal
gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined mystring.c test.c -o test && ./test
Tat ca 506 luot kiem thu deu qua

#Danh sách trường hợp biên

Trường hợpVì sao quan trọngHàm nào dễ sai
Chuỗi rỗngVòng lặp không chạy lần nào, mọi phép trừ độ dài đều nguy hiểmTất cả
Cả hai đối số đều rỗngHai điều kiện thoát cùng đúng ở lượt đầustrcmp, strstr
Tìm chính byte 0Byte kết thúc được coi là phần của chuỗistrchr, strrchr
Chuỗi con dài hơn chuỗi lớnVòng lặp ngoài kết thúc trước khi vòng trong xongstrstr
Chuỗi con rỗngChuẩn quy định trả về ngay chuỗi lớnstrstr
Ký tự mã lớn hơn 127char có dấu làm kết quả so sánh đảo dấustrcmp, strncmp
n bằng 0Vòng lặp không chạy, phải trả về 0strncmp, strncpy
Nhiều lần khớp chồng lấnVị trí trả về phải là lần đầu tiênstrstr
Khớp một phần rồi thất bạiPhải quay lại đúng vị trí kế tiếp, không nhảy quá xastrstr

#Vì sao bản chuẩn nhanh hơn nhiều

terminal
# Mỗi hàm gọi một triệu lần trên chuỗi mười nghìn ký tự
./do-toc-do
         ban tu cai   ban chuan   ti le
strlen      2.104 s     0.128 s    16.4x
strcmp      1.892 s     0.214 s     8.8x
strchr      2.011 s     0.141 s    14.3x
strstr      0.412 s     0.004 s   103.0x
Kỹ thuật của thư việnHiệu quả
Đọc tám hoặc ba hai byte một lần thay vì từng byteGiảm số lần đọc bộ nhớ xuống tám tới ba hai lần
Dùng lệnh véc tơ của vi xử lýSo mười sáu byte trong một lệnh máy
Thủ thuật bit để tìm byte 0 trong một khốiKhông cần kiểm tra từng byte riêng
Chọn cài đặt theo vi xử lý lúc nạp chương trìnhMáy mới dùng lệnh mới, máy cũ vẫn chạy được
Thuật toán tốt hơn cho strstrTuyến tính trong mọi trường hợp thay vì n nhân m

#Năm hàm không có trong chuẩn

Bốn hàm sau đây bạn sẽ cần liên tục nhưng chuẩn C không có. Viết chúng một lần rồi mang theo suốt khóa học.

tienich.c
#include <ctype.h>
#include <string.h>

/* 1. Xóa khoảng trắng ở hai đầu, sửa tại chỗ. Trả về con trỏ tới đầu mới. */
char *cat_trang(char *s)
{
    while (isspace((unsigned char)*s))
        ++s;

    if (*s == '\0') return s;      /* toàn khoảng trắng */

    char *cuoi = s + strlen(s) - 1;

    while (cuoi > s && isspace((unsigned char)*cuoi))
        --cuoi;

    cuoi[1] = '\0';

    return s;
}

/* 2. Chuyển thành chữ thường tại chỗ. */
char *ha_thap(char *s)
{
    for (char *p = s; *p != '\0'; ++p)
        *p = (char)tolower((unsigned char)*p);

    return s;
}

/* 3. Đảo ngược chuỗi tại chỗ. */
char *dao_nguoc(char *s)
{
    size_t n = strlen(s);

    for (size_t i = 0; i + 1 < n - i; ++i) {
        char t = s[i];

        s[i] = s[n - 1 - i];
        s[n - 1 - i] = t;
    }

    return s;
}

/* 4. Kiểm tra chuỗi có bắt đầu bằng tiền tố cho trước. */
int bat_dau_bang(const char *s, const char *tien_to)
{
    return strncmp(s, tien_to, strlen(tien_to)) == 0;
}

/* Kiểm tra chuỗi có kết thúc bằng hậu tố cho trước. */
int ket_thuc_bang(const char *s, const char *hau_to)
{
    size_t ls = strlen(s);
    size_t lh = strlen(hau_to);

    return lh <= ls && strcmp(s + ls - lh, hau_to) == 0;
}
terminal
./tienich
cat_trang("  xin chao  ")  -> [xin chao]
ha_thap("XIN Chao")        -> [xin chao]
dao_nguoc("abcde")         -> [edcba]
bat_dau_bang("bai.c", "bai") -> 1
ket_thuc_bang("bai.c", ".c") -> 1

Tự làm thử

  1. Cài đủ tám hàm trong mystring.c và chạy bộ kiểm thử cho tới khi qua hết.
  2. Cố tình bỏ phép ép unsigned char trong my_strcmp, chạy kiểm thử và xem trường hợp nào hỏng.
  3. Cố tình đảo thứ tự hai câu if trong my_strchr, chạy kiểm thử và xem trường hợp nào hỏng.
  4. Thêm ba mẫu dữ liệu mới vào bộ kiểm thử và xác nhận số lượt tăng lên đúng như tính toán.
  5. Đo tốc độ bốn hàm so với bản chuẩn, lập bảng tỷ lệ như trong bài.
  6. Cài năm hàm tiện ích, viết kiểm thử riêng cho từng cái với đủ trường hợp biên.
  7. Gộp tất cả thành thư viện ba file, viết một Makefile đơn giản để biên dịch và chạy kiểm thử bằng một lệnh.

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

  • Tự cài lại thư viện chuỗi là cách chắc chắn nhất để hiểu chuỗi C, và bộ kiểm thử là phần quan trọng hơn cả phần cài đặt.
  • So kết quả với bản chuẩn, nhân chéo mọi cặp mẫu, và so dấu chứ không so giá trị với nhóm hàm so sánh.
  • Chín trường hợp biên phải có trong bộ kiểm thử, đặc biệt là chuỗi rỗng, tìm byte 0, và ký tự mã lớn hơn 127.
  • Luôn chạy kiểm thử dưới -fsanitize=address,undefined. Qua hết mà không có cờ này thì chưa chứng minh được gì.
  • Bản thư viện nhanh hơn mười tới một trăm lần nhờ đọc theo khối, lệnh véc tơ và thuật toán tốt hơn.