C++关键字const总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yyl424525/article/details/88770343

const 是constant的缩写,本意是不变的,不易改变的意思。它用来告诉编译器,被修饰的这些东西,具有“只读”的特点。在编译的过程中,一旦我们的代码试图去改变这些东西,编译器就应该给出错误提示。
所以,const修饰符的作用主要是利用编译器帮助我们检查自己代码的正确性。我们使用const在源码中标示出“不应该改变”的地方,然后利用编译器,帮助我们检查这些地方是否真的没有被改变过。如果我们不小心去修改了这些地方,编译器就会报错,从而帮助我们纠正错误。使用const和不使用const,对于最终编译产生的代码并没有影响。
虽然const对于最终代码没有影响,但是尽可能使用const,将帮助我们避免很多错误,提高程序正确率。
const 在C++中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。

const修饰变量,一般有两种写法:
const TYPE value;
TYPE const value;
这两种写法在本质上是一样的。它的含义是:const修饰的类型为TYPE的变量value是不可变的。但是对于指针类型的TYPE,不同的写法会有不同情况,不可随意交换位置,后面详细介绍。

在C/C++中,常见 const 用法有以下几种:
1.const 变量
因为 const 变量的值在给定以后不能改变,所以 const 变量必须被初始化。如果我们不初始化 const 变量,编译时也会有错误提示。
在C++中,const 全局变量被用来替换一般常量宏定义。因为虽然 const 变量的值不能改变,但是依然是变量,使用时依然会进行类型检查,要比宏定义的直接替换方法更严格一些(下文还会讨论这个问题)。
在C中,const 结构体变量表示结构体中任何数据域均不允许改变,且需要另一个结构体变量进行初始化。在C++中,struct与class除了默认访问权限之外,并无本质区别。

2.const 类对象
const修饰类对象不能改变任何成员变量的值,不能调用任何非const成员函数,因为任何非const成员函数会有修改成员变量的企图。
const CDebugModule debug;
debug.m_debugLevel = 10; // 编译出错:不能直接改变成员变量
debug.SetDebugLevel(20); // 编译出错:不能通过成员函数改变成员变量
debug.PrintDebugLevel(); // 编译出错:不能调用非 const 成员函数
debug.PrintDebugLevel_const(); // 正常,可调用const 类成员函数

3. 指向 const 变量的指针
指向 const 变量的指针,指的是一个指针,其中保存着一个 const 变量的地址。
指针指向的const变量的值不能改变,但是指针的指向可以改变。
const int debugLevel = 10;
const int logLevel = 10;
const int *p = &debugLevel;
p = &logLevel; // 正常,指针的指向可以改变
*p = 10; // 编译错误,指针指向的位置是只读的
在 *p = 10, 这一句,编译器是通过“指针的类型”(const int *)还是通过其“指向变量的类型”(const int )来判断只读的呢?我们可以通过下面这个小程序来求证一下:
const int debugLevel = 10; // const 变量
int logLevel = 10; // 普通变量
const int *p; //不同于定义const常量,不用初始化
int *q;
p = &logLevel; // 我们让指向 const 变量的指针指向一个普通变量
q = (int *)&debugLevel; // 让指向普通变量的指针,指向一个 const 变量
*q = 5; // 编译正常
*p = 5; // 编译出错:位置为只读
我们可以看出,如果指针的类型为“指向const变量的指针”,即使其指向的内容是非const变量,也无法通过这个指针改变数据内容。反之,如果指针的类型是“指向非const变量的指针”,即使指向的是const变量,也可以通过这个指针改变const变量的内容。所以,编译器是通过 “指针的类型” 来判断是否只读的。
因为我们没有使用面向对象编程,也就不具备动态判断对象具体类型的能力。所以,编译器只能够静态地判断对象的类型。这样,编译器就只能识别出指针的类型,而不清楚指针指向的内容的具体类型了。当然也就只能通过指针类型来判断内容是否只读了。
再输入如下代码:
cout<<“debugLevel:”<<debugLevel<<endl;
//debugLevel:10
cout<<"*q:"<<*q<<endl;
//*q:5
cout<<“debugLevel address =”<<&debugLevel<<endl;
//debugLevel address =0x6ffe3c
cout<<“q:”<<q<<endl;
//q:0x6ffe3c
我们发现debugLevel的值是10,并不是5,没有改变。但指针q指向的变量的值是5,确实改变了。
乍一看,好像同一个地址的东西,采用变量名访问和采用指针访问,得到的结果竟然不一样。其实,之所以产生这种结果,是由于在编译器变异的过程中,对 const 变量的访问进行了优化。编译器将 const 变量直接替换为对应的内容。也就是说,在编译的过程中 :
cout<<“debugLevel:”<<debugLevel<<endl;
被换成了:
cout<<“debugLevel:”<<10<<endl;
所以,才产生了上边的现象。当然,这种替换不一定会发生,跟编译器和优化等级相关。
上文已经提到了,C++建议使用 const 全局变量来替换一般常量的宏定义。通过这个例子可以看出,使用 const 全局变量之后,由于编译器会替换其为具体内容,所以在程序实际运行中,并不会产生一次变量访问,也就使得 const 全局变量和宏定义具有相同的执行效率。同时,使用 const 全局变量,可以让编译器帮助我们进行变量类型检查,提高正确率。

