[0]zero-index clytie0x/zero-index

15.4 string ของ C++

ในบทที่ 6 ข้อความคืออาเรย์ของ char ต้องจองขนาดเผื่อไว้ก่อน ต้องเหลือช่องให้ '\0' คัดลอกด้วย strcpy ต่อด้วย strcat เทียบด้วย strcmp และระวังเขียนเกินอาเรย์ตลอดเวลา C++ มีชนิด string ที่จัดการเรื่องพวกนี้ให้ทั้งหมด ยืดหดได้เอง ใช้ + ต่อ ใช้ == เทียบได้ตรงๆ และมีคำสั่งตัดและค้นหาข้อความในตัว โจทย์ข้อความที่เคยยาวครึ่งหน้าจะเหลือไม่กี่บรรทัด

ประกาศ รับ และต่อข้อความ

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string first, last; // (1)!
    cin >> first >> last; // (2)!
    string full = first + " " + last; // (3)!
    cout << full << '\n';
    cout << full.size() << '\n'; // (4)!
    return 0;
}
  1. ไม่ต้องบอกขนาด string ยืดให้พอดีกับข้อความเอง ตอนประกาศเป็นข้อความว่าง ยาว 0 ตัว
  2. อ่านทีละคำเหมือน %s คือข้ามช่องว่างข้างหน้า แล้วอ่านจนเจอช่องว่างหรือขึ้นบรรทัดใหม่ คำจะยาวแค่ไหนก็ไม่เขียนเกิน
  3. + คือเอาข้อความมาต่อกัน ได้ข้อความใหม่ ต่อกับข้อความในเครื่องหมายคำพูดหรือกับ char ก็ได้
  4. size() คือจำนวนตัวอักษร เหมือน strlen ไม่นับ '\0' และไม่ต้องเดินนับใหม่ทุกครั้งแบบ strlen เพราะ string จำความยาวของตัวเองไว้แล้ว
Somchai Jaidee
Somchai Jaidee
14

เทียบกับสิ่งที่ต้องเขียนในบทที่ 6

งานอาเรย์ของ charstring
ประกาศchar name[105];string name;
รับหนึ่งคำscanf("%s", name);cin >> name;
ความยาวstrlen(name)name.size()
คัดลอกstrcpy(copy, name);copy = name;
ต่อท้ายstrcat(name, "!");name += "!";
เท่ากันไหมstrcmp(a, b) == 0a == b
มาก่อนในพจนานุกรมไหมstrcmp(a, b) < 0a < b
ตัวที่ iname[i]name[i]
รับทั้งบรรทัดfgets แล้วตัด '\n' เองgetline(cin, name);

คำสั่งที่มีจุดคั่นอย่าง name.size() อ่านว่า "ความยาวของ name" เป็นคำสั่งที่ติดมากับตัวแปร string แต่ละตัว คล้ายการเข้าถึงช่องของ struct ในบทที่ 11 ต่างกันตรงที่มีวงเล็บเพราะเป็นการสั่งให้ทำงาน ไม่ใช่ช่องเก็บค่า

เทียบข้อความด้วย == และ <

โปรแกรมเดียวกับตัวอย่าง strcmp ในหน้า 6.4 เขียนด้วย string

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string first, second;
    while (cin >> first >> second) {
        if (first == second) {
            cout << first << " is the same\n";
        } else if (first < second) {
            cout << first << " comes before " << second << '\n';
        } else {
            cout << first << " comes after " << second << '\n';
        }
    }
    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 ทุกบรรทัด เพราะใช้กฎเดียวกัน เทียบจากซ้ายทีละตัว ตัวแรกที่ต่างเป็นตัวตัดสิน คำที่เป็นส่วนต้นของอีกคำมาก่อน และตัวพิมพ์ใหญ่ทุกตัวมาก่อนตัวพิมพ์เล็กทุกตัว ต่างกันแค่เขียนเป็นเครื่องหมายที่อ่านรู้เรื่องทันที == != < > <= >= ใช้ได้ครบ

