C++ กับ new และ delete

วันนี้มีน้องปี 2 ส่งโค้ดมาให้ดูว่าทำไมมันพังแบบไม่รู้สาเหตุ ทั้งๆที่มันไม่น่าจะผิดตรงไหน ผมเองก็ลองไล่ดูก็หาไม่เจอเหมือนกัน พอดีน้อง Hint ขึ้นมาว่าพอเปลี่ยนจาก string เป็น char* แล้วก็ใช้ได้ปกติ พอผมดูอีกทีก็ถึงจะเข้าใจว่าเกิดอะไรขึ้น ส่วนความผิดพลาดคืออะไรนั่น เดี๋ยวผมจะอธิบายต่อไปครับ

เข้าใจว่าน้องๆปี 2 อาจจะไม่ได้เรียน C++ มา แต่ว่าอย่างเพิ่งกลัวไปครับ เพราะโดยรวมๆแล้วไวยากรณ์ของมันก็แทบจะไม่ได้แตกต่างกับ C เท่าไหร่ จะมีแค่ Concept กับรายละเอียดเล็กน้อย (ที่มีเยอะ) เท่านั่นเองที่ต่างกัน ซึ่งผมเข้าใจว่าตอนนี้หลายคนน่าจะพอเขียนได้ในระดับหนึ่งแล้ว เพราะฉะนั้นพวกพื้นฐานในการเปลี่ยนจาก C มาเป็น C++ คงจะไม่พูดอะไรมาก แต่มีเรื่องที่อยากให้ทำความเข้าใจนั่นคือ Dynamic Allocation ใน C++ ครับ

ปกติแล้วในภาษา C ถ้าจะทำการ Allocate แบบ Dynamic ก็จะใช้ฟังก์ชัน malloc หรือ calloc ส่วนตอน Deallocate ก็จะใช้ฟังก์ชัน free ซึ่งก็ไม่น่าจะมีปัญหาอะไร แต่มันมีปัญหาเมื่อใช้กับ C++ ครับ เช่น

class Student {
public:
  Student() { cout << "Message from Constructor" << endl; }
  ~Student() { cout << "Message from Destructor" << endl; }
};

int main() {
  Student *student = NULL;

  student = (Student*)malloc(1 * sizeof(Student));
  // . . .
  free(student);

  return 0;
}

ตามปกติแล้ว หลังจากที่สร้าง Object ของคลาสใดคลาสหนึ่งขึ้นมา จะต้องทำการเรียก Constructor เสมอ และเมื่อ Object ถูกทำลายก็จะต้องมีการเรียก Destructor ด้วย แต่ถ้าลองรันโปรแกรมข้างต้นดูจะพบว่า Constructor และ Destructor ไม่ได้ถูกเรียกเลย (ลองรันแล้วจะเห็นว่าไม่มีอะไรพิมพ์ออกมาทางหน้าจอ) เช่นเดียวกันครับ ต่อให้คลาสที่เราเขียนไม่จำเป็นต้องใช้ Constructor และ Destructor แต่ว่าถ้าเรามีสมาชิกของคลาสเป็นคลาสอื่นๆเช่น

class Student {
private:
  string name;
  unsigned long id;
};

เมื่อเวลาที่เราจองพื้นที่สำหรับ Object นั้นด้วยการใช้ malloc จะทำให้คลาส string ไม่ถูกเรียก Constructor ขึ้นมาทำงาน ซึ่งอาจทำให้เกิดความผิดพลาดได้ ดังนั้นด้วยเหตุนี้จึงควรที่จะหลีกเลี่ยง malloc และ free

ถ้าอย่างนั้นเวลาจะจองพื้นที่แบบ Dynamic จะต้องใช้อะไรล่ะ? คำตอบคือคำสั่ง new และ delete ครับ คำสั่ง new เป็นการสั่งให้จองพื้นที่ในส่วนของ Heap ให้ ซึ่งหลักการทำงานก็คล้ายกับ malloc มาก คือไปจองพื้นที่ไว้ให้เราและส่ง address ของตำแหน่งที่จองกลับมาให้ แต่มันมีความสามารถมากกว่า malloc หลายอย่างเช่น

  1. เมื่อเรียก new เพื่อจองพื้นที่ให้กับ Object ของคลาส มันจะเรียก Constructor ให้
  2. cast type ให้เราเสร็จเรียบร้อย ไม่ต้องมา cast เอง
  3. ไม่จำเป็นต้องคำนวนขนาดของพื้นที่ที่ต้องการจอง

แน่นอนว่าพูดไปก็อาจจะไม่เห็นภาพครับ เรามาดูตัวอย่างดีกว่า สมมติเราต้องการสร้าง Object ใหม่ของ Student ขึ้นมา 1 อัน เราใช้คำสั่ง malloc และ new ดังนี้

// malloc & free
student = (Student*)malloc(1 * sizeof(Student));
free(student);

// new & delete
student = new Student;
delete student;

คำสั่งง่ายมากครับ แค่ใช้คำว่า new แล้วตามด้วยชื่อคลาสก็เท่านั้นเอง ไม่จำเป็นต้องใส่ส่วนของการ cast type (Student*) และไม่ต้องคำนวนขนาดด้วย ถ้าเราต้องการแค่อันเดียวก็ไม่ต้องใส่อะไรเข้าไปเลย เวลาที่จะเลิกจองก็ใช้ คำสั่ง delete แล้วตามด้วยตัวแปร (อย่าสับสนนะครับ new ตามด้วยชื่อคลาส แต่ delete ตามด้วยชื่อตัวแปร) ตัว delete จะคล้ายกับ new คือสนับสนุนการทำงานของคลาส คือเมื่อมีการคืนพื้นที่ด้วย delete แล้ว Destructor ก็จะถูกเรียกขึ้นมา ซึ่งถ้าเป็น free จะไม่ทำแบบนั้นให้

กรณีที่ต้องการจองพื้นที่เป็นอาร์เรย์ คำสั่ง new และ delete จะใช้ดังนี้

// malloc & free
student_array = (Student*)malloc(30 * sizeof(Student));
free(student_array);

// new & delete
student_array = new Student[30];
delete [] student_array;

จะเห็นว่าง่ายกว่ามากครับ ถ้าอยากได้ Student จำนวน 30 ก็ใส่วงเล็บ [ ] แล้วใส่จำนวนเข้าไปได้เลย ไม่ต้องมาคำนวนด้วยการคูณด้วยขนาดของแต่ละ type เหมือนกับ malloc ตัว new ที่ฉลาดกว่าจะคำนวนให้เสร็จเรียบร้อย และไวยากรณ์ของ delete ก็เพียงแต่เพิ่ม [] หลัง delete ในกรณีที่เป็นการจองพื้นที่แบบอาร์เรย์

สรุปคือในการจองพื้นที่แบบ Dynamic ให้กับคลาสจะต้องใช้ new และ delete แทนการใช้ malloc กับ free นะครับ แต่ถ้าเป็น primitive type อย่างพวก int, float, double ฯลฯ ก็ยังสามารถใช้ malloc กับ free ได้อยู่ แต่ในเมื่อ new และ delete ฉลาดกว่า แถมใช้ง่ายกว่าเป็นไหนๆ ก็ขอแนะนำให้ใช้เป็น new และ delete ทั้งหมดครับ