4.const 指针
const指针指的是,一个指针本身经过 const 修饰,自身内容(指针指向)不应该发生改变。
const指针也需要初始化。
指针也是一种变量,只不过其内容保存的是地址而已。所以const指针的内容不能改变,也即是它的指向不能改变。
指针的指向一经确定,不能改变。指针指向的内容,可以改变。
int logLevel = 10;
int logId = 0;
int * const p = &logLevel;
int * const q; // 编译错误,未初始化 const 变量(这个错误是否报告,和所用的编译器有关)
*p = 5; // 正常,const指针指向内容可以改变
p = &logId // 编译错误,const指针自身内容(指向)不能改变

const指针和指向const变量的指针,在写法上容易使人混淆。给大家介绍一种我自己用着比较顺手的区分方法:从右向左,依次结合,const就近结合。
比如,int * const p 可以这样进行解读:
1、int * ( const p ):变量p 经过 const 修饰,为只读变量。
2、int (* (const p)):(const p 现在作为一个整体) 只读变量p是一个指针。
3、(int (* (const p))):(同样的 * const p 作为一个整体) 这个只读的指针p,指向一个int型变量。
于是,可以区分出 int * const p 是一个指向 int 型的const指针。

再比如,const int * p 可以这样解读:
1、const int (* p):变量p是一个指针。
2、(const int) (* p):(const与就近的 int 结合)这个指针指向 const int 型变量。
所以,const int * p 是一个指向 const 整形变量的指针。

在《Effective C++》一书中,这样描述:如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,则表示被指物和指针两者都是常量。
总结:可通过是否需要初始化来判断是常指针,还是指向常量的指针。
(1)定义一个指向字符常量的指针(可不初始化)

const  char *pContent;
char const * pContent;

(2)定义一个常指针(必须初始化)

char* const pContent;

值得注意的是,有的编译器对重复的 const 不会报错!所以允许像 const int const *p; 这种写法。在分析这种“错误”的写法时,只要把重复修饰的const忽略即可。

5.函数参数中的const
如果函数参数是一个const变量,则函数内部不能够改变这个参数的值。
如果函数参数是一个指向const变量的指针,允许上层使用 ”指向 const 变量的指针“ 或 普通指针 作为参数,调用函数。(如果参数声明的是普通指针,则不允许使用 指向 const 变量的指针 作为参数调用)(与编译器有关)

