12

我正在阅读 Eckel 的固定章节,并在解释 Temporaries 的部分混淆了。我能得到的是,当我们将引用传递给函数时,编译器会创建一个临时对象,它是一个 const 对象,因此即使我们将引用传递为

f(int &a){}

现在我尝试在网上查看其他一些临时文件的参考资料,结果被卡住了

http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage %2Fref%2Fcplr382.htm

都是 C++ 中的临时右值吗?

这促使我认为临时对象不仅仅是在函数内部传递引用和为其创建 const 对象。现在我可以从这两个链接中得到一些东西,但不能说我已经理解临时对象的工作、功能和使用所有的。如果有人可以解释临时人员的概念,那将非常有帮助。提前致谢。
bruce eckel 的原始示例是:

// Result cannot be used as an lvalue
class X {
    int i;
    public:
    X(int ii = 0);
    void modify();
};
X::X(int ii) { i = ii; }
void X::modify() { i++; }
X f5() {
    return X();
}
const X f6() {
    return X();
}
void f7(X& x) { // Pass by non-const reference
    x.modify();
}
int main() {
    f5() = X(1); // OK -- non-const return value
    f5().modify(); // OK
    // Causes compile-time errors:
    //! f7(f5());
    //! f6() = X(1);
    //! f6().modify();
    //! f7(f6());
} ///:~
4

4 回答 4

5

临时对象是未命名的对象(某些表达式的结果),并且始终是右值。或者也许最好说导致右值的表达式是临时的。

在 C 中,右值/临时对象并不是真正的对象(从某种意义上说,标准使用“对象”一词:位于内存中的东西)。因此,例如,它们不是 cv 限定的(像3 + 5has typeint和 not这样的表达式,并且int const函数返回值上忽略 cv 限定符)并且您不能获取它们的地址(因为它们不在内存中,他们没有地址)。在 C++ 中,类类型使问题变得模糊:您可以在右值上调用成员函数,并且该成员函数将具有this指针,这意味着即使(类类型的)右值也必须在内存中具有地址,并且 cv -qualifications 是有意义的,因为如果返回类型是 const,你不能在它上面调用非常量函数。

最后,尽管右值和临时的概念非常密切相关,但 C++ 标准以稍微不同的方式使用这些词。表达式的结果要么是右值,要么是左值(C++11 增加了其他可能性,但在您成为专家之前,您可以忽略它们),这种区别涉及所有类型。当 C++ 标准谈到临时的时,它是一个右值,它是(或已经成为)一个对象。在大多数情况下,它们具有类类型;在极少数情况下,您会在编写良好的代码中拥有一个不是类类型的临时文件,除非涉及模板。区别很重要,因为即使内置&运算符在右值上是非法的,类类型的右值具有定义的“生命周期”和内存地址。该生命周期直到完整表达式结束。因此,当涉及到类类型时,临时值和命名值的区别主要在于临时值没有名称,并且具有不同的生命周期。类类型的生命周期很重要,原因有两个:首先,它决定了何时调用析构函数,其次,如果对象“泄露”了一个指向内部数据的指针,例如std::string::c_str(),它决定了这个指针的有效时间。

最后,我提到了模板,因为这是您唯一一次拥有对非类类型的 const 引用。in 参数的通常约定是对非类类型按值传递,对类类型按 const 引用传递;然而,模板的作者不知道是否T将是一个类类型,并且在大多数情况下,将定义他的函数来接受一个T const&. 在实践中,这将是唯一一次你会得到一个非类类型的临时对象(如果模板保存了参数的地址,你可能会遇到问题)。

于 2013-02-28T09:18:28.167 回答
4

在 C++ 中,临时对象是编译器在各种上下文中创建的未命名对象。典型用途包括引用初始化、参数传递、表达式求值(包括标准类型转换)、函数返回和异常(抛出表达式)。

从这个链接

当创建临时对象来初始化引用变量时,临时对象的名称与引用变量的名称具有相同的范围。当在对完整表达式(不是另一个表达式的子表达式)求值期间创建临时对象时,它会作为其求值的最后一步被销毁,在词法上包含创建它的点。

完整表达式的销毁也有例外:

  1. 该表达式作为定义对象的声明的初始化程序出现:初始化完成后临时对象被销毁。
  2. 引用绑定到临时对象:临时对象在引用的生命周期结束时被销毁。

