Featured image of post C++变量生命周期

C++变量生命周期

C++中变量的生命周期与作用域是有差别的,再结左值引用和右值引用,引用折叠,这次就来浅谈一下C++中变量的生命周期。

在C++11及以后的版本中,可以将变量简单的分为左值,右值。其中右值又可以分为纯右值和将亡值。 不同类型的变量的生命周期与作用域也是有差别的,这次就来浅谈一下C++中变量的生命周期。

事情的起因是最近在修一个C++的bug时,发现是因为变量的生命周期问题导致的,所以就来总结一下。

提交的PR

代码位置

bug分析

有bug的代码如下:

1HashesMetaValue tmf_meta_value2(DataType::kHashes, std::string(str, sizeof(int32_t)));
1using HashesMetaValue = BaseMetaValue;
2
3class BaseMetaValue : public InternalValue {
4 public:
5  explicit BaseMetaValue(DataType type, const Slice& user_value) : InternalValue(type, user_value) {}
6  ...................
7};

可以看到,BaseMetaValue的构造函数接受的是const Slice&类型的参数, 而std::string(str, sizeof(int32_t))是一个临时变量,是一个右值(将亡值), 所以这个临时变量的生命周期只在这一行代码中,当这一行代码执行完后,这个临时变量就会被销毁, 所以tmf_meta_value2user_value就是一个悬空指针,所以在后面的代码中就会出现问题。

Slice 是一个类似于std::string_view的类,Slice不拥有数据,只是一个指向数据的指针。所以必须由调用者保证数据的生命周期。

解决方法也很简单,将BaseMetaValue的构造函数改为接受std::string类型的参数即可。

1auto key = std::string(str, sizeof(int32_t));
2HashesMetaValue tmf_meta_value1(DataType::kHashes, key);

这样key就是一个左值,生命周期会持续到tmf_meta_value1的生命周期结束。所以就不会出现悬空指针的问题了。

C++中没有严格限制变量的生命周期,我没有系统的学习过rust,但是了解过rust的一些概念,rust中有严格的生命周期检查,这样就可以避免很多悬空指针的问题, 上述的bug在rust中是不会出现的。因为rust中有严格的变量所有权的,生命周期检查。 所有权 这个概念在rust中是非常重要,我觉得这个概念在C++中也是很重要的, 想要写出高质量的代码,就需要了解这所有权这个概念。

左值、右值

左值

最简单的一个例子就是变量,变量是一个左值,它有一个内存地址,可以取地址,可以修改。

1int a = 10;  // a 是左值
2int* p = &a; // 可以取 a 的地址
3a = 20;      // a 可以被赋值

右值

右值是指不能取地址的临时对象或值,通常是短暂存在的(ephemeral),无法出现在赋值语句的左侧。

1int b = 5 + 3; // 5 + 3 是右值(纯右值)

其中右值又可以分为纯右值和将亡值。

纯右值

特性:没有身份,无法取地址,无法被赋值

常见的 字面量临时对象函数返回值

142;          // 字面量是纯右值
25 + 3;       // 计算结果是纯右值
3std::string("hello"); // 构造函数返回的临时对象是纯右值

将亡值

将亡值是“即将消亡的值”,通常是通过右值引用绑定到的临时对象,或者被显式移动(move)的对象。

特性:有身份(可以取地址),但生命周期即将结束

通常出现在使用 std::move 或函数返回右值引用的场景

1#include <utility>
2int&& getRvalue() { return 42; }
3int main() {
4    int a = 10;
5    int&& r1 = std::move(a); // std::move(a) 是将亡值
6    int&& r2 = getRvalue();  // getRvalue() 返回的临时对象是将亡值
7}

变量的生命周期

左值

左值的生命周期由存储期决定,通常是持久的,可以控制。比如局部变量,离开作用域后会被销毁。 动态分配的内存也是左值,需要手动释放。