ความยาวและตัวอักษรทีละตัว

เข้าถึงตัวที่ i ด้วย name[i] เหมือนอาเรย์ เริ่มที่ช่อง 0 ตัวสุดท้ายอยู่ที่ช่อง size() - 1 และแก้ทีละตัวได้

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string name;
    cin >> name;
    int n = name.size(); // (1)!
    name[0] = toupper(name[0]);
    int vowels = 0;
    for (int i = 0; i < n; i++) {
        if (name[i] == 'a' || name[i] == 'e' || name[i] == 'i' || name[i] == 'o' || name[i] == 'u') {
            vowels++;
        }
    }
    cout << name << '\n';
    cout << "first " << name[0] << " last " << name[n - 1] << '\n';
    cout << vowels << " vowels\n";
    string line(n, '-'); // (2)!
    cout << line << '\n';
    return 0;
}
  1. เก็บความยาวลง int ไว้ก่อน แล้วใช้ n ทั้งในลูปและตอนหาตัวสุดท้าย เหตุผลอยู่ในส่วนลองทำให้พังแบบที่ 1 และ 4
  2. สร้าง string ที่มีตัวอักษร '-' ซ้ำกัน n ตัว ใช้ทำเส้นคั่นหรือแถวของดาวได้
malee
Malee
first M last e
3 vowels
-----

toupper จาก ctype.h ในหน้า 6.1 ใช้กับ name[0] ได้ตรงๆ เพราะ name[0] ก็คือ char ตัวหนึ่ง string ไม่ได้เก็บ '\0' ให้เราเห็น จึงไม่ต้องวนจนเจอ '\0' แบบบทที่ 6 แต่ใช้ความยาวเป็นตัวหยุดลูปแทน

substr ตัดข้อความบางส่วน

s.substr(start, count) คืนข้อความใหม่ที่เริ่มจากช่อง start ยาว count ตัว ถ้าไม่ใส่ count จะเอาไปจนสุดข้อความ ตัวอย่างคือวันที่ในรูปแบบ ปี-เดือน-วัน ซึ่งทุกส่วนอยู่ตำแหน่งตายตัว

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string date;
    cin >> date;
    string year = date.substr(0, 4);
    string month = date.substr(5, 2);
    string day = date.substr(8, 2);
    cout << day << '/' << month << '/' << year << '\n';
    cout << date.substr(5) << '\n';
    return 0;
}
2026-03-07
07/03/2026
03-07
ช่อง0123456789
ตัวอักษร2026-03-07

เลข 0 ข้างหน้า 03 กับ 07 อยู่ครบ เพราะเราเก็บเป็นข้อความ ไม่ได้แปลงเป็นตัวเลข ถ้าอ่านด้วย %d จะได้ 3 กับ 7 แล้วต้องเติม 0 กลับเองแบบหน้า 2.2 โจทย์ที่ข้อมูลเป็นรหัส เบอร์โทร หรือเลขทะเบียน ที่ไม่ได้เอาไปคำนวณ เก็บเป็น string ง่ายกว่า

find ค้นหาข้อความ

s.find(x) หาว่า x ซึ่งจะเป็น char หรือข้อความก็ได้ ปรากฏครั้งแรกที่ช่องไหนของ s ถ้าไม่เจอ จะคืนค่าพิเศษชื่อ string::npos (no position) ต้องเช็กค่านี้ก่อนเอาตำแหน่งไปใช้ทุกครั้ง

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string email;
    while (cin >> email) {
        size_t at = email.find('@'); // (1)!
        if (at == string::npos) {
            cout << email << " is not an email\n";
        } else {
            cout << "user " << email.substr(0, at) << ", domain " << email.substr(at + 1) << '\n';
        }
    }
    return 0;
}
  1. ตำแหน่งที่ find คืนมามีชนิด size_t ซึ่งเป็นจำนวนเต็มไม่ติดลบแบบเดียวกับที่ strlen คืนในหน้า 6.4 เก็บด้วยชนิดนี้แล้วเทียบกับ string::npos ตรงๆ ปลอดภัยที่สุด
