谈谈 C++ 中的 const

 

C++ 用关键字 const 标识一个类型不可变. 这其实很容易理解. 不过, 对于 C++ 而言, 简单的概念也有很多可以讨论的. 我们来看一个问题.

问题

我们知道 const 可以用于修饰成员函数, 标识这个函数不能修改这个类的数据. 假设一个类有一个指针类型的成员 T *p, 我们希望通过 get() 方法获取 p 所指向的对象的引用. 如果 get()const 修饰, 它应该返回什么类型, 是 T& 还是 const T& 呢?

class C {
public:
    ??? get() const { return *p; }
private:
    T *p;
};

可能很多同学很自然地认为应该返回 const T&, 因为 get() 不应该改变数据. 的确, 很多类就是这样处理的. 例如标准库的顺序容器都有 front 方法, 返回容器中第一个元素的引用. 如 vector<int>::front()

vector<int> v = {1, 2, 3};
v.front() = 10; // int &

const vector<int> cv = {1, 2, 3};
v.front() = 10; // error: assignment of read-only location. const int &

可以看到非 const 版本返回的是 int&, 而 const 版本返回的是 const int&.

我们看另一个例子. 标准库的迭代器, 例如 vector<int>::iterator, 会重载解引用运算符 operator*(). 那么它的返回类型是什么呢?

vector<int> v = {1, 2, 3};
const auto i = v.begin();
*i = 10; // int &

它返回了 int& 而不是 const int&, 即使这个 operator*() 是 const 版本的.

引用类型, 顶层 const 和底层 const

首先我们知道, C++ 的类型分为值类型引用类型. 对于引用类型而言, 例如指针, 它有两层 const: 顶层 (top-level) const底层 (low-level) const. 顶层 const 表示这个变量本身不可变.

int a, b;
int *const p = &a;
p = &b; // error: assignment of read-only variable
*p = 10; // ok

而底层 const 表示这个变量引用的值不可变.

int a, b;
const int *p = &a;
p = &b; // ok
*p = 10; // error: assignment of read-only location

对变量赋值或初始化时, 顶层 const 可以隐式加上或去除, 底层 const 可以隐式加上, 却不能去除.

int *p;
int *const q = p; // int* -> int *const
p = q; // int *const -> int*

const int *cp;
cp = p; // int* -> const int*
p = cp; // error error: invalid conversion from ‘const int*’ to ‘int*’

如果一个类的成员函数被 const 修饰, 则这个函数的 this 指针是底层 const 的, 也就是 const T *this. 那么通过 this 指针访问到的所有成员, 也就是这个函数能访问到的所有成员, 都是顶层 const 的.

以本文开头的例子, get()const 修饰, get() 中访问到的 p 的类型应该是 T *const p. 编译器并不阻止我们在 const 成员函数里修改指针成员指向的值, 那为什么有些类要禁止修改, 而有些类允许修改呢?

引用类型还是值类型

如果一个类有一个指针类型的成员 T *p, 那么我们在拷贝这个类的对象时, 是复制这个指针本身还是复制指针指向的值呢?

class C {
public:
    C(const C &c) : p(c.p) { } // or
    C(const C &c) : p(new T(*c.p)) {}

    C &operator=(const C &c) {
        if (&c == this) return *this;
        p = c.p;
        return *this;
    } // or
    C &operator=(const C &c) {
        if (&c == this) return *this;
        delete p;
        p = new T(*c.p);
        return *this;
    }

private:
    T *p;
};

C++ 允许开发者控制对象拷贝时的行为. 我们可以仅拷贝指针, 让拷贝前后指向同一个对象; 也可以拷贝指针指向的值, 向用户隐藏这个类存在引用成员这一事实.

当我们拷贝指针指向的值时, 这个类看起来就是个值类型. 例如 std::vector, 它的内存是动态分配的, vector 对象本身只记录指向分配内存的指针. 但是我们在拷贝 vector 时, 会复制其包含的所有对象. 因此对于用户来说它就是个值类型.

既然是值类型, 就只有一层 const, 也就是顶层 const. 因此当一个 vector 是 const 的时候, vector::front() 也应该返回 const 的引用. 类需要负责将顶层的 const 传递到底层.

当我们仅拷贝指针本身时, 这个类看起来就是个引用类型. 例如 vector::iterator, 它包含一个指向 vector 中元素的指针. 当拷贝迭代器时, 仅会拷贝指针本身, 拷贝前后的迭代器指向同一个元素. 因此对于用户来说它就是个引用类型.

既然是引用类型, 就应该区分底层 const 和底层 const. 因此即使迭代器本身是 const 的, operator*() 也不会返回 const 的引用, 因为顶层 const 不会传递到底层. 怎样设置迭代器的底层 const? vector 提供了两个类, vector::iteratorvector::const_iterator. 后者无论迭代器本身是否是 const, operator*() 始终返回 const 的引用, 因为它是底层 const 的.

C++ 很强大

回到本文开头的问题. 标准答案是, 返回 const T& 还是 T& 取决于我们如何定义这个类. 如果 class C 的拷贝控制函数拷贝 (或移动) 了 p 指向的值, 则应当返回 const T&; 如果只是拷贝指针本身, 则应当返回 T&.

更一般地总结一下, 对于包含引用类型成员 (如指针, 智能指针) 的类来说, 如果要将其视为值类型, 则

  • 拷贝控制函数需要拷贝引用类型成员所引用的数据
  • 对于访问所引用数据的方法, 应当提供 const 和非 const 两个版本
class C {
public:
    C(const T &t) : p(new T(t)) {}
    C(const C &c) : p(new T(*c.p)) {}
    ~C() { delete p; }
    C &operator=(const C &c) {
        if (&c == this) return *this;
        delete p;
        p = new T(*c.p);
        return *this;
    }

    T &get() { return *p; }
    const T &get() const { return *p; }
private:
    T *p;
};

反之, 如果将其视为引用类型, 则

  • 拷贝控制函数拷贝引用类型成员本身
  • 应当通过一些方式 (如模版) 设置底层 const
  • 对于访问所引用数据的方法, 只需要 const 版本. 如果是底层非 const, 则允许修改所引用数据.
template <typename T> class C {
public:
    C(T *p) : p(p) {}
    C(const C &c) : p(c.p) {}
    C &operator=(const C &c) {
        if (&c == this) return *this;
        p = c.p;
        return *this;
    }

    T &get() const { return *p; }
private:
    T *p;
};

当然, 如果这个类是诸如某某管理器之类的单例类或不可拷贝的类, 就不需要考虑这么多了, 根据需求处理即可.

与其他语言 (Java, Go, Python) 不同, C++ 的类既可以是值类型, 又可以是引用类型, 这取决于开发者怎样设计. C++ 希望开发者可以像用内置类型一样使用自定义类型, 因此它提供了运算符重载, 拷贝控制等一系列的机制, 这让 C++ 的类很强大, 同时也比较复杂. 这就要求我们能够理解这些概念, 而不是只是记住 const 有哪几种用法.