[0]zero-index clytie0x/zero-index

15.1 เปลี่ยนจาก .c เป็น .cpp

ภาษา C++ คือภาษา C ที่ถูกเติมของใช้เข้าไปอีกมาก โค้ดภาษา C เกือบทั้งหมดที่น้องเขียนมาตลอด 14 บทจึงเอาไปใช้ใน C++ ได้ทันที หน้านี้จะย้ายโปรแกรมเดิมไปเป็นไฟล์ .cpp เขียนโปรแกรม C++ ตัวแรก รู้จักสองบรรทัดที่ขึ้นต้นโปรแกรมแข่งขันแทบทุกโปรแกรม และดูว่าโค้ด C แบบไหนที่ C++ ไม่ยอมรับ

ทำไมการแข่งขันใช้ C++

ภาษา C ทำได้ทุกอย่างที่โจทย์ต้องการ แต่หลายงานต้องเขียนเองยาวๆ ทุกครั้ง C++ มีชุดเครื่องมือสำเร็จรูปชื่อ STL (Standard Template Library แปลว่าคลังโค้ดมาตรฐาน) ที่ทำงานพวกนั้นให้ในบรรทัดเดียว และทดสอบมาแล้วอย่างดี

งานภาษา C ที่เรียนมาC++
เก็บและต่อข้อความอาเรย์ char กับ strcpy strcat strcmp ในบทที่ 6 ต้องจองขนาดเองstring ยืดได้เอง ใช้ + กับ == ได้ หน้า 15.4
เรียงข้อมูลqsort กับฟังก์ชันเปรียบเทียบในบทที่ 13sort บรรทัดเดียว บทที่ 16
อาเรย์ที่ไม่รู้ขนาดล่วงหน้าmalloc กับ free ในหน้า 10.5vector บทที่ 16
stack queue dequeเขียนเองด้วยอาเรย์ในบทที่ 14มีให้ใช้เลย บทที่ 17

โค้ดสาธิตของค่าย สอวน. และเฉลยโจทย์แข่งขันที่น้องจะไปอ่านต่อ ส่วนใหญ่เขียนด้วย C++ ทั้งนั้น ส่วนตัวภาษาเองไม่ได้ต่างจาก C มาก if for while อาเรย์ ฟังก์ชัน struct และ pointer ทำงานเหมือนเดิมทุกอย่าง สิ่งที่ต้องเรียนเพิ่มจริงๆ มีไม่มาก และอยู่ในบทนี้เกือบหมด

สร้างไฟล์ .cpp

ทำแบบเดียวกับหน้า 0.3 ต่างกันแค่นามสกุล

  1. เมนู File > New > Empty file
  2. กด Ctrl+S ทันที เลือกโฟลเดอร์ C:\code ตั้งชื่อให้ลงท้ายด้วย .cpp เช่น average.cpp
  3. พิมพ์โค้ดแล้วกด F9 เหมือนเดิม

ไม่ต้องตั้งค่าอะไรเพิ่ม Code::Blocks ดูนามสกุลแล้วเลือกตัวแปลให้เองตามตารางในหน้า 0.1 ไฟล์ .c ใช้ gcc ไฟล์ .cpp ใช้ g++ และ -Wall ที่ติ๊กไว้ตอนติดตั้งก็ใช้กับ g++ ด้วย

เปลี่ยนไฟล์เก่าเป็น .cpp ให้ใช้ Save file as

ถ้าอยากเอาไฟล์ .c เดิมมาทำต่อเป็น C++ ใช้เมนู File > Save file as แล้วตั้งชื่อใหม่ลงท้าย .cpp ไฟล์ .c ตัวเดิมยังอยู่ในโฟลเดอร์ ระวังเปิดผิดไฟล์แล้วงงว่าทำไมแก้แล้วไม่เปลี่ยน ดูชื่อไฟล์บนแท็บของ Code::Blocks ก่อนกด F9 ทุกครั้ง

โปรแกรม C เดิมใช้ต่อได้

เอาโปรแกรมหาค่าเฉลี่ยคะแนนสามวิชาจากบทที่ 1 มาบันทึกเป็น average.cpp โดยไม่แก้แม้แต่ตัวเดียว

#include <stdio.h>

