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* pT* 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里的值内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

注:文章转自微信公众号:Coder梁(ID:Coder_LT)

(0)

相关推荐

  • c++容器list、vector、map、set区别与用法详解

    c++容器list.vector.map.set区别 list 封装链表,以链表形式实现,不支持[]运算符. 对随机访问的速度很慢(需要遍历整个链表),插入数据很快(不需要拷贝和移动数据,只需改变指针的指向). 新添加的元素,list可以任意加入. vector 封装数组,使用连续内存存储,支持[]运算符. 对随机访问的速度很快,对头插元素速度很慢,尾插元素速度很快 新添加的元素,vector有一套算法. map 采用平衡检索二叉树:红黑树 存储结构为键值对<key,value> set 采用

  • C++中 set的用法

    目录 1.创建set 1.1 方法1 1.2 方法2 1.3 方法三 2.使用set 2.1 insert 2.2 emplace 2.3 emplace_hint 2.4 erase 2.5 clear 2.6 find 2.7 count 2.8 lower_bound 和 upper_bound 2.9 equal_range 3.总结 前言: 今天咱们继续来聊聊C++中的set. 上次的文章C++ set到底是什么遗留了一个问题没有回答,有些小伙伴有些疑问.就是为什么说set是关联式的容

  • C++中SetConsoleCursorPosition()移动光标函数的用法大全

    SetConsoleCursorPosition()来自于文件"windows.h",使用时记得引用此头文件. 首先说一下,这个函数的功能即是字面意思,即移动命令行中光标的位置.这里要注意的是,每次调用这个函数都是默认从左上角开始偏移,而与当前光标停留的位置无关. 然后我们剖析下这个函数,我们查看定义发现,调用这个函数需要传入两个参数,都是自定义类型,分别为 HANDLE 和 COORD. SetConsoleCursorPosition( _In_ HANDLE hConsoleOu

  • C/C++ 中memset() 函数详解及其作用介绍

    memset 函数是内存赋值函数,用来给某一块内存空间进行赋值的: 包含在<string.h>头文件中,可以用它对一片内存空间逐字节进行初始化: 原型为 : void *memset(void *s, int v, size_t n); 这里s可以是数组名,也可以是指向某一内在空间的指针: v为要填充的值: n为要填充的字节数: 例子: struct data { char num[100]; char name[100]; int n; }; struct data a, b[10]; me

  • C/C++中memset,memcpy的使用及fill对数组的操作

    对数组的整体赋值,以及两个数组间的复制容易出错,这里使用string头文件中的memset和memcpy进行 不必遍历数组,速度快. 之前没有头文件,显示decla 头文件: 代码: /* Project: 数组的整体赋值与复制 Date: 2018/07/31 Author: Frank Yu memset(数组名,0或-1,字节) memcpy(数组名,数组名,字节) */ #include<iostream> #include<cstring> //memset需要头文件 #

  • C++ bitset的简单使用示例

    bitset 一般代替 bool 数组使用,常用于优化空间,因为 bitset 中一个元素只占 1 bit. bitset 的大小在定义使就需要确定.如果想要不定长的 bitset,就需要使用 vector. bitset 的定义: bitset<16> bt; // 定义大小为16的bitset,每一位都是0 bitset<16> bt(string("11001")); // 定义大小为16的bitset,并用string初始化,注意高位为0,也就是 000

  • C/C++ memset方法的误区

    一.函数作用 最简单的调用就是将一个数组清零,代码如下: const int maxn = 1024; int a[maxn]; memset(a, 0, sizeof(a)); // 结果:a[0]=a[1]=a[...]=0; 这里 sizeof(a) = maxn * 4 = 4096: 表示的是将数组首地址 a 开始往后的 4096 个字节,都设置为 0: 二.效率对比 直接调用 memset 接口清零 和 调用循环进行清零,进行一个测试后如下: 对长度为 10000000 的数组,执行

  • C++ set到底是什么

    目录 1.set是什么 2.set有什么用 1.set是什么 如果大家学过几门编程语言,会发现各大语言的特性虽然迥异,但是总有几个东西反复出现刷存在感.它们在各个语言当中的名字虽然不太一样,底层实现也不同,但是做的事情差不多. 在C++当中,这几个东西的名字叫做vector.set和map,它们有一个共同的名字叫做STL(标准模板库)容器. 估计不少同学看到容器这两个字脑袋有点发蒙,会有一种我当然知道容器是什么意思,但是我不知道你这里说容器是什么意思的感觉.现实中的容器是用来存储东西的器皿,在编

  • C/C++ 中怎样使用SetConsoleTextAttribute()函数来控制输出字符的颜色

    当我们在命令行输出字符的时候,会想着,要是能控制输出字符的颜色就好了. 现在,满足你,有这么一个函数能够帮助我们实现这个想法. ↓↓↓↓↓ SetConsoleTextAttribute(); 依照惯例我们来看看这个函数的构成. SetConsoleTextAttribute( _In_ HANDLE hConsoleOutput, _In_ WORD wAttributes ); HANDLE在上一篇博客中我们见过.注意在使用前,要先将其变为标准输出句柄. typedef void *HAND

  • c++ bitset详解

    目录 bitset 总结 bitset 使用bitset需要添加头文件bitset ,bitset是一种特殊的容器 ,bitset数组中 ,每个成员只装1bit位的数据 , 即只能是0 或 1. bitset 的基本用法 : 初始化bitset容器 bitset<'size'> 容器名:未初始化容器数据,会默认每位都是0 也可以给一个具体的初始化:存入int数据 bitset<'size'> 容器名('num'): 会将num以二进制形式储存在bitset中,如果初始化的容器位数不

随机推荐