條款03:儘量使用const

1. 總結

  • const可用於任何做用域內的對象、函數參數、函數返回值、成員函數自身,將這些內容聲明爲const可幫助編譯器偵測出錯誤用法
  • 對於const成員函數,C++編譯器強制要求bitwise constness,但在編寫程序時應該使用"概念上的常量性"
  • const成員函數能夠修改被mutable關鍵字修飾的non-static成員變量
  • 當const和non-const成員函數有着實質等價的函數體時,令non-const版本調用const版本可避免代碼重複,但絕對不能反過來調用

2. const對象

關鍵字const能夠用於如下對象。函數

  • 在class外部修飾global或namespace做用域中的常量
  • 修飾區塊做用域(block scope)中被聲明爲static的對象
  • 修飾class內部的static和non-static成員變量
  • 面對指針,根據「左數右指」口訣,能夠指出指針自身、指針所指數據,或二者都是(或都不是)const

STL迭代器以指針爲根據塑模出來,因此STL迭代器的做用就像個T *指針。post

  • 聲明迭代器爲const就像聲明指針爲const同樣,即聲明一個T *const指針,迭代器自己不可修改,但其指向的數據能夠被修改
  • 若是但願迭代器所指向的數據不可修改,即聲明一個const T *指針,則須要使用const_iterator
std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();        //iter至關於T *const指針
*iter = 10;                                                 //沒問題
++iter;                                                     //錯誤,iter不可修改

std::vector<int>::const_iterator const_iter = vec.begin();  //const_iter至關於const T *指針
*const_iter = 10;                                           //錯誤,*const_iter不可修改
++const_iter;                                               //沒問題

3. const函數返回值和函數參數

const最具威力的用法是面對函數聲明時的應用。在一個函數聲明中,const能夠和函數參數、函數返回值、函數自身(若是是成員函數)產生關聯。
const用於函數參數只需記住一條原則:除非函數體中須要改動參數,不然就將它們聲明爲const
const用於函數返回值,每每能夠下降因使用錯誤而形成的意外,同時又不至於放棄安全性和高效性,舉個例子,看下面有理數類operator *的聲明。this

class Rational { ... };
const Rational operator * (const Rational &lhs, const Rational &rhs);
Rational a, b, c;

(a * b) = c;    //有意錯誤,對兩個數的乘積進行賦值,就比如1 = 2同樣
if (a * b = c)  //無心錯誤,將==漏寫爲=
  • 若是a、b、c都是內置類型,上述代碼直接就是不合法
  • 而對於重載了operator *的class,因爲operator =的存在,若是不對返回值使用const,編譯器就不會報錯
  • 然而,一個良好的用戶自定義類型的特徵是避免它們無故地與內置類型不兼容(見條款18)
  • 所以,將operator *的返回值聲明爲const,就可讓編譯器檢測出這種錯誤用法

4. const成員函數

const成員函數的重要性

將const用於成員函數的目的,是爲了確認該成員函數可做用於const對象身上。const成員函數之因此重要,基於兩個理由。編碼

  • 它們使class接口比較容易被理解,能夠很明顯的得知哪些函數能夠改動對象而哪些函數不能
  • 它們使操做const對象成爲可能

第2條對於編寫高效代碼是個關鍵,由於如條款20所述,改善C++程序效率的一個根本方法是以const引用的方式傳遞對象。
const引用的多是const對象,而const對象只能調用const成員函數。
因此此技術可行的前提是,有const成員函數可用來處理取得的const對象;不然,就算能將const對象傳進來,也沒有辦法去處理它。spa

C++有一個重要特性:兩個成員函數若是隻是常量性不一樣,則能夠構成重載,即便它們的參數類型、參數個數、參數順序都徹底一致。
基於這個特性,再結合上面提到的高效編碼技巧,就能夠得出以下所示的接口設計。設計

class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,因爲const對象內容不容許修改,所以返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        return text[postion];
    }
};

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

