博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++之const关键字
阅读量:4299 次
发布时间:2019-05-27

本文共 9482 字,大约阅读时间需要 31 分钟。

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;     // 错误    }private:    const 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 
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 
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 
using namespace std;int main() { const int a = 1; volatile const int b = 2; int *pa = (int *)&a; int *pb = (int *)&b; cout<
<<" "<<*pa<<"|"<<<" "<<*pb<

运行结果:

1 1|2 21 2|3 3

mutable关键字

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

#include 
using namespace std;class A{public: int a; mutable int b; void set(int a, int b)const{// this->a = a; // 报错 const_cast
(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 = 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 
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
(&b); cout<
<<" "<<*pb<

运行结果:

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 
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
(b); int *pd = (int *)&d; cout<
<<" "<<*pa<<"|"<
<<" "<<*pb<<"|"<
<<" "<<*pd<

运行结果:

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

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

#include 
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<
<<" "<<<" "<
<<" "<
<<" "<
<<" "<
<<" "<
<<" "<
<<" "<
<

运行结果:

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 
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 
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)"<
a = a.a; cout<<"void setA(const A &a)"<
b = b; }// void setB(const int b){ // 报错// this->b = b;// } void setC(int a){ cout<<"void setC(int a)"<

运行结果:

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

参考链接

转载地址:http://cqsws.baihongyu.com/

你可能感兴趣的文章
Django 源码阅读:url解析
查看>>
第三轮面试题
查看>>
Docker面试题(一)
查看>>
第四轮面试题
查看>>
第一轮面试题
查看>>
2020-11-18
查看>>
Docker面试题(二)
查看>>
一、redis面试题及答案
查看>>
消息队列2
查看>>
二、spring boot 面试题详解
查看>>
消息列队3
查看>>
spring cloud 面试题总结
查看>>
第二轮面试题
查看>>
2021-04-27
查看>>
SSM 写出乐淘商城
查看>>
高精尖面试题汇总
查看>>
Linux面试题
查看>>
24个MySQL面试题
查看>>
Redis类型面试题
查看>>
模拟面试面试题汇总
查看>>