C++中const、volatile、mutable用法小结

来源:互联网 发布:信永中和最新工资算法 编辑:程序博客网 时间:2024/06/06 19:23

首先:总结一下有关const 的常见用法

1. const修饰普通变量和指针

  const修饰变量,一般有两种写法:
  const TYPE value;
  TYPE const value;

这两种写法在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。对于一个非指针的类型TYPE,无论怎么写,都是一个含义,即value值不可变。 例如:
    const int nValue;    //nValue是const
    int const nValue;    //nValue是const

但是对于指针类型的TYPE,不同的写法会有不同情况:
l.  指针本身是常量不可变,指针不能再指向其他变量,但是可以改变指针指向的变量的内容
   (char*) const pContent;
lI. 指针所指向的内容是常量不可变,但是指针本身是变量,可以指向其他变量
    const (char) *pContent;
    (char)const *pContent;
lII.  两者都不可变,即指针本身是常量,指针所指向的变量的内容也是常量
    const char* const pContent;

识别const到底是修饰指针还是指针所指的对象,还有一个较为简便的方法,也就是沿着*号划一条线:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

2. const修饰引用,称为const引用,表明不能修改引用所引用的变量的值

    const int &ref = x;

    int &ref2 = x; //Error, ref2不是常量,可以修改引用所指向的变量x的值,而x实际上是常量,不允许修改,故不合逻辑,这种用法是错误的

3.const修饰函数参数

const修饰函数参数是它最广泛的一种用途,它表示在函数体中不能修改参数的值(包括参数本身的值或者参数其中包含的值)
void function(const int Var);     //传递过来的参数在函数内不可以改变(无意义,该函数以传值的方式调用)
void function(const char* Var);   //参数指针所指内容为常量不可变
void function(char* const Var);   //参数指针本身为常量不可变(也无意义,var本身也是通过传值的形式赋值的)
void function(const Class& Var); //引用参数在函数内不可以改变

参数const通常用于参数为指针或引用的情况,若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。

4.const修饰类对象/对象指针/对象引用

const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。

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

例如:
class AAA
{
   void func1();
   void func2() const;
}

const AAA aObj;
aObj.func1(); 错误
aObj.func2(); 正确

const AAA* aObj = new AAA();

aObj->func1(); 错误

aObj->func2(); 正确

5.const修饰数据成员

const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么,例如:
class A
{
    const int size = 100; //错误
    int array[size];       //错误,未知的size
}
const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,可以用类中的枚举常量来实现,例如:
class A
{

  enum {size1=100, size2 = 200 };
  int array1[size1];
  int array2[size2];


}
枚举常量不会占用对象的存储空间,他们在编译时被全部求值。但是枚举常量的隐含数据类型是整数,其最大值有限,且不能表示浮点数。

6.const修饰成员函数

const修饰类的成员函数,用const修饰的成员函数不能改变对象的成员变量。一般把const写在成员函数的最后
class A
{
  …
void function()const; //常成员函数, 它不改变对象的成员变量. 也不能调用类中任何非const成员函数。
}

对于const类对象/指针/引用,只能调用类的const成员函数。

7.const修饰成员函数的返回值

(1)、一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回const对象,或返回const对象的引用,则返回值具有const属性,返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。

(2)、如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针所指的内容)不能被修改,该返回值只能被赋给加const 修饰的同类型指针:
const char * GetString(void);
如下语句将出现编译错误:
char *str=GetString();
正确的用法是:
const char *str=GetString();

(3)、函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赙值函数中,目的是为了实现链式表达。如:
class A
{

    A &operate= (const A &other); //赋值函数
}
A a,b,c; //a,b,c为A的对象

a=b=c;   //正常
(a=B)=c; //不正常,但是合法
若赋值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。

const常量与define宏定义的区别

I.  编译器处理方式不同

define宏是在预处理阶段展开。

const常量是编译运行阶段使用。

II.  类型和安全检查不同

define宏没有类型,不做任何类型检查,仅仅是展开。

const常量有具体的类型,在编译阶段会执行类型检查。

III.  存储方式不同

define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存

const常量会在内存中分配(可以是堆中也可以是栈中)。

————————————————————————————————————————————————————————————————————————————

其次:介绍有关 volatile关键字的用法

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

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

volatile int i=10;

int a = i;

..........//其他代码,并未明确告诉编译器,对i进行过操作

int b = i;


volatile 指出i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响。首先用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:

#include <stdio.h>

