🌱 Dangling Pointer trong C++ và cách tránh lỗi
Trong ngôn ngữ lập trình C++, dangling pointer (con trỏ treo) là một vấn đề phổ biến khi sử dụng Pointer, có thể gây ra các lỗi và bất ổn trong ứng dụng. Trong bài viết này, chúng ta sẽ khám phá về khái niệm của dangling pointer, tại sao nó xảy ra và cách tránh lỗi này trong quá trình lập trình.👉 Dangling Pointer Là Gì?
Dangling pointer là một con trỏ vẫn tồn tại nhưng không hợp lệ hoặc không trỏ đến vùng nhớ cần thiết. Điều này thường xảy ra khi vùng nhớ mà con trỏ trỏ tới đã bị giải phóng hoặc trỏ tới vùng nhớ không đáng tin cậy.
Điều này có thể dẫn đến các hậu quả khó lường và thậm chí là crash chương trình nếu con trỏ đó được sử dụng một cách không an toàn.
👉 Nguyên nhân phổ biến dẫn đến dangling pointer
💬 Sử dụng con trỏ không được khởi tạo - Uninitialized Pointer
#include <iostream>
using namespace std;
int main()
{
int * p_number; // Dangling uninitialized pointer
std::cout << std::endl;
std::cout << "Case 1 : Uninitialized pointer : ." << std::endl;
std::cout << "p_number : " << p_number << std::endl;
std::cout << "*p_number : " << *p_number << std::endl; //CRASH!
}
Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Case 1 : Uninitialized pointer : .
p_number : 0
Tức là đến đây chương trình bị crash và không thể in ra dòng cuối cùng "*p_number : ", nguyên nhân là do chương trình đang cố gắng truy cập vào một con trỏ chưa được khởi tạo, tức là chứa giá trị NULL.
💬 Sử dụng con trỏ đã bị giải phóng - Deleted Pointer
#include <iostream>
using namespace std;
int main()
{
std::cout << std::endl;
std::cout << "Case 2 : Deleted pointer" << std::endl;
int * p_number1 {new int{67}};
std::cout << "*p_number1 (before delete) : " << *p_number1 << std::endl;
delete p_number1;
std::cout << "*p_number1 (after delete) : " << *p_number1 << std::endl;
}
Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Case 2 : Deleted pointer
*p_number1 (before delete) : 67
*p_number1 (after delete) : 1511538876
Dễ thấy, khi được cấp phát bằng toán tử new, pointer p_number1 sẽ trỏ đến số nguyên với giá trị là 67 trên vùng nhớ Heap, nên giá trị in ra khi truy xuất *p_number1 sẽ là 67.
Sau khi gọi toán tử delete, vùng nhớ Heap này sẽ bị hủy, nhưng pointer thì vẫn tồn tại. Khi cố gắng truy xuất *p_number1 sẽ trả về giá trị bất kỳ. Việc này có thể dẫn đến các phần tính toán khác trong chương trình bị sai.
💬 Nhiều con trỏ chứa cùng một địa chỉ
#include <iostream>
using namespace std;
int main()
{
std::cout << std::endl;
std::cout << "Case 3 : Multiple pointers pointing to same address : " << std::endl;
int *p_number3 {new int{83}};
int *p_number4 {p_number3};
std::cout << "p_number3 - " << p_number3 << " - " << *p_number3 << std::endl;
std::cout << "p_number4 - " << p_number4 << " - " << *p_number4 << std::endl;
// Deleting p_number3
delete p_number3;
// p_number4 points to deleted memory. Dereferncing it will lead to
// undefined behaviour : Crash/ garbage or whatever
std::cout<< "p_number4 (after deleting p_number3) - " << p_number4 << " - " << *p_number4 << std::endl;
}
Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Case 3 : Multiple pointers pointing to same address :
p_number3 - 0x5607cb8f82c0 - 83
p_number4 - 0x5607cb8f82c0 - 83
p_number4 (after deleting p_number3) - 0x5607cb8f82c0 - 1618786552
Đối với trường hợp này, cả 2 con trỏ p_number3 và p_number4 đều chứa địa chỉ của một số nguyên (giá trị là 83) trên vùng nhớ Heap. Tuy nhiên, khi ta gọi toán tử delete p_number3;, thì lúc này vùng nhớ đã cấp phát trên Heap sẽ bị hủy.
Vì vậy, giá trị khi truy xuất p_number4 cũng sẽ bị ảnh hưởng, và mang một giá trị bất kỳ, ảnh hưởng đến các câu lệnh khác trong chương trình.
👉 Giải pháp tránh Dangling Pointer
💬 Khởi tạo Pointer trước khi sử dụng / Check NULL Pointer
Giải pháp này khá đơn giản và luôn luôn phải chú ý thực hiện, đó là khởi tạo giá trị (vùng nhớ) cho pointer trước khi sử dụng. Ngoài ra, khi coding mà cần dereference pointer, các bạn dev nên chú ý check giá trị NULL cho pointer trước.
#include <iostream>
using namespace std;
int main()
{
std::cout << std::endl;
std::cout << "Solution 1 : " << std::endl;
int *p_number5{nullptr};
int *p_number6{new int(87)};
//Check for nullptr before use
if(p_number6 != nullptr)
{
std::cout << "*p_number6 : " << *p_number6 << std::endl;
}
else
{
std::cout << "Invalid address" << std::endl;
}
}
Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Solution 1 :
*p_number6 : 87
💬 Reset Pointer sau khi hủy cấp phát
Đối với trường hợp sử dụng con trỏ đã bị giải phóng - Deleted Pointer, chúng ta có thể xử lý việc sai lệch data bằng cách reset lại pointer (với giá trị là NULL) sau mỗi lần hủy cấp phát (delete). Sau đó mỗi khi sử dụng lại pointer này chúng ta cần check NULL giống như solution 1.
#include <iostream> using namespace std; int main() { //Right after you call delete on a pointer, remember to reset //the pointer to nullptr to make it CLEAR it doesn't point anywere std::cout << std::endl; std::cout << "Solution 2 : " << std::endl; int *p_number7{new int{82}}; //Use the pointer however you want std::cout << "p_number7 - " << p_number7 << " - " << *p_number7 << std::endl; delete p_number7; p_number7 = nullptr; // Reset the pointer //Check for nullptr before use if(p_number7 != nullptr) { std::cout << "*p_number7 : " << *p_number7 << std::endl; } else { std::cout << "Invalid memory access!" << std::endl; } } Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Solution 2 :
p_number7 - 0x5560bc8ce2c0 - 82
Invalid memory access!
💬 Đảm bảo tính chính xác của vùng nhớ trước khi truy xuất
Để tránh trường hợp nhiều con trỏ chứa cùng một địa chỉ, dev cần đảm bảo tính chính xác của vùng nhớ trước khi truy xuất chúng.
#in#include <iostream> using namespace std; int main() { //Solution 3 //For multiple pointers pointing to the same address , Make sure there is //one clear pointer (master pointer) that owns the memory ( responsible for releasing when // necessary) , other pointers should only be able to dereference when the master pointer is valid std::cout << std::endl; std::cout << "Solution 3 : " << std::endl; int * p_number8 {new int{382}}; // Let's say p_number8 is the master pointer int * p_number9 {p_number8}; //Dereference the pointers and use them std::cout << "p_number8 - " << p_number8 << " - " << *p_number8 << std::endl; if(!(p_number8 == nullptr)) { // Only use slave pointers when master pointer is valid std::cout<< "p_number9 - " << p_number9 << " - " << *p_number9 << std::endl; } delete p_number8; // Master releases the memory p_number8 = nullptr; if(!(p_number8 == nullptr)) { // Only use slave pointers when master pointer is valid std::cout<< "p_number9 - " << p_number9 << " - " << *p_number9 << std::endl; } else { std::cerr << "WARNING : Trying to use an invalid pointer" << std::endl; } } Run This Code
➥ Các bạn có thể bấm "Run This Code", kết quả hiển thị trên màn hình console như sau.
Solution 3 :
p_number8 - 0x565092eb12c0 - 382
p_number9 - 0x565092eb12c0 - 382
WARNING : Trying to use an invalid pointer
👉 Kết luận
Trên thực tế, chúng ta có thể sử dụng một số phương pháp khác như Smart Pointer, mình sẽ giới thiệu ở những bài viết sau.
Dangling pointer không chỉ là một vấn đề lập trình phổ biến mà còn có thể dẫn đến những hậu quả nghiêm trọng. Bằng cách sử dụng smart pointers và thực hiện những biện pháp an toàn, chúng ta có thể tránh được lỗi này và xây dựng ứng dụng C++ chất lượng cao hơn. Hãy luôn duy trì sự chú ý và kiểm soát kỹ thuật khi làm việc với con trỏ để đảm bảo rằng chúng ta không rơi vào tình trạng dangling pointer không mong muốn.
>>>= Follow ngay =<<<