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

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

  所以,const修饰符的作用主要是利用编译器帮助我们检查自己代码的正确性。我们使用const在源码中标示出“不应该改变”的地方,然后利用编译器,帮助我们检查这些地方是否真的没有被改变过。如果我们不小心去修改了这些地方,编译器就会报错,从而帮助我们纠正错误。使用const和不使用const,对于最终编译产生的代码并没有影响。

  虽然const对于最终代码没有影响,但是尽可能使用const,将帮助我们避免很多错误,提高程序正确率。

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

一、const 变量

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

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

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

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

 struct debugInfo
{
int debugLevel;
int line;
}; int main( int argc, char *argv[])
{
const struct debugInfo debug_const1; // 编译错误:未初始化只读变量(与编译器实现有关) struct debugInfo debug_not_const; debug_not_const.debugLevel = ;
debug_not_const.line = ; const struct debugInfo debug_const2 = debug_not_const; debug_const2.debugLevel = ; // 编译错误:不允许修改只读变量
debug_const2.line = ; // 编译错误:不允许修改只读变量 return ;
}

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

二、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 = ; // 编译出错:不能直接改变成员变量
debug.SetDebugLevel(); // 编译出错:不能通过成员函数改变成员变量
debug.PrintDebugLevel(); // 编译出错:不能调用非 const 成员函数
debug.PrintDebugLevel_const(); // 正常 return ;
}

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

三、指向 const 变量的指针

  指向 const 变量的指针,指的是一个指针,其中保存着一个 const 变量的地址。

  • 由于指针指向一个 const 变量,所以通过指针,不能修改这个 const 变量的值。
  • 虽然指针指向的值不能改变,但是指针的指向可以改变。
const int debugLevel = ;
const int logLevel = ; const int *p = &debugLevel;
p = &logLevel; // 正常,指针的指向可以改变 *p = ; // 编译错误,指针指向的位置是只读的

  

  在 *p = 10, 这一句,编译器是通过“指针的类型”(const int *)还是通过其“指向变量的类型”(const int )来判断只读的呢?我们可以通过下面这个小程序来求证一下:

 const int debugLevel = ;        // const 变量
int logLevel = ; // 普通变量 const int *p;
int *q; p = &logLevel; // 我们让指向 const 变量的指针指向一个普通变量
q = (int*)&debugLevel; // 让指向普通变量的指针,指向一个 const 变量 *q = ; // 编译正常
*p = ; // 编译出错:位置为只读

  通过10、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 =
*q =
debugLevel address = 0xbfaff718
q = 0xbfaff718

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

debugLevel =                      // 直接打印可以发现,const 变量的值未改变 !
*q = // 通过指针访问,发现 const 变量的值改变了!
debugLevel address = 0xa6a65318
q = 0xa6a65318 // 指针的指向并没有错误

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

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

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

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

所以,才产生了上边的现象。当然,这种替换不一定会发生,跟编译器和优化等级相关。

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

  指针也是变量的一种,所以自然有 const 类型指针。

四、const 指针

  const指针指的是,一个指针本身经过 const 修饰,自身内容(指针指向)不应该发生改变。

  • 指针的指向一经确定,不能改变。指针指向的内容,可以改变。
int logLevel = ;
int logId = ; int * const p = &logLevel;
int * const q; // 编译错误,未初始化 const 变量(这个错误是否报告,和所用的编译器有关) *p = ; // 正常,const指针指向内容可以改变
p = &logId // 编译错误,const指针自身内容(指向)不能改变

  指针也是一种变量,只不过其内容保存的是地址而已。所以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 整形变量的指针。

  采用这个方法,相信大家可以自己分辨 int const * p的含义了。

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

五、指向 const 变量的 const 指针

  这种情况是 const 指针和 指向 const 变量的指针的结合,相信大家已经能够自己分析,不再赘述。

六、const 变量作为函数参数

  在函数调用的过程中,函数的参数是建立在函数的栈上的变量。既然是变量,就可以通过 const 进行修饰。

  • 将函数参数声明为 const 类型,表示对于函数来说,这个参数是一个 const 变量。也就是说,函数内部不能够改变这个参数的值。
  • 将函数参数是一个指针,把它声明为 “指向 const 变量的指针” ,允许上层使用 ”指向 const 变量的指针“ 或 普通指针 作为参数,调用函数。(如果参数声明的是普通指针,则不允许使用 指向 const 变量的指针 作为参数调用)(与编译器有关)
 // 接收一个int变量,并在函数内部,认为它是只读的
