C++為什麼不能修改set裡的值?非要修改怎麼辦?
在上一期C++中 set的用法文章當中講解瞭set
的一些常規用法和api
,最後末尾的時候留瞭一個問題,如何修改set中的元素?今天就來聊聊這個問題。
很多同學估計會說,這還不簡單,不是有迭代器麼。我們把迭代器當做指針,去修改它指向的值不就行瞭嗎?
like this:
set<string> st{"hello", "world", "good"}; set<string>::iterator it = st.begin(); *it = "test";
但是很遺憾,你真這麼做瞭,會得到一個報錯:
報錯的意思是set
的迭代器並沒有重載等於符號,也就是說我們沒辦法使用等於符號來為它賦值。說白瞭,也就是編譯器進行瞭限制,不允許我們對set
迭代器的內容進行修改。
Effective C++
當中也明確說瞭,不要對set
集合中的元素進行修改。
不知道有沒有小夥伴去嘗試,可能有些小夥伴嘗試瞭之後會說不對啊,在我電腦上怎麼能運行?
也很簡單,大概率因為你用的是vc編譯器,比如臭名昭著的VC6.0
或者是visual studio IDE
(不是VSCode)。微軟的編譯器沒有嚴格遵循C++的標準,在很多地方有些瑕疵和隨意。這也是不推薦使用VC6.0進行C++學習的原因,因為時間久瞭,就把錯的當成對的瞭。
吐槽完畢,回到正題。既然已經知道瞭這樣修改會引發報錯,是不是就已經得到瞭答案瞭呢?
其實並沒有,因為如果我們真的去閱讀C++的標準或者是翻閱set的源碼,會發現其中是沒有明確說明set中的元素是定義成const
的。
實際上,std::set<T>
聲明一個allocator_type
,默認為std::allocator<T>。std::allocator_traits<allocator_type>::construct
將它傳遞給T *,從而構造一個T,而不是const T
。
說人話就是std::set<T>
其實不允許將元素定義成const
,既然元素不是const
類型,那麼就說明理論上是可以修改的。也就是說C++規范裡說不能改,Effective C++
中說建議不要改,但實際上底層的實現裡並沒有嚴格禁止。我們非要改還是有辦法的,那是什麼辦法呢?
老梁縱觀全網博客,也沒有看到一篇把這個問題說清楚。
在我們開始之前,首先思考一個問題,既然set
底層源碼當中的元素並不是定義成const
,那麼當我們去用迭代器去修改的時候為什麼會報錯呢?
要回答這個問題,我們隻需要查看一下set
迭代器的源碼定義即可。
老梁在大牛的源碼分析當中找到瞭一行關鍵的代碼:
原來迭代器的定義是一個const_iterator
,搞瞭半天,其實並不是set
底層限制瞭禁止修改,而是通過迭代器限制的。所以要想修改set
當中的元素,我們隻需要繞開迭代器的這個限制即可。
進一步研究可以發現,它這裡使用的是一個const_iterator
,它表示一個指向常量的迭代器,和const iterator
不同。後者表示迭代器本身是一個常量,即迭代器本身指向的位置不能修改。而前者表示迭代器指向的位置是一個const
常量,迭代器本身可以修改,指向不同的位置,但我們不能修改它指向的位置的值。
const_iterator
並沒有嚴格限制隻能指向const
修飾的變量,這也就能解釋為什麼set
當中的元素沒有const
修飾也不會報錯的原因,因為const_iterator
兼容這種情況。
const_iterator
解引用之後是一個const
修飾的變量的引用,所以我們要對它指向的內容進行修改,隻需要將它解引用的結果去除const
限制即可。那具體怎麼操作呢,我們可以使用const_cast
操作符解除const
的限制。
但它也不是萬能的,它隻能使用在引用和指針當中,用來去掉const
屬性。
這裡有必要說明一下,在C++當中const
修飾符出現的位置不同有不同的含義。以指針舉例,const T* p
和T* const p
是兩種完全不同的指針,前者表示不能通過指針去修改指向對象的內容。如p->x = 100
;這樣的操作都是非法的。而後者表示指針隻能在初始化時設置指向的內容,之後不能修改指向,如p=&t
;是非法的。
在當前問題當中,我們想要修改set
當中的元素值,遇到瞭const
限制,顯然是第一種情況。
有些同學可能會覺得疑惑,我們加上const
的目的不就是為瞭對變量做限制,從而可以在編譯的時候通過編譯器來替我們檢查一些非法的操作嗎?既然如此,又為什麼需要去掉呢?
主要的原因是有時候我們手上的變量有const修飾,但是我們想要調用一個函數,而函數的內部會對指針或引用指向的值進行修改。這個時候我們就沒辦法傳入我們手上已有的參數瞭,const_cast
操作符設計的初衷就是為瞭應對這種情況。
我們來舉個例子:
void test(int *x) { *x = 5; } int main() { int a = 3; const int *p = &a; test(p); return 0; }
如果我們編譯上面這段代碼就會遇到編譯器無情地報錯,因為我們在test函數內部修改瞭指針p的指向。
這個時候我們就可以在傳參的時候,使用const_cast
操作符來解除掉const
的限制。
test(const_cast<int*>(p));
尖括號中是我們要轉換的類型,隻能是指針或引用。如果我們輸出指針p指向的值,會得到5,因為在test函數當中進行瞭修改。
看起來好像很簡單,對吧?
但是我們接下來看兩個例子,可能會令人有些費解:
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;
這兩段代碼做的事情非常類似,也就是通過const_cast
修改瞭一個const
修飾的int
。唯一的不同是int a是直接賦值成瞭3,而int b是賦值成瞭另外一個也等於3的int。這兩者其實並沒有什麼區別,對吧?但是當我們運行代碼之後,神奇的事情發生瞭,
屏幕上輸出的結果是這樣的:
為什麼一個是3,另外一個是4呢?這兩者的邏輯明明是一樣的!
老梁發現這個問題的時候是完全震驚的,查瞭好久的資料,才從大牛博客的隻言片語當中找到瞭一點描述。原來是編譯器針對第一種情況做瞭優化,因為a初始化時給的是一個常量,所以當我們輸出的時候,編譯器就直接取瞭3代替瞭它實際原本應該的值。
關於這個解釋老梁也不能完全確認,如果有知道的小夥伴不妨在下方留言。
最後, 我們回到正題,如果我們想要修改set
當中的元素,可以怎麼操作呢?
我們知道瞭const_cast
操作符之後就完全沒有懸念瞭。
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; }
但是我們需要註意一點,我們這樣強行修改其實是沒有經過set
原本的機制的。也就是說我們雖然改瞭元素的值,但是它在紅黑樹中的位置其實是沒有變的。這樣的結果就是會導致元素失去有序性,比如上面的結果輸出的順序是:”test","hello","world
“,按道理應該是按照字典順序排序的。
這也是為什麼C++ Primer
裡強烈建議大傢不要修改set
中元素值的原因,如果真的要修改,隻能先刪除再添加瞭。雖然這樣會犧牲一點點性能,但至少可以保證set裡的數據都是安全有序的。
到此這篇關於C++為什麼不能修改set裡的值?非要修改怎麼辦?的文章就介紹到這瞭,更多相關C++修改set裡的值內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- c++中的const_cast用法大全
- C++強制類型轉換(static_cast、dynamic_cast、const_cast、reinterpret_cast)
- 關於C++中push_back()函數的用法及代碼實例
- 從c++標準庫指針萃取器談一下traits技法(推薦)
- C++右值引用與移動構造函數基礎與應用詳解