前言
C/C++总结文档: c o n s t const const 关键字。该文档总结了C++中const关键字的特点以及其使用规则。
目录
1. const 介绍
-
c
o
n
s
t
const
const 是 C++ 中常用的类型修饰符,被
c
o
n
s
t
const
const 修饰的变量被视为常量,是不能被更改的。但如果只是单纯的把被
c
o
n
s
t
const
const 修饰的变量理解为“常量”是不够准确的,因为“常量”不是一种数据类型,而
c
o
n
s
t
const
const 所修饰的变量也没有被赋予一种新的数据类型。对
c
o
n
s
t
const
const 更准确的理解应该是被
c
o
n
s
t
const
const
所修饰的变量的访问权限会变为“只读”,从而限制外部对变量的更改。
1.1 const 与 #define 的区别
- # d e f i n e define define 的本质是 C/C++ 宏(Macro),属于 预处理指令(pre-precessor directive) 的一种。因此 # d e f i n e define define 语句是在源代码文件进行预处理(对源代码中头文件和宏定义进行分析和替换的阶段)阶段执行的。而 c o n s t const const 的本质的 C/C++ 关键字(keyword),因此 c o n s t const const 语句的执行是在程序编译、运行时执行的。
- 由于 # d e f i n e define define 是 C/C++ 宏,可以被定义在当前程序的任意位置或者被当前程序包含在头文件中,因此 # d e f i n e define define 没有作用域限制。而 c o n s t const const 只能够在他被定义的函数作用域或命名空间中才能够被访问。
- C/C++ 宏在源码中是被当成字符串来处理的,在预处理阶段由编译器在其使用的地方直接进行字符串替换。编译器不会对宏进行相应的类型检查,因此相对 c o n s t const const 而言不会那么安全,而且直接使用字符串替换数值还可能会出现意想不到的错误。
2. const 修饰变量
2.1 const 修饰变量的类型
- c o n s t const const 修饰变量的类型(即 c o n s t const const + 类型),会令类型为常量,也就是这个类型的值为常量。例如:
const int a;
/* a 是一个常量整形变量,a只读 */
const int* a;
/* int* 代表一个指向int类型的指针变量,const修饰int*的意思是指针所指向的是一个常量整型变量, 即 a 的值
* (指针本身)可变,但 *a 的值(实际值)只读。注:也可以理解为 a 是一个 const int 类型的指针。
*/
2.2 const 修饰变量的值
- c o n s t const const 修饰变量本身(即 c o n s t const const + 变量),会令变量的值为常量。例如:
int* const a;
/* a 是一个指向int类型变量的指针变量,const直接修饰 a。即 a 的值(指针本身)只读,但 *a 的值(实际值)
* 可变。
*/
2.3 const 与指针
- 以下是cosnt在普通变量和指针变量中使用的一些常见场景:
int a = 20;
const int* p = &a; // 定义了一个指向常量整型的指针p,即*p的值只读
*p += 1; // 非法,因为p指向的是常量整型类型
a += 1; // 合法,因为a是普通变量
const int a = 20;
int* p = &a; // 非法,因为p是普通指针,*p的值可改。但这样就与a自身的const特性冲突了。
// c++中进制将const地址赋给非const指针。
int a = 20;
int* p = &a;
const int* q = &p; // 合法,但*p的值可改;而q把p这个变量当作int来处理,*q只读
const int** p2;
int* p1;
p2 = &p1; // 非法,因为p2是一个指向常量整型的二级指针,即**p2的值是只读的,但p1
// 是一个普通指针,*p可改,这与p2的const限制冲突了。
3. const 修饰函数
3.1 const 修饰函数参数
- 使用 c o n s t const const 来修饰函数参数常用于修饰指针传参和引用传参的场景,为参数加上 c o n s t const const 修饰意为保护参数不被意外修改的作用。例如:
void Func(const char* str);
- 在上述代码中,函数的实参中指针指向一段内存地址,在调用函数之后,函数会在栈内存中开辟一块临时空间来储存这个指针变量。实参中的指针和函数内的临时指针变量是两个不同的变量,因此他们两者的所处地址也是不同的,但他们所指向内存却是同一块。形参加上 c o n s t const const 修饰后,保护了这一块内存地址不会被意外修改。如果刻意修改的话,编译器会报错。
void Func(A instance);
void Func(const A& instance);
- 在上述代码中, A A A 是一个用户自定义类, i n s t a n c e instance instance 是 A A A 的实例化对象。如果我们采用第一行中的直接传值的方式调用函数,那么函数会在调用的时候在栈内存中创建一个 A A A 的临时对象。在这个过程中会调用 A A A 的拷贝构造函数,在函数返回时还需要调用临时对象的析构函数。这样会大大降低程序的运行性能。
- 为此我们通常会建议对象参数改为使用引用传参,这样函数接收的是一个指向对象的指针。创建一个临时指针的开销要远比创建一个临时对象的开销小。由于函数内对象的引用指向了和实参同一个对象,为了避免在函数内意外地修改了对象(例如double free的问题),对于引用传参也需要使用 c o n s t const const 来保护。
void Func(int x);
- 但不是所有的情况下函数传参都需要使用 c o n s t const const 保护。对于C++内置类型的参数,由于函数内的临时变量与实参是相互独立的,修改临时变量的行为不会意外的修改实参,因此在这种场景下不需要使用 c o n s t const const 来保护。
3.2 const 修饰函数返回值
- 如果函数的返回方式是值传递。由于值传递返回是把函数内部的临时变量赋值给函数外部接收的变量,函数内部变量的 c o n s t const const 性质不会随函数返回传递给外部变量。因此在这种场景下使用 c o n s t const const 是没有意义的。
- 如果函数的返回值是引用,或者是指针。那么经过了 c o n s t const const 修饰后,指针所指向的内容变为只读。需要注意的是,外部接受函数返回值的变量也必须是 c o n s t const const 修饰。例如:
const char* Func();
const char* str = Func();
3.3 const 修饰成员函数
- c o n s t const const 修饰成员函数时是为了保护成员变量不被成员函数修改。声明函数的时候 c o n s t const const 需要放在函数声明的最后,例如:
class MyClass
{
public:
void Func() const;
};
-
c
o
n
s
t
const
const 成员函数有以下几点规则:
- 1) c o n s t const const 对象只能访问 c o n s t const const 成员函数,非 c o n s t const const 的对象可以访问任何成员函数,包括 c o n s t const const 成员函数。
- 2)如果函数名、参数、返回值都相同的 c o n s t const const 成员函数和非 c o n s t const const 成员函数是可以构成重载,那么 c o n s t const const 对象调用 c o n s t const const 成员函数,非 c o n s t const const 对象默认调用非 c o n s t const const 的成员函数。
- 3) c o n s t const const 成员函数可以访问所有成员变量,但是只能访问 c o n s t const const 的成员函数。
- 4)非 c o n s t const const 成员函数,可以访问任何成员,包括 c o n s t const const 成员成员函数。
- 5) c o n s t const const 成员函数不能修改任何的成员变量,除非变量用 m u t a b l e mutable mutable 修饰。
这一点可以理解为,在const的成员函数中成员变量都变成const属性,无法再次修改。如果函数的返回类型为此成员变量的引用,那必须也要加上const修饰。例如:
class MyClass
{
public:
const int & Func() const { return value; }
private:
int value;
};