1void example() {
2    int a = 10;         // 左值 a,自动存储期,离开作用域销毁
3    static int b = 20;  // 左值 b,静态存储期,程序结束销毁
4    int* p = new int(30); // 左值 *p,动态存储期,直到 delete
5    delete p;
6}
  • a 的生命周期从定义到函数结束。
  • b 的生命周期贯穿整个程序。
  • p 的生命周期从 newdelete

右值

  • 纯右值:生命周期短暂,表达式结束后销毁,一旦表达式求值完成,纯右值要么被销毁,要么被用于初始化其他对象
1int main() {
2    5 + 3; // 纯右值,表达式结束后销毁
3    std::string s = std::string("hello"); // 临时对象是纯右值,构造 s 后销毁
4    const int& ref = 42; // 纯右值 42 的生命周期延长到 ref 的作用域结束
5}
  • 将亡值:生命周期取决于底层对象,通常是短暂的,或者可以被转移,可以通过右值引用绑定延长生命周期
1std::string&& rvalue = std::move(str); // str 是将亡值,通过 std::move 转为右值引用,延长生命周期

声明周期的延长

  • 绑定到常量左值引用 const & 当纯右值绑定到 const T& 时,临时对象的生命周期延长到引用的作用域结束。
    1const std::string& str = std::string("temp"); // str 有效直到作用域结束
    
  • 绑定到右值引用&& 当右值(纯右值或将亡值)绑定到 T&& 时,临时对象的生命周期延长到右值引用的作用域结束。
    1std::string&& str = std::string("temp"); // str 有效直到作用域结束
    
值类别生命周期特点是否可延长生命周期示例场景
左值 (lvalue)由存储期决定,持久且可控不需要延长,本身持久局部变量、全局变量
纯右值 (prvalue)临时,表达式结束后销毁可通过 const & 延长字面量、临时对象
将亡值 (xvalue)取决于底层对象,通常短暂或可转移可通过 && 绑定延长std::move、函数返回右值引用

左值引用、右值引用

单纯的左值和右值其实还是好理解的,但是结合C++中的引用,就会有一些特殊的情况,比如右值引用绑定到左值, 右值引用绑定到右值等等,这些情况就需要我们去了解C++中的引用的特性。

左值引用

先来看一下最简单的左值引用,我的理解,左值引用可以理解为一个别名,它是一个左值,可以取地址,可以修改,可以看做是一种简化的指针。

1int a = 42;
2int& ref = a;  // 左值引用,绑定到左值
3ref = 100;     // 修改ref实际上就是修改a

左值引用中,还有一个特殊的情况,就是 const 左值引用,它可以绑定到右值,但是不能修改。

1const int& ref = 42; // const 左值引用,绑定到右值

const 类型的左值引用,常用在函数的参数中,可以接受左值和右值。

1void func(const int& ref) {
2    // ref 可以绑定到左值或右值
3}
4
5int main() {
6    int a = 42;
7    func(a); // a 是左值
8    func(42); // 42 是右值
9}

为什么 const 左值引用可以绑定到右值呢?因为右值是临时的,不会被修改,const修饰的变量是不可以修改的, 所以可以绑定到const左值引用。

右值引用

右值引用是C++11引入的新特性,它是一个新的引用类型,用于绑定到右值。

1int&& rref = 100; // 右值引用,绑定到右值
2int x = 1013rref = x; // 错误,右值引用不能绑定到左值

右值引用有容易迷惑的地方,比如一个函数的参数是右值引用,但是这个右值在函数调用时是左值。

1void func(int&& ref) {
2    // ref 是右值引用, 但是在函数调用时是左值
3    ref = 100; // 可以修改
4}

两种引用在函数调用时的区别:

 1void func1(int& ref) {
 2    // ref 是左值引用
 3}
 4
 5void func2(int&& ref) {
 6    // ref 是右值引用
 7}
 8
 9void func3(const int& ref) {
10    // ref 是 const 左值引用
11}
12
13int main() {
14    int a = 100;
15    func1(a); // 正确 a 是左值
16    func1(101); // 错误 101 是右值
17    func2(102); // 正确 102 是右值
18    func2(a); // 错误 a 是左值
19    func3(a); // 正确 a 是左值
20    func3(103); // 正确 103 是右值
21}

