ปัญหา memory leak บน C++ ส่วนใหญ่เกิดจากภาษา Java

หลายคนคงคิดว่า มันไปเกี่ยวอะไรกับ Java ? จะบอกว่า C++ ไปเรียกใช้ Java ก็เห็นจะไม่ใช่ แล้วมันเกี่ยวยังไง ? ภาษา Java เกิดมาหลัง C++ ด้วยซ้ำไป

ที่จริงมันก็ไม่เกี่ยวกับ Java หรอกครับ ที่ผมจะบอกคือ มีหลายคน (มาก) ที่เขียนภาษา C++ ด้วยวิธีแบบ Java ซึ่งมักจะเกิดกับคนที่เรียน Java มาก่อนหัดเขียน C++ น่ะครับ

ยกตัวอย่าง ใน Java เราจะสร้าง instance ของคลาสด้วยการใช้คำสั่ง new แบบนี้

Object obj = new Object();

แล้วอยากจะใช้อะไรก็ใช้ไปเลย ใช้เสร็จแล้วก็ปล่อยมันไว้อย่างนั้น แล้ว garbage collector ก็จะมาจัดการเองเมื่อวัตถุนั้น ๆ ไม่ได้ถูกใช้งานแล้ว

ใน C++ เองก็มีการใช้ new เหมือนกัน (ความจริง new operator ของ Java ก็มาจาก C++ นี่ล่ะ) เพียงแต่ว่าใน C++ นั้นไม่มี garbage collector ดังนั้นผู้ใช้ก็จะต้องลบ object นั้น ๆ ทิ้งด้วยตัวเอง ด้วย delete operator แบบนี้

Object *obj = new Object();
...
delete obj;

แล้วจะเกิดอะไรขึ้นถ้าเราลืมเรียก delete ? คำตอบคือ object นั้นก็จะลอยอยู่ใน heap จนกว่าโปรแกรมจะปิดตัวไปนั่นล่ะครับ (หรือที่แย่กว่าก็จนกว่าจะปิดเครื่อง)อาการนี้เรียกว่า “memory leak” คือการที่หน่วยความจำใน heap ถูกจองไปแล้วไม่สามารถนำกลับมาใช้ได้อีกถึงแม้ว่ามันจะไม่ถูกอ้างถึงแล้ว

กลับไปยัง Java อีกที ใน Java เราจะสร้าง array ด้วย new operator เช่นกัน เพียงแต่จะใส่ [] เอาไว้หลัง type ด้วย แบบนี้

Object[] arrayObj = new Object[10];

อะไรทำนองนี้ ใน C++ ก็เขียนคล้ายกันได้เช่นกัน แบบนี้

Object* arrayObj = new Object[10];
...
delete[] arraryObj;

ทั้งนี้ต้องอย่างลืมว่า สร้างด้วย new [] ก็ต้องลบด้วย delete[] ด้วยนะครับ เหตุผลคือ ถ้าเราไม่ใส่ [] มันจะไปทำลาย Object แรกสุดในอะเรย์เท่านั้น

อ่านจนถึงบรรทัดนี้หลาย ๆ คนคิดว่า เออมันก็ดูไม่มีปัญหาอะไรนี่ ? ใช่ครับมันไม่มีปัญหาอะไร ถ้าทำทุกอย่างถูกต้อง ปัญหามันจะมาตอนลืมใส่ delete เลือกคำสั่งผิด (ดันใช่ delete แทน delete[]) หรือแม้กระทั่งคำสั่ง delete นั้นไม่ถูกเรียก ลองดูโค๊ดนี้

Socket *pSocket = new socket(80);
pSocket->connect();
....
delete pSocket;

ถามว่า ถ้าเกิดคำสั่ง pSocket->connect() ดันโยน exception ออกมา จะเกิดอะไรขึ้น ง่าย ๆ ครับ บรรทัด delete pSocket; ก็จะไม่ถูกเรียก และหน่วยความจำส่วนของ pSocket ก็จะรั่วไป (เผลอ ๆ พอร์ท 80 จะถูกเปิดค้างไว้ด้วยนะคราวนี้)

