为什么 std::string 即使在删除后也会导致 class 中的内存泄漏

why is std::string causing a memory leak in a class even after deleting

我正在构建一个使用 void* 作为存储方法的 class。我知道使用 void* 不是一个好主意。但我不知道它在编译时会有什么价值,所以我认为这是最好的解决方案。它适用于所有东西,包括 C 字符串,但不适用于 std::string。它会导致非常严重的内存泄漏。我构建了一个具有相同问题的 class 的基本模型。

#include<iostream>
#include<string>

class CLASS {
public:
    void* data;

    CLASS(std::string str) {
        std::string* s = new std::string;
        *s = str;
        data = s;
    }

    ~CLASS() {
        delete data;
    }
};

int main() {
    std::string str = "hi";
        while (true)
        {
            CLASS val(str);
        }
}

delete data 不起作用。 delete 将调用析构函数,然后释放一个对象的存储空间,但是 void 不能被销毁,也没有大小。

std::string的析构函数会释放它分配的内存,但这里并没有调用它。您至少需要存储 data 中保存的值的析构函数。 std::any 会为您处理这一切。

但是,请考虑使用 std::variant<T1, T2, T3, ...> 将其限制为少数已知类型。

如果分配 string,将指针指向 void *,然后删除 您将尝试删除 [=16] =] 的东西,而不是 string 的东西。例如,任何分配辅助存储的 class(超出使用 new 创建的实际对象)很可能 运行 会遇到麻烦:

#include <iostream>

class Y {
public:
    Y() { std::cout << "y constructor\n"; }
    ~Y() { std::cout << "y destructor\n"; }
};

class X {
public:
    X() { std::cout << "x constructor\n";  y = new Y(); }
    ~X() { delete y; std::cout << "x destructor\n"; }
private:
    Y *y;
};

int main() {
    X *x = new X();
    delete (void*)x;
}

此代码将“有效”,因为它是合法的,但它不会按照您的预期执行:

x constructor
y constructor

体面的编译器应该警告你:

program.cpp: In function ‘int main()’:
program.cpp:19:19: warning: deleting ‘void*’ is undefined [-Wdelete-incomplete]
   19 |     delete (void*)x;
      |                   ^

您应该删除分配的类型,以确保使用正确的析构函数。换句话说,摆脱上面显示的代码中的转换(这样你 delete 正确的类型)会更好:

x constructor
y constructor
y destructor
x destructor

现代 C++ 具有类型安全类型,可以为您完成繁重的工作,例如 variantany(如果您的单个 class 需要存储多种类型在 运行 时间决定)或模板(如果它可以用于 一个 类型但任何种类)。您应该调查这些作为 void *.

的替代方案

当你 delete 一个指针时,它必须与 new 返回的类型相同(或者,一个指向基数 class 的指针,如果它有一个 virtual 析构函数).

您不能删除一个 void* 指针并期望它知道要销毁的类型。将 void* 赋值给

时,所有类型信息都已丢失

因此,您必须将 void* 指针显式类型转换回其原始类型(就像您必须访问所指向数据的内容一样)。

此外,您还需要确保遵循 Rule of 3/5/0,以便在分配和 copy/move 操作期间管理指针 属性。

试试这个:

#include <iostream>
#include <string>

class MyClass {
public:
    void* data;

    MyClass() {
        data = nullptr;
    }

    MyClass(const std::string &str) {
        data = new std::string(str);
    }

    MyClass(const MyClass &src) {
        data = new std::string(*static_cast<std::string*>(src.data));
    }

    MyClass(MyClass &&src) {
        data = src.data; src.data = nullptr;
    }

    ~MyClass() {
        delete static_cast<std::string*>(data);
    }

    MyClass& operator=(MyClass rhs) {
        MyClass tmp(std::move(rhs));
        std::swap(data, tmp.data);
        return *this;
    }
};

int main() {
    std::string str = "hi";
    while (true)
    {
        MyClass val1(str);
        MyClass val2(val1);
        MyClass val3(std::move(val2));
        MyClass val4;
        val4 = val3;
        val4 = std::move(val3);
    }
}

当您开始引入更多类型供 void* 指向时,这会变得有点复杂。那么你需要一种方法来识别实际指向的是什么类型,例如:

#include <iostream>
#include <string>

enum MyClassType { mctNull, mctString, mctInteger, ... };

class MyClass {
public:
    void* data;
    MyClassType dataType;

    MyClass() {
        dataType = mctNull;
        data = nullptr;
    }

    MyClass(const std::string &value) {
        dataType = mctString;
        data = new std::string(value);
    }

    MyClass(int value) {
        dataType = mctInteger;
        data = new int(value);
    }

    ...

    MyClass(const MyClass &src) {
        dataType = src.dataType;
        switch (src.dataType) {
            case mctNull:
                data = nullptr;
                break;
            case mctString:
                data = new std::string(*static_cast<std::string*>(src.data));
                break;
            case mctInteger:
                data = new int(*static_cast<int*>(src.data));
                break;
            ...
        }
    }

    MyClass(MyClass &&src) {
        dataType = src.dataType; src.dataType = mctNull;
        data = src.data; src.data = nullptr;
    }

    ~MyClass() {
        switch (dataType) {
            case mctString:
                delete static_cast<std::string*>(data);
                break;
            case mctInteger:
                delete static_cast<int*>(data);
                break;
            ...
        }
    }

    MyClass& operator=(MyClass rhs) {
        MyClass tmp(std::move(rhs));
        std::swap(data, tmp.data);
        std::swap(dataType, tmp.dataType);
        return *this;
    }
};

int main() {
    std::string str = "hi";
    int i = 12345;
    int counter;

    while (true)
    {
        MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);

        MyClass val2(val1);
        MyClass val3(std::move(val2));
        MyClass val4;
        val4 = val3;
        val4 = std::move(val3);
    }
}

幸运的是,现代 C++ 提供了 std::variantstd::any,因此您根本不必手动管理这些东西,例如:

#include <iostream>
#include <string>
#include <variant>

class MyClass {
public:
    std::variant<std::string, int> data;

    MyClass() = default;

    MyClass(const std::string &value) { data = value; }
    MyClass(int value) { data = value; }
    ...
};

int main() {
    std::string str = "hi";
    int i = 12345;
    int counter;

    while (true)
    {
        MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);

        MyClass val2(val1);
        MyClass val3(std::move(val2));
        MyClass val4;
        val4 = val3;
        val4 = std::move(val3);
    }
}

您可以免费获得所有复制、移动和销毁逻辑,它实际上为您做了正确的事情。