malee@school.ac.th
somchai
p.ploy@mail.com
user malee, domain school.ac.th
somchai is not an email
user p.ploy, domain mail.com

ใส่ตำแหน่งเริ่มค้นเป็นตัวที่สองได้ s.find(x, start) จะเริ่มหาตั้งแต่ช่อง start เป็นต้นไป ใช้หาทุกตำแหน่งที่คำหนึ่งปรากฏ โดยเริ่มรอบถัดไปหลังตำแหน่งที่เพิ่งเจอ

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string text, word;
    cin >> text >> word;
    int found = 0;
    size_t pos = text.find(word);
    while (pos != string::npos) {
        cout << "found at " << pos << '\n';
        found++;
        pos = text.find(word, pos + 1);
    }
    cout << found << " times\n";
    return 0;
}
banana ana
found at 1
found at 3
2 times

ana ใน banana เจอที่ช่อง 1 และช่อง 3 สองครั้งนี้ซ้อนกันอยู่ตรงตัว a ที่ช่อง 3 การเริ่มรอบถัดไปที่ pos + 1 จึงนับแบบซ้อนกันได้ ถ้าโจทย์ห้ามนับซ้อน ให้เริ่มรอบถัดไปที่ pos + word.size() แทน จะได้แค่ครั้งเดียว อ่านโจทย์ให้ดีว่าต้องการแบบไหน

getline รับทั้งบรรทัด

cin >> หยุดที่ช่องว่างเหมือน %s ถ้าต้องการทั้งบรรทัดรวมช่องว่าง ใช้ getline(cin, line) ซึ่งต่างจาก fgets ในหน้า 6.3 ตรงที่ไม่เก็บ '\n' ไว้ในข้อความ จึงไม่ต้องตัดเอง และไม่ต้องกำหนดขนาด

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string line;
    int count = 0;
    while (getline(cin, line)) {
        count++;
        cout << count << " [" << line << "] " << line.size() << '\n';
    }
    return 0;
}
rice 2 kg
eggs 10
fish sauce
1 [rice 2 kg] 9
2 [eggs 10] 7
3 [fish sauce] 10

วงเล็บเหลี่ยมใส่ไว้ให้เห็นขอบของข้อความ ไม่มี '\n' ติดมา และความยาวนับเฉพาะตัวอักษรในบรรทัด while (getline(cin, line)) อ่านจนหมดข้อมูลแบบเดียวกับ while (cin >> x)

ไฟล์จาก Windows บน grader ที่เป็น Linux

หน้า 6.3 เล่าไว้ว่าไฟล์ที่สร้างบน Windows จบบรรทัดด้วย '\r' กับ '\n' และ grader บน Linux ไม่แปลงให้ getline ตัด '\n' ทิ้งแต่ไม่ตัด '\r' ถ้าโจทย์ไหนสงสัยว่าจะเจอ ให้ตัดเองด้วย if (line.size() > 0 && line[line.size() - 1] == '\r') line.pop_back(); โดย pop_back() คือลบตัวสุดท้ายทิ้งหนึ่งตัว

กับดัก cin >> ตามด้วย getline

กับดักเดียวกับ scanf ตามด้วย fgets ในหน้า 6.3 ย้ายมาอยู่ใน C++ ครบทุกอย่าง

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int room;
    string name;
    cin >> room;
    getline(cin, name);
    cout << "room " << room << '\n';
    cout << "name [" << name << "]\n";
    return 0;
}
5
Somchai Jaidee
room 5
name []

