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


    ⚓️作者简介:即将大四的北京某能源高校学生。

    📚座右铭:“九层之台,起于垒土” 。所以学习技术须脚踏实地。

    📖这里推荐一款刷题、模拟面试神器,可助你斩获大厂offer:点我免费刷题、模拟面试


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

    1、变量中的const

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

    1.1 普通变量

    直接在普通变量类型声明符前加上 const,可以将声明为 const 类型:

    const int a = 0;
    
    • 1

    这样就把 a 声明成了一个 const 类型的常量,所以我们不能再改变它的值了,所以下面试图改变 a 的语句将会编译报错:

    a = 10;
    
    • 1

    修改局部变量的值:

    但是如果 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    程序通过强制类型转换将 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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第三行将非常量对象 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码将 int 型的引用 r1 绑定到 double 型变量 i 上,然后改变 i 的值,我们发现 r1 并没有改变,它的值反而是绑定 i 时 i 的值。这是因为引用变量的类型与被引用对象的类型不同时,中间会有如下操作:

    double i = 1.0;
    int temp = i;
    const int &r1 = temp;
    
    • 1
    • 2
    • 3

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二三行是一个意思,表示 p 是指向常量的指针;第四行表示 p 是常量指针;第五行表示 p 是指向常量的常量指针。
    上面二三行的赋值同样发生了类型转换,从 int * 转换为 const int *。

    指向常量的指针和常量指针

    顾名思义:常量指针就是指针本身是常量,指针的值不能改变,也就是指针不能改变指向的对象,所以常量指针必须初始化;指向常量的指针就是指向的变量时常量,被指变量不能被修改。
    也可以将两者结合,就有了指向常量的常量指针,其具有指向常量的指针和常量指针的共同性质。

    修改指向常量的指针和常量指针

    	int age2 = 20;
    	*p1 = 20;
    	*p3 = 20;
    	p1 = age2;
    	p3 = age2;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第二行会报错,因为 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    形参的顶层 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    由于 const 引用也是底层 const ,所以上面两个函数是不同的函数,即重载函数,编译并不会报错。

    3、类常量成员函数

    面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证const对象的常量性,编译器须区分区分试图修改类对象与不修改类对象的函数。例如:

    const Screen blankScreen;
    blankScreen.display();   // 对象的读操作
    blankScreen.set(*);    // 错误:const类对象不允许修改
    
    • 1
    • 2
    • 3

    C++中的常量对象,以及常量对象的指针或引用都只能调用常量成员函数。

    要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const,例如:

    class Screen {
    public:
       char get() const;
    };
    
    • 1
    • 2
    • 3
    • 4

    在类外定义const成员函数时,还必须加上const关键字:

    char Screen::get() const {
       return screen[cursor];
    }
    
    • 1
    • 2
    • 3

    若将成员成员函数声明为const,则该函数不允许修改类的数据成员。例如:

    class Screen {
    public:
        int get_cursor() const {return cursor; }
        int set_cursor(int intival) const { cursor = intival; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在上面成员函数的定义中,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];    // 不是错误的
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    const成员函数可以被具有相同参数列表的非const成员函数重载,例如:

    class Screen {
    public:
        char get(int x,int y);
        char get(int x,int y) const;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这种情况下,类对象的常量性决定调用哪个函数。

    const Screen cs;
    Screen cc2;
    char ch = cs.get(0, 0);  // 调用const成员函数
    ch = cs2.get(0, 0);     // 调用非const成员函数
    
    • 1
    • 2
    • 3
    • 4

    const成员函数不能修改类对象数据成员的深层解析:

    调用成员函数时,通过一个名为this的隐式参数来访问调用该函数的对象成员。例如:

    Name bozai;
    bozai.setName("bozai");
    bozai.getName("BOZAI");
    
    • 1
    • 2
    • 3

    调用setName时隐式传入 this 形参,通过改变 this->m_sName 的值来改变bozai对象的m_sName。

    当调用getName时,同样是隐式传入 this 形参,不过此时的 this 被 const 修饰了,所以不能通过 this 修改对象的成员了。

    🎉到此 const 限定符的讲解就到此结束了,本文主要讲解了 const 的三个大的应用方面,整体来说还是比较详细的,如果有讲解不到位或有误的地方恳请大家批评与交流。

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

  • 相关阅读:
    Mysql出现问题:慢查询日志失效解决方案
    python实现Map+函数式接口替换多级if-else结构
    联邦学习框架和数据隐私综述
    企业防护DDoS的注意事项,你知道几个?
    手机备忘录如何批量导出来,备忘录整体导出方法介绍
    微信小程序图表的引入并解决van-tab切换时不显示图表的问题
    AI框架的未来将何去何从,跟着MindSpore一起探讨其趋势
    软件性能测试方法有哪些?性能测试报告需要多少钱?
    Mac m1 上编译阿里 OSS c++ 供 UE 使用
    【C语言 | 数组】C语言数组详解(经典,超详细)
  • 原文地址:https://blog.csdn.net/weixin_45773137/article/details/126297568