NDK 基础(四)—— C++ 高级特性1

1、STL 容器

STL(Standard Template Library)译为标准模板库,它是一套强大的标准库,是 C++ 标准库的一部分,在 iostream 中。STL 提供了包括容器在内的许多常用的数据结构和算法,以及用于操作这些数据结构的迭代器。STL 被分为 STL 包、算法包和迭代器,是为了提供更好的模块化和可扩展性:

  1. STL 包(Containers):
    STL 包提供了各种常用的容器类,如向量(vector)、链表(list)、集合(set)、映射(map)等。这些容器类为数据存储和管理提供了不同的实现方式和特性,例如动态数组、双向链表、红黑树等。STL 包的目的是提供通用数据结构的实现,并提供统一的接口和操作方法,使开发人员能够方便地使用这些数据结构。
  2. 算法包(Algorithms):
    算法包提供了大量的算法实现,如排序、查找、遍历、变换等。这些算法可以应用于各种不同类型的容器,而不依赖于具体的容器实现。通过使用迭代器作为算法的输入和输出,算法包提供了一种通用的方式来操作容器中的元素。算法包的设计目标是提供高效、可复用、泛化的算法实现,并促进代码的可读性和可维护性。
  3. 迭代器(Iterators):
    迭代器是 STL 的核心概念之一。迭代器提供了一种统一的访问容器元素的方式,它封装了对容器底层数据结构的访问方式,使得算法能够独立于容器的具体实现进行操作。迭代器提供了类似指针的接口,可以通过解引用操作符(*)来访问元素,并支持移动、比较等操作。通过使用迭代器,算法包可以在不同类型的容器上工作,提供了更大的灵活性和可扩展性。

将 STL 划分为 STL 包、算法包和迭代器的主要目的是提供模块化的设计,并促进代码的重用和扩展。这种划分使得开发人员可以根据具体的需求选择性地使用STL的不同部分,同时也使得 STL 的设计更加灵活和可扩展,可以方便地添加自定义的容器类和算法实现。

1.1 vector

vector 内部封装了一个容量可变的数组作为容器,需要导入 vector, 示例如下:

#include <iostream>
#include <vector>using namespace std;// 声明方式
void sample1() {vector<int> vector1;// 声明 10 个元素,都被默认初始化为 0vector<int> vector2(10);// 声明 10 个元素,都被默认初始化为 1vector<int> vector3(10, 1);
}// 操作数据
void sample2() {vector<int> vector;// 1.插入数据,vector.begin() 是头位置,vector.end() 是末尾vector.insert(vector.begin(), 40);vector.insert(vector.begin(), 50);vector.insert(vector.end(), 60);vector.insert(vector.end(), 120);// 循环遍历打印:50 40 60 120for (int i = 0; i < vector.size(); ++i) {// [] 是运算符重载cout << vector[i] << " ";}cout << endl;// 2.修改数据vector.front() = 66;vector.back() = 77;vector[1] = 666;// 遍历输出:66 666 60 77for (int i: vector) {cout << i << " ";}cout << endl;// 3.删除,可以删除一个,也可以传入删除的范围vector.erase(vector.begin());
//    vector.erase(vector.begin(), vector.end());// 666 60 77for (int i: vector) {cout << i << " ";}cout << endl;// 4.通过迭代器遍历 vectorfor (::vector<int>::iterator it = vector.begin(); it != vector.end(); it++) {cout << *it << " ";}cout << endl;// 5.简化迭代器的声明,使用 auto 关键字做类型推导for (auto it = vector.begin(); it != vector.end(); it++) {cout << *it << " ";}cout << endl;// 6.更简单的迭代方式for (int & it : vector) {cout << it << " ";}
}

注意一下第 4 条中,声明迭代器变量的时候,它的完整类型是 std::vector<int>::iterator,如果导入了 std 命名空间的话,可以省去 std 简写为 ::vector<int>::iterator

1.2 stack

需要导入 stack,示例代码:

#include <iostream>
#include <stack>using namespace std;int main() {stack<int> stack;// 数据进栈stack.push(20);stack.push(40);stack.push(60);// 由于 stack 没有重载 [] 运算符也没有迭代器,只能通过// top() 获取栈顶数据,通过 pop() 将栈顶弹出以遍历栈顶// 数据,但是通过 pop() 弹栈后数据就不在栈中了while (!stack.empty()) {cout << "栈顶元素为:" << stack.top() << endl;// 弹出栈顶元素stack.pop();}
}

1.3 queue

NDK 中用 queue 用的比较多,其他的容器基本用不到:

#include <iostream>
#include <queue>using namespace std;int main() {queue<int> queue;// 数据入队queue.push(10);queue.push(20);queue.push(30);// 与栈类似,队列没有重载 [] 也没有迭代器,想遍历队列// 中的数据只能通过让数据出队的方式while (!queue.empty()) {cout << "队首元素为:" << queue.front() << ",队尾元素为:" << queue.back() << endl;// 队首元素出队queue.pop();}
}

输出结果:

队首元素为:10,队尾元素为:30
队首元素为:20,队尾元素为:30
队首元素为:30,队尾元素为:30

1.4 priority_queue

优先级队列是对 vector 的封装,默认使用 less 结构体,将内部元素从大到小排列:

void sample2() {// 默认的 priority_queue,按照从大到小排序// 实际上隐含了 <> 中的 less 结构体priority_queue<int> priorityQueue;priorityQueue.push(10);priorityQueue.push(20);priorityQueue.push(30);priorityQueue.push(40);while (!priorityQueue.empty()) {cout << priorityQueue.top() << " ";priorityQueue.pop();}cout << endl;// 定义时传入 greater 结构体,使得从小到大排序priority_queue<int, vector<int>, greater<>> priorityQueue1;priorityQueue1.push(10);priorityQueue1.push(20);priorityQueue1.push(30);priorityQueue1.push(40);while (!priorityQueue1.empty()) {cout << priorityQueue1.top() << " ";priorityQueue1.pop();}
}

