用了这么久,你真的懂nullptr吗?
你是否在混着用NULL
和nullptr
?
你是否分不清这二者到底有什么区别?
滥用NULL
到底会带来什么不可思议的错误?
今天,我们就来讨论讨论nullptr
这个空指针的救世主。在C++11之前,几乎所有人写代码时对NULL
又爱又恨,爱是因为他确实能够起到空指针的作用,恨是刚躲过了这个坑,又遇见了那个妖,魑魅魍魉怎么它就这么多。还好,C++11给我们带来了希望的曙光——nulltpr
。
什么是NULL
?
这个问题想要回答好还真是不容易。从定义上来看,NULL
是一个宏,它被定义为0
(也就是int
型的零),或者0L
(long
型的零),或者其他__零指针常数__;但是一般来说我们还是把它当作整形来看待。零指针常数之所以得名,是因为他们能够被转化为空指针。在C中,NULL
也可以被理解为(void*)0
,因为void
类型的指针都可以被转化为其他任何指针类型。详情请看文章。。。
虽然NULL
很明显是为了指针而被制造出来的,但是如果你把它当作参数传到函数中去,它就会“原形毕露”——编译器会把它当作一个int
类型,或一个long
类型,而不是一个指针类型。比如说下边的例子:
class Girlfriend {/* ... */};
void kissGirlfriend(Girlfriend* gf);
void kissGirlfriend(int gfId);
int main() {
// 参数匹配后,其实调用的是第二个函数,你的第零号女朋友莫名其妙被亲了一下
kissGirlfriend(NULL);
}
从上边代码来看,我们很明显是想传一个空指针进去,让程序调用第一个重载函数,但很可惜的是,这种情况下我们唯一能确定的事情就是我们的预想肯定不会发生——可能有两个结果。
- 如果
NULL
被定义为0
(int
零),那么编译器会自然而然调用第二个重载函数,毕竟这是一个完美参数匹配。 - 如果
NULL
被定义为0L
或者其他形式的整型零,编译器会报错,告诉你有一个指向不明的函数调用,——因为0L
可以被等同地转化为空指针或int
,由此就产生了二义性。
让我们看看这个问题怎么解决。第一个尝试:我们可以试着让一个enum
来代替那个函数的int
,然后去消除二义性。同时让我们给参数一个名字,让代码看起来指向性也更明确一些:
class Girlfriend {/* ... */};
enum GirlfriendId {/* ... */};
void kissGirlfriend(Girlfriend* gf);
void kissGirlfriend(GirlfriendId gfId);
int main() {
auto noGf = NULL;
kissGirlfriend(noGF); //还是ERROR
}
在这里,noGf
不是指针,是一个正儿八经的整形变量,另一方面,从整型零到空指针的转化只能发生在零指针常数上。所以编译器很无奈的告诉我们,他不知道该把noGf
转化为一个GirlfriendId
还是Girlfriend*
。
NULL
的弊端
上边两个例子都向我们展示了问题所在:NULL
终究只是一个宏。它是一个整型,它不是指针,所以一旦涉及类型转换就会有风险。因而随之带来的问题是,我们没有办法在不显示声明指针类型的情况下定义一个空指针。
nullptr
自从C++11以来,有一个小特性完美的解决了这个问题——nullptr
。作为一个字面常量和一个零指针常数,它可以被隐式转换为任何指针类型。我们只需稍作修改上边的例子,你就会得到:
void kissGirlfriend(GirlFriend* gf);
void kissGirlfriend(int gfId);
int main() {
kissGirlfriend(nullptr); //调用第一个重载函数
}
这时编译器就会乖乖听话了:因为nullptr
是没办法转换成int
的,它会隐式转化为一个空的Girlfriend*
从而正确的函数会被调用。下边这个例子对nullptr
的类型进行说明:
class Girlfriend {/* ... */};
enum GirlfriendId {/* ... */};
void kissGirlfriend(Girlfriend* gf);
void kissGirlfriend(GirlfriendId gfId);
int main() {
auto noGf = nullptr;
kissGirlfriend(noGF); //OK
}
nullptr
有它自己的类型,std::nullptr_t
,他可以隐式转换为任何指针类型。所以上例中noGf
现在有了nullptr_t
类型并且可以转换为Girlfriend*
,而不是GirlfriendId
。所以,无论在哪里,都请使用nullptr
,而不是NULL
、0
或者其他零指针常数。
nullptr
和智能指针
智能指针并不是真正的指针,他们是类。所以上边所有的隐式转化对诸如shared_ptr<T>
等智能指针都不会奏效。但很幸运的是,对于nullptr
自己的类型,智能指针类都重载了针对这种类型的构造器,所以下边的代码其实是合法的:
shared_ptr<Girlfriend> gfPtr = nullptr;
unique_ptr<Boyfriend> bfPtr = nullptr;
需要注意的是,除了从auto_ptr
到unique_ptr
的转换,这是唯一一种智能指针的隐式构造函数。因此,当一个函数的参数类型是智能指针类型时,理论上你也可以直接传一个nullptr
进去,而不必画蛇添足地再额外用nullptr
创建一个智能指针出来:
void doSomething(unique_ptr<Object> objectPtr);
int main() {
doSomething(nullptr); //OK
}
向后兼容
更妙的是,C++11标准规定了任何零指针常量都可以隐式转化为nullptr
,这让我们能够有效地兼容使用NULL
的老代码。也就是说,如果你将nullptr
和nullptr_t
引入老代码,代码将会很好地兼容以前的逻辑。尤其是当我们想向03版及之前的代码中引入智能指针时,我们更能体会到这种兼容特性的妙处:
//C++03 version:
void doSomething(Object* objectPtr) {
//...
delete objectPtr;
}
int main() {
doSomething(NULL);
}
我们通过把原始的指针用unique_ptr
替换来替换时,如果某处忘记了把NULL
替换成nullptr
怎么办?别担心,这样不会引起错误:
//引入 unique_ptr
void doSomething(unique_ptr<Object> objectPtr) {
//...
}
int main() {
doSomething(NULL); // OK
}
原因就在于,NULL
是一个零指针常数,可以隐式转换为nullptr
,而nullptr
又可以调用unique_ptr<T>
的隐式构造器,通过两次隐式转换,我们就可以完美完成上边的调用。
结语
nullptr
是一个有用的小特性,它能让你的代码更安全,也更readable。还有什么理由不用它呢?