Smart Pointer trong C++? Nó cũng chỉ là 1 "đối tượng" thôi, dễ hiểu mà!

Đây là bài viết đầu tiên của mình về 1 chức năng của 1 ngôn ngữ lập trình riêng: "Con trỏ thông minh (Smart pointer) trong C++". Tạm gác qua mọi "ác cảm" của thế giới với C++, cũng như "con trỏ", mình sẽ giới thiệu khái quát một cách "vô cùng dễ hiểu" về Smart pointer trong C++ trước. Có thời gian, mình sẽ viết 1 seri bài viết nói về "con trỏ" của C++; và đương nhiên trong đó, mình sẽ viết đầy đủ toàn bộ về Smart Pointer. 

(nguồn: TeckTalk Fb fanpage)

Vì sao lại là Smart Pointer? Khác j Pointer thông thường?

Đúng vậy, vì nó "thông minh" hơn, nên nó được gọi là Smart Pointer thôi; còn thằng Pointer cổ điển thì là Stupid Pointer chứ sao! Vì nó "smart" hơn nên nó có nhiều khác bọt so với Pointer chứ. 

Trong C/C++, có 2 vùng bộ nhớ để lưu lại các biến, gọi là Stack và Heap. Khi bạn khai báo một biến mới mà dùng từ khóa new thì biến đó được quẳng vào Heap, còn nếu ko dùng new thì nó sẽ được quẳng vào Stack. Tuy đều là các vùng chứa bộ nhớ, nhưng Stack và Heap có đặc điểm khác nhau để phục vụ những mục đích riêng. Các đối tượng được lưu trong Stack sẽ "tự động" hủy khi thoát khỏi "phạm vi" của chúng trong chương trình. Còn Heap thì ko! Đây chính là điều khiến người ta ghét Pointer, từ đó ghét C++!

Bạn cần biết rằng đa số việc sử dụng new để khai báo thêm biến mới sẽ đều phải sử dụng đến Pointer. Do đó, đa số các đối tượng của chương trình sẽ được lưu trong Heap.

"Scope (phạm vi)" của 1 khối lệnh trong trương trình được giới hạn bằng 2 dấu mở và đóng ngoặc nhọn "{}"
Lưu ý rằng các khai báo phần tử mà cần dùng cặp dấu ngoặc nhọn như trong Mảng và các cấu trúc dữ liệu mở rộng trong thư viện STL ko phải Scope nhé! Còn tập các dòng lệnh sau các cấu trúc lặp hay điều khiển như for hay if thì mới là Scope. Chương trình main cũng chứa trong nó 1 phạm vi nhất định nhé; vì bạn có thể viết code ở ngoài hàm main thoải mái.

Có 3 cách để thoát khỏi Scope:
  • Gặp dấu đóng ngoặc nhọn }
  • Gặp return
  • Gặp Exception trong Scope hiện tại mà ko xử lý catch trong cùng Scope.
Vậy với thể loại biến không lưu trong Stack thì sao, nó ko thể "tự hủy" khi thoát khỏi Scope thì phải hủy bằng tay? Đúng vậy, ta phải hủy bằng tay với lệnh delete (trong C++) và free (trong C). Sau khi ta hủy bằng tay xong, vùng nhớ đó sẽ được "giải phóng" và được Hệ điều hành thu hồi để cấp phát cho lần xin bộ nhớ khác. Nếu ko làm điều này, cộng thêm bạn code "ngu" hoặc chương trình cồng kềnh; khả năng rất cao là bạn sẽ gặp lỗi tràn bộ nhớ "Out of Memory". Đừng có coi thường lỗi này và lấy lý do là ngày nay thừa RAM chứ có thiếu RAM như những năm cuối thế kỷ 20 đâu! Bạn sẽ phải trả giá rất đắt! Chương trình bị crash, full RAM, bạn sẽ phải khởi động lại máy. Nếu máy đó là Server thì bạn phải khởi động lại Server! Vậy ai cho bạn cái quyền khởi động lại Server khi đang chạy?

Lưu ý thêm, hãy phân biệt 2 khái niệm "tràn bộ nhớ" và "tràn bộ đệm" nhé! Tràn bộ nhớ (out of memory) là lỗi lập trình; còn Tràn bộ đệm "buffer overflow" là 1 loại tấn công trong Security! Mình sẽ viết bài về nó sau.

Đừng tự ti vì bạn code "ngu"! Khi làm việc với C++, ai cũng có thể sẽ bị "ngu" thôi! Không phải vì kỹ năng kém; mà đôi khi là do sơ suất! Vấn đề ở đây là làm sao để chương trình tránh khỏi Tràn bộ nhớ khi ta chẳng may "quên" delete hay free? Đương nhiên là có giải pháp; và giải pháp đó được gọi là Smart Pointer.

