【问题标题】:C++ const changed through pointer, or is it? [duplicate]C++ const 通过指针改变,或者是吗? [复制]
【发布时间】:2017-04-27 01:49:06
【问题描述】:

在 c 中,可以使用如下指针更改 const:

//mainc.c
#include <stdio.h>

int main(int argc, char** argv) {
    const int i = 5;
    const int *cpi = &i;

    printf("  5:\n");
    printf("%d\n", &i);
    printf("%d\n", i);
    printf("%d\n", cpi);    
    printf("%d\n", *cpi);   

    *((int*)cpi) = 8;
    printf("  8?:\n");
    printf("%d\n", &i);
    printf("%d\n", i);
    printf("%d\n", cpi);
    printf("%d\n", *cpi);
}

常量已更改,如输出所示:

如果我们在 c++ 中尝试同样的方法:

//main.cpp
#include <iostream>

using std::cout;
using std::endl;

int main(int argc, char** argv) {
    const int i = 5;
    const int *cpi = &i;

    cout << "  5:" << '\n';
    cout << &i << '\n';
    cout << i << '\n';
    cout << cpi << '\n';    
    cout << *cpi << '\n';   

    *((int*)cpi) = 8;
    cout << "  8?:" << '\n';
    cout << &i << '\n';
    cout << i << '\n';
    cout << cpi << '\n';
    cout << *cpi << '\n';

    int* addr = (int*)0x28ff24;
    cout << *addr << '\n';
}

结果不是很清楚:

从输出看,i 仍然是 5 并且仍然位于 0x28ff24,因此 const 没有改变。但同时cpi也是0x28ff24(与&amp;i相同)但它指向的值是8(不是5)。

谁能解释一下这里发生了什么魔法?

在此解释:https://stackoverflow.com/a/41098196/2277240

【问题讨论】:

  • 我想是未定义的行为。
  • 虽然从语言的角度来看这是未定义的行为,但我们可以猜测 g++ 中发生了什么。我认为当您直接使用 i 时,编译器会使用值 5。在您通过指针访问变量的地方,您将获得修改后的值。但是,在某些其他架构上,可以将常量放置在不可写内存中,并且尝试写入该内存可能会被硬件忽略或导致其他故障。
  • 我发誓此常见问题解答存在规范副本,但我找不到。
  • 顺便提一下,printf("%d\n", &amp;i);printf("%d\n", cpi); 都会调用未定义的行为,因为您使用了错误的格式说明符。

标签: c++ c pointers constants


【解决方案1】:

从最初声明为 const 的变量(甚至通过 C++ 中的指针或引用)中丢弃 const,然后尝试通过该指针或引用更改变量的行为是 未定义。

因此,如果将 i 声明为 const int i = 5;,则更改 i 是未定义的行为:您观察到的输出就是这种情况的体现。

【讨论】:

  • 那么&amp;i*cpi 一定是在撒谎,而且看起来不像是后者。我不应该相信 const 的地址吗?是虚拟地址吗?
  • 为了更加迂腐,抛弃const 很好,但实际上修改对象是UB。这种情况可能出现在一些通用代码中。
  • @Quentin:指出这是一件好事。谢谢。
  • @grabantot:如果你的程序调用了未定义的行为,那么你就不能信任任何东西。你甚至不能相信你认为在未定义行为被调用之前发生的事情!
  • @grabantot 未定义的行为意味着任何事情都可能发生。编译器甚至不必在“任何东西”的定义上保持一致!不仅在这行代码中,任何一次调用未定义行为的程序都会使整个运行中毒。这背后的基本原理是允许实现在不应该发生的情况下做任何“最便宜”的事情,从而允许优化。永远不要修改最初声明为 const 的变量!
【解决方案2】:

根据 C11 6.7.3/6,这是未定义的行为:

如果尝试修改使用 通过使用具有非 const 限定的左值的 const 限定类型 类型,行为未定义。

(C++ 会有类似的规范性文本。)

而且由于它是未定义的行为,任何事情都可能发生。包括:奇怪的输出、程序崩溃、“似乎工作正常”(这个版本)。