cin >> room อ่าน 5 แล้วหยุดก่อน '\n' ท้ายบรรทัดแรก getline เจอ '\n' นั้นทันที จึงได้บรรทัดว่างแล้วจบ ชื่อจริงไม่ถูกอ่านเลย ต่างจาก fgets แค่ตรงที่คราวนี้ได้ข้อความว่างเปล่า ไม่ใช่ '\n' หนึ่งตัว

วิธีแก้คืออ่านทิ้งให้ถึง '\n' ก่อนเรียก getline แบบเดียวกับลูป getchar ในหน้า 6.3 ใน C++ ใช้ cin.ignore

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int room;
    string name;
    cin >> room;
    cin.ignore(1000000, '\n'); // (1)!
    getline(cin, name);
    cout << "room " << room << '\n';
    cout << "name [" << name << "]\n";
    return 0;
}
  1. อ่านทิ้งไปเรื่อยๆ จนกว่าจะทิ้ง '\n' ไปแล้วหนึ่งตัว หรือจนทิ้งครบหนึ่งล้านตัว เลขหนึ่งล้านแค่เป็นเพดานที่ใหญ่พอ บรรทัดแรกของโจทย์ไม่มีทางยาวขนาดนั้น
5
Somchai Jaidee
room 5
name [Somchai Jaidee]

น้องอาจเห็นโค้ดในเว็บอื่นเขียน cin.ignore(); เฉยๆ ซึ่งทิ้งแค่หนึ่งตัว ใช้ได้เฉพาะเมื่อหลังเลข 5 มี '\n' ตามมาทันที ถ้ามีช่องว่างเกินท้ายบรรทัด หรือมี '\r' จากไฟล์ Windows ตัวที่ถูกทิ้งจะเป็นตัวนั้นแทน แล้ว getline ก็ได้บรรทัดว่างเหมือนเดิม แบบที่ใส่ '\n' ปลอดภัยกว่า

จำง่ายๆ

หลัง cin >> ถ้าคำสั่งอ่านถัดไปคือ getline ต้องมี cin.ignore(1000000, '\n'); คั่นเสมอ ส่วน cin >> ตามด้วย cin >> ไม่มีปัญหา เพราะ >> ข้ามช่องว่างและ '\n' ข้างหน้าให้เอง

ลองทำให้พัง

แบบที่ 1 เทียบ int กับ size() ตรงๆ

เขียนลูปนับสระโดยใช้ name.size() ในเงื่อนไขตรงๆ แบบที่ใช้กับ n ในโปรแกรม vowels

