const详解
c++中const
关键字被用来表示一种"常量属性",即变量的值不可被修改。这个关键字也是从C语言中继承来的,但是C语言的const
由于无法保证运行时变量的常量属性,所以C++对此引入了常量表来进行升级;const
应用于指针的用法和特性几乎和C语言一致;同时,C++也针对引用类型提供了const
关键字支持。为了支持OOP,const也可以用来修饰方法。
一、用于普通对象的const
众所周知,C语言中的修饰普通对象的const
仅能维持编译期的值不变性,但是运行时值依然是可变的:
int main(int argc, char *argv)
{
const int a = 0;
a = 10; // (编译期的const保持)编译错误,因为试图修改一个常量。
// (运行时const属性失效)
int *pa = (int *)&a;
*pa = 20; // 正确,此时a的值被修改为20,失去常量属性。
return 0;
}
为了维持运行时的const
语义,c++引入了常量表来记录const对象的名称和值,并将之后所有对const对象的访问修改为访问常量表中的值,以此保证const对象始终如一的常量属性。同时为了保持向下兼容C,C++采取另外一种常量对象的内存分配方式,即:代码中的const常量并不会立刻分配内存,只有当需要该对象的地址时,才为其分配内存,但从不使用该内存。
int main(int argc, char *argv)
{
const int a = 0; // a成为常量,<a, 0>被保存进入了常量表中,但是并未在栈上为a分配内存。
a = 10; // 编译错误,因为试图修改一个常量。
int *pa = (int *)&a; // 此时,需要常量a的地址,因此编译器被迫在栈上为a分配一块内存。
*pa = 20; // 此时,编译器被迫为a分配的内存被写为20,但是编译器不会使用这块内存。
printf("a = %d\n", a); // 但是,每次对a的访问都会去访问常量表,因此a仍然为0,保持了常量属性。
return 0;
}
然而,并非所有的常量值都能在编译期被确定。当只有在运行时才能确定常量的值时,该常量将不会进入常量表,仍然保持C语言的运行时不安全的常量属性:
int situation1()
{
int v = 0;
const int a = v; // 编译期间无法确定a的值,因此a不会进入常量表。
a = 10; // 编译错误,因为试图修改一个常量。
int *pa = (int *)&a; // 取得了真正a变量的地址。
*pa = 20; // 此时a的值被修改为20,失去常量属性。
return 0;
}
int situation2()
{
const volatile int a = 0; // 编译期间无法确定a的值,因此a不会进入常量表。
a = 10; // 编译错误,因为试图修改一个常量。
int *pa = (int *)&a; // 取得了真正a变量的地址。
*pa = 20; // 此时a的值被修改为20,失去常量属性。
return 0;
}
在另外一种情况下,如果赋值号两边的数据类型不同,那么将会产生类型截断
,此时const也会失去常量语义:
int situation3()
{
const long a = 0; // a成为常量,<a, 0>被保存进入了常量表中,但是并未在栈上为a分配内存。
const int b = a; // 发生了类型截断,因此即便编译期可以确定值,也不会进入常量表。
int *pb = (int *)&b; // 取得了真正a变量的地址。
*pb = 20; // 此时b的值被修改为20,失去常量属性。
return 0;
}
二、用于指针的const
当const
用于指针时,其行为和C语言中基本一致。例如,顶层const
表示指针不能再指向其它地址,底层const
保证目前指向的地址中的数据不可被修改:
int main(int argc, char *argv[])
{
const int *a = nullptr; // 底层const,表示地址中的数据不可被修改。
int *const b = nullptr; // 顶层const,表示指针不能指向其它位置。
const int *const c = nullptr; // 顶层和底层const,表示指针不能指向其它位置,
// 并且地址中的数据不可被修改。
return 0;
}
三、用于引用的const
const引用
没有顶层和底层之分,所有的const引用
都是底层的。当针对左值引用使用const
时,其行为如同C语言中的const
一样,会产生一个新的编译期常量:
int main(int argc, char *argv[])
{
const int &a = 0;
a = 10; // 编译错误,因为试图修改一个常量。
int *pa = (int *)&a;
*pa = 20; // 正确,a的值被修改为20.
return 0;
}
右值不能绑定到左值引用上,但是可以绑定到常量左值引用上,这是因为它可以保证部分右值的不可修改属性。同理,从语义上来看,没有必要使用const
修饰右值引用。
四、使用const修饰方法
在类的方法声明中使用const
,则表示这个方法不会导致当前对象的改变;同时,为了保持对象的常量属性,const对象
只能调用const方法
。
class Object
{
int _value;
public:
Object(int value) : _value(value) {}
int get_value() const { return _value; }
void set_value(int value) { _value = value; }
};
int main(int argc, char *argv[])
{
Object o1(10);
o1.get_value(); // 正确, 10
o1.set_value(20); // 正确
const Object o2(10);
o2.get_value(); // 正确, 10
o2.set_value(20); // 错误,const对象只能调用const方法。
return 0;
}