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);
#endifmystring.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ợp | Vì sao quan trọng | Hàm nào dễ sai |
|---|---|---|
| Chuỗi rỗng | Vòng lặp không chạy lần nào, mọi phép trừ độ dài đều nguy hiểm | Tất cả |
| Cả hai đối số đều rỗng | Hai điều kiện thoát cùng đúng ở lượt đầu | strcmp, strstr |
| Tìm chính byte 0 | Byte kết thúc được coi là phần của chuỗi | strchr, strrchr |
| Chuỗi con dài hơn chuỗi lớn | Vòng lặp ngoài kết thúc trước khi vòng trong xong | strstr |
| Chuỗi con rỗng | Chuẩn quy định trả về ngay chuỗi lớn | strstr |
| Ký tự mã lớn hơn 127 | char có dấu làm kết quả so sánh đảo dấu | strcmp, strncmp |
| n bằng 0 | Vòng lặp không chạy, phải trả về 0 | strncmp, strncpy |
| Nhiều lần khớp chồng lấn | Vị trí trả về phải là lần đầu tiên | strstr |
| Khớp một phần rồi thất bại | Phải quay lại đúng vị trí kế tiếp, không nhảy quá xa | strstr |
#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ện | Hiệu quả |
|---|---|
| Đọc tám hoặc ba hai byte một lần thay vì từng byte | Giả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ối | Khô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ình | Má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 strstr | Tuyế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") -> 1Tự làm thử
- Cài đủ tám hàm trong
mystring.cvà chạy bộ kiểm thử cho tới khi qua hết. - Cố tình bỏ phép ép
unsigned chartrongmy_strcmp, chạy kiểm thử và xem trường hợp nào hỏng. - Cố tình đảo thứ tự hai câu
iftrongmy_strchr, chạy kiểm thử và xem trường hợp nào hỏng. - 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.
- Đo tốc độ bốn hàm so với bản chuẩn, lập bảng tỷ lệ như trong bài.
- 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.
- 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.