51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

谈谈 C++ 中的 const

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

问题 {#问题}

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

|---------------------|------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | hljs c++ class C { public: ??? get() const { return *p; } private: T *p; }; |

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

|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | hljs c++ 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*(). 那么它的返回类型是什么呢?

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

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

引用类型, 顶层 const 和底层 const {#引用类型-顶层-const-和底层-const}

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

|-----------------|----------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | hljs c++ int a, b; int *const p = &a; p = &b; // error: assignment of read-only variable *p = 10; // ok |

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

|-----------------|----------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | hljs c++ int a, b; const int *p = &a; p = &b; // ok *p = 10; // error: assignment of read-only location |

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

|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | hljs c++ 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, 那么我们在拷贝这个类的对象时, 是复制这个指针本身还是复制指针指向的值呢?

|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | hljs c++ 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++ 很强大 {#c-很强大}

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

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

  • 拷贝控制函数需要拷贝引用类型成员所引用的数据
  • 对于访问所引用数据的方法, 应当提供 const 和非 const 两个版本

|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | hljs c++ 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, 则允许修改所引用数据.

|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | hljs c++ 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 有哪几种用法.

赞(0)
未经允许不得转载:工具盒子 » 谈谈 C++ 中的 const