[原创] 基础中的基础(二):C/C++ 中 const 修饰符用法总结

  在这篇文章中,我总结了一些C/C++语言中的 const 修饰符的常见用法,供你们参考。 const 的用法,也是技术性面试中常见的基础问题,但愿可以帮你们梳理一下知识,给你们一点点帮助。做者是菜鸟一枚,不免出错,还望各位大牛不吝赐教。面试

  首先,来看看const的基本含义。在 C/C++ 语言中,const关键字是一种修饰符。所谓“修饰符”,就是在编译器进行编译的过程当中,给编译器一些“要求”或“提示”,但修饰符自己,并不产生任何实际代码。就 const 修饰符而言,它用来告诉编译器,被修饰的这些东西,具备“只读”的特色。在编译的过程当中,一旦咱们的代码试图去改变这些东西,编译器就应该给出错误提示。编程

  因此,const修饰符的做用主要是利用编译器帮助咱们检查本身代码的正确性。咱们使用const在源码中标示出“不该该改变”的地方,而后利用编译器,帮助咱们检查这些地方是否真的没有被改变过。若是咱们不当心去修改了这些地方,编译器就会报错,从而帮助咱们纠正错误。使用const和不使用const,对于最终编译产生的代码并无影响。小程序

  虽然const对于最终代码没有影响,可是尽量使用const,将帮助咱们避免不少错误,提升程序正确率。函数

 

  在C/C++中,常见 const 用法有如下几种:优化

 

1、const 变量this

  const 变量指的是,此变量的值是只读的,不该该被改变。spa

  • 若是咱们在程序中试图修改 const 变量的值,在编译的时候,编译器将给出错误提示。
  • 正由于 const 变量的值在给定之后不能改变,因此 const 变量必须被初始化(若是不初始化,以后还怎么赋值呢?)若是咱们不初始化 const 变量,编译时也会有错误提示。
const int debugLevel = 10;
const int logLevel;                // 编译错误:未初始化const变量(这个错误是否提示,和所用的编译器有关)

debugLevel = 5;                    // 编译错误:给只读变量赋值

 

  在C++中,const 全局变量被用来替换通常常量宏定义。由于虽然 const 变量的值不能改变,可是依然是变量,使用时依然会进行类型检查,要比宏定义的直接替换方法更严格一些(下文还会讨论这个问题)。debug

  结构体变量也是一种变量,const 结构体变量是什么含义呢?指针

 

 1 struct debugInfo
 2 {
 3     int debugLevel;
 4     int line;
 5 };
 6 
 7 int main( int argc, char *argv[])
 8 {
 9     const struct debugInfo debug_const1;  // 编译错误:未初始化只读变量(与编译器实现有关)
10     
11     struct debugInfo debug_not_const;
12 
13     debug_not_const.debugLevel = 10;
14     debug_not_const.line = 5;
15 
16     const struct debugInfo debug_const2 = debug_not_const;
17 
18     debug_const2.debugLevel = 10;  // 编译错误:不容许修改只读变量
19     debug_const2.line = 2;         // 编译错误:不容许修改只读变量
20 
21     return 0;
22 }

 

  在C中,const 结构体变量表示结构体中任何数据域均不容许改变,且须要另外一个结构体变量进行初始化。在C++中,struct与class除了默认访问权限以外,并没有本质区别。在下一节进行讨论。code

 

2、const 类对象

   const类对象指的是,此类对象不该该被改变。

  • const 类对象与 const 变量并没有实质不一样,只在于类对象的 “改变” 定义。
  • 类对象的 “改变” 定义:改变任何成员变量的值,调用任何非const成员函数
class CDebugModule
{
    public:
        CDebugModule() {};
        ~CDebugModule() {};
    public:
        int m_debugLevel;

    public:
        void SetDebugLevel(int debugLevel) { m_debugLevel = debugLevel;};
        void PrintDebugLevel(void) { cout << m_debugLevel;};
        void PrintDebugLevel_const(void) const { cout << m_debugLevel;};   // const 类成员函数
};

int main( int argc, char *argv[])
{
    const CDebugModule debug;

    debug.m_debugLevel = 10;       // 编译出错:不能直接改变成员变量
    debug.SetDebugLevel(20);       // 编译出错:不能经过成员函数改变成员变量
    debug.PrintDebugLevel();       // 编译出错:不能调用非 const 成员函数
    debug.PrintDebugLevel_const(); // 正常

    return 0;
}

  不能改变 const 类对象的任何成员变量,这一点比较好理解,由于 const 自己就带有不可改变变量取值(内部状态)的含义。为什么const 类成员不能调用非const成员函数呢?咱们将在 第九节“const 成员函数” 进行探讨。在C++中,struct和class没有明显差异,再也不赘述。

 

