为什么C++允许我将const char赋值给const char *?

71

令我惊讶的是,这段代码可以编译通过:

const char* c_str()
{
    static const char nullchar = '\0';
    return nullchar;
}

出现了一个bug,我很庆幸及时发现了它。

这是C++的意图还是编译器的bug?为什么会故意忽略数据类型?
在Visual C++ 2010和GCC中可以工作,但考虑到明显的数据类型不匹配,我不明白为什么它能够工作。(static也不是必需的。)


@Mehrdad 或许是指针是64位的原因?我对标准不是很熟悉,但我相信很快会有了解它的人来给我们答案。 - ta.speot.is
1
@ta.speot.is:我认为这与CPU架构无关... - user541686
3
当然,C++98确实有编译时常量表达式的概念。 - Managu
1
@Mehrdad,constexpr本来就是C++11特有的...但是根据C++03标准的§5.19.1,const变量是一个常量表达式...一个整数常量表达式只能包含字面值(2.13),枚举器,用常量表达式初始化的整数或枚举类型的const变量或静态数据成员(8.5),整数或枚举类型的非类型模板参数以及sizeof表达式。 - obataku
2
@ta.speot.is "也许指针是64位的?" 大小不是问题。 - curiousguy
显示剩余9条评论
8个回答

69

根据您的定义,nullchar是一个整数常量表达式,其值为0。

C++03标准将空指针常量定义为:“空指针常量是一个整数类型的积分常数表达式(5.19),其求值结果为零。” 简而言之,您的nullchar是空指针常量,这意味着它可以被隐式转换并赋值给任何指针。

注意,所有这些元素都需要才能使该隐式转换起作用。例如,如果您使用的是'\1'而不是'\0',或者如果您没有为nullchar指定const限定符,则不会获得隐式转换,您的赋值将失败。

包含此转换是有意的,但广泛认为是不可取的。 0作为空指针常量是从C继承而来的。我相当确定Bjarne以及大多数C++标准委员会成员(和C++社区的大多数人)都非常希望删除这种特定的隐式转换,但这样做将破坏与大量C代码(可能接近全部)的兼容性。


