Ch17: Classes¶
TL;DR: Encapsulation: Class มัดรวมข้อมูล (Member Variable) และพฤติกรรม (Method) เข้าด้วยกัน โดยเริ่มต้นสมาชิกจะเป็น
privateและสามารถควบคุมการเข้าถึงด้วยpublicหรือprivate| C++ จะทำการคอมไพล์แต่ละไฟล์.cppแยกกันอย่างเป็นอิสระจนได้เป็นไฟล์ออบเจกต์ (Object File) ส่วนไฟล์เฮดเดอร์ (.h) จะคอยทำหน้าที่แชร์ตัวประกาศ (Declarations) ร่วมกันระหว่างไฟล์ต่างๆ และมีตัวลิงก์ (Linker) คอยรวบรวมประกอบเข้าด้วยกันในขั้นตอนสุดท้าย — การทำสองส่วนนี้สับสนจะส่งผลให้เกิดข้อผิดพลาดประเภท "undefined reference" (หาข้อมูลอ้างอิงไม่เจอ) หรือ "multiple definition" (นิยามซ้ำซ้อน)
⚡ Quick Reference¶
#include <iostream>
#include <string>
class MemberClass {
public:
MemberClass() = default;
};
class FriendClass;
class ClassName {
private:
// สมาชิกประเภท private เพื่อประโยชน์ในการปกป้องข้อมูล (Encapsulation)
int m_val {0};
mutable int m_counter {0}; // mutable: ปลดล็อกข้อจำกัด const ให้กับบางตัวแปร
public:
// ClassName() = default; - สั่งสร้าง Default Constructor รูปแบบเริ่มต้นอัตโนมัติ
ClassName() = default;
// ClassName() = delete; - สั่งยกเลิก Constructor เพื่อป้องกันไม่ให้เรียกใช้งาน
ClassName(double) = delete;
// Parameterized Constructor ร่วมกับระบบตั้งค่าเริ่มต้น (Initializer List)
ClassName(int val) : m_val(val) {}
// Constructor Delegation (เรียกใช้งานคอนสตรัคเตอร์ตัวอื่น)
ClassName(int val, double) : ClassName(val) {}
// Copy Constructor สำหรับโคลนข้อมูลเชิงลึก (Deep Copy)
ClassName(const ClassName& other) : m_val(other.m_val) {}
// Destructor สั่งรันอัตโนมัติเมื่อทำลายออบเจกต์เพื่อคืนพื้นที่
~ClassName() {}
// static - ตัวแปรแชร์ร่วมกันระหว่างวัตถุทั้งหมดภายใต้คลาสเดียวกัน
inline static int s_shared_val = 42; // inline static (C++17) ทำการระบุค่าได้ทันที
// const method: เมธอดประเภทคงที่ ห้ามแก้ไขค่าตัวแปรสมาชิกในคลาส
int get_value() const {
m_counter++; // ทำได้เนื่องจากตัวแปรประกาศกำกับเป็น mutable
return m_val;
}
// return *this; - ส่งคืนข้อมูลอ้างอิงวัตถุเพื่อทำคำสั่งต่อเนื่อง (Method Chaining)
ClassName& set_value(int val) {
m_val = val;
return *this;
}
// ประกาศ Friend Function เพื่อมอบสิทธิ์เข้าถึงส่วน private
friend void print_private(const ClassName& obj);
};
// นิยามคำสั่งทำงานของฟังก์ชันเพื่อน
void print_private(const ClassName& obj) {
std::cout << obj.m_val << std::endl;
}
// struct - สมาชิกเริ่มต้นจะเป็นแบบ public ทั้งหมด
struct Point {
int x;
int y;
};
int main() {
ClassName obj(10);
obj.set_value(20).get_value(); // อ้างอิงด้วยสัญลักษณ์จุดร่วมกับเรียกคำสั่งต่อเนื่อง
ClassName* p_obj = &obj;
p_obj->get_value(); // อ้างอิงด้วยสัญลักษณ์ลูกศรผ่านพอยน์เตอร์
const ClassName const_obj(5); // วัตถุประเภทคงที่
Point p{1, 2};
auto [x, y] = p; // Structured bindings (C++20)
return 0;
}
โมเดลการสร้างและการคอมไพล์แยกส่วน (Build Model & Separate Compilation)¶
// ---- math_utils.h (header: เฉพาะตัวประกาศเท่านั้น) ----
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int square(int x); // เฉพาะประกาศเท่านั้น ไม่มีเนื้อหาฟังก์ชัน
#endif // include guard ป้องกันการดึงเฮดเดอร์ซ้ำซ้อน
// ---- math_utils.cpp (translation unit: ส่วนนิยาม) ----
#include "math_utils.h"
int square(int x) { // เนื้อหาการทำงานจริงของฟังก์ชันอยู่ที่นี่
return x * x;
}
// ---- main.cpp (อีกหนึ่ง translation unit) ----
#include "math_utils.h"
#include <iostream>
int main() {
std::cout << square(5) << '\n'; // linker จะทำการเชื่อมการเรียกใช้นี้ไปที่ math_utils.cpp
return 0;
}
// ขั้นตอนการ Build (ตามแนวคิด):
// g++ -c math_utils.cpp -o math_utils.o <- ขั้นตอน compile
// g++ -c main.cpp -o main.o <- ขั้นตอน compile
// g++ math_utils.o main.o -o app <- ขั้นตอน link
🧠 Core Concepts¶
- Encapsulation: Class มัดรวมข้อมูล (Member Variable) และพฤติกรรม (Method) เข้าด้วยกัน โดยเริ่มต้นสมาชิกจะเป็น
privateและสามารถควบคุมการเข้าถึงด้วยpublicหรือprivate - Object Lifecycle: Constructor และ Destructor (
~ClassName) เป็นเมธอดพิเศษที่ไม่มีชนิดข้อมูลส่งกลับ ซึ่งเรียกทำงานโดย Compiler เพื่อจัดเตรียมสมาชิกตัวแปรเริ่มต้นและทำความสะอาดสลัดหน่วยความจำเมื่อวัตถุถูกทำลาย - Const Correctness & Shared Members: วัตถุประเภท
constจะสามารถเรียกใช้งานได้เฉพาะเมธอดที่มีสัญลักษณ์constกำกับท้ายเท่านั้น โดยชนิดข้อมูลประเภทstaticจะแชร์ค่าร่วมกันระหว่างวัตถุทั้งหมดในคลาส - Translation Unit: แต่ละไฟล์
.cppหลังจากที่พรีโพรเซสเซอร์ (Preprocessor) ขยายการทำงานของคำสั่ง#includeต่างๆ แล้ว จะกลายสภาพเป็นหนึ่ง Translation Unit ซึ่งจะถูกคอมไพล์แยกกันอย่างเป็นอิสระจนได้เป็นไฟล์ออบเจกต์ (.o) - Declaration vs Definition: การประกาศ (Declaration ในไฟล์
.h) คือการบอกคอมไพเลอร์ให้ทราบว่ามีฟังก์ชัน/ตัวแปรนี้อยู่ ส่วนการนิยาม (Definition ในไฟล์.cpp) คือการเขียนเนื้อหาการทำงานจริง ดังนั้นไฟล์เฮดเดอร์ควรเก็บเฉพาะตัวประกาศ เพื่อให้ไฟล์.cppหลายๆ ไฟล์สามารถแชร์สิทธิ์เรียกใช้งานร่วมกันได้โดยไม่เกิดข้อขัดแย้ง - The Linker: หลังจากที่แต่ละ Translation Unit ถูกคอมไพล์แยกกันเรียบร้อยแล้ว Linker จะทำการรวมไฟล์ออบเจกต์เหล่านั้นเข้าด้วยกัน และทำการค้นหาจุดเรียกใช้งานระหว่างกันเพื่อสร้างเป็นไฟล์ประมวลผล (Executable File) เพียงไฟล์เดียว ซึ่งข้อผิดพลาดอย่าง "undefined reference" และ "multiple definition" จะเกิดในขั้นตอนนี้ ไม่ใช่ตอน Compile-time
- Include Guards: ระบบ
#ifndef/#define/#endif(หรือ#pragma once) ช่วยป้องกันไม่ให้ไฟล์เฮดเดอร์ถูกประมวลผลซ้ำสองครั้งใน Translation Unit เดียวกัน ซึ่งหากเกิดขึ้นจะทำให้เกิดข้อผิดพลาดประกาศข้อมูลซ้ำซ้อน
⚠️ Pitfalls (Quick Scan)¶
| ข้อผิดพลาด | วิธีแก้ |
|---|---|
| ลืมใส่เครื่องหมาย Semicolon หลังปิดปีกกา Class | ใส่เครื่องหมาย ; หลังเครื่องหมายปีกกาปิดของคลาสเสมอ |
| เข้าถึงสมาชิกที่เป็น private จากคำสั่งภายนอกคลาส | เปิดสิทธิ์เป็น public: หรือสร้าง Getter/Setter ช่วยจัดการข้อมูล |
| พยายามประกาศออบเจกต์แบบไม่มีอาร์กิวเมนต์หลังจากเขียน Parameterized Constructor | เขียนสั่ง ClassName() = default; ชัดเจนในคลาส |
| ประกาศระบุ Constructor ไว้ในสโคป private ในกรณีทั่วไป | กำหนดให้ Constructor อยู่ในสโคป public เสมอ |
| ใช้ Copy Constructor ค่าเริ่มต้นกับสมาชิกที่เป็น Pointer ดิบ | เขียน Copy Constructor ส่วนตัวเพื่อโคลนจองหน่วยความจำใหม่และทำ Deep Copy |
| เรียกใช้เมธอดแก้ไขค่าตัวแปรบนวัตถุคงที่ (Const Object) | กำกับสัญลักษณ์ const ท้ายเมธอดสำหรับเมธอดประเภทเรียกดูอย่างเดียว |
| ส่งพอยน์เตอร์หรือการอ้างอิงกลับไปยังตัวแปรท้องถิ่น (Local Variable) ภายในเมธอดแบบ const | ส่งกลับโดยรูปแบบคัดลอกค่า (Value) หรืออ้างอิงตัวแปรสมาชิกของคลาสแทน |
| ดึงข้อมูลอ้างอิงชนิดแก้ค่าได้จากฟังก์ชันประเภท Getter | ทำ Overload ให้ Getter ส่งค่ากลับเป็น Const Reference บน Const Method |
| ปล่อยให้ Constructor ชนิดรับพารามิเตอร์ตัวเดียวทำการแปลงรูปแบบอัตโนมัติ | เขียนกำกับพารามิเตอร์ฟังก์ชันด้วย Keyword explicit เสมอ |
| เขียนคำสั่งกำหนดค่าในตัว Constructor แทนที่จะใช้ Initializer List | ประกาศค่าเริ่มต้นสมาชิกคลาสผ่านระบบ Initializer List เสมอ |
ใช้คีย์เวิร์ด static ในระดับ Global เพื่อจำกัดสิทธิ์เรียกใช้งานฟังก์ชันภายในไฟล์ |
ใช้ Anonymous Namespace ในการจำกัดขอบเขตการเข้าถึงโค้ดภายในไฟล์แทน |
| ประกาศนิยามตัวแปรซ้ำกันข้ามเฮดเดอร์ไฟล์โดยไม่ได้ประกาศเป็น inline | เขียนเฉพาะคำประกาศในเฮดเดอร์ และนิยามในไฟล์ .cpp หรือใส่คีย์เวิร์ด inline ข้างหน้า |
| นิยามเนื้อหาฟังก์ชันในเฮดเดอร์โดยตรง | ประกาศเฉพาะใน .h และนิยามในไฟล์ .cpp ที่คู่กัน (เว้นแต่จะระบุเป็น inline) |
| ลืมลิงก์ไฟล์ออบเจกต์ | ตรวจสอบให้แน่ใจว่าได้คอมไพล์และลิงก์ทุกไฟล์ .cpp ที่สร้างสัญลักษณ์ที่จำเป็นต้องใช้ |
📖 Full Details¶
Cause → Effect → Fix พร้อม timestamp (คลิกเพื่อดู)
* **ลืมใส่เครื่องหมาย Semicolon หลังปิดปีกกา Class** -> **เกิด Compile-time error บ่งชี้ปัญหาไวยากรณ์แปลกๆ** -> **ใส่เครื่องหมาย `;` หลังเครื่องหมายปีกกาปิดของคลาสเสมอ (50:00)** * **เข้าถึงสมาชิกที่เป็น private จากคำสั่งภายนอกคลาส** -> **เกิด Compile-time error แจ้งว่าสมาชิกเป็นแบบ inaccessible** -> **เปิดสิทธิ์เป็น `public:` หรือสร้าง Getter/Setter ช่วยจัดการข้อมูล (51:00)** * **พยายามประกาศออบเจกต์แบบไม่มีอาร์กิวเมนต์หลังจากเขียน Parameterized Constructor** -> **เกิด Compile-time error เนื่องจาก Compiler ปิดการสร้าง Default Constructor อัตโนมัติ** -> **เขียนสั่ง `ClassName() = default;` ชัดเจนในคลาส (52:00)** * **ประกาศระบุ Constructor ไว้ในสโคป private ในกรณีทั่วไป** -> **เกิด Compile-time error เนื่องจากโค้ดภายนอกสั่งสร้างออบเจกต์ไม่ได้** -> **กำหนดให้ Constructor อยู่ในสโคป public เสมอ (53:00)** * **ใช้ Copy Constructor ค่าเริ่มต้นกับสมาชิกที่เป็น Pointer ดิบ** -> **ออบเจกต์จะแย่งพอยน์เตอร์แอดเดรสเดียวกัน ทำให้เกิดแครชดับเบิลฟรี (Double-free) ตอนโปรแกรมจบการทำงาน** -> **เขียน Copy Constructor ส่วนตัวเพื่อโคลนจองหน่วยความจำใหม่และทำ Deep Copy (54:00)** * **เรียกใช้เมธอดแก้ไขค่าตัวแปรบนวัตถุคงที่ (Const Object)** -> **เกิด Compile-time error เนื่องจากวัตถุ const ห้ามเปลี่ยนแปลงค่า** -> **กำกับสัญลักษณ์ `const` ท้ายเมธอดสำหรับเมธอดประเภทเรียกดูอย่างเดียว (56:00)** * **ส่งพอยน์เตอร์หรือการอ้างอิงกลับไปยังตัวแปรท้องถิ่น (Local Variable) ภายในเมธอดแบบ const** -> **เกิดตัวแปรค้าง (Dangling pointer/reference) เนื่องจากตัวแปรท้องถิ่นโดนทำลาย** -> **ส่งกลับโดยรูปแบบคัดลอกค่า (Value) หรืออ้างอิงตัวแปรสมาชิกของคลาสแทน (57:00, 58:00)** * **ดึงข้อมูลอ้างอิงชนิดแก้ค่าได้จากฟังก์ชันประเภท Getter** -> **เปิดช่องให้คำสั่งภายนอกแอบมาแก้ไขข้อมูลสมาชิกระดับ private ของคลาสตรงๆ** -> **ทำ Overload ให้ Getter ส่งค่ากลับเป็น Const Reference บน Const Method (59:00)** * **ปล่อยให้ Constructor ชนิดรับพารามิเตอร์ตัวเดียวทำการแปลงรูปแบบอัตโนมัติ** -> **เกิดการสร้างวัตถุชั่วคราวขึ้นมาทำงานเงียบๆ จากตัวแปรชนิดอื่น ส่งผลให้ตรรกะเพี้ยน** -> **เขียนกำกับพารามิเตอร์ฟังก์ชันด้วย Keyword `explicit` เสมอ (60:00)** * **เขียนคำสั่งกำหนดค่าในตัว Constructor แทนที่จะใช้ Initializer List** -> **สมาชิกจะโดน Zero-init รอบหนึ่งก่อนเกิดการเขียนทับ ซึ่งไม่สามารถทำได้กับข้อมูล const หรือ reference** -> **ประกาศค่าเริ่มต้นสมาชิกคลาสผ่านระบบ Initializer List เสมอ (62:00)** * **ใช้คีย์เวิร์ด `static` ในระดับ Global เพื่อจำกัดสิทธิ์เรียกใช้งานฟังก์ชันภายในไฟล์** -> **โปรแกรมรันได้แต่คำสั่ง `static` ถูกยกเลิกความนิยม (Deprecated) ในมาตรฐานใหม่สำหรับกรณีนี้** -> **ใช้ Anonymous Namespace ในการจำกัดขอบเขตการเข้าถึงโค้ดภายในไฟล์แทน (64:00)** * **ประกาศนิยามตัวแปรซ้ำกันข้ามเฮดเดอร์ไฟล์โดยไม่ได้ประกาศเป็น inline** -> **เกิด Linker error บ่งชี้ปัญหา multiple definition ของตัวแปรเดี่ยว** -> **เขียนเฉพาะคำประกาศในเฮดเดอร์ และนิยามในไฟล์ `.cpp` หรือใส่คีย์เวิร์ด `inline` ข้างหน้า (65:00)** * **นิยามเนื้อหาฟังก์ชันในเฮดเดอร์โดยตรงโดยไม่ระบุ `inline`** -> **ไฟล์ `.cpp` แต่ละไฟล์ที่ดึงเฮดเดอร์นี้ไปใช้จะสร้างสัญลักษณ์ซ้ำขึ้นมาเอง ทำให้เกิดข้อผิดพลาด Linker "multiple definition" (นิยามซ้ำซ้อน)** -> **เก็บเฉพาะตัวประกาศไว้ในเฮดเดอร์ แล้วเขียนเนื้อหาในไฟล์ `.cpp` หรือระบุเป็น `inline` หากจำเป็นต้องเขียนเนื้อหาฟังก์ชันไว้ในเฮดเดอร์จริงๆ** * **ลืมใส่ Include Guard หรือ `#pragma once`** -> **หากเฮดเดอร์ถูกดึงไปใช้งานสองครั้งในหนึ่ง Translation Unit (พบบ่อยในกรณีดึงไฟล์ซ้อนกันหลายทอด) คอมไพเลอร์จะมองเห็นการประกาศซ้ำซ้อน** -> **ใส่ครอบเฮดเดอร์ด้วย `#ifndef/#define/#endif` หรือใช้ `#pragma once` เสมอ** * **ลืมลิงก์ไฟล์ออบเจกต์ที่ทำหน้าที่นิยามสัญลักษณ์ที่ต้องใช้** -> **การคอมไพล์รายไฟล์จะผ่านลุล่วง แต่ในขั้นตอนรวมไฟล์ (Link) ขั้นสุดท้ายจะแครชด้วยข้อผิดพลาด "undefined reference to ..."** -> **ตรวจสอบว่าคำสั่ง Link ขั้นสุดท้ายมีไฟล์ออบเจกต์ (`.o` หรือ `.obj`) ของทุกฟังก์ชันที่ถูกเรียกครบถ้วน** * **ประกาศฟังก์ชันในเฮดเดอร์แต่ไม่มีการนิยามโค้ดในไฟล์ `.cpp` ใดๆ เลย** -> **ไฟล์ทุกไฟล์ที่ดึงเฮดเดอร์ไปใช้งานจะคอมไพล์ผ่านไม่มีปัญหา แต่จะเกิดข้อผิดพลาดขั้นตอน Link เพราะไม่มีเนื้อหาโค้ดจริง** -> **เขียนนิยามคำสั่งฟังก์ชันนั้นไว้ในไฟล์ `.cpp` เพียงไฟล์เดียวเท่านั้น** * **นิยามฟังก์ชันที่ไม่ใช่ `inline` ไว้เหมือนกันในสองไฟล์ `.cpp`** -> **Linker ค้นพบสัญลักษณ์ชื่อซ้ำกันสองตัวและไม่สามารถเลือกตัวใดตัวหนึ่งได้ ทำให้เกิดข้อผิดพลาด "multiple definition"** -> **รับประกันให้มีเพียง Translation Unit เดียวเท่านั้นที่นิยามฟังก์ชันที่ไม่ใช่ inline หรือตัวแปรโกลบอล (Global Variable) นั้นๆ**📎 Repo Files¶
26.Classes/26.2YourFirstClass/main.cpp26.Classes/26.3Constructors/main.cpp26.Classes/26.4DefaultedConstructors/main.cpp26.Classes/26.5SettersAndGetters/main.cpp26.Classes/26.6ClassAcrossMultipleFiles/26.Classes/26.8ManagingClassObjectsThroughPointers/main.cpp26.Classes/26.9Destructors/main.cpp26.Classes/26.10OrderOfConstructorDestructorCalls/main.cpp26.Classes/26.11ThisPointer/main.cpp26.Classes/26.12Struct/main.cpp26.Classes/26.13.SizeOfClassObjects/main.cpp27.ZoomingInOnClassObjects/27.2ConstObjects/27.ZoomingInOnClassObjects/27.3ConstObjectsAsFunctionParameters/27.ZoomingInOnClassObjects/27.4ConstMemberFunctions/27.ZoomingInOnClassObjects/27.5GettersThatDoubleAsSetters/27.ZoomingInOnClassObjects/27.6DanglingPointersAndReferences/27.ZoomingInOnClassObjects/27.8MutableObjects/27.ZoomingInOnClassObjects/27.9StructuredBindings/28.DivingDeepIntoConstructorsAndInitialization/28.2DefaultParametersForConstructors/28.DivingDeepIntoConstructorsAndInitialization/28.3InitializerListsForConstructors/28.DivingDeepIntoConstructorsAndInitialization/28.5ExplicitConstructors/28.DivingDeepIntoConstructorsAndInitialization/28.6ConstructorDelegation/28.DivingDeepIntoConstructorsAndInitialization/28.7CopyConstructors/28.DivingDeepIntoConstructorsAndInitialization/28.8ObjectsStoredInArraysAreCopies/28.DivingDeepIntoConstructorsAndInitialization/28.10MoveConstructors/28.DivingDeepIntoConstructorsAndInitialization/28.11DeletedConstructors/29.Friends/29.2FriendFunctions/29.Friends/29.3FriendClasses/30.ConstAndStaticMembers/30.3StaticMemberVariables/30.ConstAndStaticMembers/30.4InlineStaticMemberVariables/30.ConstAndStaticMembers/30.9StaticMemberFunctions/30.ConstAndStaticMembers/30.11InClassMemberVariableInitialization/31.Namespaces/31.2CreatingNamespaces/31.Namespaces/31.6UsingDeclarations/31.Namespaces/31.7AnonymousNamespaces/32.ProgramsWithMultipleFiles/32.2CompilingAndLinking_Model/32.ProgramsWithMultipleFiles/32.4DeclarationsAndDefinitions/32.ProgramsWithMultipleFiles/32.5OneDefinitionRule/32.ProgramsWithMultipleFiles/32.6Linkage/32.ProgramsWithMultipleFiles/32.9InlineVariablesAndFunctions/32.ProgramsWithMultipleFiles/main.cpp