3、指向 const 变量的指针

  指向 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 )来判断只读的呢?咱们能够经过下面这个小程序来求证一下:

 1 const int debugLevel = 10;        // const 变量
 2 int logLevel = 10;                // 普通变量
 3 
 4 const int *p;
 5 int *q;
 6 
 7 p = &logLevel;                    // 咱们让指向 const 变量的指针指向一个普通变量
 8 q = (int*)&debugLevel;            // 让指向普通变量的指针,指向一个 const 变量
 9 
10 *q = 5;  // 编译正常
11 *p = 5;  // 编译出错:位置为只读

  经过十、11行程序的编译结果,咱们能够看出,若是指针的类型为“指向const变量的指针”,即便其指向的内容是非const变量,也没法经过这个指针改变数据内容。反之,若是指针的类型是“指向非const变量的指针”,即便指向的是const变量,也能够经过这个指针改变const变量的内容(稍后讨论这一点)。因此,编译器是经过 “指针的类型” 来判断是否只读的。

  说到这点,我以为能够这么解释。由于咱们没有使用面向对象编程,也就不具有动态判断对象具体类型的能力。因此,编译器只可以静态地判断对象的类型。这样,编译器就只能识别出指针的类型,而不清楚指针指向的内容的具体类型了。固然也就只能经过指针类型来判断内容是否只读了。

  在上面,咱们经过指针,“改变”了const变量的内容,若是咱们在上边的程序中添加上输出,会是什么结果?

12 printf("debugLevel = %d\n", debugLevel);
13 printf("*q = %d\n", *q);

14 printf("debugLevel address = %x\n", &debugLevel);
15 printf("q = %x\n", q);

   从上边的说明,咱们能够想象,debugLevel的值,被咱们经过指针改变了,因此输出应该是:

debugLevel = 5
*q = 5
debugLevel address = 0xbfaff718
q = 0xbfaff718

  可是,事实上,这个结果是不肯定的跟您所用的编译器和优化级别有关。我在g++ 4.1.2上,编译运行得出了如下结果:

 

debugLevel = 10                     // 直接打印能够发现,const 变量的值未改变 !
*q = 5                              // 经过指针访问,发现 const 变量的值改变了!
debugLevel address = 0xa6a65318
q = 0xa6a65318                      // 指针的指向并无错误

   乍一看,好像同一个地址的东西,采用变量名访问和采用指针访问,获得的结果居然不同。其实,之因此产生这种结果,是因为在编译器变异的过程当中,对 const 变量的访问进行了优化。编译器将 const 变量直接替换为对应的内容。也就是说,在编译的过程当中 :

printf("debugLevel = %d\n", debugLevel);

 这个语句,被直接替换成了:

printf("debugLevel = %d\n", 10);

 因此,才产生了上边的现象。固然,这种替换不必定会发生,跟编译器和优化等级相关。

  上文已经提到了,C++建议使用 const 全局变量来替换通常常量的宏定义。经过这个例子能够看出,使用 const 全局变量以后,因为编译器会替换其为具体内容,因此在程序实际运行中,并不会产生一次变量访问,也就使得 const 全局变量和宏定义具备相同的执行效率。同时,使用 const 全局变量,可让编译器帮助咱们进行变量类型检查,提升正确率。

 

  指针也是变量的一种,因此天然有 const 类型指针。

 

4、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变量的指针,在写法上容易令人混淆。给你们介绍一种我本身用着比较顺手的区分方法:从右向左,依次结合,const就近结合。

  好比,int * const p 能够这样进行解读:

  一、int * ( const p ):变量p 通过 const 修饰,为只读变量。

  二、int (* (const p)):(const p 如今做为一个总体) 只读变量p是一个指针。

  三、(int (* (const p))):(一样的 * const p 做为一个总体) 这个只读的指针p,指向一个int型变量。

  因而,能够区分出 int * const p 是一个指向 int 型的const指针。

 

  再好比,const int * p 能够这样解读:

  一、const int (* p):变量p是一个指针。

  二、(const int) (* p):(const与就近的 int 结合)这个指针指向 const int 型变量。

  因此,const int * p 是一个指向 const 整形变量的指针。

 

  采用这个方法,相信你们能够本身分辨 int const * p的含义了。

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

 

5、指向 const 变量的 const 指针

  这种状况是 const 指针和 指向 const 变量的指针的结合,相信你们已经可以本身分析,再也不赘述。

 

