欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++之memcpy導(dǎo)致的深拷貝問題分析

 更新時間:2025年09月18日 09:33:10   作者:一枝小雨  
使用memcpy拷貝vector中自定義類型元素(如string)時,僅復(fù)制指針值導(dǎo)致懸空指針,引發(fā)未定義行為,而循環(huán)賦值通過調(diào)用賦值運(yùn)算符實現(xiàn)深拷貝,正確復(fù)制對象內(nèi)容,標(biāo)準(zhǔn)庫vector通過類型特質(zhì)區(qū)分類型,對非平凡類型調(diào)用構(gòu)造/析值函數(shù),避免此類問題

代碼與講解承接上文:C++之vector深度剖析及模擬實現(xiàn)

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

打印結(jié)果是 3 和 4 沒有問題,但是 1 和 2 都是亂碼。是擴(kuò)容時出現(xiàn)了問題。

void reserve(size_t n)
{
        // 提前算好size,不然后續(xù)會改變 _start 位置,就算不了size了
        size_t sz = size();
        if (n > capacity())
        {
                T* tmp = new T[n];
                if (_start)        //防止第一次進(jìn)來_start為空,memcpy出錯
                {
                        memcpy(tmp, _start, sizeof(T) * sz);
                        delete[] _start;/* 這里出現(xiàn)了問題 */
                }
                _start = tmp;
                _finish = tmp + sz;
                _endofstorage = tmp + n;
        }
}

問題分析

  • memcpy是內(nèi)存的二進(jìn)制格式拷貝,將一段內(nèi)存空間中內(nèi)容原封不動的拷貝到另外一段內(nèi)存空間中
  • 如果拷貝的是自定義類型的元素,memcpy既高效又不會出錯,但如果拷貝的是自定義類型元素,并且自定義類型元素中涉及到資源管理時,就會出錯,因為memcpy的拷貝實際是淺拷貝。

問題根源:memcpy 的淺拷貝特性

memcpy 函數(shù)執(zhí)行的是逐字節(jié)的淺拷貝,它只是簡單地將內(nèi)存中的字節(jié)從一個位置復(fù)制到另一個位置,而不會調(diào)用任何構(gòu)造函數(shù)或賦值運(yùn)算符。

對于 vector<string> 這種情況:

  • 每個 string 對象內(nèi)部包含指向?qū)嶋H字符串?dāng)?shù)據(jù)的指針
  • 使用 memcpy 時,只是復(fù)制了這些指針值,而不是指針指向的實際字符串?dāng)?shù)據(jù)
  • 當(dāng)原 vector 被銷毀時,原 string 對象會調(diào)用析構(gòu)函數(shù)釋放它們指向的內(nèi)存
  • 但新 vector 中的 string 對象仍然指向已被釋放的內(nèi)存區(qū)域,導(dǎo)致懸空指針
  • 訪問這些懸空指針指向的內(nèi)存就是未定義行為,表現(xiàn)為亂碼或程序崩潰

解決方案:使用循環(huán)賦值實現(xiàn)深拷貝

當(dāng)將擴(kuò)容代碼改為:

void reserve(size_t n)
{
        // 提前算好size,不然后續(xù)會改變 _start 位置,就算不了size了
        size_t sz = size();
        if (n > capacity())
        {
                T* tmp = new T[n];
                if (_start)        //防止第一次進(jìn)來_start為空,memcpy出錯
                {
                        // memcpy(tmp, _start, sizeof(T) * sz);        拷貝自定義類型時會導(dǎo)致淺拷貝問題
                        for (size_t i = 0; i < sz; ++i)
                        {
                                tmp[i] = _start[i];
                        }
                        delete[] _start;
                }
                _start = tmp;
                _finish = tmp + sz;
                _endofstorage = tmp + n;
        }
}

這里發(fā)生了以下關(guān)鍵變化:

  • 調(diào)用了賦值運(yùn)算符:對于每個元素,都會調(diào)用 string::operator=,這是一個深拷貝操作
  • 創(chuàng)建獨立副本:每個新 string 對象都會分配自己的內(nèi)存并復(fù)制字符串內(nèi)容
  • 避免懸空指針:新舊 vector 中的 string 對象指向不同的內(nèi)存區(qū)域,互不影響