如果为具有构造函数的类创建临时对象,编译器会调用适当的(匹配的)构造函数来创建临时对象。

当临时对象被销毁且存在析构函数时,编译器调用析构函数销毁临时对象。当您退出创建临时对象的范围时,它会被销毁。如果引用绑定到临时对象,则临时对象在引用超出范围时被销毁,除非它在控制流中断之前被销毁。例如,由构造函数初始化程序为引用成员创建的临时对象在离开构造函数时被销毁。

在此类临时对象是冗余的情况下,编译器不会构造它们,以创建更有效的优化代码。当您调试程序时,尤其是内存问题时,这种行为可能是一个考虑因素。

让我们这样总结一下。可以出于以下原因创建这些临时对象:

  1. 使用与正在初始化的引用的基础类型不同的类型的初始化程序来初始化 const 引用。

  2. 存储返回用户定义类型的函数的返回值。仅当您的程序不将返回值复制到对象时,才会创建这些临时对象。因为返回值没有复制到另一个对象,所以创建了一个临时对象。创建临时变量的更常见情况是在计算表达式期间必须调用重载的运算符函数。这些重载的运算符函数返回一个用户定义的类型,该类型通常不会复制到另一个对象。

  3. 将强制转换的结果存储到用户定义的类型。当给定类型的对象显式转换为用户定义类型时,该新对象将被构造为临时对象。

让我们考虑这个例子

class X {
   / / ...
public:
   / / ...
   X(int);
   X(const X&);
   ~X();
};

X f(X);

void g()
{
   X a(1);
   X b = f(X(2));
   a = f(a);
}

X(2)在这里,一个实现可能会在将它传递给f()usingX的复制构造函数之前使用一个临时的来构造;或者,X(2)可以在用于保存论点的空间中构造。此外,在使用的复制构造函数f(X(2))将其复制到 b 之前,可以使用临时保存结果;X或者,f()的结果可能在b. 另一方面,表达式a=f(a)需要一个临时的结果f(a),然后分配给a

于 2013-02-28T08:12:02.337 回答
1

让我们从一个例子开始:

float f(int a);
int g(float b);
int main() { int a=10; std::cout << g(f(a)); }

函数调用如下所示:

int ---f----> float ---g---> int

当编译器为 g(f(a)) 生成代码时,它需要一些内存来存储调用 f() 后生成的浮点数。这个内存区域称为临时的。它是 f() 和 g() 之间的内存。

于 2013-02-28T08:11:30.350 回答
0

临时对象是计算的“副产品”。它们没有显式声明,顾名思义,它们是临时的。不过,您应该知道编译器何时创建临时对象,因为通常可以防止这种情况发生。

正如他的帖子中的几个答案详细介绍了临时工是什么,但我想添加一些与它们相关的“额外开销”相关的内容,但也有一些方法可以避免它们。

  1. 临时发生的最常见的地方是通过值将对象传递给方法。形式参数是在堆栈上创建的。这可以通过使用按地址传递或按引用传递来防止。

  2. 编译器可能会在分配对象时创建临时对象。例如,可以为将 int 作为参数的构造函数分配一个 int。编译器将使用 int 作为参数创建一个临时对象,然后对该对象调用赋值运算符。您可以通过在构造函数的声明中使用显式关键字来防止编译器在您背后执行此操作。

  3. 当对象按值返回时,经常使用临时对象。必须返回对象的方法通常必须创建要返回的对象。由于构造此对象需要时间,因此我们希望尽可能避免它。有几种方法可以做到这一点。

    3.a. 不是返回一个对象,而是向方法添加另一个参数,允许程序员传入程序员想要存储结果的对象。这样,该方法就不必创建额外的对象。它只会使用传递给方法的参数。这种技术称为返回值优化 (RVO)。

RVO 是否会导致实际优化取决于编译器。不同的编译器以不同的方式处理这个问题。帮助编译器的一种方法是使用计算构造函数。可以使用计算构造函数代替返回对象的方法。计算构造函数采用与要优化的方法相同的参数,但不是根据参数返回对象,而是根据参数的值初始化自身。

  1. 使用 = 运算符可以避免临时性。例如,代码

    a = b + c; 可以写成 a=b; a+=c;。

于 2013-02-28T08:27:00.440 回答