引用折叠

引用折叠是C++11引入的新特性,也可以叫万能引用,用于简化引用的使用,主要用于模板的参数推导。 主要用在模板的参数推导中,当一个模板参数是引用时,引用折叠规则会决定最终的引用类型。

C++11引入右值引用 T&& 后,允许绑定到右值,同时在模板中可能出现复杂的类型推导情况,例如:

  • T& &
  • T& &&
  • T&& &
  • T&& &&

为了简化模板参数推导,引入了引用折叠规则:

引用折叠规则:

  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&
  • T&& && -> T&&

简单来说,左值引用 & 具有优先级,只要组合中出现左值引用,最终结果就是左值引用。 右值引用 && 只有在没有左值引用的情况下才会保留。

 1template<typename T>
 2void func(T&& param) {
 3    // param 的类型取决于传入的值类别
 4}
 5
 6int main() {
 7    int x = 100;
 8    func(x);      // T 推导为 int&,T&& 折叠为 int&
 9    func(101);     // T 推导为 int,T&& 保持为 int&&
10}
  • 当传入左值x时,T推导为int&T&&变成int& &&,折叠为int&
  • 当传入右值101时,T推导为intT&&保持为int&&

引用折叠生效的一个前提是模板参数要发生类型推导,如果模板参数是明确的类型,引用折叠规则不会生效。

1template<typename T>
2void func(std::vector<T>&& param); // 普通右值引用,只能绑定右值
 1template <typename T>
 2class A
 3{
 4public:
 5    void push(T&& t)
 6    {
 7        data_.push_back(std::forward<T>(t));
 8    }
 9
10private:
11    std::vector<T> data_;
12};
13
14int main() {
15    A<int> a;
16    // int x = 100;
17    // a.push(x);     // 编译错误:左值不能绑定到右值引用
18    a.push(101);      // 正确:右值
19    return 0;
20}

这里的 T&& 是一个右值引用,但是在模板参数推导时,会根据传入的参数类型来决定最终的引用类型。 因为在初始化 A 类时,T 的类型已经确定了,所以 T&& 不会发生引用折叠。

改进

 1template <typename T>
 2class A
 3{
 4public:
 5    template <typename U>
 6    void push(U&& u)
 7    {
 8        data_.push_back(std::forward<U>(u));
 9    }
10
11private:
12    std::vector<T> data_;
13};
14int main() {
15    A<int> a;
16    int x = 100;
17    a.push(x);         
18    a.push(101);        
19    return 0;
20}

这里的 U&& 是一个万能引用,它会根据传入的参数类型来决定最终的引用类型,所以可以正确的推导出左值和右值。

总结

C++中的变量可以分为左值和右值,右值又可以分为纯右值和将亡值。不同类型的变量的生命周期与作用域也是有差别的。

简单说有明确变量名,可以取地址,可以修改的是左值,没有明确变量名,不能取地址,不能修改的是右值。

左值和右值配合引用,可以更好的控制变量的生命周期,同时也会带来一些特殊的情况,比如引用折叠。

  • 左值引用:左值引用是一个别名,它是一个左值,可以取地址,可以修改。作为函数参数时,只能绑定左值。
  • const 左值引用:可以绑定到右值,但是不能修改。
  • 右值引用:右值引用是一个新的引用类型,用于绑定到右值。作为函数参数时,只能绑定右值。
  • 引用折叠:引用折叠是C++11引入的新特性,用于简化引用的使用,主要用于模板的参数推导。只有在发生类型推导时才会生效。

左值引用和右值引用,应该要配合 std::movestd::forward 使用,这样可以更好的控制变量的生命周期。 这里先挖一个坑,后面有空再来聊这部分内容。

发表了60篇文章 · 总计141.46k字
本博客已稳定运行
© QX
使用 Hugo 构建
主题 StackJimmy 设计