const是一个谎言吗?(因为const可以被强制转换)

26

可能是重复问题:
说服我使用const正确性

CC++中的关键字const有什么用处,既然它允许这样的事情?

void const_is_a_lie(const int* n)
{ 
    *((int*) n) = 0;
}

int main()
{
    int n = 1;
    const_is_a_lie(&n);
    printf("%d", n);
    return 0;
}

输出:0

很显然,const 不能保证参数不可修改。


4
请注意,虽然在您特定的情况下这样做是可以接受的,但是“强制转换掉const属性”有时会导致“未定义行为”。 - K-ballo
1
@K-ballo 我不明白为什么这么多好的回答 - 像你的一样!- 被放在评论中... - Kijewski
2
强制类型转换,一切皆有可能 :) - Op De Cirkel
1
@K-ballo 是的,UB来自于赋值操作。 - Captain Obvlious
1
这不是重复的问题。他在问为什么要使用const,如果你可以将其转换掉,另一个问题是“为什么我应该使用const而不是只是不改变东西”。 - CoffeeTableEspresso
显示剩余3条评论
5个回答

45

const是你对编译器做出的承诺,而不是它向你保证的内容。

例如,

void const_is_a_lie(const int* n)
{ 
    *((int*) n) = 0;
}

#include <stdio.h>
int main()
{
    const int n = 1;
    const_is_a_lie(&n);
    printf("%d", n);
    return 0;
}

http://ideone.com/Ejogb 上显示的输出结果为:

1

由于使用了const关键字,编译器可以假设该值不会更改,从而可以跳过重新读取它的步骤,这样可以让程序运行得更快.

在这种情况下,由于const_is_a_lie()违反了约定,所以会发生奇怪的事情。请不要违反合同,并且感谢编译器帮助您遵守合同。强制转换是有害的。


1
特别是,C编译器可以合法(并且合理地)得出结论,即“n”永远不会改变(它是局部的,从未在“main”中修改,并且没有非const引用从“main”中逃逸),因此可以将“printf”常量折叠为打印“1”,即使“n”不是“const”。 - Chris Dodd
3
@ChrisDodd: 不,我认为不行。逃逸的引用是const限定符并不重要 - 只要一个非常量对象的引用逃逸了,这就是最重要的。您是否有标准中支持您观点的参考文献? - caf
5
在引用一个非 const 对象时,强制去除其 const 属性并不属于未定义行为 (UB),因此我不确定这个说法是否准确。 - Puppy
5
实际上,强制转换去除const限定符并不会导致未定义行为。只有在修改常量对象时才会导致未定义行为。 - GManNickG
3
“const”通常作为一种文档形式很有用,但你提供的例子并不是。如果一个对象通过原始指针返回,无论是否使用“const”,调用者都不应该删除它。在现代C++风格中,如果要转移所有权,对象将作为“std::unique_ptr”返回,并随其携带其删除器。 - Ben Voigt
显示剩余9条评论

10
在这种情况下,n 是一个指向常量 int 的指针。当你将它转换为 int* 时,你移除了 const 修饰符,因此允许该操作。
如果你告诉编译器移除 const 修饰符,它将很乐意地这样做。如果你让编译器执行其工作,它将帮助确保你的代码是正确的。通过强制类型转换消除常量性,你告诉编译器,你知道 n 的目标不是常量,并且你真正想要改变它。
如果你指针所指向的东西最初实际上被声明为 const,那么你试图更改它就会引发 未定义行为,并且任何事情都可能发生。它可能有效。写入操作可能不可见。程序可能崩溃。你的显示器可能打你。(好吧,最后一句可能不会发生。)
void const_is_a_lie(const char * c) {
    *((char *)c) = '5';
}

int main() {
    const char * text = "12345";
    const_is_a_lie(text);
    printf("%s\n", text);

    return 0;
}

根据您的特定环境,const_is_a_lie 可能会出现段错误(也称为访问冲突),因为编译器/运行时可能将字符串文字值存储在不可写内存页面中。

标准对修改 const 对象有如下规定:

7.1.6.1/4 限定符(cv-qualifiers) [dcl.type.cv]

任何声明为 mutable (7.1.1) 的类成员可以被修改,但在 const 对象生命周期(3.8)中尝试修改其内容都会导致未定义行为(undefined behavior)。

"医生,我这样做会很疼!" "那就别这么做了。"


6

Your...

int n = 1;

确保n存在于读写内存中;它是一个非const变量,因此稍后尝试修改它将具有定义的行为。有了这样一个变量,您可以拥有对它的const和/或非const指针和引用的混合 - 每个的常数性只是程序员防止在该代码“分支”中意外更改的一种方式。我说“分支”,因为您可以将访问n所给予的视为一棵树,其中-一旦标记了const的分支,则所有子分支(进一步指向/引用n的指针/引用,无论是否从中初始化了其他本地变量、函数参数等)都需要保持const,除非您明确地取消该常数性概念。取消const是安全的(如果可能会令人困惑),对于像您的n这样的可变变量,因为它们最终仍然写回到一个可修改/可变/非const的内存地址中。在这些情况下,你可以想象所有奇怪的优化和缓存都不允许发生问题,因为标准要求并保证在我刚才描述的情况下有明智的行为。