如果需要从小到大排列,则需要在创建 priority_queue 时在类型参数中传入 greater 结构体。

less 和 greater 结构体的作用有点类似于 Java 的 Comparator:

  template<typename _Tp>struct less : public binary_function<_Tp, _Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x < __y; }};template<typename _Tp>struct greater : public binary_function<_Tp, _Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x > __y; }};

1.5 list

int main() {list<int> list;// 向 list 中插入数据可以使用 push_xxx 或 insert 两种方式list.push_front(10);list.push_back(20);list.insert(list.begin(), 30);list.insert(list.end(), 40);// 遍历结果,::list<int>::iterator 这个迭代器类型通常使用类型推断的 auto 代替/*for (::list<int>::iterator it = list.begin(); it != list.end(); it++) {cout << *it << " ";}*/for (auto it = list.begin(); it != list.end(); it++) {cout << *it << " ";}cout << endl;// 修改数据list.front() = 66;list.back() = 99;for (const auto &item: list) {cout << item << " ";}cout << endl;// 删除数据list.erase(list.begin());// 注意删除时不能使用 list.end(),因为该函数返回的是// 迭代器最后一个位置的下一个位置,因此插入时可以使用,但删除// 时使用会导致越界而崩溃,使用 pop_back() 删除最后一个元素
//    list.erase(list.end());list.pop_back();// 最好带上 &for (int value: list) {cout << value << " ";}cout << endl;
}

遍历 list 有多种方式,上述代码示例展示了三种。

1.6 set

int main() {// set 默认会用 less 对内部数据进行从大到小的排序,// 这里我们声明一个 greater 的set<int, greater<int>> set;set.insert(1);set.insert(2);set.insert(3);set.insert(4);// 插入的返回值是 pair<iterator, bool>,两个类型第一个// 是迭代器,可以指定如何排序;第二个是 bool 值表示插入是否成功pair<::set<int, less<>>::iterator, bool> pair = set.insert(2);bool insert_success = pair.second;if (insert_success) {cout << "插入成功" << endl;// 使用迭代器遍历for (; pair.first != set.end(); pair.first++) {cout << *pair.first << " ";}} else {cout << "插入失败" << endl;}
}

1.7 map

// map 演示
void sample1() {map<int, string> map;// 1.使用 insert 插入数据,有三种形式map.insert({0, "第零个元素"});map.insert(std::map<int, string>::value_type(3, "第三个元素"));map.insert(pair<int, string>(1, "第一个元素"));map.insert(make_pair(2, "第二个元素"));// 2.使用下标操作符 []map[4] = "第四个元素";// 3.使用 emplace 函数map.emplace(5, "第五个元素");// 4.利用迭代器遍历输出 map 内容for (std::map<int, string>::iterator it = map.begin(); it != map.end(); it++) {cout << it->first << "," << it->second << "\t";}cout << endl;// 5.判断插入操作结果,具体见 insertMapstring str = "覆盖后的第五个元素";insertMap(map, 5, str);str = "新存入的第六个元素";insertMap(map, 6, str);// 6.使用下标操作符可以修改已经存在的 key 对应的 valuemap[3] = "修改后的第三个元素";// 7.查找操作auto findIterator = map.find(3);if (findIterator != map.end()) {cout << "查找成功:" << findIterator->first << "," << findIterator->second << endl;} else {cout << "查找失败!" << endl;}
}

示例运行结果:

1.遍历 map 中的数据:
0,第零个元素	1,第一个元素	2,第二个元素	3,第三个元素	4,第四个元素	5,第五个元素	
----------------------------------------------------
2.使用 insert 插入数据测试:
插入失败!
0,第零个元素	1,第一个元素	2,第二个元素	3,第三个元素	4,第四个元素	5,第五个元素	
插入成功!
0,第零个元素	1,第一个元素	2,第二个元素	3,第三个元素	4,第四个元素	5,第五个元素	6,新存入的第六个元素	
----------------------------------------------------
3.使用 [] 修改 map 的 value 并查找的结果:
查找成功:3,修改后的第三个元素

以上例子展示了几个知识点:

  1. 向 map 中添加元素的几种方式:insert()、下标操作符和 emplace():

    • insert():已经存在的 key 无法通过 insert() 再次插入,会插入失败
    • 下标操作符:可以添加新的 key,也可以修改已存在的 key 对应的 value
    • emplace():C++11 中引入的功能,用于直接在 std::map 中就地构造键值对,而无需创建临时对象或使用 make_pair 等函数,避免了不必要的拷贝或移动操作。如果要插入的 key 已经存在,则会插入失败
  2. 遍历 map 需要迭代器,可以通过 map.begin() 获取在起始位置的迭代器,除了代码中演示的 std::map<int, string>::iterator 这种类型,还有几种常用类型的迭代器:

    	  // 我们演示中使用的正向迭代器typedef typename _Rep_type::iterator		 iterator;// 常量迭代器typedef typename _Rep_type::const_iterator	 const_iterator;// 反向迭代器typedef typename _Rep_type::reverse_iterator	 reverse_iterator;
    

    此外,在声明迭代器时,类型可以使用 auto 进行类型推断:auto it = map.begin()

  3. 插入操作是有返回结果的:

    // 将向 map 插入元素的操作提取出来
    void insertMap(map<int, string> &map, int key, string &value) {// 判断操作结果,pair 的第一个参数是迭代器,第二个参数是操作结果pair<::map<int, string>::iterator, bool> result = map.insert(pair(key, value));if (result.second) {cout << "插入成功!" << endl;} else {cout << "插入失败!" << endl;}// 通过迭代器遍历输出当前 map 内的元素/*for (auto it = map.begin(); it != map.end(); it++) {cout << it->first << "," << it->second << "\t";}*/for (auto &it: map) {cout << it.first << "," << it.second << "\t";}cout << endl;
    }
    

    结果的类型是 pair<::map<int, string>::iterator, bool>,第一个就是迭代器,可用于获取操作后 map 内的元素,第二个表示插入操作是否成功。此外,遍历 map 还有一个简单写法:for (auto &it: map)

  4. 查找操作可以通过 map.find(key) 完成,返回值是一个迭代器,通过迭代器对象的 first 和 second 可以分别获取 key 和 value,从演示代码的结果可以看出,key = 3 的 value 已经被下标操作符成功修改

  5. map 默认会对 key 进行从小到大的排序,并且不允许 key 重复

