Bài 13.218 phút đọc
Con trỏ và chuỗi
Sau bài này bạn sẽ làm được
- Duyệt và xử lý chuỗi hoàn toàn bằng con trỏ
- Tạo lát cắt chuỗi mà không sao chép
- Viết trình quét chuỗi đơn giản
- Nhận ra ưu và nhược của cách tiếp cận bằng con trỏ
Chuỗi là nơi con trỏ tỏa sáng nhất trong C. Vì chuỗi tự kết thúc bằng byte 0, bạn không cần biến đếm, không cần độ dài, chỉ cần một con trỏ chạy. Bài này gom lại những mẫu mà bạn sẽ dùng suốt đời viết C.
#Duyệt chuỗi bằng con trỏ
duyet.c
/* Mẫu cơ bản nhất */
void in_chuoi(const char *s)
{
while (*s != '\0')
putchar(*s++);
}
/* Cách viết cô đọng, gặp khắp nơi trong mã C */
void in_chuoi2(const char *s)
{
while (*s)
putchar(*s++);
}
/* Đếm ký tự thỏa điều kiện */
size_t dem_nguyen_am(const char *s)
{
size_t n = 0;
for (; *s; ++s)
if (strchr("aeiouAEIOU", *s) != NULL)
++n;
return n;
}So sánh với cách duyệt bằng chỉ số
| Bằng chỉ số | Bằng con trỏ | |
|---|---|---|
| Cú pháp | for (size_t i = 0; s[i]; ++i) | for (; *s; ++s) |
| Cần biến phụ | Có, biến i | Không |
| Biết vị trí | Có, chính là i | Phải trừ con trỏ đầu |
| Trả về vị trí tìm thấy | Trả về i | Trả về con trỏ, tự nhiên hơn |
| Duyệt song song hai chuỗi | Dễ, dùng chung i | Cần hai con trỏ |
Thư viện chuẩn chọn cách con trỏ cho mọi hàm chuỗi, và đó là lý do strchr và strstr trả về con trỏ chứ không phải chỉ số.
#Lát cắt không sao chép
Vì chuỗi C chỉ là một con trỏ và một quy ước kết thúc, bạn tạo được lát cắt của phần đuôi mà không tốn một byte nào.
lat-cat.c
const char *s = "/home/an/bai.c";
const char *ten = strrchr(s, '/') + 1; /* trỏ vào "bai.c", không sao chép gì */
printf("%s\n", ten); /* bai.c */
/* Bỏ khoảng trắng đầu chuỗi cũng chỉ là dịch con trỏ */
const char *p = " xin chao";
while (*p == ' ') ++p;
printf("[%s]\n", p); /* [xin chao] */Cấu trúc lát cắt
lat.c
/* Nhiều dự án C hiện đại dùng cấu trúc này thay cho chuỗi kết thúc byte 0. */
typedef struct {
const char *dau;
size_t len;
} Lat;
static Lat lat_tao(const char *s, size_t len)
{
Lat l = { s, len };
return l;
}
static void lat_in(Lat l)
{
printf("%.*s", (int)l.len, l.dau);
}
static int lat_bang(Lat a, Lat b)
{
return a.len == b.len && memcmp(a.dau, b.dau, a.len) == 0;
}#Mẫu trình quét chuỗi
Đây là mẫu bạn sẽ dùng khi phải phân tích một chuỗi có cấu trúc: dòng cấu hình, biểu thức toán học, dữ liệu định dạng đơn giản.
quet.c
#include <ctype.h>
#include <stdio.h>
/* Bỏ khoảng trắng, trả về con trỏ mới. */
static const char *bo_trang(const char *p)
{
while (isspace((unsigned char)*p))
++p;
return p;
}
/* Đọc một số nguyên, cập nhật con trỏ qua tham số ra. */
static int doc_so(const char **pp, long *ra)
{
const char *p = bo_trang(*pp);
if (!isdigit((unsigned char)*p))
return -1;
long v = 0;
while (isdigit((unsigned char)*p)) {
v = v * 10 + (*p - '0');
++p;
}
*ra = v;
*pp = p;
return 0;
}
/* Đọc một ký tự cụ thể, ví dụ dấu phẩy. */
static int doc_ky_tu(const char **pp, char c)
{
const char *p = bo_trang(*pp);
if (*p != c) return -1;
*pp = p + 1;
return 0;
}
int main(void)
{
const char *s = " 12 , 34,56 ";
long a, b, c;
if (doc_so(&s, &a) == 0
&& doc_ky_tu(&s, ',') == 0
&& doc_so(&s, &b) == 0
&& doc_ky_tu(&s, ',') == 0
&& doc_so(&s, &c) == 0)
printf("%ld %ld %ld\n", a, b, c);
else
fprintf(stderr, "Sai dinh dang\n");
return 0;
}terminal
./quet
12 34 56
#Tách chuỗi thành từ
Cách 1: không phá chuỗi gốc
tach-1.c
#include <string.h>
void tach_tu(const char *s)
{
const char *TRANG = " \t\n";
while (*s) {
s += strspn(s, TRANG); /* bỏ dấu phân cách */
size_t len = strcspn(s, TRANG); /* đo độ dài từ */
if (len == 0) break;
printf("[%.*s]\n", (int)len, s);
s += len;
}
}Cách 2: strtok, phá chuỗi gốc
tach-2.c
char s[] = " xin chao the gioi ";
for (char *tu = strtok(s, " \t\n"); tu != NULL; tu = strtok(NULL, " \t\n"))
printf("[%s]\n", tu);terminal
./tach
[xin] [chao] [the] [gioi]
Cách 3: tự tách bằng con trỏ, giữ được ô rỗng
tach-3.c
/* Tách theo đúng một ký tự phân cách, giữ nguyên ô rỗng.
Phù hợp cho dữ liệu CSV đơn giản. */
size_t tach(char *s, char pc, char **ra, size_t co)
{
size_t n = 0;
if (co == 0) return 0;
ra[n++] = s;
for (char *p = s; *p; ++p)
if (*p == pc) {
*p = '\0';
if (n < co) ra[n++] = p + 1;
}
return n;
}
char csv[] = "a,,b,c";
char *o[8];
size_t n = tach(csv, ',', o, 8);
for (size_t i = 0; i < n; ++i)
printf("[%s]\n", o[i]); /* [a] [] [b] [c] */#Ưu và nhược của cách dùng con trỏ
| Ưu điểm | Nhược điểm |
|---|---|
| Không cần biến đếm, mã ngắn hơn | Khó biết đang ở vị trí nào nếu cần |
| Cắt chuỗi tốn thời gian hằng số | Con trỏ chỉ hợp lệ khi chuỗi gốc còn sống |
| Khớp với cách thư viện chuẩn làm việc | Người mới đọc khó hơn |
| Không cấp phát, không sao chép | Dễ vô tình sửa chuỗi gốc khi không định |
Trộn hai phong cách trong một hàm
size_t dem(const char *s, char c)
{
size_t n = 0;
for (size_t i = 0; s[i]; ++i)
if (*(s + i) == c) /* lúc chỉ số, lúc con trỏ */
++n;
return n;
}Chọn một phong cách và giữ nhất quán
size_t dem(const char *s, char c)
{
size_t n = 0;
for (; *s; ++s)
if (*s == c) ++n;
return n;
}Tự làm thử
- Viết hàm in chuỗi ngược bằng con trỏ, không dùng chỉ số nào.
- Cài hàm tách phần mở rộng của tên file bằng
strrchr, xử lý đủ trường hợp biên. - Cài cấu trúc
Latvới ba hàm tạo, in và so sánh, dùng nó để tách một dòng cấu hình. - Cài trình quét đọc dãy số cách nhau bằng dấu phẩy, thử với dữ liệu hợp lệ và không hợp lệ.
- Tách một chuỗi thành từ bằng cả ba cách trong bài, so sánh kết quả với dữ liệu có nhiều khoảng trắng liền nhau.
- Gọi
strtokvới hằng chuỗi và quan sát chương trình sập, rồi gọi lồng hai vòngstrtokvà quan sát kết quả sai. - Tạo tình huống con trỏ treo như ví dụ trong bài, chạy dưới
-fsanitize=addressvà đọc thông báo.
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
- Mẫu duyệt chuỗi bằng con trỏ là
while (*s) ... ++s;, không cần biến đếm và không cầnstrlen. - Cắt phần đuôi chuỗi chỉ là dịch con trỏ, tốn thời gian hằng số. Cắt phần đầu thì cần ghi byte 0 hoặc mang theo độ dài.
- Định dạng
%.*sin đúng số ký tự cho trước, là cầu nối giữa lát cắt và hàm thư viện chuẩn. - Trình quét dùng
const char **làm tham số ra để đẩy con trỏ của người gọi tiến lên sau mỗi bước. - Tránh
strtok: nó sửa chuỗi gốc, giữ trạng thái static và nuốt ô rỗng. Dùngstrspnvớistrcspnthay thế.