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

发布于 2024-08-05 01:23:44 字数 561 浏览 5 评论 0原文

在 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);

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

In c++ you can create new instances of a class on both the heap and stack. When overloading an operator are you able to instantiate on the stack in a way that makes sense?

As I understood it an instance that sits on the stack is removed as soon as the function is done executing. This makes it seems as though returning a new instance sitting on the stack would be a problem.

I am writing this knowing there has to be a way, but I am not sure what the best practice is.
If I have some class that is designed to always reside in the stack how do I go about operator overloading?

Any info would be helpful, thanks

{EDIT}
I am overloading the + operator.
Right now I use this code

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

I was skeptical about instantiating c like so:

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

because that would allocate it to the stack. My concern is that the stack pointer is going to change once this function finishes executing, and the instance will no longer be safe since any new local variables defined could erase it. Is this not a concern?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

子栖 2024-08-12 01:23:44

如果您正在谈论例如 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);
    }
};

编译器然后会担心关于返回值实际存储的位置(在堆栈上)。

例如,如果可以的话,它会应用返回值优化,但原则上发生的事情是“好像”您所做的工作在运算符重载的堆栈上构造了一些值,然后在返回时将其复制到需要的地方成为下一个。如果调用者指定了返回值,则会将其复制到那里。如果调用者将其按值传递给其他函数,则它将被复制到调用约定规定的任何位置,以便成为该函数参数。如果调用者采用 const 引用,则会将其复制到隐藏在堆栈上的临时引用。

If you're talking about for example operator+, where the object returned is not either of those input, then the answer is you instantiate on the stack and return by value:

struct SomeClass {
    int value;
};

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

or

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

or even:

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);
    }
};

The compiler then worries about where (on the stack) the return value is actually stored.

It will for example apply return-value optimizations if it can, but in principle what's happening is "as-if" the work you do constructs some value on the stack of your operator overload, and then at return this is copied to wherever it needs to be next. If the caller assigns the return value, it's copied there. If the caller passes it by value to some other function, it's copied wherever the calling convention says it needs to be in order to be that function parameter. If the caller takes a const reference, then it's copied to a temporary hidden away on the stack.

世态炎凉 2024-08-12 01:23:44

C++:RAII 和临时对象

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

但你忽略了C++使用临时对象是必要的。您必须了解编译器何时创建(然后优化)临时变量以使您的代码正常工作。

临时对象

请注意,在下文中,我将描述正在发生的事情的一个非常简化的“纯粹”观点:编译器可以并且将会进行优化,其中将删除无用的临时对象......但行为保持不变。

整数?

让我们慢慢开始: 当你玩整数时会发生什么:

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++ 编译器会优化掉大部分临时内容,因此可能被视为效率低下的过程只是一个想法,仅此而已。

关于您的代码

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

尽管您心存疑虑,正确的代码应该更像是:

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) ;
}

C++ : RAII and Temporaries

You're right about objects on stack being destroyed once going out of scope.

But you ignore that C++ will use temporary objects are necessary. You must learn when a temporary variable will be created (and then optimized away) by the compiler for your code to work.

Temporary Objects

Note that in the following, I'm describing a very simplified "pure" viewpoint of what's happening: Compilers can and will do optimizations, and among them, will remove useless temporaries... But the behavior remains the same.

Integers?

Let's start slowly: What is supposed to happen when you play with integers:

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

The code above could be written as:

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

Parameters by value

When you call a function with a parameter passed "by value", the compiler will make a temporary copy of it (calling the copy constructor).
And if you return from a function "by value", the compiler will, again, make a temporary copy of it.

Let's imagine an object of type T. The following code:

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

   return t ;
}

void bar()
{
   T t0, t1 ;

   // etc.

   t1 = foor(t0) ;
}

could be written as the following inlined code:

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...
}

So, despite the fact you don't write code, calling or returning from a function will (potentially) add a lot of "invisible code", needed to pass data from one level of the stack to the next/previous.

Again, you need to remember that the C++ compiler will optimize away most temporary, so what could be seen as an innefficient process is just an idea, nothing else.

About your code

Your code will leak: You "new" an object, and don't delete it.

Despite your misgivings, the right code should be more like:

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

Which with the following code:

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

Will produce the following pseudo code:

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 ;
}

About your code, version 2

You make too much temporaries. Of course, the compiler will probably remove them, but then, no need to take sloppy habits.

You should at the very least write the code as:

inline Point Point::operator+ (const Point & a)
{
   return Point(this->x+a.x,this->y+ a.y) ;
}
(り薆情海 2024-08-12 01:23:44

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

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

因为 operator+() 变得非常简单微不足道,您可能想要内联它。这将是迄今为止生成的代码:

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

这些是一些基本的语法和语义特性,(希望)所有阅读本文的人都会同意。现在,我在代码中使用了一条经验法则,我发现它非常有用,但可能不是每个人都会同意:

  • 二元运算符平等地对待两个参数(这通常意味着它们不会更改其中任何一个参数) ),应该实现为自由函数,处理其左参数(通常:更改它)的二元运算符应该实现为成员函数。

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

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

