C/C++ const

const类型

  1. const修饰的变量一经初始化,不能改变其值,而且必须进行初始化。
  2. 可以使用任意对象(包含非const对象)初始化const对象。
  3. const对象默认只在当前文件中有效。如果需要在其他文件中使用,在声明和定义的文件中都需要加上extern关键字;而非const对象在定义时不需要使用extern关键字,只需要在其他文件的声明中加上extern即可。
  4. 编译器遇到const变量时,会将所有该变量出现的地方都换成相应的值。

引用和const

可以把应用绑定到const对象上,就像绑定到其他常量上,称为对常量的引用(reference to const),和普通引用的不同在与,对常量的引用不能用作修改它绑定的对象:

1
2
3
4
5
const int ci = 1024;
const int &r1 = ci;

r1 = 42; //这行代码是错的,因为r1是对常量的引用
int &r2 = ci; //这行代码也是错的,因为r2是普通引用

常量引用是对常量的引用的简称。严格来说,并不存在常量引用,因为引用不是一个对象,我们没办法让引用本身恒定不变。但是因为C++中,引用绑定的对象是不能更改的,所以从这层意义上理解,所有的引用又都称得上常量。引用的对象是常量还是非常量决定了引用能进行的操作,但是影响不到引用和对象的绑定关系。

初始化和对常量的引用

之前说过引用的类型对象必须和引用对象的类型一样。但是有两个例外,**一个例外就是初始化常量引用时可以使用任意表达式作为初值。**如:

1
2
3
4
5
int i = 43;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1*2;
int &r4 = r1*2;

根据上面的例子我们也可以看出,对常量的引用可能引用一个非常量对象,我们不能通过r改变i的值,但是我们可以直接改变i的值,因为ri是绑定的,所以r的值也改变了。

当把一个常量引用绑定到另一种类型上的时候,会发生什么?

1
2
double dval = 3.14;
const int &ri = dval;

上述代码将一个dobule类型的变量绑定到一个const int型引用上,为了让ri绑定一个整数,编译器将上述代码变成了以下形式:

1
2
const int temp = dval;
const int &ri = temp;

ri绑定到了一个临时对象temp上,但是我们想要ri操作的是dval而不是temp,所以这种行为被判定为非法。

引用和左值,右值

  1. 非常量引用的初始值必须是左值。
  2. 常量引用的初始值可以是右值。比如const int &a = 7;
  3. 常量引用的初始值可以为左值。

指针和const

指针也可以指向常量。类似于对常量的引用(常量引用),有指向常量的指针(pointer to const)。

  1. 之前说过指针指向的类型对象必须和指针的类型一样。但是有两个例外,一个例外就是指向常量的指针可以使用非常量对象的地址进行初始化。
  2. 要想存放指向常量的地址,必须使用指向常量的指针。
  3. 指向常量的指针不能用于改变其所指对象的值,很容易明白,因为指向的是常量,而常量的值是不能修改的,所以指向常量的指针自然不能修改它指向的对象的值。
  4. 那么指向常量的指针的指向能否改变?当然可以了,要不然就不会有const指针了,即指针本就就是常量对象,指向不能改变的指针对象。

可以认为指向常量的指针和对常量的引用都是他们自认为指向了常量对象,而常量对象不能被修改,所以就不能修改所指对象的值。

1
2
3
4
const double pi = 3.14;
double *ptr = π //报错,必须是指向常量的指针
const double *cptr = π //
*cptr = 4; //不能修改常量指针指向的常量的值

const指针

引用不是对象,而指针是对象,因此就像其他对象一样,可以把指针本身定义为常量,叫做常量指针,常量指针必须初始化。
const指针必须初始化,它指向变量的值能否改变只取决于它指向变量的类型。

1
2
3
4
int val = 3;
int *const curErr = &val; //
const double pi = 3.14;
const double *const pip = π //

顶层const

用顶层const表示指针本身是一个常量,用底层const表示指针指向的对象是一个常量。指针可以既是顶层const又是底层const
更一般的,顶层const可以表示任何的对象是常量,这一点对任意类型都使用。而底层const则和指针,引用等复合类型有关。
可以把一个顶层const赋值给非顶层const对象;但是不能把底层const赋值给非底层const对象,这个多加几句,因为底层const一定是和复合类型指针和引用相关的,所以要把它赋值给一个非底层的const对象,这个待赋值的对象也一定是指针和引用,如果它是非底层const对象,那么就可以通过它修改它指向的对象了,而这个对象是const的,不能被修改。

