[0]zero-index clytie0x/zero-index

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)แปลว่า
น้อยกว่า 0a มาก่อน b
เท่ากับ 0a กับ b เหมือนกันทุกตัว
มากกว่า 0a มาหลัง 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,0000.017 วินาที0.013 วินาที
100,0000.33 วินาที0.013 วินาที
200,0001.23 วินาที0.014 วินาที
1,000,00030.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'