C++中的const

顶层const和底层const

定义

顶层const代表指针本身是一个常量,而底层const则表示指针指向的对象是一个常量。

int a = 10;
const int *p = &a;  // 底层const, 不能通过*p改变a,又叫指向常量的指针。
int* const p = &a;  // 顶层const, p本身的值不能发生改变,又叫常量指针。
const int* const p = &a; // 顶层const + 底层const 

拷贝操作

当进行拷贝操作时,顶层const不起作用(拷贝不会改变对象本身的值),但拷入和拷出对象的底层const应该一样,或者可以转化为相同的const类型(非const可以转化为const)。

const int a = 10;
const int b = a; // 正确,顶层const不起作用
int c = a;       // 正确,顶层const不起作用


const int *p1 = &a;   // 正确,底层非const可以转化为底层const
int *p2 = p1;         // 错误,p1的底层const不能转化为底层非const
int* const p3 = p1;   // 错误,p1的底层const不能转化为底层非const

constexpr

constexpr总是把对象声明为顶层const

constexpr int *p1 = nullptr; // 相当于 int* const p1 = nullptr;
constexpr const int *p2 = nullptr; // 相当于 const int* const p2 = nullptr;

typedef

在typedef复合类型的时候,不能简单地把typedef展开。下面的例子说明了这一情况。

typedef char *pstring;
const pstring cstr = 0;
const pstring *ps;

在这里,cstr是指向char的常量指针,而ps是一个指针,它指向的对象是一个指向char的常量指针。原因如下:这两条声明语句的基本数据类型都是 const pstring ,const是对给定类型的修饰,而给定的类型是 pstring ,也即 char * ,所以const修饰的是指向char的指针,也即,这个指针是一个常量,因此 const pstring 声明的类型是 char * const 类型。

这与直接把typedef展开时的理解是完全不同的。直接展开时,const pstring就变成了const char * ,前者的数据类型是指针,而后者的数据类型是char, * 只是声明符的一部分。如果直接展开的话,上述语句就变成了

const char *cstr = 0;
const char **ps;

这样一来,cstr就变成了一个指向char类型常量的指针,ps变成了一个指向const char * 类型的指针。

auto

auto一般会忽略掉顶层const和引用,保留底层const。

const int ci = 1, &cr = ci;
auto b = ci;  // int
auto c = cr;  // int
auto d = &ci; // const int *,因为对顶层const对象取地址得到底层const

当需要auto带顶层const时,应explicly写出。

const auto &r = 1;

decltype

decltype将会保留变量的顶层const和引用。

const int ci = 0, &cr = ci;
decltype(ci) x = 0;  // const int
decltype(cr) y = 0;  // const int &
decltype(cr) z;      // 错误,类型为const int &,必须绑定到一个对象上

若decltype接受一个变量,则推断出变量的类型;若接受一个表达式,则分两种情况:

  1. 表达式是一个左值,则得到对应类型的引用。(因为可以使用左值得到对应的对象,且能对对象进行赋值,这与引用的功能相同)

  2. 表达式是一个右值,则得到对应的类型。

若在变量的两边加上一对或多对括号,则变量会被当成是一个表达式,且这个表达式是一个左值,所以一定将得到对应类型的引用。

   C++
  最后修订:2022-9-2