c ++运算符重载内存问题

时间:2009-09-19 21:07:39

标签: c++ memory operator-overloading

在c ++中,您可以在堆和堆栈上创建类的新实例。当重载一个操作符时,你能够以一种有意义的方式在堆栈上实例化吗?

据我所知,一旦函数执行完毕,就会删除堆栈上的实例。这使得好像返回一个坐在堆栈上的新实例会有问题。

我写这篇文章知道必须有办法,但我不确定最佳做法是什么。 如果我有一些设计为始终驻留在堆栈中的类,我该如何处理运算符重载?

任何信息都会有所帮助,谢谢

{编辑} 我正在重载+运算符。 现在我使用这段代码

Point Point::operator+ (Point a)
{
Point *c = new Point(this->x+a.x,this->y+ a.y);
return *c;
}

我对如此实例化c持怀疑态度:

Point c(this->x + a.x, this->y, a.y);

因为那会将它分配给堆栈。我担心的是,一旦这个函数完成执行,堆栈指针就会改变,并且实例将不再安全,因为定义的任何新的局部变量都可以擦除它。这不是一个问题吗?

6 个答案:

答案 0 :(得分:12)

如果您正在谈论例如operator+,其中返回的对象不是那些输入中的任何一个,那么答案是您在堆栈上实例化并按值返回:

struct SomeClass {
    int value;
};

SomeClass operator+(const SomeClass &lhs, const SomeClass &rhs) {
    SomeClass retval;
    retval.value = lhs.value + rhs.value;
    return retval;
}

class SomeClass {
    int value;
public:
    SomeClass operator+(const SomeClass &rhs) const {
        SomeClass retval;
        retval.value = this->value + rhs.value;
        return retval;
    }
};

甚至:

class SomeClass {
    int value;
public:
    SomeClass(int v) : value(v) {}
    friend SomeClass operator+(const SomeClass &lhs, const SomeClass &rhs) {
        return SomeClass(lhs.value + rhs.value);
    }
};

编译器然后担心返回值实际存储在哪里(在堆栈上)。

例如,如果可以的话,它将应用返回值优化,但原则上发生的是“as-if”你所做的工作在你的运算符重载的堆栈上构造一些值,然后在返回时将其复制到它需要在哪里下一个。如果调用者分配了返回值,则将其复制到那里。如果调用者通过值将其传递给某个其他函数,则会将其复制到调用约定所需的任何位置,以便成为该函数参数。如果调用者采用const引用,则将其复制到隐藏在堆栈上的临时引用。

答案 1 :(得分:3)

C ++:RAII和Temporaries

关于堆栈上的对象一旦超出范围就被销毁是对的。

但是你忽略了C ++将使用临时对象是必要的。您必须了解编译器何时创建临时变量(然后进行优化)才能使代码生效。

临时对象

请注意,在下文中,我描述了一个非常简化的“纯粹”观点:编译器可以并且将会进行优化,其中,将删除无用的临时工......但行为仍然是相同的。 / p>

整数?

让我们慢慢开始:当您使用整数时应该发生什么:

int a, b, c, d ;
// etc.
a = b + (c * d) ;

上面的代码可以写成:

int a, b, c, d ;
// etc.
int cd = c * d ;
int bcd = b + cd ;
a = bcd ;

按值的参数

当您使用“按值”传递的参数调用函数时,编译器将为其创建一个临时副本(调用复制构造函数)。 如果从“按值”函数返回,编译器将再次制作它的临时副本。

让我们想象一下类型为T的对象。以下代码:

T foo(T t)
{
   t *= 2 ;

   return t ;
}

void bar()
{
   T t0, t1 ;

   // etc.

   t1 = foor(t0) ;
}

可以写成以下内联代码:

void bar()
{
   T t0, t1 ;

   // etc.

   T tempA(t1)     // INSIDE FOO : foo(t0) ;
   tempA += 2 ;    // INSIDE FOO : t *= 2 ;
   T tempB(tempA)  // INSIDE FOO : return t ;

   t1 = tempB ;    // t1 = foo...
}

因此,尽管您不编写代码,但是从函数调用或返回将(可能)添加大量“不可见代码”,需要将数据从堆栈的一个级别传递到下一个/上一个级别。

同样,你需要记住,C ++编译器会最大限度地优化掉,所以可以被视为一个无效的过程只是一个想法,没有别的。

关于您的代码

您的代码将泄漏:您“新”了一个对象,并且不删除它。

尽管您有疑虑,但正确的代码应该更像:

Point Point::operator+ (Point a)
{
   Point c = Point(this->x+a.x,this->y+ a.y) ;
   return c ;
}

使用以下代码:

void bar()
{
    Point x, y, z ;
    // etc.
    x = y + z ;
}

将生成以下伪代码:

void bar()
{
    Point x, y, z ;
    // etc.
    Point tempA = z ;  // INSIDE operator + : Point::operator+ (Point a)
    Point c = z ;      // INSIDE operator + : Point c = Point(this->x+a.x,this->y+ a.y) ;
    Point tempB = c ;  // INSIDE operator + : return c ;

    x = tempB ;        // x = y + z ;
}