void main()

{

int i=10;
int a = i;
printf("i= %d/n",a);

//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道

__asm {
  mov dword ptr [ebp-4], 20h
}

int b = i;
printf("i= %d/n",b);
}

然后,在调试版本模式运行程序,输出结果如下:
i = 10
i = 32

然后,在release版本模式运行程序,输出结果如下:
i = 10
i = 10

输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。下面,我们把 i的声明加上volatile关键字,看看有什么变化:

#include <stdio.h>

void main()

{
volatile int i=10; //表示让编译器不对其进行优化
int a = i;
printf("i= %d/n",a);

__asm {

  mov dword ptr [ebp-4], 20h

}
 
int b = i;
printf("i= %d/n",b);
}

分别在调试版本和release版本运行程序,输出都是:
i = 10
i = 32

这说明这个关键字发挥了它的作用!

关于volatile的补充信息:


一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

    1). 并行设备的硬件寄存器(如:状态寄存器)

    2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

    3). 多线程应用中被几个任务共享的变量

    我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile的重要性:

    1). 一个参数既可以是const还可以是volatile吗?解释为什么。

    2). 一个指针可以是volatile 吗?解释为什么。

    3). 下面的函数有什么错误:

         int square(volatile int *ptr)
         {
              return *ptr * *ptr;
         }

    下面是答案:

    1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

    2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

    3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

    int square(volatile int *ptr)
    {
         int a,b;
         a = *ptr;
         b = *ptr;
         return a * b;
     }
    由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
     long square(volatile int *ptr)
    {
            int a;
            a = *ptr;
            return a * a;
     }

————————————————————————————————————————————————————————————————————————————

最后:介绍有关 mutable关键字的用法

mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能用于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。

我们知道,假如类的成员函数不会改变对象的状态,那么这个成员函数一般会声明为const。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。下面是一个小例子:

#include <iostream>using namespace std;class Test{public:    Test(int x):x_(x), outputTimes_(0)    {    }    //因为Getx()这个成员函数不会去更改x数据成员,所以用const修饰,称之为const成员函数,它不能去修改数据成员的值,即不会修改类的状态    int Getx() const      {        //x_ = 100 ;// Error, const成员函数不能修改数据成员的值        cout << "const Getx..."<< endl;        return x_;    }    //非const成员函数与上面const成员函数构成重载    int Getx()  //非const与const成员函数是可以构成重载的    {        cout << "Getx..."<<endl;        return x_;    }    int Setx()    {        return x_;    }    void Output() const     {        //x_ = 102; //Error,不能修改非mutable数据成员        cout << "x = "<< x_ << endl;        outputTimes_++; //可以修改mutable类型的数据成员    }        int GetOutputTimes() const    {        return outputTimes_;    }private:    int x_;    mutable int outputTimes_; //mutable数据成员,可以突破const限制,在const成员函数中也被修改    };int main(){    //const对象,对象是常量,对象的状态不能更改,声明时要初始化    const Test t(10);    t.Getx(); //前提:若int Getx() const注释掉,则const对象是不能调用非const的Getx()成员函数的       //t.Setx(); //Error,理由同上,即const对象只能调用const成员函数,理由是:const对象是常量,对象的状态是不允许更改的,而非const即普通成员函数可以更改对象的状态,这所以为了实现不允许修改对象的状态这一目的,const对象禁止调用非const成员函数,而只能调用const成员函数,因为const成员函数也是禁止修改数据成员的,符合这一目的。    Test t2(20);    t2.Getx(); //调用的是非const的Getx,因为t2是普通对象,不是const对象    t2.Output(); //普通对象可以调用const成员函数,如果有重载的非const成员函数,则调用的是非const的成员函数    t2.Output(); //每输出一次,outputTimes_加1,因为它是mutable类型的数据成员,允许在const成员函数中被修改    cout << "outputTimes= "<< t2.GetOutputTimes() << endl;     return 0;}

运行结果:

const Getx...

Getx...

x=20

x=20

outputTimes=2

这里需要注意的是const对象由于不能修改对象的状态,const对象只能调用const成员函数,而普通对象可以调用任何的成员函数。此外const对象在声明时要进行初始化。这与const修改的变量是一致的,如const int x = 100; 当类中定义了2个同名称的函数,其中一个用const修饰,另一个没有const修饰,他们之间是构成重载的,这时const对象调用的是const成员函数,而普通对象调用的是非const成员函数。

0 0
原创粉丝点击