Vector 的內(nèi)存布局

當(dāng)你創(chuàng)建一個vector<string>時,內(nèi)存布局是這樣的:

_vector 對象本身:
_start    -> [string對象1][string對象2][string對象3][string對象4]...
_finish   -> 指向最后一個元素的下一個位置
_endofstorage -> 指向分配的內(nèi)存塊的末尾

關(guān)鍵點是:vector 存儲的是 string 對象本身,而不是指向 string 對象的指針。這些 string 對象在內(nèi)存中是連續(xù)存儲的。

“Vector 存儲的是 string 對象本身”的含義

(下圖中的string類成員是假設(shè)出來的,實際成員可能不一樣,但內(nèi)存布局是一樣的)

_start[0] 這個內(nèi)存位置存儲的是:
[ char* _str | size_t _size | size_t _capacity | ...其他成員 ]

更詳細(xì)的內(nèi)存結(jié)構(gòu)圖說明:

 Vector內(nèi)存布局 (棧上或堆上)
┌─────────────────────────────────────────────────────────────┐
│  _start指針  │ 指向vector內(nèi)部數(shù)組的起始位置                 │
├─────────────────────────────────────────────────────────────┤
│  _finish指針 │ 指向最后一個元素的下一個位置                 │
├─────────────────────────────────────────────────────────────┤
│_endofstorage指針│ 指向分配的內(nèi)存塊的末尾                    │
└─────────────────────────────────────────────────────────────┘
    ↓
    ┌─────────┬─────────┬─────────┬─────────┐ ← vector內(nèi)部數(shù)組(在堆上)
    │ string0 │ string1 │ string2 │ string3 │
    └─────────┴─────────┴─────────┴─────────┘
        │         │         │         │
        │         │         │         │
        ▼         ▼         ▼         ▼
    ┌─────┐   ┌─────┐   ┌─────┐   ┌─────┐   ← 每個string對象的_str成員指向的
    │"1111│   │"2222│   │"3333│   │"4444│     字符串?dāng)?shù)據(jù)(也在堆上,但不同位置)
    └─────┘   └─────┘   └─────┘   └─────┘

關(guān)鍵點分解

vector的對象數(shù)組:當(dāng)你創(chuàng)建vector<string> v(4)時,vector會在堆上分配一塊足夠大的連續(xù)內(nèi)存,用來存放4個完整的string對象。

每個string對象:這塊內(nèi)存中的每個"格子"都包含一個完整的string對象,包括:

  • char* _str(指針,通常4或8字節(jié))
  • size_t _size(通常4或8字節(jié))
  • size_t _capacity(通常4或8字節(jié))

可能的其他成員變量

字符串?dāng)?shù)據(jù):每個string對象的_str成員指向另一塊堆內(nèi)存,那里存儲著實際的字符串內(nèi)容("1111", "2222"等)。

為什么循環(huán)賦值有效

現(xiàn)在讓我們看看循環(huán):

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

對于每次迭代:

  • _start[i]獲取第i個string對象
  • tmp[i]獲取新數(shù)組中第i個位置(此時可能是一個未初始化的string對象)
  • 調(diào)用string的賦值運(yùn)算符string::operator=,將右側(cè)string的內(nèi)容復(fù)制到左側(cè)string

重要的是:這不是簡單的內(nèi)存拷貝,而是調(diào)用了string類的賦值運(yùn)算符,它會進(jìn)行深拷貝 - 分配新的內(nèi)存并復(fù)制字符串內(nèi)容。

重新理解拷貝問題

現(xiàn)在我們就能明白為什么memcpy有問題而循環(huán)賦值正確了:

memcpy:只復(fù)制了vector數(shù)組內(nèi)存塊(包含string對象的成員變量),包括復(fù)制了_str指針值。結(jié)果是新舊vector中的string對象指向相同的字符串?dāng)?shù)據(jù)內(nèi)存。

