C++临时对象生命周期

2023-12-13 10:51:48

引言

朋友问了我一段代码:

const string & foo(const string & a, const string & b)
{
	return a.empty() ? b : a;
}
int main ()
{
	auto & s = foo("", "foo"); // auto is const string
	cout << s << '\n';
	return 0;
}

可以思考一下上面的代码能否通过编译?如果可以会输出什么?

UB

对于上面的代码,是可以通过编译的。使用GCC的话输出foo,那么代码似乎没有问题。
其实不然,上述的代码发生了UB(UB是Undefined Behaviour的缩写,意思是未定义行为既具体会发生什么由编译器的实现决定没有官方的要求)。
我们将上面的代码稍作更改:

const string & foo(const string & a, const string & b)
{
	return a.empty() ? b : a;
}
int main ()
{
	auto & s = foo("", "foo"); // auto is const string
	int a[100] = {0};
	cout << s << '\n';
	return 0;
}

在使用GCC的情况下,上面的代码输出了空串。

为什么?临时对象的生命周期

要知道为什么会出现上面的问题,我们需要先了解临时对象的生命周期。

  • 临时对象:临时对象往往是指右值(纯右值和将亡值)。
  • 生命周期:一个对象的生命周期可以理解为从调用构造函数开始到调用析构函数结束的整个过程。

当一个对象的生命周期结束后(调用析构函数后)其不能再被继续使用否则会发生未定义行为。
例如:通过new申请的对象,在delete之后继续进行解引用,此时会发生未定义行为(访问野指针)。
对于上面提到的代码实际上,在输出的时候,"""foo"的生命周期已然结束,所以出现了未定义行为,这是因为在string中用于存放数据的内存已经被回收了(临时对象调用过析构函数),但是s中依然有指向数据的指针(或者引用),当没有数据对对应地址进行写操作的时候,依然能够读出之前的数据,但是增加int a[100] = {0}之前的内存已经被覆盖,因此此时输出空串(但实际上由于是UB此时发生什么都是可以的,这里只是在根据结果解释)。
一个问题:string不是存放在堆上吗,而申请的int a[100]存放在栈上,为什么可以覆盖堆上的内容?

C++string的长度大于某个值时(这个值可能是16),其数据才回被放置在堆上,否则还是存放在栈上。同时string中存放着在栈上的数据,例如字符串长度变量,以及指针存放在栈上(使用new可以使其存放在堆上),通过int a[100] = {0}可以使长度清0和指针变成空指针。

简单说明了上面代码的问题之后,知道了是因为临时对象已经被析构了,导致其发生了未定义行为。那么,临时对象的生命周期究竟如何呢?

  • 对于没有绑定对引用的临时变量其创建完成后,即开始进行析构。例如string("aaa");从该语句的下一行开始,临时变量已经被析构。需要注意的是:string a = string("aaa");实际上会调用移动构造函数,所以该临时变量已经绑定到引用上了,此时不属于未绑定到引用上的临时变量。
  • 对于绑定到引用的临时变量,其生命周期在引用脱离作用域时结束。例如:
    {
    	string &&a = string("xxx");	
    } // string("xxx") is finalized here.
    
  • 特殊地,对于将同一个临时变量绑定到两个不同的引用上,其生命周期以第一个引用为准。例如:
    {
    	string &&a = string("xxx");
    	{
    		string &&b = std::move(a);
    	}
    	// using a here is OK
    } // string("xxx") is finalized here.
    

所以对于引言中的例子,在函数返回之后返回值赋值之前,临时变量已经被析构了。

参考

Lifetime of a temporary cppreference

文章来源:https://blog.csdn.net/qq_45523675/article/details/134835080
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。