C++——限定符const

写在前面

这一篇博客总结一下我对于C++中const限定符的理解。由于我学习年限不长,所以不能添加很多自己实际经验的理解,所以这篇博客主要是自己查阅网上的一些资料,对资料进行整合、分类、理解,然后整理的一篇博客。一方面能帮助自己更好的理解这个灵活的限定符,另一方面提供一些自己的简单的理解。还望多多批评指正。

参考文献:

const限定符定义

《C++ Primer》中是这样定义的:“有时我们希望定义这样一种变量,它的值不能被改变”,“另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以使用关键字const对变量的类型加以限定”。所以可以看出const限定符的作用是:

  • 告诉编译器与外部引用者,const限定符修饰的变量或者对象的值,不能改变。

const与define

仅仅考虑对于变量,要想让变量的值不改变,可以使用const,也可以使用define宏定义。如代码如下:

#define MAX 10000
//OR
const int MAX=10000;

那么,看上去二者实现功能一样,都是让MAX的值一直为10000,有何区别呢?

如果使用#define,那么如果出现错误,编译器并不会提示MAX,因为在预处理阶段已经把MAX替换成了10000,因此编译器会莫名其妙的提示10000这个数字出现了错误,从而不利于程序debug,但是如果使用const int MAX=10000,程序后面尝试修改MAX变量,编译器就会准确的提示MAX有错误,从而可以轻易地定位。

const修饰变量

const修饰变量,以下两种定义形式在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。

const TYPE ValueName = value;
TYPE cosnt ValueName = value;

注意: 因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。

注意: 默认状态下,const对象仅在文件内有效。当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。所谓如果程序包含多个文件,而且多个外部文件(非const定义文件)有用到const变量,即在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize;

const修饰引用

可以把引用绑定到const对象上,const对象与其他对象一样,这称为对常量的引用。代码示例如下:

const int c1 = 1024;
const int &r1 = c1;

引用的类型必须与其所引用对象的类型一致。但是这里有例外:可以运行常量引用绑定非常量的对象、字面值,一般表达式,实例代码如下:

int i =42;
const int &r1 = i; 
const int &r2 = 42;
const int &r3 = r1*2;

以上表达式均正确,其中,r1是常量引用,绑定到非常量对象i上,含义是不允许通过r1修改i,但是i还是可以修改。代码如下:

i++;  //Right
r1++; //Error

但是将一个非常量引用绑定到一个常量对象上是错误的。错误代码如下:

//Error
int & r4 = r1 *2;

const修饰指针

1.指向常量的指针
与引用一样,也可以让指针指向常量或非常量。指向常量的指针(pointer to const)不能用于改变其所指向的对象。要想储存常量对象的地址,只能使用指向常量的指针。指向常量的指针代码示例如下:

const double pi = 3.14;
const double *cptr = π

由于一般指针的类型必须与其所指向对象的类型一致,所以,既然这个指针指向常量,常量是:const TYPE,所以指针自然也是:const TYPE *ptrName = &ValueName;

当然,与引用一样,也存在特殊情况,C++允许让一个指向常量的指针指向一个非常量。实例代码如下:

double dval = 3.14;
const double *cptr = dval;

与之前所说的常量引用一样,指向常量的指针仅仅要求不能通过该指针改变对象的值。

2.“常量”指针
指针本身也是一种对象,所以C++允许把指针本身设置为常量,也就是这里的常量指针——const pointer。与常量一样,常量指针必须初始化,一旦初始化完成,它的值,即存在指针中的那个地址,就不能改变,成为一个常量。

把 * 放在const限定符之后来说明指针是一个常量。示例代码如下:

int errNumb = 0;
int *const curErr = &errNumb;   //curErr将一直储存errNumb的地址,curErr本身不能改变,但是errNumb可以改变

const int pi = 3.14;
const int *const cptr = π    //cptr将一直储存pi的地址,cptr本身不能改变,同时,pi也不能改变
                                //不能改变指针,也不能通过指针改变指针所指向的对象