不幸的是,也有可能取消真正固有const变量的常数性,比如说const int o = 1;,任何尝试修改它们的企图都将具有未定义的行为。这有许多实际原因,包括编译器将它们放置在只读内存中(例如UNIX mprotect(2)),以便尝试写入时会导致CPU陷阱/中断,或者每当需要原始设置值时从变量中读取(即使没有在使用该值的代码中提到变量的标识符),或者使用原始值的编译时内联副本-忽略变量本身的任何运行时更改。因此,标准将行为留给了未定义。即使它们按您的意图被修改,程序的其余部分之后也将具有未定义的行为。

但是,这不应该令人惊讶。对于类型也是同样的情况-如果你有...

double d = 1;
*(int*)&d = my_int;
d += 1;

你有没有欺骗编译器关于d类型的信息?最终,d占用的内存在硬件级别上可能是未分类的,因此编译器对其的看法仅限于对其进行位模式的重排。但是,根据my_int的值和您的硬件上的双精度表示,您可能已创建d中的无效位组合,这些组合不代表任何有效的双精度值,因此尝试将内存读回到CPU寄存器中和/或执行类似+= 1的操作时,具有未定义的行为并且可能会例如生成CPU陷阱/中断。

这不是C或C ++中的错误...它们被设计为让您向硬件发出可疑请求,以便如果您知道自己在做什么,则可以执行一些奇怪但有用的操作,并且很少需要退回到汇编语言编写低级代码,即使是设备驱动程序和操作系统也是如此。

尽管如此,正是因为转换可能是不安全的,因此在C ++中引入了更明确和有针对性的转换符号。无法否认风险-您只需要理解您正在要求什么,为什么有时可以,而有时则不行,并且学会接受它。


2
类型系统的存在是为了帮助你,而不是保姆式的照顾你。你可以用许多方法绕过类型系统,不仅仅是关于const的问题,每一次这样做,你都会减少程序的一个安全性。你可以忽略const正确性甚至基本的类型系统,通过传递void*并根据需要进行转换。这并不意味着const或者类型是虚假的,只是你可以强制编译器按照你的方式执行。 const的作用在于让编译器知道你的函数契约,并且让它帮助你避免违反它。同样,变量被赋予类型也是为了让你不需要猜测如何解释数据,因为编译器会帮助你。但是它不会像保姆一样,如果你强制删除const属性或告诉它如何检索数据,编译器就会放任你,毕竟是你设计了应用程序,谁能怀疑你的判断力呢...
此外,在某些情况下,你可能会导致未定义的行为,甚至使你的应用程序崩溃(例如,如果你从一个真正的const对象中去掉const并修改该对象,你可能会发现副作用在某些地方是看不到的(编译器假定值不会改变,因此执行常量折叠),或者如果常量被加载到只读内存页中,你的应用程序可能会崩溃。

1

“const”从未保证不可变性:标准定义了一个“const_cast”,允许修改常量数据。

“const”对于您声明更多意图并避免更改仅用于读取的数据非常有用。如果您这样做,您将获得编译错误提示您三思而后行。您可以改变主意,但不建议这样做。

正如其他答案所提到的,编译器可能会在使用常数时进行更多优化,但好处并不总是显着的。


2
只是对于阅读此内容的任何人而言,const_cast 仅存在于 C++ 中,而非 C。 - cdhowie
1
const_cast仅仅是删除cv限定符。标准仍然规定“在其生命周期内修改const对象会导致未定义的行为”。据我所知,没有任何地方规定删除cv限定符会释放对象以进行修改。 - Captain Obvlious
@ChetSimpson 经过一些思考,我认为您可以修改一个const 对象,但不能修改const 数据。例如,一个对象可以是const并且有一个mutable字段或者使用const_cast。一个常量(如const int i = 3)绝不能被修改。可能有更精确的条件来解释这一点,但我没有时间深入挖掘。 - J.N.
@J.N. 7.1.6.1/4。唯一不会产生未定义行为的方法是对象最初被声明为可变性。 (我在其他评论中关于赋值产生UB的错误)。如果您仅将其应用于const_cast,则可能会产生未定义行为。但是,如果将其应用于const限定的对象,则始终会导致UB。我们可能从不同的角度表达了相同的意思;) - Captain Obvlious
@GManNickG,这就是我在早前对K-Ballo的评论中所指的错误之处。 - Captain Obvlious
显示剩余3条评论

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