'\0' 和 NULL 可以互换使用吗?

57

在指针的上下文中,NULL经常被使用,并在多个标准库(如)中通过宏定义为整数0'\0'是空字符,由8个零位组成。顺便说一句,8个零位等同于整数0

在某些情况下,虽然这被认为是可怕的风格,但这两者可以互换使用:

int *p='\0';
if (p==NULL) //evaluates to true
    cout << "equal\n";

或者

char a=NULL;
char b='\0';
if (a==b) //evaluates to true
    cout << "equal again\n";

在SO上已经有很多类似的问题了,例如这个问题(NULL、'\0'和0之间的区别是什么)的最佳回答说“它们并不是完全相同的东西。”

请问有人能提供一个示例,说明NULL\0不能互换吗?最好是一个实际应用而不是一种病态情况。


22
-Wzero-as-null-pointer-constant翻译成中文为“将零视为空指针常量”。 - Biffen
3
Biffen, mjs,我的问题是真实的 - 我想要理解这两者之间的区别。您是在暗示这两者是等价的,并且只是风格上的区别(我假设这就是gcc标志的目的)吗? - cwest
17
请使用 C++11 和 nullptr,而不是 NULL - Basile Starynkevitch
11
请参见 利用 PVS-Studio 发现的 GCC 中的错误:静态分析器发现可能存在的一个 bug,原因是将 char*\0 进行了比较,这很可能是作者在进行比较之前忘记对指针进行解引用(以获取字符)的结果。正确的类型设置可以使工具更加强大。 - Matthieu M.
2
@cwest 顺便说一下,欢迎来到StackOverflow,这是一个很好的、写得不错的第一个问题。 - mjs
显示剩余8条评论
7个回答

54

有人能提供一个例子,说明NULL和\0不能互换吗?

NULL'\0'之间的区别可能会影响重载决议。

例如 (在Coliru上查看):

#include <iostream>

// The overloaded function under question can be a constructor or 
// an overloaded operator, which would make this example less silly
void foo(char)   { std::cout << "foo(char)"  << std::endl; }
void foo(int)    { std::cout << "foo(int)"   << std::endl; }
void foo(long)   { std::cout << "foo(long)"  << std::endl; }
void foo(void*)  { std::cout << "foo(void*)" << std::endl; }

int main()
{
    foo('\0'); // this will definitely call foo(char)
    foo(NULL); // this, most probably, will not call foo(char)
}

请注意,在Coliru使用的gcc编译器将NULL定义为0L,这意味着对于这个例子,foo(NULL)解析为foo(long)而不是foo(void*)这篇答案详细讨论了这一方面。


7
为了完整起见,也可以调用foo(nullptr)。 - Emilio Garavaglia
4
我认为这个例子很糟糕,因为在当前的实现中,foo(NULL) 更可能调用 foo(int) 而不是 foo(void*)。是的,这仍然不是 foo(char),但程序员想要调用的更可能是 foo(void*) - user743382
8
在C++中,foo(NULL)将始终调用foo(int)foo(long),因为C++标准禁止将NULL定义为(void*)0。有关详细信息,请参见我的答案:https://dev59.com/CFkS5IYBdhLWcg3wFi4M#40396865 - VLL
7
这是一个很好的例子,清楚地表明了在哪些情况下NULL不能与'\0'互换。更让人高兴的是,它还揭示了NULLnullptr之间存在相同的问题。虽然问题没有被描述,但我认为只要答案不建议使用NULL,这就足够了。特别是因为这个问题与所问的问题有些偏题。 - eerorika
1
这只是一个可以接受的例子,而不是一个出色的例子。阅读完这个答案后,我心中仍然有疑虑。确实,它回答了问题,但还有改进的空间。 - anatolyg
显示剩余2条评论

37

C++中宏NULL的定义

根据Leon的回答,在函数有多个重载形式时,\0 会优先选择参数类型为 char 的重载形式。然而需要注意的是,在典型的编译器上,NULL 实际上会优先选择参数类型为 int 而不是 void* 的重载形式!

造成这种混淆的原因可能是 C 语言允许将 NULL 定义为 (void*)0。但是,C++ 标准明确规定了:(draft N3936, page 444):

NULL 的可能定义包括00L,但不包括(void*)0

这种限制是必要的,因为例如 char *p = (void*)0 是合法的 C 代码,但是不合法的 C++ 代码;而 char *p = 0 则在两者中都是合法的。

在 C++11 及以上版本中,如果需要一个行为类似指针的空常量,应使用 nullptr

