Sử dụng chuỗi trong C
Chuỗi trong C liên quan rất nhiều đến con trỏ. Khi bạn đã quen sử dụng con trỏ, bạn có thể vận dùng vào xử lý chuỗi hiệu quả hơn so với trong Pascal.
Một chuỗi trong C đơn giản là một mảng kí tự. Dòng sau đây khai báo một mảng có thể lưu giữ một chuỗi lên đến 99 kí tự.
char str[100];
Mảng str lưu giữ kí tự như sau: str[0] là kí tự đầu của mảng, str[1] là kí tự thứ hai, v.v.. Nhưng tại sao một mảng 100 phần tử lại chỉ lưu được có 99 kí tự. Bởi vì C dùng null-terminated strings, nghĩa là kết thúc một chuỗi luôn được đánh dấu bởi kí tự có mã ASCII là 0 (kí tự null), được kí hiệu trong C là ‘\0’
Loại chuỗi này rất khác so với strings trong Pascal. Trong Pascal, mỗi string là một mảng kí tự, trong đó dành ra một byte để lưu giữ số kí tự chứa trong mảng. Cấu trúc này cho phép Pascal thuận lợi hơn khi cần biết độ dài của chuỗi. Pascal chỉ cần trả về giá trị đã lưu giữ, trong khi C cần phải đếm kí tự cho đến khi nó gặp ‘\0’. Nghĩa là C sẽ chậm hơn Pascal nhiều trong một số trường hợp, nhưng trong một số trường hợp khác nó lại nhanh hơn, như chúng ta sẽ thấy trong ví dụ dưới đây.
Tất cả các hàm hỗ trợ xử lý chuỗi đều được đặt trong thư viện <string.h> (trên một số hệ thống là <strings.h>). Bởi vì C bản thân nó không hỗ trợ các công cụ xử lý strings, do đó bạn sẽ cảm thấy hơi bất tiện trong việc viết mã. Ví dụ, trong Pascal, muốn copy một chuỗi sang một chuỗi khác, bạn làm rất dễ dàng như sau:
program samp;
var s1,s2:string;
begin
s1:='hello';
s2:=s1;
end.
var s1,s2:string;
begin
s1:='hello';
s2:=s1;
end.
Trong C, như đã biết, chúng ta không thể đơn giản gán một mảng cho một mảng khác, mà phải copy từng phần tử. Thư viên string chứa hàmstrcpy cho phép bạn làm điều này. Đoạn mã sau cho thấy cách sử dụng hàm strcpy để copy chuỗi:
#include <string.h>
void main()
{
char s1[100],s2[100];
strcpy(s1,"hello"); /* copy "hello" vào s1 */
strcpy(s2,s1); /* copy s1 vào s2 */
}
void main()
{
char s1[100],s2[100];
strcpy(s1,"hello"); /* copy "hello" vào s1 */
strcpy(s2,s1); /* copy s1 vào s2 */
}
Hàm strcpy được sử dụng khi bạn cần khởi tạo một chuỗi. Một khác biệt nữa giữa Pascal và C là so sánh chuỗi. Trong Pascal, so sánh chuỗi được xây dựng ngay trong cơ sở ngôn ngữ (các toán tử <, >, =, v.v… làm việc với chuỗi) . Còn trong C, bạn phải dùng hàm strcmp trong thư viện string, so sành hai chuỗi và trả về một số nguyên cho biết kết quả so sánh. Kết quả bằng 0 nghĩa là hai chuỗi bằng nhau, bằng giá trị âm nghĩa làs1 < s2, bằng giá trị dương nghĩa là s1 > s2. Trong Pascal, đoạn mã như sau:
program samp;
var s1,s2:string;
begin
readln(s1);
readln(s2);
if s1=s2 then
writeln('bằng nhau')
else
if (s1<s2) then
writeln('s1 bé hơn s2')
else
writeln('s1 lớn hơn s2');
end.
var s1,s2:string;
begin
readln(s1);
readln(s2);
if s1=s2 then
writeln('bằng nhau')
else
if (s1<s2) then
writeln('s1 bé hơn s2')
else
writeln('s1 lớn hơn s2');
end.
Đây là đoạn mã tương đương trong C:
#include <stdio.h>
#include <string.h>
void main()
{
char s1[100],s2[100];
gets(s1);
gets(s2);
if (strcmp(s1,s2)==0)
printf("bằng nhau\n");
else if (strcmp(s1,s2)<0)
printf("s1 bé hơn s2\n");
else
printf("s1 lớn hơn s2\n");
}
#include <string.h>
void main()
{
char s1[100],s2[100];
gets(s1);
gets(s2);
if (strcmp(s1,s2)==0)
printf("bằng nhau\n");
else if (strcmp(s1,s2)<0)
printf("s1 bé hơn s2\n");
else
printf("s1 lớn hơn s2\n");
}
Một số hàm thông dụng khác trong thư viện string là strlen, trả về độ dài của chuỗi; strcat nối hai chuỗi. Bạn có thể tham khảo thêm phần help của Turbo C. Lưu ý rằng nhiều khả năng xử lý chuỗi của Pascal, như copy, delete, pos, v.v… không có trong C. Do đó bạn cần phải tự xây dựng một số hàm xử lý chuỗi cho riêng mình. Hãy bắt đầu với việc viết lại hàm strlen. Sau đây là một cách viết mã theo phong cách các bạn đã quen dùng với Pascal:
int strlen(char s[])
{
int x;
x=0;
while (s[x] != '\0')
x=x+1;
return(x);
}
{
int x;
x=0;
while (s[x] != '\0')
x=x+1;
return(x);
}
Hầu hết các lập trình viên C không thích cách tiếp cận này bởi vì nó có vẻ kém hiệu quả. Thay vào đó, họ thường dùng cách tiếp cận dựa trên con trỏ:
int strlen(char *s)
{
int x=0;
while (*s != '\0')
{
x++;
s++;
}
return(x);
}
{
int x=0;
while (*s != '\0')
{
x++;
s++;
}
return(x);
}
Bạn có thể viết gọn lại như sau:
int strlen(char *s)
{
int x=0;
while (*s++)
x++;
return(x);
}
{
int x=0;
while (*s++)
x++;
return(x);
}
Có lẽ một chuyên gia về C còn có thể làm đoạn mã trên ngắn hơn nữa.
Tuy nhiên thực tế khi chạy và so sánh ba chương trình trên, bạn sẽ thấy thời gian thực hiện của chúng như nhau hoặc sai khác rất nhỏ. Điều đó có nghĩa là, bạn nên viết mã theo bất kì cách nào mã bạn thấy dễ hiểu nhất. Con trỏ thông thường làm chương trình chạy nhanh hơn, nhưng hàmstrlen trên lại không thuộc về trường hợp đó.
Chúng ta hãy tiếp tục với hàm strcopy:
strcpy(char s1[],char s2[])
{
int x;
for (x=0; x<=strlen(s2); x++)
s1[x]=s2[x];
}
{
int x;
for (x=0; x<=strlen(s2); x++)
s1[x]=s2[x];
}
Lưu ý dấu <= bởi vì đoạn mã cần copy cả kí tự ‘\0’. Một điều nữa là đoạn mã này rất kém hiệu quả, bởi vì hàm strlen được gọi lại mỗi khi bạn lặp lại vòng for. Để giải quyết vấn đề này, bạn có thể dùng đoạn mã dưới đây:
strcpy(char s1[],char s2[])
{
int x,len;
len=strlen(s2);
for (x=0; x<=len; x++)
s1[x]=s2[x];
}
{
int x,len;
len=strlen(s2);
for (x=0; x<=len; x++)
s1[x]=s2[x];
}
Còn đây là phiên bản sử dụng con trỏ:
strcpy(char *s1,char *s2)
{
while (*s2 != '\0')
{
*s1 = *s2;
s1++;
s2++;
}
}
{
while (*s2 != '\0')
{
*s1 = *s2;
s1++;
s2++;
}
}
Bạn có thể viết ngắn hơn nữa:
strcpy(char *s1,char *s2)
{
while (*s2)
*s1++ = *s2++;
}
{
while (*s2)
*s1++ = *s2++;
}
Thậm chí bạn có thể viết là while(*s1++=*s2++); Lần này, khi chạy thử, bạn sẽ thấy phiên bản thứ nhất chạy rất chậm, trong khi đó phiên bản thứ ba và thứ tư nhanh hơn so với phiên bản thứ hai. Trong trường hợp này, con trỏ thực sự hiệu quả.
Sử dụng con trỏ đối với chuỗi đôi khi tạo ra hiệu quả rõ rệt về tốc độ. Ví dụ giả sử bạn muốn loại bỏ khoảng trắng ở đầu trong một chuỗi. Trong Pascal, bạn thường phải sử dụng hàm delete như sau:
program samp;
var s:string;
begin
readln(s);
while (s[1] <> ' ') and (length(s)>0) do
delete(s,1,1);
writeln(s);
end;
var s:string;
begin
readln(s);
while (s[1] <> ' ') and (length(s)>0) do
delete(s,1,1);
writeln(s);
end;
Đoạn mã trên kém hiệu quả bởi vì nó dời toàn bộ mảng kí tự đi một vị trí mỗi khi tìm thấy một khoảng trắng ở đầu chuỗi. Cách tốt hơn là như sau:
program samp;
var s:string;
x:integer;
begin
readln(s);
x:=0;
while (s[x+1] <> ' ') and (x<length(s)) do
x:=x+1;
delete(s,1,x);
writeln(s);
end;
var s:string;
x:integer;
begin
readln(s);
x:=0;
while (s[x+1] <> ' ') and (x<length(s)) do
x:=x+1;
delete(s,1,x);
writeln(s);
end;
Với kỹ thuật trên, mỗi kí tự chỉ cần dịch chuyển một lần. Trong C, thậm chí bạn có thể không cần phải dịch chuyển kí tự nào như sau:
#include <stdio.h>
#include <string.h>
void main()
{
char s[100],*p;
gets(s);
p=s;
while (*p==' ')
p++;
printf("%s\n",p);
}
#include <string.h>
void main()
{
char s[100],*p;
gets(s);
p=s;
while (*p==' ')
p++;
printf("%s\n",p);
}
Đoạn mã này nhanh hơn nhiều so với Pascal, đặc biệt đối với những chuỗi dài.
Thực hành nhiều sẽ giúp bạn học được thêm những thủ thuật với chuỗi.
Những chuỗi hằng
Bạn hãy thử hai đoạn chương trình sau:
Đoạn 1:
{
char *s;
s="hello";
printf("%s\n",s);
}
char *s;
s="hello";
printf("%s\n",s);
}
Đoạn 2:
{
char s[100];
strcpy(s,"hello");
printf("%s\n",s);
}
char s[100];
strcpy(s,"hello");
printf("%s\n",s);
}
Hai đoạn mã trên đưa ra cùng một kết quả, nhưng cách họat động của chúng hoàn toàn khác nhau. Trong đoạn 2, bạn không thể viết s=”hello”;. Để hiểu sự khác nhau, bạn cần phải biết họat động của bảng chuỗi hằng (string constant table) trong C.
Khi chương trình được thực thi, trình biên dịch tạo ra một file object, chứa mã máy và một bảng chứa tất cả các chuỗi hằng khai báo trong chương trình. Trong đoạn 1, lệnh s=”hello”; xác định rằng s chỉ đến địa chỉ của chuỗi hello trong bảng chuỗi hằng. Bởi vì chuỗi này nằm trong bảng chuỗi hằng, và là một bộ phận trong mã exe, nên bạn không thể thay đổi được nó. Bạn chỉ có thể dùng nó theo kiểu chỉ-đọc (read-only).
Trong đoạn 2, chuỗi hello cũng tồn tại trong bảng chuỗi hằng, do đó bạn có thể copy nó vào mảng kí tự tên là s. Bởi vì s không phải là một con trỏ, lệnh s=”hello”; sẽ không làm việc.
Lưu ý khi sử dụng String với lệnh malloc
Giả sử bạn viết đoạn chương trình sau:
void main()
{
char *s;
s=(char *) malloc (100);
s="hello";
free(s);
}
{
char *s;
s=(char *) malloc (100);
s="hello";
free(s);
}
Đoạn mã trên biên dịch được, nhưng nó tạo ra một lỗi “segmentation fault” ngay ở dòng free. Bởi vì lệnh malloc tạo ra một khối bộ nhớ 100 bytes và trỏ s vào đó, nhưng dòng s=”hello”; lại trỏ s đến một chuỗi trong bảng chuỗi hằng, còn khối bộ nhớ 100 bytes bị bỏ qua. Do đó lệnh free gặp lỗi vì không thể giải phóng một khối bộ nhớ trong khu vực mã exe.
Đoạn mã đúng phải là như sau:
void main()
{
char *s;
s=(char *) malloc (100);
strcpy(s,"hello");
free(s);
}
{
char *s;
s=(char *) malloc (100);
strcpy(s,"hello");
free(s);
}
Kết luận
Đến đây, hy vọng các bạn có thể dùng C để giải quyết được những bài toán tin học như đối với Pascal. Trong khuôn khổ bài viết này chỉ trình bày được một số nội dung cơ bản mà các bạn thường hay phải dùng khi giải toán; thông qua những đối chiếu, so sánh với Pascal, là ngôn ngữ lập trình mà các bạn đã biết. Ngày nay, C cũng như C++ là những ngôn ngữ đựoc sử dụng nhiều nhất do tính hiệu quả rất cao; mong rằng bài viết sẽ giúp các bạn tìm hiểu thêm về chúng.
0 nhận xét