[0]zero-index clytie0x/zero-index

15.5 bool reference และ auto

หน้าสุดท้ายของบทนี้รวมของเล็กๆ ที่ C++ เพิ่มให้ภาษา C แต่ใช้บ่อยมากในโค้ดแข่งขัน คือชนิด bool สำหรับจริงเท็จ reference ที่ให้ฟังก์ชันแก้ตัวแปรของคนเรียกได้โดยไม่ต้องใช้ * กับ & แบบ pointer ในบทที่ 10 ลูป for แบบใหม่ที่เดินทีละตัวในข้อความหรืออาเรย์ คำว่า auto และปิดท้ายด้วย new กับ delete ที่ใช้แทน malloc กับ free

bool เก็บจริงหรือเท็จ

ในบทที่ 3 เราใช้ int เก็บผลของเงื่อนไข จริงคือ 1 เท็จคือ 0 C++ มีชนิด bool สำหรับงานนี้โดยเฉพาะ มีค่าได้แค่สองค่าคือ true กับ false เห็นชื่อชนิดก็รู้ทันทีว่าตัวแปรนี้เป็นคำตอบแบบใช่หรือไม่ใช่

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

bool is_vowel(char c) // (1)!
{
    return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int score;
    string word;
    cin >> score >> word;
    bool passed = score >= 50; // (2)!
    cout << passed << '\n'; // (3)!
    if (passed) {
        cout << "pass\n";
    }
    bool has_vowel = false;
    int n = word.size();
    for (int i = 0; i < n; i++) {
        if (is_vowel(word[i])) {
            has_vowel = true;
        }
    }
    if (!has_vowel) {
        cout << "no vowels in " << word << '\n';
    }
    cout << (int)sizeof(passed) << '\n';
    return 0;
}
  1. ฟังก์ชันที่ตอบใช่หรือไม่ใช่ คืน bool อ่านง่ายกว่าคืน int มาก เวลาเรียกก็เขียนในเงื่อนไขได้ตรงๆ
  2. ผลของการเปรียบเทียบเก็บลง bool ได้เลย
  3. cout พิมพ์ true เป็น 1 และ false เป็น 0 ถ้าโจทย์ให้ตอบเป็นคำ ต้องใช้ if เลือกพิมพ์เอง
72 rhythm
1
pass
no vowels in rhythm
1

บรรทัดสุดท้ายบอกว่า bool ใช้แค่ 1 byte เทียบกับ int ที่ใช้ 4 byte อาเรย์ที่จดว่าอะไรเคยเจอแล้ว อย่าง bool seen[1000001]; จึงใช้หน่วยความจำแค่หนึ่งในสี่ของอาเรย์ int ขนาดเท่ากัน ส่วน ! && || ใช้กับ bool ได้เหมือนที่ใช้กับเงื่อนไขในบทที่ 3 ทุกอย่าง

reference ชื่อที่สองของตัวแปร

reference คือการตั้งชื่อเพิ่มให้กล่องที่มีอยู่แล้ว ประกาศด้วย & หลังชื่อชนิด ไม่ได้สร้างกล่องใหม่ ทั้งสองชื่อชี้กล่องเดียวกัน แก้ผ่านชื่อไหนก็เปลี่ยนที่กล่องเดียวกัน

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

int main()
{
    int score = 70;
    int &bonus_target = score;
    bonus_target += 5;
    cout << score << '\n';
    score = 90;
    cout << bonus_target << '\n';
    return 0;
}
75
90

int &bonus_target = score; แปลว่า bonus_target คือชื่อเล่นของ score บวกผ่านชื่อเล่น score ก็เปลี่ยน เปลี่ยน score ชื่อเล่นก็เห็น reference ต้องผูกกับตัวแปรตั้งแต่ตอนประกาศ และผูกแล้วเปลี่ยนไปผูกตัวอื่นไม่ได้

ใช้ในฟังก์ชันเดียวๆ แบบนี้ไม่ค่อยมีประโยชน์ ประโยชน์จริงอยู่ที่พารามิเตอร์ของฟังก์ชัน

ส่งตัวแปรให้ฟังก์ชันแก้

หน้า 8.2 เห็นแล้วว่าฟังก์ชันได้แค่สำเนา สลับค่าในฟังก์ชันแล้วตัวแปรของคนเรียกไม่เปลี่ยน บทที่ 10 แก้ด้วยการส่งที่อยู่ไปเป็น pointer ใน C++ ทำได้ง่ายกว่านั้น ให้พารามิเตอร์เป็น reference

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