Leon的建议在实践中如何工作

这段代码定义了单个函数的多个重载形式。每个重载形式输出参数类型:

#include <iostream>

void f(int) {
    std::cout << "int" << std::endl;
}

void f(long) {
    std::cout << "long" << std::endl;
}

void f(char) {
    std::cout << "char" << std::endl;
}

void f(void*) {
    std::cout << "void*" << std::endl;
}

int main() {
    f(0);
    f(NULL);
    f('\0');
    f(nullptr);
}

在 Ideone 上运行此代码输出

int
int
char
void*
因此,我认为过载的问题不是实际应用中的问题,而是一种病态情况。在C++11中,NULL常量无论如何都会出现错误,应该替换为nullptr

如果NULL不是零怎么办?

另一个病态案例由Andrew Keeton在另一个问题中提出:

请注意,在C语言中什么是空指针。这并不取决于底层架构。如果底层架构将空指针值定义为地址0xDEADBEEF,则由编译器来解决这个问题。

因此,在这种有趣的架构上,以下方法仍然是检查空指针的有效方法:

if (!pointer)
if (pointer == NULL)
if (pointer == 0)
下面是无效的检查空指针的方法:
#define MYNULL (void *) 0xDEADBEEF
if (pointer == MYNULL)
if (pointer == 0xDEADBEEF)
总的来说,我认为这些差异大多是风格上的。如果您有一个接受int参数的函数和一个重载了该函数并接受char参数的函数,并且它们的功能不同,当您使用\0NULL常量调用它们时,您将会注意到它们之间的区别。但是,一旦将这些常量放入变量中,差异就消失了,因为所调用的函数是从变量的类型推导出来的。 使用正确的常量使代码更易于维护,并传达含义更清晰。当您需要表示数字时,请使用0,当您需要表示字符时,请使用\0,当您需要表示指针时,请使用nullptrMatthieu M.在评论中指出,GCC存在一个错误,其中将char*\0进行比较,而本意是对指针进行解引用并将char\0进行比较。如果代码库中使用了适当的风格,则此类错误更容易被检测到。 回答您的问题,实际上没有真正的用例会阻止您可以互换使用\0NULL。只是出于风格和一些边缘情况的原因。

1
我的 NULL (g++ 6.2.0) 是 long int,所以这里的代码不会编译,除非我像Leon一样添加一个 long 重载(否则它在 intchar 之间是有歧义的)。 - Toby Speight
4
为什么要特别要求 NULL 不是 (void*) - Kyle Strand
5
@KyleStrand char *p = (void *)0; 是合法的 C 语言,但是不合法的 C++ 语言。char *p = 0; 也是合法的 C++ 语言。 - chi

8
请不要这样做。它是一种反模式,并且实际上是错误的。NULL用于空指针,'\0'是空字符。它们在逻辑上是不同的东西。
我从未见过这种情况:
int* pVal='\0';

但这是相当普遍的:

char a=NULL;

但这不是好的编程风格。它会使代码的可移植性变差,而且我认为也更难以阅读。此外,在混合使用 C/C++ 的环境中,它也容易引起问题。
它依赖于关于任何特定实现如何定义 NULL 的假设。例如,一些实现使用一个简单的
#define NULL 0

其他人可能会使用:

#define NULL ((void*) 0)

我看到有人将其定义为整数,以及各种奇怪的处理方法。

在我的看法中,NULL 应只用于表示无效地址。如果你需要一个空字符,请使用 '\0'。或者把它定义为 NULLCHR。但这不是很规范。

这样可以使你的代码更具可移植性——如果你更改编译器、环境或编译器设置,不会出现有关类型等方面的警告。在 C 或混合 C/C++ 环境中,这可能更为重要。

以下是可能出现的警告示例:

#define NULL 0
char str[8];
str[0]=NULL;

这相当于:
#define NULL 0
char str[8];
str[0]=0;

我们正在将整数值分配给字符。这可能会导致编译器警告,如果出现足够多的情况,很快就看不到任何重要的警告了。对我来说,这才是真正的问题。代码中有警告会产生两个副作用:
  1. 出现足够多的警告后,你就不会发现新的警告。
  2. 它传达了“警告是可以接受的”信号。
在这两种情况下,实际的漏洞可能会被忽略,如果我们读取警告(或启用-Werror),则编译器会捕获这些漏洞。