int main()
{
    int math, science, english;
    scanf("%d %d %d", &math, &science, &english);
    double average = (math + science + english) / 3.0;
    printf("%f\n", average);
    return 0;
}
70 85 90
81.666667

คอมไพล์ผ่านด้วย g++ และได้ผลเหมือนตอนเป็นภาษา C ทุกอย่าง scanf printf stdio.h string.h math.h ใช้ใน C++ ได้หมด ถ้าไปเจอโค้ดที่เขียน #include <cstdio> หรือ #include <cmath> นั่นคือชื่อแบบ C++ ของไฟล์เดียวกัน ตัด .h ออกแล้วเติม c ข้างหน้า ใช้แบบไหนก็ได้

มีโค้ด C ส่วนน้อยที่ C++ ไม่ยอมรับ จะเจอในส่วนลองทำให้พังท้ายหน้า

โปรแกรม C++ ตัวแรก

#include <iostream> // (1)!

int main()
{
    std::cout << "Hello, C++!\n"; // (2)!
    return 0;
}
  1. iostream ย่อมาจาก input output stream เป็นไฟล์ที่มี cin กับ cout ซึ่ง C++ ใช้รับและแสดงผลแทน scanf กับ printf
  2. cout อ่านว่า ซีเอาต์ (character output) คือหน้าจอ เครื่องหมาย << คือส่งข้อความทางขวาไปที่ cout รายละเอียดอยู่ในหน้า 15.2
Hello, C++!

std:: และ using namespace std

ทุกอย่างที่ C++ เตรียมไว้ให้ ทั้ง cout cin string sort และอีกหลายร้อยชื่อ ถูกเก็บรวมไว้ในกลุ่มชื่อ std (standard) กลุ่มชื่อแบบนี้เรียกว่า namespace เหมือนนามสกุลของคน มีคนชื่อสมชายหลายคน แต่สมชาย ใจดี มีคนเดียว ชื่อเต็มของ cout จึงเป็น std::cout และ :: ก็คือการบอกว่าเป็นของกลุ่มไหน

เขียน std:: ทุกครั้งเหนื่อย บรรทัด using namespace std; บอก compiler ว่า "ชื่อไหนหาไม่เจอ ให้ไปดูใน std ด้วย" แล้วเขียนแค่ cout ได้เลย

#include <iostream>
using namespace std;

int main()
{
    cout << "Hello, C++!\n";
    cout << "Hello again!\n";
    return 0;
}
Hello, C++!
Hello again!

bits/stdc++.h ดึงมาทั้งหมดในบรรทัดเดียว

ของแต่ละอย่างใน C++ อยู่คนละไฟล์ cout อยู่ใน iostream string อยู่ใน string ตัวจัดทศนิยมอยู่ใน iomanip sort อยู่ใน algorithm ลืม include ตัวไหนก็คอมไพล์ไม่ผ่าน คนแข่งจึงนิยมใช้

#include <bits/stdc++.h>

ไฟล์นี้ไม่ได้มีของใหม่อะไร มันแค่ include ไฟล์มาตรฐานของ C++ ทุกไฟล์ให้ในครั้งเดียว รวมทั้ง cstdio cstring และ cmath ซึ่งก็คือ stdio.h string.h และ math.h ฉบับ C++ ตั้งแต่ตรงนี้ไป โปรแกรมในเอกสารนี้จะขึ้นต้นแบบนี้ทุกโปรแกรม

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

int main()
{
    int width, height;
    scanf("%d %d", &width, &height);
    cout << "area = " << width * height << '\n';
    return 0;
}
12 7
area = 84

โปรแกรมนี้ใช้ scanf จาก stdio.h และ cout จาก iostream โดยไม่ได้ include สองไฟล์นั้นเอง เพราะ bits/stdc++.h ดึงมาให้แล้ว แต่อย่าเพิ่งเอา scanf กับ cout มาปนกันจริงจัง หน้า 15.3 จะบอกว่าทำไม

ข้อดีและข้อเสีย

สองบรรทัดนี้สะดวกมาก แต่มีราคาที่ต้องรู้

