文章目录
前言
这不能说是很严谨,只是根据现象归纳结论。
1 const的分类
可将const分为顶层const和底层const
- 顶层const:修饰变量本身不可以修改
- 底层const:在指针或引用中,修饰变量指向的内存不可修改
2 不同类型中,有哪些const,以及它们的意义
这里把类型分为三种
-
普通类型(右值引用类型也是)。例如
int
只有顶层const,底层const无效。顶层const修饰变量为只读 -
引用类型(左值引用)。
只有底层const,顶层const无效。底层const修饰绑定的变量为只读理解1
将int& a
理解为int * const p
即指针常量。自然而然,const int & a
也就是const int* const p
,引用类型前的const也就是一个底层const。理解2(推荐)
比如const T&
。可以这样理解,const是一个用来修饰类型T
的顶层const,限制类型T
不可修改。 -
指针类型。
既有顶层const,也有底层const。顶层const修饰指针的指向不可改变(这也是为什么引用只可以在初始化时绑定一次变量的原因)。底层const修饰指针指向的内容不可改变。
3 值的类型
既然要说传参,那么,就不得不对实参进行分类讨论
- c++11标准,将值分为 左值和右值。
- c++17标准,将值分为 左值、纯右值和将亡值
将亡值就是即将移动的对象,马上就要消亡了。
可是,由于右值引用是在C++11出现的。因此,在传参过程中,对右值引用推导的处理还是要归为对右值的处理。下面会讨论不同函数参数对左值和右值(纯右值、右值引用)的处理。
4 在传参过程中,const的变化
这里分为三类:左值引用限定符模板传参、右值引用限定符模板传参以及非引用模板传参
这里是模板声明定义,以及一些变量的定义
template <typename T>
void fun(T a) {}
template <typename T>
void cfun(const T a) {}
template <typename T>
void lfun(T& a) {}
template <typename T>
void clfun(const T& a) {}
template <typename T>
void rfun(T&& a) {}
template <typename T>
void crfun(const T&& a) {}
//变量定义
int i = 1;
const int ci = 1;
int* p = nullptr;
const int* cp = nullptr;
int* const pc = nullptr;
1 传参过程中的一些规则
- 在值传递过程中,顶层const会被忽略
int* const a = 1;
fun(a); //T 被推导为int*,const会被抛弃
- 在引用传递或地址传递时,顶层const会参与传递。
引用传递:函数参数包含引用限定符。(把引用看成指针常量,可以归为地址传递)
地址传递:对变量取地址作为实参传递。
int* const a = 1;
const int b = 1;
lfun(a); // 引用传递,T被推导为int * const,指针本来就有顶层const,还是顶层const
lfun(b); // 引用传递,T被推导为const int,顶层const变为底层const。 不论是原来的顶层const
// 还是引用类型的底层const, 最终都是要限制变量b和引用绑定的变量b,不可修改
fun(&b); // 地址传递,T被推导为const int *。原因同上
- 引用折叠,只会出现在函数参数包含引用限定符的函数模板中。
规则:X& &&、X& &、X&& &
都被解释为X&
,X&& &&
被解释为X&&
。(在右值引用解析中会用到) - 如果函数参数中,已有const,那么,推导出的
T
会去掉实参推导出的const。
去掉const情况:如果
const int b = 1;
const int* p = nullptr;
//T会被推导为 const int*类型,const T就是 const int *const类型
//const int *p 中的const是底层const,会有效传递给模板参数,而函数参数中也有一个const,
//它会自动成为指针的顶层const,最终const T就是 const int *const类型
fun(p);
//T 会被推导为int, const T& 就是const int&
//const int b中的const是顶层const,在引用传递时会被提升为底层const。而由于,函数参数中
//已有const,并且它是一个引用类型只能有一个底层const,它会自动成为底层const,
//所以实参的底层const被抛弃
clfun(b);
- 应还有一条特殊的,数组名作为实参传递,被传递给普通模板会退化为普通指针类型,被传递给引用模板会传递数组对象。
2 非引用模板传参
左值和右值的基本类型都会被推导为int,顶层const不传递。
函数模板声明定义
template <typename T>
void fun(T a) {}
template <typename T>
void cfun(const T a) {}
以下示例
fun(i); //T会被推导为int
fun(ci); //T会被推导为 int
fun(1); //T会被推导为int
fun(std::move(ci)); //T会被推导为int
//ci 是 const int类型,std::move(ci) 是const int&&类型,const是顶层const忽略,因此为int
对于 cfun
可以自己尝试,差不多,值得注意的是 cfun(p)
,函数参数中的const优先成为顶层const。
3 左值引用模板传参
左值和右值的基本类型都会被推导为int,修饰词另算。
template <typename T>
void lfun(T& a) {}
template <typename T>
void clfun(const T& a) {}
以下示例(第三个和第四个很有意思)
lfun(i); //T 推断为 int
lfun(ci); //T 推断为 const int
lfun(std::move(i));
//错误。std::move(i)是 int&&类型,一个右值,T会被推导为int,那么,T&为int&,
//只能绑定可修改的左值,自然绑定右值
lfun(std::move(ci));
//正确。std::move(ci)是 const int&&类型,T会被推导为 const int,那么,T&为const int&
//这是可以接受右值的,因此OK
clfun(ci); //T 推断为 int, 因为const已有,实参传递的const 被抛弃
总结:
T&
通常只接受左值,也可以接受const修饰的右值const T&
可以接受任意类型
4 右值引用模板传参
把这个独立出来,它很不一样。上述有些规则在
const T&&
中不满足。
- 左值会被推导为左值引用
- 右值正常推导
template <typename T>
void rfun(T&& a) {}
以下示例
rfun(i); // T被推导为 int&,因此 T& 是int& &,引用折叠后变为 int&
rfun(ci);// T被推导为 const int& , 同上
rfun(1); // T被推导为 int, T&为int&&
总结:
T&&
可以用来绑定任何类型的值const T&&
只能用来绑定右值
5 归纳几个重点
-
const的分为顶层const和底层const,他们的作用是什么。
-
普通类型只有顶层const,引用类型也只有’顶层const’(特殊的)。
对于引用类型,在初始化时就已经确定了绑定的对象,也就是引用类型的顶层const。既然确定了绑定的对象,那么,这个绑定的对象是否可修改呢?它就由引用类型的底层const,也就是类型
T
的顶层const确定。 -
变量的底层const,在传参过程中会被考虑。变量的顶层const,只有在引用传递和地址传递时才会被考虑
-
在已有const的函数参数中,模板参数
T
可能会收到影响。
比如:const丢失
以上讨论的是函数传参过程中const的行为。