C++之memcpy導(dǎo)致的深拷貝問(wèn)題分析
代碼與講解承接上文:C++之vector深度剖析及模擬實(shí)現(xiàn)
memcpy:更深一層次的深淺拷貝問(wèn)題
/* 自定義類型 */
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 沒(méi)有問(wèn)題,但是 1 和 2 都是亂碼。是擴(kuò)容時(shí)出現(xiàn)了問(wèn)題。
void reserve(size_t n)
{
// 提前算好size,不然后續(xù)會(huì)改變 _start 位置,就算不了size了
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start) //防止第一次進(jìn)來(lái)_start為空,memcpy出錯(cuò)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;/* 這里出現(xiàn)了問(wèn)題 */
}
_start = tmp;
_finish = tmp + sz;
_endofstorage = tmp + n;
}
}問(wèn)題分析
- memcpy是內(nèi)存的二進(jìn)制格式拷貝,將一段內(nèi)存空間中內(nèi)容原封不動(dòng)的拷貝到另外一段內(nèi)存空間中
- 如果拷貝的是自定義類型的元素,memcpy既高效又不會(huì)出錯(cuò),但如果拷貝的是自定義類型元素,并且自定義類型元素中涉及到資源管理時(shí),就會(huì)出錯(cuò),因?yàn)閙emcpy的拷貝實(shí)際是淺拷貝。
問(wèn)題根源:memcpy 的淺拷貝特性
memcpy 函數(shù)執(zhí)行的是逐字節(jié)的淺拷貝,它只是簡(jiǎn)單地將內(nèi)存中的字節(jié)從一個(gè)位置復(fù)制到另一個(gè)位置,而不會(huì)調(diào)用任何構(gòu)造函數(shù)或賦值運(yùn)算符。
對(duì)于 vector<string> 這種情況:
- 每個(gè)
string對(duì)象內(nèi)部包含指向?qū)嶋H字符串?dāng)?shù)據(jù)的指針 - 使用
memcpy時(shí),只是復(fù)制了這些指針值,而不是指針指向的實(shí)際字符串?dāng)?shù)據(jù) - 當(dāng)原 vector 被銷毀時(shí),原
string對(duì)象會(huì)調(diào)用析構(gòu)函數(shù)釋放它們指向的內(nèi)存 - 但新 vector 中的
string對(duì)象仍然指向已被釋放的內(nèi)存區(qū)域,導(dǎo)致懸空指針 - 訪問(wèn)這些懸空指針指向的內(nèi)存就是未定義行為,表現(xiàn)為亂碼或程序崩潰
解決方案:使用循環(huán)賦值實(shí)現(xiàn)深拷貝
當(dāng)將擴(kuò)容代碼改為:
void reserve(size_t n)
{
// 提前算好size,不然后續(xù)會(huì)改變 _start 位置,就算不了size了
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start) //防止第一次進(jìn)來(lái)_start為空,memcpy出錯(cuò)
{
// memcpy(tmp, _start, sizeof(T) * sz); 拷貝自定義類型時(shí)會(huì)導(dǎo)致淺拷貝問(wèn)題
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)算符:對(duì)于每個(gè)元素,都會(huì)調(diào)用
string::operator=,這是一個(gè)深拷貝操作 - 創(chuàng)建獨(dú)立副本:每個(gè)新
string對(duì)象都會(huì)分配自己的內(nèi)存并復(fù)制字符串內(nèi)容 - 避免懸空指針:新舊 vector 中的
string對(duì)象指向不同的內(nèi)存區(qū)域,互不影響
Vector 的內(nèi)存布局
當(dāng)你創(chuàng)建一個(gè)vector<string>時(shí),內(nèi)存布局是這樣的:
_vector 對(duì)象本身: _start -> [string對(duì)象1][string對(duì)象2][string對(duì)象3][string對(duì)象4]... _finish -> 指向最后一個(gè)元素的下一個(gè)位置 _endofstorage -> 指向分配的內(nèi)存塊的末尾
關(guān)鍵點(diǎn)是:vector 存儲(chǔ)的是 string 對(duì)象本身,而不是指向 string 對(duì)象的指針。這些 string 對(duì)象在內(nèi)存中是連續(xù)存儲(chǔ)的。
“Vector 存儲(chǔ)的是 string 對(duì)象本身”的含義
(下圖中的string類成員是假設(shè)出來(lái)的,實(shí)際成員可能不一樣,但內(nèi)存布局是一樣的)
_start[0] 這個(gè)內(nèi)存位置存儲(chǔ)的是: [ char* _str | size_t _size | size_t _capacity | ...其他成員 ]
更詳細(xì)的內(nèi)存結(jié)構(gòu)圖說(shuō)明:
Vector內(nèi)存布局 (棧上或堆上)
┌─────────────────────────────────────────────────────────────┐
│ _start指針 │ 指向vector內(nèi)部數(shù)組的起始位置 │
├─────────────────────────────────────────────────────────────┤
│ _finish指針 │ 指向最后一個(gè)元素的下一個(gè)位置 │
├─────────────────────────────────────────────────────────────┤
│_endofstorage指針│ 指向分配的內(nèi)存塊的末尾 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────┬─────────┬─────────┬─────────┐ ← vector內(nèi)部數(shù)組(在堆上)
│ string0 │ string1 │ string2 │ string3 │
└─────────┴─────────┴─────────┴─────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ← 每個(gè)string對(duì)象的_str成員指向的
│"1111│ │"2222│ │"3333│ │"4444│ 字符串?dāng)?shù)據(jù)(也在堆上,但不同位置)
└─────┘ └─────┘ └─────┘ └─────┘關(guān)鍵點(diǎn)分解
vector的對(duì)象數(shù)組:當(dāng)你創(chuàng)建vector<string> v(4)時(shí),vector會(huì)在堆上分配一塊足夠大的連續(xù)內(nèi)存,用來(lái)存放4個(gè)完整的string對(duì)象。
每個(gè)string對(duì)象:這塊內(nèi)存中的每個(gè)"格子"都包含一個(gè)完整的string對(duì)象,包括:
char* _str(指針,通常4或8字節(jié))size_t _size(通常4或8字節(jié))size_t _capacity(通常4或8字節(jié))
可能的其他成員變量
字符串?dāng)?shù)據(jù):每個(gè)string對(duì)象的_str成員指向另一塊堆內(nèi)存,那里存儲(chǔ)著實(shí)際的字符串內(nèi)容("1111", "2222"等)。
為什么循環(huán)賦值有效
現(xiàn)在讓我們看看循環(huán):
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}對(duì)于每次迭代:
_start[i]獲取第i個(gè)string對(duì)象tmp[i]獲取新數(shù)組中第i個(gè)位置(此時(shí)可能是一個(gè)未初始化的string對(duì)象)- 調(diào)用string的賦值運(yùn)算符
string::operator=,將右側(cè)string的內(nèi)容復(fù)制到左側(cè)string
重要的是:這不是簡(jiǎn)單的內(nèi)存拷貝,而是調(diào)用了string類的賦值運(yùn)算符,它會(huì)進(jìn)行深拷貝 - 分配新的內(nèi)存并復(fù)制字符串內(nèi)容。
重新理解拷貝問(wèn)題
現(xiàn)在我們就能明白為什么memcpy有問(wèn)題而循環(huán)賦值正確了:
memcpy:只復(fù)制了vector數(shù)組內(nèi)存塊(包含string對(duì)象的成員變量),包括復(fù)制了_str指針值。結(jié)果是新舊vector中的string對(duì)象指向相同的字符串?dāng)?shù)據(jù)內(nèi)存。
循環(huán)賦值:tmp[i] = _start[i]調(diào)用了string的賦值運(yùn)算符,這個(gè)運(yùn)算符會(huì):
- 釋放
tmp[i]原有資源(如果有) - 為新的字符串?dāng)?shù)據(jù)分配內(nèi)存
- 復(fù)制字符串內(nèi)容
- 更新size和capacity成員
一個(gè)很好的驗(yàn)證方式
我們可以添加一些調(diào)試輸出來(lái)驗(yàn)證這個(gè)理解:
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;
}
}這個(gè)代碼會(huì)顯示string對(duì)象本身是連續(xù)存儲(chǔ)的,但每個(gè)string對(duì)象指向的字符串?dāng)?shù)據(jù)在不同的內(nèi)存地址。
為什么標(biāo)準(zhǔn)庫(kù)vector沒(méi)有這個(gè)問(wèn)題
標(biāo)準(zhǔn)庫(kù)的 std::vector 使用了一種叫做"類型特質(zhì)(type traits)"的技術(shù),能夠識(shí)別類型是否是"平凡可拷貝(trivially copyable)"的。
對(duì)于平凡可拷貝的類型(如基本數(shù)據(jù)類型、簡(jiǎn)單結(jié)構(gòu)體),它使用 memcpy 等高效方法;對(duì)于非平凡類型(如 string),它會(huì)調(diào)用拷貝構(gòu)造函數(shù)或賦值運(yùn)算符。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C語(yǔ)言通過(guò)三種方法實(shí)現(xiàn)屬于你的通訊錄
本文將實(shí)現(xiàn)一個(gè)通訊錄,來(lái)實(shí)現(xiàn)人員的增刪插改功能。文中通過(guò)三種形式來(lái)實(shí)現(xiàn)用戶的增刪插改,其實(shí)也就是一點(diǎn)點(diǎn)的優(yōu)化版本,從靜態(tài)的實(shí)現(xiàn),到動(dòng)態(tài)的實(shí)現(xiàn),最后以文件的形式來(lái)完成,請(qǐng)大家和我一起往下看吧2022-11-11
C++ opencv ffmpeg圖片序列化實(shí)現(xiàn)代碼解析
這篇文章主要介紹了C++ opencv ffmpeg圖片序列化實(shí)現(xiàn)代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
減少C++代碼編譯時(shí)間的簡(jiǎn)單方法(必看篇)
下面小編就為大家?guī)?lái)一篇減少C++代碼編譯時(shí)間的簡(jiǎn)單方法(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01
使用C/C++語(yǔ)言生成一個(gè)隨機(jī)迷宮游戲
迷宮相信大家都走過(guò),主要是考驗(yàn)?zāi)愕倪壿嬎季S。今天小編使用C語(yǔ)言生成一個(gè)隨機(jī)迷宮游戲,具體實(shí)現(xiàn)代碼,大家通過(guò)本文學(xué)習(xí)吧2016-12-12
C語(yǔ)言實(shí)現(xiàn)洗牌與發(fā)牌游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言洗牌與發(fā)牌游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12
Qt實(shí)現(xiàn)自定義驗(yàn)證碼輸入框控件的方法
本文主要介紹了Qt實(shí)現(xiàn)自定義驗(yàn)證碼輸入框控件的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04

