代码与讲解承接上文:【C++】vector 深度剖析及模拟实现-CSDN博客
memcpy:更深一层次的深浅拷贝问题
/* 自定义类型 */
void test_vector5()
{
vector<string> v;
v.push_back("11111111111111111111111111111");
v.push_back("22222222222222222222222222222");
v.push_back("33333333333333333333333333333");
v.push_back("44444444444444444444444444444");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
打印结果是 3 和 4 没有问题,但是 1 和 2 都是乱码。是扩容时出现了问题。
void reserve(size_t n)
{
// 提前算好size,不然后续会改变 _start 位置,就算不了size了
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start) //防止第一次进来_start为空,memcpy出错
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;/* 这里出现了问题 */
}
_start = tmp;
_finish = tmp + sz;
_endofstorage = tmp + n;
}
}
问题分析:
memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
问题根源:memcpy 的浅拷贝特性
memcpy
函数执行的是逐字节的浅拷贝,它只是简单地将内存中的字节从一个位置复制到另一个位置,而不会调用任何构造函数或赋值运算符。
对于 vector<string>
这种情况:
每个
string
对象内部包含指向实际字符串数据的指针使用
memcpy
时,只是复制了这些指针值,而不是指针指向的实际字符串数据当原 vector 被销毁时,原
string
对象会调用析构函数释放它们指向的内存但新 vector 中的
string
对象仍然指向已被释放的内存区域,导致悬空指针访问这些悬空指针指向的内存就是未定义行为,表现为乱码或程序崩溃
解决方案:使用循环赋值实现深拷贝
当将扩容代码改为:
void reserve(size_t n)
{
// 提前算好size,不然后续会改变 _start 位置,就算不了size了
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start) //防止第一次进来_start为空,memcpy出错
{
// memcpy(tmp, _start, sizeof(T) * sz); 拷贝自定义类型时会导致浅拷贝问题
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = tmp + sz;
_endofstorage = tmp + n;
}
}
这里发生了以下关键变化:
调用了赋值运算符:对于每个元素,都会调用
string::operator=
,这是一个深拷贝操作创建独立副本:每个新
string
对象都会分配自己的内存并复制字符串内容避免悬空指针:新旧 vector 中的
string
对象指向不同的内存区域,互不影响
Vector 的内存布局
当你创建一个vector<string>
时,内存布局是这样的:
_vector 对象本身:
_start -> [string对象1][string对象2][string对象3][string对象4]...
_finish -> 指向最后一个元素的下一个位置
_endofstorage -> 指向分配的内存块的末尾
关键点是:vector 存储的是 string 对象本身,而不是指向 string 对象的指针。这些 string 对象在内存中是连续存储的。
“Vector 存储的是 string 对象本身”的含义
(下图中的string类成员是假设出来的,实际成员可能不一样,但内存布局是一样的)
_start[0] 这个内存位置存储的是:
[ char* _str | size_t _size | size_t _capacity | ...其他成员 ]
更详细的内存结构图说明:
Vector内存布局 (栈上或堆上)
┌─────────────────────────────────────────────────────────────┐
│ _start指针 │ 指向vector内部数组的起始位置 │
├─────────────────────────────────────────────────────────────┤
│ _finish指针 │ 指向最后一个元素的下一个位置 │
├─────────────────────────────────────────────────────────────┤
│_endofstorage指针│ 指向分配的内存块的末尾 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────┬─────────┬─────────┬─────────┐ ← vector内部数组(在堆上)
│ string0 │ string1 │ string2 │ string3 │
└─────────┴─────────┴─────────┴─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ← 每个string对象的_str成员指向的
│"1111│ │"2222│ │"3333│ │"4444│ 字符串数据(也在堆上,但不同位置)
└─────┘ └─────┘ └─────┘ └─────┘
关键点分解:
vector的对象数组:当你创建
vector<string> v(4)
时,vector会在堆上分配一块足够大的连续内存,用来存放4个完整的string对象。每个string对象:这块内存中的每个"格子"都包含一个完整的string对象,包括:
char* _str
(指针,通常4或8字节)size_t _size
(通常4或8字节)size_t _capacity
(通常4或8字节)可能的其他成员变量
字符串数据:每个string对象的
_str
成员指向另一块堆内存,那里存储着实际的字符串内容("1111", "2222"等)。
为什么循环赋值有效
现在让我们看看循环:
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
对于每次迭代:
_start[i]
获取第i个string对象tmp[i]
获取新数组中第i个位置(此时可能是一个未初始化的string对象)调用string的赋值运算符
string::operator=
,将右侧string的内容复制到左侧string
重要的是:这不是简单的内存拷贝,而是调用了string类的赋值运算符,它会进行深拷贝 - 分配新的内存并复制字符串内容。
重新理解拷贝问题
现在我们就能明白为什么memcpy
有问题而循环赋值正确了:
memcpy:只复制了vector数组内存块(包含string对象的成员变量),包括复制了
_str
指针值。结果是新旧vector中的string对象指向相同的字符串数据内存。循环赋值:
tmp[i] = _start[i]
调用了string的赋值运算符,这个运算符会:释放
tmp[i]
原有资源(如果有)为新的字符串数据分配内存
复制字符串内容
更新size和capacity成员
一个很好的验证方式
我们可以添加一些调试输出来验证这个理解:
void test_debug() {
vector<string> v;
v.push_back("dfb");
v.push_back("asdf ds akjfhksdhfkhasdfkhskdfhk");
v.push_back("12bbbbbb6161rtb616t1b6r1t6516161bbb");
v.push_back("646asdg56as6dg65s16551agsd");
cout << "Address of vector array: " << (void*)v.begin() << endl;
for (int i = 0; i < v.size(); i++) {
cout << "Address of string object " << i << ": " << (void*)&v[i] << endl;
cout << "Address of string data " << i << ": " << (void*)v[i].c_str() << endl;
cout << "Sizeof(string): " << sizeof(string) << endl;
}
}
这个代码会显示string对象本身是连续存储的,但每个string对象指向的字符串数据在不同的内存地址。
为什么标准库vector没有这个问题
标准库的 std::vector
使用了一种叫做"类型特质(type traits)"的技术,能够识别类型是否是"平凡可拷贝(trivially copyable)"的。对于平凡可拷贝的类型(如基本数据类型、简单结构体),它使用 memcpy
等高效方法;对于非平凡类型(如 string
),它会调用拷贝构造函数或赋值运算符。