当前位置: 首页 > 文档资料 > C++ FAQ Lite >

[18] const正确性

优质
小牛编辑
132浏览
2023-12-01

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指向一个constFred对象——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”是什么意思?

意思是xFred对象的一个别名,但不能通过x来修改Fred对象。

例如,假设Fred类有一个叫做inspect()const成员函数,那么写x.inspect()是可以的。但如果Fred类有一个非const成员函数mutate(),那么写x.mutate()就是个错误(编译器会捕获这种错误;不会在运行时检查;因此const不会降低运行速度)。

18.7 “Fred& const x”有意义吗?

没意义。

为了理解这个声明,需要从右往左读这个声明。因此“Fred& const x”的意思是“x是一个指向Fredconst引用”。但这是多余的,因为引用本来就是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);        ←下标运算符通常成对出现...
 };

当对一个非constMyFredList对象使用下标运算符时,编译器会调用非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();
 }

但是,当对一个constMyFredList对象使用下标运算符时,编译器会调用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去除掉thisconst(但是记着读下面的注意事项)。例如在Set::lookup() const中,可以这么写:

 Set* self = const_cast<Set*>(this);
   // 在这么做之前,记着读下面的**注意事项**

然后,selfthis内容一样(即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变为constint不会通过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!!...
 }

记住:请不要用指针转换绕过这里。别这么做就是了!