3.顶层const与底层const
由于指针与指针所指向的对象都是对象,这里就有两个对象需要我们描述,所以:

  • 顶层const(top-level):指针本身这个对象是一个const(常量);
  • 底层const(low-level):指针所指向的对象是一个const(常量)。

一定要注意这里有两个对象:指针本身是一个对象;指针所指向的对象是另一个对象。更一般的,顶层const可以表示任意的对象是常量。底层const则与指针与引用等复合类型的基本类型等部分有关。

这里要注意特殊的一点就是,由于指针本身是一个对象,所以也可以构造一个指针指向目前这个指针。所以指针本身这个对象,既可以所谓顶层const,也可以作为底层const。

为了便于理解,可以参考代码如下:

int i = 0;
int *const p1 = &i;     //pi是一个常量指针,p1本身地址大小不能改变,是一个顶层const
const int c1 = 42;      //c1是一个常量,c1不能改变,是一个顶层const
const int *p2 = &c1;    //p2是一个指向常量的指针,不能通过p2去更改c1,但是p2可以更改,是一个底层const
const int const *p3 = p2;   //p3是一个常量指针,而且是指向一个常量指针的指针。
                            //所以p3本身地址大小不能改变,同时不能通过p3去更改p2的值。
                            //靠右的const,离p3近的const是顶层const;靠左的const,离p3远的const是底层const
const int &r = c1;      //常量的引用,r,不能改变r,c1自然也就无法改变,是一个底层const

const修饰函数参数

const修饰函数参数首先可以辨析一下函数参数的两种种类:实参与形参。参考:形参和实参的区别。简单的来说:

  • 形参:出现在函数定义中,在整个函数体内都可以使用, 离开该函数则不能使用。
  • 实参:出现在主调函数中,进入被调函数后,实参变量也不能使用。
  • 形参和实参的功能是作数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。

1.值传递形参

如果函数是按照值传递的方式传递实参,函数声明代码如下:

void function(const TYPE Var);

传递过来的参数在函数function()内不可改变。当然也改变不了,因为这里是按照值传递,函数function()内会创建一个Var的拷贝,而Var本身不会改变。

2.引用传递形参

如果函数是按照引用传递的方式传递实参,函数声明代码如下:

void function(const TYPE& Var);     //TYPE是C++内建类型,比如int、double等
void function(const Class& Var);    //Class是用户自定义类,包含构造、析构函数,诸多成员变量与成员函数

由于传递过来的是引用,所以如果函数function()内对&Var,Var的引用,进行了修改,Var本身也是会改变的。这时候为了保证Var本身不会被函数function()修改,就在&Var,Var的引用添加const限定符。表示Var的引用可以在函数function()内改变,但是Var本身是不会改变的。

这样的做的效果与值传递一模一样。如果形参的类型是内建类型,那么常量引用与值传递效率差不多,但是,如果形参类型是自定义类,包括构造函数、析构函数与成员变量和成员函数等,常量引用效率会高很多,因为值传递是传递副本,会调用构造、析构等诸多过程,而引用传递传递地址,

尽量多用常量引用。与普通的引用传递相比,这样有许多好处:

  • 常量引用会限制函数能接受的实参类型:如果是普通引用作为形参,那么就不能const对象、字面值或者需要类型转换的对象传递给普通的引用形参;
  • 函数声明了常量引用的形参可以在其他函数中调用的更方便:如果这里有另一个函数function1(const Type& Var)中调用了function(),那么如果function形参是普通引用形参,就会出错。

3.指针传递形参

如果函数是以指针传递的方式传递实参,函数声明代码如下:

void function(const char* Var);     //参数指针所指内容为常量不可变
void function(char* const Var);     //参数指针本身地址为常量不可变,但是Var本身是有可能改变的。

