const 与 volatile 一起并称 CV 限定符,用于指定被声明对象或被命名类型的常量性或易变性。
const 全称 constant,其指定一个约束,告知编译器该变量无法被修改。对于那些明确不发生改变的变量,应尽可能使用 const
,以获得编译器的帮助。
const 普通变量
声明时,const
与 typename
顺序可以互换,并且可以直接初始化常量,之后就不能对常量进行修改了。
const int a; // OK!
int const b = 0; // OK!
int c = 1;
a = c; // ERROR! 无法对 const 进行修改
c = a; // OK!
int const nums[10]; // OK!
const 指针
const 搭配指针使用时,也会出现不同的顺序:
- 底层 const:
const
位于*
之前,是为常量指针,表明指针指向的变量为常量,无法通过指针修改指向的对象; - 顶层 const:
const
位于*
之后,是为指针常量,表明指针为常量,无法修改指针变量本身;
const int foo = 0;
int bar = 1;
int *p = &foo; // ERROR! 常量只能被常量指针指向
int *q = (int*)&foo; // OK!
const int* a = &foo; // OK! 常量指针
int const* b = &bar; // OK! 可以指向非常量,但无法通过 *b = ? 的方式修改变量 bar
int* const d = &bar; // OK! 指针常量
const int* const d = &foo; // OK! 指向常量的常量指针
// *a = 1, d = &foo 这些操作都是编译报错的
上面这段代码其实存在漏洞,即将 foo
的地址强转为 int*
类型并赋值给了 int* q
。这是不安全的,因为 p
并不是常量指针,可以凭借 p
修改指向的变量,如果加上下面这段代码,则会发现一些奇妙的事:
(*q)++;
std::cout << q << " " << *q << "\n"
<< &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 0 1
不难发现,q
与 &foo
为同一个地址,但奇怪的是,*q
与 foo
值不同,并且 foo
竟然与 *(&foo)
的值也产生了差异!
Actually,函数中定义的常量放在内存的栈区,而栈内存是可以通过指针修改的,不过即使修改了这块内存,在程序中仍然看起来没有修改常量,这是因为 C++ 编译器对 const 做了优化——当编译器遇到 foo
的定义时,会在内存的某个位置开辟一张表,然后将 key-value 对 {foo, 0}
写入该表,这就相当于将常量 0 绑定在符号 foo
上,之后每次取数据 foo
时,虽然运行时栈会为 foo
分配内存,但不是取栈中地址对应的值,而会去查这张表,然后用 value: 0 直接代替。
所以尽管指针 p
对地址上的值进行了修改,打印 foo
时我们得到的并不是真正的地址 &foo
上的值。
但并不是所有的常量声明时都是如此,比如将上面的代码略作修改:
// case 1
int bar = 0;
const int foo = bar;
int *q = (int*)&foo;
(*q)++;
std::cout << q << " " << *q << "\n"
<< &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 1 1
// case 2
constexpr int bar = 0;
const int foo = bar;
int *q = (int*)&foo;
(*q)++;
std::cout << q << " " << *q << "\n"
<< &foo << " " << foo << " " << *(&foo);
// output:
// 0x78fe0c 1
// 0x78fe0c 0 1
发现上面两个 case 唯一的区别在于变量 bar
是否为 constexpr,换句话说,就是用于初始化 foo
的变量值是否在编译时可知。如果只有到了运行时才能确定常量 foo
的值,那么编译器并不会写 key-value 进表,而是表现地像 C 语言一样,直接从栈上获取数据;反之,如果在编译时就能确定值(比如上面那个 const int foo = 0;
),编译器就会跟我们上面讨论的一样运作。
所以有些时候改用
constexpr
是更好的选择。
而如果对全局变量进行 const
约束,此时变量分配在静态区,那么无论怎样都无法修改。
const int a;
int main() {
int *p = (int*)&a;
(*p)++; // ERROR!
}
const 引用
修饰引用时,const
只能位于 &
左侧,毕竟引用变量本身一经初始化就无法更改,自带 const 语义。这种情况下,const + 引用均视为常量引用,即引用的变量为常量,无法修改。
const int foo = 1;
int bar = 2;
const int& a = foo; // OK!
const int& b = bar; // OK!
int const& c = foo; // OK!
int& const d = foo; // ERROR!
// a = 1 编译报错
const 函数
const 与函数搭配只有两种情况:
- 修饰形参。此时函数体内无法修改 const 修饰的形参;
- 修饰函数返回值。
const 类成员变量
类中定义常量主要有以下实现方式:
枚举。此时枚举变量相当于静态变量,在编译时可知。
class A { public: enum test { foo, bar }; // static int nums1[test::foo]; int nums2[test::bar]; }; std::cout << A::test::foo; // output: 0
const 修饰。仅用 const 修饰的变量为非静态变量,只有在运行时才能确定值。仅能在构造函数的初始化列表进行初始化,或者直接就地初始化。此后无法再修改。
class A { public: const int foo{0}; int bar[foo]; // ERROR! invalid use of non-static A::foo // A() { foo = 1; } // ERROR! A(): foo(1) {} void test() { foo++; } // ERROR! foo is const };
const 类成员函数
除了普通函数的用法外,类成员函数还可以在函数体前加上 const
修饰符,表明该成员函数不会修改任何非 mutable 关键字修饰的成员变量。此时 this 隐式为 const *
(更严谨地说应该为 const *const
,因为 this 不可修改指向),表明在该函数体内,编译器将该对象视为 const 对象。对于 const 对象,只能调用 const 成员函数,因为非 const 函数无法保证不会修改成员变量。
class Foo {
public:
void show() const {
std::cout << bar;
bar++; // ERROR!
Foo* p = this; // ERROR! this 为 const Foo*
const Foo* q = this; // OK!
}
private:
int bar{1};
};
此外,const
类成员函数亦可与不加 const
的同名成员函数产生不同重载版本。至于调用哪个重载版本,就看调用的对象是否为常量,如果是常量,则调用 const 版本,否则调用非 const 版本。
class Foo {
public:
// 两个重载版本
void show() const { std::cout << "const\n"; }
void show() { std::cout << "non-const\n"; }
};
int main() {
Foo a;
const Foo b;
const Foo* c = &a;
a.show();
b.show();
c->show();
}
// output:
// non-const
// const
// const
与宏定义的区别
宏定义 #define | 常量 const |
---|---|
宏定义,相当于字符替换 | 常量声明 |
预处理器处理 | 编译器处理 |
无类型安全检查 | 有类型安全检查 |
不分配内存 | 要分配内存 |
存储在代码段 | 存储在数据段 |
可通过 #undef 取消 |
不可取消 |