void OutputInt_const( const int a )
{
a = ; // 编译错误:不允许修改只读变量
printf("a = %d\n", a);
} // 接收一个int变量,在函数内部,认为它是普通的
void OutputInt_not_const( int a )
{
a = ; // 正常
printf("a = %d\n", a);
} // 接收一个 指向const型整形数的指针
void OutputInt_p_const( const int *a )
{
*a = ; // 编译错误:
printf("*a = %d\n", *a);
} // 接收一个普通指针
void OutputInt_p_not_const( int *a )
{
*a = ;
printf("*a = %d\n", *a);
} // 主函数
int main( int argc, char *argv[])
{
int logLevel = ;
const int debugLevel = ; OutputInt_const(logLevel);
OutputInt_const(debugLevel); OutputInt_not_const(logLevel);
OutputInt_not_const(debugLevel); OutputInt_p_const(&logLevel);
OutputInt_p_const(&debugLevel); OutputInt_p_not_const(&logLevel);
OutputInt_p_not_const(&debugLevel); // 编译错误:从 const int * 到 int * 转换失败(与编译器有关) return ;
}

  为什么对于指针参数的要求特殊?其实我们可以仔细想一下 const 在修饰参数时的作用。

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

  既然 const 修饰函数参数时,不会限制入参是否为只读,为什么 OutputInt_p_not_const( int *a ) 和 OutputInt_p_const( const int *a ) 的调用方式有区别呢(见44、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 *。

七、const 返回值

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

  • 函数返回const返回值,主要用于函数返回const引用。
 #include <string>

 using namespace std;

 // 返回 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 ;
}

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

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

八、const 成员变量

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

  • const 成员变量必须被初始化(在相关构造函数的初始化列表中),初始化后,不能够被修改
  • 静态 const 成员变量需要在类外部单独定义并初始化(可定义在头文件)
 class CDebugModule
{
public:
CDebugModule();
~CDebugModule(); public:
const int m_debugLevel;
static const int m_debugInfo; }; const int CDebugModule::m_debugInfo = ; // 静态常量成员需要在类外进行单独定义和初始化 CDebugModule::CDebugModule()
: m_debugLevel() // const 成员在构造函数初始化列表中初始化
{
} CDebugModule::~CDebugModule()
{
} int main(int argc, char *argv[])
{
CDebugModule debugModule; debugModule.m_debugLevel = ; // 编译错误,不能改变只读成员
CDebugModule::m_debugInfo = ; // 编译错误,不能改变只读成员 return ;
}

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

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

九、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;
} int main(int argc, char *argv[])
{
CDebugModule debugModule; debugModule.SetDebugLevel_not_const();
debugModule.SetDebugLevel_const(); return ;
}

  在成员函数调用的过程中,都有一个 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指针。所以依然能够调用。

 十、总结  

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

  本文的内容就这么多了,感谢您能够看到最后,希望对您能够有一点点帮助 ^_^