以上写法都没有问题,但是要根据实际情况选择合适的写法,一般是第一种:指针指向的内容为常量不可改变用的较多。第二种情况,指针本身地址不可改变就要看具体情况具体分析了。

const修饰函数返回值

const修饰函数返回值其实用的并不是很多,它的含义和const修饰普通变量以及指针的含义基本相同。

1.函数返回常量

如果函数本身返回值是TYPE类型的某个对象,在TYPE前添加const限定符,就是限定函数返回一个常量了。代码如下:

const int fun1();       //调用时 const int Val = fun1();

但是,由于函数会把返回值复制到外部临时的存储单元中,这样加const限定没有任何价值。

2.函数返回指向常量的指针

如果函数本身返回值是TYPE* 一个指针,在最前面添加const限定符,这样限定函数返回一个指向常量的指针。代码如下:

const int* fun2();      //调用时 const int *pValue = fun2(); 
                        //我们可以把fun2()看作成一个变量,即指针内容不可变。

3.函数返回常量指针

如果函数本身返回值是一个TYPE* 一个指针,限定符添加在TYPE与*之间,这样限定函数返回一个常量指针。代码如下:

int* const fun3();      //调用时 int* const pValue = fun2(); 
                        //我们可以把fun2()看作成一个变量,即指针本身不可变。

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

const修饰类

1.const修饰类的成员函数

const修饰类的成员函数,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。

class A{const int nValue;           //成员常量不能被修改
    …
    A(int val){ nValue = val; } //只能在初始化列表中赋值
} 

2.const修饰类的成员函数

const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。

class A{const int nValue;           //成员常量不能被修改
    void func(){};              //非常成员函数
    void function() const;      //常成员函数, 它不改变对象的成员变量.                        
                                //也不能调用类中任何非const成员函数。
    A(int val){ nValue = val; } //只能在初始化列表中赋值
}

void A::function() const{     //这里依然不能忘记const
    nvalue = 0;               //错误,常量成员函数不能修改成员变量的值  
    func();                   //错误,常量成员函数不能调用非常量成员函数  
}

int main(void)  
{  
    const A a;                //创建常量对象
    a.nValue = 1;             //错误,常量对象不能被修改  
    a.func();                 //错误,常量对象上面不能执行非常量成员函数,即使这个成员函数为空,因为编译器不知道这     
                              //个非常量成员函数会不会对常量对象进行某些修改
    s.function();             //正确,常量对象上面可以执行常量函数  
    return 0;  
}  

const成员函数存在的意义在于它能被const常对象调用,在定义一个对象或者一个变量时,如果在类型前加一个const,如const int x;,则表示定义的量为一个常量,它的值不能被修改。但是创建的对象却可以调用成员函数,调用的成员函数很有可能改变对象的值,或者对象的成员变量。把那些肯定不会修改对象的各个属性值的成员函数加上const说明符,这样,在编译时,编译器将对这些const成员函数进行检查,如果确实没有修改对象值的行为,则检验通过。

然而,有些时候,我们却必须要让const函数具有修改某个成员数据值的能力。比如一些内部的状态量,对外部用户无所谓,但是对整个对象的运行却大有用处,如支持缓存的技术。遇到这种问题,我们可以把一个成员数据定义为mutable(多变的),它表示这个成员变量可以被const成员函数修改却不违法。比如下面定义了一个is_valid类成员:

class List
{
private:
     ……
     mutable bool is_valid;
     ……
public:
      bool CheckList() const{
          if(length >= 1) then return is_valid =true;
          else return is_valid = false; //正确!
      };
}

这样,即使像CheckList这样的const成员函数修改它也是合法的。但需要注意的是,不可滥用mutabe描述符,如果在某个类中只有少数一部分是被允许const常量函数修改的,使用mutable是再合适不过的。如果大部分数据都定义为mutable,那么最好将这些需要修改的数据放入另一个独立的对象里,并间接地访问它。

猜你喜欢

转载自blog.csdn.net/zy2317878/article/details/80671464