Bỏ qua để đến nội dung

Con trỏ null (nullptr) và void*

Con trỏ null: “không trỏ đến đâu cả”

Phần tiêu đề “Con trỏ null: “không trỏ đến đâu cả””

Một con trỏ luôn cần trỏ đến một địa chỉ hợp lệ - nhưng đôi khi bạn cần một con trỏ “chưa trỏ đến gì cả”, hoặc muốn thể hiện rằng nó không còn hợp lệ nữa. Đó là lúc dùng nullptr (C++11 trở lên):

#include <iostream>
int main() {
int* ptr = nullptr; // ptr không trỏ đến bất kỳ đâu
if (ptr == nullptr) {
std::cout << "Con tro chua tro toi dau ca" << std::endl;
}
return 0;
}

Một con trỏ chưa được khởi tạo sẽ chứa một địa chỉ ngẫu nhiên trong bộ nhớ (giá trị rác), rất nguy hiểm nếu bạn vô tình dereference nó:

int* dangerous_ptr; // KHÔNG khởi tạo - chứa địa chỉ rác
// std::cout << *dangerous_ptr; // CỰC KỲ NGUY HIỂM - hành vi không xác định (undefined behavior)
int* safe_ptr = nullptr; // Khởi tạo rõ ràng là "chưa trỏ đến đâu"
if (safe_ptr != nullptr) {
std::cout << *safe_ptr; // An toàn - có kiểm tra trước khi dùng
}

Luôn khởi tạo con trỏ khi khai báo - dùng nullptr nếu chưa có địa chỉ hợp lệ để gán.

Cố gắng truy cập giá trị thông qua một con trỏ null sẽ khiến chương trình crash ngay lập tức (segmentation fault):

int* ptr = nullptr;
// std::cout << *ptr; // CRASH! Không có gì ở địa chỉ 0 (null) để đọc

Vì vậy, một thói quen quan trọng khi làm việc với con trỏ là luôn kiểm tra null trước khi dereference:

void print_value(int* ptr) {
if (ptr != nullptr) {
std::cout << *ptr << std::endl;
} else {
std::cout << "Con tro rong, khong co gi de in" << std::endl;
}
}

Trước C++11, người ta thường dùng NULL (một macro, thực chất là 0) để biểu diễn con trỏ null:

int* ptr1 = NULL; // Cách cũ, kế thừa từ C
int* ptr2 = 0; // Cũng hoạt động nhưng dễ gây nhầm lẫn với số 0
int* ptr3 = nullptr; // Cách hiện đại, RÕ RÀNG là con trỏ null - nên dùng cách này

nullptr được ưu tiên vì nó có kiểu riêng biệt, giúp trình biên dịch phân biệt rõ ràng giữa “con trỏ null” và “số nguyên 0”, tránh một số lỗi nạp chồng hàm khó phát hiện.

void*: con trỏ không xác định kiểu dữ liệu

Phần tiêu đề “void*: con trỏ không xác định kiểu dữ liệu”

void* là một con trỏ đặc biệt có thể trỏ đến bất kỳ kiểu dữ liệu nào, nhưng đổi lại không thể dereference trực tiếp (vì trình biên dịch không biết dữ liệu tại đó có kích thước bao nhiêu byte):

#include <iostream>
int main() {
int x = 10;
double y = 3.14;
void* generic_ptr;
generic_ptr = &x;
std::cout << *(static_cast<int*>(generic_ptr)) << std::endl; // 10 - phải ép kiểu lại trước khi dùng
generic_ptr = &y;
std::cout << *(static_cast<double*>(generic_ptr)) << std::endl; // 3.14
return 0;
}

void* thường gặp trong các API cấp thấp (ví dụ: một số hàm thư viện C như malloc), nhưng trong C++ hiện đại, template thường là lựa chọn tốt hơn để viết code dùng chung cho nhiều kiểu dữ liệu.

  • nullptr biểu diễn một con trỏ “không trỏ đến đâu cả” - luôn dùng nó thay vì để con trỏ chưa khởi tạo
  • Dereference một con trỏ null sẽ khiến chương trình crash - luôn kiểm tra != nullptr trước khi dùng
  • nullptr (C++11) là cách viết hiện đại, an toàn hơn NULL hoặc 0
  • void* là con trỏ không xác định kiểu, cần ép kiểu lại (static_cast) trước khi dereference