6.4 ฟังก์ชันใน string.h
หาความยาว คัดลอก เปรียบเทียบ และต่อข้อความ เป็นงานที่ทำบ่อยจนภาษา C มีฟังก์ชันสำเร็จรูปให้ใน string.h หน้านี้ใช้สี่ตัวที่เจอในโจทย์บ่อยที่สุด แล้วปิดท้ายด้วยเรื่องที่ทำให้โปรแกรมถูกแต่ได้ Time Limit Exceeded คือเรียก strlen ในเงื่อนไขลูป ซึ่งวัดเวลาจริงให้ดู
strlen หาความยาว
ในหน้า 6.2 นับความยาวด้วยการเดินจนเจอ '\0' strlen ทำแบบเดียวกันให้ ได้จำนวนตัวอักษรที่ไม่นับ '\0'
#include <stdio.h>
#include <string.h>
int main()
{
char word[105];
scanf("%s", word);
int length = strlen(word);
printf("%s has %d letters\n", word, length);
printf("first %c last %c\n", word[0], word[length - 1]);
return 0;
}
banana
banana has 6 letters
first b last a
- ตัวสุดท้ายอยู่ที่ช่อง
length - 1เพราะช่องเริ่มที่ 0 ช่องword[length]คือ'\0' - เก็บผลของ
strlenไว้ในตัวแปรintก่อนใช้เสมอ เหตุผลมีสองข้อ อยู่ในหัวข้อท้ายหน้าและในส่วนลองทำให้พัง strlenต่างจากsizeofที่ใช้กับfgetsในหน้าที่แล้วsizeof wordได้ 105 เสมอเพราะเป็นขนาดของอาเรย์ ส่วนstrlen(word)คือความยาวของข้อความที่อยู่ในอาเรย์ตอนนี้
strcpy คัดลอก
ตัวแปร int คัดลอกด้วย = ได้ แต่อาเรย์ทำแบบนั้นไม่ได้ ลองดู
char name[20];
name = "Somchai";
copy.c:6:10: error: assignment to expression with array type
6 | name = "Somchai";
| ^
compiler บอกว่ากำหนดค่าให้สิ่งที่เป็นอาเรย์ไม่ได้ ต้องคัดลอกทีละช่อง ซึ่ง strcpy(ปลายทาง, ต้นทาง) ทำให้ จำลำดับว่า ปลายทางอยู่หน้า เหมือน ปลายทาง = ต้นทาง
#include <stdio.h>
#include <string.h>
int main()
{
char name[20];
strcpy(name, "Somchai");
printf("%s\n", name);
return 0;
}
Somchai
งานที่ต้องใช้ strcpy จริงๆ คือ "จำคำที่ดีที่สุดไว้" เช่น หาชื่อที่ยาวที่สุดในห้อง แบบเดียวกับหาค่ามากสุดในบทที่ 5 ต่างกันแค่ตอนเจอค่าที่ดีกว่า ต้องคัดลอกทั้งคำมาเก็บแทนการใช้ =
#include <stdio.h>
#include <string.h>
int main()
{
int n;
char name[25], longest[25] = "";
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%s", name);
if (strlen(name) > strlen(longest)) {
strcpy(longest, name);
}
}
printf("%s\n", longest);
return 0;
}
4
Nok Somchai Pim Kanokwan
Kanokwan
longest เริ่มเป็น "" คือสตริงว่าง มีแค่ '\0' ช่องแรก ความยาว 0 ชื่อแรกที่อ่านเข้ามาจึงยาวกว่าเสมอ ทำหน้าที่เหมือนตั้งค่าเริ่มต้นของค่ามากสุดให้น้อยกว่าทุกค่า
ปลายทางต้องใหญ่พอใส่ต้นทางบวก '\0' strcpy ไม่ตรวจให้ ถ้าเล็กไปจะเขียนทับหน่วยความจำส่วนอื่นเงียบๆ
strcmp เปรียบเทียบ
strcmp(a, b) เทียบสองข้อความตามลำดับในพจนานุกรม ได้ผลเป็นตัวเลข
ผลของ strcmp(a, b) | แปลว่า |
|---|---|
| น้อยกว่า 0 | a มาก่อน b |
| เท่ากับ 0 | a กับ b เหมือนกันทุกตัว |
| มากกว่า 0 | a มาหลัง b |
#include <stdio.h>
#include <string.h>
int main()
{
char first[105], second[105];
while (scanf("%s %s", first, second) == 2) {
int result = strcmp(first, second);
if (result < 0) {
printf("%s comes before %s\n", first, second);
} else if (result > 0) {
printf("%s comes after %s\n", first, second);
} else {
printf("%s is the same\n", first);
}
}
return 0;
}
apple apricot
apple app
Zebra apple
mango mango
apple comes before apricot
apple comes after app
Zebra comes before apple
mango is the same
strcmp เทียบจากซ้ายทีละช่อง ช่องแรกที่ต่างกันเป็นตัวตัดสิน โดยดูว่ารหัส ASCII ของใครน้อยกว่า
appleกับapricotเหมือนกันสองตัวแรก ช่องที่ 2 คือpกับrและpมาก่อนappleกับappเหมือนกันจนappหมดก่อน ช่องที่ 3 คือlกับ'\0'ซึ่งมีรหัส 0 น้อยที่สุด คำที่สั้นกว่าจึงมาก่อนZebraมาก่อนappleเพราะ'Z'คือ 90 แต่'a'คือ 97 ตัวพิมพ์ใหญ่ทุกตัวมาก่อนตัวพิมพ์เล็กทุกตัว ถ้าโจทย์ให้เทียบโดยไม่สนตัวพิมพ์ ต้องแปลงเป็นตัวพิมพ์เล็กทั้งสองคำก่อน
ใช้แค่ว่าผลน้อยกว่า เท่ากับ หรือมากกว่า 0 อย่าเขียน strcmp(a, b) == -1 เพราะไม่ได้สัญญาว่าจะได้ -1 พอดี แบบเดียวกับเรื่อง isalpha ในหน้า 6.1
strcat ต่อท้าย
strcat(ปลายทาง, ต้นทาง) เอาต้นทางไปต่อท้ายข้อความที่อยู่ในปลายทาง โดยเริ่มเขียนทับตรง '\0' เดิม แล้วปิดท้ายด้วย '\0' ใหม่
#include <stdio.h>
#include <string.h>
int main()
{
char first[25], last[25], full[55];
scanf("%s %s", first, last);
strcpy(full, first);
strcat(full, " ");
strcat(full, last);
int length = strlen(full);
printf("%s\n", full);
printf("%d\n", length);
return 0;
}
Somchai Jaidee
Somchai Jaidee
14
ขนาดของ full คิดจากกรณีมากสุด ชื่อ 24 ตัว ช่องว่าง 1 นามสกุล 24 และ '\0' อีก 1 รวม 50 ช่อง ประกาศ 55 เผื่อไว้ strcat ไม่ตรวจขนาดให้เหมือน strcpy
ทำไมเรียก strlen ในเงื่อนไขลูปถึงช้า
โจทย์ให้แปลงคำยาวไม่เกิน 1000000 ตัวเป็นตัวพิมพ์ใหญ่ สองโปรแกรมนี้ต่างกันแค่บรรทัดเดียว และได้คำตอบเหมือนกันทุกตัวอักษร
#include <stdio.h>
#include <string.h>
#include <ctype.h>
char word[1000005];
int main()
{
scanf("%s", word);
for (int i = 0; i < strlen(word); i++) {
word[i] = toupper(word[i]);
}
printf("%s\n", word);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <ctype.h>
char word[1000005];
int main()
{
scanf("%s", word);
int length = strlen(word);
for (int i = 0; i < length; i++) {
word[i] = toupper(word[i]);
}
printf("%s\n", word);
return 0;
}
hello
HELLO
hello
HELLO
กับคำสั้นๆ สองแบบเร็วพอกัน แต่พอวัดกับคำยาวที่สุ่มจากตัวพิมพ์เล็ก ได้ตามตารางนี้
| ความยาวคำ | strlen ในเงื่อนไขลูป | เก็บความยาวไว้ก่อนลูป |
|---|---|---|
| 10,000 | 0.017 วินาที | 0.013 วินาที |
| 100,000 | 0.33 วินาที | 0.013 วินาที |
| 200,000 | 1.23 วินาที | 0.014 วินาที |
| 1,000,000 | 30.9 วินาที | 0.024 วินาที |
วัดบนโน้ตบุ๊ก ASUS Vivobook S 14 ซีพียู AMD Ryzen AI 9 HX 370 Windows 11 คอมไพล์ด้วย GCC 14.2 ของ Code::Blocks ตามค่าที่ตั้งในบทที่ 0 (-Wall ไม่มี -O2) แต่ละช่องคือค่ากลางของการรันสามครั้ง นับรวมเวลาเปิดโปรแกรมและอ่านข้อมูลเข้า ซึ่งกินราว 0.013 วินาทีอยู่แล้ว
สาเหตุคือ strlen ไม่ได้จำความยาวไว้ที่ไหน ทุกครั้งที่เรียก มันเดินจากช่อง 0 ไปจนเจอ '\0' ใหม่ คำยาว n ตัวจึงเดิน n ก้าวต่อการเรียกหนึ่งครั้ง และเงื่อนไขลูปถูกเช็กทุกรอบ รวม n + 1 ครั้ง งานทั้งหมดจึงประมาณ n × n ก้าว คำยาวขึ้น 10 เท่า เวลาเพิ่มราว 100 เท่า ดูได้จากแถว 100,000 กับ 1,000,000 ในตาราง ส่วนแบบที่เก็บความยาวไว้ก่อน เดินแค่ n ก้าวตอนเรียก strlen ครั้งเดียว บวกอีก n รอบในลูป
grader ส่วนใหญ่ให้เวลาข้อละ 1 วินาที แบบแรกตกตั้งแต่คำยาวสองแสนตัว ทั้งที่คำตอบถูกทุกตัว
-O2 ช่วยได้บางครั้ง อย่าหวัง
ถ้าลูปแค่อ่าน word ไม่แก้ เช่น นับว่ามีตัว a กี่ตัว คอมไพล์ด้วย -O2 แล้ว compiler ย้าย strlen ออกไปนอกลูปให้เอง วัดได้ 0.013 วินาทีที่ 200,000 ตัว แต่ลูปที่แก้ word แบบในตัวอย่าง compiler ไม่กล้าย้าย ยังใช้ 1.23 วินาทีแม้เปิด -O2 และ Code::Blocks ของน้องไม่ได้เปิด -O2 จึงช้าทั้งสองแบบ เขียนให้เร็วเองตั้งแต่แรกปลอดภัยกว่า
มีสองวิธีที่ใช้ได้เสมอ เลือกแบบไหนก็ได้
- เก็บ
int length = strlen(word);ไว้ก่อนลูป แล้วใช้i < length - ไม่เรียก
strlenเลย ใช้word[i] != '\0'เป็นเงื่อนไขแบบในหน้า 6.2
ถ้าคอมไพล์แบบแรกด้วย -Wextra จะได้คำเตือนเรื่องเทียบ int กับชนิดของ strlen ด้วย ซึ่ง Code::Blocks ที่เปิดแค่ -Wall ไม่แสดง เป็นอีกเหตุผลที่ให้เก็บผลไว้ใน int ก่อน
ลองทำให้พัง
แบบที่ 1 พิมพ์ strlen ด้วย %d ตรงๆ
ในโปรแกรม length แก้แค่บรรทัดที่ 9 ซึ่งเป็น printf บรรทัดแรก ให้ส่ง strlen(word) ตรงๆ แทน length บรรทัดอื่นไม่ต้องแตะ
printf("%s has %d letters\n", word, strlen(word));
length.c:9:21: warning: format '%d' expects argument of type 'int', but argument 3 has type 'size_t' {aka 'long long unsigned int'} [-Wformat=]
9 | printf("%s has %d letters\n", word, strlen(word));
| ~^ ~~~~~~~~~~~~
| | |
| int size_t {aka long long unsigned int}
| %lld
strlen ไม่ได้คืน int แต่คืนชนิดชื่อ size_t ซึ่งบนเครื่องนี้คือจำนวนเต็มไม่ติดลบขนาด 8 byte บนเครื่องที่เขียนเอกสารนี้บังเอิญพิมพ์ banana has 6 letters ถูก แต่เปลี่ยนเครื่องแล้วอาจผิด compiler แนะนำ %lld ซึ่งก็ไม่ตรงชนิดทีเดียว วิธีที่ง่ายและถูกเสมอคือเก็บลง int ก่อนแบบในตัวอย่าง ความยาวในโจทย์ไม่มีทางเกิน int อยู่แล้ว
แบบที่ 2 เทียบสตริงด้วย ==
#include <stdio.h>
int main()
{
char answer[10];
scanf("%s", answer);
if (answer == "yes") {
printf("OK\n");
} else {
printf("NO\n");
}
return 0;
}
answer.c:7:16: warning: comparison with string literal results in unspecified behavior [-Waddress]
7 | if (answer == "yes") {
| ^~
บนเครื่องที่เขียนเอกสารนี้ พิมพ์ yes แล้วได้ NO เพราะ == กับอาเรย์ไม่ได้เทียบตัวอักษร แต่เทียบว่าเป็นอาเรย์ก้อนเดียวกันในหน่วยความจำไหม ซึ่ง answer กับ "yes" อยู่คนละที่เสมอ เรื่องที่อยู่นี้จะเข้าใจชัดในบทที่ 10 ตอนนี้จำว่าเทียบสตริงต้องใช้ strcmp(answer, "yes") == 0
สรุป
strlen(s)ได้ความยาวไม่นับ'\0'เก็บลงintก่อนใช้strcpy(dest, src)คัดลอก ปลายทางอยู่หน้า และต้องใหญ่พอ เพราะอาเรย์ใช้=ไม่ได้strcmp(a, b)ได้น้อยกว่า 0 เท่ากับ 0 หรือมากกว่า 0 ตามลำดับพจนานุกรม ตัวพิมพ์ใหญ่มาก่อนตัวพิมพ์เล็ก ห้ามเทียบสตริงด้วย==strcat(dest, src)ต่อท้าย dest ต้องมีที่พอสำหรับทั้งสองข้อความ- ห้ามเรียก
strlenในเงื่อนไขลูป คำยาว n ตัวจะกลายเป็นงาน n × n เก็บความยาวไว้ก่อน หรือใช้word[i] != '\0'