在成员函数末尾添加const是一种好习惯 - 在适当的地方?

时间:2011-04-29 04:17:48

标签: c++ const const-correctness

在C ++中,在每次函数不修改对象时,在成员函数定义的末尾添加const是一种很好的做法,即,每次函数“合格”时对于const? 我知道在这种情况下有必要:

class MyClass {

public:
int getData() const;
};

void function(const MyClass &m) { int a = m.getData(); dosomething... }

但除此之外,以及const对实际功能的其他用途,向末尾添加const实际上会改变代码的执行方式(更快/更慢),或者它只是编译器处理案例的“标志”如上所述? 换句话说,如果类中的功能不需要const(最后),添加它会有什么不同吗?

7 个答案:

答案 0 :(得分:17)

请参阅Herb Sutter(C ++标准委员会秘书长达10年)excellent article about const correctness

关于优化,他后来写了this article,他说“const主要是针对人类,而不是编译器和优化器”。优化是不可能的,因为“太多事情可能出错...... [你的函数]可能会执行const_casts。”

然而,const正确性是一个好主意,原因有两个:它是一个便宜的(就你的时间而言)断言可以发现错误;并且,它表明函数理论上不应该修改对象的外部状态,这使得代码更容易理解。

答案 1 :(得分:5)

  

每次函数不修改对象时,即每次函数“符合”const的时候?

在我看来,是的。它确保您在涉及对象的const个对象或const表达式上调用此类函数:

void f(const A & a)
{
   a.inspect(); //inspect must be a const member function.
}

即使它修改了一个或几个内部变量一次或两次,即使这样我通常也会使它成为const成员函数。这些变量使用mutable关键字声明:

class A
{
     mutable bool initialized_cache; //mutable is must!

     public:
         void inspect() const //const member function
         {
               if ( !initialized_cache)
               {
                    initialized_cache= true;
                    //todo: initialize cache here
               }
               //other code
         }
};

答案 2 :(得分:3)

是。通常,逻辑const的每个函数都应该是const。唯一的灰色区域是您通过指针修改成员的位置(可以将其设置为const但可以说不应该是const)或者修改用于缓存计算但其他方面没有效果的成员(可以说应该是使用const,但需要使用关键字mutable来执行此操作。)

使用const这个词非常重要的原因是:

  1. 这是其他开发人员的重要文档。开发人员会假设任何标记为const的东西都不会改变对象(这就是为什么在通过指针对象改变状态时使用const可能不是一个好主意),并假设任何未标记为const的东西都会发生变异。

    < / LI>
  2. 这将导致编译器捕获无意的突变(如果标记为const的函数无意中调用非const函数或突变元素,则会导致错误。)

答案 3 :(得分:1)

是的,这是一个很好的做法。

在软件工程级别,它允许您拥有只读对象,例如你可以通过使对象const来防止对象被修改。如果一个对象是const,则只允许在其上调用const函数。

此外,我相信编译器可以进行某些优化,如果他知道只读取对象(例如,在对象的多个实例之间共享公共数据,因为我们知道它们永远不会被修改)。

答案 4 :(得分:1)

'const'系统是C ++真正凌乱的功能之一。它在概念上很简单,用'const'声明的变量被添加成常量并且不能被程序改变,但是,在这种方式中必须用来代替C ++缺少的一个特性,它会变得可怕复杂而令人沮丧的限制。以下尝试解释如何使用'const'以及它为什么存在。在指针和'const'的混合中,指向变量的常量指针对于可以在值中更改但不在内存中移动的存储非常有用,而指针(常量或其他)对于从函数中返回常量字符串和数组非常有用。 ,因为它们是作为指针实现的,否则程序可能会尝试改变和崩溃。在编译过程中,将检测到尝试更改不可更改的值,而不是难以追踪崩溃。

例如,如果返回固定的“Some text”字符串的函数写成

char *Function1()
{ return “Some text”;}

然后程序可能会崩溃,如果它意外地试图改变值

