C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++之memcpy导致的深拷贝

C++之memcpy导致的深拷贝问题分析

作者:一枝小雨

使用memcpy拷贝vector中自定义类型元素(如string)时,仅复制指针值导致悬空指针,引发未定义行为,而循环赋值通过调用赋值运算符实现深拷贝,正确复制对象内容,标准库vector通过类型特质区分类型,对非平凡类型调用构造/析值函数,避免此类问题

代码与讲解承接上文:C++之vector深度剖析及模拟实现

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 函数执行的是逐字节的浅拷贝,它只是简单地将内存中的字节从一个位置复制到另一个位置,而不会调用任何构造函数或赋值运算符。

对于 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;
        }
}

这里发生了以下关键变化:

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对象,包括:

可能的其他成员变量

字符串数据:每个string对象的_str成员指向另一块堆内存,那里存储着实际的字符串内容("1111", "2222"等)。

为什么循环赋值有效

现在让我们看看循环:

for (size_t i = 0; i < sz; ++i)
{
    tmp[i] = _start[i];
}

对于每次迭代:

重要的是:这不是简单的内存拷贝,而是调用了string类的赋值运算符,它会进行深拷贝 - 分配新的内存并复制字符串内容。

重新理解拷贝问题

现在我们就能明白为什么memcpy有问题而循环赋值正确了:

memcpy:只复制了vector数组内存块(包含string对象的成员变量),包括复制了_str指针值。结果是新旧vector中的string对象指向相同的字符串数据内存。

循环赋值:tmp[i] = _start[i]调用了string的赋值运算符,这个运算符会:

一个很好的验证方式

我们可以添加一些调试输出来验证这个理解:

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),它会调用拷贝构造函数或赋值运算符。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文