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 กับฟังก์ชันเปรียบเทียบในบทที่ 13 | sort บรรทัดเดียว บทที่ 16 |
| อาเรย์ที่ไม่รู้ขนาดล่วงหน้า | malloc กับ free ในหน้า 10.5 | vector บทที่ 16 |
| stack queue deque | เขียนเองด้วยอาเรย์ในบทที่ 14 | มีให้ใช้เลย บทที่ 17 |
โค้ดสาธิตของค่าย สอวน. และเฉลยโจทย์แข่งขันที่น้องจะไปอ่านต่อ ส่วนใหญ่เขียนด้วย C++ ทั้งนั้น ส่วนตัวภาษาเองไม่ได้ต่างจาก C มาก if for while อาเรย์ ฟังก์ชัน struct และ pointer ทำงานเหมือนเดิมทุกอย่าง สิ่งที่ต้องเรียนเพิ่มจริงๆ มีไม่มาก และอยู่ในบทนี้เกือบหมด
สร้างไฟล์ .cpp
ทำแบบเดียวกับหน้า 0.3 ต่างกันแค่นามสกุล
- เมนู File > New > Empty file
- กด Ctrl+S ทันที เลือกโฟลเดอร์
C:\codeตั้งชื่อให้ลงท้ายด้วย.cppเช่นaverage.cpp - พิมพ์โค้ดแล้วกด 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;
}
iostreamย่อมาจาก input output stream เป็นไฟล์ที่มีcinกับcoutซึ่ง C++ ใช้รับและแสดงผลแทนscanfกับprintfcoutอ่านว่า ซีเอาต์ (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++ ตรงๆ จะคอมไพล์ไม่ผ่าน ทางแก้มีสองทาง
- เปลี่ยนชื่อ เช่น
cntfreqnxtprvstkqueวิธีนี้ง่ายและแน่นอนที่สุด - ถ้าอาเรย์ไม่ใหญ่ ประกาศไว้ใน
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::coutusing namespace std;ทำให้เขียนสั้นลงได้ #include <bits/stdc++.h>ดึงทุกไฟล์มาตรฐานมาในบรรทัดเดียว คอมไพล์ช้าลงราวหนึ่งวินาที แต่เวลารันไม่เปลี่ยน- อย่าตั้งชื่อตัวแปรนอก
mainว่าcountnextprevmaxเพราะชนกับชื่อในstd mallocใน C++ ต้อง cast และคำอย่างclassnewใช้เป็นชื่อตัวแปรไม่ได้