1、term3:Use const whenever possible
const允许你定义一个语义约束,指定一个"不该被改动的”对象。
(1)const 修饰指针,迭代器,函数返回值。
const修饰指针
char greeting[] = "Hello";
char* p = greeting;
const char* p = greeting; //指向字符常量的指针,p指向的字符不能修改
char* const p = greeting; //指向字符的常量指针,p本身的值不能够被修改
const char* const p = greeting; //指向字符常量的常量指针,p指向的字符,p本身都不能被修改
为了方便理解:
在第一个声明中,p 是一个指针,它指向一个 const char。
在第二个声明中,p 是一个 const 指针,它指向一个 char。
在第三个声明中,p 是一个const指针,它指向一个const char。
const修饰迭代器
举个栗子:
std::vector<int> vec //整数类型的std::vector
...
const std::vector<int>::iterator iter = vec.begin();
//定义一个常量迭代器,相当于T* const
//迭代器不得指向不同的东西,但他所指向的东西的值是可以改动的;
*iter = 10; //正确
++iter; //错误
std::vector<int>::const_iterator cIter = vec.begin();
//声明一个常量迭代器,相当于const T*
//迭代器不能修改容器中的元素,但是可以修改迭代器的指向;
*citer = 10; //错误
++citer; //正确
const修饰函数返回值
const int max(int a,int b){
return (a > b ? a:b) ;
}
int c = 7;
max(a,b) = c;
//将一个常量的值,赋值给一个函数调用的返回值是没有意义的;
//const关键字在这里确保了函数不会进行任何可能影响程序状态
//的修改,并且使得函数的输出具有确定性。
(2)const修饰成员函数
首先要明确const在成员函数中的作用:
逻辑常量性(Logical constness):
作用: const 关键字表示该成员函数在逻辑上不会修改对象的状态。这是一种承诺,告诉编译器和其他程序员,调用这个成员函数不会改变对象的内部数据成员。
实现: 在 const 成员函数中,你不能修改非 mutable 数据成员,也不能调用非 const 成员函数。这样确保了对象在逻辑上的不变性。
允许 const 对象调用:
作用: 将 const 关键字应用于成员函数使得可以在常量对象上调用这个函数。如果一个对象被声明为 const,那么只能调用它的 const 成员函数,而不能调用普通的非 const 成员函数。
实现: 在 const 成员函数中,编译器会将 this 指针视为指向常量对象的指针,这意味着在这个函数中不能修改对象的非 mutable 成员。
我举个栗子:
class TextBlock{
public:
...
const char& operator[](std::size_t position)const
{return text[position];}
char& operator[](std::size_t position)
{return text[position];}
private:
std::string text;
};
TextBlock tb("hello");
std::cout << tb[0]; //调用non-const TextBlock::operator[]
TextBlock ctb("world");
std::cout << ctb[0]; //调用const TextBlock::operator[]
在介绍之前先引入两个概念:Bitwise Constness和Logical Constness
Bitwise Constness:
定义: “Bitwise constness” 关注的是对象在二进制表示层面是否保持不变。在这个概念中,如果对象被声明为 const,那么其底层二进制表示不应该发生变化。
实现: 通常通过将成员函数声明为 const 来实现 “bitwise constness”,以确保在 const 对象上调用这些函数时,对象的二进制表示不会发生变化。这样的函数不应修改任何非 mutable 的成员。
Logical Constness:
定义: “Logical constness” 关注的是对象的逻辑状态是否保持不变。在这个概念中,即使对象的物理状态(内部数据成员的值)发生了改变,它在外部的视角下仍然是常量。
实现: 通常通过将成员函数声明为 const,并在这些函数中不修改对象的逻辑状态。这允许在 const 对象上调用这些函数,保持对象在外部视角下的常量性。
区别:
“Bitwise constness” 关注对象底层二进制表示的变化,主要通过成员函数的 const 修饰符来实现,限制对数据成员的修改。
“Logical constness” 关注对象的逻辑状态的变化,同样通过成员函数的 const 修饰符来实现,限制对逻辑状态的修改。
在很多情况下,这两个概念是相关的,因为 “logical constness” 的实现通常涉及不修改对象的物理状态,从而保持 “bitwise constness”。然而,它们的重点略有不同,前者侧重于二进制表示,而后者更关注对象在逻辑上的不变性。
来举个栗子:
class CTextBlock{
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true; //error!!!
}
return textLength;
}
//在const成员函数内不能赋值给textLength和lengthIsValid
用mutable释放掉non-static成员变量的bitwise constness的约束:
class CTextBlock{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true; //这样就是没问题的
}
return textLength;
}
const成员函数不能改变(除static)成员变量的值,同理const对象不可以调用non-const函数。但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址,则不算事const函数,但能通过bitwise测试。
解决方法:使用mutable可以消除non-static成员变量的bitwise constness约束。
举一个其他栗子:
#include <iostream>
using namespace std;
class MyClass{
public:
//带有mutable 关键字的非静态成员变量
mutable int mutableData;
//构造初始化函数 mutableData
Myclass(int data):mutableData(data){}
//const成员函数,逻辑上保持不变性,但允许修改mutableData
int getData() const{
//修改mutableData,不违反const成员函数的规定
mutableData++;
return mutableData
}
}
int main(){
Myclass obj(42);
cout << "Initial data: " <<obj.getData()<<endl;
const Myclass constobj(100);
cout << "Const object data: "<<constObj.getData()<<endl;
return 0;
}
这个函数运行后的输出结果是:
Initial data: 43
Const object data: 101
在const 和non-const成员函数中避免重复
但是很遗憾mutable不能解决所有问题,假设你要做若干操作,同时放进const和non-const的operator[]中,就会发生代码重复的问题。
举个栗子:
class TextBlock{
public:
...
const char& operator[](std::size_t position) const
{
... //边界检验
... //日志记数据访问
... //检验数据完整性
return text[position];
}
char& operator[](std::size_t position)
{
... //边界检验
... //日志记数据访问
... //检验数据完整性
return text[position];
}
private:
std::string text;
};
这段代码的问题很多,代码重复伴随的维护,代码膨胀等问题。最好的方法是只实现一次,但是调用的时候灵活一些。从内容看两个operator[]做的工作一样,但是留下哪个呢,留下non-const的那个,因为不论谁调用non-const operator[],都一定要首先有个non-const对象。所以利用non-const operator[]调用 const是一个避免代码重复的安全做法。
举个栗子:
class TextBlock{
public:
...
const char& operator[](std::size_t position) const //保持不变
{
... //边界检验
... //日志记数据访问
... //检验数据完整性
return text[position];
}
char& operator[](std::size_t position)
{
return
const_cast<char&> //移除第一次转型添加的const
(static_cast<const TextBlock&>(*this)[position]);
//将TextBlock类型转化cosnt TextBlock
//使得能够调用const operator[]
}
...
}
这份代码有2个转型操作,第一次用来为*this 添加const,这就告诉operator[]调用const的版本,第二次从const operator[]的返回值中移除const。
2、总结
书山有路勤为径,学海无涯苦作舟。
3、参考
3.1 《More Effective C++》