C++ const的使用
摘要
在C++中,const可以用在很多的场合,本文尝试说明const常用的几种场景,并解释为什么要这样使用。
const的含义
在进行讨论之前,先说明const是个什么东西。
const
其实是一个语义约束,告诉编译器和其他程序员某值应该保持不变。在程序编译的过程中该约束由编译器保证,如果违反了const约束,编译报错。
函数形参数列表
最常使用const
的大概就是在函数的参数列表了吧,如:
int max(const int a, const int b);
通常应该将函数的参数声明为const
,告诉编译器和调用此函数的程序员该值在函数内部不会发生更改,因为函数传参是按值传递的,即使在函数内部修改了该值,调用者也不会知道。除非有足够的理由,否则都应该将参数声明为const
,而对于自定义的对象,应该使用引用传递,而不是值传递,避免发生拷贝。使用引用时,则更需要注意const的使用,如果一个函数的行为不应该更改参数的值,就必须为其添加const
约束,否则一旦在函数实现时不小心更改了该值,该行为就会反应给调用者,此时函数的实际行为与期望的行为不同,如果调用者利用了这个‘特性’,那后续是否应该修复BUG就时另一个问题了。如果函数会修改传递的参数的值,那当然不需要添加const
约束,此时调用者看到函数的声明就可以知道那些值可能会发生变动,那些不会。
另一个好处是,声明为const
可以让函数的适用场景更加广泛。一个non-const的对象可以安全的转换为const,而一个const对象转换为non-const则通常是不安全的,应该禁止了。
声明时:
char greeting[] = "Hello";
char* p = greeting; //指针不是常量,指针指向的值也不是常量
//cp是一个指针,这个指针指向了一个字符(字符指针),这个字符是个常量
const char* cp = greeting; //指针不是常量,指向的值是常量
//pc是一个常量,这个常量是一个指针,这个指针指向一个字符
char* const pc = greeting; //指针是常量,指向的值不是常量
//cpc是个常量,这个常量是个指针,这个指针指向了一个字符,指向的字符是个常量
const char* const cpc = greeting; //指针是常量,指向的值也是常量
当const
配合指针一起使用时,有时会觉得其含义难以理清,但是只要使用一下从右到左进行分析就可以看出其含义。
const
用于函数返回值
令函数返回一个常量值,可以降低因客户错误而造成的意外,而又不至于放弃安全性的高效性
如果函数的返回值是可以进行赋值操作的,如[]
运算符,应该返回引用,但是如果是一个常量的对象的[]
,则应该返回一个常量引用,故应该有两个不同的重载:
const T& operator[](const size_t pos) const;
T& operator[](const size_t pos);
避免客户对常量对象做修改操作
对于函数应该是返回值的情况,应该不允许客户执行赋值操作,这通常会发生不符合预期的结果。
如fun(a) = 10
显然是不应该出现的,此时将函数返回值声明为const
可以有效的防止这种情况的发生。
const
作用于类成员函数时
使用const
声明成员函数时,则该函数可以作用于const对象,表明该函数不会改动对象的内容,且使得操作const
对象成为可能。
当两个成员函数仅仅时‘常量性’不同,也时可以被重载的。以上文提到的[]
为例:
class TextBlock{
private:
std::string text;
public:
explicit TextBlock(const std::string& text) : text(text){}
//operator[] for const object
const char& operator[](const std::size_t pos) const{
std::cout << "const operator[]" << std::endl;
return text[pos];
}
//operator[] for non-const object
char& operator[](const std::size_t pos){
std::cout << "non const operator[] " << std::endl;
return text[pos];
}
};
int main(int, char**) {
TextBlock tb("Hello");
const TextBlock ctb("World");
tb[0]; //调用non-const operator[]
ctb[0]; //调用const operator[]
return 0;
}
执行结果
non const operator[]
const operator[]
而对于
tb[0] = 'a'; //正确
ctb[0] = 'a'; //错误
的编译结果为:
error: assignment of read-only location ‘ctb.TextBlock::operator’
‘bitwise constness’和’logical constness’
在继续讨论之前,需要区分下两个概念:
‘bitwise constness’:成员函数只有在不更改对象的任何成员变量时才被认为时const的
‘logical constness’:成员函数可以更改所处理对象的部分内容,只要不会被客户端侦测到即可
'bitwise constness’存在着问题:如果类里面包含了一个指向其他对象的指针,该指针的内容没有改变,但是指针指向的值发生了改变,此时对象拥有的指针没有改变,即对象的每一位数据都没有发现改变,但是对客户端而言对象确确实实发生了改变。而编译器通常也只能保证’bitwise constness’的成立。
如果部分对象采用懒加载的方式,可能在第一次查询时才会去加载数据,此时需要去更改对象的内容,但在客户端看来该对象应该不变的,即客户端不会侦测到内容的更改,但是实际上却发生了修改。但是编译器会按照’bitwise constness’的约束进行检测,不允许进行修改,此时可以通过mutable
释放掉non-static成员变量的bitwise constness约束。
在const 和 non-const成员函数中避免重复
如上文提到的operator []
操作,包含了const版本和non-const版本,可以发现,两个版本的代码基本是一致的,如果需要边界检验,数据完整性验证等操作,维护两个版本的代码会导致大量的重复代码
一个可行的方案是通过non-const版本的函数通过调用const版本的函数实现:
class TextBlock{
private:
std::string text;
public:
explicit TextBlock(const std::string& text) : text(text){}
//operator[] for const object
const char& operator[](const std::size_t pos) const{
...
...
return text[pos];
}
//operator[] for non-const object
char& operator[](const std::size_t pos){
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[pos]
);
}
};
上述代码使用static_cast<const T&>
为添加const约束,从而调用const版本的operator []
,如果不添加const,则会产生递归调用non-const版本的operator []
的情况
在返回的使用又通过const_cast<T&>
移除const
移除const只能通过const_cast
实现,且大部分情况下都不应该使用。
const成员函数承诺不会改变对象的逻辑状态,而non-const成员函数并不保证这一点,因此从non-const成员函数中调用const成员函数是安全的,但是反过来则是非常危险的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!