void swap_values(int &a, int &b) // (1)!
{
    int temp = a;
    a = b;
    b = temp;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int first, second;
    cin >> first >> second;
    swap_values(first, second); // (2)!
    cout << first << ' ' << second << '\n';
    return 0;
}
  1. a กับ b เป็น reference จึงไม่ใช่สำเนา แต่เป็นชื่อเล่นของตัวแปรที่คนเรียกส่งมา ในฟังก์ชันใช้ a b เหมือนตัวแปรธรรมดา ไม่ต้องมี *
  2. เรียกด้วยชื่อตัวแปรตรงๆ ไม่ต้องใส่ & หน้า first กับ second
3 8
8 3

เทียบกับฟังก์ชัน swap แบบ pointer ในหน้า 10.2

pointer (บทที่ 10)reference (C++)
ประกาศพารามิเตอร์void swap(int *x, int *y)void swap_values(int &a, int &b)
ใช้ในฟังก์ชัน*x = *y;a = b;
เรียกswap(&first, &second);swap_values(first, second);
ส่งของที่ไม่มีอยู่ได้ไหมได้ ส่ง NULL แล้วพังตอนรันไม่ได้ ต้องผูกกับตัวแปรจริงเสมอ

ข้างในทำงานแบบเดียวกัน คือส่งที่อยู่ของกล่องไป แต่ compiler จัดการ & กับ * ให้ทั้งหมด โอกาสลืมดอกจันหรือลืมเครื่องหมายแอนด์จึงหายไป ฟังก์ชันที่ต้องส่งผลกลับหลายค่า ซึ่งหน้า 8.2 บอกว่าต้องรอ pointer ก็ใช้ reference ได้ เช่น void min_max(int score[], int n, int &lowest, int &highest)

C++ มี swap ให้แล้ว

ตัวอย่างนี้ตั้งชื่อว่า swap_values เพราะใน std มีฟังก์ชัน swap ที่ทำงานแบบเดียวกันอยู่แล้ว เขียน swap(first, second); ได้เลยโดยไม่ต้องเขียนฟังก์ชันเอง และใช้กับ string หรือชนิดอื่นได้ด้วย

ส่ง string แบบไม่ต้องคัดลอก

ส่ง string ให้ฟังก์ชันแบบธรรมดา ฟังก์ชันจะได้สำเนาทั้งข้อความ ต่างจากอาเรย์ char ในบทที่ 8 ที่ส่งไปแค่ที่อยู่ ข้อความยาวแสนตัว เรียกฟังก์ชันหนึ่งครั้งก็คัดลอกแสนตัว ถ้าเรียกเป็นแสนครั้งจะช้าลงชัดเจน วิธีแก้คือส่งเป็น reference และใส่ const (ค่าคงที่) ไว้ข้างหน้า เพื่อสัญญาว่าฟังก์ชันจะอ่านอย่างเดียว ไม่แก้ข้อความของคนเรียก

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

bool is_vowel_at(const string &text, int i)
{
    char c = text[i];
    return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string text;
    int q;
    cin >> text >> q;
    int count = 0;
    for (int k = 0; k < q; k++) {
        int i;
        cin >> i;
        if (is_vowel_at(text, i)) {
            count++;
        }
    }
    cout << count << '\n';
    return 0;
}
programming 4
0 2 5 8
3

ทดลองกับข้อความยาว 105 ตัวและคำถาม 105 ข้อ บนเครื่องเดียวกับหน้า 15.3 ด้วยวิธีวัดแบบเดียวกัน

พารามิเตอร์แบบ Code::Blocksแบบ -O2
string text (คัดลอกทุกครั้ง)0.26 วินาที0.45 วินาที
const string &text0.04 วินาที0.03 วินาที

คำตอบเท่ากัน แต่แบบคัดลอกช้ากว่าราวสิบเท่า และจะยิ่งแย่ลงถ้าข้อความยาวขึ้น ถ้าใส่ const แล้วเผลอแก้ text ในฟังก์ชัน compiler จะไม่ยอมคอมไพล์ ช่วยกันพลาดได้อีกชั้น หลักง่ายๆ คือ string ที่ส่งให้ฟังก์ชันอ่าน ให้ส่งเป็น const string & ส่วน int double char ส่งแบบธรรมดาได้ เพราะคัดลอกตัวเลขตัวเดียวเร็วมาก

for แบบเดินทีละตัว