代码如下所示:

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

对我来说,这是它的最终规范形式,我在代码中写下它,而无需太多考虑,无论它是什么类型。

实现operator+=() 留给读者作为练习。 <代码>:)

You've already had a few good answers. Here's a few more points I'd like to add to them:

  • You should try to avoid copying Point objects. Since they are bigger than built-in types (from your code I assume they consist of two built-ins), copying them is, on most architectures, more expensive than passing them around per reference. That changes your operator to: Point Point::operator+ (Point&) (Note that you have to copy the result, as there's no place it can be stored persistently so you can pass around a reference to it.)
  • However, to make the compiler check you didn't screw it up and accidentally modified the operator's argument, you pass it per const reference: Point Point::operator+ (const Point&).
  • Since operator+() (other than, e.g., operator+=()) doesn't change its left argument either, you should make the compiler check that, too. For a binary operator that is a member function, the left argument is what the this pointer points to. To make this a constant in a member function, you have to inject a const at the end of the member function's signature. That makes it: Point Point::operator+ (const Point&) const. Now your operator is what's usually called const-correct.
  • Usually, when you provide operator+() for your type, people will expect operator+=() to also be present, so usually you should implement both. Since they behave quite similar, to not to be redundant you should implement one on top of the other. The easiest and most efficient (and therefor more or less canonical) way to do this is to implement + on top of +=. That makes operator+() quite easy to write -- and what's even more important: basically it looks the same for every type you implement it for:

Since operator+() became quite trivial, you would probably want to inline it. This would then be the resulting code so far:

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

These are a few basic syntactic and semantic peculiarities which (hopefully) all reading this will agree to. Now here comes a rule of thumb that I use for my code and which I find very helpful, but which probably not everyone will agree to:

  • Binary operators that treat both of their arguments equally (which usually means they don't change either of them), should be implemented as free functions, binary operators that treat their left argument (usually: that change it) should be implemented as member functions.

The reason for the latter (take operator+=() as an example) is rather straight-forward: In order to change it, they might need to have access to the left argument's innards. And changing class object's innards is best done through member functions.

The reasons for the former are not as simple. Among other things, Scott Meyers had an excellent article explaining that, contrary to popular belief, using non-member functions often actually increase encapsulation. But then there's also the fact that for the this argument of member functions, some rules (implicit conversions, dynamic dispatch etc.) differ from those for the other arguments. Since you want both arguments to be treated equally, it might be surprising under some circumstances to have different rules apply to the left-hand side.

The code then looks like this:

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

To me, this is the ultimate canonical form of it which I write down in my code without much thinking about it, no matter what type it is.

Implementing operator+=() is left as an exercise to the reader. :)

猛虎独行 2024-08-12 01:23:44

数据和代码是正交的概念。让代码处理堆中的对象与驻留在堆栈上的对象有什么区别? (前提是您在这两种情况下都尊重对象范围)

Data and Code are orthogonal concepts. What difference does it make to have Code work on an object from the Heap as opposed to one residing on the stack? (provide you are respecting object scope in both cases)

近箐 2024-08-12 01:23:44

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

You are correct that data on the stack is unusable when the function executes. However, it is perfectly okay to return copies of data on the stack (which is what you are doing). Just make sure you don't return pointers to data on the stack.

星軌x 2024-08-12 01:23:44

使用您的代码:

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

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

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

    Point  c(a + b);
}

这里运算符 + 在堆栈上创建一个局部变量。通过返回将其复制回调用点(c 的构造函数)。然后使用c的复制构造函数将内容复制到c中。

但你认为这对于复制构建来说似乎有点昂贵。从技术上来说是的。但是编译器可以优化掉额外的复制结构(所有现代编译器都非常擅长)。

回到你的代码。

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

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

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

Using your code:

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

This will work fine.
Basically it creates result localy (on the stack). But the return statement copies the result back to the calling point (just like an int). It uses the Point copy constructor to copy the value back to the call point.

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

    Point  c(a + b);
}

Here the operator + creates a local on the stack. This is copied back to the call point (The constructor for c) by the return. Then the copy constructor for c is used to copy the content into c.

But you think that seems a little costly on the copy construction. Technically yes. But the compiler is allowed to optimize away the extra copy constructions (and all modern compilers are very good at it).

Returning to your code.

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

Don't do this. Here you have allocated dynamically but you are copying the result back to the call point (as described above using the copy constructor). So by the time control returns to the call point you have lost the pointer and can't de-allocate the memory (Thus a memory leak).

The difference between Java and C++ is that when we return pointers we use smart pointers to help the caller identify who is responsible for freeing the memory (look up pointer ownership).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文