【讨论】:

    【解决方案3】:

    const_cast&lt;Type *&gt;()或c型转换(Type *)的规则:
    转换是删除const声明,而不是删除值(对象)本身的const

    const Type i = 1;
    // p is a variable, i is an object
    const Type * p = &i; // i is const --- const is the property of i, you can't remove it
    (Type *)p; // remove the const of p, instead the const of i ---- Here p is non-const but i is ALWAYS const!
    

    现在,如果您尝试将 i 的值更改为 p,这是未定义行为,因为 i 始终为常量。

    何时使用这种转换?
    1) 如果您可以确保指向的值不是 const。
    例如

    int j = 1;
    const int *p = &j;
    *(int *)p = 2; // You can change the value of j because j is NOT const
    

    2) 指向的值是 const,但您只能读取它,永远不要更改它。

    如果您确实需要更改 const 值,请重新设计代码以避免这种情况。

    【讨论】:

    • @MM 谢谢。事实上,我只是复制了 The C++ Programming Language 中的句子,但我的例子并不合适。我会重新编辑我的答案。
    【解决方案4】:

    所以经过一番思考,我想我知道这里发生了什么。尽管它依赖于架构/实现,因为正如玛丽安指出的那样,它是未定义的行为。我的设置是 mingw 5.x 32bit on windows 7 64 bit,以防有人感兴趣。

    C++ consts 的行为类似于#defines,g++ 用其在编译代码中的值替换所有i 引用(因为 i 是一个 const),但它也将5(i 值)写入内存中的某个地址以提供访问权限i 通过指针(虚拟指针)。并用该地址替换所有出现的&amp;i(编译器不完全这样做,但你知道我的意思)。

    在 C 中,const 大多被视为通常的变量。唯一的区别是编译器不允许直接更改它们。

    这就是为什么 Bjarne Stroustrup 在他的书中说你不需要 C++ 中的#defines。

    证据来了:

    【讨论】:

    • 这里到底发生了什么并不重要,重要的是它是未定义的行为,在 C++ 和 C 中。不同的编译器版本,不同的操作系统,这些都会破坏代码,但事实并非如此便携的。它甚至可以在完全相同的系统上随机中断。
    • @RaphaelMiedl 也许你是对的,但我想深入了解它,听到每个人都说“这是未定义的行为,忘记它”有点令人失望。
    • @RaphaelMiedl 我觉得它非常有趣且富有洞察力。
    • 更重要的是要理解“这就是它的行为方式”是一个不正确的陈述。 “这就是它曾经的行为方式”就是你所得到的。
    【解决方案5】:

    这违反了严格的别名规则(编译器假定两个不同类型的指针从不引用相同的内存位置)与编译器优化相结合(编译器不执行第二次内存访问以读取i,而是使用前一个变量)。

    EDIT(如 cmets 中所建议的):

    来自 ISO C++ 标准 (N3376) 的工作草案:

    "如果程序试图通过 行为是以下类型之一以外的泛左值 不明确的 [...] — 动态类型的 cv 限定版本 目的, [...] — 有符号或无符号类型 对应于动态类型的 cv 限定版本 目的, [...] — 作为(可能是 cv 限定的)基类的类型 对象的动态类型的类型,”

    据我了解,它指定了一个可能的 cv 限定类型可以用作别名,但不是 cv 限定类型的非 cv 限定类型可以用作别名。

    【讨论】:

    • 严格的别名规则是指不同基本类型的变量。此问题与严格别名无关。
    • 据我了解 (3.10) 它完全是严格的别名。那里写着据我了解它指定,一个可能的 cv 限定类型可以用作别名,但不是 cv 限定类型的非 cv 限定类型可以。
    • 我倾向于同意,尽管需要同时考虑 C 和 C++ 的事实无助于全面回答这个问题的困难。也许@tsp,您可以开始将您的一些答案 cmets 吸收到答案本身中。
    • 我对标准进行了更仔细的研究,我意识到我错了。我对此感到非常抱歉。 :(
    • 我做了一个微不足道的修改,让反对者有机会撤回。
    【解决方案6】:

    询问带有特定标志的特定编译器对该代码的处理比“C”或“C++”的处理更有成效,因为C和C++都不会对这样的代码做任何一致的事情。这是未定义的行为。任何事情都有可能发生。

    例如,将const 变量粘贴在内存的只读页面中是完全合法的,如果程序尝试写入它会导致硬件故障。或者,如果您尝试写入它,就会默默地失败。或者将取消引用的int*const int* 转换为可以在不影响原始文件的情况下修改的临时副本。或者在重新分配后修改对该变量的每个引用。或者在 const 变量不能更改的假设下重构代码,以便操作以不同的顺序发生,并且您最终会在您认为自己修改变量之前修改该变量,或者之后修改它。或者使i 成为对常量1 的其他引用的别名,并在程序的其他地方也修改它们。或者破坏一个程序不变量,使程序以完全不可预测的方式出错。或者打印一条错误消息并在它捕获到这样的错误时停止编译。或者行为取决于月相。或者别的什么。

    有编译器、标志和目标的组合可以做这些事情,但 可能 是月球阶段错误的例外。不过,我听说过的最有趣的变体是,在某些 Fortran 版本中,您可以将常数 1 设置为 -1,并且所有循环都会向后运行。

    像这样编写生产代码是一个糟糕的主意,因为您的编译器几乎肯定不能保证此代码在您的下一次构建中会做什么。

    【讨论】:

      【解决方案7】:

      简短的回答是 C++ 'const' 声明规则允许它在 C 必须取消引用变量的地方直接使用常量值。即C++编译语句

      cout << i << '\n';
      

      好像实际上写的是

      cout << 5 << '\n';
      

      所有其他非指针值都是解引用指针的结果。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-25
        • 1970-01-01
        • 2020-02-04
        • 1970-01-01
        相关资源
        最近更新 更多