Nó cũng chỉ là 1 Object thôi bạn à!

Smart Pointer là 1 loại dữ liệu để "giả lập" Pointer thật sự. Nó bổ sung thêm khả năng tự "dọn rác" mà Pointer ko có. Đương nhiên sẽ xuất hiện trade-off ở đây; và mình sẽ nói kỹ hơn trong loạt bài về Pointer. Smart Pointer được thiết kế dựa trên quan điểm của Proxy Pattern (1 pattern khá hay trong tập hợp các Design Pattern). Do đó, bạn cứ hiểu Smart Pointer tuy smart thật, nhưng nó cũng là 1 Object thôi; để hiểu nó thì ko hề quá khó!

Khi tạo ra 1 Smart Pointer, để "giả lập" được như Pointer, ta phải tiến hành nạp chồng (overload) các toán tử truy xuất dữ liệu (toán tử *) cũng như truy xuất thành phần (2 toán tử .->). Khi đó, ta mới có thể dùng Smart Pointer y như một Pointer thông thường. 

Đây là cách tôi xây dựng 1 Smart Pointer và sử dụng nó.
Trong chương trình trên, Smart Pointer tôi tạo ra chính là các đối tượng được đại diện bởi lớp Ptr. Dựa theo quan điểm của Proxy Pattern, bên trong Ptr sẽ chứa một Pointer thật (int *ptr). Tôi đã nạp chồng thêm toán tử truy xuất dữ liệu (toán tử *) cho lớp Ptr; từ đó các đối tượng được tạo ra từ lớp Prt này đều có thể làm việc giống như 1 Pointer thật (đương nhiên chưa đủ vì chưa nạp chồng 2 toán tử truy xuất thành phần).

Trong hàm main, ta thấy rằng đối tượng prt được tạo ra sẽ nằm trong Stack; do đó khi gặp return, nó sẽ có thể tự hủy. Khi nó tự hủy, "destructor" của Ptr sẽ được gọi tới và thực hiện lệnh delete. Tất cả những điều này đều là auto và ta ko cần phải delete bằng tay nữa! Quá dễ hiểu phải ko! Game là dễ! Quẩy lên!

Lưu ý về mặt code: "ko nên sử dụng "using namespace std;" nhé! Với kinh nghiệm code thực tế của mình và mình có được từ những người có kinh nghiệm hơn, thì nên viết rõ namespace ra. Nếu namespace dài quá, bạn có thể đặt lại tên cho ngắn gọn; nhưng tuyệt đối sử dụng using namespace ... ở trong 1 chương trình lớn! Lý do là vì sẽ có khả năng các namespace khác nhau sẽ có các hàm cùng tên, cùng kiểu tham số luôn (nói chung là giống nhau y hệt về mặt khai báo). Do đó nếu sử dụng using namespace ở đây, trình biên dịch sẽ ko biết bạn muốn sử dụng hàm cần thực hiện ở namespace nào, có thể dẫn đến crash hoặc chạy ko theo mong muốn của bạn.

Đừng lạm dụng cho dù bạn đã hiểu!

Bình tĩnh, "quẩy sớm ăn j!" Haha! Như tôi đã nói, việc sử dụng Smart Pointer sẽ tạo ra hiện tượng trade-off! Smart Pointer tuy "thông minh", tự biết "dọn rác" giúp bạn; nhưng đánh đổi lại, nó lại cồng kềnh hơn Pointer cổ điển! Bạn thấy "rảnh tay" hơn, nhưng trình của bạn sẽ chạy "chậm" hơn. Do đó, nếu có thể, hãy vẫn nên sử dụng Pointer cổ điển. Smart Pointer chỉ nên sử dụng nếu bạn thực sự thấy quá khó khăn trong việc "quản lý bộ nhớ" với những chương trình cồng kềnh!

C++ hỗ trợ 3 loại Smart Pointer để bạn có thể sử dụng. Ngoài ra trong thư viện boost còn hỗ trợ thêm 1 loại Smart Pointer nữa. Nhưng nhớ hãy cân nhắc kỹ trước khi sử dụng, vì bạn sẽ phải trade-off đó!

Nhận xét

Bài đăng phổ biến từ blog này

Trên con đường tu đạo luôn cực kỳ theo đuổi!

C++ Con trỏ (Pointer) toàn thư: Phần 4: Con trỏ "đa cấp". Đánh nhau bằng con trỏ.

Quan hệ giữa các phân phối xác suất thông dụng nhất: Beta và Dirichlet không giống Gaussian!