C++修改set里的值 C++为什么不能修改set里的值?非要修改怎么办?
Coder_LT 人气:0在上一期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里的数据都是安全有序的。
注:文章转自微信公众号:Coder梁(ID:Coder_LT)
加载全部内容