注意cout陷阱,访问空指针(char*)崩溃

先来看一段代码:

#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;
}
  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值