解决c++中包含指针字段的自定义类使用vector产生的内存错误

2019-07-21  本文已影响0人  江海小流

场景描述

先从一个例子开始吧

# include <iostream>
# include <vector>
# include <cstring>

class StringItem {
    public:
        char* data;
        int length;

        StringItem(const char *data) {
            this->length = strlen(data);
            this->data = new char[this->length + 1];
            for (int i = 0; i < this->length + 1; i++) this->data[i] = data[i];
        }

        StringItem(const StringItem& ins) {
            this->length = ins.length;
            this->data = new char[this->length + 1];
            for (int i = 0; i < this->length + 1; i++) this->data[i] = ins.data[i];
        }

        ~StringItem() {
            delete[] this->data;
        }

        std::string toString() {
            return std::string(this->data);
        }
};

int main(void) {
    std::vector<StringItem> v;
    StringItem item_1("Hello");
    StringItem item_2("Big");
    StringItem item_3("Mom");
    v.push_back(item_1);
    v.push_back(item_2);
    v.push_back(item_3);
    std::cout << "A" << std::endl;


    std::cout << "Before erase" << std::endl;
    for (int i = 0; i < v.size(); i++) {
        std::cout << i << " " << v[i].toString() << std::endl;
    }

    v.erase(v.begin());

    std::cout << std::endl << "After erase" << std::endl;
    for (int i = 0; i < v.size(); i++) {
        std::cout << i << " " << v[i].toString() << std::endl;
    }

    return 0;
}

上述代码申明了一个类StringItemmain函数中的测试代码尝试新建一个元素类型为StringItemvector,在插入几条数据后再删除一个数据,分别输出如下(如果要在vector中使用StringItem,必须将拷贝构造函数写好):

➜  cpp ./main
A
Before erase
0 Hello
1 Big
2 Mom

After erase
0 Big
1 @�'��U

从结果中发现,vector中最后一个元素中指针指向的数据变了,而且像是指向一块随机的内存。

分析

问题出在哪呢?

  1. 应该是vector中最后一个元素的指针指向的内存被 delete[] 了。
  2. 删除的是第二个,为什么第三个也会被删除呢?

为了解决这个问题,可以用gdb看看在erase时,具体发生了

在上述代码的47行(erase)打上一个端点

(gdb) b 47
Breakpoint 1 at 0x1182: file main.cpp, line 47.
(gdb) r
Starting program: /home/micl/Projects/VSCodeProjects/works/cpp/main 
A
Before erase
0 Hello
1 Big
2 Mom

Breakpoint 1, main () at main.cpp:47
47          v.erase(v.begin());

通过多次step后,发现执行删除的主要代码在这里

154         _M_erase(iterator __position)
155         {
156           if (__position + 1 != end())
157             _GLIBCXX_MOVE3(__position + 1, end(), __position);
158           --this->_M_impl._M_finish;
159           _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
160           return __position;
161         }

可以发现erase的实现方式是:通过把待删除后面所有的元素往前移一个位置,再删除掉最后一个元素。

这样是没有问题的,那么问题出在哪里呢?进一步,将vector存放的数据打印出来发现:

(gdb) p *this
$1 = std::vector of length 3, capacity 4 = {{data = 0x55555576aef0 "Hello", length = 5}, {data = 0x55555576afd0 "Big", length = 3}, {data = 0x55555576aed0 "Mom", length = 3}}
(gdb) n
157             _GLIBCXX_MOVE3(__position + 1, end(), __position);
(gdb) n
158           --this->_M_impl._M_finish;
(gdb) p *this
$2 = std::vector of length 3, capacity 4 = {{data = 0x55555576afd0 "Big", length = 3}, {data = 0x55555576aed0 "Mom", length = 3}, {data = 0x55555576aed0 "Mom", length = 3}}
(gdb) n
159           _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
(gdb) p *this
$3 = std::vector of length 2, capacity 4 = {{data = 0x55555576afd0 "Big", length = 3}, {data = 0x55555576aed0 "Mom", length = 3}}
(gdb) n
160           return __position;
(gdb) p *this
$4 = std::vector of length 2, capacity 4 = {{data = 0x55555576afd0 "Big", length = 3}, {data = 0x55555576aed0 "@\257vUUU", length = 3}}

_GLIBCXX_MOVE3 调用后,vector最后两个元素指向了同一个地址,之后_Alloc_traits::destroy便会把"Mom"所在的内存释放掉,所以便出现了这样的错误。

解决方法

要解决这个问题,要么杜绝掉纯内存复制的情况(如果要用vector,显然不行),要么在多个StringItem中的指针指向同一块内存时,不能每一个StringItem都释放这块内存。c++ 里面有一个 shared_ptr 可以用于解决这样的问题。

更改后代码如下:

# include <iostream>
# include <vector>
# include <cstring>
# include <memory>

class StringItem {
    public:
        std::shared_ptr<char> data;
        int length;

        StringItem(const char *data): length(strlen(data)), 
                                      data(new char[this->length + 1], [](char *p) {delete[] p;}) {
            for (int i = 0; i < this->length + 1; i++) this->data.get()[i] = data[i];
        }

        StringItem(const StringItem& ins): length(ins.length), 
                                           data(new char[this->length + 1], [](char *p){delete[] p;}) {
            for (int i = 0; i < this->length + 1; i++) this->data.get()[i] = ins.data.get()[i];
        }

        ~StringItem() {
        }

        std::string toString() {
            return std::string(this->data.get());
        }
};

int main(void) {
    std::vector<StringItem> v;
    StringItem item_1("Hello");
    StringItem item_2("Big");
    StringItem item_3("Mom");
    v.push_back(item_1);
    v.push_back(item_2);
    v.push_back(item_3);
    std::cout << "A" << std::endl;


    std::cout << "Before erase" << std::endl;
    for (int i = 0; i < v.size(); i++) {
        std::cout << i << " " << v[i].toString() << std::endl;
    }

    v.erase(v.begin());

    std::cout << std::endl << "After erase" << std::endl;
    for (int i = 0; i < v.size(); i++) {
        std::cout << i << " " << v[i].toString() << std::endl;
    }

    

    return 0;
}

shared_ptr 会记录有多少个指针指向了对应的内存区域,当没有指针指向这块区域时便会释放掉这一块内存,因此就不需要在析构函数中delete[] data了,同时也可以上述问题。

上一篇下一篇

猜你喜欢

热点阅读