对常量的引用(常量引用)可以和常量对量绑定;
对常量的引用(常量引用)可以和非常量对量绑定;
非常量引用不可以和常量对象绑定;
非常量引用可以和非常量对象绑定;

指向常量的指针可以指向常量对象;
指向常量的指针可以指向非常量对象;
常量指针可以指向非常量对象;
常量指针不可以指向常量对象;
普通指针不可以指向常量对象;
普通指针可以指向非常量对象;

常量指针(顶层const)可以用来初始化非常量对象。
对常量的引用和指向常量的指针(底层const)不可以用来初始化非常量对象。

constexpr和常量表达式,constexpr函数

  1. 常量表达式,值不会改变,并且在编译过程中就能得到计算结果的表达式。字面值和用常量表达式初始化的const对象也是常量表达式。比如:

    1
    2
    int size = 23;  //不是常量表达式,因为他不是`const`对象
    const int sz = get_size(); //sz是常量,但不是常量表达式,因为它的值需要等到运行时才能获得。

  2. 常量表达式一定是常量,但是常量不一定是常量表达式。

  3. 声明为constexpr的变量由编译器验证它是否是常量表达式。

  4. 声明为constexpr的变量一定是个常量,而且必须用常量表达式初始化。

  5. 如果认定变量一定是个常量表达式,就把它声明成constexpr类型,即constexpr用于声明常量表达式。

  6. constexpr函数的形参和返回值都必须是字面值类型,并且函数体有且只有一条return语句。

  7. constexpr函数不一定返回常量表达式。

字面值类型

算术类型,引用和指针都属于字面值类型,string,IO库和类不属于字面值类型。算术类型包含整形和浮点型,整形中又包含整数,字符和布尔。constexpr只能用于字面值类型。
指针和引用能用定义成constexpr,但是初值受到严格限制,一个constexpr指针的初始值必须是nullptr0或者某个固定地址的对象。一般来说,函数内部的变量(除了static变量)没有存在固定地址中,而所有函数之外的对象地址固定不变,能用来初始化constexpr指针。constexpr声明的指针是顶层const,即指针本身是个const,它指向的对象不能变,它指向的对象的值能变。

const_cast

  1. const_cast只能改变运算对象的底层const,将常量对象转换成非常量对象,这种性质叫做去掉const性质。如果对象本身不是一个常量,使用强制类型转换获得写权限是一个合法的行为,如果对象是一个常量,使用const_cast执行写操作就会产生未定义的后果。const_cast还可以将一个非const对象变成const对象。

    1
    2
    3
    4
    5
    6
    7
    8
    string s1("hello"); 
    const string s2("world");

    string *p1 = &s1;
    const string *p2 = &s2;

    string *p3 = const_cast<string *>(p2); //去掉底层const,但是通过p3写它指向的东西是未定义行为。
    const string *p4 = const_cast<const string*>(p1); //将非底层const转换成底层const。

  2. 只有const_cast能改变表达式的常量属性,使用其他类型的命名强制类型转换改变表达式的常量属性都会引发编译器的错误,注意不能使用const_cast改变表达式的类型。

  3. 通常用于有函数重载的上下文。

const形参和实参

当形参是const时,必须注意顶层const,顶层const作用于对象本身。当用实参初始化形参时,会忽略掉顶层const,即形参的顶层const被忽略掉了。当形参有顶层const时,传递给它常量或者非常量对象都是可以的。

指针或者引用形参和const

形参的初始化方式和变量的初始化方式是一样的,所以指针或者引用形参和const结合时,按照const变量的初始化规则执行就行。

尽量使用常量引用

把函数不会修改的形参定义成普通的引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。(比如,不能把const对象,字面值或者需要类型转换的对象传递给普通的引用传参)

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

重载和const

重载和const形参

顶层const不影响传入函数的对象

  1. 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
  2. 如果形参是某种类型的指针或引用,即形参是底层const,区分其指向的是常量对象还是非常量对象可以实现函数重载。

const_cast和重载

const_cast在重载函数的情景中有最有用。如下函数:

1
const string& stringCompare(const string &s1, const string &s2);

函数的形参是底层const,可以接收常量或者非常量的实参。但是返回的都是const string的引用,如果输入是两个string的引用,返回一个const string的引用显然是不合理的,这时候就可以使用const_cast了。对上述代码做一个改进:

1
2
3
4
5
6
string& stringCompare(string &s1, string &s2)
{

auto &r = stringCompare(static_cast<const string &>(s1), static_cast<const string &>(s2)a);
return static_cast<string &>(r);
}

参考文献

1.《C++ Primer》第五版
2.https://www.zhihu.com/question/36052573/answer/65756850