1.8 multimap

key 可以重复,key 的重复数据可以分组,对 key 排序,value 不排序,主要用于对 key 进行分组排序时使用:

void sample2() {// 1.key 可以重复  2.key 重复的数据可以分组  3.key 会排序  4.value 不会排序multimap<int, string> multimap;multimap.insert(make_pair(10, "十个1"));multimap.insert(make_pair(10, "十个2"));multimap.insert(make_pair(10, "十个3"));// 输出时仅对 key 排序,而 value 输出的顺序就是 1、3、2multimap.insert(make_pair(30, "三十1"));multimap.insert(make_pair(30, "三十3"));multimap.insert(make_pair(30, "三十2"));multimap.insert(make_pair(20, "二十1"));multimap.insert(make_pair(20, "二十2"));multimap.insert(make_pair(20, "二十3"));// 遍历输出for (auto &it: multimap) {cout << it.first << "," << it.second << endl;}// 输出指定的 key 组内的所有键值对int key;cout << "输入要查询的 key:" << endl;cin >> key;auto iterator = multimap.find(key);while (iterator != multimap.end()) {cout << iterator->first << "," << iterator->second << endl;iterator++;// 控制结束逻辑,因为 multimap.end() 是整个 multimap 结束的位置if (iterator->first != key) {break;}}
}

通过 while 循环判断结束条件时,iterator != multimap.end() 是对于整个 multimap 循环的判断条件,而当我只想输出 find() 的结果时,需要再加上一个对 key 的判断,当 iterator 的 key 不是我们输入的 key 时,就应该跳出循环。

1.9 对象在容器中的生命周期

考虑如下代码的运行结果,以及在执行过程中,Person 会执行几次拷贝构造函数和析构函数:

class Person {
private:string name;
public:Person(string name) : name(name) {}void setName(string name) {this->name = name;}string getName() {return this->name;}Person(const Person &person) {this->name = person.name; // 浅拷贝cout << "Person:" << name << " 执行拷贝构造函数" << endl;}~Person() {cout << "Person:" << name << " 执行析构函数" << endl;}
};int main() {vector<Person> vector;Person person("FirstPerson");vector.insert(vector.begin(), person);person.setName("NewFirstPerson");Person newPerson = vector.front();cout << "new person name = " << newPerson.getName() << endl;return 0;
}

解析结果:

int main() {vector<Person> vector;// person 弹栈会执行一次析构函数Person person("FirstPerson");// person 传给 insert() 内的临时变量,执行一次拷贝构造函数// insert() 内 Person 的临时变量在该函数弹栈时会执行一次析构vector.insert(vector.begin(), person);// 调用 setName() 的是容器外的 person,不会修改容器内的 personperson.setName("NewFirstPerson");// vector.front() 从容器内取出 Person,通过拷贝构造函数给到 newPerson// newPerson 在 main() 弹栈时会调用一次析构Person newPerson = vector.front();// newPerson 是容器内取出的 Person 对象,该对象的 name 是未被修改的,还是 FirstPersoncout << "new person name = " << newPerson.getName() << endl;return 0;
}

运行结果:

Person:FirstPerson 执行拷贝构造函数
Person:FirstPerson 执行拷贝构造函数
new person name = FirstPerson
Person:FirstPerson 执行析构函数
Person:NewFirstPerson 执行析构函数
Person:FirstPerson 执行析构函数

三次析构函数依次是 insert() 内临时变量的 Person、newPerson 以及 person。

2、谓词

在 C++ 中,谓词(Predicate)是指一种可调用对象(函数、函数指针、函数对象或 Lambda 表达式),用于对某个条件进行判断。谓词在算法中经常被用作判断元素是否满足某个条件或用于元素排序的依据。

谓词常用于标准库算法中,例如 std::find_ifstd::sortstd::remove_if 等。这些算法接受一个谓词作为参数,并根据谓词的返回值来执行相应的操作。

一个谓词通常是一个函数,其返回类型为 bool,接受一个或多个参数。根据谓词的返回值,可以判断元素是否符合某个条件。如果谓词返回 true,则表示元素满足条件;如果返回 false,则表示元素不满足条件。

以下是一个示例,展示了如何使用谓词在 std::find_if 中查找符合条件的元素:

#include <iostream>
#include <vector>
#include <algorithm>bool isOdd(int num) {return num % 2 != 0;
}int main() {std::vector<int> numbers = {1, 2, 3, 4, 5};// 使用谓词查找第一个奇数auto it = std::find_if(numbers.begin(), numbers.end(), isOdd);if (it != numbers.end()) {std::cout << "找到了第一个奇数:" << *it << std::endl;} else {std::cout << "未找到奇数" << std::endl;}return 0;
}

在上述代码中,定义了一个谓词函数 isOdd,用于判断一个整数是否为奇数。然后,使用 std::find_if 算法并传入谓词函数 isOdd,来查找 numbers 容器中的第一个奇数。如果找到了奇数,就输出它的值;否则,输出未找到奇数的消息。

