如何自定义C++ STL 关联式容器排序规则,和修改容器的键

自定义容器排序规则

1) 使用函数对象自定义排序规则

不知道函数对象是什么请浏览C++ 函数对象(仿函数)

无论关联式容器中存储的是基础类型(如 int、double、float 等)数据,还是自定义的结构体变量或类对象(包括 string 类),都可以使用函数对象的方式为该容器自定义排序规则。
下面样例以 set 容器为例,演示了如何用函数对象的方式自定义排序规则:

#include <iostream>
#include <set> // set
#include <string> // string
using namespace std;

//定义函数对象类 class cmp { public:   //重载 () 运算符   bool operator ()(const string &a,const string &b) {     //按照字符串的长度,做升序排序(即存储的字符串从短到长)     return (a.length() < b.length());   } };
int main() {   //创建 set 容器,并使用自定义的 cmp 排序规则   std::set<string, cmp>myset{"http://c.biancheng.net/stl/",                   "http://c.biancheng.net/python/",                   "http://c.biancheng.net/java/"};   //输出容器中存储的元素   for (auto iter = myset.begin(); iter != myset.end(); ++iter) {     cout << *iter << endl;   }   return 0; }

程序执行结果为:

http://c.biancheng.net/stl/
http://c.biancheng.net/java/
http://c.biancheng.net/python/

重点分析一下 6~13 行代码,其定义了一个函数对象类,并在其重载 () 运算符的方法中自定义了新的排序规则,即按照字符串的长度做升序排序。在此基础上,程序第 17 行代码中,通过将函数对象类的类名 cmp 通过 set 类模板的第 2 个参数传递给 myset 容器,该容器内部排序数据的规则,就改为了以字符串的长度为标准做升序排序。

需要注意的是,此程序中创建的 myset 容器,由于是以字符串的长度为准进行排序,因此其无法存储相同长度的多个字符串。

上面程序中,函数对象类 cmp 也可以使用 struct 关键字创建:

//定义函数对象类
struct cmp {
  //重载 () 运算符
  bool operator ()(const string &a, const string &b) {
    //按照字符串的长度,做升序排序(即存储的字符串从短到长)
    return (a.length() < b.length());
  }
};

值得一提的是,在定义函数对象类时,也可以将其定义为模板类。比如:

//定义函数对象模板类
template <typename T>
class cmp {
public:
  //重载 () 运算符
  bool operator ()(const T &a, const T &b) {
    //按照值的大小,做升序排序
    return a < b;
  }
};

注意,此方式必须保证 T 类型元素可以直接使用关系运算符(比如这里的 < 运算符)做比较。

2) 重载关系运算符实现自定义排序

其实在 STL 标准库中,本就包含几个可供关联式容器使用的排序规则,如表 1 表示。

排序规则功能
std::less<T>    底层采用 < 运算符实现升序排序,各关联式容器默认采用的排序规则。
std::greater<T> 底层采用 > 运算符实现降序排序,同样适用于各个关联式容器。
std::less_equal<T> 底层采用 <= 运算符实现升序排序,多用于 multimap 和 multiset 容器。
std::greater_equal<T> 底层采用 >= 运算符实现降序排序,多用于 multimap 和 multiset 容器。

上表中的这些排序规则,其底层也是采用函数对象的方式实现的。以 std::less<T> 为例,其底层实现为:

template <typename T>
struct less {
  //定义新的排序规则
  bool operator()(const T &_lhs, const T &_rhs) const {
    return _lhs < _rhs;
  }
};

在此基础上,当关联式容器中存储的数据类型为自定义的结构体变量或者类对象时,通过对现有排序规则中所用的关系运算符进行重载,也能实现自定义排序规则的目的。

注意,当关联式容器中存储的元素类型为结构体指针变量或者类的指针对象时,只能使用函数对象的方式自定义排序规则,此方法不再适用。

举个例子:

#include <iostream>
#include <set> // set
#include <string> // string
using namespace std;