for (int i = 0; i < name.size(); i++) {
vowels.cpp:13:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::__cxx11::basic_string<char>::size_type' {aka 'long long unsigned int'} [-Wsign-compare]
   13 |     for (int i = 0; i < name.size(); i++) {
      |                     ~~^~~~~~~~~~~~~

หน้า 6.4 เคยเจอคำเตือนเรื่อง signedness กับ strlen แต่ตอนนั้นต้องเปิด -Wextra ถึงจะเห็น ใน C++ คำเตือนนี้อยู่ใน -Wall น้องจึงเห็นใน Code::Blocks ทุกครั้ง size() คืนจำนวนเต็มไม่ติดลบ (unsigned) แต่ i เป็น int ที่ติดลบได้ ลูปนี้ทำงานถูก แต่คำเตือนที่ขึ้นทุกครั้งจะกลบคำเตือนที่สำคัญจริง แก้ด้วยการเก็บความยาวลง int ก่อนแบบโปรแกรม vowels หรือเขียน i < (int)name.size()

แบบที่ 2 เทียบตัวอักษรกับข้อความ

อยากรู้ว่าชื่อขึ้นต้นด้วย S ไหม

if (name[0] == "S") {
initial.cpp:10:17: warning: comparison with string literal results in unspecified behavior [-Waddress]
   10 |     if (name[0] == "S") {
initial.cpp:10:17: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]

name[0] เป็น char ตัวเดียว แต่ "S" ในเครื่องหมายคำพูดคู่เป็นข้อความ เทียบกันไม่ได้ ภาษา C แค่เตือนแล้วได้ผลมั่ว แต่ C++ ไม่ยอมคอมไพล์เลย แก้เป็น name[0] == 'S' ตัวอักษรตัวเดียวใช้เครื่องหมายคำพูดเดี่ยวเสมอ ส่วนการเทียบทั้งคำ name == "Somchai" ใช้คู่ได้ เพราะทั้งสองข้างเป็นข้อความ

แบบที่ 3 เช็ก find ด้วย >= 0

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string email;
    cin >> email;
    if (email.find('@') >= 0) {
        cout << "valid\n";
    } else {
        cout << "invalid\n";
    }
    cout << email.find('@') << '\n';
    return 0;
}
somchai
valid
18446744073709551615

ไม่มี @ เลยแต่ได้ valid เพราะ find คืนจำนวนที่ไม่ติดลบ ซึ่ง >= 0 จริงเสมอ บรรทัดสุดท้ายเผยว่า string::npos จริงๆ คือเลขที่ใหญ่ที่สุดที่ชนิดนี้เก็บได้ คือ 264 - 1 ไม่ใช่ -1 ใน Code::Blocks ที่ใช้ -Wall อย่างเดียว คอมไพล์ผ่านเงียบๆ ถ้าเพิ่ม -Wextra จะได้คำเตือน comparison of unsigned expression in '>= 0' is always true เทียบกับ string::npos ตรงๆ แบบในหัวข้อ find เสมอ

แบบที่ 4 ตัดเกินความยาว

โปรแกรมพิมพ์สามตัวท้ายของคำ ลองกับคำที่สั้นกว่าสามตัว

#include <bits/stdc++.h>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string word;
    cin >> word;
    cout << word.substr(word.size() - 3) << '\n';
    return 0;
}

ป้อน hi แล้วโปรแกรมพังทันที Code::Blocks แสดง

terminate called after throwing an instance of 'std::out_of_range'
  what():  basic_string::substr: __pos (which is 18446744073709551615) > this->size() (which is 2)

Process returned -1073740791 (0xC0000409)

มีสองเรื่องในข้อความนี้ อย่างแรก word.size() เป็นจำนวนไม่ติดลบ 2 ลบ 3 จึงไม่ได้ -1 แต่วนไปเป็นเลขที่ใหญ่ที่สุดแบบเดียวกับ npos อย่างที่สอง substr ตรวจว่าตำแหน่งเริ่มต้นเกินความยาวไหม ถ้าเกินจะหยุดโปรแกรมด้วยข้อความ out_of_range ซึ่ง grader จะนับเป็น runtime error แก้ด้วยการเก็บความยาวลง int ก่อนคำนวณ แล้วเช็กว่ายาวพอไหมก่อนตัด ส่วน word[i] ไม่ตรวจอะไรให้เลยเหมือนอาเรย์ เกินขอบแล้วได้ค่ามั่วเงียบๆ แบบหน้า 5.5

สรุป

  • string ไม่ต้องจองขนาด ต่อด้วย + += เทียบด้วย == < ได้ตรงๆ
  • s.size() คือความยาว เก็บลง int ก่อนใช้ในลูปหรือก่อนลบ
  • s.substr(start, count) ตัดข้อความ ตำแหน่งเริ่มเกินความยาวแล้วโปรแกรมพัง
  • s.find(x) คืนตำแหน่งแรกที่เจอ หรือ string::npos ถ้าไม่เจอ เทียบกับ npos เสมอ ห้ามเทียบกับ 0 หรือ -1
  • getline(cin, line) รับทั้งบรรทัด ไม่เก็บ '\n'
  • หลัง cin >> จะใช้ getline ต้องมี cin.ignore(1000000, '\n'); ก่อน