5
#define NULL (void*) 0 只在 C 语言中(如果添加括号)是有效的,而在 C++ 中不适用。在 C++03 中,char c = NULL; 是可移植的,但不好的风格。C++11 允许使用 #define NULL nullptr,并且 char c = NULL; 可能会失败。 - user743382
我没有意识到C++11的限制。这使得不做它的情况更加强烈。稍后将更新答案。 - mjs
1
自标准化以前,这已经不是限制了。从来没有允许将void*隐式转换为T*(对于T != void),因此将NULL定义为((void*)0)将使其在大多数预期的情况下变得无用。 - CB Bailey
1
@CharlesBailey 公平地说,C和C++都有一种特殊的转换方式,可以将空指针常量转换为任何指针类型,即使这些类型不允许从同一类型的其他值进行隐式转换。在C中,void (*fp)(void) = (void *) 0;是有效的,即使void *p = 0; void (*fp)(void) = p;是无效的。C++也可以有类似的特殊例外,其中int *ip = (void *) 0;将是有效的,即使void *p = 0; int *ip = p;是无效的,就像int *ip = 0;是有效的,即使int i = 0; int *ip = i;是无效的。 - user743382

8
是的,它们在解决重载函数时可能会表现出不同的行为。 func('\0') 调用 func(char)
func(NULL) 调用 func(integer_type)
您可以使用nullptr来消除混淆,它始终是指针类型,在赋值/比较值或函数重载解析时不显示任何歧义。
char a = nullptr; //error : cannot convert 'std::nullptr_t' to 'char' in initialization
int x = nullptr;  //error : nullptr is a pointer not an integer

请注意,它仍然与NULL兼容:
int *p=nullptr;
if (p==NULL) //evaluates to true

C++编程第四版Stroustrup书摘:

在旧代码中,通常使用0或NULL代替nullptr(§7.2.2)。然而,使用nullptr可以消除整数(如0或NULL)和指针(如nullptr)之间的潜在混淆。



7
计算机程序有两种读者。
第一种是计算机程序,比如编译器。
第二种是人类,比如你和你的同事。
程序通常可以接受用一种零替换另一种零。
当然也有例外,但这不是重点。
重要的是你正在影响人类读者。
人类读者非常注重上下文。 如果使用错误的零,你就在向人类读者<强行>传递错误信息。他们会因此而愤怒。
被欺骗的人更容易忽略程序中的错误。被欺骗的人会看到“虚假”的“错误”。修复这些“虚假”的错误时,会引入真正的错误。
不要欺骗人类读者。 你正在欺骗的其中一个人就是你未来的自己。 你如果这样做了,将来也会因此而愤怒。

4

C++14草案N3936摘录:

18.2 类型[support.types]

3 在这个国际标准中,宏NULL是一个实现定义的C++空指针常量(4.10)。

4.10 指针转换[conv.ptr]

1 空指针常量是一个整数字面值(2.14.2),其值为零或类型为std::nullptr_t的prvalue。
空指针常量可以转换为指针类型;结果是该类型的空指针值,并且可与对象指针或函数指针类型的任何其他值区分开来。

因此,NULL可以是任何值为零的整数字面值,或者是nullptr这种类型的值,而'\0'始终是零窄字符字面值。

因此,在一般情况下不能互换,尽管在指针上下文中,您看不到除了样式之外的任何差异。

例如:

#include <iostream>
#include <typeinfo>

int main() {
    std::cout << typeid('\0').name << '\n'
        << typeid(NULL).name << '\n'
        << typeid(nullptr).name << '\n';
}

1

让我们来定义C/C++中的NULL。

根据C/C++参考资料,NULL被定义为一个扩展到空指针常量的宏。接下来我们可以看到,空指针常量可以转换为任何指针类型(或指向成员的指针类型),这样就获得了一个空指针值。这是一个特殊的值,表示指针没有指向任何对象。

参考C的定义:

空指针常量是一个整数常量表达式,其计算结果为零(如0或0L),或将这样的值强制转换为void*类型(如(void *)0)。

参考C++98的定义:

空指针常量是一个整数常量表达式,其计算结果为零(如0或0L)。

参考C++11的定义:

空指针常量是一个整数常量表达式,其计算结果为零(如0或0L),或者是nullptr_t类型的值(如nullptr)。

方法重载示例。

假设我们有以下方法:

class Test {
public:
    method1(char arg0);
    method1(int arg0);
    method1(void* arg0);
    method1(bool arg0);
}

使用参数NULLnullptr调用method1应该调用method1(void* arg0);。但是,如果我们使用参数'\0'0调用method1,则应执行method1(char arg0);method1(int arg0);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接