void OutputInt_const( const int a )
{
    a = 5;  // 编译错误:不允许修改只读变量
    printf("a = %d\n", a);
}
void OutputInt_p_const( const int *a )
{
    *a = 5;  // 编译错误:
    printf("*a = %d\n", *a);
}
// 接收一个普通指针
void OutputInt_p_not_const( int *a )
{
    *a = 5;
    printf("*a = %d\n", *a);
}

主函数中:

int logLevel = 10;
const int debugLevel = 5;
OutputInt_p_const(&logLevel);
OutputInt_p_const(&debugLevel);
OutputInt_p_not_const(&logLevel);
OutputInt_p_not_const(&debugLevel);    // 编译错误:从 const int * 到 int * 转换失败(与编译器有关)

其中,void OutputInt_p_const( const int *a )中的const 在此处并不是修饰函数参数的,而是与 int * 组合,描述了参数的一种类型。传入参数时,可以隐式转换。
为什么int * 可以隐式转换为 const int *,但是反向就不可以呢?隐式转换不放宽对于变量的要求,而 const 型的变量显然比非 const 型变量要求严格,所以不能由 const int * 转为 int *。

6. const 返回值
const 型的返回值,指的是函数的返回值为一个 const 变量。
函数返回const返回值,主要用于函数返回const引用。


// 返回 const 引用的函数
const string& SetVersion_const(string & versionInfo)
{
    versionInfo = "V0.0.3";
    return versionInfo;
}

// 返回普通引用的函数
string& SetVersion_not_const(string & versionInfo)
{
    versionInfo = "V0.0.3";
    return versionInfo;
}

// 主函数
int main( int argc, char *argv[])
{
    string versionInfo;
    SetVersion_const(versionInfo) = "V0.0.5";      // 编译错误,返回的引用为 const 引用,不允许修改。
    SetVersion_not_const(versionInfo) = "V0.0.5";  // 正常,返回的引用为普通引用,可以修改内容。
    return 0;
}

引用是一个对象的别名,相当于 const 指针,其指向一经确定,就不能改变了。而 const 引用,则相当于指向 const 变量的 const 指针,其指向和指向的内容均不允许改变。所以在函数返回 const 引用时,不能够通过函数返回的引用对实际对象进行任何修改,即便对象本身不是 const 的。在本例中,versionInfo 在 main 函数中不是const的。SetVersion_const 函数返回了一个指向 versionInfo 的 const 引用,不能通过此引用,对 versionInfo 进行修改。

7.const 成员变量
const 成员变量指的是类中的成员变量为只读,不能够被修改(包括在类外部和类内部)。
const 成员变量必须被初始化(在相关构造函数的初始化列表中),初始化后,不能够被修改。
静态 const 成员变量需要在类外部单独定义并初始化(可定义在头文件)

class CDebugModule
{
public:
    CDebugModule();
    ~CDebugModule();
public:
    const int m_debugLevel;
    static const int m_debugInfo;
};
const int CDebugModule::m_debugInfo = 1;  // 静态常量成员需要在类外进行单独定义和初始化
CDebugModule::CDebugModule() : m_debugLevel(10)  // const 成员在构造函数初始化列表中初始化
{ }

CDebugModule::~CDebugModule()
{}
int main(int argc, char *argv[])
{
    CDebugModule debugModule;
    debugModule.m_debugLevel = 10;  // 编译错误,不能改变只读成员
    CDebugModule::m_debugInfo = 10; // 编译错误,不能改变只读成员
    return 0;
}

类对象的实例化过程可以理解为包含以下步骤:首先,开辟整个类对象的内存空间。之后,根据类成员情况,分配各个成员变量的内存空间,并通过构造函数的初始化列表进行初始化。最后,执行构造函数中的代码。由于 const 成员变量必须在定义(分配内存空间)时,就进行初始化。所以需要在够在函数的初始化列表中初始化。const成员在初始化之后,其值就不允许改变了,即便在构造内部也是不允许的。
静态成员变量并不属于某个类对象,而是整个类共有的。静态成员变量可以不依附于某个实例化后的类对象进行访问。那么,静态成员变量的值,应该在任何实例化操作之前,就能够进行改变(否则,只有实例化至少一个对象,才能访问静态成员)。所以,静态成员变量不能够由构造函数进行内存分配,而应该在类外部单独定义,在实例化任何对象之前,就开辟好空间。又由于 const 成员变量 必须初始化,所以静态成员变量必须在定义的时候就初始化。

