C++之const关键字

来源:互联网 发布:电脑办公软件培训班 编辑:程序博客网 时间:2024/05/12 13:34

const关键字的使用

const是一个C++语言的限定符,它限定一个变量不允许被改变。

  • 修饰常量

用const修饰的变量是不可变的,以下两种定义形式在本质上是一样的:

const int a = 10;int const a = 10;
  • 修饰指针

如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

int a = 10;const int* p = &a;            // 指针指向的内容不能变int const* p = &a;            // 同上int* const p = &a;            // 指针本身不能变const int* const p = &a;      // 两者都不能变int const* const p = &a;      // 同上
  • 修饰引用

以下两种定义形式在本质上是一样的:

int a = 10;const int& b = a;int const& b = a;
  • 修饰函数参数

用const修饰函数参数,传递过来的参数在函数内不可以改变。

void func (const int& n) {     n = 10;        // 编译错误}
  • 修饰函数返回值

用const修饰函数返回值的含义和用const修饰普通变量以及指针的含义基本相同。

const int* func() {  // 返回的指针所指向的内容不能修改    // return p;}
  • 修饰类成员变量

用const修饰的类成员变量,只能在类的构造函数初始化列表中赋值,不能在类构造函数体内赋值。

class A {public:    A(int x) : a(x) { // 正确         //a = x;     // 错误    }privateconst int a;};
  • 修饰类成员函数

用const修饰的类成员函数,在该函数体内不能改变该类对象的任何成员变量, 也不能调用类中任何非const成员函数。

class A {public:    int& getValue() const {        // a = 10;    // 错误        return a;    }private:    int a;            // 非const成员变量};
  • 修饰类对象

用const修饰的类对象,该对象内的任何成员变量都不能被修改。
因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。

class A { public:    void funcA() {}    void funcB() const {}};int main {    const A a;    a.funcB();    // 可以    a.funcA();    // 错误    const A* b = new A();    b->funcB();    // 可以    b->funcA();    // 错误}
  • 在类内重载成员函数
class A {public:    void func() {}    void func() const {}   // 重载};

const常量与#define的区别

  • 编译器处理方式不同。#define宏是在预编译阶段展开(字符替换);内置数据类型const常量是编译运行阶段使用(常量折叠)
  • 类型和安全检查不同。#define宏没有类型,不做任何类型检查,仅仅是进行字符替换;const常量有具体的类型,在编译阶段会执行类型检查
  • 存储方式不同。#define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存;const常量会在内存中分配(栈区或者全局数据区)
  • 另外,定义的const常量只在作用域内有效,而#define宏则是在定义范围内有效,例如可以将类的成员变量定义为const常量,此外,const常量可以是数组、类以及结构体等复杂数据类型,这些都是#define宏无法做到的。

换句话说,宏是字符常量,在预编译完宏替换完成后,该宏名字会消失,所有对宏的引用已经全部被替换为它所对应的值,编译器当然没有必要再维护这个符号。而常量折叠发生的情况是,对const常量的引用全部替换为该常量的值,但是,常量并不会消失,编译器会将其放入到符号表中,同时,会为该常量分配空间。

#include <iostream>using namespace std;int main() {#define DATA 1    const int Data = 2;    int a = DATA;    int b = Data;    return 0;#undef DATA}

在下面的汇编代码可以看到,#define宏与const常量在汇编中都被替换成立即数,与代码一起存于代码区。

(gdb) disassemble /m mainDump of assembler code for function main():4   int main() {   0x00401460 <+0>: push   %ebp   0x00401461 <+1>: mov    %esp,%ebp   0x00401463 <+3>: and    $0xfffffff0,%esp   0x00401466 <+6>: sub    $0x10,%esp   0x00401469 <+9>: call   0x401a20 <__main>5   #define DATA 16       const int Data = 2;=> 0x0040146e <+14>:    movl   $0x2,0xc(%esp)7       int a = DATA;   0x00401476 <+22>:    movl   $0x1,0x8(%esp)8       int b = Data;   0x0040147e <+30>:    movl   $0x2,0x4(%esp)9       return 0;   0x00401486 <+38>:    mov    $0x0,%eax10  #undef DATA11  }   0x0040148b <+43>:   leave   0x0040148c <+44>:    retEnd of assembler dump.

const常量在C与C++中的区别

在C++中,编译器会对内置整型数据类型的const常量进行常量替换,而在C中,则编译器没有做这部分优化,每次都要在const常量所在内存进行读取。

int main(){    const int a = 1;    int b = a;    return 0;}

C版本汇编代码:

(gdb) disassemble /m mainDump of assembler code for function main:6   int main(){   0x00401460 <+0>: push   %ebp   0x00401461 <+1>: mov    %esp,%ebp   0x00401463 <+3>: and    $0xfffffff0,%esp   0x00401466 <+6>: sub    $0x10,%esp   0x00401469 <+9>: call   0x4019a0 <__main>7       const int a = 1;=> 0x0040146e <+14>:    movl   $0x1,0xc(%esp)8       int b = a;   0x00401476 <+22>:    mov    0xc(%esp),%eax   0x0040147a <+26>:    mov    %eax,0x8(%esp)9       return 0;   0x0040147e <+30>:    mov    $0x0,%eax10  }   0x00401483 <+35>:   leave   0x00401484 <+36>:    retEnd of assembler dump.

C++版本汇编代码:

(gdb) disassemble /m mainDump of assembler code for function main():1   int main(){   0x00401460 <+0>: push   %ebp   0x00401461 <+1>: mov    %esp,%ebp   0x00401463 <+3>: and    $0xfffffff0,%esp   0x00401466 <+6>: sub    $0x10,%esp   0x00401469 <+9>: call   0x4019a0 <__main>2       const int a = 1;=> 0x0040146e <+14>:    movl   $0x1,0xc(%esp)3       int b = a;   0x00401476 <+22>:    movl   $0x1,0x8(%esp)4       return 0;   0x0040147e <+30>:    mov    $0x0,%eax5   }   0x00401483 <+35>:   leave   0x00401484 <+36>:    retEnd of assembler dump.

跟const有关的关键字

typedef关键字

通过typedef关键字可以给指针类型定义别名,当通过该别名定义const常量时等价于<数据类型> * const <变量名>=<初始值>

#include <iostream>using namespace std;int main() {    typedef int * A;    int a = 1;    const A pa = &a;//    *(pa++);  // 错误    (*pa)++;    return 0;}

volatile关键字

volatile关键字是一种类型修饰符,用它声明的类型变量表示可能被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。

在下面代码中,const常量b通过volatile声明后,编译器不会对其进行常量替换,每次访问b,都从它所在的内存读取,所以通过指针改变b所在内存的数据,const常量b也会发生改变。

#include <iostream>using namespace std;int main() {    const int a = 1;    volatile const int b = 2;    int *pa = (int *)&a;    int *pb = (int *)&b;    cout<<a<<" "<<*pa<<"|"<<b<<" "<<*pb<<endl;    (*pa)++;(*pb)++;    cout<<a<<" "<<*pa<<"|"<<b<<" "<<*pb<<endl;    return 0;}

运行结果:

1 1|2 21 2|3 3

mutable关键字

在C++中,mutable关键字也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中或者类的实例为const常量。

#include <iostream>using namespace std;class A{public:    int a;    mutable int b;    void set(int a, int b)const{//      this->a = a;  // 报错        const_cast<A *>(this)->a = a;        this->b = b;    }};int main() {    const A a = { 1, 2 };    cout << a.a << " " << a.b << endl;//  a.a = 3;  // 报错    const_cast<A&>(a).a = 3;    a.b = 4;    cout << a.a << " " << a.b << endl;    a.set(5, 6);    cout << a.a << " " << a.b << endl;    return 0;}

运行结果:

1 23 45 6

const的进一步探究

结合代码与汇编指令,浮点型const常量Pi初始化时,编译器并没有将3.14以立即数的形式保存在代码区,而是将其存放在常量区,而且Pi、a与c都是在同一地址获取数据,这与字符串指针的初始化很像。正因为如此,每次访问Pi都需要从内存中读取数据,所以当我们可以通过指针改变Pi的值。在这里,const属性的修改可以通过const_cast运算符与传统转换方式实现。

#include <iostream>using namespace std;#define PI 3.14int main() {    const float Pi = 3.14;    float a = PI;    float b = Pi;    float c = 3.14;    float *pb = const_cast<float *>(&b);    cout<<b<<" "<<*pb<<endl;    (*pb)++;    cout<<b<<" "<<*pb<<endl;    return 0;}

运行结果:

3.14 3.144.14 4.14

部分汇编代码:

(gdb) disassemble /m mainDump of assembler code for function main():···6       const float Pi = 3.14;=> 0x00401476 <+22>:    flds   0x405068   0x0040147c <+28>:    fstps  -0xc(%ebp)7       float a = PI;   0x0040147f <+31>:    flds   0x405068   0x00401485 <+37>:    fstps  -0x10(%ebp)8       float b = Pi;   0x00401488 <+40>:    flds   -0xc(%ebp)   0x0040148b <+43>:    fstps  -0x1c(%ebp)9       float c = 3.14;   0x0040148e <+46>:    flds   0x405068   0x00401494 <+52>:    fstps  -0x14(%ebp)···End of assembler dump.

编译器会对内置整型数据类型const常量进行常量替换,但对于数组、结构体以及类等复杂数据类型,由于编译器不知道如何直接替换,因此必须要访问内存去获取数据。在下面的代码中,我们通过指针操作改变const常量的值,其中,常量a的引用在编译过程中就已经被编译器替换成立即数1并存于代码区,而常量数组b则没有被编译器进行优化,每次都需要从内存中读取数据,所以当我们通过指针改变了b[0]的值,常量数组的值也发生了改变。另外,我们也注意到,当const常量的初始化值为变量时,编译器也不会对其进行常量替换。

#include <iostream>using namespace std;int main() {    const int a = 1;    const int b[] = {2};    int c = 3;    const int d = c;    int *pa = (int *)&a;    int *pb = const_cast<int *>(b);    int *pd = (int *)&d;    cout<<a<<" "<<*pa<<"|"<<b[0]<<" "<<*pb<<"|"<<d<<" "<<*pd<<endl;    (*pa)++;(*pb)++;(*pd)++;    cout<<a<<" "<<*pa<<"|"<<b[0]<<" "<<*pb<<"|"<<d<<" "<<*pd<<endl;    return 0;}

运行结果:

1 1|2 2|3 31 2|3 3|4 4

在下面的代码中,全局常量a与b、局部静态常量d、字符串指针h与i的初始化字符串保存在常量区,数据不可进行修改,如果强制修改,就会出现内存读写错误,与const无关。另外,修改指针pf所指向的内存数据后,字符常量f的输出字符不变是编译器优化的结果,通过pf输出可以看到,此处内存的数据已经被修改。

#include <iostream>using namespace std;const char a = '0';const char b[] = "1";volatile const char c = '2';int main() {    static const char d = '3';    volatile static const char e = '4';    const char f = '5';    const char g[] = "6";    const char *h = "7";    char *i = "8";    char *pa = (char *)&a;    char *pb = (char *)b;    char *pc = (char *)&c;    char *pd = (char *)&d;    char *pe = (char *)&e;    char *pf = (char *)&f;    char *pg = (char *)g;    char *ph = (char *)h;    char *pi = (char *)i;    cout<<a<<" "<<b<<" "<<c<<" "<<d<<" "<<e<<" "<<f<<" "<<g<<" "<<h<<" "<<i<<endl;//    (*pa)++;  // 运行错误//    (*pb)++;  // 运行错误//    (*pc)++;  // 可能运行错误,跟编译器有关//    (*pd)++;  // 运行错误//    (*pe)++;  // 可能运行错误,跟编译器有关    (*pf)++;    (*pg)++;//    (*ph)++;  // 运行错误//    (*pi)++;  // 运行错误    cout<<a<<" "<<b<<" "<<c<<" "<<d<<" "<<e<<" "<<f<<" "<<g<<" "<<h<<" "<<i<<endl;    cout<<*pa<<" "<<*pb<<" "<<*pc<<" "<<*pd<<" "<<*pe<<" "<<*pf<<" "<<*pg<<" "<<*ph<<" "<<*pi<<endl;    return 0;}

运行结果:

0 1 2 3 4 5 6 7 80 1 2 3 4 5 7 7 80 1 2 3 4 6 7 7 8

在下面的代码与汇编中,类A的常量数据成员a在实例定义时初始化,而静态常量数据成员b保存在全局数据区,为所有类A实例所共享,在编译时由编译器进行常量替换,而且,通过类实例访问的b也同样被优化。

#include <iostream>using namespace std;class A{public:    const int a = 1;    static const int b = 2;};int main() {    A a;    int b = a.a;    int c = A::b;    int d = a.b;    return 0;}

汇编代码:

(gdb) disassemble /m mainDump of assembler code for function main():10  int main() {   0x00401460 <+0>: push   %ebp   0x00401461 <+1>: mov    %esp,%ebp   0x00401463 <+3>: and    $0xfffffff0,%esp   0x00401466 <+6>: sub    $0x10,%esp   0x00401469 <+9>: call   0x401a10 <__main>11      A a;=> 0x0040146e <+14>:    movl   $0x1,(%esp)12      int b = a.a;   0x00401475 <+21>:    mov    (%esp),%eax   0x00401478 <+24>:    mov    %eax,0xc(%esp)13      int c = A::b;   0x0040147c <+28>:    movl   $0x2,0x8(%esp)14      int d = a.b;   0x00401484 <+36>:    movl   $0x2,0x4(%esp)15      return 0;   0x0040148c <+44>:    mov    $0x0,%eax16  }   0x00401491 <+49>:   leave   0x00401492 <+50>:    retEnd of assembler dump.

当类成员函数的形参为引用类型时,可以通过对该形参添加const限定重载成员函数,否则在编译时会报重定义错误。对于setA()的调用,我们可以假设对应实参为const常量,此时若不通过指针操作且该函数未被重载,编译时就会报错。而对于setB()的调用,不管形参有没有加const限定,都是将实参的数据复制到形参对其进行初始化,编译器无法区分这两个函数。此外,也可以通过对成员函数加const限定来重载成员函数,此时,若该类的实例的const常量,则只能调用加const限定的成员函数。

#include <iostream>using namespace std;class A{public:    int a;    int b;    int c;    void setA(A &a){        this->a = a.a;        cout<<"void setA(A &a)"<<endl;    }    void setA(const A &a){        this->a = a.a;        cout<<"void setA(const A &a)"<<endl;    }    void setB(int b){        this->b = b;    }//    void setB(const int b){   // 报错//        this->b = b;//    }    void setC(int a){        cout<<"void setC(int a)"<<endl;    }    void setC(int a) const{        cout<<"void setC(int a) const"<<endl;    }};int main() {    A a;    const A b = {1,2,3};    a.setA(a);    a.setA(b);    a.setC(4);    b.setC(5);    return 0;}

运行结果:

void setA(A &a)void setA(const A &a)void setC(int a)void setC(int a) const

参考链接

C++ const关键字总结
C++中的const完全解析
C++常量折叠
C++中const的实现机制深入分析
C++中const、volatile、mutable、explicit的用法
C/C++要点全掌握(五)——mutable、volatile

原创粉丝点击