Con trỏ void, còn được biết đến là con trỏ chung (generic pointer), là một loại con trỏ đặc biệt mà có thể trỏ vào các đối tượng thuộc bất kỳ kiểu dữ liệu nào! Một con trỏ void được khai báo giống như một con trỏ bình thường, sử dụng từ khóa void làm kiểu dữ liệu của con trỏ:

void *ptr; // ptr is a void pointer

Một con trỏ void có thể trỏ đến các đối tượng của bất kỳ kiểu dữ liệu nào:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

int nValue;
float fValue;
 
struct Something
{
    int n;
    float f;
};
 
Something sValue;
 
void *ptr;
ptr = &nValue; // valid
ptr = &fValue; // valid
ptr = &sValue; // valid

Tuy nhiên, bởi vì con trỏ void không biết được kiểu dữ liệu của đối tượng mà nó đang trỏ tới, nên nó không thể được dereferenced một cách trực tiếp để lấy giá trị như con trỏ bình thường! Thay vào đó, con trỏ void trước tiên phải được ép kiểu một cách rõ ràng sang một kiểu dữ liệu con trỏ khác trước khi nó được dereferenced.

int value{ 5 };
void *voidPtr{ &value };
 
// std::cout << *voidPtr << '\n'; // illegal: cannot dereference a void pointer
 
int *intPtr{ static_cast<int*>(voidPtr) }; // however, if we cast our void pointer to an int pointer...
 
std::cout << *intPtr << '\n'; // then we can dereference it like normal

Kết quả in ra là:

5

Câu hỏi rõ ràng tiếp theo là: Nếu một con trỏ void không biết được nó đang trỏ đến cái gì, làm thế nào để chúng ta biết được phải ép kiểu nó sang kiểu dữ liệu nào? Câu trả lời là, tất cả đều phụ thuộc vào khả năng kiểm soát code của bạn, bạn phải biết được mình cần ép kiểu con trỏ void sang kiểu dữ liệu nào, để code của bạn chạy đúng theo ý muốn của bạn.

Dưới đây là một ví dụ về việc sử dụng con trỏ void:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
enum class Type
{
    INT,
    FLOAT,
    CSTRING
};
 
void printValue(void *ptr, Type type)
{
    switch (type)
    {
        case Type::INT:
            std::cout << *static_cast<int*>(ptr) << '\n'; // cast to int pointer and dereference
            break;
        case Type::FLOAT:
            std::cout << *static_cast<float*>(ptr) << '\n'; // cast to float pointer and dereference
            break;
        case Type::CSTRING:
            std::cout << static_cast<char*>(ptr) << '\n'; // cast to char pointer (no dereference)
            // std::cout knows to treat char* as a C-style string
            // if we were to dereference the result, then we'd just print the single char that ptr is pointing to
            break;
    }
}
 
int main()
{
    int nValue{ 5 };
    float fValue{ 7.5f };
    char szValue[]{ "Mollie" };
 
    printValue(&nValue, Type::INT);
    printValue(&fValue, Type::FLOAT);
    printValue(szValue, Type::CSTRING);
 
    return 0;
}

Kết quả in ra:

5
7.5
Mollie

1. Thêm vài điều về con trỏ void

Có thể gán giá trị null cho con trỏ void:

void *ptr{ nullptr }; // ptr is a void pointer that is currently a null pointer

Mặc dù một số trình biên dịch sẽ cho phép việc xóa đi một con trỏ void mà trỏ đến vùng nhớ được cấp phát động, nhưng chúng ta nên tránh làm như vậy, bởi vì nó có thể dẫn đến lỗi hành vi không xác định (undefined behavior).

Không thể thực hiện các phép số học con trỏ trên một con trỏ void. Điều này là do số học con trỏ yêu cầu con trỏ phải biết được kích thước của đối tượng mà nó đang trỏ đến là bao nhiêu, để nó có thể tăng hoặc giảm con trỏ một cách thích hợp.

Lưu ý rằng không có thứ gì gọi là tham chiếu void (void reference). Điều này là do tham chiếu void sẽ có kiểu void &, và nó sẽ không biết được kiểu dữ liệu của giá trị mà nó đang tham chiếu tới là kiểu dữ liệu gì.

2. Tổng kết

Nói chung, nên tránh sử dụng con trỏ void trừ khi thực sự cần thiết, mặc dù chúng rất đắc lực trong việc cho phép bạn tránh được những kiểm tra về kiểu dữ liệu. Tuy nhiên, điều này lại vô tình cho phép bạn làm những việc vô nghĩa, và trình biên dịch cũng sẽ không đưa ra phàn nàn về những việc vô nghĩa này. Ví dụ, đoạn code sau đây sẽ hợp lệ:

int nValue{ 5 };
printValue(&nValue, Type::CSTRING);

Nhưng ai biết được kết quả thực sự sẽ như thế nào!

Mặc dù hàm trên trông có vẻ là một cách gọn gàng để tạo ra một hàm duy nhất mà xử lý nhiều kiểu dữ liệu, nhưng C++ thực sự có cung cấp một cách tốt hơn để làm điều tương tự (thông qua việc nạp chồng hàm – overloading function) mà vẫn giữ được các phép kiểm tra kiểu dữ liệu để giúp tránh được việc sử dụng sai. Trong nhiều trường hợp sử dụng khác, trước kia, khi con trỏ void từng được dùng để xử lý nhiều kiểu dữ liệu, giờ đây được thực hiện tốt hơn bằng cách sử dụng các templates (khuôn mẫu), chúng cũng cung cấp các phép kiểm tra kiểu dữ liệu mạnh mẽ.

Tuy nhiên, mặc dù rất hiếm, nhưng bạn vẫn có thể tìm thấy một số trường hợp mà việc sử dụng con trỏ void là hợp lý. Nhưng trước tiên, hãy đảm bảo rằng không có cách nào tốt hơn (an toàn hơn) để làm điều tương tự bằng các cơ chế khác của ngôn ngữ! 

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!