⚓️作者简介:即将大四的北京某能源高校学生。
座右铭:“九层之台,起于垒土” 。所以学习技术须脚踏实地。
这里推荐一款刷题、模拟面试神器,可助你斩获大厂offer:点我免费刷题、模拟面试
const限定符的用途很广,普通变量、指针与引用、函数与函数参数、类成员变量成员函数都能用const修饰。虽然const能用的地方很多,但是大都万变不离其宗,它的作用也比较单一,今天博仔就带你来详细解析 const。
编写程序过程中,我们有时不希望改变某个变量的值。此时就可以使用关键字 const 对变量的类型加以限定。
直接在普通变量类型声明符前加上 const,可以将声明为 const 类型:
const int a = 0;
这样就把 a 声明成了一个 const 类型的常量,所以我们不能再改变它的值了,所以下面试图改变 a 的语句将会编译报错:
a = 10;
修改局部变量的值:
但是如果 a 是局部变量,就可以通过指针来修改 a 的值:
const int a = 0;
int *p = (int *)&a;
*p = 10;
cout << "a = " << a << endl;
cout << "*p = " << *p << endl;
cout << "p = " << p << endl;
cout << "&p = " << &p << endl;
------------------------------------------------------
out:
a = 0
*p = 10
p = 0x61ff0c
&p = 0x61ff08
程序通过强制类型转换将 a 的地址转换为 int * 类型,并赋值给整型指针 p,然后通过 p 将 a 的值修改为 10。
程序正常运行,但是 a 的值和 *p 的值并不相同,明明已经通过指针改变了地址中的内容,这是为什么呢?难道一个地址能存储两个值?当然不能。
这就是 C++ 中的常量折叠 ,因为常量是在运行时初始化的,编译器对常量进行优化,直接将常量值放在编译器的符号表中,使用常量时直接从符号表中取出常量的值,省去了访存这一步骤。
a 是常量,编译器对 a 在预处理时就进行了替换。a 的地址中的值则被 p 所改变。从 a 的地址与 p 的地址可以看出,a 存储在栈中,所以能对其进行修改。
修改全局变量的值
通过指针修改位于静态存储区的的const变量,语法上没有报错,编译不会出错,一旦运行就会报告异常。因为全局变量存储于静态存储区,静态存储区中的常量只有读权限,不能修改它的值。
const volatile
在局部 const 变量的类型声明符前加上 volatile 关键字可以使用到该常量的地方不会使用对应符号表中的值,而会间接使用栈中的值。
const volatile int a = 0;
int *p = (int *)&a;
*p = 10;
cout << "a = " << a << endl;
cout << "*p = " << *p << endl;
cout << "p = " << p << endl;
cout << "&p = " << &p << endl;
-------------------------------------------------
out:
a = 10
*p = 10
p = 0x61ff0c
&p = 0x61ff08
从上面代码中输出的结果就能看出,所有用到该常量的地方不会替换成了定义时所赋予的值,在运行的时候将会使用通过指针修改后的值。这样就避免了常量折叠的问题。
我们还可以对引用使用 const 限定符,在引用声明的类型声明符前加上 const 就可以声明对const的引用,常量引用不能用来修改它所绑定的对象。
引用绑定到同一种类型,并修改值
直接上例子:
int i = 0;
const int j = 0;
const int &r1 = i;
r1 = 20;
const int &r2 = j;
r2 = 20;
int &r3 = j;
第三行将非常量对象 i 绑定到 const 引用 r1 上,此过程中发生了隐式类型转换,i 的类型为 int,r1 的类型为 const int &, 所以这个过程 i 就从 int 转换为了 const int,所以不能通过 r1 改变 i 的值,但可以直接改变 i 的值。但是 const int 类型不能转换为 int。
可以这样想,一个普通变量,能被修改也可以不被修改,所以可以转换为const类型;一个const类型变量,不能被修改,所以不能转换为普通变量。
第五行将常量对象 j 绑定到 const 引用 r2 上,不能直接改变 j 的值也不能通过常量引用改变 j 的值。
第七行将常量对象绑定到 const 引用 r3 上,报错,不能将常量对象绑定到常量引用上。
绑定到另一种类型,并修改值
直接上例子:
double i= 1.0;
const int &r1 = i;
i = 2.0;
cout << "i = " << i << endl;
cout << "r1 = " << r1 <<endl;
---------------------------------------
out:
i = 2
r1 = 1
上面的代码将 int 型的引用 r1 绑定到 double 型变量 i 上,然后改变 i 的值,我们发现 r1 并没有改变,它的值反而是绑定 i 时 i 的值。这是因为引用变量的类型与被引用对象的类型不同时,中间会有如下操作:
double i = 1.0;
int temp = i;
const int &r1 = temp;
r1 引用的是临时量 temp,而不是 i,所以才会出现上面的情况。
当使用const修饰指针变量时,情况就复杂起来了。const可以放置在不同的地方,因此具有不同的含义。来看下面一个例子:
int age = 39;
const int * p1 = &age;
int const * p2 = &age;
int * const p3 = &age;
const int * const p4 = &age;
二三行是一个意思,表示 p 是指向常量的指针;第四行表示 p 是常量指针;第五行表示 p 是指向常量的常量指针。
上面二三行的赋值同样发生了类型转换,从 int * 转换为 const int *。
指向常量的指针和常量指针
顾名思义:常量指针就是指针本身是常量,指针的值不能改变,也就是指针不能改变指向的对象,所以常量指针必须初始化;指向常量的指针就是指向的变量时常量,被指变量不能被修改。
也可以将两者结合,就有了指向常量的常量指针,其具有指向常量的指针和常量指针的共同性质。
修改指向常量的指针和常量指针
int age2 = 20;
*p1 = 20;
*p3 = 20;
p1 = age2;
p3 = age2;
第二行会报错,因为 p1 是指向常量的指针,不能通过指针修改 age 的值;第五行会报错,因为 p3 是常量指针,只能指向 age,不能指向其他变量。
任意常量对象为顶层const,包括常量指针;指向常量的指针和声明const的引用都为底层const。
我们已经了解了变量中const修饰符的作用,调用函数就会涉及变量参数的问题,那么在形参列表中const形参与非const形参有什么区别呢?
同样,先来看看普通变量:
void fun(const int i){
i = 0;
cout << i << endl;
}
void fun(int i){
i = 0;
cout << i << endl;
}
int main(){
const int i = 1;
fun(i);
return 0;
}
形参的顶层 const 在初始化时会被忽略,所以上面定义的两个函数实际上是一个函数。编译时会出现'void fun(int)' previously defined here
错误。
与 const 指针变量一样,指向常量的指针形参指向的值不能修改;常量指针形参不能指向其他变量;指向常量的常量指针形参指向的值不能被修改,也不能指向其他变量。
#include
using namespace std;
void fun(const int* i){
cout << *i << endl;
}
void fun(int* i){
*i = 0;
cout << *i << endl;
}
int main(){
const int i = 1;
//调用 fun(const int* i),没有 fun(const int* i),则会编译报错,因为没有匹配形参的函数。
fun(&i);
int j = 1;
//调用 fun(int* i),没有 fun(int* i),则会调用 fun(const int* i),此时 j 的值不会被改变
fun(&j);
return 0;
}
p1 指向的值不能修改;p2 不能指向其他变量;p3 指向的值不能被修改,也不能指向其他变量。
此外,形参的底层 const 在初始化时不会被忽略,所以上面的两个函数时不同的函数,即重载函数,上面例子编译并不会报错,若果再加上一个void fun(int *const i)
就会报错,因为这个函数定义里面 i 是顶层 const。
与 const 引用一样,const 引用不会改变被引用变量的值。
#include
using namespace std;
void fun(const int& i){
cout << i << endl;
}
void fun(int& i){
i = 0;
cout << i << endl;
}
int main(){
const int i = 1;
//调用 fun(const int& i),没有 fun(const int& i),则会编译报错,因为没有匹配形参的函数。
fun(i);
int j = 1;
//调用 fun(int& i),没有 fun(int& i),则会调用 fun(const int& i),此时 j 的值不会被改变
fun(j);
return 0;
}
由于 const 引用也是底层 const ,所以上面两个函数是不同的函数,即重载函数,编译并不会报错。
面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证const对象的常量性,编译器须区分区分试图修改类对象与不修改类对象的函数。例如:
const Screen blankScreen;
blankScreen.display(); // 对象的读操作
blankScreen.set(‘*’); // 错误:const类对象不允许修改
C++中的常量对象,以及常量对象的指针或引用都只能调用常量成员函数。
要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const,例如:
class Screen {
public:
char get() const;
};
在类外定义const成员函数时,还必须加上const关键字:
char Screen::get() const {
return screen[cursor];
}
若将成员成员函数声明为const,则该函数不允许修改类的数据成员。例如:
class Screen {
public:
int get_cursor() const {return cursor; }
int set_cursor(int intival) const { cursor = intival; }
};
在上面成员函数的定义中,ok()的定义是合法的,error()的定义则非法。
值得注意的是,把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。例如:
class Name {
public:
void setName(const string &s) const;
char *getName() const;
private:
char *m_sName;
};
void setName(const string &s) const {
m_sName = s.c_str(); // 错误!不能修改m_sName;
for (int i = 0; i < s.size(); ++i)
m_sName[i] = s[i]; // 不是错误的
}
const成员函数可以被具有相同参数列表的非const成员函数重载,例如:
class Screen {
public:
char get(int x,int y);
char get(int x,int y) const;
};
在这种情况下,类对象的常量性决定调用哪个函数。
const Screen cs;
Screen cc2;
char ch = cs.get(0, 0); // 调用const成员函数
ch = cs2.get(0, 0); // 调用非const成员函数
const成员函数不能修改类对象数据成员的深层解析:
调用成员函数时,通过一个名为this
的隐式参数来访问调用该函数的对象成员。例如:
Name bozai;
bozai.setName("bozai");
bozai.getName("BOZAI");
调用setName时隐式传入 this 形参,通过改变 this->m_sName 的值来改变bozai对象的m_sName。
当调用getName时,同样是隐式传入 this 形参,不过此时的 this 被 const 修饰了,所以不能通过 this 修改对象的成员了。
到此 const 限定符的讲解就到此结束了,本文主要讲解了 const 的三个大的应用方面,整体来说还是比较详细的,如果有讲解不到位或有误的地方恳请大家批评与交流。
希望大家多多关注,三连支持。你们的支持是我源源不断创作的动力。