ข้อดีข้อเสีย
#include <bits/stdc++.h>บรรทัดเดียวครบ ไม่ต้องจำว่าอะไรอยู่ไฟล์ไหน ไม่มีวันลืม includeคอมไพล์ช้าลง และมีเฉพาะใน GCC compiler อื่นอย่าง Visual Studio ไม่มีไฟล์นี้
using namespace std;ไม่ต้องเขียน std:: นำหน้าทุกชื่อชื่อใน std หลายร้อยชื่อเข้ามาปนกับชื่อตัวแปรของเรา ถ้าตั้งชื่อซ้ำจะคอมไพล์ไม่ผ่าน

คอมไพล์ช้าลงแค่ไหน ลองกด F9 กับโปรแกรมที่พิมพ์ข้อความเดียว จับเวลาตั้งแต่เริ่มคอมไพล์จนได้ไฟล์ .exe ด้วย GCC 14.2 ของ Code::Blocks ตัวเลือก -Wall บนโน้ตบุ๊ก AMD Ryzen AI 9 HX 370 ระบบ Windows 11 เสียบสายชาร์จ โหมดพลังงาน Balanced คอมไพล์แบบละ 6 ครั้ง ทิ้งครั้งแรก แล้วเอาค่ากลางของ 5 ครั้งที่เหลือ

ไฟล์ที่ includeเวลาคอมไพล์
ภาษา C กับ stdio.hประมาณ 0.26 วินาที
C++ กับ iostreamประมาณ 0.53 วินาที
C++ กับ bits/stdc++.hประมาณ 1.37 วินาที

ช้าลงราวหนึ่งวินาทีต่อการกด F9 หนึ่งครั้ง แต่ไม่มีผลกับเวลารัน grader จับเวลาเฉพาะตอนโปรแกรมทำงาน ไม่นับตอนคอมไพล์ ส่วนเรื่องมีเฉพาะใน GCC ก็ไม่เป็นปัญหา เพราะ Code::Blocks และ grader ของการแข่งขันส่วนใหญ่ใช้ GCC

สรุปคือในการแข่งขันใช้ทั้งสองบรรทัดได้เลย ส่วนงานเขียนโปรแกรมจริงในบริษัทจะไม่ใช้ เพราะโค้ดต้องคอมไพล์ได้หลาย compiler และโปรแกรมใหญ่หลายพันไฟล์ ข้อเสียที่ต้องระวังในการแข่งขันจริงๆ มีข้อเดียวคือชื่อชนกัน ดูแบบที่ 2 ข้างล่าง

ลองทำให้พัง

แบบที่ 1 ลืม using namespace std

ในโปรแกรม hello ที่ include iostream ลบบรรทัด using namespace std; ทิ้ง

hello.cpp:5:5: error: 'cout' was not declared in this scope; did you mean 'std::cout'?
    5 |     cout << "Hello, C++!\n";
      |     ^~~~
      |     std::cout

compiler หา cout ไม่เจอ เพราะชื่อเต็มคือ std::cout และมันเดาให้ด้วย ต่อจากนี้ยังมี note อีกหนึ่งชุดที่ชี้ไปยังบรรทัดที่ประกาศ std::cout ในไฟล์ iostream ของ compiler ไม่ต้องอ่าน แก้ด้วยการใส่ using namespace std; กลับ หรือเขียน std::cout เต็มๆ

แบบที่ 2 ตั้งชื่อตัวแปรชนกับชื่อใน std

อาเรย์นับความถี่ที่ใช้มาตั้งแต่บทที่ 5 มักชื่อ count และประกาศไว้นอก main เพราะใหญ่ ลองย้ายโปรแกรมแบบนั้นมาเป็น C++

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

int count[101];

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        int score;
        cin >> score;
        count[score]++;
    }
    cout << count[100] << '\n';
    return 0;
}
count.cpp:13:9: error: reference to 'count' is ambiguous
   13 |         count[score]++;
      |         ^~~~~

ambiguous แปลว่ากำกวม ใน std มีฟังก์ชันชื่อ count อยู่แล้ว (ใช้นับของใน vector ซึ่งจะได้เรียนทีหลัง) พอเราประกาศ count ของเราเองนอก main ด้วย ก็มีสองตัวที่ชื่อเหมือนกัน compiler ไม่รู้ว่าหมายถึงตัวไหน ข้อความนี้ขึ้นซ้ำอีกครั้งที่บรรทัด 15 และแต่ละครั้งตามด้วย note ยาวหลายบรรทัดที่ไล่ชื่อ count ทุกตัวที่เป็นไปได้ ตัวสุดท้ายในรายการคือ 'int count [101]' ของเรา

