有关 QString::toStdString() 使用的一个细节问题

之前有朋友问起一个看起来比较“奇怪”的问题,记录如下。因为这个问题豆子也曾经遇到,初学者很有可能感到迷惑。

当时的代码是这样子的:

// 1
QString str = "Hello, world!";
char *cStr = str.toStdString().c_str();

// 2
std::string sstr = str.toStdString();
char *cStr2 = sstr.c_str();

// 3
func(str.toStdString().c_str());

我们主要关心的是几个toStdString()函数出现的位置。前面两个语句(第2行和第4、5行)看似是一样的行为,其实不然。当你分别运行这两段代码的时候会发现,第一段很可能会出现段错误,第二段却能正常运行。而第一段和第三段又非常类似,不同之处在于前者是直接获得一个char *类似,后者则将其传给一个函数作为参数。奇怪的是,原本第一段出错的代码在第三段中却是正常的。

第一段和第二段的区别之处在于,第一段代码直接调用了QString::toStdString()函数的返回值std::stringc_str()函数,第二段先将QString::toStdString()函数的返回值保存下来,然后再调用其c_str()函数。

为了解释前两段代码执行效果的不同,我们需要检查QString::toStdString()的签名:

std::string QString::toStdString() const;

这个函数返回一个与这个QString内容相同的std::string对象。注意这个函数的返回值是一个对象。在 C++ 中,函数返回对象一般是类似下面的代码:

clazz foo() const
{
    clazz c;
    c.member = 0;
    return c;
}

注意这里的返回对象,其实是一个临时对象。在上面代码中,虽然我们在函数体内创建了一个clazz的对象c,但返回的并不是“这个”对象,而是由 C++ 创建一个临时对象,再将这个临时对象返回。注意这里是“临时对象”,临时对象是有生命周期的。《C++ 程序设计语言》第 10 章中写道,“除非一个临时对象被约束到某个引用,或者被用于作为命名对象的初始化,否则它将在创建它的那个完整表达式结束时销毁”。所谓“完整表达式”,是指不是其它表达式的子表达式的表达式。简单来说,一个完整表达式的标识一般是一个分号。

这句看似绕口的话解释了之前所有的现象。在第一段代码中,由于函数返回一个临时变量,我们立即调用了这个临时对象的c_str()函数。这一切都没有问题。之后,完整表达式结束(遇到分号),而这个临时变量没有赋值给某个引用或用于给某个对象初始化,所以这个临时变量被立即销毁。由此对象获得的c_str()函数结果同样被销毁,因此发生段错误。在第二段代码中,这个临时变量用于给sstr对象初始化,我们之后调用的是这个新的被初始化完成的对象的函数,也就是正常的。第三段代码虽然也没有赋值给某个引用或用于给某个对象初始化,但在str.toStdString().c_str()语句结束后,表达式并没有结束,而是继续执行函数调用。直到函数调用返回,才遇到代表表达式结束的分号,此时临时变量才会销毁。而这时候我们已经成功执行了函数代码。所以一切都没有问题。

至此我们明白了这种看似奇怪的现象其实只是一个 C++ 语言的陷阱,甚至与 Qt 没有一点关系。同样类似的陷阱还可能发生在QString::toUtf8()QString::toAscii()之类的函数身上。

6 Comments

  1. zhouqian 2014年4月14日
    • 豆子 2014年4月14日
  2. chonghua 2014年4月17日
    • 豆子 2014年4月18日
  3. Sirit 2014年4月22日
    • 豆子 2014年4月23日

Leave a Reply