5

以下“最小”示例应显示规则 3(半)的使用。

#include <algorithm>
#include <iostream>

class C
{
    std::string* str;
public:
    C()
        : str(new std::string("default constructed"))
    {
        std::cout << "std ctor called" << std::endl;
    }
    C(std::string* _str)
        : str(_str) 
    {
        std::cout << "string ctor called, "
            << "address:" << str << std::endl;
    }
    // copy ctor: does a hard copy of the string
    C(const C& other)
        : str(new std::string(*(other.str)))
    {
        std::cout << "copy ctor called" << std::endl;
    }

    friend void swap(C& c1, C& c2) {
        using std::swap;
        swap(c1.str, c2.str); 
    }

    const C& operator=(C src) // rule of 3.5
    {
        using std::swap;
        swap(*this, src);
        std::cout << "operator= called" << std::endl;
        return *this;
    }

    C get_new() {
        return C(str);
    }
    void print_address() { std::cout << str << std::endl; }
};

int main()
{
    C a, b;
    a = b.get_new();
    a.print_address();
    return 0;
}

像这样编译它(g ++版本:4.7.1):

g++ -Wall test.cpp -o test

现在,应该怎么办?我假设该行将a = b.get_new();制作一个硬拷贝,即分配一个新字符串。原因:operator=()在这个设计模式中,它的参数是典型的,每个值,它调用一个复制 ctor,它将进行深度复制。到底发生了什么?

std ctor called
std ctor called
string ctor called, address:0x433d0b0
operator= called
0x433d0b0

复制 ctor从未被调用过,因此,复制是软的——两个指针是相等的。为什么不调用复制ctor?

4

3 回答 3

4

副本正在被删除。

没有副本,因为b.get_new();它的“临时”C对象正是在最终成为operator=. 编译器能够管理这一点,因为一切都在一个翻译单元中,因此它有足够的信息来进行这种转换。

您可以使用 flag 消除 clang 和 gcc 中的构造省略-fno-elide-constructors,然后输出将如下所示:

std ctor called
std ctor called
string ctor called, address:0x1b42070
copy ctor called
copy ctor called
operator= called
0x1b420f0

返回值优化消除了第一个副本。使用 RVO,该函数构造最终直接返回到返回值应该去的位置的对象。

我不确定第二个副本的省略是否有特殊名称。那就是从 的返回值复制get_new()到参数 for operator= ()

正如我之前所说,将两个副本一起删除会导致get_new()将其对象直接构造到参数 to 的空间中operator= ()


请注意,两个指针相等,如:

std ctor called
std ctor called
string ctor called, address:0xc340d0
operator= called
0xc340d0

本身并不表示错误,这不会导致双重释放;因为副本被省略了,所以没有该对象的额外副本保留对分配的字符串的所有权,因此不会有额外的空闲。

但是,您的代码确实包含与三规则无关的错误:get_new()正在将指针传递给对象自己的str成员,并且它创建的显式对象(在输出中的“字符串 ctor 调用,地址:0xc340d0”行)正在获取所有权str已由原始对象管理的对象 ( ) b。这意味着b和内部创建的对象get_new()都试图管理相同的字符串,这将导致双重释放(如果实现了析构函数)。

要查看此更改默认构造函数以显示str它创建的:

C()
    : str(new std::string("default constructed"))
{
    std::cout << "std ctor called. Address: " << str << std::endl;
}

现在输出将如下所示:

std ctor called. Address: 0x1cdf010
std ctor called. Address: 0x1cdf070
string ctor called, address:0x1cdf070
operator= called
0x1cdf070

所以打印的最后两个指针相同是没有问题的。问题在于正在打印的第二个和第三个指针。固定get_new()

C get_new() {
    return C(new std::string(*str));
}

将输出更改为:

std ctor called. Address: 0xec3010
std ctor called. Address: 0xec3070
string ctor called, address:0xec30d0
operator= called
0xec30d0

并解决了双重释放的任何潜在问题。

于 2013-10-11T19:19:25.063 回答
3

允许 C++ 优化返回类实例的函数中的复制构造。

发生的情况get_new是直接返回从 member 新构造的对象_str,然后将其用作分配的源。这称为“返回值优化”(RVO)。

请注意,虽然编译器可以自由优化复制构造,但仍需要检查复制构造是否可以合法调用。例如,如果您有一个非友元函数而不是成员函数返回和实例,并且复制构造函数是私有的,那么即使在使函数可访问之后,副本最终可能会被优化掉,您也会收到编译器错误。

于 2013-10-11T19:19:22.067 回答
1

目前尚不清楚为什么您希望使用复制 ctor。get_new() 函数在返回值时不会创建 C 对象的新副本。这是一种称为Return Value Optimization的优化,任何 C++ 编译器都会实现它。

于 2013-10-11T19:17:25.683 回答