头图

1. 背景

在Java中我们定义常量通常用final static TYPE variableName = xxx来实现,在C语言中我们通常用预编译宏来实现:#define MAX 100,在C++中虽然我们仍可以使用预编译宏,但是已经不推荐这么干了。在Effective c++ 的条款1中:提到“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。而且有时候用宏,会出现意想不到的输出结果。const是C++给我们提供的新的定义常量的方法:

const int MAX = 100;
const std::string NAMESPACE = "android";

他们主要有下面几点区别:

  1. 类型和安全检查不同:宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,容易出错;const常量有类型区别,需要在编译阶段进行类型检查;
  2. 编译器处理方式不同:宏定义是一个"编译时"概念,在预处理阶段展开,不能对宏定义进行调试;const常量是一个"运行时"概念,在程序运行使用,类似于一个只读变量;
  3. 存储方式不同:宏定义是直接替换,不会分配内存,存储于程序的代码段中;const常量需要进行内存分配,存储于程序的数据段中;
  4. 定义域不同:就算是定义在方法体内的宏变量也可以在其他地方任意访问,而const不行;
  5. 定义后能否取消:宏定义可以通过#undef来取消,const常量定义后将在定义域内永久有效;
  6. 是否可以做函数参数:宏定义不能作为参数传递给函数,const常量可以在函数的参数列表中出现。

其实const做定义常量很好理解,而且和宏定义的区别也很好理解,只要记住宏展开只是替换,而const常量其实是只读的变量,上述的区别就很好理解了。但是关于const还有很多容易踩到的坑,下面我们在做一些详细分析和记录。

2. const修饰变量的注意事项

  1. const对象一旦创建后它的值就不能改变,所以const对象必须初始化。
  2. 与非const类型所能参与的操作相比,const类型的对象能完成其中的大部分,主要的区别点在于const类型的对象上执行不改变其内容的操作。利用一个对象初始化另外一个对象,它们是不是const都不重要:
int i = 100;
const int MAX = i;//正确
int j = MAX; //正确
  1. 默认状态下,const对象仅在文件内有效,如果需要在多个文件中使用,那么不管声明还是定义都要添加extern关键字。

3. const和复合类型结合

复合类型是基于其他类型定义的类型,我们接触到最常用的有引用和指针。

3.1 const的引用

把引用绑到const对象上,称为对常量的引用。对常量的引用被不能被用作修改它所绑定的对象:

const int a = 100;
const int &ar = &a;//正确
ar = 200; //错误,ar是对常量的引用
int &ar2 = a;//错误,让一个非常量引用指向一个常量对象。

还有一个关于引用的例外情况:

double pi = 3.14;
int &r1 = pi; //错误:Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
const int &r2 = pi;//正确

为什么double类型的对象绑定到非const的int引用报错,而绑定到const的int引用正常呢?

这里面想要绑定涉及到一个强制类型转换的问题,double转int会强转:

double pi = 3.14;
int temp = pi;
int &r = temp;

这里面r引用绑定了一个临时量对象。临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。如果r不是常量引用,那么就允许改变r所引用对象的值,此时r引用绑定的是temp而不是pi。而我们即使要改变也是想改变pi而不是temp,既然我们基本上不会想着把引用绑定到临时量上,所以C++把这种行为归为非法。

3.2 指向const的指针

不能用于改变指针所指对象的值的指针称为指向常量的指针

const int i = 100;
const int * ip = &i;
*ip  = 200;//错误,不能给const指针指向的对象赋值。

注意:常量指针和常量引用不过是一种“自以为是”的规则,不能通过该指针或者该引用改变对象的值,但是他们指向的对象不一定是一个常量,可以通过其他句柄来修改他们指向对象的值:

int i = 100;
const int *ip = &i;
i = 300;//正确
std::cout<< "*p = " << *ip << std::endl;

和对常量的引用一样,指向常量的指针所指的对象不是一定要是常量。它们都可以共同其他句柄修改被它们引用或者指向的对象的值。

3.3 const指针

指针是对象而引用不是,所以C++允许把指针本身定为常量。常量指针必须初始化。C++中把*放在const关键字之前用以说明指针是一个常量,不变的是指针本身的值而非指向的那个值。

int i= 0;
int *const pi = &i;
int j = 1;
pi = &j;//错误 Cannot assign to variable 'pi1' with const-qualified type 'int *const'

最佳实践:面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真是含义。

离pi最近的是const,那么pi本身是一个常量对象,对象的类型由声明符的其余部分确认,声明符的下一个符号是*,意味着pi是一个常量指针,最后,声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。

int i = 10;
const int *pi  = &i;

这个pi解释为pi是一个指针,指向的是int对象,并且这个对象是常量。

3.4 指针的顶层const和底层const

我们经常在代码中看到下面这种形式:

int i = 10;
const int *const pi = &i;

因为指针本身是个对象,它又可以指向一个对象,所以一个指针既可以是常量也可以同时指向一个常量,所以就有上面的形式的指针。

  • 顶层const:表示指针本身是个常量,可以表示任意的对象是常量,对任何数据类型都适用;
  • 底层const:表示指针所指的对象是一个常量,只和指针与引用等复合类型的基本类型部分有关系。
int i = 10;
const int j =100; //顶层const
int *const p1 = &i;//顶层const 
const int* p2 = &i;//底层const
const int *r = i; //底层const

执行对象拷贝时,顶层const不受影响,但是拷入和拷出的对象必须有相同的底层const,或者两个对象的数据类型必须能够转换(非常量可以转换为常量,反之不行)。

可以这样理解,一个可以被修改的对象我们可以暂时不允许它修改,但是一个不可以修改的对象允许修改却是不允许的。

4. 总结

本文介绍了const与宏定义常量的区别,以及const修饰变量的注意事项,绑定常量的引用、指向常量的指针和常量指针。并介绍了顶层const与底层const的区别。


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei