C++為什么不能修改set里的值?非要修改怎么辦?
在上一期C++中 set的用法文章當(dāng)中講解了set
的一些常規(guī)用法和api
,最后末尾的時(shí)候留了一個(gè)問(wèn)題,如何修改set中的元素?今天就來(lái)聊聊這個(gè)問(wèn)題。
很多同學(xué)估計(jì)會(huì)說(shuō),這還不簡(jiǎn)單,不是有迭代器么。我們把迭代器當(dāng)做指針,去修改它指向的值不就行了嗎?
like this:
set<string> st{"hello", "world", "good"}; set<string>::iterator it = st.begin(); *it = "test";
但是很遺憾,你真這么做了,會(huì)得到一個(gè)報(bào)錯(cuò):
報(bào)錯(cuò)的意思是set
的迭代器并沒(méi)有重載等于符號(hào),也就是說(shuō)我們沒(méi)辦法使用等于符號(hào)來(lái)為它賦值。說(shuō)白了,也就是編譯器進(jìn)行了限制,不允許我們對(duì)set
迭代器的內(nèi)容進(jìn)行修改。
Effective C++
當(dāng)中也明確說(shuō)了,不要對(duì)set
集合中的元素進(jìn)行修改。
不知道有沒(méi)有小伙伴去嘗試,可能有些小伙伴嘗試了之后會(huì)說(shuō)不對(duì)啊,在我電腦上怎么能運(yùn)行?
也很簡(jiǎn)單,大概率因?yàn)槟阌玫氖莢c編譯器,比如臭名昭著的VC6.0
或者是visual studio IDE
(不是VSCode)。微軟的編譯器沒(méi)有嚴(yán)格遵循C++的標(biāo)準(zhǔn),在很多地方有些瑕疵和隨意。這也是不推薦使用VC6.0進(jìn)行C++學(xué)習(xí)的原因,因?yàn)闀r(shí)間久了,就把錯(cuò)的當(dāng)成對(duì)的了。
吐槽完畢,回到正題。既然已經(jīng)知道了這樣修改會(huì)引發(fā)報(bào)錯(cuò),是不是就已經(jīng)得到了答案了呢?
其實(shí)并沒(méi)有,因?yàn)槿绻覀冋娴娜ラ喿xC++的標(biāo)準(zhǔn)或者是翻閱set的源碼,會(huì)發(fā)現(xiàn)其中是沒(méi)有明確說(shuō)明set中的元素是定義成const
的。
實(shí)際上,std::set<T>
聲明一個(gè)allocator_type
,默認(rèn)為std::allocator<T>。std::allocator_traits<allocator_type>::construct
將它傳遞給T *,從而構(gòu)造一個(gè)T,而不是const T
。
說(shuō)人話(huà)就是std::set<T>
其實(shí)不允許將元素定義成const
,既然元素不是const
類(lèi)型,那么就說(shuō)明理論上是可以修改的。也就是說(shuō)C++規(guī)范里說(shuō)不能改,Effective C++
中說(shuō)建議不要改,但實(shí)際上底層的實(shí)現(xiàn)里并沒(méi)有嚴(yán)格禁止。我們非要改還是有辦法的,那是什么辦法呢?
老梁縱觀全網(wǎng)博客,也沒(méi)有看到一篇把這個(gè)問(wèn)題說(shuō)清楚。
在我們開(kāi)始之前,首先思考一個(gè)問(wèn)題,既然set
底層源碼當(dāng)中的元素并不是定義成const
,那么當(dāng)我們?nèi)ビ玫魅バ薷牡臅r(shí)候?yàn)槭裁磿?huì)報(bào)錯(cuò)呢?
要回答這個(gè)問(wèn)題,我們只需要查看一下set
迭代器的源碼定義即可。
老梁在大牛的源碼分析當(dāng)中找到了一行關(guān)鍵的代碼:
原來(lái)迭代器的定義是一個(gè)const_iterator
,搞了半天,其實(shí)并不是set
底層限制了禁止修改,而是通過(guò)迭代器限制的。所以要想修改set
當(dāng)中的元素,我們只需要繞開(kāi)迭代器的這個(gè)限制即可。
進(jìn)一步研究可以發(fā)現(xiàn),它這里使用的是一個(gè)const_iterator
,它表示一個(gè)指向常量的迭代器,和const iterator
不同。后者表示迭代器本身是一個(gè)常量,即迭代器本身指向的位置不能修改。而前者表示迭代器指向的位置是一個(gè)const
常量,迭代器本身可以修改,指向不同的位置,但我們不能修改它指向的位置的值。
const_iterator
并沒(méi)有嚴(yán)格限制只能指向const
修飾的變量,這也就能解釋為什么set
當(dāng)中的元素沒(méi)有const
修飾也不會(huì)報(bào)錯(cuò)的原因,因?yàn)?code>const_iterator兼容這種情況。
const_iterator
解引用之后是一個(gè)const
修飾的變量的引用,所以我們要對(duì)它指向的內(nèi)容進(jìn)行修改,只需要將它解引用的結(jié)果去除const
限制即可。那具體怎么操作呢,我們可以使用const_cast
操作符解除const
的限制。
但它也不是萬(wàn)能的,它只能使用在引用和指針當(dāng)中,用來(lái)去掉const
屬性。
這里有必要說(shuō)明一下,在C++當(dāng)中const
修飾符出現(xiàn)的位置不同有不同的含義。以指針舉例,const T* p
和T* const p
是兩種完全不同的指針,前者表示不能通過(guò)指針去修改指向?qū)ο蟮膬?nèi)容。如p->x = 100
;這樣的操作都是非法的。而后者表示指針只能在初始化時(shí)設(shè)置指向的內(nèi)容,之后不能修改指向,如p=&t
;是非法的。
在當(dāng)前問(wèn)題當(dāng)中,我們想要修改set
當(dāng)中的元素值,遇到了const
限制,顯然是第一種情況。
有些同學(xué)可能會(huì)覺(jué)得疑惑,我們加上const
的目的不就是為了對(duì)變量做限制,從而可以在編譯的時(shí)候通過(guò)編譯器來(lái)替我們檢查一些非法的操作嗎?既然如此,又為什么需要去掉呢?
主要的原因是有時(shí)候我們手上的變量有const修飾,但是我們想要調(diào)用一個(gè)函數(shù),而函數(shù)的內(nèi)部會(huì)對(duì)指針或引用指向的值進(jìn)行修改。這個(gè)時(shí)候我們就沒(méi)辦法傳入我們手上已有的參數(shù)了,const_cast
操作符設(shè)計(jì)的初衷就是為了應(yīng)對(duì)這種情況。
我們來(lái)舉個(gè)例子:
void test(int *x) { *x = 5; } int main() { int a = 3; const int *p = &a; test(p); return 0; }
如果我們編譯上面這段代碼就會(huì)遇到編譯器無(wú)情地報(bào)錯(cuò),因?yàn)槲覀冊(cè)趖est函數(shù)內(nèi)部修改了指針p的指向。
這個(gè)時(shí)候我們就可以在傳參的時(shí)候,使用const_cast
操作符來(lái)解除掉const
的限制。
test(const_cast<int*>(p));
尖括號(hào)中是我們要轉(zhuǎn)換的類(lèi)型,只能是指針或引用。如果我們輸出指針p指向的值,會(huì)得到5,因?yàn)樵趖est函數(shù)當(dāng)中進(jìn)行了修改。
看起來(lái)好像很簡(jiǎn)單,對(duì)吧?
但是我們接下來(lái)看兩個(gè)例子,可能會(huì)令人有些費(fèi)解:
const int a = 3; int *r = const_cast<int*>(&a); (*r)++; cout << a << endl; int i = 3; const int b = i; int *r2 = const_cast<int*>(&b); (*r2)++; cout << b << endl;
這兩段代碼做的事情非常類(lèi)似,也就是通過(guò)const_cast
修改了一個(gè)const
修飾的int
。唯一的不同是int a是直接賦值成了3,而int b是賦值成了另外一個(gè)也等于3的int。這兩者其實(shí)并沒(méi)有什么區(qū)別,對(duì)吧?但是當(dāng)我們運(yùn)行代碼之后,神奇的事情發(fā)生了,
屏幕上輸出的結(jié)果是這樣的:
為什么一個(gè)是3,另外一個(gè)是4呢?這兩者的邏輯明明是一樣的!
老梁發(fā)現(xiàn)這個(gè)問(wèn)題的時(shí)候是完全震驚的,查了好久的資料,才從大牛博客的只言片語(yǔ)當(dāng)中找到了一點(diǎn)描述。原來(lái)是編譯器針對(duì)第一種情況做了優(yōu)化,因?yàn)閍初始化時(shí)給的是一個(gè)常量,所以當(dāng)我們輸出的時(shí)候,編譯器就直接取了3代替了它實(shí)際原本應(yīng)該的值。
關(guān)于這個(gè)解釋老梁也不能完全確認(rèn),如果有知道的小伙伴不妨在下方留言。
最后, 我們回到正題,如果我們想要修改set
當(dāng)中的元素,可以怎么操作呢?
我們知道了const_cast
操作符之后就完全沒(méi)有懸念了。
set<string> st{"hello", "world", "good"}; set<string>::iterator it = st.begin(); const_cast<string&>(*it) = "test"; for (auto it = st.begin(); it != st.end(); it++) { cout << *it << endl; }
但是我們需要注意一點(diǎn),我們這樣強(qiáng)行修改其實(shí)是沒(méi)有經(jīng)過(guò)set
原本的機(jī)制的。也就是說(shuō)我們雖然改了元素的值,但是它在紅黑樹(shù)中的位置其實(shí)是沒(méi)有變的。這樣的結(jié)果就是會(huì)導(dǎo)致元素失去有序性,比如上面的結(jié)果輸出的順序是:"test","hello","world
",按道理應(yīng)該是按照字典順序排序的。
這也是為什么C++ Primer
里強(qiáng)烈建議大家不要修改set
中元素值的原因,如果真的要修改,只能先刪除再添加了。雖然這樣會(huì)犧牲一點(diǎn)點(diǎn)性能,但至少可以保證set
里的數(shù)據(jù)都是安全有序的。
到此這篇關(guān)于C++為什么不能修改set里的值?非要修改怎么辦?的文章就介紹到這了,更多相關(guān)C++修改set
里的值內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
注:文章轉(zhuǎn)自微信公眾號(hào):Coder梁(ID:Coder_LT)
- C++中 set的用法
- C++ set到底是什么
- c++ bitset詳解
- C/C++ memset方法的誤區(qū)
- C++中SetConsoleCursorPosition()移動(dòng)光標(biāo)函數(shù)的用法大全
- C/C++ 中怎樣使用SetConsoleTextAttribute()函數(shù)來(lái)控制輸出字符的顏色
- C/C++中memset,memcpy的使用及fill對(duì)數(shù)組的操作
- C++ bitset的簡(jiǎn)單使用示例
- C/C++ 中memset() 函數(shù)詳解及其作用介紹
- c++容器list、vector、map、set區(qū)別與用法詳解
相關(guān)文章
詳解C++的JSON靜態(tài)鏈接庫(kù)JsonCpp的使用方法
這篇文章主要介紹了C++的JSON靜態(tài)鏈接庫(kù)JsonCpp的使用方法,演示了使用JsonCpp生成和解析JSON的方法,以及C++通過(guò)JSON方式的socket通信示例,需要的朋友可以參考下2016-03-03淺談C++類(lèi)型轉(zhuǎn)化(運(yùn)算符重載函數(shù))和基本運(yùn)算符重載(自增自減)
下面小編就為大家?guī)?lái)一篇淺談C++類(lèi)型轉(zhuǎn)化(運(yùn)算符重載函數(shù))和基本運(yùn)算符重載(自增自減)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06關(guān)于在MFC中將窗口最小化到托盤(pán)實(shí)現(xiàn)原理及操作步驟
最小化的原理:首先要將窗口隱藏,然后在右下角繪制圖標(biāo);恢復(fù)的原理:將窗口顯示,再將托盤(pán)中的圖片刪除,接下來(lái)介紹實(shí)現(xiàn)方法,感興趣的朋友可以了解下啊,希望本文對(duì)你有所幫助2013-01-01解析C語(yǔ)言基于UDP協(xié)議進(jìn)行Socket編程的要點(diǎn)
這篇文章主要介紹了C語(yǔ)言通過(guò)UDP協(xié)議進(jìn)行Socket編程的要點(diǎn),文中還提到了相關(guān)ARP與ICMP協(xié)議的作用,需要的朋友可以參考下2016-02-02