[原创] 基础中的基础(二):C/C++ 中 const 修饰符用法总结的更多相关文章

  1. C/C++ 中 const 修饰符用法总结

    C/C++ 中 const 修饰符用法总结 在这篇文章中,我总结了一些C/C++语言中的 const 修饰符的常见用法,供大家参考. const 的用法,也是技术性面试中常见的基础问题,希望能够帮大家 ...

  2. 转载----C/C++ 中 const 修饰符用法总结

    感谢原创作者,写的好详细.不忍错过,所以转载过来了... 原文地址: https://www.cnblogs.com/icemoon1987/p/3320326.html 在这篇文章中,我总结了一些C ...

  3. MySQL 聚合函数(二)Group By的修饰符——ROLLUP

    原文为MySQL 5.7 官方手册:12.20.2 GROUP BY Modifiers 一.ROLLUP 修饰符的意义 GROUP BY子句允许添加WITH ROLLUP修饰符,该修饰符可以对分组后 ...

  4. Delphi 中 函数参数中的 const 修饰符的本质以及注意事项

    来自:http://blog.csdn.net/farrellcn/article/details/9096787 ------------------------------------------ ...

  5. C++中 容易忽视的const 修饰符

    C++可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的有点: (1)const常量有数据类型,而宏常量没有数据类型.编译器可以对const进行类型安全检查,而后者只进行 ...

  6. 一、MySQL中的索引 二、MySQL中的函数 三、MySQL数据库的备份和恢复 四、数据库设计和优化(重点)

    一.MySQL中的索引###<1>索引的概念 索引就是一种数据结构(高效获取数据),在mysql中以文件的方式存在.存储建立了索引列的地址或者指向. 文件 :(以某种数据 结构存放) 存放 ...

  7. Java基础学习笔记(四) - 认识final关键字、权限修饰符和内部类

    一.final关键字 为什么要使用 final 关键字? 通过继承我们知道,子类可以重写父类的成员变量和方法.final 关键字可以用于修饰父类,父类成员变量和方法,使其内容不可以被更改. 1.被修饰 ...

  8. 从零开始学 Web 之 Vue.js(二)过滤器,按键修饰符,自定义指令

    大家好,这里是「 从零开始学 Web 系列教程 」,并在下列地址同步更新...... github:https://github.com/Daotin/Web 微信公众号:Web前端之巅 博客园:ht ...

  9. java中的方法重载与重写以及方法修饰符

    1. 方法重载Overloading , 是在一个类中,有多个方法,这些方法的名字相同,但是具有不同的参数列表,和返回值 重载的时候,方法名要一样,但是参数类型和参数个数不一样,返回值类型可以相同,也 ...

随机推荐

  1. Thread.currentThread().getName() ,对象实例.getName() 和 this.getName()区别

    原文链接:http://www.cnblogs.com/signheart/p/922dcf75dd5fe6b418d4475af89c4664.html 使用Thread.currentThread ...

  2. rpmdb open failed的解决办法

      错误信息如下:    “错误:无法从 /var/lib/rpm 打开软件包数据库      CRITICAL:yum.main:       Error: rpmdb open failed”   ...

  3. 如果程序集是从 Web 上下载的,即使它存储于本地计算机,Windows 也会将其标记为 Web 文件,http://go.microsoft.com/fwlink/?LinkId=179545

    使用Silverlight,经常弄出很多莫名的XXX文件来于Web,神马信任程序集,就Build个程序都那么麻烦,应该在所有发布时注明一些最基本的配置说明,最BT莫过于连下载程序集的地方都找不到. 若 ...

  4. Devexpress VCL Build v2013 vol 14.1.3 发布

    我修,我修,修修修. New Major Features in 14.1 What's New in VCL Products 14.1 Breaking Changes To learn abou ...

  5. Java中创建数组的几种方法

    Java中创建数组的几种方法 public static void main(String[] args) { //创建数组的第一种方法 int[] arr=new int[6]; int intVa ...

  6. 人体感应模块控制LCD1602背景灯是否开启

    /* Web client This sketch connects to a website (http://www.google.com) using an Arduino Wiznet Ethe ...

  7. SPSS-Friedman 秩和检验-非参数检验-K个相关样本检验 案例解析

    三人行,必有我师,是不是真有我师?三种不同类型的营销手段,最终的营销效果是否一样,随即区组秩和检验带你进入分析世界 今天跟大家讨论和分享一下:spss-Friedman 秩和检验-非参数检验-K个(多 ...

  8. No cache or cacheManager properties have been set. Authorization cache cannot be obtained.

    20235 [http-bio-8080-exec-10] INFO o.a.shiro.realm.AuthorizingRealm - No cache or cacheManager prope ...

  9. 9) 依赖查询 & 镜像站

    依赖查询 http://mvnrepository.com/ Maven仓库查询 http://search.maven.org 仓库 加上这两个,如果使用中央仓库 Eclipse 极有可能会卡死 & ...

  10. NSData与UIImage之间的转换

    1 //NSData转换为UIImage 2 NSData *imageData = [NSData dataWithContentsOfFile: imagePath]; 3 UIImage *im ...