优先使用nullptr
- 优先使用 nullptr,而不是 0 和 NULL
- 避免在整数和指针类型上重载
回看旧识
空指针
空指针(null pointer
)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空,以下是几个生成空指针的方法。
int *p1 = nullptr;//等价于int *p1=0;
int *p2 = 0; //直接将p2初始化为字面常量0
//需要首先#include<cstdlib>
int *p3 = NULL;//等价于int *p3 = 0;
void*
void *
也称为泛型指针,是一种特殊的指针类型,可用于存放任意对象的地址即可以指向任何数据类型的对象。void *
是没有关联数据类型的指针。void *
可以保存任何类型的地址,并且可以转换为任何类型。malloc()
和calloc()
返回void *
类型,并且这允许使用这些函数来分配任何数据类型的内存(仅仅因为void*
)。void
指针像普通指针一样声明,使用void
关键字作为指针的类型:void* ptr;
示例
int nValue;
float fValue;
struct Something{
int n;
float f;
};
Something sValue;
void* ptr;
ptr = &nValue; // valid
ptr = &fValue; // valid
ptr = &sValue; // valid
在c++
中必须显式地将malloc
的返回值类型转换为(int *)
。
C
允许将void*
指针赋值给任何指针类型,而不需要强制转换,而在c++
中则不可以。在c++
中,必须显式地对void*
指针进行类型转换
例如,以下内容在C
中是有效的,但在c++
中可以:
void* ptr;
int *i = ptr; // 从void*到int*的隐式转换
类似地,
int *j = malloc(sizeof(int) * 5); // 从void*到int*的隐式转换
为了使上述代码在c++中也能编译,我们必须使用显式类型转换,如下所示,
void* ptr;
int *i = (int *) ptr;
int *j = (int *) malloc(sizeof(int) * 5);
不知void *
所指,如何强转
#include <iostream>
#include <cassert>
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';
break;
case Type::tFloat:
std::cout << *static_cast<float*>(ptr) << '\n';
break;
case Type::tCString:
std::cout << static_cast<char*>(ptr) << '\n';
break;
default:
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指针不知道它所指向的对象类型,所以解除对void指针的引用是非法的。相反,在执行解引用之前,必须首先将void指针转换为另一种指针类型。
下述代码无法通过编译
#include <iostream>
using namespace std;
int main(){
int a = 10;
void* ptr = &a;
cout << *ptr;
return 0;
}
编译报错如下:
source>: In function 'int main()':
<source>:8:13: error: 'void*' is not a pointer-to-object type
8 | cout << *ptr;
| ^~~~
如欲取址修改如下,此项必须确保转换后所得的类型就是指针所指的类型,类型一旦不符,将产生未定义的后果。
#include <iostream>
using namespace std;
int main(){
int a = 10;
void* ptr = &a;
int *ptr1 = static_cast<int*>(ptr);
cout << *ptr1;
return 0;
}
运行结果如下:
10
C
标准不允许使用空指针进行指针算术运算。
不可能对
void
指针进行指针运算。 这是因为指针运算需要指针知道它所指向的对象大小,因此它可以适当地递增或递减指针。
- 在GNU C中,对指向void的指针和指向函数的指针支持加减操作。这是通过将void或函数的大小视为1来实现的。
- 这样做的结果是,在void和函数类型上也允许sizeof,并返回1。
- 选项-Wpointer-arith会在使用这些扩展时发出警告。
void *与空指针(null pointer)区别?
void
指针可以指向任何类型的对象,但不知道它指向的是什么类型的对象。void
指针必须显式转换为另一种类型的指针才能执行间接转换。空指针是指不指向地址的指针。void
指针可以是空指针。因此,void
指针指的是指针的类型,而null pointer
指的是指针的值(地址)。
NULL
C
中说明
宏 NULL
是实现定义的空指针常量,可为
- 值为 0 的整数常量表达式
- 转型为
void*
的值为 0 的整数常量表达式
空指针常量能转换为任何类型;转换结果是该类型的空指针值。
可能的实现
// 兼容 C++ :
#define NULL 0
// 不兼容 C++ :
#define NULL (10*2 - 20)
#define NULL ((void*)0)
c++
中说明
宏 NULL 是实现定义的空指针常量,可为
- 求值为零的整数类型字面常量表达式右值 (
C++11
前) - 零值整数字面量,或为
std::nullptr_t
类型纯右值(C++11
起)
空指针常量可以隐式转换为任何指针类型;这种转换结果是该类型的空指针值。若空指针常量拥有整数类型,它亦可转换为 std::nullptr_t
类型纯右值。
c++
中可能的实现
#define NULL 0
// C++11 起
#define NULL nullptr
C
中,宏 NULL
可以拥有类型 void*
,但这在 C++
中不允许。
nullptr
考虑下面这两个foo
函数:
void foo(char*);
void foo(int);
那么 foo(NULL);
这个语句将会去调用 foo(int)
,从而导致代码违反直觉。为了解决这个问题,C++11
引入了nullptr
关键字,专门用来区分空指针、 0。
关键词
nullptr
代表指针字面量。它是std::nullptr_t
类型的纯右值。存在从nullptr
到任何指针类型及任何成员指针类型的隐式转换。同样的转换对于任何空指针常量也存在,空指针常量包括std::nullptr_t
的值,以及宏NULL
。
#include <cstddef>
#include <iostream>
template<class T>
constexpr T clone(const T& t){
return t;
}
void g(int*){
std::cout << "函数 g 已调用\n";
}
int main(){
g(nullptr); // 良好
g(NULL); // 良好
g(0); // 良好
g(clone(nullptr)); // 良好
// g(clone(NULL)); // 错误:非字面量的零不能为空指针常量
// g(clone(0)); // 错误:非字面量的零不能为空指针常量
}
std::nullptr_t
typedef decltype(nullptr) nullptr_t;
(C++11 起)
std::nullptr_t
是空指针字面量nullptr
的类型。它是既非指针类型亦非指向成员指针类型的独立类型。
#include <cstddef>
#include <iostream>
void f(int*){
std::cout << "Pointer to integer overload\n";
}
void f(double*){
std::cout << "Pointer to double overload\n";
}
void f(std::nullptr_t){
std::cout << "null pointer overload\n";
}
int main(){
int* pi {}; double* pd {};
f(pi);
f(pd);
f(nullptr); // 无 void f(nullptr_t) 可能有歧义
// f(0); // 歧义调用:三个函数全部为候选
// f(NULL); // 若 NULL 是整数空指针常量则为歧义
// (如在大部分实现中的情况)
}
拥抱nullptr
避免重载决议
文中开头第2条,不要在指针和整形之间做重载,对c++那个版本都是适用的准则。这样可以规避重载决议,但nullptr
会是更好的选择。
示例见第一章的nullptr
小节
让模板更清晰
示例如下:
#include <iostream>
int func(void *ptr){
return 0;
}
template<typename FuncType,
typename PtrType>
decltype(auto) Call(FuncType func,PtrType ptr) {
return func(ptr);
}
int main(){
// auto r1 = Call(func,0);// error: invalid conversion from 'int' to 'void*'
// auto r2 = Call(func,NULL);//error: invalid conversion from 'long int' to 'void*'
auto r3 = Call(func,nullptr);
return 0;
}
auto r3 = Call(func,nullptr);
实例后的函数如下
template<>
int Call<int (*)(void *), nullptr_t>(int (*func)(void *), nullptr_t ptr)
{
return func(ptr);
}
参考文献
[1] C 语言中 void* 详解及应用
[2] How does ‘void*’ differ in C and C++?
[3] Should the compiler warn on pointer arithmetic with a void pointer?
[4] 11.14 — Void pointers