美文网首页
一文理解const关键字

一文理解const关键字

作者: 乖张的小乌龟 | 来源:发表于2018-12-26 19:32 被阅读32次

    const关键字在c/c++中的作用是限定修饰的变量为不可变的,初始化后将不可更改,任何试图更改const修饰的变量的行为都将导致编译错误。
    const的修饰规则比较复杂,可以参照如下代码理解。

    // NOTE: 以下代码仅为说明使用,在正常编码时,所有局部变量请在声明的同时进行初始化操作。
    const int a;        //编译错误, 常量变量'a'需要初始值设定项
    const int b = 15;   //正确
    int c;
    
    const int *p;       //正确, p为指向 const int 的指针, p可以被修改,指向不同的const int 变量。
    p = &b;             //正确
    int const *q;       //正确, q和q类型一样,没有任何区别
    q = &c;             //正确, q为const int型指针,但是可以指向int型变量,代表不会通过指针q修改c的值。
    *q = 12;            //编译错误, 不能修改p指针所指向的内容。
    
    int * const x;      //编译错误, 常量变量'x'需要初始值设定项,x是常量指针,指向int型的变量,必须初始化且不可更改。
    int * const y = &b; //编译错误, y指向的变量类型应该为int型,而非const int 型,因为代码后续可能通过y指针修改b的值。
    int * const z = &c; //正确, z指针永远指向c当前的地址,不可更改z指针的值。
    
    const int * const u = &b;   //正确, 常量指针u指向常量变量b,不可修改u的值,也不可通过u修改b的值。
    

    当搞不清楚const到底修饰的是指针还是类型的时候,可以根据const*的位置判断:

    1. const*的左边时,修饰的是是类型, const int * pint const * qconst 都是用来修饰int的.
    2. const*的右边时,修饰的是指针, int * const yconst是修饰指针的。

    通过const修饰指针理解了const关键字的含义,下面看看const关键字还可以修饰哪些内容:
    const可以修饰变量,修饰引用变量,修饰函数的参数,修饰函数的返回值,修饰类的成员变量,修饰类的成员函数

    const修饰变量

    int a = 15;
    const int b = a;    //正确,使用变量a的值初始化b,b今后不可被更改。
    const int c = b;    //正确,使用常量变量b的值初始化c, c今后也不可被更改。
    int d = c;          //正确,使用常量变量c的值初始化d.
    
    b = c;              //编译错误,不能修改常量变量b
    c = d;              //编译错误,不能修改常量变量c
    

    const修饰引用

    int a = 27;
    const int &b = a;   //正确,b的类型为const int &, 这行代码翻译下就是:b是a的常量引用,即只可以通过b访问a,而不可以通过b修改a。
    int &c = a;         //正确,c的类型为int &,这行代码翻译下就是:c是a的引用(别名),可以通过c访问a,也可以通过c修改a。
    c = 12;             //正确,a的值变为12,访问b也会得到12
    b = c;              //编译错误,b为常量类型,不可被修改。
    
    const int d = c;    //正确,d为const int 类型,初始化为12,
    const int &e = d;   //正确,e为d的别名。
    int &f = d;         //编译错误,无法使用const int 给 int & 变量赋值。 
                        //因为f没有说自己不会修改变量的值,但是d的值是不可更改的,使用d给f赋值,可能导致d的值在后面被f修改,所以编译不通过。
    
    int &g;             //编译错误,引用类型必须初始化
    const int &h;       //编译错误,引用类型必须初始化
    

    const修饰函数的参数

    理解函数的参数是掌握函数调用中是非常重要的一个环节。
    其中最重要得一个概念就是明白函数调用中使用的是按值传递还是按引用传递

    按值传递(by value)/按引用传递(by reference)

    1. 按值传递的函数参数是原始对象的拷贝,对函数参数的操作和传入参数无关。
    2. 按引用传递的函数参数就是传递过来的对象的别名,对函数参数的操作就是对传入参数进行操作。
      以下是一个按值传递没有得到预期结果的示例:
    #include <iostream>
    void swap(int a, int b)  //a、b为形参
    {
        int temp = a;
        a = b;
        b = temp;
    }
    int mian()
    {
        int x = 1;
        int y = 2;
        swap(x,y);      //x、y为实参
        std::cout << "x = " << x << " y = " << y << std::endl;
    }
    //以上代码执行过程为,
    //  x = 1,
    //  y = 2,
    //  a = x,(临时变量a使用x的值进行初始化,也可以说a是x的一份拷贝)
    //  b = y,(临时变量b使用y的值进行初始化,也可以说b是y的一份拷贝)
    //  temp = a,(temp = 1)
    //  a = b,(a = 2)
    //  b = temp,(b = 1)
    //  函数运行结束,a和b生命周期结束,被析构。
    //  输出 x = 1, y = 2,
    

    以上代码没有得到预期结果的原因是使用了按值传递后,形参实参拷贝,对形参的修改和实参没有任何关系了。
    所以以上代码只是交换了ab的值,xy的值没有发生任何变化。
    修改为按引用传递即可,修改如下:

    void swap(int &a, int &b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    

    const修饰函数参数

    了解了按值传递按引用传递的区别后,下面我们看看const关键字在修饰函数参数时对代码产生的影响。
    下面是一个按引用传递的例子:

    int abs_good(const int& x)
    {
        if (x < 0) { return -x;}
        else { return x;}
    }
    int abs_bad(int& x)
    {
        if (x < 0) { return -x;}
        else { return x;}
    }
    //上面是两个求绝对值的函数。
    int main()
    {
        int a = -15;
        const int b = -7;
    
        int c = abs_good(a);    //c = 15;
        int d = abs_bad(a);     //d = 15;
    
        int e = abs_good(b);    //e = 7;
        int f = abs_bad(b);     //编译错误,因为b为const int,不可被更改。
                               //但是abs_bad函数没有明说自己不会去修改b的值,
                              //编译器便认为abs_bad可能修改b的值(虽然它现在实际上没有修改,但难免以后不会去修改),所以编译不通过。
    }
    

    由上面按引用传递的例子可知,当不需要对函数的参数进行修改时,最好在声明参数时说明,这样可保证调用的最大兼容性,int型参数与const int型参数均可调用。同时代码的可读性与安全性更有保障。

    下面是一个按值传递的示例:

    int abs_good(const int x)
    {
        if (x < 0) { return -x;}
        else { return x;}
    }
    int abs_normal(int x)
    {
        if (x < 0) { return -x;}
        else { return x;}
    }
    //上面是两个求绝对值的函数。
    int main()
    {
        int a = -15;
        const int b = -7;
    
        int c = abs_good(a);    //c = 15;
        int d = abs_normal(a);     //d = 15;
    
        int e = abs_good(b);    //e = 7;
        int f = abs_normal(b);  //f = 7; 正确,abs_normal参数中的x为b的拷贝,对x的修改对b不产生任何影响
    }
    

    由上面按值传递的例子可知,在按值传递过程中,形参实参各自独立。
    按值传递时无需增加const关键字,亦不会影响函数调用的兼容性。
    但是在对形参确实不需要修改时,建议还是增加const关键字,提高代码的可读性,同时让编译器帮我们做检查,确保我们确实没有无意中修改。
    函数参数为指针时,和按引用传递类似,下面简要示例下,自行体会:

    #include <iostream>
    void swap(int* pa, int* pb)
    {
        int temp = *pa;
        *pa = *pb;
        *pb = temp;
    }
    
    int abs_good(const int * x)
    {
        if (*x < 0) { return -*x; }
        else { return *x; }
    }
    
    int main()
    {
        int a = 12;
        int b = -22;
        swap(&a, &b);
        const int c = abs_good(&a);
        std::cout << "a = " << a << " b = " << b << " c = " << c << std::endl;
    }
    

    const修饰函数返回值

    当函数返回值是按值传递时,使用const关键字修饰没有任何效果,只有当函数的返回值是按引用传递时,才有必要使用const关键字。
    在混合是用c/c++编程时,我们经常遇到的一个场景类型如下:

    #include <stdio.h>
    #include <string>
    //一个古老的糟糕接口
    void print_message_bad(char *msg)
    {
        printf("Message[%s]\n", msg);
    }
    
    int main()
    {
        std::string Info = "hello world";
        print_message_bad(Info.c_str());    //编译错误,"const char *" 类型的实参与 "char *" 类型的形参不兼容
        return 0;
    }
    

    以上代码编译不通过的原因是:std::string的成员函数c_str()的返回值是const char *类型的,不可以传递给char *类型的参数。
    其原因是:std::string类型禁止直接修改其指针所指向的内存内容,只能读取其指针所指向的内存中的内容。
    因为std::string类型的size()length()等函数在内容修改时需要相应的进行变更,所以可以使用std::string类型提供的接口,否则会导致std::string行为的异常。

    #include <stdio.h>
    const int kMessageLen = 64;
    const char* get_message(int line)
    {
        char *p = new char[kMessageLen];
        snprintf(p, kMessageLen, "Message line :[%d]", line);
        return p;
    }
    int main()
    {
        int line = 15;
        const char *msg = get_message(line);    //
        printf("%s\n", msg);
        char *msg_error = get_message(line);    //编译错误,原因同上
        return 0;
    }
    

    上面说明了const修饰返回指针的情况,下面展示const修饰返回引用的情况。

    const int& get_const_reference(const int & x)
    {
        return x;
    }
    
    int main()
    {
        int a = 15;
        int b = get_const_reference(a);
        const int c = get_const_reference(b);
        //??? 上面两行可以编译通过么?为什么?
        a = 17;
        b = 18;
        c = 19;
        //??? a b c 的值均为19么?
    
        int &d = get_const_reference(a);
        const int &e = get_const_reference(c);
        //???上面两行可以编译通过么?为什么?
        return 0;
    }
    

    好了,通过上面的例子,应该已经明白了const关键字修饰返回函数返回值的作用。
    这种用法常见于类的成员函数,当希望你可以访问内部数据同时又希望保护内部数据时,可以通过const修饰的函数返回值来完成。

    const修饰类成员变量

    #include <string>
    #include <iostream>
    class Student{
    public:
        explicit Student(std::string name, int age): age_(age), name_(name)
        {}
    
        ~Student() {};
    
        int get_age() { return age_; }
        const std::string& get_name() { return name_; }
    
    private:
        int age_;
        const std::string name_;    //编译错误??? const变量未初始化?
    };
    
    int main()
    {
        Student xiaoMing("xiaoMing", 8);
        Student xiaoHong("xiaoHong", 7);
        std::cout << "name: " << xiaoMing.get_name() << " age: " << xiaoMing.get_age() << std::endl;
        std::cout << "name: " << xiaoHong.get_name() << " age: " << xiaoHong.get_age() << std::endl;
        return 0;
    }
    

    const修饰类的成员变量时,不需要在声明时进行初始化,需要在构造函数中初始化。
    这个规则其实比较容易理解,只有在类对象实例化(构造/存在)的时候,const修饰的类的成员变量才开始实例化(构造/存在)。
    其实在c++11以后可以对类的成员变量在声明的时候赋值,此值可以作为默认值,在构造函数中未提供相应值时,会使用此值。详细信息:参考《深入理解 C++11》
    可以执行以下代码查看:

    // NOTE:此代码在c++98会编译不通过
    #include <string>
    #include <iostream>
    class Student{
    public:
        Student() {}
        Student(std::string name, int age): age_(age), name_(name)
        {}
    
        ~Student() {};
    
        int get_age() { return age_; }
        const std::string& get_name() { return name_; }
    
    private:
        int age_ = 165;
        const std::string name_ = "gua";  
    };
    
    int main()
    {
        Student xiaoMing("xiaoMing", 8);
        Student oldMan;
        std::cout << "name: " << xiaoMing.get_name() << " age: " << xiaoMing.get_age() << std::endl;
        std::cout << "name: " << oldMan.get_name() << " age: " << oldMan.get_age() << std::endl;
        return 0;
    }
    

    下面是muduo网络库中Timestamp中的一个代码片段,注意其中的static const int kMicroSecondsPerSecond = 1000 * 1000;

    class Timestamp :
    {
     public:
      Timestamp()
        : microSecondsSinceEpoch_(0)
      {
      }
    
      // default copy/assignment/dtor are Okay
      
      string toString() const;
      string toFormattedString(bool showMicroseconds = true) const;
    
      bool valid() const { return microSecondsSinceEpoch_ > 0; }
    
      static const int kMicroSecondsPerSecond = 1000 * 1000;    //在c++98和c++11下均可以编译通过。为什么?
    
     private:
      int64_t microSecondsSinceEpoch_;
    };
    
    class A {
        const static int num1; // 声明
        const static int num2 = 13; // 声明和初始化
    };
    const int A::num1 = 12; // 定义并初始化,必须类外初始化
    const int num2;  // 定义
    

    const修饰类成员函数

    const修饰类的成员函数
    在设计类的时候,一个重要原则就是对于不改变数据成员成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:

    (1)有 const 修饰的成员函数(const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
    (2)除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量对象(即使用 const 修饰的类对象)可以调用 const 成员函数,而不能调用非const修饰的函数。
    详细内容可访问:
    C++之const类成员变量,const成员函数

    相关文章

      网友评论

          本文标题:一文理解const关键字

          本文链接:https://www.haomeiwen.com/subject/desklqtx.html