C++标准组织文档翻译:常量正确性,Const Correctness
我的公有(public)成员函数的常量性(constness),应当基于该个方法与对象的 逻辑状态 ( logical state )还是 物理状态 ( physical state )的关系来确定? |
这是个好东西。 它的意思是,使用关键字 const 来阻止那些常量( const )对象 发生 改变。例如 , 妳想创建一个函数 f() ,它接受一个 std::string 类型的 参数,并且 , 妳想要向调用者保证, 不会改变调用者向 f() 中传递的 std::string ,那么, 妳可以这样定义 f() 及它的 std::string 参数……
•. void f1(const std::string& s); // 传递 的是指向常量的引用
•. void f2(const std::string* sptr); // 传递 的是指向常量的指针
•. void f3(std::string s); // 传递 的是值
在“ 指向常量的引用 ”和“ 指向常量的指针 ”两种情况中, 在 f() 函数中任何的想要改变调用 者传入的 std::string 变量的尝试, 都会在编译过程中被编译器标记 为编译错误。 这种检查,完全是在编译过程中完成的:对于此处 的 常量 ( const ),不会有任何运行时的空间及速度上的损耗。 在“ 传递 的是值 ” ( f3() ) 那种情况中, 被调用的函数得到的是调用 者的 std::string 变量的一个副本。 这意味着, f3() 可以改变它在本地得到的这个副本,但是, 当 f3() 返回时,这个副本就会被销毁。特别说明 一点, f3() 无法改变调用 者的 std::string 对象。
反过来的例子,假设妳想创建一个函数 g() ,它接受一个 std::string 类型的参数,并且妳想让调用者知道, g() 可能会改变调用者的 std::string 对象。 在这种情况下,妳可以这样定义 g() 及它的 std::string 参数……
•. void g1(std::string& s); // 传入 的是指向非常量对象的引用
•. void g2(std::string* sptr); // 传入 的是指向非常量对象的指针
这些函数中未指定 const ,因此,它向编译 器表明, 要允许它们 ( 但不要求它们一定这么做 )改变调用者的 std::string 对象。 也就是说,它们可以将自己的 std::string 传递给任意一个 f() 函数,但是,只有 f3() ( 即,以 “传值”方式接受参数的那个函数 ) 可以将自己的 std::string 传递给 g1() 或 g2() 。如果 f1() 或 f2() 需要调用任何一个 g() 函数的话,那么,必须创建 std::string 对象 的一个本地副本,再将该副本传递给 g() 函数;传递 给 f1() 或 f2() 的参数,是不可以直接传递给任何 一个 g() 函数的。例如 ,
void g1(std::string& s);
void f1(const std::string&s)
{
g1(s); // 编译 时错误,因为 s 是常量 ( const )
std::stringlocalCopy=s;
g1(localCopy); // 编译正常进行 ,因为 localCopy 不是常量 ( const )
}
在以上示例中, g1() 所进行的任何变动, 都只对 f1() 中的本地变量 localCopy 起作用。特别 是, 以引用方式传递给 f1() 的那个常量( const )参数,未发生任何改变。
为参数声明常量属性( const -ness),本身 就是另一种形式的类型安全性。
如果 妳发现通常所用的类型安全性措施起到 了作用,使得妳的系统保持正确运行 ( 它确实有这个作用;尤其是在大型系统中 ) ,那么,妳会发现,常量( const )正确性同样会起到有效的帮助作用。
常量正确 性的好处是,它会阻止妳 无意 间 改变某个 妳并未打算改变的东西。 妳所需要做的,就是, 多敲几个字母 ( 也就是 const 关键字 ) , 以对妳的代码进行修饰,得到 的好处就是, 向编译器 和 其它程序员提供一些额外 的重要的语义信息 — 这些信息,可帮助编译器阻止错误 发生,可被其它程序员用作文档来阅读。
从概念上来说, 举个例子,妳可以将 const std::string 看成是 与普通的 std::string 不同的另一个类,因为 ,带有 const 的那个变种中,相当 于缺少了各种可对对象进行改变的操作,而不带 const 的那个变种中是拥有这些操作的。例如, 妳可以认为, const std::string 就是缺失了赋值操作符 += 以及其它任何可改变其内容的操作。
应该 在最最 最 开始就实现。
在旧的代码中补入常量正确性的话,会产生一个雪球效应: 妳在“此处”所加入的 每一个 const ,会导致 在“它处”再加入四个 const 。
尽早加入 const ,尽量多地加入 const 。
它的意思是, p 指向一个类型为 X 的对象,但是 ,不能通过 p 来改变那个 X 对象( p 也可以是 NULL )。按照 从右向左的顺序来解读它: “p 是一个指针,指向一个 X ,后者是一个常量。 ”
例如 ,假设类 X 中有一个 常量成员函数 ,比如, inspect() const ,那么,可以这样调用, p->inspect() 。但是,假设 类 X 中有一个 非常量成员函数 ,比如 mutate() ,那么 ,这样写就会发生编译错误, p->mutate() 。
重要 的是,这个错误是 由编译器在编译过程中捕捉的 — 不会在运行时进行任何的检查。 这就意味着, const 并不会降低妳的程序的运行速度, 也不需要妳来编写任何的测试用例以进行运行时检查 — 编译器 在编译过程中已经为妳做了这些事了。
按照从右向左的顺序来解读这些指针声明。
•. const X* p ,表示的是, “ p 指向 一个 X ,后者是常量( const ) ”: 无法 通过 p 来改变 X 对象。
•. X* const p ,表示的是, “ p 是一个常量 ( const ) 指针,指向 一个 X ,后者 不是常量(non- const ) ”: 妳无法改变指针 p 本身,但是 妳可以 通过 p 来改变 X 对象。
•. const X* const p ,表示的是, “ p 是一个常量( const )指针,指向 一个 X ,后者是一个常量( const ) ”: 妳无法改变指针 p 本身,也无法 通过 p 来改变 X 对象。
还有,我之前是不是说过?按照从右向左的顺序来解读指针声明。
它表示, x 是某个 X 对象的一个别名, 但妳无法 通过 x 来改变那个 X 对象。
从右向左解读: “ x 是一个指向 X 的引用,后者 是常量( const )。 ”
例如,假设 X 中 有某个 常量成员函数 ,比如 inspect() const ,那么 ,妳可以这样调用, x.inspect() 。但是 ,假设X中有某个 非常量成员函数 ,名为mutate(),那么, 这样调用就会出错,x.mutate() 。
这一点,与 指向常量 的指针 完全相同 ,包括其中 的那个事实,即,编译器 在编译过程中为妳做出所有的检查, 这就意味着, const 并不会降低妳的程序的运行速度, 也不需要妳编写额外 的测试用例来在运行时进行检查。
X const& x 等价 于 const X& x , X const* x 等价于 const X* x 。
某些 人,喜欢采用 const放在右侧( const -on-the-right) 的风格,并且 称之为 “ 一致性const ”(“consistent const .”)。 事实上,const放在右侧的风格,确实比另一种风格更具有一致性: const放在右侧风格中, 永远 会将 const 放置在它所修饰(constifies)的事物的右侧, 而另一种风格呢, 有些时候 会将 const 放置在左侧, 有些时候 会将 const 放置在右侧。
使用 const放在右侧风格时, 一个常量本地变量,在定义时,应当将 const 放置在右侧: int const a = 42; 。类似 地, 一个常量静态(static)变量,定义时应当这样写,static double const x = 3.14;。基本 上,每个const,都被放置在它修饰的事物的右侧, 这还包括,const 必须 被放置在右侧的情况: 常量成员函数 。
尽管 有着这些好处,但是,const放在右侧风格并不是非常流行,因此 ,那些 有着已有C++代码的组织中,倾向 于继续使用之前的风格, 这样,它们的整体代码 库就具有一个一致的代码规范。
另一个建议:选择 最适合妳的组织中 平均水平程序 员 的风格。 并不去迎合那些 (极少 的 )专家, 也不去迎合那些 (极少 的 ) 呆瓜,而是去迎合那些平均水平程序员。除非 妳想要解雇她/他们再雇用新人,否则, 妳就应当使用一个能让 她/他们 理解妳的代码的风格。
这就意味着,妳应当根据现状来做出决定,而不是去迎合某人的假设或成见。这方面并没有一刀切的标准。没有哪个决策是在所有时间对所有组织都正确的,因此,不要让任何人朝着任何方向做出武断的决策。多做思考并无坏处。
另一个警告:如果 妳决定使用 const放在右侧的风格,那么, 要想办法确保 妳的同事不会将 X const& 错误地写成 无意义的 “ X& const x ” 。类似 地,要确保, 同事们不要将 X const* 错误 地写成 语义 上完全不同而语法上狠相似的 “ X* const ” — X const* 与 X* const 在意义上完全不同 ,只是长得像。
没有任何意义。
要弄清楚上面这个声明的意义的话,则, 从右向左解读 : “ x 是一个常量( const )引用,指向 一个 X ” 。 这是多余的 — 引用 一定是常量的( const ),因为 , 妳永远不可能掰开某个引用 再让它指向另一个不同的对象 。永远 不可能。无论 妳带不带 const 都一样。
换句话说, “ X& const x ” 在功能上与“ X& x ”是等价的。由于 妳在 & 之后加上的 const 没有起到任何作用,所以 ,妳不应该加上它: 它会导致人们产生误解 — 其中 的 const 会导致某些 人以为 X 是常量( const ), 就好像“ const X& x ”一样。
只对对象进行查询(inspects)(不是改变(mutates))的成员函数。
常量 ( const )成员函数, 是在成员函数的参数列表后面加上 const 后缀作为标识的。带有后缀 的成员函数,被称为“常量成员函数”或“查询器”(“inspectors.”)。 不带 const 后缀的成员函数,被称为“ 非常量(non- const )成员函数 ”或“改变 器 ”(“mutators.”)。
class Fred {
public:
void inspect() const; // 这个成员函数承诺了 不会 改变 *this
void mutate(); // 这个成员函数可能会改变 *this
};
void userCode(Fred&changeable, const Fred&unchangeable)
{
changeable.inspect(); // 没毛病: 并不改变一个本身可被改变的对象
changeable.mutate(); // 没毛病:改变 一个可被改变的对象
unchangeable.inspect(); // 没毛病:并不改变一个不可被改变的对象
unchangeable.mutate(); // 错误 :尝试改变 一个不可改变的对象
}
尝试调用 unchangeable.mutate() 的代码, 会在编译过程中产生一个错误。对于 const ,没有任何 的运行时空间及时间代价,并且 妳也无需编写测试用例 来在运行时对它进行检查。
inspect() 成员函数末尾 的 const ,应当被用来指明, 这个函数不会改变该对象的 抽象 ( abstract )(客户代码 所看到的 )状态。 这跟另一个说法是稍微有点不同的: 该方法不会改变该对象的结构( struct )中的“各个原始位状态”(“raw bits”)。 C++编译 器们不被允许进行“ 位级别 的 ” (“bitwise”) 的解析,除非它们能够解决别名问题 (aliasing problem), 这个问题一般是无法解决的 ( 也就是说,可能 会存在一个 非常量(non- const )的别名 , 该别名就能够用来修改该对象的状态 ) 。对于 这个别名问题的另一个 (重要 的 )解读 就是:利用 一个指向常量的指针来指向某个对象, 并不能保 证 该对象不会被改变;它只能保证 该对象不会 通过 该指针 被改变。
如果妳想从 某个查询方法 中返回妳的 this 对象的某个成员的引用,那么 , 妳应当通过指向常量 的引用(reference-to-const)( const X& inspect() const ) 或 值( X inspect() const ) 来返回它。
class Person {
public:
const std::string&name_good() const; // 正确 :调用者无法改变 Person 对象 的名字( name )
std::string&name_evil() const; // 错误 :调用 者能够改变 Person 对象的名字( name )
int age() const; // 也是正确的:调用 者无法改变 Person 对象的年纪( age )
// ...
};
void myCode(const Person&p) // myCode()承诺 不会改变这个Person 对象
{
p.name_evil()= "Igor"; // 但是 myCode() 还是改变了它!!
}
好消息是,如果妳写错了代码,编译器 通常 会发现这一点。特别 地,如果 妳无意 间使用 非常量(non- const )引用返回了妳的 this 对象的某个成员,例如 上面的 Person::name_evil() ,那么 ,编译 器 通常 会在编译其(这个例子中就是 Person::name_evil() )具体实现代码时检测 到这一点并且 给出 一个编译时错误。
坏消息是,编译器 并非每次 都能 捕获到这种错误:某些情况 下,编译器根本 不会向妳给出编译时错误消息。
换句话说:妳需要 思考 。如果 它将 妳 吓住了,那么,换 个工作吧 ;思考 并无坏处。
记住 本小节中自始至终贯穿的“常量哲学” :常量 ( const )成员函数, 不能改变 ( 或允许调用者来改变 )对应 的 this 对象的 逻辑 ( logical )状态( 也就是 抽象 状态,也就是 显义 ( meaningwise )状态 )。 想一想,一个对象,它的 意义 ( means ) 是什么, 而不要去想它在内部是如何实现的。 一个人(Person), 其年龄(age)和名字(name)是这个人(Person)的逻辑组成部分, 而这个人(Person)的邻居 (neighbor) 和雇主 (employer) 却不是。 一个查询(inspector)方法, 它返回的是该个( this )对象的逻辑/抽象/显义状态, 不能 返回指向那个部分 的一个非常量(non- const )的指针( 或引用 ),无论那个部分 在内部 具体 是 以物理嵌入该个( this )对象的直接数据成员的方式实现的,还是别的什么方式。
常量重载 ,能够帮助妳实现常量正确性。
常量重载 ,指的是这样一种情况, 妳同时拥有一个 查询方法 和一个 改变方法 ,它们拥有相同 的名字,拥有相同个数 和类型的参数。 这两个方法的唯一不同点就是,查询方法 是常量(const)方法,而改变方法是非常量( non- const)方法 。
常量重载 的最常见用法就是,用于下标(subscript)操作符中。 一般说来,妳应当先尝试使用 某个标准容器模板 ,例如 std::vector ,但是,假如妳确实需要创建自己的带有下标操作符的类,那么,应当遵循以下规则: 下标操作符通常 是成对出现的。
class Fred { /*...*/ };
class MyFredList {
public:
const Fred&operator[](unsigned index) const; // 下标操作符通常 是成对出现的
Fred&operator[](unsigned index); // 下标操作符通常 是成对出现的
// ...
};
常量 ( const )下标操作符返回一个常量引用( const -reference), 这样,编译器就会阻止调用者无意间改变对应 的 Fred 。 非常量(non- const )下标操作符返回一个非常量引用(non- const reference),通过 这种方式,妳向调用者 (及编译器)告知, 它是被允许改变 Fred 对象的。
每当 妳的 MyFredList 类的某个用户代码调用下标操作符时,编译器根据 它们 的 MyFredList 实例的常量性(constness)来选择要具体调用哪个重载方法。如果调用 者当时拥有一个 MyFredList a 或一个 MyFredList& a ,那么 , a[3] 这个代码就会调用到非常量下标操作符,因而 ,调用者会得到指向某个 Fred 的一个非常量引用:
举个例子,假设 , 类( class ) Fred 中, 有一个查询方法 inspect() const 和一个改变方法 mutate() :
void f(MyFredList& a) // 此处 的 MyFredList 是非常量引用
{
// 可以通过 a[3] 来调用 Fred 的 查询方法(仅仅查询 其属性却不改变 ):
Fredx=a[3]; // 并不会改变位于 a[3] 的 Fred 对象: 只是对那个Fred做了一次复制
a[3].inspect(); // 并不会改变位于 a[3] 的 Fred 对象: inspect() const 是一个查询方法
// 可 以通过 a[3] 来调用那些 确实 会对 Fred 做出改变的方法:
Fredy;
a[3]=y; // 改变 了位于 a[3] 的 Fred 对象
a[3].mutate(); // 改变 了位于 a[3] 的 Fred 对象: mutate() 是一个改变方法
}
如果调用者拥有一个 const MyFredList a 或一个 const MyFredList& a ,那么, a[3] 就会调用到常量( const )版本的下标操作符,于是 ,调用者最终得到的是指向某个 Fred 的一个常量( const )引用。 这种写法,允许调用者查询位于 a[3] 的 Fred 对象的属性, 但会阻止调用者无意间改变位于 a[3] 的 Fred 。
void f( const MyFredList& a) // 此处 的 MyFredList 是常量引用
{
// 可以调用那些 不会 改变 a[3] 处的 Fred 对象的方法:
Fredx=a[3];
a[3].inspect();
// 如果妳尝试改变a[3]处的Fred 对象,那么, 编译时错误 (真幸运!):
Fredy;
a[3]=y; // 幸运 (!) 编译 器在编译时捕捉到这个错误
a[3].mutate(); // 幸运 (!) 编译 器在编译时捕捉到这个错误
}
对于下标 和 函数调用的常量(Const)重载,在 此处 、 此处 、 此处 、 此处 和 此处 展示。
当然 ,妳也可以在下标操作符之外的地方使用常量重载。
因为那样会导致妳自外向内地设计妳的类,而不是反过来自内向外地设计,这就会使得妳的类和对象更容易被理解及使用、更直观、更不易出错、更快。(我承认,这样说起来太简单了点。要弄清所有这些来龙去脉的话,妳只需要继续读完本文档即可!)
让我们自内向外地理解这个问题 — 妳将会 (应当) 自外向内地设计妳的类 ,但是 ,如果妳对这个概念还狠陌生的话,那么, 自内向外的理解更容易一些。
从内部来看,妳的对象拥有物理 (或者 说固有的(concrete),或者说位级别的(bitwise) )状态 。 这个状态,对于程序员来说狠容易阅读及理解;对于 这个状态,如果将类看作C 语言风格的结构体( struct ),那么,它就会原样地被呈现出来。
从外部来看,妳的这些对象,都拥有这些类的外部用户, 这些用户被限制为只能使用公有( public )成员函数和友元( friend s )函数。 这些外部用户,也会感知到该对象具有自己的状态,例如, 该对象是一个 Rectangle 类,具有 width() 、 height() 和 area() 方法 ,那么 ,妳的用户就会说,那三个东西就是该对象的逻辑(或者 说是抽象的,或者说是显义的 )状态的组成部分。对于 一个外部用户来说, Rectangle 对象实际上拥有面积(area)属性,即便面积 是当场计算出来的(例如, area() 方法返回 该对象的宽度( width )和高度( height )的乘积 )也无所谓。事实 上,这也正是重要之处, 妳的用户并不知道也并不关心妳是如何实现这些方法的; 妳的用户仍然感知到,从她/他们的角度来说,妳的对象在逻辑上拥有一个显义状态,其由宽度、高度和面积组成。
area() 示例, 就展示了一种情况, 在那种情形下,逻辑状态 中包含了一些并不直接体现在物理状态中的元素。 反过来也成立:各个 类,有些时候会有意地将它们对象的物理(固有 、位级别 )状态中的某些部分向用户隐藏 — 对于 这个隐藏状态,它们倾向于不提供任何的公有( public )成员函数或友元( friend s )函数,以避免让用户读取或写入其值,甚至都不让用户知道这个状态的存在。 这就意味着,在该对象的物理状态中,存在着某些完全在逻辑状态中找不到对应元素的部分。
对于后面 这种情况,举个例子, 一个集合对象,可能会对它最后一次查找的结果进行缓存, 以期望提高下次查找 的性能。 这个缓存,显然是该对象的物理状态的一部分,但是 , 同时,它也是一个内部实现细节,可能并不会暴露给用户 — 它可能并不是该对象的逻辑状态的一部分。 当妳自外向内地思考时,要区分这些东西就狠容易了:如果 该个集合对象的用户没有任何办法能够检查该缓存本身的状态,那么,该个缓存就是 透明 的 ( transparent ),因而 就不是位于该对象的逻辑状态中。
逻辑状态。
接下来这一部分比较难懂。甚至理解起来 还 会 狠痛苦。建议 妳找个地方坐下。并且 , 为了 妳的安全着想, 请确保附近没有尖锐的物品。
让我们回过头去查看 集合对象示例 。记住 : 有一个查找方法,它会对最后 一次的查询结果进行缓存,以期望加快日后 的查找速度。
让我们先明确一点:假设 ,这个查找方法 不会 对该个集合对象的逻辑状态做出 任何 改变。
那么……痛苦的时刻到了。准备好了没有?
现在开始 了:如果 这个查找方法不会对该个集合对象的逻辑状态做出任何改变, 而 确实 会改变该个集合对象的 物理 状态( 它会对真正的缓存做出真正的改变 ) ,那么 ,该个查找方法应当是常量( const )方法吗?
答案是明确的,是的。(每个规则都有例外,因此,在“是的”旁边确实应当放置一个星号,但是,大部分时间,答案就是 是的 。)
这就是“ 逻辑 常量 性 ”与“ 物理 常量 性 ”的区别。 它意味着, 到底要不要使用 const 来修饰一个方法, 主要 取决 于 该个方法是否会令 逻辑 状态保持 不变, 无论 ( 坐下来了没有? ) ( 妳可能需要坐下了 ) 无论 这个方法 会不会给该个对象的实际物理状态做出任何实际的变动。
如果妳还是没有完全弄懂,或者妳还没有进入痛苦状态,那么,我们再将问题拆开成两部分来看:
•. 如果 一个方法改变了该个对象的逻辑状态中任何部分,那么,在逻辑上讲,它就是一个改变器 (mutator) ; 即使 (实际 发生的情况也 是 如此! )这个方法并未改变该个对象的固有(concrete)状态中的任何物理部分 , 它也不应当被修饰为常量( const )方法。
•. 反过来,一个方法,如果它并不改变该个对象的逻辑状态的任何部分,即使 (实际 发生的情况也是如此! ) 这个方法改变 了该个对象的固有(concrete)状态中的物理部分 ,它在逻辑上来说也是一个查看器(inspector) ,因此应当修饰为常量( const )方法 。
如果妳觉得困惑,那么重新阅读一遍。
如果 妳并不困惑,但是狠生气,那么,狠好: 妳可能不太喜欢这一点,但是妳最少理解了它。 深呼吸,并且跟着我重复念: “ 一个方法的常量性( const ness ),应当 在该个对象的外界产生意义。 ”
如果 妳仍然狠生气,那么,重要 的事情说 三遍: “ 一个方法的常量性( constness ), 只对该个对象的用户产生意义,而那些用户呢,只会看到 该个对象的逻辑状态。 ”
如果 妳仍然生气,那么抱歉, 它就是这样。 闭上嘴,接受它。 确实,是会有例外;任何规则都有例外。但是,作为 一个规则,大体上来说,此处的这个 逻辑常量 性 ( logical const ) 概念,对于妳和妳的软件都是有好处的。
再补充一点。 这么重复来讲可能会显得有点愚蠢,不过,让我们来明确一点,究竟什么 样才算是一个方法改变了该对象的逻辑状态。如果 妳是站在该个类 之外 来看待问题 — 妳是一个普通用户,无论 妳是否首先调用了那个查找方法, 妳能够做出的任何一个实验( 妳调用的任何一个方法或方法序列 )都会返回 相同的结果 (相同 的返回值,相同的异常或没有异常 ) 。如果 该个查找函数改变了未来 任何 一个方法 的 任何 一个行为 ( 不是仅仅让它变得更快了,而是改变了输出,改变了返回值,改变了异常 ),那么, 该个查找方法就改变了该个对象的逻辑状态 — 它是一个改变器。但是,如果 ,该个查找方法仅仅是 让某些东西运行得更快,那么,它是一个查看器。
使用 mutable 关键字(或者 ,作为最后备选项,使用 const_cast ) 。
少数 的查看器需要向对象的物理状态中做出一些外部用户看不到的改变 — 改变 物理状态 而不是逻辑状态 。
例如, 之前讨论 的集合对象 , 对它的最后一次查找结果进行了缓存,以期望提高下次查找 的性能。 在这个示例中,由于这个缓存无法被该个集合对象的任何公有接口( 除了故意计时之外 )观察到,所以 ,它的存在性和状态并不是该对象的逻辑状态的组成部分,所以 ,对它进行的改变,对于外部用户来说是不可见的。 这个查找方法,是一个查看器, 因为 它从不会改变该个对象的逻辑状态,即便 , 按照目前的实现来看,它实际上改变 了这个对象的物理状态 , 也是如此 。
如果某些方法 只改变了物理状态,但未改变逻辑状态,那么 ,一般来说, 这样的方法应当被标记为常量( const )方法,因为 ,它们确实是查看器方法。 这就导致一个问题: 当编译器发现妳的常量( const )方法在尝试改变当前 this 对象的物理状态的时候, 它会报告错误的 — 它会针对 妳的代码输出一个错误消息。
C++编译器使用 mutable 关键字 来帮助妳更好 地使用这个 逻辑常量性 概念。 在这个例子的情况下, 妳就需要使用 mutable 关键字来对缓存字段进行修饰, 这样,编译器就知道, 这个字段是允许在某个常量方法中进行改变,或者通过任何的常量指针或引用来进行改变。 在我们的专业术语中, mutable 关键字 , 将对象的物理状态中那些不属于逻辑状态的部分标记出来了。
mutable 关键字 的位置,应当放置于数据成员声明之前 , 也就是说, 与 const 位于相同的位置。 另一种方法,并不建议的方法,就是, 将 this 指针的常量性给强行去掉(cast away),例如使用 const_cast 关键字 :
Set* self = const_cast <Set*>( this );
// 先阅读下面的 注意事项 !
执行 完这一行代码之后, self 的二进制形式就与 this 完全相同,也就是说, self == this ,但是 self 是一个 Set* ,而不是 const Set* (技术 上说, this 是一个, const Set* const ,但是 ,最右边的那个 const ,与我们现在讨论的东西无关 )。 这就意味着,妳可以使用 self 来修改被 this 指向的那个对象。
注意事项 : 对于 const_cast ,存在 一种极端不太可能出现的错误。仅仅 当 三个罕见的事情同时出现时,才会出现这个问题: 一个数据成员本身应当是可变的( mutable )(例如 上面讨论的那个东西 );编译器 不支持 mutable 关键字,并且/或者程序 员 并未使用它; 一个对象,最初被定义为常量 ( const )( 不同于那种普通的对象,即,本身不是常量对象,但被一个指向常量的指针所指向 ) 。尽管 这种组合太罕见,以致于可能永远不会发生在妳身上,但是,一旦它真的发生了,代码就可能会出错 ( 标准 上说, 其行为是未定义的 ) 。
如果 妳 曾 想要使用 const_cast ,那么应当优先使用 mutable 。 换句话说,如果妳需要改变某个对象中的某个成员,而那个对象又是被一个指向常量的指针所指向,那么, 最安全简单的手段就是, 向该个成员的声明代码中加入 mutable 。如果 妳 确认 那个实际的对象并不是常量 (例如, 妳确认那个对象是像这样声明的: Set s; ) ,那么可以使用 const_cast ,但是 ,如果那个对象本身可能是常量(例如, 它可能是这样声明的: const Set s; ),那么 ,就应该使用 mutable ,不能使用 const_cast 。
请 不要争辩说, 在机器 Z 上,编译器 Y 的 X 版本允许妳改变某个常量对象的某个非可变(non- mutable )成员。 我不在乎这一点 — 根据编程语言 的定义,这样做是违反规则的,因此 ,妳的代码 ,可能在遇到另一个编译器时出错,甚至,遇到同一个编译器的不同版本(升级)时出错。 不要这么做。直接使用 mutable 。应当写出 确保 能正常工作 的代码,而不是 似乎 没出错 的代码。
理论上来说,是的;实际上来说,不是。
即便 是编程语言中完全禁止 了 const_cast , 在那种情况下,唯一 的一个避免 让某个常量成员函数调用破坏 掉寄存器缓存 的手段就是,解决别名问题 ( 也就是说,证明这一点,证明, 并没有任何其它的非常量指针指向了该个对象 ) 。 这个,只在罕见的情况下才是成立的 ( 该个对象是 在该个常量成员函数的调用期间构造的,并且 , 在该个对象的构造时间和该个常量成员函数的调用时间之间的所有非常量成员函数的调用都是静态绑定的,并且 , 这些调用都是内联的( inline d),并且 ,该个构造函数本身也是内联的,并且 ,该个构造函数所调用的每个成员函数都是内联的 ) 。
因为 ,“ const int* p ”的意思是,“ p 承诺 不会改变 *p 本身 , ” 而 不是 说,“ *p 承诺 着完全不改变。 ”
将某个 const int* 指向某个整型变量( int ),并不会导致那个整型变量变成常量( const -ify)。 该个整型变量,确实无法通过此处的这个 const int* 来改变,但是 ,假如 别的地方有某处代码用一个 int* (注意 :没有 const ) 来指向同一个(“别名”)整型变量,那么,那个 int* 就可以用来改变该个整型变量。例如:
void f( const int * p1, int * p2)
{
int i=*p1; // 获取 *p1 的(原始)值
*p2= 7; // 如果 p1 == p2 ,那么,这行代码也会改变 *p1
int j=*p1; // 获取 *p1 的(可能 是新的 )值
if (i!=j){
std::cout<< "*p1 changed, but it didn't change via pointer p1!\n";
assert(p1==p2); // 这是唯一一个可能导致 *p1 变更的原因
}
}
int main()
{
int x= 5;
f(&x,&x); // 这是完全符合规则的 ( 没毛病! )
// ...
}
注意 ,此处的 main() 和 f(const int*,int*) ,可能 是位于不同的编译单元中,并且是在不同的日子编译的。 在那种情况下,编译器没有任何方法来在编译期间探测到这种别名关系。因此 ,我们无法 在编程语言的规则中禁止这种行为。实际 上,我们也不打算制造出这样一条规则,因为 ,总体上来说,这是一个特性, 它使得妳能够让多个指针指向同一个东西。其中某个指针承诺 了不改变底层的那个“东西”, 这只是那个 指针 本身的承诺; 而 不是 那个“东西”做出的承诺。
“ const Fred* p ” ,表示的是,该个 Fred 对象无法通过指针 p 来改变,但是,可能存在其它 的不通过常量( const )来访问到该个对象的途径 (例如,某个别名 的非常量指针,比如, Fred* ) 。例如, 妳有两个指针,“ const Fred* p ”和“ Fred* q ”,它们指向 同一个 Fred 对象(别名), 这样,指针 q 可用来改变该个 Fred 对象,而指针 p 却不可以。
class Fred {
public:
void inspect() const; // 常量成员函数
void mutate(); // 非常量成员函数
};
int main()
{
Fred f;
const Fred*p=&f;
Fred*q=&f;
p->inspect(); // 没毛病:未对 *p 进行改变
p->mutate(); // 错误 :无法通过 p 来改变 *p
q->inspect(); // 没毛病: q 是允许对该对象进行查看的
q->mutate(); // 没毛病: q 是允许对该对象进行改变的
f.inspect(); // 没毛病: f 是允许对该对象进行查看的
f.mutate(); // 没毛病: f 是允许对该对象进行改变的
// ...
}
因为 ,这种转换,是无效的,且危险的: Foo** → const Foo** 。
C++允许进行 这种(安全 的 )转换: Foo* → Foo const* 。但是,如果 妳尝试做这种隐式转换,则会报告错误: Foo** → const Foo** 。
下面说明 ,为何这种行为会导致报告错误。 不过,首先说明一下, 最常见的解决办法是:简单 地将 const Foo** 修改成 const Foo* const* :
class Foo { /* ... */ };
void f(const Foo**p);
void g(const Foo* const*p);
int main()
{
Foo**p= /*...*/;
// ...
f(p); // 错误 : Foo** 向const Foo**的 这种转换是不符合规则的,并且是含义不明确的
g(p); // 正确 : 将 Foo** 转换成 const Foo* const* ,是符合规则且含义明确的
// ...
}
为什么说这种转换是危险的呢: Foo** → const Foo** ?因为 ,它会使得妳能够意外地毫无阻碍地在不做任何额外转换的情况下修改一个常量 ( const ) Foo 对象:
class Foo {
public:
void modify(); // 对此处的 this 对象做出某些修改
};
int main()
{
const Foo x;
Foo*p;
const Foo**q=&p; // q现在指向p ;这是 (幸运 ! ) 一个错误
*q=&x; // p现在指 向 x
p->modify(); // 哎呀 :修改了一个常量( const ) Foo !!
// ...
}
如果 q = &p 那一行是 被允许的,那么, q 就会指向 p 。 下一行, *q = &x ,对 p 本身做出了改变(因为 *q 就是 p ) ,让它指向 x 。 那就是个坏事了,因为, 我们就丢失了 const 修饰 符的作用了: p 是一个 Foo* ,而 x 是一个 const Foo 。 p->modify() 那一行,利用 了 p 具有的对被引用对象进行修改的能力, 这就是真正问题之所在,因为 ,我们最终产生的效果是,修改了一个常量 ( const ) Foo 对象。
类比一下,如果妳让某个罪犯披上合法的外衣,那么,他/她就会利用那个外衣所提供的信用。这是坏事。
欣慰 的是, C++ 会阻止妳这么做: q = &p 这一行, 会被C++编译器标记为编译时错误。 提醒 : 请不要 通过指针转换手段 来绕过这个编译时错误。干脆 就 不要这么写 !
未知美人
金巧巧
Your opinionsHxLauncher: Launch Android applications by voice commands