场景描述
先从一个例子开始吧
# 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;
}
上述代码申明了一个类StringItem
,main
函数中的测试代码尝试新建一个元素类型为StringItem
的vector
,在插入几条数据后再删除一个数据,分别输出如下(如果要在vector
中使用StringItem
,必须将拷贝构造函数写好):
➜ cpp ./main
A
Before erase
0 Hello
1 Big
2 Mom
After erase
0 Big
1 @�'��U
从结果中发现,vector
中最后一个元素中指针指向的数据变了,而且像是指向一块随机的内存。
分析
问题出在哪呢?
- 应该是
vector
中最后一个元素的指针指向的内存被delete[]
了。 - 删除的是第二个,为什么第三个也会被删除呢?
为了解决这个问题,可以用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
了,同时也可以上述问题。
网友评论