[18] const正确性
FAQs in section [18]:
- [18.1] 什么是“const正确性”?
- [18.2] “const正确性”是如何与普通的类型安全有何联系?
- [18.3] 我应该“尽早”还是“推迟”确定const正确性?
- [18.4] “const Fred* p”是什么意思?
- [18.5] “const Fred p”、“Fred const p”和“const Fred* const p”有什么不同?
- [18.6] “const Fred& x”是什么意思?
- [18.7] “Fred& const x”有意义吗?
- [18.8] “Fred const& x”是什么意思?
- [18.9] “Fred const* x”是什么意思?
- [18.10] 什么是“const成员函数”?
- [18.11] 返回引用的成员函数和const成员函数之间有什么联系?
- [18.12] “const重载”是做什么用的?
- [18.13] 如果我想让一个const成员函数对数据成员做“不可见”的修改,应该怎么办?
- [18.14] const_cast会导致无法优化么?
- [18.15] 当我用const int*指向一个int后,为什么编译器还允许我修改这个int?
- [18.16] “const Fred p”的意思是p不会改变么?
- [18.17] 当把Foo转换成const Foo时为什么会出错?
18.1 什么是“const
正确性”?
这是个好东西。意思是用const
关键字来阻止const
对象被修改。
例如,如果你要编写一个函数f()
,它接收std::string
类型的参数,并且想要对调用者保证不会修改调用者传过来的std::string
参数,可以按以下方法声明f()
void f1(const std::string& s); //传const引用
void f2(const std::string* sptr); //传const指针
void f3(std::string s); //传值
在传const
引用和传const
指针的情况下,任何试图在f()
内部修改std::string
的行为都会在编译时被编译器标记为错误。这完全是在编译时做的,所以使用const
没有运行时的空间或速度损失。在传值时(f3()
),被调用函数获得了调用者std::string
的一份拷贝。也就是说,f3()
可以修改这个拷贝,但返回时这个拷贝会被销毁。尤其是f3()
无法修改调用者的std::string
对象。
举个反例,如果想要编写一个函数g()
,也是接收std::string
,但想要告知调用者g()有可能会修改调用者的std::string
对象。这时,可以按以下方法声明g()
:
void g1(std::string& s); //传非const引用
void g2(std::string* sptr); //传非const指针
在这些函数中省去const
,就是告诉编译器允许(但不强制)它们修改调用者的std::string
对象。因此,这些g()
函数可以把它们的std::string
传递给任何f()
函数,但只有f3()
(通过传值接收参数)能够将其参数传递给g1()
或g2()
。如果f1()
或f2()
需要调用g()
函数,必须给g()
传递一份std::string
的本地拷贝。f1()
或f2()
的参数不能直接传递给g()
函数。例如:
void g1(std::string& s);
void f1(const std::string& s)
{
g1(s); // 编译错误,因为s是const的
std::string localCopy = s;
g1(localCopy); // 正确,因为localCopy不是const的
}
当然,在上面的例子中,任何g1()
所做的修改都会反映到f1()
函数内的localCopy
对象。特别是,通过const
引用传递给f1()
的参数不会被修改。
18.2 “const
正确性”是如何与普通的类型安全有何联系?
将参数声明为const
正是另外一种形式的类型安全。这就好像const std::string
是与std::string
不同的类一样。因为const
变量缺少一些非const
变量所具有的一些变更性操作(例如,可以想象以下,const std::string
没有赋值操作符)。
如果你发现普通的类型安全有助于构建正确的系统(的确有帮助,尤其是对于大型系统来说),你会发现const
正确性也有帮助。
18.3 我应该“尽早”还是“推迟”确定const
正确性?
应该在最最最开始。
事后保证const
正确性会导致一种滚雪球效应:每次你在一个地方添加了const
会需要在四个更多的地方也添加const
。
18.4 “const Fred* p
”是什么意思?
意思是p
是一个指向Fred
类的指针,但不能通过p
来修改Fred
对象(当然p
也可以是NULL
指针)。
例如,假设Fred
类有一个叫做inspect()
的const
成员函数,那么写p->inspect()
是可以的。但如果Fred
类有一个非const
成员函数mutate()
,那么写p->mutate()
就是个错误(编译器会捕获这种错误;不会在运行时测试;因此const
不会降低运行速度)。
18.5 “const Fred* p
”、“Fred* const p
”和“const Fred* const p
”有什么不同?
应该从右往左读指针声明。
const Fred* p
表明p
指向一个const
的Fred
对象——Fred
对象不能通过p
修改。Fred* const p
表明p
是一个指向Fred
对象的const
指针——可以通过p
修改Fred
对象,但不能修改p
本身。cosnt Fred* const p
表明“p
是一个指向const Fred
对象的const
指针”——不能修改p
,也不能通过p
修改Fred
对象。
18.6 “const Fred& x
”是什么意思?
意思是x
是Fred
对象的一个别名,但不能通过x
来修改Fred
对象。
例如,假设Fred
类有一个叫做inspect()
的const
成员函数,那么写x.inspect()
是可以的。但如果Fred
类有一个非const
成员函数mutate()
,那么写x.mutate()
就是个错误(编译器会捕获这种错误;不会在运行时检查;因此const
不会降低运行速度)。
18.7 “Fred& const x
”有意义吗?
没意义。
为了理解这个声明,需要从右往左读这个声明。因此“Fred& const x
”的意思是“x
是一个指向Fred
的const
引用”。但这是多余的,因为引用本来就是const
的。你不能重新绑定一个引用。不管有没有const
,都不行。
换句话说,“Fred& cosnt x
”在功能上与“Fred& x
”是一样的。因为在&
后面加上const
没什么用,因此为了避免迷惑就不应该多此一举。有人可能会认为这里加上const
后指向的Fred
对象就是const
的了,就好像“const Fred& x
”一样。
18.8 “Fred const& x
”是什么意思?
“Fred cosnt& x
”在功能上与const Fred& x
相同。然而,真正的问题是应该用哪一种。
答案:绝没有任何人能够为你所在的机构做决定,除非他们了解你的机构。没有放之四海而皆准的规则。没有对所有机构都“正确”的答案。所以不要让任何人做仓促的选择。“思考(Think)”并非一个四字母的单词。
例如,一些机构看重的是一致性,并且已经有大量代码使用“const Fred&
”了。对于他们来说,不管是否有优点,“Fred const&
”都不是个好选择。还有很多其它的商业环境,一些倾向于“Fred const&
”,其它则倾向于“const Fred&
”。
采用适合你机构中普通维护程序员的写法。不是专家,不是傻瓜,而是维护代码的普通程序员。除非你决定解雇他们并雇佣新人,否则就要确保他们能够理解你的代码。根据实际情况做商业决定,而不是根据其它什么人的假设。
使用“Fred const&
”需要克服一些惯性。现在大多数C++书籍都使用const Fred&
,大多数程序员学C++时接触的就是这种语法,并且仍然这么用。这并非是说const Fred&
一定对你的机构好。但在更改(这种风格)期间,和/或在招收新人时,的确可能会引起一些混乱。一些机构认为用Fred const&
带来的好处更大,其它机构则不这么认为。
另一个警告:如果决定用Fred const&
,确保采取措施使人们不会误写成没意义的“Fred& const x
”。
18.9 “Fred const* x
”是什么意思?
“Fred cosnt* x
”在功能上与const Fred* x
相同。然而,真正的问题是应该用哪一种。
答案:绝没有任何人能够为你所在的机构做决定,除非他们了解你的机构。没有放之四海而皆准的规则。没有对所有机构都“正确”的答案。所以不要让任何人做仓促的选择。“思考(Think)”并非一个四字母的单词。
例如,一些机构看重的是一致性,并且已经有大量代码使用“const Fred*
”了。对于他们来说,不管是否有优点,“Fred const*
”都不是个好选择。还有很多其它的商业环境,一些倾向于“Fred const*
”,其它则倾向于“const Fred*
”。
采用适合你机构中普通维护程序员的写法。不是专家,不是傻瓜,而是维护代码的普通程序员。除非你决定解雇他们并雇佣新人,否则就要确保他们能够理解你的代码。根据实际情况做商业决定,而不是根据其它什么人的假设。
使用“Fred const*
”需要克服一些惯性。现在大多数C++书籍都使用const Fred*
,大多数程序员学C++时接触的就是这种语法,并且仍然这么用。这并非是说const Fred*
一定对你的机构好。但在更改(这种风格)期间,和/或在招收新人时,的确可能会引起一些混乱。一些机构认为用Fred const*
带来的好处更大,其它机构则不这么认为。
另一个警告:如果决定用Fred const*
,确保采取措施使人们不会误写成语义不同但语法相似的“Fred* const x
”。这两者虽然第一眼看上去非常相似,但含义完全不同。
18.10 什么是“const
成员函数”?
是指仅查看(而不改变)对象的成员函数。
const
成员函数会在紧跟函数参数列表的后面跟一个const
关键字。有const
后缀的成员函数被称作“const
成员函数”或者是“查看函数”(inspector)。没有const
后缀的成员函数被称作“非const
函数”或“变更函数”(mutator)。
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
后缀表示不会改变对象的(调用方可见的)抽象状态。这并非保证不改变对象的“底层二进制位”。C++编译器不允许将其解释为“按位”(不变),除非能解决别名问题,而别名问题一般无法解决(即可能存在会修改对象状态的非const
别名)。另外一个对这种别名问题的(重要)认识是:用一根“指向const
对象的指针”并不能保证对象不改变,它只是保证对象不会通过该指针被改变。
18.11 返回引用的成员函数和const
成员函数之间有什么联系?
如果想要从一个查看函数中返回this
对象的引用,那么应该返回指向cosnst
对象的引用,即const X&
。
class Person {
public:
const std::string& name_good() const; ← 正确:调用者不能修改name
std::string& name_evil() const; ← 错误:调用者能够修改name...
};
void myCode(const Person& p) ← 这里保证不会修改Person 对象...
{
p.name_evil() = "Igor"; ← ...但还是修改了!!
}
好消息是当你犯这种错误时,编译器通常能够发现。尤其是如果不小心返回了this
对象的非const
引用,例如上面的Person::name_evil()
,编译器在编译这个成员函数时,通常能够发现并给出一条编译错误。
坏消息是编译器并不能发现所有这种错误:在一些情况下编译器无法产生一条错误消息。
最后:你需要思考,并记住本FAQ所述的原则。如果你通过引用返回的对象在逻辑上是this
对象的一部分,而不管其是否在物理上放在了this
对象内,那么const
方法应该返回const
引用或直接按值返回。(this
对象的“逻辑”部分与对象的“抽象状态”相关。请参阅前一个FAQ。)
18.12 “const
重载”是做什么用的?
当一个查看函数和一个变更函数名字相同,且参数个数与类型也相同时就有用了——即两者的不同之处仅在于一个有const
另一个没有const
。
const
重载的一个常见应用是下标运算符。通常应该尽量使用标准模板容器,例如std::vector
,但有时会需要在自己的类中支持下标运算符。一个经验法则是:下标运算符通常成对出现。
class Fred { ... };
class MyFredList {
public:
const Fred& operator[] (unsigned index) const; ←下标运算符通常成对出现
Fred& operator[] (unsigned index); ←下标运算符通常成对出现...
};
当对一个非const
的MyFredList
对象使用下标运算符时,编译器会调用非const
的下表运算符。因为返回的是一个普通Fred&
,所以能够查看或修改对应的Fred
对象。例如,假设Fred
类有一个查看函数Fred::inspect() const
和一个变更函数Fred::mutate()
:
void f(MyFredList& a) ← MyFredList是非const的
{
// 可以调用不修改a[3]处Fred对象的方法:
Fred x = a[3];
a[3].inspect();
// 可以调用修改a[3]处Fred对象的方法:
Fred y;
a[3] = y;
a[3].mutate();
}
但是,当对一个const
的MyFredList
对象使用下标运算符时,编译器会调用const
的下标运算符。因为会返回const Fred&
,所以可以查看对应的Fred
对象而不能修改它。
void f(const MyFredList& a) ← MyFredList是const的
{
// 可以调用不修改a[3]处Fred对象的方法:
Fred x = a[3];
a[3].inspect();
// 错误(很幸运!):试图改变a[3]出的Fred对象:
Fred y;
a[3] = y; ← 幸运的是编译器在编译时发现了这个错误。
a[3].mutate(); ← 幸运的是编译器在编译时发现了这个错误。
}
在以下FAQ中演示了针对下标运算符和函数调用运算符的const
重载:[13.10], [16.17], [16.18], [16.19]和[35.2]
当然除了下标运算符,其它函数也可以进行const
重载。
18.13 如果我想让一个const
成员函数对数据成员做“不可见”的修改,应该怎么办?
用mutable
(或者实在没办法了,用最后一招const_cast
)
少数查看函数需要对数据成员做适当的修改(例如,一个Set
对象可能想要缓存上一次查看的内容,以便下一次查看时能够提高性能)。这里“适当”的意思是,所做的修改不会从对象的接口上反映到外部(否则该成员函数就应该是一个变更函数,而不是查看函数了)。
这时,需要修改的数据成员应标记为mutable
(把mutable
关键字放在数据成员的声明前;即和const
的位置一样)。这就通知编译器说这个数据成员允许在const
成员函数中被修改。如果编译器不支持mutable
关键字,那么可以通过const_cast
去除掉this
的const
(但是记着读下面的注意事项)。例如在Set::lookup() const
中,可以这么写:
Set* self = const_cast<Set*>(this);
// 在这么做之前,记着读下面的**注意事项**
然后,self
和this
内容一样(即self == this
为真),但self
类型是Set*
而不是const Set*
(技术上来讲,是const Set* const
,不过最右边的const
与这里的问题无关)。因此可以使用self
来修改this
所指向的对象。
注意:const_cast
可能会导致一种非常罕见的错误。这个错误仅在三件很少见的事情同时发生时出现:数据成员本应该是mutable
(例如上面所说的),编译器不支持mutable
,并且对象原本就定义为const
(不是通过一根指向const
对象的指针来访问的普通const
对象)。虽然这种组合非常罕见,甚至永远不会发生,但如果真的发生了,那么这种代码可能就不能正常运行(标准说这种行为是未定义的)。
如果想要用const_cast
,那么应该用mutable
替代。换句话说,如果需要修改一个对象的成员,而又是通过指向const
对象的指针来访问这个对象,那么最安全和最简单的做法就是给该成员的声明前加上mutable
。如果你确信实际对象不是const
的(例如能够确定对象是像这样声明的:Set s;
),那么也可以用const_cast
。但如果对象本身就是const
的(例如可能声明为:const Set s;
),那么就应该用mutable
而不是const_cast
。
请不要告诉我说Y编译器的X版本在Z机器上允许修改const
对象的非mutable
成员。我不管这个——根据标准这是错误的,如果换一个编译器,甚至是同一编译器的不同版本(升级版),你的代码就可能会失败。不要这么做。用mutable
吧。
18.14 const_cast
会导致无法优化么?
在理论上是的;在实际中不会。
即使语言本身禁止了const_cast
,要想在调用const
成员函数时避免读写寄存器的唯一办法是解决别名问题(即要证明没有其它指向该对象的非const
指针)。这只有在极少数情况下才能办到(当在调用const
成员函数时构造对象,所有在构造对象和调用const
成员函数之间调用的非const
成员函数是静态绑定的,并且所有这些调用包括构造函数都是内联的,同时构造函数调用的任何成员函数也要是内联的)。
18.15 当我用const int*
指向一个int
后,为什么编译器还允许我修改这个int
?
因为“const int* p
”意思是“p
保证不会修改*p
”,而不是说“*p
保证不变”。
用const int*
指向一个int
,不会使这个int
变为const
。int
不会通过const int*
被修改,但如果有另外一个int*
(注意没有const
)指向该int
(这就是“别名”),那么这个int*
指针可以用来修改int
。例如:
void f(const int* p1, int* p2)
{
int i = *p1; // 获得*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*)
可能是在不同的编译单元中,并且不是在同一天编译的。因此,编译器就无法在编译时发现别名。因此无法在语言中禁止这种事情。实际上,我们甚至不想添加这样一个规则。因为一般来说,允许很多指针指向同一个对象,这是一个功能。当指针保证说不去修改所指内容时,这只是该指针作出的保证,而不是内容所做的保证。
18.16 “const Fred* p
”的意思是*p
不会改变么?
不是!(这个与int指针的别名问题相关)
“const Fred* p
”意思是不能通过指针p
来修改Fred
,但有可能不经过const
(例如一个非const
指针Fred*
),而是通过其它途径来访问object
。例如,如果有两根指针“const Fred* p
”和“Fred* q
”都指向同一个Fred
对象(别名),那么指针q
可以用来修改Fred
对象,但指针p
不能。
class Fred {
public:
void inspect() const; // const成员函数
void mutate(); // 非const成员函数
};
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来修改对象...
18.17 当把Foo**
转换成const Foo**
时为什么会出错?
因为把Foo**
转换成const Foo**
是非法且危险的。
C++允许Foo*
到const Foo*
的转换(这是安全的)。但如果想要将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!!...
}
记住:请不要用指针转换绕过这里。别这么做就是了!