6、const 变量做为函数参数

  在函数调用的过程当中,函数的参数是创建在函数的栈上的变量。既然是变量,就能够经过 const 进行修饰。

  • 将函数参数声明为 const 类型,表示对于函数来讲,这个参数是一个 const 变量。也就是说,函数内部不可以改变这个参数的值。
  • 将函数参数是一个指针,把它声明为 “指向 const 变量的指针” ,容许上层使用 ”指向 const 变量的指针“ 或 普通指针 做为参数,调用函数。(若是参数声明的是普通指针,则不容许使用 指向 const 变量的指针 做为参数调用)(与编译器有关)
 1 // 接收一个int变量,并在函数内部,认为它是只读的
 2 void OutputInt_const( const int a )
 3 {
 4     a = 5;  // 编译错误:不容许修改只读变量
 5     printf("a = %d\n", a);
 6 }
 7 
 8 // 接收一个int变量,在函数内部,认为它是普通的
 9 void OutputInt_not_const( int a )
10 {
11     a = 5;  // 正常
12     printf("a = %d\n", a);
13 }
14 
15 // 接收一个 指向const型整形数的指针
16 void OutputInt_p_const( const int *a )
17 {
18     *a = 5;  // 编译错误:
19     printf("*a = %d\n", *a);
20 }
21 
22 // 接收一个普通指针
23 void OutputInt_p_not_const( int *a )
24 {
25     *a = 5;
26     printf("*a = %d\n", *a);
27 }
28 
29 // 主函数
30 int main( int argc, char *argv[])
31 {
32     int logLevel = 10;
33     const int debugLevel = 5;
34 
35     OutputInt_const(logLevel);              
36     OutputInt_const(debugLevel);         
37 
38     OutputInt_not_const(logLevel);       
39     OutputInt_not_const(debugLevel);  
40 
41     OutputInt_p_const(&logLevel);
42     OutputInt_p_const(&debugLevel);
43 
44     OutputInt_p_not_const(&logLevel);
45     OutputInt_p_not_const(&debugLevel);    // 编译错误:从 const int * 到 int * 转换失败(与编译器有关)
46 
47     return 0;
48 }

 

  为何对于指针参数的要求特殊?其实咱们能够仔细想一下 const 在修饰参数时的做用。

  首先,函数参数是函数内部可见的一个变量。在const 修饰函数参数时,仅仅表示此函数内部对于这个变量的限制。对于传进来的参数,在外边到底是什么样子的,函数内部并不关心。因此,函数 void OutputInt_const( const int a ) 并不会在乎传入的参数在main函数中是不是只读的。它只会在函数内部,将入参看成只读变量处理。

  既然 const 修饰函数参数时,不会限制入参是否为只读,为何 OutputInt_p_not_const( int *a ) 和 OutputInt_p_const( const int *a ) 的调用方式有区别呢(见4四、45行)?

  其实答案很简单,const 在此处并非修饰函数参数的!此处的 const ,与 int * 组合,描述了参数的一种类型。OutputInt_p_const函数要求的参数是:指向只读整形的指针。因此,只要调用时传入的参数不是一个指向只读整形数的指针,就会发生类型不匹配。在示例41行的调用中,使用一个 int * 来调用 OutputInt_p_const 函数,发生类型不匹配,可是 int * 能够隐式转换为 const int *,因此此处调用能够成功。但在45行中,采用 const int * 来调用须要 int * 的 OutputInt_p_not_const 函数,发生类型不匹配, const int * 不可以隐式转换为 int *,因此此处调用失败。

  为何 int * 能够隐式转换为 const int *,可是反向就不能够呢?相信各位读者已经想到了。隐式转换不放宽对于变量的要求,而 const 型的变量显然比非 const 型变量要求严格,因此不能由 const int * 转为 int *。

 

7、const 返回值

  const 型的返回值,指的是函数的返回值为一个 const 变量。

  • 函数返回const返回值,主要用于函数返回const引用。
 1 #include <string>
 2 
 3 using namespace std;
 4 
 5 // 返回 const 引用的函数
 6 const string& SetVersion_const(string & versionInfo)
 7 {
 8     versionInfo = "V0.0.3";
 9     return versionInfo;
10 }
11 
12 // 返回普通引用的函数
13 string& SetVersion_not_const(string & versionInfo)
14 {
15     versionInfo = "V0.0.3";
16     return versionInfo;
17 }
18 
19 // 主函数
20 int main( int argc, char *argv[])
21 {
22     string versionInfo;
23 
24     SetVersion_const(versionInfo) = "V0.0.5";      // 编译错误,返回的引用为 const 引用,不容许修改。
25  
26     SetVersion_not_const(versionInfo) = "V0.0.5";  // 正常,返回的引用为普通引用,能够修改内容。
27     
28     return 0;
29 }

  引用是一个对象的别名,至关于 const 指针,其指向一经肯定,就不能改变了。而 const 引用,则至关于指向 const 变量的 const 指针,其指向和指向的内容均不容许改变。因此在函数返回 const 引用时,不可以经过函数返回的引用对实际对象进行任何修改,即使对象自己不是 const 的。在本例中,versionInfo 在 main 函数中不是const的。SetVersion_const 函数返回了一个指向 versionInfo 的 const 引用,不能经过此引用,对 versionInfo 进行修改。

  为何会出现这种现象?相信你们都能理解了。请参考 指向 const 变量指针 的相关内容。

  