我们再结合 set 看一个稍微复杂些的例子,如何自定义谓词:

class Person {
public:string name;int id;Person(string name, int id) : name(name), id(id) {}
};bool compare(const Person &p1, const Person &p2) {return p1.id < p2.id;
}int main() {set<Person> set;// 构建对象并添加到 set 中Person p1("Snake", 1);Person p2("kevin", 2);Person p3("Derry", 3);set.insert(p1);set.insert(p2);set.insert(p3);
}

代码意图很简单,向 set 中插入 Person 对象,但是编译器在调用 insert() 时报错了:In template: invalid operands to binary expression ('const Person' and 'const Person'),大致意思是,set 内部对元素进行排序时,不支持你自定义的 Person 类型。因此我们需要自定义谓词替换掉自带的 less 或 greater。

定义一个结构体并重载运算符 ()

struct compare {
public:// 函数要声明为 constbool operator()(const Person &p1, const Person &p2) const {return p1.id < p2.id;}
};int main() {set<Person, compare> set;// 构建对象并添加到 set 中Person p1("Snake", 6);Person p2("kevin", 10);Person p3("Derry", 3);set.insert(p1);set.insert(p2);set.insert(p3);// 这里的 const 也是强制要求的for (const Person &person: set) {cout << person.name << "," << person.id << endl;}
}

输出结果:

Derry,3
Snake,6
kevin,10

需要注意要将比较函数声明为 const 函数,否则编译也会报错:

static assertion failed due to requirement 'is_invocable_v<const compare &, const Person &, const Person &>': comparison object must be invocable as const

3、仿函数

C++ 中的仿函数(Functor)与谓词(Predicate)是两个相近的概念,我们通过区分二者来引入仿函数。

它们在某些方面有相似之处,但在语义上略有不同,谓词更强调条件判断的功能,而仿函数更强调类对象的可调用性和状态:

  1. 谓词:指一个可调用的函数或函数对象(如函数指针、Lambda 表达式、std::function 等),它接受一个或多个参数并返回一个布尔值。谓词用于在算法中进行条件判断,比如在 STL 算法中的 std::find_ifstd::remove_if 等,或者在容器的成员函数中,如 std::maplower_boundupper_bound 等:

    #include <iostream>
    #include <algorithm>
    #include <vector>bool isEven(int num) {return num % 2 == 0;
    }int main() {std::vector<int> nums = {1, 2, 3, 4, 5};auto it = std::find_if(nums.begin(), nums.end(), isEven);if (it != nums.end()) {std::cout << "Found an even number: " << *it << std::endl;}return 0;
    }
    

    isEven 函数是一个谓词,它接受一个整数参数并返回一个布尔值,用于判断一个数是否为偶数。std::find_if 算法使用该谓词来查找第一个满足条件的偶数。

  2. 仿函数:是一个类或结构体,它重载了函数调用运算符 operator(),使得类对象可以像函数一样被调用。仿函数在行为上类似于函数,可以用于传递到算法中。它可以有自己的状态,可以在调用过程中存储信息:

    #include <iostream>
    #include <algorithm>
    #include <vector>struct MultiplyBy {int factor;MultiplyBy(int f) : factor(f) {}int operator()(int num) const {return num * factor;}
    };int main() {std::vector<int> nums = {1, 2, 3, 4, 5};MultiplyBy multiplyBy2(2);std::transform(nums.begin(), nums.end(), nums.begin(), multiplyBy2);for (int num : nums) {std::cout << num << " ";}std::cout << std::endl;return 0;
    }
    

    在上述代码中,MultiplyBy 是一个仿函数,它接受一个整数参数 factor,并在调用时返回传入数值与 factor 相乘的结果。在 std::transform 算法中,我们使用 multiplyBy2 对每个元素进行乘法操作。

3.1 for_each()

结合算法包中的 for_each() 来看看如何自定义仿函数。

首先,for_each() 是算法包的内容,需要导入 algorithm,我们先看一下它的源码:

  // stl_algo.htemplate<typename _InputIterator, typename _Function>_GLIBCXX20_CONSTEXPR_Functionfor_each(_InputIterator __first, _InputIterator __last, _Function __f){// concept requirements__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)__glibcxx_requires_valid_range(__first, __last);for (; __first != __last; ++__first)__f(*__first);return __f; // N.B. [alg.foreach] says std::move(f) but it's redundant.}

for_each() 有三个参数,前两个是迭代器对象 __first__last,最后一个是一个函数 __f,方法体就是通过 for 循环从 __first__last 执行 __f 函数,并且传入 __first 这个迭代器的值,然后返回 __f,返回值类型为 _Function

据此,我们可以通过自定义仿函数来实现这个传入的函数,比如说打印 set 内的所有元素:

#include <iostream>
#include <algorithm>
#include <set>using namespace std;class FunctorPrint {
public:// 第一个括号表示重载 (),第二个 () 是重载函数的变量// 这个 content 其实是迭代器传过来的的值void operator()(int content) {cout << content << " ";}
};void printSet(int content) {cout << content << " ";
}int main() {set<int> set;set.insert(30);set.insert(20);set.insert(10);// 使用仿函数FunctorPrint functorPrint;for_each(set.begin(), set.end(), functorPrint);cout << endl;// 使用仿函数,直接传入一个 FunctorPrint 类的对象for_each(set.begin(), set.end(), FunctorPrint());cout << endl;// 使用函数for_each(set.begin(), set.end(), printSet);
}

输出结果:

10 20 30 
10 20 30 
10 20 30 

通过上述例子可以帮我们加深理解:

  1. 当一个类重载了函数调用运算符 () 后,就成为了一个仿函数,该类的对象就相当于一个函数
  2. 仿函数本质上还是一个类或者结构体,本质上不是函数,只是因为它的使用方式很像函数,因此才叫仿函数