4
值得一提的是,在C语言中,值为0的const变量并不符合空指针常量的要求,这意味着原作者的代码在C语言中无效(因此无论怎样更改都无法修复,因为从C语言的角度来看,它已经是有问题的了)。 - AnT stands with Russia
2
所以可能是这样的:为什么允许将整数常量表达式隐式转换为空指针(而不仅仅是允许字面量)?(请注意,这是一个假设性问题)。 - Managu
2
@Mehrdad:在C或C++中都没有这样的转换。在C++中,它通过隐式转换为bool来工作。在C中,它通过将指针隐式比较为字面量0来工作,即if (p)等同于if (p != 0)。后者也不使用转换为int - AnT stands with Russia
1
@Managu:在C语言中,生成命名常量的唯一方法是:1)宏(即#define),2)枚举。宏存在众所周知的严重问题。枚举除了int类型之外不能有任何其他类型。这个问题必须得到解决。为了解决这个问题,C++将常量的概念扩展到包括const对象。const对象具有类型和作用域。 - AnT stands with Russia
1
@Mehrdad:如果你想的话,你可以用if (p != 0)if ((p != 0) != 0)if (((p != 0) != 0) != 0)等方式替换掉if (p)。这些变体都是等效的。但编译器不一定会遵循这条路径。 - AnT stands with Russia
显示剩余19条评论

28

这是一个古老的历史:它可以追溯到C语言。

C语言中没有 null 这个关键字。在C语言中,空指针常量可能是以下两种情况之一:

  • 带有值0的整型常量表达式,例如 0, 0L, '\0'(请记住 char 是一种整型类型),(2-4/2)
  • 将此类表达式强制转换为 void*,例如 (void*)0, (void*)0L, (void*)'\0', (void*)(2-4/2)

NULL 宏定义(不是关键字!)会扩展成这样的空指针常量。

在最初的C++设计中,只允许使用整型常量表达式作为空指针常量。近年来,C++ 中添加了 std::nullptr_t

在C++中,但不是在C语言中,整型类型的 const 变量如果使用整型常量表达式进行初始化,则是一个整型常量表达式:

const int c = 3;
int i;

switch(i) {
case c: // valid C++
// but invalid C!
}

因此,使用表达式'\0'初始化的 const char 是一个空指针常量:

int zero() { return 0; }

void foo() {
    const char k0 = '\0',
               k1 = 1,
               c = zero();
    int *pi;

    pi = k0; // OK (constant expression, value 0)
    pi = k1; // error (value 1)
    pi = c; // error (not a constant expression)
}

你认为这不是良好的语言设计吗?


更新以包含C99标准的相关部分... 根据§6.6.6...

一个整数常量表达式应该具有整数类型,并且只能具有操作对象,这些操作对象是整数常量、枚举常量、字符常量、其结果为整数常量的sizeof表达式和作为强制转换操作数的浮点常量,这些强制转换运算符在整数常量表达式中只能将算术类型转换为整数类型,除非它们是sizeof运算符的操作数的一部分。

一些C++专属程序员的澄清:

  • C使用术语“常量”来表示C++程序员所知道的“字面值”。
  • 在C ++中,sizeof始终是编译时常量;但是C有可变长度数组,因此sizeof有时不是编译时常量。

然后,我们看到§6.3.2.3.3说明...

值为0的整数常量表达式或将此类表达式强制转换为类型void *的表达式称为空指针常量。 如果将null指针常量转换为指针类型,则所得到的指针,称为空指针,保证不等于任何对象或函数的指针。


为了看到这个功能有多古老,请参见C99标准中相同的镜像部分...

§6.6.6

一个整数常量表达式应该具有整数类型,并且只能具有操作对象,这些操作对象是整数常量、枚举常量、字符常量、其结果为整数常量的sizeof表达式和作为强制转换操作数的浮点常量,这些强制转换运算符在整数常量表达式中只能将算术类型转换为整数类型,除非它们是sizeof运算符的操作数的一部分。

§6.3.2.3.3

值为0的整数常量表达式或将此类表达式强制转换为类型void *的表达式称为空指针常量。 如果将null指针常量转换为指针类型,则所得到的指针,称为空指针,保证不等于任何对象或函数的指针。


3
非常感谢提供这么棒的信息。关于你的问题:不,我认为这并不是良好的语言设计,因为它显然在我的代码中引入了一个不必要的错误。 :P - user541686

14
nullchar是一个(编译时的)常量表达式,值为0。因此它可以隐式转换为空指针。
更详细地说:我在这里引用了1996年草案标准中的内容。 char是一种整数类型。nullchar是const类型,因此它是一个(编译时的)整数常量表达式,根据第5.19.1节:

5.19 常量表达式 [expr.const]

1 在多个场合下,C++要求表达式评估为整数或枚举常量...一个整数常量表达式可以涉及 ... const变量...

此外,nullchar评估为0,允许将其隐式转换为指针,根据第4.10.1节:

4.10 指针转换 [conv.ptr]

1 整数类型的整型常量表达式(expr.const)右值,其计算结果为零(称为空指针常量),可以转换为指针类型。

也许一个直观的原因"为什么"允许这样做(仅凭直觉)是指针宽度未指定,因此允许从任何大小的整型常量表达式转换为空指针。


根据较新的C++03标准中相关的部分更新...... 根据§5.19.1......

整数常量表达式只能涉及文本(2.13),枚举类型,使用常量表达式初始化的整型或枚举类型的const变量或静态数据成员,整型或枚举类型的非类型模板参数以及sizeof表达式。

然后,我们看看§4.10.1......

空指针常量是一个整数类型的常量表达式(5.19)右值,其求值结果为零。空指针常量可以转换为指针类型;结果是该类型的空指针值,并且与指向对象或函数类型的每个其他指针值都不同。相同类型的两个空指针值应该相等。


在GCC中,编译器不会生成错误的唯一情况是当值为0时。如果您编写static const char nullchar = '\x30'或任何其他值,则编译将失败。因此,Managu是正确的:0是一个特殊情况。如果您想在gcc中获得警告,请在命令行上使用-Wconversion,它会在所有转换(您没有明确使用强制转换的情况)上发出警告。不确定MSVC是否适用。 - Mr Lister
2
我也非常好奇。我想知道这是否真的被C++标准允许,或者是常量替换过早引起的副作用。 - sylvain.joyeux
@veer:即使是gcc,有时也不遵循标准。 - sylvain.joyeux
@Managu:感谢您提供标准中的引用!这绝对是我正在寻找的信息。 - sylvain.joyeux
@Mehrdad:“...但是(为什么)C++允许这样做?”因为C++会隐式地执行很多操作,从而避免您多打几个字符的痛苦。有时,这会导致令人讨厌且难以发现的错误,就像您刚刚经历的那样。 - Giorgio
显示剩余5条评论

11

这段代码能编译通过的原因与之前代码相同

const char *p = 0; // OK

const int i = 0;
double *q = i; // OK

const short s = 0;
long *r = s; // OK

右侧的表达式具有类型int和short,而被初始化的对象是指针。这让你感到惊讶吗?
在C++语言(以及C语言)中,值为0的整数常量表达式(ICE)具有特殊状态(尽管在C和C++中,ICE的定义不同)。它们被认为是“空指针常量”。当它们用于指针上下文中时,它们会隐式转换为相应类型的空指针。
在这个上下文中,类型char是一个整数类型,与int并没有太大的区别,因此通过0初始化的const char对象在C++中也是一个空指针常量(但在C中不是)。
顺便说一下,在C++中,类型bool也是一个整数类型,这意味着通过false初始化的const bool对象也是一个空指针常量。
const bool b = false;
float *t = b; // OK

一份针对C++11的缺陷报告改变了空指针常量的定义。在修正后,空指针常量只能是“值为零的整数字面量或类型为std::nullptr_t的prvalue”。在修正后,上述指针初始化在C++11中不再是良构的。


1
“_整数常量表达式(ICE)_”不要与内部编译器错误混淆 ;) - curiousguy
@curiousguy:我把它和H₂O混淆了 ;) - user541686