8.const成员函数
const成员函数指的是,此函数不应该修改任何成员变量。
传给const成员函数的this指针,是指向 const 对象 的 const 指针。
const成员函数,不能够修改任何成员变量,除非成员变量被 mutable 修饰符修饰。

class CDebugModule
{
public:
    CDebugModule() {};
    ~CDebugModule();

public:
    int m_debugLevel_not_mutable;        // 不带 mutable 修饰的成员变量
    mutable int m_debugLevel_mutable;    // 带 mutable 修饰的成员变量

public:
    void SetDebugLevel_not_const(int debugLevel);   // 非 const 成员函数
    void SetDebugLevel_const(int debugLevel) const; // const 成员函数
};
 
void CDebugModule::SetDebugLevel_not_const(int debugLevel)
{
    m_debugLevel_not_mutable = debugLevel;
    m_debugLevel_mutable = debugLevel;
    return;
}

void CDebugModule::SetDebugLevel_const(int debugLevel) const
{
    m_debugLevel_not_mutable = debugLevel; // 编译错误,const 成员函数不能修改一般的成员变量
    m_debugLevel_mutable = debugLevel;     // 正常,当成员变量被 mutable 修饰时,const成员函数就能修改了
    return;
}

在成员函数调用的过程中,都有一个 this 指针被当做参数隐性地传递给成员函数(可能通过栈,也可能通过CPU寄存器)。这个this指针,指向调用这个函数的对象(这样,成员函数才能找到成员变量的地址,从而对其进行操作)。这个this指针,是一个指向const对象的const指针,不能修改其指向(不希望这个对象的函数,修改了那个对象的成员变量)。
  传递给const成员函数的this指针,指向一个const对象。也就是说,在const成员函数内部,这个this指针是一个指向const对象的const指针。通过第二节的探讨,相信大家已经能够明白,为什么const成员函数不能修改任何成员变量了。
  mutable 修饰符使得const函数的行为有了一些灵活性。相当于提醒编译器,这个成员变量比较特殊,就不要进行任何只读检查了。
  为什么 介绍的const 对象只能够调用const成员函数呢?其实是这样的。由于对象本身通过 const 修饰,那么指向这个对象的指针也就是指向const对象的const指针了。换句话说,指向这个对象的this指针就是指向const对象的const指针。一般成员函数要求的this指针(别忘了this指针也是一个参数)为:指向对象的const指针。所以此处发生了参数不匹配,无法进行调用。而 const 成员函数要求的this指针,恰恰是 指向const对象的const指针。所以依然能够调用。

9.总结

类型 表达式示例 注意事项
const变量 const int a; 不能修改值,必须初始化
const 类对象 const MyClass a; 不能修改成员变量的值,不能调用非 const 函数
指向 const 变量的指针 const int * a; 指向内容不可变,指向可变
const 指针 int * const a; 指向内容可变,指向不可变
指向 const 变量的 const 指针 const int * const a; 指向内容不可变,指向也不可变;等价于const 引用:
const 变量作为函数参数 void myfun(const int a); 函数内部不能改变此参数指向 const 变量的指针做参数,允许上层用一般指针调用。(反之不可)
const 返回值 const string& myfun(void); 用于返回const引用上层不能使用返回的引用,修改对象
const 成员变量 const int a; 或static const int a; 必须在初始化列表初始化,之后不能改变static const 成员变量需要单独在类外定义和初始化
const 成员函数 void myfun(void) const; this指针为指向const对象的const指针不能修改 非mutable 的成员变量

猜你喜欢

转载自blog.csdn.net/yyl424525/article/details/88770343