ข้ามไปที่เนื้อหา

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.cpp
  • 26.Classes/26.3Constructors/main.cpp
  • 26.Classes/26.4DefaultedConstructors/main.cpp
  • 26.Classes/26.5SettersAndGetters/main.cpp
  • 26.Classes/26.6ClassAcrossMultipleFiles/
  • 26.Classes/26.8ManagingClassObjectsThroughPointers/main.cpp
  • 26.Classes/26.9Destructors/main.cpp
  • 26.Classes/26.10OrderOfConstructorDestructorCalls/main.cpp
  • 26.Classes/26.11ThisPointer/main.cpp
  • 26.Classes/26.12Struct/main.cpp
  • 26.Classes/26.13.SizeOfClassObjects/main.cpp
  • 27.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