ลูปที่เดินดูทุกตัวในข้อความตั้งแต่ตัวแรกถึงตัวสุดท้ายเขียนบ่อยจน C++ มีรูปแบบสั้นให้ เรียกว่า range-based for

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string word;
    cin >> word;
    int vowels = 0;
    for (char c : word) { // (1)!
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            vowels++;
        }
    }
    cout << vowels << '\n';
    for (char &c : word) { // (2)!
        c = toupper(c);
    }
    cout << word << '\n';
    int score[4] = {72, 85, 64, 90};
    int total = 0;
    for (int x : score) { // (3)!
        total += x;
    }
    cout << total << '\n';
    return 0;
}
  1. อ่านว่า "สำหรับ c แต่ละตัวใน word" รอบแรก c คือตัวที่ 0 รอบถัดไปคือตัวที่ 1 ไปจนครบ ไม่ต้องมี i ไม่ต้องดูความยาว จึงไม่มีทางเกินขอบ แต่ c เป็นสำเนาของแต่ละตัว
  2. ใส่ & แล้ว c เป็น reference ของตัวอักษรในข้อความจริงๆ แก้ c ก็คือแก้ word
  3. ใช้กับอาเรย์ที่ประกาศขนาดไว้ได้ด้วย ลูปจะเดินครบทั้ง 4 ช่อง
education
5
EDUCATION
311

ลูปแบบนี้ใช้ได้เมื่อต้องการดูทุกตัวตามลำดับเท่านั้น ถ้าต้องรู้ว่าตอนนี้อยู่ช่องไหน ต้องดูตัวข้างเคียง หรือเดินถอยหลัง ยังต้องใช้ for แบบมี i เหมือนเดิม และใช้กับอาเรย์ที่ส่งเข้ามาเป็นพารามิเตอร์ของฟังก์ชันไม่ได้ เพราะในฟังก์ชันไม่รู้ขนาดของอาเรย์ ส่วนบทที่ 16 จะใช้ลูปแบบนี้กับ vector ตลอด

auto ให้ compiler เลือกชนิดให้

ถ้าประกาศตัวแปรพร้อมค่าเริ่มต้น เขียน auto แทนชื่อชนิดได้ compiler จะดูจากค่าเริ่มต้นว่าควรเป็นชนิดอะไร

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

int main()
{
    auto count = 3;
    auto height = 1.75;
    auto grade = 'A';
    string name = "Malee";
    auto copy = name;
    copy += "!";
    cout << count << ' ' << height << ' ' << grade << ' ' << copy << '\n';
    for (auto c : name) {
        cout << c << '.';
    }
    cout << '\n';
    return 0;
}
3 1.75 A Malee!
M.a.l.e.e.

3 เป็น int 1.75 เป็น double 'A' เป็น char และ copy เป็น string ตาม name ตอนนี้ auto ช่วยได้ไม่มาก เพราะชื่อชนิดที่เรารู้จักสั้นอยู่แล้ว มันจะมีประโยชน์จริงในบทที่ 16 ถึง 20 ที่ชื่อชนิดของ STL ยาวเป็นบรรทัด ระหว่างนี้ใช้ใน for (auto c : s) และ for (auto &c : s) ก็พอ ส่วนตัวแปรที่เก็บตัวเลข ให้เขียนชนิดเองเสมอ เหตุผลอยู่ในแบบที่ 3 ของส่วนลองทำให้พัง

new และ delete

C++ มีคำสั่งขอและคืนหน่วยความจำของตัวเอง ใช้แทน malloc กับ free ไม่ต้องคำนวณขนาดด้วย sizeof และไม่ต้อง cast แบบที่เจอในแบบที่ 3 ของหน้า 15.1

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

struct Node { // (1)!
    int value;
    Node *next;
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    int *score = new int[n]; // (2)!
    Node *head = nullptr;
    for (int i = 0; i < n; i++) {
        cin >> score[i];
        Node *node = new Node; // (3)!
        node->value = score[i];
        node->next = head;
        head = node;
    }
    for (Node *p = head; p != nullptr; p = p->next) {
        cout << p->value << ' ';
    }
    cout << '\n';
    cout << score[0] + score[n - 1] << '\n';
    while (head != nullptr) {
        Node *next_node = head->next;
        delete head; // (4)!
        head = next_node;
    }
    delete[] score; // (5)!
    return 0;
}
  1. ใน C++ ชื่อ struct ใช้เป็นชื่อชนิดได้เลย ไม่ต้องมี typedef และไม่ต้องเขียน struct Node เต็มๆ ข้างในก็ได้
  2. ขออาเรย์ int ขนาด n ช่อง แทน (int *)malloc(n * sizeof(int))
  3. ขอ node หนึ่งตัว แทน malloc(sizeof(Node)) ในหน้า 14.1 ซึ่งถ้าย้ายมาเป็น C++ ต้องเติม cast เป็น (Node *)malloc(sizeof(Node)) ตามหน้า 15.1
  4. คืน node ที่ได้จาก new ทีละตัว แทน free
  5. ของที่ขอเป็นอาเรย์ด้วย new ... [n] ต้องคืนด้วย delete[] มีวงเล็บเหลี่ยม
