《Effective C++》Item3:尽量使用const

const的一个奇妙的特性是,他允许你指定一个语义约束,即被const修饰的对象都是不可修改的,而编译器会帮助你在编译时维护这个性质,这能让你更早地发现程序设计过程中的漏洞和缺陷。因此,只要允许,就应该明确地使用const对变量进行限定。

const对象

只要是对象,那么不管它在哪,都可以添加const修饰;对于指针,还可以分别指定指针本身以及指针所指向的对象是否可以修改:

char greeting[] = "hello";
char *p1 = greeting;
const char *p2 = greeting;
const char *const p3 = greeting;

const最具威力的功能是针对函数声明的应用。在一个函数声明中,const可以和函数返回值、函数的各个参数以及函数自身(如果该函数是成员函数的话)产生关联。

令函数返回一个常量值,可以降低因为函数调用者的操作错误而造成程序崩溃的可能性。举个例子,考虑有理数乘法的实现函数:

class Rational { /* ... */ };
const Rational &operator*(const Rational &r1, const Rational &r2) { /*...*/ }

这里为什么要返回const对象呢?因为如果不加这个限定,那么客户端可能会写出这样的代码:

(a * b) = c;

这样做是没有意义的,但是很多时候可能出现类似这样的手误,例如:

if((a * b) = c) {
    //...
}

为返回值添加一个const,就能轻易地避免这样的问题。

const成员函数

const施加于成员函数的目的是表明该函数可以作用于const对象之上。这一类成员函数很重要,这是因为:

  • 不论是对于类的使用者还是编译器而言,知道那些函数不会改变对象是非常重要的。
  • 只能在const对象上调用const成员函数

需要注意的是,两个成员函数如果只是常量属性不同,那么是可以构成重载的。这是一个C++当中非常重要的特性。例如下面的这个类,用于封装一大段文字:

class TextBlock
{
    //...
    const char &operator[](std::size_t index) const { return text[index]; }
    char &operator[](std::size_t index) { return text[index]; }

private:
    std::string text;
};

这个类可能会通过以下方式调用:

void printText(const TextBlock &block)
{
    std::cout << block[0];
}

只要重载operator[]运算符并给出不同的返回结果,就可以令const非constTextBlock对象获得不同的处理。

成员函数如果是const修饰的,那么意味着什么?有两种回答:

  • 二进制常量
  • 逻辑常量

二进制常量指的是成员函数不会修改对象在内存中的任何一个二进制位。然而很多成员函数的做法是:在对象内部维护一个指针或者引用,并在const成员函数中通过这个指针或者引用修改外部对象,这种情况就属于逻辑常量。
例如,CTextBlock类中可能会缓存文本的长度:

class CTextBlock
{
    char *text;
    std::size_t length;
    bool isLengthValid;

    //...
public:
    std::siz_t length() const 
    {
        if(!isLengthValid) {
            length = std::strlen(text);
            isLengthValid = true;
        }
        return length;
    }
};

此时CTextBlocklength()成员函数从直觉上看很显然应该被修饰为const,但是其中却需要修改缓存的长度,这就违反了二进制常量性。解决的方法是使用mutable关键字,它解除了const在某个成员对象上所施加的二进制常量性:

class CTextBlock
{
    char *text;
    mutable std::size_t length;
    mutable bool isLengthValid;

    //...
public:
    std::size_t length() const 
    {
        if(!isLengthValid) {
            length = std::strlen(text);
            isLengthValid = true;
        }
        return length;
    }
};

const非const成员函数中复用代码

mutable很好用,但它同样不是一个万能灵药,至少它无法避免代码重复。 例如,在上面的operator[]函数中,可能不是简单地检查某个字符,还会进行边界检查、日志打印、数据完整性校验等工作。此时,很容易写出以下代码:

class TextBlock
{
    //...
    const char &operator[](std::size_t index) const { 
        //边界检查...
        //打印日志...
        //数据完整性校验...
        return text[index]; 
    }

    char &operator[](std::size_t index) { 
        //边界检查...
        //打印日志...
        //数据完整性校验...
        return text[index]; 
    }

private:
    std::string text;
};

原文中,作者不认为将这些代码抽取到一个私有成员函数中是一个好办法。但我认为尚可。

真正需要做的事情是令其中一个方法直接调用另外一个方法:

class TextBlock
{
    //...
    const char &operator[](std::size_t index) const { 
        //边界检查...
        //打印日志...
        //数据完整性校验...
        return text[index]; 
    }

    char &operator[](std::size_t index) { 
        return const_cast<char&>(
            static_cast<const TextBlock &>(*this)[position];
        );
    }

private:
    std::string text;
};

反向做法:令const成员函数调用非const成员函数是不可取的,因为这可能会违背编译器要求的二进制常量属性。

【注意】:

  • 将某些东西声明为const可以让编译器帮我们更快地检查出问题。const可以作用于对象、函数签名的任何位置、成员函数本体中。
  • 编译器对const对象将强制检查其二进制常量性;但是从编码的角度来看,我们也要注重逻辑常量性。
  • const函数实现和非const函数实现几乎相同时,可以让非const函数调用const函数以复用实现代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值