循環(huán)賦值:tmp[i] = _start[i]調(diào)用了string的賦值運(yùn)算符,這個運(yùn)算符會:

  • 釋放tmp[i]原有資源(如果有)
  • 為新的字符串?dāng)?shù)據(jù)分配內(nèi)存
  • 復(fù)制字符串內(nèi)容
  • 更新size和capacity成員

一個很好的驗證方式

我們可以添加一些調(diào)試輸出來驗證這個理解:

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對象本身是連續(xù)存儲的,但每個string對象指向的字符串?dāng)?shù)據(jù)在不同的內(nèi)存地址。

為什么標(biāo)準(zhǔn)庫vector沒有這個問題

標(biāo)準(zhǔn)庫的 std::vector 使用了一種叫做"類型特質(zhì)(type traits)"的技術(shù),能夠識別類型是否是"平凡可拷貝(trivially copyable)"的。

對于平凡可拷貝的類型(如基本數(shù)據(jù)類型、簡單結(jié)構(gòu)體),它使用 memcpy 等高效方法;對于非平凡類型(如 string),它會調(diào)用拷貝構(gòu)造函數(shù)或賦值運(yùn)算符。

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • C語言通過三種方法實現(xiàn)屬于你的通訊錄

    C語言通過三種方法實現(xiàn)屬于你的通訊錄

    本文將實現(xiàn)一個通訊錄,來實現(xiàn)人員的增刪插改功能。文中通過三種形式來實現(xiàn)用戶的增刪插改,其實也就是一點點的優(yōu)化版本,從靜態(tài)的實現(xiàn),到動態(tài)的實現(xiàn),最后以文件的形式來完成,請大家和我一起往下看吧
    2022-11-11
  • C++ opencv ffmpeg圖片序列化實現(xiàn)代碼解析

    C++ opencv ffmpeg圖片序列化實現(xiàn)代碼解析

    這篇文章主要介紹了C++ opencv ffmpeg圖片序列化實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-08-08
  • 減少C++代碼編譯時間的簡單方法(必看篇)

    減少C++代碼編譯時間的簡單方法(必看篇)

    下面小編就為大家?guī)硪黄獪p少C++代碼編譯時間的簡單方法(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01
  • 使用C/C++語言生成一個隨機(jī)迷宮游戲

    使用C/C++語言生成一個隨機(jī)迷宮游戲

    迷宮相信大家都走過,主要是考驗?zāi)愕倪壿嬎季S。今天小編使用C語言生成一個隨機(jī)迷宮游戲,具體實現(xiàn)代碼,大家通過本文學(xué)習(xí)吧
    2016-12-12
  • C語言實現(xiàn)洗牌與發(fā)牌游戲

    C語言實現(xiàn)洗牌與發(fā)牌游戲

    這篇文章主要為大家詳細(xì)介紹了C語言洗牌與發(fā)牌游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-12-12
  • C語言 socketpair用法案例講解

    C語言 socketpair用法案例講解

    這篇文章主要介紹了C語言 socketpair用法案例講解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • Qt實現(xiàn)自定義驗證碼輸入框控件的方法

    Qt實現(xiàn)自定義驗證碼輸入框控件的方法

    本文主要介紹了Qt實現(xiàn)自定義驗證碼輸入框控件的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • C++實現(xiàn)猜牌小游戲

    C++實現(xiàn)猜牌小游戲

    這篇文章主要為大家詳細(xì)介紹了C++實現(xiàn)猜牌小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-07-07
  • C++實現(xiàn)屏幕截圖

    C++實現(xiàn)屏幕截圖

    這篇文章主要為大家詳細(xì)介紹了C++實現(xiàn)屏幕截圖功能,截圖自動保存為png格式文件,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • 基于C++實現(xiàn)TCP聊天室功能

    基于C++實現(xiàn)TCP聊天室功能

    這篇文章主要為大家詳細(xì)介紹了基于C++實現(xiàn)TCP聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-07-07

最新評論