ปัญหา 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;

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

Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

Latest posts by Wutipong Wongsakuldej (see all)

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

Leave a Reply

Your email address will not be published. Required fields are marked *