move 与 析构函数

第一次尝试使用std::move优化代码,就遇到了坑

简化代码如下:

class moveTest
{
public:
    moveTest() { std::cout << "construct moveTest" << std::endl; };
    ~moveTest() {
        if ((buf) != NULL) delete[](buf); (buf) = NULL;
        std::cout << "delete moveTest" << std::endl;
    };
    void initbuf() { buf = new int[10]; };
private:
	int* buf = nullptr;
};

void push(moveTest& m)
{
	moveTest testA = std::move(m);
};

int main()
{	moveTest b;
	b.initbuf();
	push(b);
	return 0;
} 

上述代码运行时,报错。错误原因 moveTest对象析构出发断点

解释如下:
根据您提供的代码,错误原因是因为在A类中的push函数中,将moveTest对象m移动赋值给testA成员变量时,会调用moveTest类的移动赋值运算符。

然而,根据您提供的代码,moveTest类没有显式定义移动赋值运算符。这会导致使用默认的移动赋值运算符,而默认的移动赋值运算符会简单地将成员变量逐个移动赋值,而不会处理指针成员变量。

moveTest类中,存在一个指针成员变量buf,在默认的移动赋值运算符中,只会简单地将指针成员变量复制到目标对象中。这会导致两个对象的buf指向同一块内存,当其中一个对象被销毁时,会释放内存,导致另一个对象的buf成为悬空指针。

因此,当moveTest对象mpush函数中被移动赋值给testA成员变量后,m对象的析构函数被调用,尝试释放内存,但由于buf成为悬空指针,导致触发断点。

要解决这个问题,您可以在moveTest类中显式定义移动赋值运算符,确保正确处理指针成员变量的移动赋值。例如:

moveTest& operator=(moveTest&& other)
{
    if (this != &other)
    {
        delete[] buf;
        buf = other.buf;
        other.buf = nullptr;
    }
    return *this;
}

通过显式定义移动赋值运算符,确保在移动赋值时正确处理buf指针,避免悬空指针的问题。这样,当moveTest对象被移动赋值时,不会触发断点。

悬空指针是指指向已释放或无效的内存的指针。当我们尝试使用悬空指针时,就会导致未定义行为,这可能包括访问无效的内存、触发段错误、导致程序崩溃等。

在您的代码中,当moveTest对象mpush函数中被移动赋值给testA成员变量后,m对象的析构函数被调用。在析构函数中,您尝试释放指针成员变量buf所指向的内存:

~moveTest() { 
    if ((buf) != NULL) delete[](buf); (buf) = NULL; 
    std::cout << "delete moveTest" << std::endl;
};

然而,在移动赋值运算符中,您没有将buf指针设置为nullptr,而是将其赋值给了目标对象,导致m对象的buf成为悬空指针。因此,在析构函数中尝试释放悬空指针时,会触发断点。

触发断点的具体原因可能是由于调试器检测到了悬空指针的释放,而断点是调试器的一种行为,用于提醒开发人员可能存在的问题。

为了解决这个问题,需要在移动赋值运算符中将buf指针设置为nullptr,以避免在析构函数中释放悬空指针。

正确代码如下:

class moveTest
{
public:
    moveTest() { std::cout << "construct moveTest" << std::endl; };
    ~moveTest() {
        if ((buf) != NULL) delete[](buf); (buf) = NULL;
        std::cout << "delete moveTest" << std::endl;
    };
    void initbuf() { buf = new int[10]; };


    // 移动构造函数
    moveTest(moveTest&& other) noexcept {
        buf = other.buf;
        other.buf = nullptr;
    }

    // 移动赋值运算符
    moveTest& operator=(moveTest&& other) noexcept {
        if (this != &other) {
            delete[] buf;
            buf = other.buf;
            other.buf = nullptr;
        }
        return *this;
    }



private:
	int* buf = nullptr;
};

为了保证 buf 在移动构造函数中被正确处理,需要在移动构造函数中将源对象的 buf 指针设置为 nullptr,并将资源所有权转移给新对象。以下是一个示例:

class moveTest {
public:
    moveTest() { std::cout << "construct moveTest" << std::endl; };
    ~moveTest() { 
        if (buf != nullptr) delete[] buf; 
        std::cout << "delete moveTest" << std::endl;
    };
    void initbuf() { 
        buf = new int[10]; 
    };

    // 自定义移动构造函数
    moveTest(moveTest&& other) noexcept : buf(other.buf) {
        std::cout << "moveTest move constructor called" << std::endl;
        other.buf = nullptr; // 将源对象的 buf 指针设置为 nullptr
    }

private:
    int* buf = nullptr;
    int a, b, c, d, e, f;
};

在上述代码中,我们在 moveTest 类中添加了一个自定义的移动构造函数。在移动构造函数中,我们将源对象的 buf 指针赋值给新对象的 buf 指针,并将源对象的 buf 指针设置为 nullptr,以确保资源所有权正确转移。

以下是一个使用自定义移动构造函数的示例:

int main() {
    moveTest obj1;
    obj1.initbuf();

    moveTest obj2(std::move(obj1)); // 使用移动构造函数将 obj1 的值移动到 obj2 中

    return 0;
}

在上述代码中,我们首先创建了一个名为 obj1moveTest 对象,并调用了 initbuf 方法来初始化 buf 数组。然后,我们使用移动构造函数将 obj1 的值移动到 obj2 中。在移动构造函数中,obj1buf 指针被设置为 nullptr,而 obj2buf 指针指向了原来的 buf 数组。最后,当程序结束时,会调用 obj1obj2 的析构函数,释放 buf 数组的内存。

运行上述代码,将会输出以下内容:

construct moveTest
moveTest move constructor called
delete moveTest
delete moveTest

这表明自定义的移动构造函数成功地将 buf 的所有权从 obj1 转移到了 obj2,并正确地处理了 buf 的释放。

在自定义移动构造函数中,成员变量 a、b 和 c 不需要显式处理,因为它们是基本类型(如 int)的成员变量,可以直接进行移动。当对象被移动时,这些基本类型的成员变量会自动从源对象移动到目标对象中。

在移动构造函数中,只需要处理拥有资源所有权的成员变量,例如指针或动态分配的内存。对于基本类型的成员变量,编译器会自动处理移动操作。

你可能感兴趣的:(C++,c++,开发语言)