ก็ต้องครอบ try/catch อีก กลายเป็น

Socket *pSocket = new socket(80);
try {
pSocket->connect();
....
} catch (...) {
    delete pSocket;
    pSocket = nullptr;
}

if(pSocket) delete pSocket;

วิธีประกาศตัวแปรแบบพื้นฐาน

อ่านถึงตรงนี้ก็คงคิดว่า โห C++ ยากจัง …. ทำไมมันวุ่นวายจัง หลาย ๆ คนน่าจะจับได้แล้วว่าปัญหามันอยู่ตรงไหน โค๊ดข้างบนทั้งหมดเป็นความผิดพลาดของมือใหม่ C++ เลยล่ะครับ คืองี้ครับ ใน C++ วิธีการสร้าง instance ของ class นั้นเราเขียนแบบนี้ครับ

Object obj;

และการสร้าง array ก็เขียนแค่

Object arrayObj[10];

เท่านั้นเอง ตัวแปรชุดนี้ถูกสร้างขึ้นใน stack และเมื่อโปรแกรมรันจนสุด scope ที่ตัวแปรตัวนั้นทำงานอยู่ มันก็จะถูกทำลายไปเอง เราไม่ต้องไปยุ่งอะไรกับมัน

ส่วนโค๊ด socket ข้างบนน่ะเหรอ ? ก็เขียนแค่นี้เอง

Socket socket(80);
socket.connect();

… เนี่ย พอแล้ว เมื่อสิ้นสุดสโคปของโค้ด ไม่ว่าจะเพราะว่ารันจนถึงปีกกาปิด หรือมี exception โยนออกมา ตัวแปร socket จะถูกทำลายไปเองโดยอัตโนมัติ ไม่ต้องไปยุ่งอะไรกับมันเช่นกัน

คำสั่ง new

แล้วไอ้คำสั่ง new มันทำอะไรกันแน่ … คำสั่ง new คือการสร้างวัตถุใน heap แล้วคืนค่าที่อยู่กลับมาให้ เราก็เอา pointer ไปรับมันมา ซึ่งนั่นหมายถึงเราจะมีวัตถุสองชิ้น ชิ้นหนึ่งคือวัตถุใน heap อีกชิ้นหนึ่งคือ pointer ที่สร้างขึ้นเพื่อไปรับค่าที่คืนมาจาก delete

เจ้า pointer นี่ก็อยู่ใน stack เหมือนตัวแปรอื่น ๆ นี่แหละ ดังนั้นเมื่อรันไปหมด scope หรือเกิด exception ขึ้นมันก็จะถูกทำลายทิ้ง แต่วัตถุที่มันชี้ไปนั้นก็จะไม่ถูกทำลายตามไปด้วย ซึ่งผู้ที่เรียกใช้ new นั้นจะต้องรับผิดชอบในการเรียกใช้ delete(ไม่งั้นหน่วยความจำก็รั่ว)

และอะไรก็ตามที่มีคนเข้ามาเกี่ยวข้อง ก็จะเป็นจุดที่มักจะเกิดปัญหา ถูกไหมครับ ?

ข้อแนะนำตรงนี้คือ ถ้าไม่จำเป็น ก็อย่าไปเรียกใช้มัน เป็นการเลี่ยงปัญหาไม่ให้เกิด

แต่ถ้าจำเป็นล่ะ ? ถ้าสมมติเราใช้ polymorphism เยอะ ๆ (ซึ่งไม่สามารถใช้กับ stack variable ได้) อย่างเช่นเราเขียนคลาสพวก abstract factory ที่ต้องคืนค่าเป็น pointer ที่มีไทป์เป็น abstract class (ซึ่งไม่สามารถอยู่ได้ด้วยตัวมันเอง) วิธีที่ง่ายที่สุดคือใช้ smart pointer

