Think Like a Programmer #5 — Con trỏ và Bộ nhớ Động: Khi "địa chỉ" là chìa khoá
Mở đầu
Từ hồi còn đi học, mình nhớ cái cảm giác lần đầu thấy con trỏ trong C — mấy cái dấu * với & loạn xạ hết cả lên. Hồi đó mình chỉ biết nhắm mắt viết theo, chứ hỏi "tại sao" thì chịu. Sang tới C++ cũng vậy, new với delete cứ như bùa phép.
Nhưng mà, như mấy bài trước mình đã kể, sách Think Like a Programmer của thầy V. Anton Spraul không phải là sách dạy cú pháp. Thầy dạy cách nghĩ. Qua Chương 1 (Strategies), Chương 2 (Pure Puzzles), Chương 3 (Arrays), mỗi chương mình đều có cái "à ra thế" riêng.
Tới Chương 4 — Solving Problems with Pointers and Dynamic Memory — mình biết ngay đây sẽ là chương "tủ" của cuốn sách này.
Ảnh: Brett Jordan — Pexels
Chương 4: Giải quyết vấn đề với Con trỏ và Bộ nhớ Động
Ôn lại căn bản — nhưng không nhàm chán
Spraul bắt đầu chương bằng cách ôn lại pointer fundamentals — con trỏ là biến lưu địa chỉ, dùng & lấy địa chỉ, dùng * để dereference. Nghe thì cơ bản, nhưng thầy có cách giải thích rất dễ hiểu: con trỏ là một alias, một cái tên khác cho một vùng nhớ.
Cái hay là thầy không dừng ở lý thuyết. Thầy vẽ memory diagram, chỉ rõ từng ô nhớ trên stack, giá trị của từng biến, con trỏ trỏ tới đâu. Thầy bắt người đọc trace từng dòng code bằng tay.
Cách dạy này làm mình nhớ tới câu thầy nói ngay từ đầu sách: "Program building is a creative process; program understanding is not." — Hiểu chương trình là một quá trình có phương pháp, không phải nghệ thuật.
Dynamic memory — lúc nào cần?
Spraul giải thích rất rõ sự khác nhau giữa stack và heap:
- Static allocation (trên stack) — tự động giải phóng khi ra khỏi scope
- Dynamic allocation (trên heap) — tự quản lý, dùng
newvàdelete
Tình huống điển hình: khi bạn đọc file và không biết trước có bao nhiêu dòng, hoặc khi bạn xây dựng một cấu trúc dữ liệu cần tồn tại lâu hơn hàm đã tạo ra nó.
Linked list — "killer app" của con trỏ
Chương này dành kha khá thời gian cho linked list, và mình nghĩ đó là lựa chọn đúng đắn. Linked list là cấu trúc dữ liệu kinh điển mà nếu không có con trỏ thì không làm được.
Spraul hướng dẫn từng bước:
- Định nghĩa node (struct chứa data + pointer tới node tiếp theo)
- Thêm node vào đầu danh sách
- Duyệt danh sách
- Tìm và xoá node
Mỗi bước đều kèm memory diagram và giải thích tại sao con trỏ hoạt động như vậy. Đây chính là cái mình thích nhất ở sách này — thầy không chỉ cho bạn code mẫu, thầy dạy bạn cách suy nghĩ về code.
Các cạm bẫy thường gặp
Spraul dành hẳn một phần để nói về những lỗi kinh điển với con trỏ:
| Lỗi | Hậu quả | Cách phòng tránh |
|---|---|---|
| Memory leak | Quên delete, chương trình ngốn RAM | Dùng RAII, smart pointers (C++) |
| Dangling pointer | Dùng con trỏ sau khi delete | Set con trỏ về NULL sau delete |
| Double delete | delete hai lần cùng một vùng nhớ | Dùng unique_ptr, shared_ptr |
| Buffer overflow | Ghi dữ liệu quá kích thước mảng | Luôn kiểm tra bounds |
Điều mình tâm đắc nhất là cách thầy hướng dẫn debug với memory diagrams. Khi trace từng dòng code và vẽ trạng thái bộ nhớ, hầu hết bug pointer đều lộ diện.
Ownership — ai chịu trách nhiệm?
Một khái niệm Spraul nhấn mạnh là ownership — khi cấp phát động, phải xác định rõ ai (hàm nào, class nào) chịu trách nhiệm giải phóng vùng nhớ đó. Đây là ý tưởng rất mạnh, và sau này mình thấy nó xuất hiện rõ nhất trong Rust với ownership system.
Ảnh: Alexandra — Pexels
Cảm nhận của mình
Con trỏ là một trong những thứ làm mình tốn nhiều thời gian nhất hồi mới học lập trình. Không phải vì nó khó về mặt kỹ thuật, mà vì nó đòi hỏi một mental model khác hẳn với những gì mình đã quen.
Với biến thường, bạn nghĩ "tôi có một hộp, trong hộp đựng số 42". Với con trỏ: "tôi có một hộp, trong hộp đựng một tờ giấy ghi số nhà của một hộp khác". Nghe thì đơn giản, nhưng khi code dài cả trăm dòng, việc nhớ được "ai trỏ tới ai" không hề dễ.
Cái mình thích ở cách Spraul dạy là thầy không né tránh sự phức tạp. Thầy đối diện với nó, phân tích từng bước, và đưa ra phương pháp cụ thể (vẽ memory diagram, trace bằng tay). Đây không phải là "mẹo" hay "trick" — đây là kỹ thuật tư duy có thể áp dụng cho mọi ngôn ngữ.
Phần về ownership cũng rất quan trọng. Mình thấy trong thực tế, phần lớn bug "use after free" và memory leak đều xuất phát từ việc không xác định rõ ownership. Nếu từ đầu bạn biết "biến A là chủ, biến B chỉ mượn tạm", mọi thứ rõ ràng hơn hẳn.
Và mình cũng thấy vui là những bài học này giờ đã được "đóng gói" vào ngôn ngữ — Rust có ownership, C++ có smart pointers. Nhưng để hiểu được tại sao những thứ đó tồn tại, bạn vẫn cần hiểu con trỏ.
Kết
Chương 4 này đối với mình là chương "gỡ rối" nhiều nhất vì nó giải thích thứ mình từng sợ nhất — con trỏ — theo một cách có hệ thống và dễ tiếp cận.
Qua 4 chương rồi, mình thấy rõ một điều: Think Like a Programmer không phải là sách dạy C++ (dù code mẫu viết bằng C++). Nó là sách dạy tư duy giải quyết vấn đề, và Spraul khéo léo dùng những khái niệm khó nhất của C++ (con trỏ, bộ nhớ động) làm công cụ để rèn tư duy đó.
Hẹn mấy bạn chương sau — Solving Problems with Classes. 🚀
📋 Phụ lục thuật ngữ
- Pointer — con trỏ, biến lưu địa chỉ của một vùng nhớ
- Dereference — truy xuất giá trị tại địa chỉ mà con trỏ đang trỏ tới (dùng *ptr)
- Dynamic memory — bộ nhớ cấp phát động trên heap, tự quản lý
- Memory leak — rò rỉ bộ nhớ, quên giải phóng vùng nhớ đã cấp phát
- Dangling pointer — con trỏ trỏ tới vùng nhớ đã được giải phóng
- Ownership — khái niệm ai chịu trách nhiệm giải phóng một vùng nhớ
- Linked list — danh sách liên kết, cấu trúc dữ liệu dùng con trỏ để kết nối các phần tử
- RAII (Resource Acquisition Is Initialization) — kỹ thuật trong C++ gắn vòng đời tài nguyên với vòng đời đối tượng
- Smart pointer — con trỏ thông minh (unique_ptr, shared_ptr) tự động giải phóng bộ nhớ