8、const 成员变量

  const 成员变量指的是类中的成员变量为只读,不可以被修改(包括在类外部和类内部)。

  • const 成员变量必须被初始化(在相关构造函数的初始化列表中),初始化后,不可以被修改
  • 静态 const 成员变量须要在类外部单独定义并初始化(可定义在头文件)
 1 class CDebugModule
 2 {
 3 public:
 4     CDebugModule();
 5     ~CDebugModule();
 6 
 7 public:
 8     const int m_debugLevel;
 9     static const int m_debugInfo;
10 
11 };
12 
13 const int CDebugModule::m_debugInfo = 1;  // 静态常量成员须要在类外进行单独定义和初始化
14 
15 CDebugModule::CDebugModule()
16     : m_debugLevel(10)  // const 成员在构造函数初始化列表中初始化
17 {
18 }
19 
20 CDebugModule::~CDebugModule()
21 {
22 }
23 
24 int main(int argc, char *argv[])
25 {
26     CDebugModule debugModule;
27 
28     debugModule.m_debugLevel = 10;  // 编译错误,不能改变只读成员
29     CDebugModule::m_debugInfo = 10; // 编译错误,不能改变只读成员
30 
31     return 0;
32 }

  类对象的实例化过程能够理解为包含如下步骤:首先,开辟整个类对象的内存空间。以后,根据类成员状况,分配各个成员变量的内存空间,并经过构造函数的初始化列表进行初始化。最后,执行构造函数中的代码。因为 const 成员变量必须在定义(分配内存空间)时,就进行初始化。因此须要在够在函数的初始化列表中初始化。const成员在初始化以后,其值就不容许改变了,即使在构造内部也是不容许的。

  静态成员变量并不属于某个类对象,而是整个类共有的。静态成员变量能够不依附于某个实例化后的类对象进行访问。那么,静态成员变量的值,应该在任何实例化操做以前,就可以进行改变(不然,只有实例化至少一个对象,才能访问静态成员)。因此,静态成员变量不可以由构造函数进行内存分配,而应该在类外部单独定义,在实例化任何对象以前,就开辟好空间。又因为 const 成员变量 必须初始化,因此静态成员变量必须在定义的时候就初始化。

 

 9、const 成员函数

   const成员函数指的是,此函数不该该修改任何成员变量。

  • 传给const成员函数的this指针,是指向 const 对象 的 const 指针
  • const成员函数,不可以修改任何成员变量,除非成员变量被 mutable 修饰符修饰
 1 class CDebugModule
 2 {
 3 public:
 4     CDebugModule() {};
 5     ~CDebugModule();
 6 
 7 public:
 8     int m_debugLevel_not_mutable;        // 不带 mutable 修饰的成员变量
 9     mutable int m_debugLevel_mutable;    // 带 mutable 修饰的成员变量
10 
11 public:
12     void SetDebugLevel_not_const(int debugLevel);   // 非 const 成员函数
13     void SetDebugLevel_const(int debugLevel) const; // const 成员函数
14 };
15  
16 void CDebugModule::SetDebugLevel_not_const(int debugLevel)
17 {
18     m_debugLevel_not_mutable = debugLevel;
19     m_debugLevel_mutable = debugLevel;
20     return;
21 }
22 
23 void CDebugModule::SetDebugLevel_const(int debugLevel) const
24 {
25     m_debugLevel_not_mutable = debugLevel; // 编译错误,const 成员函数不能修改通常的成员变量
26     m_debugLevel_mutable = debugLevel;     // 正常,当成员变量被 mutable 修饰时,const成员函数就能修改了
27     return;
28 }
29 
30 int main(int argc, char *argv[])
31 {
32     CDebugModule debugModule;
33 
34     debugModule.SetDebugLevel_not_const(10);
35     debugModule.SetDebugLevel_const(10);
36 
37     return 0;
38 }

 

  在成员函数调用的过程当中,都有一个 this 指针被当作参数隐性地传递给成员函数(可能经过栈,也可能经过CPU寄存器)。这个this指针,指向调用这个函数的对象(这样,成员函数才能找到成员变量的地址,从而对其进行操做)。这个this指针,是个 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指针。因此依然可以调用。

  

 10、总结  

 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 的成员变量

 

  本文的内容就这么多了,感谢您可以看到最后,但愿对您可以有一点点帮助 ^_^

相关文章
相关标签/搜索