4
3 9 2 7
7 2 9 3
10

ในการแข่งขันใช้ new ไม่บ่อย อาเรย์ที่ไม่รู้ขนาดมักประกาศไว้นอก main ตามขนาดมากสุดในโจทย์ หรือใช้ vector ในบทที่ 16 ซึ่งยืดได้เองและคืนหน่วยความจำให้เอง ที่ยังเจออยู่คือการสร้าง node ของ linked list หรือต้นไม้ ให้รู้จักไว้พออ่านโค้ดคนอื่นออก

ลองทำให้พัง

แบบที่ 1 ส่งตัวเลขให้พารามิเตอร์ที่เป็น reference

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

void add_bonus(int &score, int bonus)
{
    score += bonus;
}

int main()
{
    int math = 70;
    add_bonus(math, 5);
    add_bonus(75, 5);
    cout << math << '\n';
    return 0;
}
bonus.cpp:13:15: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
   13 |     add_bonus(75, 5);
      |               ^~
bonus.cpp:4:21: note:   initializing argument 1 of 'void add_bonus(int&, int)'
    4 | void add_bonus(int &score, int bonus)
      |                ~~~~~^~~~~

บรรทัดที่ 12 ผ่าน แต่บรรทัดที่ 13 ไม่ผ่าน reference ต้องเป็นชื่อเล่นของกล่องที่มีอยู่จริง 75 ไม่ใช่กล่อง จะบวกโบนัสเข้าไปเก็บที่ไหนก็ไม่ได้ rvalue ในข้อความหมายถึงค่าที่ไม่มีกล่องของตัวเองแบบนี้ ผลคำนวณอย่าง math + 1 ก็ส่งไม่ได้ด้วยเหตุผลเดียวกัน ส่วน note บรรทัดถัดมาชี้ไปที่พารามิเตอร์ตัวที่ทำให้เกิดปัญหา

แบบที่ 2 ลืม & ใน for แบบเดินทีละตัว

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string name;
    cin >> name;
    for (char c : name) {
        c = toupper(c);
    }
    cout << name << '\n';
    return 0;
}
somchai
somchai

ไม่มีอะไรเปลี่ยนเลย และไม่มีคำเตือนแม้เปิด -Wextra เพราะ c เป็นสำเนาของแต่ละตัว แก้สำเนาแล้วสำเนาก็หายไปตอนจบรอบ แบบเดียวกับฟังก์ชันที่ได้สำเนาในหน้า 8.2 ต้องการแก้ข้อความจริงต้องเป็น for (char &c : name)

แบบที่ 3 ใช้ auto กับผลรวม

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    auto total = 0;
    for (int i = 0; i < n; i++) {
        long long price;
        cin >> price;
        total += price;
    }
    cout << total << '\n';
    return 0;
}
3
1000000000 1000000000 1000000000
-1294967296

ราคาอ่านเป็น long long ถูกแล้ว แต่ auto total = 0; ได้ชนิดจากเลข 0 ซึ่งเป็น int ผลรวมสามพันล้านจึงล้นแบบเดียวกับหน้า 1.3 ไม่มีคำเตือนสักบรรทัด auto ไม่ได้ดูว่าเราจะเอาตัวแปรไปเก็บอะไรในอนาคต ดูแค่ค่าเริ่มต้นเท่านั้น ตัวแปรที่เก็บตัวเลขให้เขียน long long total = 0; เองเสมอ

สรุป

  • bool เก็บ true หรือ false ใช้ 1 byte cout พิมพ์เป็น 1 กับ 0
  • int &r = x; คือชื่อที่สองของ x พารามิเตอร์แบบ int &a ให้ฟังก์ชันแก้ตัวแปรของคนเรียกได้ เรียกด้วยชื่อตัวแปรตรงๆ
  • string ที่ส่งให้ฟังก์ชันอ่าน ส่งเป็น const string & จะได้ไม่คัดลอกทั้งข้อความทุกครั้ง
  • for (char c : s) ได้สำเนาทีละตัว for (char &c : s) แก้ตัวจริงได้
  • auto เลือกชนิดจากค่าเริ่มต้น ใช้ในลูปแบบเดินทีละตัวได้ แต่ตัวแปรเก็บตัวเลขให้เขียนชนิดเอง
  • new กับ delete ใช้แทน malloc กับ free อาเรย์ต้องคืนด้วย delete[]

กลับไปเช็กรายการ ผ่านบทนี้เมื่อ แล้วไปทำแบบฝึกหัด