先来看一段代码:
#include <iostream>
int main() {
const char* np = nullptr;
printf("printf np:%s\n", np);
std::cout << "np: " << np << std::endl;
}
在代码中,我们分别使用printf()函数和cout函数输出了空指针。最后运行的结果是 printf输出(null)
,cout崩溃。
cout崩溃的原因:
C++中的std::cout可以自动推断变量类型,从而把变量的值输出到标准输出上。而空指针是指向内存保留区的不可访问内存的指针,如果我们使用cout输出了空指针就会发生错误。
而printf函数在使用中直接访问空指针也会出现错误,但在一些条件下,printf对访问空指针的情况做出了处理,使得我们在访问空指针的时候不会出错,并且输出(null)
的提示。参考:空指针可以输出"(null)"???
处理函数形参为空指针的情况
一般情况下,函数形参中有指针,我们会需要判断传入的指针非空,即if(nullptr != p)
。而我们也可以仿照printf函数的思想,我们可以编写出类似的重载函数,处理函数传入空指针的情况:
void test_ptr(double* d)
{
std::cout << *d << std::endl;
}
// 如果是nullptr指针,打印输出提示
void test(std::nullptr_t)
{
std::cout << "nullptr" << std::endl;
}
void test(double* d)
{
test_ptr(d);
}
int main()
{
test(nullptr);
}
参考自:c++ passing nullptr through multiple functions
注:在《c++ passing nullptr through multiple functions》原文中,重载了void test_ptr(std::nullptr_t)
函数,是无法达到我们想要的结果的。
错误示范:
void test_ptr(std::nullptr_t)
{
std::cout << "nullptr" << std::endl;
}
void test_ptr(double *d)
{
std::cout << *d << std::endl;
}
void test(double *d)
{
test_ptr(d);
}
int main()
{
test(nullptr);
}
因为在main函数内调用test函数,通过 test(double*) 进行实参传递形参后,参数类型变成了 double*类型。虽然形参的值是nullptr
,但程序在编译阶段已经确定了函数的调用,而编译阶段对栈变量的值又是未知的,因此在test 函数内部调用的是 test_ptr(double* d)
版本的函数。test_ptr(std::nullptr_t)
版本函数却没有被调用。
方案一:
因此,我们可以把test 定义为一个模板函数,使得test充当“中间件”,转发传入的参数类型。这样在test内部调用 test_ptr 时,参数是 nullptr 就可以直接匹配到第一个空指针处理函数了。
void test_ptr(std::nullptr_t)
{
std::cout << "nullptr" << std::endl;
}
void test_ptr(double* d)
{
std::cout << *d << std::endl;
}
template<typename T>
void test(T d)
{
test_ptr(d);
}
int main()
{
test(nullptr);
}
方案二:
在编译时期进行检查可以使用,C++11中提供的静态断言 static_assert
。而在或者C++14的std::is_null_pointer
可以判断指针是否为空指针。有关is_null_pointer的资料请参考:std::is_null_pointer
可以看到使用静态断言的方法,我们可以在程序运行之前就可以找到程序中的错误,避免了空指针引发的程序崩溃。
为什么有的nullptr可以使用cout输出,比如int*类型
我们可以看到,在输出 int* 类型的空指针时,可以正常输出,并且输出了00000000
。比如下面这段代码:
int main()
{
int* p = nullptr;
cout << p << endl;
return 0;
}
我们再来看一组代码,在以下代码中我们测试了float、double、long等类型,在指针为空时都输出了 00000 ,而当指针类型为 char* 或者为 string* 时程序就会崩溃。
int main()
{
int* pi = nullptr;
std::cout << pi << endl;
float* pf = nullptr;
std::cout << pf << endl;
double* pd = nullptr;
std::cout << pd << endl;
long* pl = nullptr;
std::cout << pl << endl;
char* pc = nullptr;
std::cout << pc << endl;
return 0;
}
要想明白其中的原理,我们得先从cout与char*讲起。
- cout函数可以自动根据变量的类型输出变量的值
- char* 类型存储字符串时,通过尾部的结束字符’\0’判断字符串是否结束。
对于指针类型变量p,它可以存放同类型变量的地址,而输出的时候可以输出指针中存放的地址值,也可以顺着地址访问指向的空间。
如图所示,int* 指针pI存放arr数组的首地址。char* pC存放str的首地址。
在输出pI时,我们可以选择输出pI中存放的 0x100 这个地址值。( printf("%p \n", pI);
),也可以选择输出pI指针中存放的arr中的值 (printf("%d \n", *pI);
)。
同理,在输出pC时,我们可以选择输出pI中存放的 0x200 这个地址值。( printf("%p \n", pI);
),也可以选择输出pI指针中存放的str中的值 (printf("%c \n", *pC);
或 printf("%s \n", pC);
)。
可以看到,在输出字符串str时,我们可以使用printf("%c \n", *pC);
输出单个字符,或者使用 printf("%s \n", pC);
输出整个字符串。而在输出int类型数组,double类型数组时就只能一个一个的输出数组内的元素,这得益于char类型字符数组有一个’\0’的终止字符。
cout函数输出char*类型
对于cout函数,它可以自动推断参数类型并输出其中的内容,这的确比我们使用printf时手动指定参数类型方便了许多,但有时候cout这种自作聪明的方法不见得就是我们所需要的。
比如当我们想要输出char* 指针p中存放的地址时(即str的地址),cout却自作主张的访问了指针所指向的空间(str中的值),输出了指向空间的值。而我们反而需要把p主动声明为void* 类型才能正确的输出p中的地址。
char str[] = { 'a','b','c','d','e','\0' };
char* ps = str;
std::cout << ps << std::endl; // 输出 abcde
std::cout << (void*)ps << std::endl; // 输出str的地址
而我们在使用int、float等其他类型时,却没有这样的困扰。
int arr[] = { 1,2,3,4,5 };
int* pi = arr;
std::cout << pi << std::endl; // 输出 arr的地址
std::cout << (void*)pi << std::endl; // 输出 arr的地址
或许 这就是为什么cout在输出空指针时程序会崩溃的原因。
空指针即指向 0x00000000 地址的指针,而在内存中 0x00000000 地址段是内存保留区,是不可访问的区域。
而当int、float等类型指针为空指针时,使用 cout << pI << endl;
是不会访问 0x00000000 这片内存的,而是直接输出了这个地址值,正如上面我们做的实验一样。
而当我们使用 char 类型的指针时,cout默认的是访问该指针指向的内存的值,也就是当 pC = nullptr时,cout << pC << endl;
访问了内存中的 0x00000000 区域,因此造成了程序的崩溃。
以下是有关cout输出int* 与 char* 指针的实例
int main(void) {
char str[] = { 'a','b','c','d','e','\0' };
std::cout << str << std::endl; // 正常输出 abcde
char* ps = str;
std::cout << *ps << std::endl; // 输出单个字符 a
std::cout << ps << std::endl; // 输出 abcde
std::cout << (void*)ps << std::endl; // 输出str的地址
char* pc = &str[0];
std::cout << *pc << std::endl; // 输出单个字符 a
std::cout << pc << std::endl; // 输出 abcde // 访问指针指向的空间
std::cout << (void*)pc << std::endl; // 输出str的地址 // 指针存放的地址值
int arr[] = { 1,2,3,4,5 };
int* pi = &arr[0];
std::cout << *pi << std::endl; // 输出 arr[0]
std::cout << pi << std::endl; // 输出 arr[0]的地址 // 只是输出指针的值
std::cout << (void*)pi << std::endl; // 输出 arr[0]的地址
return 0;
}