【C++】超详细入门 —— 一文带你搞懂const限定符

const限定符的用途很广,普通变量、指针与引用、函数与函数参数、类成员变量成员函数都能用const修饰。虽然const能用的地方很多,但是大都万变不离其宗,它的作用也比较单一,今天博仔就带你来详细解析 const。

1、变量中的const

编写程序过程中,我们有时不希望改变某个变量的值。此时就可以使用关键字 const 对变量的类型加以限定。

1.1 普通变量

直接在普通变量类型声明符前加上 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

从上面代码中输出的结果就能看出,所有用到该常量的地方不会替换成了定义时所赋予的值,在运行的时候将会使用通过指针修改后的值。这样就避免了常量折叠的问题。

1.2 const 修饰引用

我们还可以对引用使用 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。

:bell:可以这样想,一个普通变量,能被修改也可以不被修改,所以可以转换为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,所以才会出现上面的情况。

1.3 const 修饰指针

当使用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,不能指向其他变量。

1.4 顶层与底层const

任意常量对象为顶层const,包括常量指针;指向常量的指针和声明const的引用都为底层const

2、const 函数形参

我们已经了解了变量中const修饰符的作用,调用函数就会涉及变量参数的问题,那么在形参列表中const形参与非const形参有什么区别呢?

2.1 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 int 实参可以传给 int 形参。
  • 与普通 const 变量一样,第一个 fun 中的形参 i 只可读;第二个function中的 i 则可读可写。

2.2 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;
}

p1 指向的值不能修改;p2 不能指向其他变量;p3 指向的值不能被修改,也不能指向其他变量。

此外,形参的底层 const 在初始化时不会被忽略,所以上面的两个函数时不同的函数,即重载函数,上面例子编译并不会报错,若果再加上一个void fun(int *const i)就会报错,因为这个函数定义里面 i 是顶层 const。

2.3 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 ,所以上面两个函数是不同的函数,即重载函数,编译并不会报错。

3、类常量成员函数

面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证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 的三个大的应用方面,整体来说还是比较详细的,如果有讲解不到位或有误的地方恳请大家批评与交流。

希望大家多多关注,三连支持。你们的支持是我源源不断创作的动力。

全部评论
支持博主总结的很全面
点赞 回复 分享
发布于 2022-08-24 09:06 湖北

相关推荐

工作基本定了,比较满意。这深度学习真是难搞啊,论文看不懂,代码看不懂,不会改,创新点???&nbsp;还有一个月中期,怎么搞,已经抑郁了,没有小论文要求,我真是非常幸运了,不然就我这科研能力…………&nbsp;感觉自己对自己失去信心,无时无刻不在否定自己取得一些成绩都是运气好罢了,真的能顺利毕业吗?严重内耗、无力感爆棚啊…………
AI牛可乐:哎呀,听起来你最近压力好大呀,毕业论文和工作的双重压力一定让你感觉像是背了两座山呢。深度学习确实挺复杂的,就像是要在一片迷雾中找到出路一样。但是,你已经很棒了,工作都定好了,这是很大的成就呢! 论文的事情,别太担心。每个人都有看不懂代码和论文的时候,这是正常的。至于创新点嘛,有时候灵感就像小猫一样,需要一点一点地哄出来的。你可以先试试把你看过的资料整理一下,说不定就能找到新的思路呢。 中期答辩前,你可以列个计划,一步一步来,把大问题分解成小问题,一点一点克服。记得,你不是一个人在战斗,你的导师、同学都是你的战友哦。 至于信心嘛,别否定自己,每个人都会有低谷的时候。你已经取得的成绩,都是你实力的体现。毕业不是问题,只要你不放弃,就一定能顺利走过去的。加油哦,我相信你可以的! 对了,如果你愿意的话,可以点击我的头像,我们私信聊,也许我能帮你出出主意,或者就是简单地聊聊天,分散一下你的注意力也好呀。🐮💪🌟
点赞 评论 收藏
分享
头像
11-21 11:39
四川大学 Java
是红鸢啊:忘了还没结束,还有字节的5k 违约金
点赞 评论 收藏
分享
10-24 13:36
门头沟学院 Java
Zzzzoooo:更新:今天下午有hr联系我去不去客户端,拒了
点赞 评论 收藏
分享
评论
1
3
分享
牛客网
牛客企业服务