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

发布于:2024-05-01 ⋅ 阅读:(38) ⋅ 点赞:(0)

本系列文章主要是介绍一些 NDK 开发所需的基础知识,目录如下:

NDK 基础(一)—— C 语言知识汇总
NDK 基础(二)—— C++ 语言基础与特性1
NDK 基础(三)—— C++ 语言基础与特性2
NDK 基础(四)—— C++ 高级特性1
NDK 基础(五)—— C++ 高级特性2

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 个元素,都被默认初始化为 0
    vector<int> vector2(10);
    // 声明 10 个元素,都被默认初始化为 1
    vector<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 120
    for (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 77
    for (int i: vector) {
        cout << i << " ";
    }
    cout << endl;

    // 3.删除,可以删除一个,也可以传入删除的范围
    vector.erase(vector.begin());
//    vector.erase(vector.begin(), vector.end());
    // 666 60 77
    for (int i: vector) {
        cout << i << " ";
    }
    cout << endl;

    // 4.通过迭代器遍历 vector
    for (::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_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x < __y; }
    };

  template<typename _Tp>
    struct greater : public binary_function<_Tp, _Tp, bool>
    {
      _GLIBCXX14_CONSTEXPR
      bool
      operator()(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.判断插入操作结果,具体见 insertMap
    string str = "覆盖后的第五个元素";
    insertMap(map, 5, str);
    str = "新存入的第六个元素";
    insertMap(map, 6, str);

    // 6.使用下标操作符可以修改已经存在的 key 对应的 value
    map[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、2
    multimap.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,不会修改容器内的 person
    person.setName("NewFirstPerson");
    // vector.front() 从容器内取出 Person,通过拷贝构造函数给到 newPerson
    // newPerson 在 main() 弹栈时会调用一次析构
    Person newPerson = vector.front();
    // newPerson 是容器内取出的 Person 对象,该对象的 name 是未被修改的,还是 FirstPerson
    cout << "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:
    // 函数要声明为 const
    bool 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.h
  template<typename _InputIterator, typename _Function>
    _GLIBCXX20_CONSTEXPR
    _Function
    for_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_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x > __y; }
    };

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

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

class CompareClass {
public:
    // 使得字符串从大到小排列,即 Z ~ A
    bool operator()(const string &string1, const string &string2) const {
        return string1 > string2;
    }
};

int main() {
    // set 存放 string 类型数据,比较器为 CompareClass
    set<string, CompareClass> set;

    set.insert(set.begin(), "AAA");
    set.insert(set.begin(), "ZZZ");
    set.insert(set.begin(), "HHH");
    set.insert(set.begin(), "QQQ");

    // 遍历 set
    for (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: 7
    
        return 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: 7
    
        return 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_CONSTEXPR
    inline _InputIterator
    find(_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_CONSTEXPR
      explicit
      _Iter_equals_val(_Value& __value)
	: _M_value(__value)
      { }

      template<typename _Iterator>
	_GLIBCXX20_CONSTEXPR
	bool
	operator()(_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
    _OutputIterator
    transform(_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_val
      return 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,最后返回 __n
	if (__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 void
    random_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 替换成 999
    replace(result.begin(), result.end(), 300, 999);
    // 在 result.begin() 到 result.begin() + 2 这个范围内,将 200 替换成 666
    replace(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>
    void
    replace(_ForwardIterator __first, _ForwardIterator __last,
	    const _Tp& __old_value, const _Tp& __new_value)
    {
      ...
	  // 遍历,如果迭代器的值与 __old_value 相同,就给他替换成 __new_value
      for (; __first != __last; ++__first)
	if (*__first == __old_value)
	  *__first = __new_value;
    }

网站公告

今日签到

点亮在社区的每一天
去签到