//自定义类 class myString { public:   //定义构造函数,向 myset 容器中添加元素时会用到   myString(string tempStr) :str(tempStr) {};   //获取 str 私有对象,由于会被私有对象调用,因此该成员方法也必须为 const 类型   string getStr() const; private:   string str; };
string myString::getStr() const{   return this->str; }
//重载 < 运算符,参数必须都为 const 类型 bool operator <(const myString &stra, const myString & strb) {   //以字符串的长度为标准比较大小   return stra.getStr().length() < strb.getStr().length(); }
int main() {   //创建空 set 容器,仍使用默认的 less<T> 排序规则   std::set<myString>myset;   //向 set 容器添加元素,这里会调用 myString 类的构造函数   myset.emplace("http://c.biancheng.net/stl/");   myset.emplace("http://c.biancheng.net/c/");   myset.emplace("http://c.biancheng.net/python/");   //   for (auto iter = myset.begin(); iter != myset.end(); ++iter) {     myString mystr = *iter;     cout << mystr.getStr() << endl;   }   return 0; }

程序执行结果为:

http://c.biancheng.net/c/
http://c.biancheng.net/stl/
http://c.biancheng.net/python/

在这个程序中,虽然 myset 容器表面仍采用默认的 std::less<T> 排序规则,但由于我们对其所用的 < 运算符进行了重载,使得 myset 容器内部实则是以字符串的长度为基准,对各个 mystring 类对象进行排序。
另外,上面程序以全局函数的形式实现对 < 运算符的重载,还可以使用成员函数或者友元函数的形式实现。其中,当以成员函数的方式重载 < 运算符时,该成员函数必须声明为 const 类型,且参数也必须为 const 类型:

bool operator <(const myString & tempStr) const {
  //以字符串的长度为标准比较大小
  return this->str.length() < tempStr.str.length();
}

至于参数的传值方式是采用按引用传递还是按值传递,都可以(建议采用按引用传递,效率更高)。

同样,如果以友元函数的方式重载 < 运算符时,要求参数必须使用 const 修饰:

//类中友元函数的定义
friend bool operator <(const myString &a, const myString &b);
//类外部友元函数的具体实现
bool operator <(const myString &stra, const myString &strb) {
  //以字符串的长度为标准比较大小
  return stra.str.length() < strb.str.length();
}

 

修改容器中键值对的键的方法

对于如何修改容器中某个键值对的键,所有关联式容器可以采用同一种解决思路,即先删除该键值对,然后再向容器中添加修改之后的新键值对。
那么,是否可以不删除目标键值对,而直接修改它的键呢?
首先可以明确的是,map 和 multimap 容器只能采用“先删除,再添加”的方式修改某个键值对的键。因为C++ STL 标准中明确规定,map 和 multimap 容器用于存储类型为 pair<const K, V> 的键值对。显然,只要目标键值对存储在当前容器中,键的值就无法被修改。

直接修改 map 或 multimap 容器中某个键值对的键是行不通的。但对于 set 或者 multiset 容器来说,却是可行的。和 map、multimap 不同,C++ STL 标准中并没有用 const 限定 set 和 multiset 容器中存储元素的类型。换句话说,对于 set<T> 或者 multiset<T> 类型的容器,其存储元素的类型是 T 而不是 const T。


事实上,对 set 和 multiset 容器中的元素类型作 const 修饰,是违背常理的。举个例子,假设我们使用 set 容器存储多个学生信息,如下是一个表示学生的类:

 

class student {
public:
  student(string name, int id, int age) :name(name), id(id), age(age) {}
  
  const int& getid() const {     return id;   }
  void setname(const string name){     this->name = name;   }
  string getname() const{     return name;   }
  void setage(int age){     this->age = age;   }
  int getage() const{     return age;   } private:   string name;   int id;   int age; };

 

在创建 set 容器之前,我们还需要为其设计一个排序规则,这里假定以每个学生的 id 做升序排序,其排序规则如下:

class cmp {
public:
  bool operator ()(const student &stua, const student &stub) {
    //按照字符串的长度,做升序排序(即存储的字符串从短到长)
    return stua.getid() < stub.getid();
  }
};

做完以上所有的准备工作后,就可以创建一个可存储 student 对象的 set 容器了,比如:

set<student, cmp> myset{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };

由此创建的 myset 容器中,存储的数据依次为:

{"zhangsan",10,20}
{"wangwu",15,19}
{"lisi",20,21}

注意,set 容器中每个元素也可以看做是键和值相等的键值对,但对于这里的 myset 容器来说,其实每个 student 对象的 id 才是真正的键,其它信息(name 和 age)只不过是和 id 绑定在一起而已。因此,在不破坏 myset 容器中元素的有序性的前提下(即不修改每个学生的 id),学生的其它信息是应该允许修改的,但有一个前提,即 myset 容器中存储的各个 student 对象不能被 const 修饰(这也是 set 容器中的元素类型不能被 const 修饰的原因)。

总之,set 和 multiset 容器的元素类型没有用 const 修饰。所以从语法的角度分析,我们可以直接修改容器中元素的值,但一定不要修改元素的键。

例如,在已创建好的 myset 容器的基础上,如下代码尝试修改 myset 容器中某个学生的 name 名字:

set<student>::iterator iter = mymap.begin();
(*iter).setname("xiaoming");

注意,如果读者运行代码会发现,它也是无法通过编译的。
虽然 C++ STL 标准没有用 const 修饰 set 或者 multiset 容器中元素的类型,但也做了其它工作来限制用户修改容器的元素。例如上面代码中,*iter 会调用 operator*,其返回的是一个 const T& 类型元素。这意味着,C++ STL 标准不允许用户借助迭代器来直接修改 set 或者 multiset 容器中的元素。
那么,如何才能正确修改 set 或 multiset 容器中的元素呢?最直接的方式就是借助 const_cast 运算符,该运算符可以去掉指针或者引用的 const 限定符。

 

比如,我们只需要借助 const_cast 运算符对上面程序稍作修改,就可以运行成功:

set<student>::iterator iter = mymap.begin();
const_cast<student&>(*iter).setname("xiaoming");

由此,mymap 容器中的 {"zhangsan",10,20} 就变成了 {"xiaoming",10,20}。

再次强调,虽然使用 const_cast 能直接修改 set 或者 multiset 容器中的元素,但一定不要修改元素的键!如果要修改,只能采用“先删除,再添加”的方式。另外,不要试图以同样的方式修改 map 或者 multimap 容器中键值对的键,这违反了 C++ STL 标准的规定。

参考C语言中文网-STL-关联式容器部分

posted @ 2021-09-03 20:17  默行于世  阅读(577)  评论(0编辑  收藏  举报