在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
(与&i
相同),但它指向的值为8
(不是5
)。
有人可以解释一下这里发生了什么样的魔法?
答案 0 :(得分:36)
从变量(甚至通过C ++中的指针或引用)中抛弃const
的行为,最初声明为const
,然后尝试通过该指针更改变量或引用,未定义。
如果将i
声明为const int i = 5;
,则更改{{1}}是未定义的行为:您正在观察的输出是其表现形式。
答案 1 :(得分:16)
根据C11 6.7.3 / 6:
,这是未定义的行为如果尝试修改用a定义的对象 通过使用具有非const限定的左值的const限定类型 类型,行为未定义。
(C ++将有类似的规范性文本。)
由于它是未定义的行为,任何事情都可能发生。包括:奇怪的输出,程序崩溃,“似乎工作正常”(这个版本)。
答案 2 :(得分:4)
const_cast<Type *>()
或c-type转换(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)如果可以确保指向的值不是常数
例如
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值,请重新设计代码以避免此类情况。
答案 3 :(得分:3)
所以经过一番思考后我猜我知道这里发生了什么。虽然它依赖于架构/实现,因为它是未定义的行为,正如Marian指出的那样。我的设置是Windows 7 64位mingw 5.x 32bit,以防有人感兴趣。
C ++ consts就像#defines一样,g ++用编译代码中的值替换所有i
引用(因为我是一个const),但它也将5
(i值)写入内存中的某个地址通过指针(虚拟指针)提供对i
的接受。并用该地址替换&i
的所有出现(不完全是编译器做的,但你知道我的意思)。
在C中,对象的处理方式大多与通常的变量一样。唯一的区别是编译器不允许直接更改它们。
这就是为什么Bjarne Stroustrup在他的书中说你在c ++中不需要#defines。
答案 4 :(得分:2)
违反严格别名规则(编译器假定两个不同类型的指针从不引用相同的内存位置)与编译器优化相结合(编译器没有执行第二次内存访问以读取i
但是使用前一个变量。)
编辑(如评论中所示):
从ISO C ++标准的工作草案(N3376):
“如果程序试图通过访问对象的存储值 行为是除以下类型之一以外的glvalue 未定义[...] - 动态类型的cv限定版本 对象,[...] - 有符号或无符号类型的类型 对应于动态类型的cv限定版本 对象,[...] - 一种类型(可能是cv限定的)基类 对象的动态类型的类型,“
据我所知,它指定了一个可能的cv限定类型可以用作别名,但不能用于cv限定类型的非cv限定类型。
答案 5 :(得分:2)
使用某些标志设置的特定编译器对该代码的作用比“C”或“C ++”的作用更为富有成效,因为C和C ++都不会像这样的代码一致地执行任何操作。这是未定义的行为。任何事情都可能发生。
例如,将i
变量放在内存的只读页面中会导致硬件故障,如果程序试图写入它,那将是完全合法的。或者如果你试着写它就静静地失败。或者将const
的解除引用int*
转换为临时副本,可以在不影响原始版本的情况下进行修改。或者在重新分配后修改对该变量的每个引用。或者在假设const int*
变量不能改变以便操作以不同顺序发生的情况下重构代码,并且在您认为之后修改变量之前最终修改变量。或者使const
为常量i
的其他引用的别名,并在程序的其他位置修改它们。或者打破程序不变量,使程序以完全不可预测的方式出错。或者打印错误消息并停止编译,如果它捕获到这样的错误。或者依赖于月相的行为。或其他任何事情。
有一些编译器,标志和目标的组合可以完成这些工作,其中可能例外的月相错误。不过,我听说过的最有趣的变体是,在某些版本的Fortran中,你可以将常量1设置为-1,并且所有循环都会向后运行。
编写像这样的生产代码是一个糟糕的想法,因为你的编译器几乎肯定不能保证这个代码在你的下一个版本中会做什么。
答案 6 :(得分:0)
简短的回答是C ++&#39; const&#39;声明规则允许它直接在C必须取消引用变量的地方使用常量值。即,C ++编译语句
cout << i << '\n';
好像它实际写的是
cout << 5 << '\n';
所有其他非指针值都是解引用指针的结果。