3

我使用 C++ 已经有一段时间了,但至少还有一件事我无法理解,在网上也找不到很好的解释。它与内存管理有关,可以用这个例子来说明:

考虑 std::string 的字符串连接运算符,它看起来像这样

std::string operator+(const string& s1, string& s2)

众所周知,它返回一个新创建的对象(另一个 std::string),其中包含连接的两个原始字符串。我的问题是:这怎么可能?这个对象在内存中的什么位置?

我的意思是,如果我必须编写函数实现,我会做这样的事情

std::string std::string::operator+(const string& s1, string& s2)
{
std::string *res=new std::string;
// fill res with the content of s1 and s2
return *res;
}

但是通过这种方式我知道我会导致内存泄漏,因为如果我调用该函数一百万次,我将生成一百万个字符串,这些字符串在程序结束之前不会被释放。另一方面,我可以这样做:

std::string& std::string::operator+(const string& s1, string& s2)
{
std::string res;
// fill res with the content of s1 and s2
return res;
}

但是通过这种方式,我将返回对局部变量的引用,该变量在函数返回后立即变为垃圾。最后我可以简单地写

std::string std::string::operator+(const string& s1, string& s2)
{
std::string res;
// fill res with the content of s1 and s2
return res;
}

并按值传递结果,这应该完成任务,但在我看来效率很低,因为我必须将整个 res (理论上可能非常大)对象复制到调用函数。我这样说是因为我实际上正在研究一个线性代数库,并且只需执行例如矩阵加法会非常好

m3=m1+m2;

就像字符串连接一样,但如果唯一的解决方案是复制回结果对象,那么使用 100MB 的双矩阵是不切实际的。目前我使用的功能是

matrix& matrix::sum(matrix& m1, matrix& m2)

以这种方式使用

m3.sum(m2,m1);

这看起来很难看,也阻止我在一行中对多个矩阵求和,我必须写

m4.sum(m1,m2)
m4.sum(m4,m3)

或者

(m4.sum(m1,m2)).(m4,m3)

如果我真的想在一行中完成它,但它绝对不可读。

有没有更好的方法来做所有这些事情?

提前致谢

4

3 回答 3

5

这个版本是对的

std::string std::string::operator+(const string& s1, string& s2)
{
    std::string res;
    // fill res with the content of s1 and s2
    return res;
}

大多数编译器采用一种称为返回值优化的优化技术来处理复制值的低效率问题。标准明确允许这样做,称为复制省略

在 C++11 中还有另一种方式:当你返回 string 时res,它变成了一个 r 值,并且将使用move 构造函数代替复制构造函数,这也很便宜。但同样,大多数编译器优化了复制和移动。

最后不知道为什么要自己实现一个矩阵库。如果不是作业,请改用 Eigen。优化矩阵代数是一项非常艰巨的工作,需要大量的底层理解。

于 2013-09-26T07:50:05.997 回答
3

正如已经指出的那样,在类似重载的情况下 operator+,您必须返回一个完整的对象(按值)。一般来说,这比想象的要少。RVO 之类的东西使它不再是一个问题。另一方面,在(大)矩阵的情况下,它可能成为一个严重的问题,不仅仅是因为运行时间,还因为内存考虑;如果你有这样的表达:

m = m1 + m2 + m3 + m4 + m5;

将有四个临时的,它们都将持续到完整的表达结束。如果矩阵很大,则会产生很大的内存压力。在这种情况下,通常的技术是返回一些特殊类型,它只是保持指向左右参数的指针;operator=(和构造函数)然后重载以采用这种类型,并动态构建最终矩阵。就像是:

class MatrixProxy
{
    void* operator new( size_t );   //  Prevent dynamic allocation
public:
    virtual int rows() const = 0;
    virtual int columns() const = 0;
    virtual double get( int row, int column ) const = 0;
};

class MatrixOpAddResults : public MatrixProxy
{
    MatrixProxy const* lhs;
    MatrixProxy const* rhs;
public:
    MatrixOpAddResults( Matrix const& lhs, Matrix const& rhs )
        : lhs( &lhs )
        , rhs( &rhs )
    {
        assert( lhs->rows() == rhs->rows() && lhs->columns() == rhs->columns() );
    }
    int rows() const override
    {
        return lhs->rows();
    }
    int columns() const override
    {
        return lhs->columns();
    }
    double get( int row, int column ) const override
    {
        return lhs->get( row, column ) + rhs->get( row, column );
    }
};

MatrixProxy operator+( MatrixProxy const& lhs, MatrixProxy const& rhs )
{
    return MatrixProxy( lhs, rhs );
}

那么,例如...

Matrix::Matrix( MatrixProxy const& other )
    : m_rows( other.rows() )
    , m_columns( other.columns() )
    , m_data( other.rows() & other.columns() )
{
    std::vector<double>::const_iterator dest = m_data.begin();
    for ( int i = 0; i != m_rows; ++ i ) {
        for ( int j = 0; j != m_columns; ++ j ) {
            *dest = other.get( i, j );
            ++ dest;
        }
    }
}

当然,Matrix 本身也应该派生自 MatrixProxy。而且您需要为每个运算符创建一个 Results 类。

现代的趋势是在这里使用模板,而不是继承。然而,我发现基于继承的解决方案更清晰、更易于理解(因为它更明确),至少在解释该技术时,两者最终都应该生成完全相同的代码(假设结果类中的所有函数都是内联的)。

最后:除非这是为了个人理解,否则有几个免费的优秀库可以使用上述技术实现矩阵。(想到了 Blitz++,虽然我不知道它目前的状态。)

于 2013-09-26T09:06:32.393 回答
3

现代编译器将执行“复制省略”,这几乎意味着您的最后一个字符串示例实际上并没有复制结果字符串,它只是将结果存储在调用代码提供的位置。这同样适用于您自己的设计vectormatrix

于 2013-09-26T07:46:14.940 回答