关于常量引用,左值引用,右值引用的一些理解
简单理解
代码块1:
1 | double dval = 3.14; |
代码块2:
1 | double dval = 3.14; |
上述代码都可以编译通过,看代码块2,在将dval
的值赋值到ri
上的时候,会生成中间变量,这个变量是一个临时值,也就是一个右值,这时想要创建引用,就必须使用右值引用来接收,代码块1编译通过,证明了这一点。
int &b = dval;
会编译错误,不能使用左值引用来接受,是因为C++ 标准规定,非const
左值引用不能绑定到临时对象上。这种设计的初衷是为了防止程序员意外修改转换后临时对象的值,或者在引用一个临时对象时产生意外的副作用。
为什么可以用const int temp
来接收呢,因为临时量,也就是字面量,或者说右值,可以用来初始化变量,也可以用来初始化常量,int aa = dval;
使用字面量初始化一个变量,编译通过。
为什么const int &rt = temp;
就可以使用左值引用来接收,因为const int temp = dval;
,已经将右值固化,有了固定的内存地址,此时temp
是左值。两个步骤都必须存在const
的原因就是为了延长临时对象的生命周期,安全绑定,避免意外转换造成引用为空。
引用的底层是常量指针,指针本身不可以修改,指向的对象可以修改。常量引用就可以理解为指向常量的常量指针,指针本身以及指向的对象都不可以修改。
所有的隐式转换产生的中间值都是右值吗?
在C++中,隐式转换生成的中间值通常是右值,但存在一些特殊场景需要具体分析。以下是详细解答:
1. 基本规则
隐式转换(如算术类型转换、类类型的构造函数或转换运算符)生成的临时对象通常是右值,因为:
- 它们没有名称,无法被直接寻址(如
3.14
转int
生成的临时3
)。 - 它们的生命周期仅限于当前表达式(除非被引用延长)。
示例1:算术类型转换
1 | double dval = 3.14; |
示例2:类类型转换
1 | class A { |
2. 例外情况
隐式转换的中间值不一定是右值,如果转换结果本身是左值:
- 当隐式转换通过返回左值引用的转换函数完成时,中间值为左值。
示例3:转换函数返回左值引用
1 | class B { |
这里,B
到 int
的隐式转换返回的是对成员 value
的左值引用,因此中间值是左值。
3. 关键区分
- 右值:临时对象、字面量(如
42
)、表达式计算结果(如a + b
)。 - 左值:具名对象、函数返回左值引用的结果、可通过地址访问的实体。
示例4:隐式转换的中间值类型
1 | class C { |
4. 总结
场景 | 中间值类型 | 原因 |
---|---|---|
算术类型转换 | 右值 | 临时对象无法寻址 |
类构造函数隐式转换 | 右值 | 生成临时对象 |
转换函数返回值类型 | 右值 | 如 operator int() |
转换函数返回左值引用 | 左值 | 如 operator int&() |
因此:
- 大部分隐式转换生成的中间值是右值。
- 若隐式转换返回左值引用,则中间值为左值(需显式定义相关转换函数)。
详解explicit
在C++中,explicit
是一个关键字,用于修饰构造函数或类型转换运算符,以禁止编译器在某些上下文中进行隐式类型转换。它的主要作用是增强代码的安全性,避免意外的隐式转换导致的潜在问题。
1. explicit
的作用
- 禁止隐式转换:
explicit
修饰的构造函数或转换运算符只能用于显式转换,不能用于隐式转换。 - 提高代码清晰度:强制程序员明确表达意图,避免隐式转换带来的歧义。
2. explicit
的适用场景
(1) 构造函数
当类的构造函数只有一个参数(或除第一个参数外都有默认值)时,该构造函数可以用于隐式类型转换。如果希望禁止这种隐式转换,可以将构造函数声明为 explicit
。
示例1:隐式转换的问题
1 | class A { |
- 这里
func(42)
会隐式调用A(int)
构造函数,生成一个临时A
对象。 - 这种隐式转换可能导致代码难以理解,甚至隐藏潜在的错误。
示例2:使用 explicit
禁止隐式转换
1 | class A { |
- 通过
explicit
修饰构造函数,func(42)
会编译失败,必须显式调用A(42)
。
(2) 类型转换运算符
explicit
也可以用于修饰类型转换运算符,禁止隐式调用转换函数。
示例3:隐式类型转换运算符
1 | class B { |
- 这里
int x = b;
会隐式调用operator int()
,将b
转换为int
。
示例4:使用 explicit
禁止隐式转换
1 | class B { |
- 通过
explicit
修饰转换运算符,int x = b;
会编译失败,必须显式调用static_cast<int>(b)
。
3. explicit
的优点
避免意外的隐式转换:
- 隐式转换可能导致难以发现的错误,例如将
int
隐式转换为类类型时,可能产生不符合预期的行为。 explicit
强制显式转换,使代码更安全。
- 隐式转换可能导致难以发现的错误,例如将
提高代码可读性:
- 显式转换明确表达了程序员的意图,使代码更易于理解和维护。
防止歧义:
- 在某些情况下,隐式转换可能导致函数重载解析的歧义,
explicit
可以避免这种问题。
- 在某些情况下,隐式转换可能导致函数重载解析的歧义,
4. explicit
的注意事项
仅适用于单参数构造函数:
- 多参数构造函数不能用于隐式转换,因此不需要
explicit
。 - 例外:如果多参数构造函数中除第一个参数外都有默认值,则仍可能用于隐式转换。
- 多参数构造函数不能用于隐式转换,因此不需要
C++11 扩展:
- 在 C++11 之前,
explicit
只能用于构造函数。 - 从 C++11 开始,
explicit
可以用于类型转换运算符。
- 在 C++11 之前,
显式转换的方式:
- 使用
static_cast
或直接调用构造函数(如A(42)
)。
- 使用
5. 实际应用场景
(1) 防止隐式构造
1 | class String { |
(2) 防止隐式类型转换
1 | class Boolean { |
6. 总结
特性 | 隐式转换 | 显式转换(explicit ) |
---|---|---|
构造函数 | 允许隐式调用 | 必须显式调用 |
类型转换运算符 | 允许隐式调用 | 必须显式调用 |
优点 | 代码简洁 | 避免意外行为,提高代码安全性 |
适用场景 | 简单、明确的转换 | 复杂或易出错的转换 |
explicit
的核心作用:禁止隐式转换,强制显式表达意图。- 使用建议:在构造函数和转换运算符中,优先使用
explicit
,除非有明确的理由允许隐式转换。