TextBlock text;
const TextBlock const_text;

print(text);        //調用char &operator [] ()
print(const_text);  //調用const char &operator [] () const

bitwise constness

C++編譯器要求const成員函數不能更改對象內的任何non-static成員變量,簡單地說就是const成員函數中不能出現對non-static成員變量的賦值操做。
這種要求實質上是不能更改對象內的任何一個bit,所以叫作bitwise constness。
不幸的是,許多const成員函數雖然不徹底具有const性質,但卻能經過C++編譯器的bitwise檢驗,更具體地說,就是:指針

  • "概念上的常量性"來看,一個更改了指針指向數據的成員函數不能算是const成員函數
  • 但若是隻有指針隸屬於對象,那麼稱此函數爲bitwise constness不會引起編譯器異議
class TextBlock
{
private:
    char *pText;
public:
    char &operator [] (std::size_t postion) const
    {
        return pText[postion];
    }
};

const TextBlock text("Hello");  //聲明一個const對象
char *pc = &text[0];            //調用const char &operator []取得一個指針,指向text的數據
*pc = 'J';                      //經過pc指針將text的數據改成了"Jello"

上面這個class將operator []聲明爲const成員函數,但卻返回了一個reference指向對象內部數據,這種作法是錯誤的,條款28對此有深入討論,咱們暫時先忽略它。
從編譯器bitwise constness的角度看,上述代碼不存在任何問題,但你終究仍是改變了const對象的值,這種狀況導出所謂的logical constness。日誌

logical constness

logical constness指的是,const成員函數能夠修改它所處理對象內的某些bits,但前提是用戶察覺不到這種修改。
要想在const成員函數中修改non-static成員變量,須要對這些成員變量使用mutable關鍵字,mutable能夠去除non-static成員變量的bitwise constness約束。code

class CTextBlock
{
private:
    char *pText;
    mutable std::size_t textLength;  //最近一次計算的文本長度
    mutable bool lengthIsValid;      //目前的長度是否有效
public:
    std::size_t length() const;
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }

    return textLength;
}

length()的實現固然不是bitwise constness,由於textLength和lengthIsValid均可能被修改,但這兩個成員變量被修改對於const CTextBlock對象是能夠接受的

5. 在const和non-const成員函數中避免重複

如今咱們對class TextBlock作一些修改,假設operator []不單只是返回一個reference指向某字符,還執行邊界檢查、日誌數據訪問、數據完整性檢驗等工做。

class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,因爲const對象內容不容許修改,所以返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //邊界檢查
        ...         //日誌數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        ...         //邊界檢查
        ...         //日誌數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }
};

operator[]的const和non-const版本中的代碼重複,可能會隨着編譯時間、持續維護、代碼膨脹等因素而成爲使人頭痛的問題。
將重複代碼封裝到一個private函數中,並分別在兩個函數中調用它,不失爲一個解決該問題的好辦法,但依然存在代碼重複,如函數調用、return語句。
真正最好的辦法是:先實現operator []的const版本,而後在non-const版本中調用它。以下示例代碼所示,這種方法有兩個技術要點。

  • 先使用static_cast爲*this添加const屬性
  • 接下來調用const版本成員函數,並使用const_cast去除返回值中的const,最後做爲non-const函數的返回值返回
class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,因爲const對象內容不容許修改,所以返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //邊界檢查
        ...         //日誌數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        const TextBlock &const_this = static_cast<const TextBlock &>(*this);  //將自身從TextBlock &轉換爲const TextBlock &
        return const_cast<char &>(const_this[postion]);                       //調用const版本的operator [],並去除返回值中的const屬性,而後返回
    }
};

注意,千萬不要令const版本調用ono-const版原本避免代碼重複,由於const版本調用non-const版本的惟一方法是去除自身的const屬性,這絕對不是個好事情。

相關文章
相關標籤/搜索