Smart Pointer

Smart Pointer เป็น pointer ที่ฉลาดขึ้นมาอีกหน่อย กล่าวคือ มันรู้ว่ามันจะต้องไปทำลายวัตถุที่ตัวมันชี้ไปหาด้วยนั่นเอง

ใน C++11/14 นั้นจะมี Smart Pointer อยู่สองตัว ก็คือ std::shared_pointer กับ std::unique_pointer แต่ถ้าคุณไม่ได้ใช้ compiler ที่รองรับก็สามารถใช้ boost library ได้เช่นกัน (ก็จะเป็น boost::shared_pointer กับ boost::scoped_pointer ตามลำดับ)

สำหรับวิธีใช้นั้นก็ไม่ยาก ในตำแหน่งที่เราระบุประเภทของตัวแปร แทนที่เราจะใช้ pointer เปลือย ๆ ก็เอา smart pointer มารับเท่านั้นเอง เช่น

#include<memory>

Font* FontFactory::Load();
....
std::shared_pointer<Font*> font = FontFactory::Load(path);

หรือแบบนี้ก็ได้นะ เหมือนกัน

auto font = std::shared_pointer<Font*>(FontFactory::Load(path));

ความแตกต่างระหว่าง std::shared_pointer กับ std::unique_pointer ก็คือ เราสามารถสร้างสำเนาของ std::shared_pointer ได้ ในขณะที่เราทำแบบเดียวกันกับ std::unique_pointer ไม่ได้ นั่นหมายถึงคุณจะทำแบบข้างล่างไม่ได้

std::unique_ptr<int> u1 = new int(10);
std::unique_ptr<int> u2 = u1; //compile error;

ข้อแม้ของการใช้ smart pointer ใน standard libary ก็คือ ไทป์ของวัตถุที่เราจะสร้าง smart pointer นั้นจะต้องมี delete operator มารองรับด้วย ไม่เช่นนั้นเราต้องระบุวิธีทำลายให้ smart pointer รู้ (ไม่เช่นนั้นมันทำไม่เป็น) ซึ่งผมจะไม่เขียนถึงตอนนี้นะครับ

Dynamic Array

ใน C++ การใช้ new[] เราจะเรียกว่าเป็นการสร้าง dynamic array เพราะว่าเราสามารถใช้สร้าง array ที่เราไม่ทราบขนาดในตอนเขียนโค๊ดได้ เช่น

int* pArray = new int[10];

ข่าวร้ายคือ ตอนนี้เรายังไม่มีวิธีอื่นในการสร้าง dynamic array (นอกเหนือไปจากอีกวิธีนึงของภาษา C ซึ่งไม่ควรใช้พอกัน) ดังนั้นก็จะมีกรณีที่จำเป็นจริง ๆ ก็ใช้ new[] ไปพลาง ๆ ก่อน

แต่กรณีที่จำเป็นต้องใช้ dynamic array จริง ๆ นั้นก็เห็นจะมีแต่การเรียกใช้คำสั่งในภาษา C (ที่ถูก #include เข้ามา) ในกรณีส่วนใหญ่เราสามารถใช้ std::vector แทนได้ ซึ่ง std::vector นั้นมีความสามารถเหนือกว่า array นิดหน่อยตรงที่มันสามารถขยายขนาดของตัวมันเองได้ ในขณะที่ตัวมันเองโดยโครงสร้างแล้วเป็น array list ดังนั้น performance ก็ไม่น่าจะต่ำกว่า array จริง ๆ มากมายเท่าไหร่

อย่างโค๊ดข้างบนเราก็สามารถใช้ std::vector แทนได้ แบบนี้

#include<vector>

std::vector<int> intVec(10);

intVec[0] = 10;

อะเไรก็ว่ากันไป

One thought on “ปัญหา memory leak บน C++ ส่วนใหญ่เกิดจากภาษา Java

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

This site uses Akismet to reduce spam. Learn how your comment data is processed.