Function1()[1]=’a’;

如果编写了原始函数,编译器会发现错误

 const char *Function1()
 { return "Some text";}

因为编译器会知道该值是不可更改的。 (当然,编译器理论上可以解决这个问题,但C不是那么聪明。) 当使用参数调用子例程或函数时,可以读取作为参数传递的变量以将数据传输到子例程/函数中,写入以将数据传输回调用程序或两者都执行。有些语言允许用户直接指定,例如使用'in:','out:'和&amp; 'inout:'参数类型,而在C中,必须在较低级别工作,并指定传递变量的方法,选择一个也允许所需数据传输方向。

例如,像

这样的子程序
void Subroutine1(int Parameter1)
{ printf("%d",Parameter1);}

接受默认C&amp; C中传递给它的参数。 C ++方式是一个副本。因此子程序可以读取传递给它的变量的值,但不能改变它,因为它只对副本进行了任何改动,并在子程序结束时丢失了

void Subroutine2(int Parameter1)
{ Parameter1=96;}

将保留未调整的变量而不设置为96。

在C ++中为参数名称添加'&amp;'(这是一个非常令人困惑的符号选择,因为'&amp;'在C中其他地方的变量前面生成指针!)就像导致实际变量本身一样而不是副本,用作子程序中的参数,因此可以写入,从而将数据传回子程序。因此

void Subroutine3(int &Parameter1) 
{ Parameter1=96;}

会将调用它的变量设置为96.这种将变量作为自身而不是副本传递的方法在C中称为“引用”。

传递变量的方式是C的C ++添加。要在原始C中传递可变变量,使用指向变量的指针作为参数,然后改变它所指向的内容,这是一个相当复杂的方法。例如

void Subroutine4(int *Parameter1) 
{ *Parameter1=96;}

工作但需要在被调用的例程中每次使用变量,如此改变,调用例程被改变以传递指向变量的指针,这是相当麻烦的。

但'const'在哪里?那么,第二种常见用途是通过引用或指针而不是复制来传递数据。也就是说,复制变量会浪费太多内存或花费太长时间。对于大型复合用户定义的变量类型(C ++中C&amp;'classes'中的'结构'),这种情况尤其可能。所以子例程声明了

void Subroutine4(big_structure_type &Parameter1);

可能正在使用'&amp;',因为它会改变传递给它的变量,或者它可能只是为了节省复制时间,如果函数是在其他人的库中编译的话,就无法分辨它是什么。如果需要信任子程序而不改变变量,这可能是一种风险。

要解决此问题,可以在参数列表中使用'const',如

void Subroutine4(big_structure_type const &Parameter1);

这将导致变量在没有复制的情况下传递,但是在被修改之后将其停止。这很麻烦,因为它本质上是从一个双向变量传递方法创建一个只有变量的传递方法,该方法本身是由一个只有变量的传递方法,只是为了欺骗编译器进行一些优化。

理想情况下,程序员不需要控制这个详细指定变量传递方式的细节,只说明信息的方向,让编译器自动优化它,但C是为远程原始低级编程设计的现在这些计算机必须明确地执行它,因此计算机必须明确地执行它。

答案 5 :(得分:0)

我的理解是它确实只是一面旗帜。但是,那就是说,你想在任何地方添加它。如果您无法添加它,并且代码中的其他功能会执行类似

的操作
void function(const MyClass& foo)
{
   foo.getData();
}

您将遇到问题,因为编译器无法保证getData不会修改foo。

答案 6 :(得分:0)

使成员函数const确保调用具有const个对象的代码仍然可以调用该函数。它是关于这个编译器检查 - 这有助于创建健壮的自我记录代码并避免意外修改对象 - 而不是运行时性能。所以是的,如果函数的性质不需要修改对象的可观察值,你应该总是添加const(它仍然可以修改明确前缀为mutable关键字的成员变量,这是用于一些古怪的用途,如内部缓存和计数器,不影响对象的客户端可见行为。)