void 指针(又称泛型指针)是一种特殊指针,可指向任意数据类型的对象。其声明方式与普通指针相同,只是类型关键字为 void
:
void* ptr {}; // ptr 为 void 指针
void 指针能够指向任何类型的对象:
int nValue {};
float fValue {};
struct Something
{
int n;
float f;
};
Something sValue {};
void* ptr {};
ptr = &nValue; // 合法
ptr = &fValue; // 合法
ptr = &sValue; // 合法
然而,由于 void 指针并不知道所指向对象的具体类型,因此禁止直接解引用。必须先将其显式转换为相应类型的指针,然后才能解引用:
int value { 5 };
void* voidPtr { &value };
// std::cout << *voidPtr << '\n'; // ❌ 非法:void 指针不可解引用
int* intPtr { static_cast<int*>(voidPtr) }; // 先转换为 int*
std::cout << *intPtr << '\n'; // ✅ 合法,输出 5
程序输出:
5
随之而来的问题是:既然 void 指针不知道指向什么类型,我们怎么知道该转换成哪种指针?——最终要靠程序员自行记录。
下面是一个使用 void 指针的完整示例:
#include <cassert>
#include <iostream>
enum class Type
{
tInt, // 注意:此处不能使用 "int",因其为关键字,故用 "tInt"
tFloat,
tCString
};
void printValue(void* ptr, Type type)
{
switch (type)
{
case Type::tInt:
std::cout << *static_cast<int*>(ptr) << '\n'; // 转换为 int* 并解引用
break;
case Type::tFloat:
std::cout << *static_cast<float*>(ptr) << '\n'; // 转换为 float* 并解引用
break;
case Type::tCString:
std::cout << static_cast<char*>(ptr) << '\n'; // 转换为 char*,std::cout 按 C 风格字符串输出
// 若对结果再解引用,则仅打印单个字符
break;
default:
std::cerr << "printValue(): 提供的类型无效\n";
assert(false && "type not found");
break;
}
}
int main()
{
int nValue { 5 };
float fValue { 7.5f };
char szValue[]{ "Mollie" };
printValue(&nValue, Type::tInt);
printValue(&fValue, Type::tFloat);
printValue(szValue, Type::tCString);
return 0;
}
输出:
5
7.5
Mollie
void 指针杂项
- 可赋空指针值:
void* ptr { nullptr };
2. 删除 void 指针会导致未定义行为;若必须删除,应先 `static_cast` 回原类型。
3. 不允许对 void 指针进行指针算术,因为指针算术需知道所指对象大小。
4. 不存在 void 引用(`void&`),因其无法得知所引用值的具体类型。
结论
一般而言,应尽量避免使用 void 指针,除非万不得已。void 指针绕过了类型检查,易导致无意义的操作且编译器不会报错。例如:
```cpp
int nValue { 5 };
printValue(&nValue, Type::tCString); // 合法但结果未知
尽管上述示例看似优雅,C++ 提供了更好的替代方案:
- 函数重载可在保持类型安全的同时处理多种数据类型;
- 模板同样能安全地实现泛型功能。
只有在确认无更安全、更合适的语言机制可用时,才应使用 void 指针。
测验
问题 #1
void 指针与空指针有何区别?
解答
void 指针是一种可指向任意类型的指针类型,其本身可为空或非空;而空指针(nullptr
)是一个特殊的值,表示“不指向任何对象”。二者概念不同:
- void 指针强调“类型未知”;
- 空指针强调“地址为空”。