关于常量引用,左值引用,右值引用的一些理解

简单理解

代码块1:

1
2
3
double dval = 3.14;
// 使用右值引用接受隐式转换产生的临时量(字面量)
int &&rd = dval;

代码块2:

1
2
3
4
5
6
double dval = 3.14;
const int &ri = dval;
// 实际赋值过程如下,产生了一个中间变量
double dval = 3.14;
const int temp = dval;
const int &rt = temp;

上述代码都可以编译通过,看代码块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.14int 生成的临时 3)。
  • 它们的生命周期仅限于当前表达式(除非被引用延长)。

示例1:算术类型转换

1
2
3
double dval = 3.14;
int &&rd = dval; // 隐式生成临时int(右值),绑定到右值引用
const int &ri = dval; // 同样绑定到临时int(右值)

示例2:类类型转换

1
2
3
4
5
6
7
8
9
10
class A {
public:
A(int x) {} // 隐式构造函数
};

void func(A a) {}

int main() {
func(42); // 隐式生成临时A对象(右值)
}

2. 例外情况

隐式转换的中间值不一定是右值,如果转换结果本身是左值:

  • 当隐式转换通过返回左值引用的转换函数完成时,中间值为左值。

示例3:转换函数返回左值引用

1
2
3
4
5
6
7
8
9
10
11
class B {
public:
int value = 42;
operator int&() { return value; } // 返回左值引用
};

int main() {
B b;
int &ri = b; // 隐式转换返回左值引用(左值)
ri = 100; // 修改的是b.value
}

这里,Bint 的隐式转换返回的是对成员 value 的左值引用,因此中间值是左值。


3. 关键区分

  • 右值:临时对象、字面量(如 42)、表达式计算结果(如 a + b)。
  • 左值:具名对象、函数返回左值引用的结果、可通过地址访问的实体。

示例4:隐式转换的中间值类型

1
2
3
4
5
6
7
8
9
10
11
class C {
public:
C(double x) {} // 隐式构造函数
operator int() { return 42; } // 返回右值(值类型)
};

int main() {
C c = 3.14; // 隐式转换生成的临时C对象(右值)
int x = c; // 隐式转换调用operator int(),生成右值
const int &rx = c; // 绑定到隐式转换生成的右值
}

4. 总结

场景 中间值类型 原因
算术类型转换 右值 临时对象无法寻址
类构造函数隐式转换 右值 生成临时对象
转换函数返回值类型 右值 operator int()
转换函数返回左值引用 左值 operator int&()

因此:

  • 大部分隐式转换生成的中间值是右值
  • 若隐式转换返回左值引用,则中间值为左值(需显式定义相关转换函数)。

详解explicit

在C++中,explicit 是一个关键字,用于修饰构造函数类型转换运算符,以禁止编译器在某些上下文中进行隐式类型转换。它的主要作用是增强代码的安全性,避免意外的隐式转换导致的潜在问题。


1. explicit 的作用

  • 禁止隐式转换explicit 修饰的构造函数或转换运算符只能用于显式转换,不能用于隐式转换。
  • 提高代码清晰度:强制程序员明确表达意图,避免隐式转换带来的歧义。

2. explicit 的适用场景

(1) 构造函数

当类的构造函数只有一个参数(或除第一个参数外都有默认值)时,该构造函数可以用于隐式类型转换。如果希望禁止这种隐式转换,可以将构造函数声明为 explicit

示例1:隐式转换的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public:
A(int x) : value(x) {} // 允许隐式转换
int value;
};

void func(A a) {
std::cout << a.value << std::endl;
}

int main() {
func(42); // 隐式调用 A(int),将 42 转换为 A 类型
}
  • 这里 func(42) 会隐式调用 A(int) 构造函数,生成一个临时 A 对象。
  • 这种隐式转换可能导致代码难以理解,甚至隐藏潜在的错误。
示例2:使用 explicit 禁止隐式转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
public:
explicit A(int x) : value(x) {} // 禁止隐式转换
int value;
};

void func(A a) {
std::cout << a.value << std::endl;
}

int main() {
// func(42); // 错误:不能隐式调用 explicit 构造函数
func(A(42)); // 正确:显式调用构造函数
}
  • 通过 explicit 修饰构造函数,func(42) 会编译失败,必须显式调用 A(42)

(2) 类型转换运算符

explicit 也可以用于修饰类型转换运算符,禁止隐式调用转换函数。

示例3:隐式类型转换运算符
1
2
3
4
5
6
7
8
9
class B {
public:
operator int() { return 42; } // 允许隐式转换
};

int main() {
B b;
int x = b; // 隐式调用 operator int()
}
  • 这里 int x = b; 会隐式调用 operator int(),将 b 转换为 int
示例4:使用 explicit 禁止隐式转换
1
2
3
4
5
6
7
8
9
10
class B {
public:
explicit operator int() { return 42; } // 禁止隐式转换
};

int main() {
B b;
// int x = b; // 错误:不能隐式调用 explicit 转换运算符
int x = static_cast<int>(b); // 正确:显式调用转换运算符
}
  • 通过 explicit 修饰转换运算符,int x = b; 会编译失败,必须显式调用 static_cast<int>(b)

3. explicit 的优点

  1. 避免意外的隐式转换

    • 隐式转换可能导致难以发现的错误,例如将 int 隐式转换为类类型时,可能产生不符合预期的行为。
    • explicit 强制显式转换,使代码更安全。
  2. 提高代码可读性

    • 显式转换明确表达了程序员的意图,使代码更易于理解和维护。
  3. 防止歧义

    • 在某些情况下,隐式转换可能导致函数重载解析的歧义,explicit 可以避免这种问题。

4. explicit 的注意事项

  1. 仅适用于单参数构造函数

    • 多参数构造函数不能用于隐式转换,因此不需要 explicit
    • 例外:如果多参数构造函数中除第一个参数外都有默认值,则仍可能用于隐式转换。
  2. C++11 扩展

    • 在 C++11 之前,explicit 只能用于构造函数。
    • 从 C++11 开始,explicit 可以用于类型转换运算符。
  3. 显式转换的方式

    • 使用 static_cast 或直接调用构造函数(如 A(42))。

5. 实际应用场景

(1) 防止隐式构造

1
2
3
4
5
6
7
8
9
10
11
class String {
public:
explicit String(int size) { /* 分配内存 */ }
};

void printString(const String& s) {}

int main() {
// printString(10); // 错误:不能隐式调用 explicit 构造函数
printString(String(10)); // 正确:显式调用
}

(2) 防止隐式类型转换

1
2
3
4
5
6
7
8
9
10
class Boolean {
public:
explicit operator bool() const { return true; }
};

int main() {
Boolean b;
// if (b) { /* ... */ } // 错误:不能隐式调用 explicit 转换运算符
if (static_cast<bool>(b)) { /* ... */ } // 正确:显式调用
}

6. 总结

特性 隐式转换 显式转换(explicit
构造函数 允许隐式调用 必须显式调用
类型转换运算符 允许隐式调用 必须显式调用
优点 代码简洁 避免意外行为,提高代码安全性
适用场景 简单、明确的转换 复杂或易出错的转换
  • explicit 的核心作用:禁止隐式转换,强制显式表达意图。
  • 使用建议:在构造函数和转换运算符中,优先使用 explicit,除非有明确的理由允许隐式转换。
作者

神明大人

发布于

2025-02-13

更新于

2025-03-09

许可协议