3.2 为何要使用仿函数

简单说就是扩展性强,源码中频繁使用仿函数。仿函数对象本身就相当于一个可调用的函数,可以接收参数并返回结果。这为我们提供了一种将函数的行为封装在对象中的灵活方式。

还是 set 的例子:

#include <iostream>
#include <set> // STL包
#include <algorithm> // 算法包using namespace std;// 普通函数
void show(int __first) {cout << "函数收到的参数:" << __first << endl;
}// 仿函数:C++ 内置源码使用仿函数频率高,因为其扩展性强
class ShowClass {
public:int count = 0;void showCount() {cout << "输出 count:" << count << endl;}void operator()(int first) {cout << "仿函数接收到的数据:" << first << endl;count++;}
};int main() {set<int> set;set.insert(10);set.insert(20);set.insert(30);// 统计打印次数,使用普通函数是做不到的for_each(set.begin(), set.end(), show);// 使用仿函数可以做到ShowClass showClass;// 重新接收 for_each 的返回值showClass = for_each(set.begin(), set.end(), showClass);showClass.showCount();return 0;
}

在 for_each() 遍历 set 集合时,普通函数只能输出打印内容,无法统计输出了多少次。而仿函数借助本身是类或结构体的特性,可以在内部定义变量和函数来保存打印次数并输出。从这个很小的例子就可看出,使用仿函数的扩展性确实要比使用普通函数要好。

上述代码中有一个需要注意的地方,就是 for_each() 传入的函数与其返回值的函数对象不是同一个对象(因为参数是函数对象,不是函数引用或指向函数的指针,所以在传递时,方法内使用的是实参的副本,与传入的对象不是同一个对象),所以 showClass 才使用 for_each() 的返回值重新赋值,否则调用 showCount() 得到的结果就是 0。以下是完整的测试结果:

函数收到的参数:10
函数收到的参数:20
函数收到的参数:30
仿函数接收到的数据:10
仿函数接收到的数据:20
仿函数接收到的数据:30
输出 count:3

3.3 使用仿函数定义比较器

我们在介绍容器时介绍过,如果容器中存放的类型是用户自己定义的类,那么你需要自己为这个类定义一个比较器,在创建容器对象时传递进去,以支持容器的排序功能。在学习了仿函数后,我们再来看这两个系统提供的比较器:

  template<typename _Tp>struct greater : public binary_function<_Tp, _Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x > __y; }};template<typename _Tp>struct less : public binary_function<_Tp, _Tp, bool>{_GLIBCXX14_CONSTEXPRbooloperator()(const _Tp& __x, const _Tp& __y) const{ return __x < __y; }};

相信你已经看出来了,less 和 greater 实际上就是仿函数,我们也可以仿照它们创建自定义比较器:

class CompareClass {
public:// 使得字符串从大到小排列,即 Z ~ Abool operator()(const string &string1, const string &string2) const {return string1 > string2;}
};int main() {// set 存放 string 类型数据,比较器为 CompareClassset<string, CompareClass> set;set.insert(set.begin(), "AAA");set.insert(set.begin(), "ZZZ");set.insert(set.begin(), "HHH");set.insert(set.begin(), "QQQ");// 遍历 setfor (auto &it: set) {cout << it << endl;}
}

输出结果:

ZZZ
QQQ
HHH
AAA

4、STL 算法包

主要讲算法包中的常用算法。

4.1 find_if

在容器中查找给定范围内符合谓词函数条件的元素,并返回一个迭代器:

// find_if
void sample1() {// 构造 set 数据set<string, less<>> set;set.insert("AAA");set.insert("BBB");set.insert("CCC");// 通过 find_if 在 set 中查找与参数给定相同的字符串auto iterator = find_if(set.begin(), set.end(),bind2nd(equal_to<string>(), "CCC"));if (iterator != set.end()) {cout << "查找到了" << endl;} else {cout << "没查找到" << endl;}
}

使用的条件函数为 equal_to(),该函数需要两个参数作比较,但是我们能给出的只有想要查找的 CCC 这个字符串,容器中的字符串我们是无法通过参数传进去的,因此这里使用了函数适配器 bind2nd() 来解决这个问题。该适配器函数的意思是将 equal_to() 的第二个参数固定为 CCC,然后让 set 中从 begin 到 end 的元素作为 equal_to() 的第一个参数来进行计算。

函数适配器

C++ 的函数适配器是一种用于修改或扩展函数行为的工具,它们可以用于将现有的函数或函数对象进行包装和转换,以满足特定的需求。

C++ 标准库提供了几种常见的函数适配器,包括:

  1. std::bindstd::bind 函数适配器用于绑定函数或成员函数的参数,并返回一个新的函数对象。通过绑定参数,可以延迟函数的调用或部分应用函数参数。例如:

    #include <iostream>
    #include <functional>void foo(int a, int b) {std::cout << "Sum: " << a + b << std::endl;
    }int main() {auto boundFunc = std::bind(foo, 3, std::placeholders::_1);boundFunc(4);  // 输出:Sum: 7return 0;
    }
    

    在上述示例中,我们使用 std::bind 将函数 foo 的第一个参数绑定为 3,第二个参数使用占位符 std::placeholders::_1 表示。这样,我们可以通过调用 boundFunc(4) 来执行原始函数 foo,并将绑定的参数和传递的参数一起使用。

  2. std::functionstd::function 是一个通用的函数封装器,可以用于存储和调用各种可调用对象,包括函数指针、函数对象、Lambda 表达式等。例如:

    #include <iostream>
    #include <functional>int add(int a, int b) {return a + b;
    }int main() {std::function<int(int, int)> func = add;int result = func(3, 4);  // 调用存储的函数对象std::cout << "Result: " << result << std::endl;  // 输出:Result: 7return 0;
    }
    

    在上述示例中,我们使用 std::function 创建了一个函数对象 func,指定了函数签名为 int(int, int)。然后,我们将函数 add 赋值给 func,并通过 func(3, 4) 调用存储的函数对象,得到结果 7。

  3. std::bind1ststd::bind2nd:这两个函数适配器用于将二元函数转换为一元函数,通过固定其中一个参数的值来生成新的函数对象。例如:

    #include <iostream>
    #include <functional>
    #include <algorithm>
    #include <vector>int main() {std::vector<int> numbers = {1, 2, 3, 4, 5};std::transform(numbers.begin(), numbers.end(), numbers.begin(), std::bind1st(std::plus<int>(), 10));for (int num : numbers) {std::cout << num << " ";  // 输出:11 12 13 14 15}return 0;
    }
    

    在上述示例中,我们使用 std::bind1ststd::plus<int>() 二元函数转换为一元函数,并将第一个参数固定为 10。然后,我们使用 std::transform 算法将 numbers 容器中的每个元素加上 10。