关于您的代码,版本2

你赚得太多临时工。当然,编译器可能会删除它们,但是,不需要采取草率的习惯。

您至少应该将代码编写为:

inline Point Point::operator+ (const Point & a)
{
   return Point(this->x+a.x,this->y+ a.y) ;
}

答案 2 :(得分:2)

你已经有了一些很好的答案。以下是我想补充的几点:

  • 您应该尽量避免复制Point个对象。因为它们比内置类型更大(从你的代码我认为它们包含两个内置函数),在大多数体系结构中复制它们比每个引用传递它们更昂贵。这会将您的操作符更改为:Point Point::operator+ (Point & )(请注意,您必须复制结果,因为没有地方可以持久存储,因此您可以传递给它的引用。)
  • 但是,为了使编译器检查你没有搞砸并意外地修改了操作符的参数,你可以按照const引用传递它:Point Point::operator+ ( const Point&)
  • 由于operator+()(除了,例如,operator+=())也没有更改其左参数,因此您应该让编译器检查它。对于作为成员函数的二元运算符,左参数是this指针指向的参数。要使this成员函数中的常量,必须在成员函数签名的末尾注入const。这样做:Point Point::operator+ (const Point&) const 。现在你的操作符通常被称为const-correct。
  • 通常,当您为自己的类型提供operator+()时,人们会期望operator+=()也存在,所以通常您应该同时实现这两个类型。由于它们的行为非常相似,为了不冗余,你应该在另一个之上实现一个。最简单,最有效(以及更多或更少规范)的方法是在+之上实现+=。这使得operator+()非常容易编写 - 而且更重要的是:基本上它对于您实现它的每种类型看起来都是一样的:

由于operator+()变得非常简单,您可能希望inline它。这将是到目前为止的结果代码:

 inline Point Point::operator+ (const Point& rhs) const
 {
    Point result(this);
    result += a;
    return result;
 }

这些是一些基本的句法和语义特征,(希望)所有人都会同意这一点。现在这里有一个经验法则,我用于我的代码,我觉得非常有帮助,但可能不是每个人都会同意:

  • 同等对待两个参数的二元运算符(通常意味着它们不会改变它们中的任何一个)应该被实现为自由函数,处理它们的左参数的二元运算符(通常:改变它)应该被实现作为会员职能。

后者的原因(以operator+=()为例)非常简单:为了改变它,他们可能需要访问左参数的内部。更改类对象的内部结构最好通过成员函数完成。

前者的原因并不那么简单。除其他事项外,Scott Meyers had an excellent article解释说,与普遍看法相反,使用非成员函数通常实际上增加封装。但是事实上,对于成员函数的this参数,一些规则(隐式转换,动态分派等)与其他参数的规则不同。由于您希望两个参数得到平等对待,因此在某些情况下,将不同的规则应用于左侧可能会令人惊讶。

然后代码如下:

 inline Point operator+ (const Point& lhs, const Point& rhs) const
 {
    Point result(lhs);
    result += rhs;
    return result;
 }

对我来说,这是它的最终规范形式,我在我的代码中写下来,不管它是什么类型都没有多少思考。

实施operator+=()留给读者练习。 :)

答案 3 :(得分:0)

数据和代码是正交概念。让Code对Heap中的对象进行处理而不是驻留在堆栈上的对象有什么区别? (在两种情况下都提供尊重对象范围)

答案 4 :(得分:0)

正确执行函数时,堆栈上的数据无效是正确的。但是,在堆栈上返回数据副本是完全可以的(这就是你正在做的事情)。只需确保不返回指向堆栈数据的指针。

答案 5 :(得分:0)

使用您的代码:

Point Point::operator+ (Point a)
{
    Point result(this->x+a.x,this->y+ a.y);
    return result;
}

这样可以正常工作 基本上它创建结果localy(在堆栈上)。但是return语句将结果复制回调用点(就像int一样)。它使用Point拷贝构造函数将值复制回调用点。

int main()
{
    Point  a(1,2);
    Point  b(2,3);

    Point  c(a + b);
}

这里operator +在堆栈上创建一个本地。这将通过返回复制回调用点(c的构造函数)。然后使用c的复制构造函数将内容复制到c。

但是你认为复制结构似乎有点代价。技术上是的。但是允许编译器优化掉额外的拷贝结构(所有现代编译器都非常擅长)。

返回您的代码。

Point Point::operator+ (Point a)
{
    Point *c = new Point(this->x+a.x,this->y+ a.y);
    return *c;
}

不要这样做。这里您已动态分配,但您将结果复制回调用点(如上所述使用复制构造函数)。因此,当时间控制返回到调用点时,您丢失了指针并且无法取消分配内存(因此内存泄漏)。

Java和C ++之间的区别在于,当我们返回指针时,我们使用智能指针来帮助调用者识别谁负责释放内存(查找指针所有权)。