Phát hiện Memory Leak bằng CRT library: P2

Như ở phần trước tôi đã giới thiệu sơ qua về CRT libaries và một ví dụ đơn giản để demo. Ở bài viết này, tôi sẽ đem đến một (vài) ví dụ khác mà khó phát hiện được memory leak bằng mắt để cho bạn đọc thấy sự lợi hại của việc áp dụng tool là như thế nào. Ok bắt đầu thôi

Bài viết phần 1: Phát hiện Memory Leak bằng CRT library: P1

Ex#2: Memory leak với class

Phân tích ví dụ

Lần này chúng ta sẽ thử debug một chương trình có class nhé, chương trình như sau

Mô tả một chút về chương trình trên, chúng ta có 2 class là Parent Child (lớp con của Parent). Mỗi lớp đều có thuộc tính là một con trỏ và sẽ được khởi tạo bằng hàm new trong constuctor và sẽ được delete destructor. Chương trình chính sẽ khởi tạo ra 2 đối tượng thuộc lớp này sau đó delete đi để đảm bảo không có leak.

Cố gắng đừng chạy chương trình và hãy thử debug bằng mắt và não xem coi có gì bất thường không nhé! Bạn có 5p….

OK hết giờ! Sao!? Bạn phát hiện ra được gì thú vị không? Chắc là không nhỉ? Mỗi hàm new đều được đi kèm với delete rồi, leak sao được!

Giờ chạy thử và xem kết quả nhé

Ớ đậu!? Nhớ delete rồi mà sao leak được ta!? Ok kéo lên dòng 35 của file source.cpp chúng ta thấy hàm new cho p_child của class Child. Rà soát lại thử đoạn code hàm main coi có gì bất thường không. Line 48, 49 chắc chắn là không có gì bất thường ở đây cả. Khởi tạo một con trỏ kiểu Child trỏ tới kiểu Child, ok xem tiếp. Line 51, 51 á à chỗ này hơi lạ, con trỏ kiểu Parent trỏ tới kiểu Child, hmmm…Bây giờ chúng ta thử đặt log bên trong các hàm constructor destructor để xem chuyện gì đang diễn ra. Chương trình được viết lại như sau

Output của chương trình

Child* p_obj1 = new Child and delete p_obj1
-> create parent: 0x4b88a0
— (parent) new int(0) 0x4b5ca0
— -> create child: 0x4b88a0
— — (child) new int(1) 4b88d8
— — (child) delete int(1) 4b88d8
— –< detele child: 0x4b88a0
— (parent) delete int(0) 0x4b5ca0
–< delete parent: 0x4b88a0
<======================>
Parent* p_obj2 = new Child and delete p_obj2
-> create parent: 0x4b88a0
— (parent) new int(0) 0x4b5ca0
— -> create child: 0x4b88a0
— — (child) new int(1) 4b88d8
— (parent) delete int(0) 0x4b5ca0
–< delete parent: 0x4b88a0

Với biến p_obj1, chúng ta có thể thấy new đều đi với delete theo cặp, nên sẽ không có leak ở trường hợp này. Tuy nhiên ở biến p_obj2 thì lại khác, có (child) new int(1) mà lại không thấy (child) delete int(1) ở đâu cả và đó là lúc leak xảy ra.

Vậy là CRT libraris đã phát hiện chính xác, nhưng tại sao hàm delete destructor của Child lại không được gọi trong trường hợp này? Để ý chúng ta thấy là chúng ta thấy p_obj2 là pointer kiểu Parent, khi new thì new kiểu Child. Khi chúng ta delete con trỏ này bằng delete p_obj2 trình biên dịch không thể nào biết được thức tế nó là một object kiểu Child. Và do đó, nó sẽ gọi hàm destructor ~Parent() và không sẽ không có hàm destructor ~Child() nào được gọi và vì thế leak sẽ xảy ra.

Giải quyết memory leak

Nguyên nhân thì đã tìm ra và hiểu rõ ràng. Vậy giờ chúng ta phải làm sao để giải quyết vấn đề này ngăn chặn memory leak!?

Thật đơn giản chúng ta chỉ cần thêm từ khoá virtual vô trước mỗi hàm destructor để khi delete p_obj2, hàm destructor của Child cũng sẽ được gọi theo và như thế là sẽ không bị leak nữa!