ชื่อที่ชนบ่อยในโค้ดของน้อง ทดลองแล้วว่าประกาศเป็นอาเรย์นอก main คู่กับ using namespace std; แล้วคอมไพล์ไม่ผ่าน ได้แก่ count next prev max size left data y1 stack queue และ deque ที่น่าตกใจคือ next prev stack queue และ deque ซึ่งบทที่ 14 ใช้เป็นชื่ออาเรย์ทั้งหมด ย้ายโค้ดบทนั้นมาเป็น C++ ตรงๆ จะคอมไพล์ไม่ผ่าน ทางแก้มีสองทาง

  • เปลี่ยนชื่อ เช่น cnt freq nxt prv stk que วิธีนี้ง่ายและแน่นอนที่สุด
  • ถ้าอาเรย์ไม่ใหญ่ ประกาศไว้ใน main แทน ชื่อในฟังก์ชันจะบังชื่อใน std ให้เอง ไม่กำกวม

แบบที่ 3 malloc โดยไม่ cast

เอาฟังก์ชัน push_front จากหน้า 14.1 มาไว้ในไฟล์ .cpp

Node *node = malloc(sizeof(Node));
list.cpp:11:24: error: invalid conversion from 'void*' to 'Node*' [-fpermissive]
   11 |     Node *node = malloc(sizeof(Node));
      |                  ~~~~~~^~~~~~~~~~~~~~
      |                        |
      |                        void*

malloc คืนที่อยู่ชนิด void * ซึ่งแปลว่าที่อยู่ของอะไรก็ได้ ภาษา C ยอมแปลงเป็น Node * ให้เงียบๆ แต่ C++ เข้มงวดกว่า ไม่แปลงให้เอง ต้อง cast เอง Node *node = (Node *)malloc(sizeof(Node)); หรือเปลี่ยนไปใช้ new ของ C++ ซึ่งอยู่ในหน้า 15.5 นี่คือจุดที่โค้ด C จากบทที่ 14 ต้องแก้บ่อยที่สุดตอนย้ายมา C++

แบบที่ 4 ใช้คำสงวนของ C++ เป็นชื่อตัวแปร

C++ มีคำสงวนเพิ่มจากภาษา C หลายคำ เช่น class new delete this bool true false ชื่อเหล่านี้เคยใช้เป็นชื่อตัวแปรในภาษา C ได้ แต่ใน C++ ไม่ได้แล้ว เช่นเก็บเลขห้องเรียนไว้ในตัวแปรชื่อ class

int class = 3;
printf("%d\n", class);
room.cpp:5:5: error: expected primary-expression before 'int'
    5 |     int class = 3;
      |     ^~~
room.cpp:6:20: error: expected primary-expression before 'class'
    6 |     printf("%d\n", class);
      |                    ^~~~~

ข้อความไม่ได้บอกตรงๆ ว่าชื่อนี้ห้ามใช้ แต่ทั้งสองจุดที่ ^ ชี้ อยู่ที่บรรทัดที่มีคำว่า class ถ้าโค้ด C ที่เคยคอมไพล์ผ่านมาพังแบบงงๆ ที่บรรทัดธรรมดา ให้ดูว่ามีชื่อตัวแปรเป็นคำภาษาอังกฤษที่ C++ ใช้อยู่หรือเปล่า เปลี่ยนเป็น room หรือ class_no ก็หาย

สรุป

  • บันทึกไฟล์เป็น .cpp แล้ว Code::Blocks ใช้ g++ ให้เอง โค้ด C ส่วนใหญ่ใช้ต่อได้ทันที
  • ของมาตรฐานของ C++ อยู่ใน namespace std ชื่อเต็มคือ std::cout using namespace std; ทำให้เขียนสั้นลงได้
  • #include <bits/stdc++.h> ดึงทุกไฟล์มาตรฐานมาในบรรทัดเดียว คอมไพล์ช้าลงราวหนึ่งวินาที แต่เวลารันไม่เปลี่ยน
  • อย่าตั้งชื่อตัวแปรนอก main ว่า count next prev max เพราะชนกับชื่อใน std
  • malloc ใน C++ ต้อง cast และคำอย่าง class new ใช้เป็นชื่อตัวแปรไม่ได้