6
它并没有忽略数据类型,这不是一个错误。它利用你所输入的const,并看到它的值实际上是整数0(char是一个整数类型)。
整数0是一个有效(按定义)的空指针常量,它可以转换为指针类型(成为空指针)。
你想要空指针的原因是有一个指针值“指向无处”,并且可以被检查(即,你可以将空指针与整数0进行比较,你将得到true的返回值)。
如果你去掉const,你会得到一个错误。如果你把double放在那里(像许多其他非整数类型一样;我猜异常只有可以通过重载转换操作符转换为const char*的类型),你会得到一个错误(即使没有const)。等等。
整个问题在于,在这种情况下,你的实现看到你正在返回一个空指针常量;你可以将其转换为指针类型。

5

似乎这个问题的真正答案已经在评论中得到了回答。总结如下:

  • The C++ standard allows const variables of integral type to be considered "integral constant expressions." Why? Quite possibly to bypass the issue that C only allows macros and enums to hold the place of integral constant expression.

  • Going (at least) as far back as C89, an integral constant expression with value 0 is implicitly convertible to (any type of) null pointer. And this is used often in C code, where NULL is quite often #define'd as (void*)0.

  • Going back to K&R, the literal value 0 has been used to represent null pointers. This convention is used all over the place, with such code as:

    if ((ptr=malloc(...)) {...} else {/* error */}
    

实际上,如果我记得正确的话,这也是出于历史原因;在某个时候,已经有很多代码将(char字符串)定义为char *,因此当引入const时,允许进行相关赋值被认为是合理的。我甚至不确定是否可以在K&R本身中阅读到这个通知。 - mlvljr

2

我认为问题在于空字符在类型之间是通用的。你所做的是在返回空字符时设置一个空指针。如果使用任何其他字符,这将失败,因为你没有将字符的地址传递给指针,而是将字符的值传递给了指针。空值是一个有效的指针和字符值,因此可以将空字符设置为指针。

简而言之,无论是数组、指针还是变量,都可以使用null来设置空值。


2

这里有一个自动转换功能。如果您成功运行此程序:

#include <stdio.h>
const char* c_str()
{
    static const char nullchar = '\0';
    return nullchar;
}

int main()
{
    printf("%d" , sizeof(c_str()));
    return 0;
}

在我的电脑上,输出将为4,大小与指针相同。

编译器会自动进行类型转换。请注意,至少gcc会发出警告(我不知道VS的情况)。


1
“auto cast” 这个概念并不存在。它要么是隐式转换,要么就是强制转换(显式转换)。 - curiousguy

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