与 find 的区别

find 没有自定义仿函数,但 find_if 有。实际上 find 就是对 find_if 的封装:

  template<typename _InputIterator, typename _Tp>_GLIBCXX20_CONSTEXPRinline _InputIteratorfind(_InputIterator __first, _InputIterator __last,const _Tp& __val){// concept requirements__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)__glibcxx_function_requires(_EqualOpConcept<typename iterator_traits<_InputIterator>::value_type, _Tp>)__glibcxx_requires_valid_range(__first, __last);return std::__find_if(__first, __last,__gnu_cxx::__ops::__iter_equals_val(__val));}

可以看到,返回值是调用了三个参数的 __find_if(),前两个参数是首尾迭代器,最后一个是一个仿函数,会判断迭代器的值与参数的 __val 是否相同:

  template<typename _Value>struct _Iter_equals_val{_Value& _M_value;_GLIBCXX20_CONSTEXPRexplicit_Iter_equals_val(_Value& __value): _M_value(__value){ }template<typename _Iterator>_GLIBCXX20_CONSTEXPRbooloperator()(_Iterator __it){ return *__it == _M_value; }};

4.2 for_each

for_each() 也是算法包中的一个函数,但是在 3.1 节我们已经详细介绍过,也看过其源码,因此这里不再赘述。

4.3 transform

我们使用四个参数的 transform() 举例。前两个参数分别是输入集合的起始迭代器和尾部迭代器,第三个参数是接收结果的容器的起始迭代器,最后一个参数是执行变换的函数对象:

class AddTransform {
public:int operator()(int num) {return num + 10;}
};// transform
void sample2() {vector<int> vec;vec.insert(vec.end(), 100);vec.insert(vec.end(), 200);vec.insert(vec.end(), 300);AddTransform addTransform;// 1.使用输入数据的 vec 作为结果的接收方transform(vec.begin(), vec.end(), vec.begin(), addTransform);for (auto &it: vec) {cout << it << " ";}cout << endl;// 2.使用新的 vector 容器接收变换操作的结果vector<int> result;result.resize(vec.size());transform(vec.begin(), vec.end(), result.begin(), addTransform);for (auto &it: result) {cout << it << " ";}
}

输出结果:

110 210 310 
120 220 320 

我们来看一下 transform() 的源码:

  template<typename _InputIterator, typename _OutputIterator,typename _UnaryOperation>_GLIBCXX20_CONSTEXPR_OutputIteratortransform(_InputIterator __first, _InputIterator __last,_OutputIterator __result, _UnaryOperation __unary_op){// concept requirements__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)__glibcxx_function_requires(_OutputIteratorConcept<_OutputIterator,// "the type returned by a _UnaryOperation"__typeof__(__unary_op(*__first))>)__glibcxx_requires_valid_range(__first, __last);for (; __first != __last; ++__first, (void)++__result)*__result = __unary_op(*__first);return __result;}

变换函数 __unary_op 必须接收 _InputIterator 的数据作为参数,并且输出 *_OutputIterator 类型的数据作为返回值,这就是 AddTransform 内重载 () 时参数类型与返回值类型都为 int 的原因。

4.4 count 与 count_if

示例代码:

void sample3() {vector<int> vec;vec.push_back(1);vec.push_back(2);vec.push_back(7);vec.push_back(5);vec.push_back(2);cout << "2的个数有" << count(vec.begin(), vec.end(), 2) << "个" << endl;int result = count_if(vec.begin(), vec.end(), binder2nd(less<int>(), 2));cout << "比2小的个数有" << result << "个" << endl;result = count_if(vec.begin(), vec.end(), binder2nd(equal_to<int>(), 2));cout << "等于2的个数有" << result << "个" << endl;result = count_if(vec.begin(), vec.end(), binder2nd(greater<int>(), 2));cout << "比2大的个数有" << result << "个" << endl;
}

输出结果:

2的个数有2个
比2小的个数有1个
等于2的个数有2个
比2大的个数有2个

源码分析:

  template<typename _InputIterator, typename _Tp>count(_InputIterator __first, _InputIterator __last, const _Tp& __value){// 封装 count_if,传入的谓词为 __iter_equals_valreturn std::__count_if(__first, __last,__gnu_cxx::__ops::__iter_equals_val(__value));}template<typename _InputIterator, typename _Predicate>__count_if(_InputIterator __first, _InputIterator __last, _Predicate __pred){typename iterator_traits<_InputIterator>::difference_type __n = 0;// 遍历for (; __first != __last; ++__first)// 如果谓词对于 __first 成立,就对 __n 加 1,最后返回 __nif (__pred(__first))++__n;return __n;}

count 实际上就是对 count_if 传入了一个固定的谓词 __iter_equals_val:

  template<typename _Value>struct _Iter_equals_val{_Value& _M_value;// 构造函数,保存 __value 到成员 _M_value 上_Iter_equals_val(_Value& __value): _M_value(__value){ }template<typename _Iterator>bool// 判断迭代器的值是否与 _M_value 相同operator()(_Iterator __it){ return *__it == _M_value; }};

4.5 merge

合并两个容器:

void sample4() {vector<int> vec1;vec1.push_back(10);vec1.push_back(20);vec1.push_back(30);vec1.push_back(40);vector<int> vec2;vec2.push_back(50);vec2.push_back(60);vec2.push_back(70);vec2.push_back(80);// 接收结果的容器,容量为 vec1 和 vec2 容量之和vector<int> resultVec;resultVec.resize(vec1.size() + vec2.size());merge(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), resultVec.begin());// 遍历for (const auto &it: resultVec) {cout << it << " ";}
}

merge 的五个参数,依次为:vec1 的首尾迭代器、vec2 的首尾迭代器、接收合并结果的首迭代器。

4.6 sort

示例代码:

void sample5() {vector<int> vec;vec.push_back(10);vec.push_back(30);vec.push_back(20);// 使用默认的 less 进行从小到大的排序sort(vec.begin(), vec.end());for (auto &it: vec) {cout << it << " ";}cout << endl;// 使用显式的 greater 从大到小排序sort(vec.begin(), vec.end(), greater<>());for (auto &it: vec) {cout << it << " ";}
}

当然,你也可以自定义谓词传给 sort() 的第三个参数。

4.7 random_shuffle

随机打乱容器中的数据:

void sample6() {vector<int> vec;vec.push_back(65);vec.push_back(53);vec.push_back(84);vec.push_back(11);vec.push_back(22);vec.push_back(33);vec.push_back(44);// 先排好顺序sort(vec.begin(), vec.end());// 打乱顺序random_shuffle(vec.begin(), vec.end());// 输出结果for (auto &it: vec) {cout << it << " ";}
}

输出结果:

53 22 84 33 11 65 44 

需要注意的是 random_shuffle() 已经被标记为 deprecated,因为该方法产生随机数的种子无法作为参数传入:

  template<typename _RandomAccessIterator>inline voidrandom_shuffle(_RandomAccessIterator __first, _RandomAccessIterator __last){if (__first != __last)for (_RandomAccessIterator __i = __first + 1; __i != __last; ++__i){// 使用 std::rand() 产生随机数_RandomAccessIterator __j = __first+ std::rand() % ((__i - __first) + 1);if (__i != __j)std::iter_swap(__i, __j);}}

系统推荐使用 std::shuffle(),使用前需要导入 random:

#include <random>// 使用 std::shuffle 打乱顺序std::random_device randomDevice;std::mt19937 rng(randomDevice());std::shuffle(vec.begin(), vec.end(), rng);// 输出结果for (auto &it: vec) {cout << it << " ";}

4.8 copy 与 replace

示例代码:

void sample7() {vector<int> vec;vec.push_back(100);vec.push_back(200);vec.push_back(300);vec.push_back(400);// 拷贝到一个新建的容器中vector<int> result;result.resize(vec.size());copy(vec.begin(), vec.end(), result.begin());for (auto &it: result) {cout << it << " ";}cout << endl;// 替换 result 中的数据,将 300 替换成 999replace(result.begin(), result.end(), 300, 999);// 在 result.begin() 到 result.begin() + 2 这个范围内,将 200 替换成 666replace(result.begin(), result.begin() + 2, 200, 666);for (auto &it: result) {cout << it << " ";}
}

示例结果:

100 200 300 400 
100 666 999 400 

replace() 源码:

  template<typename _ForwardIterator, typename _Tp>voidreplace(_ForwardIterator __first, _ForwardIterator __last,const _Tp& __old_value, const _Tp& __new_value){...// 遍历,如果迭代器的值与 __old_value 相同,就给他替换成 __new_valuefor (; __first != __last; ++__first)if (*__first == __old_value)*__first = __new_value;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/4321.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【热门话题】Chrome 插件研发详解:从入门到实践

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 Chrome 插件研发详解&#xff1a;从入门到实践一、引言二、Chrome 插件基础概念…

【DevOps入门到精通】导读:探索软件开发与运维的协同之道

目录 DevOps简介 专栏结构概览 第一部分&#xff1a;入门阶段 DevOps概述 核心实践 工具与环境 第二部分&#xff1a;提高阶段 深入CI/CD 自动化测试进阶 监控与日志 第三部分&#xff1a;精通阶段 容器化与微服务架构 DevSecOps 高级监控与优化 第四部分&#…

应用实战 | 别踩白块小游戏,邀请大家来PK挑战~

“踩白块会输”是一个简单的微信小程序游戏&#xff0c;灵感来自当年火热的别踩白块游戏&#xff0c;程序内分成三个模块&#xff1a;手残模式、经典模式和极速模式&#xff0c;分别对应由易到难的三种玩法&#xff0c;可以查看游戏排名。动画效果采用JS实现&#xff0c;小程序…

多线程事务怎么回滚

1、背景介绍 1&#xff0c;最近有一个大数据量插入的操作入库的业务场景&#xff0c;需要先做一些其他修改操作&#xff0c;然后在执行插入操作&#xff0c;由于插入数据可能会很多&#xff0c;用到多线程去拆分数据并行处理来提高响应时间&#xff0c;如果有一个线程执行失败…

Git重修系列 ------ Git的使用和常用命令总结

一、Git的安装和配置 git安装&#xff1a; Git - Downloads git首次配置用户信息&#xff1a; $ git config --global user.name "kequan" $ git config --global user.email kequanchanqq.com $ git config --global credential store 配置 Git 以使用本地存储机…

mysql主库delete一个没主键的表导致从库延迟很久问题处理

一 问题描述 发现线上环境一个从库出现延迟&#xff0c;延迟了2天了&#xff0c;还没追上主库。 查看当前运行的sql及事务&#xff0c;发现这个sql语句是在delete一个没主键的表。 二 问题模拟 这里在测试环境复现下这个问题。 2.1 在主库造数据 use baidd; CREATE TABL…

【数据库】Redis

文章目录 [toc]Redis终端操作进入Redis终端Redis服务测试切换仓库 String命令存储字符串普通存储设置存储过期时间批量存储 查询字符串查询单条批量查询 Key命令查询key查询所有根据key首字母查询判断key是否存在查询指定的key对应的value的类型 删除键值对 Hash命令存储hash查…

软件测试_v模型_w模型

v模型&#xff1a; w模型&#xff1a; 一、V模型的8个阶段及其对应关系如下&#xff1a; 1. 需求分析&#xff1a;明确项目的需求&#xff0c;为后续设计提供依据。 2. 总体设计&#xff1a;根据需求分析&#xff0c;设计系统的总体架构。 3. 详细设计&#xff1a;在总体设计的…

在no branch上commit后,再切换到其他分支,找不到no branch分支的修改怎么办?

解决办法 通过git reflog我们可以查看历史提交记录&#xff0c;这里的第二条提交&#xff08;fbd3ea8&#xff09;就是我在no branch上的提交。 再通过git checkout -b backup fbd3ea8&#xff0c;恢复到上次提交的状态&#xff0c;并且为其创建个分支backup&#xff0c;此时…

(七)Servlet教程——Idea编辑器集成Tomcat

1. 点击桌面上Idea快捷方式打开Idea编辑器&#xff0c;假如没有创建项目的话打开Idea编辑器后的界面展示如下图所示 2. 点击界面左侧菜单中的自定义 3. 然后点击界面中的“所有设置...”,然后点击“构建、执行、部署”&#xff0c;选择其中的“应用程序服务器” 4. 点击“”按钮…

C语言-动态内存分配

即使行动导致错误&#xff0c;却也带来了学习与成长;不行动则是停滞与萎缩。&#x1f493;&#x1f493;&#x1f493; •&#x1f319;知识回顾 亲爱的友友们大家好&#xff01;&#x1f496;&#x1f496;&#x1f496;&#xff0c;我们紧接着要进入一个新的内容&#xff0c;…

k8s RBAC 角色访问控制详解与生产中的实际应用案例

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、前言 1、k8s简介 2、RBAC简介 二、RBAC关键…

【JavaParser笔记04】如何使用JavaParser依赖库生成Java类、接口、方法、属性、注释等内容

这篇文章,主要介绍如何使用JavaParser依赖库生成Java类、接口、方法、属性、注释等内容。 目录 一、JavaParser代码生成 1.1、引入依赖 1.2、生成Java类 1.3、生成属性和setter、

centos学习-网络配置命令-实用技巧

CentOS网络配置命令详解&#xff1a;轻松掌握网络配置技巧 在CentOS服务器的搭建和管理过程中&#xff0c;网络配置是至关重要的一环。良好的网络配置不仅可以确保服务器的稳定运行&#xff0c;还能够保障网络安全和数据传输效率。本文将详细介绍CentOS网络配置命令&#xff0…

【Pytorch报错】RuntimeError: cuDNN error: CUDNN_STATUS_INTERNAL_ERROR

报错信息&#xff1a; File "/root/miniconda3/lib/python3.8/site-packages/torch/nn/modules/conv.py", line 297, in _conv_forwardreturn F.conv1d(input, weight, bias, self.stride, RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling cubl…

【AMBA Bus ACE 总线 6 -- ACE cache maintenance 详细介绍】

文章目录 ACE cache maintenance什么叫 cache maintenance operations呢?ACE cache line statesACE cache maintenance 什么叫 cache maintenance operations呢? 比如ARM CPU 对自己的Icache 和 Dcache会有大量的transaction操作,也即maintenance操作,如果cache 是dirty 话…

Python的历史演变与作用

目录 1.概述 2.起源 3.发展阶段 4.Python 3的诞生 5.现状与未来 6.Python的作用 6.1.Web开发 6.2.数据科学与人工智能 ​​​​​​​6.3.自动化与脚本编程 ​​​​​​​6.4.教育与学习 ​​​​​​​6.5.其他领域 7.结语 1.概述 Python&#xff0c;一门富有表…

26.统一网关Gateway

网关的功能 1.身份认证&#xff0c;权限的校验。 2.服务的路由&#xff0c;负载均衡。用户请求被分配到哪一个微服务。一个微服务可以有多个实例&#xff0c;所以使用负载均衡。 3.请求限流。 springcloud网关实现有两种&#xff1a;gateway, zuul zuul是基于servlet实现的…

如何在vue中写 键盘控制方法

仅使用一个方法块来监听键盘事件并在按下 L 键时输出 "L"&#xff0c;您可以直接在组件的 mounted 钩子函数中定义匿名函数来处理键盘事件。这样做可以避免在组件中定义额外的方法&#xff0c;使代码更加简洁。以下是一个简化的示例代码&#xff1a; export default …

JavaEE——介绍 HTTPServlet 三部分使用与 cookie 和 session 的阐述

文章目录 一、HTTPServlet介绍其中的关键 三个方法 二、HTTPServletRequest(处理请求)1.分块介绍方法作用get 为前缀的方法字段中 含有 getParameter 字段 的方法(前后端交互)&#xff1a;字段中 含有 getHeader 字段 的方法&#xff1a; 2.解释前后端的交互过程3.使用 json 格…