C++ 泛型编程与模板

  1. 一、 模板
    1. 1.1 函数模板
      1. 1.1.1 函数模板定义
      2. 1.1.2 函数模板注意事项
      3. 1.1.3 函数模板案例
      4. 1.1.4 普通函数与函数模板的区别
      5. 1.1.5 普通函数与函数模板的调用规则
      6. 1.1.6 模板的局限性 / 具体化的模板
    2. 1.2 类模板
      1. 1.2.1 类模板语法
      2. 1.2.2 类模板与函数模板区别
      3. 1.2.3 类模板中的成员函数创建时机
      4. 1.2.4 类模板对象做函数参数
      5. 1.2.5 类模板与继承
      6. 1.2.6 类模板成员函数类外实现
      7. 1.2.7 类模板成员函数类外实现
      8. 1.2.8 类模板与友元
      9. 1.2.9 类模板案例
    3. 1.3 写模板的坑
  2. 二、STL 初识
    1. 2.1 STL 的诞生
    2. 2.2 STL 的介绍
    3. 2.3 STL 六大组件
    4. 2.4 STL 中的容器、算法、迭代器
    5. 2.5 容器算法迭代器初识
      1. 2.5.1 vector 存放内置数据类型
      2. 2.5.2 vector 存放自定义数据类型
      3. 2.5.3 vector 容器嵌套容器
  3. 三、STL 常用容器
    1. 3.1 string 容器
      1. 3.1.1 string 基本概念
      2. 3.1.2 string 构造函数
      3. 3.1.3 string 赋值操作
      4. 3.1.4 string 字符串拼接
      5. 3.1.5 string 查找和替换
      6. 3.1.6 string 字符串比较
      7. 3.1.7 string 字符存取
      8. 3.1.8 string 插入和删除
      9. 3.1.9 string 子串
    2. 3.2 vector 容器
      1. 3.2.1 vector 基本概念
      2. 3.2.2 vector 构造函数
      3. 3.2.3 vector 赋值操作
      4. 3.2.4 vector 容量和大小
      5. 3.2.5 vector 插入和删除
      6. 3.2.6 vector 数据存取
      7. 3.2.7 vector 互换容器
      8. 3.2.8 vector 预留空间
    3. 3.3 Deque 容器
      1. 3.3.1 deque 容器基本概念
      2. 3.3.2 deque 构造函数
      3. 3.3.3 deque 赋值操作
      4. 3.3.4 deque 大小操作
      5. 3.3.5 deque 插入和删除
      6. 3.3.6 deque 数据存取
      7. 3.3.7 deque 排序
    4. 3.4 案例 - 评委打分
      1. 3.4.1 案例描述
      2. 3.4.2 代码实现
    5. 3.5 stack 容器
      1. 3.5.1 基本概念
      2. 3.5.2 stack 常用接口
    6. 3.6 queue 容器
      1. 3.6.1 基本概念
      2. 3.6.2 常用接口
    7. 3.7 list 容器
      1. 3.7.1 基本概念
      2. 3.7.2 list 构造函数
      3. 3.7.3 list 赋值和交换
      4. 3.7.4 list 大小操作
      5. 3.7.5 list 插入和删除
      6. 3.7.6 list 数据存取
      7. 3.7.7 list 反转和排序
      8. 3.7.8 排序案例
    8. 3.8 set/multiset 容器
      1. 3.8.1 基本概念
      2. 3.8.2 set 大小和交换
      3. 3.8.3 set 插入和删除
      4. 3.8.5 set 查找和统计
      5. 3.8.6 set 返回值
      6. 3.8.7 pair 对组创建
      7. 3.8.8 set 容器排序
    9. 3.9 map/multimap 容器
      1. 3.9.1 map 基本概念
      2. 3.9.2 map 构造和赋值
      3. 3.9.3 map 大小和交换
      4. 3.9.4 map 插入和删除
      5. 3.9.5 map 查找和统计
      6. 3.9.6 map 容器排序
    10. 3.10 案例 - 员工分组
      1. 3.10.1 案例描述
      2. 3.10.2 代码实现
  4. 四、STL- 函数对象
    1. 4.1 函数对象
      1. 4.1.1 函数对象概念
      2. 4.1.2 函数对象使用
    2. 4.2 谓词 Pred
    3. 4.3 内建函数对象
      1. 4.3.1 基本概念
      2. 4.3.2 算术仿函数
      3. 4.3.3 关系仿函数
      4. 4.3.4 逻辑仿函数
  5. 五、STL 常用算法
    1. 5.1 常用遍历算法
      1. 5.1.1 for_each
      2. 5.1.2 transform
    2. 5.2 常用查找算法
    3. 5.3 常用排序算法
    4. 5.4 常用拷贝和替换算法
    5. 5.5 常用算术生成算法
    6. 5.6 常用集合算法
  6. 六、异常处理
    1. 6.1 基本的异常处理
    2. 6.2 异常机制
    3. 6.3 案例
    4. 6.4 异常类型
      1. 6.4.1 标准异常
      2. 6.4.2 自定义异常

本阶段主要针对 C++泛型编程 STD技术做详细学习,探讨 C++ 更深层次的使用。

一、 模板

模板就是建立通用的模具,大大提高复用性。学习模板是为了会用别人提供的模板

  • C++ 另一种编程思想称为泛型编程,主要利用的技术就是模板
  • C++ 提高两种模板机制:函数模板和类模板

1.1 函数模板

1.1.1 函数模板定义

函数模板:建立一个通用函数,其函数返回类型和形参类型可以不具体制定,用一个虚拟的类型来代表

语法:

template<typename T>     // 注意这里没有分号;
void func(T &a){...}     // 函数声明或定义

解释:

  • template — 声明创建模板
  • typename — 表明其后面的符号是一种数据类型,可以用 class 代替
  • T — 通用的数据类型,名称可以替换,通常为大写字母
template<typename T>  // 声明一个模板,告诉编译器 T 为通用数据类型,不要报错

void mySwap(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {

    float a = 1.1;
    float b = 2.2;

    mySwap(a, b);             // 第一种调用:自动类型推导
    cout << a << endl;
    cout << b << endl;

    int c = 10;
    int d = 20;
    mySwap<int>(c, d);       // 第二种调用:显示指定类型
    cout << c << endl;
    cout << d << endl;

    system("pause");
}

总结:

  • 函数模板用关键字 template
  • 使用函数模板有两种方式:自动类型推导、显示指定类型
  • 模板的目的时为了提高函数复用性,将类型参数化。

1.1.2 函数模板注意事项

注意事项:

  • 自动类型推导,必须推导出一致的数据类型 T,才可使用

  • 模板必须要确定出 T 的数据类型,才可以使用

    template<typename T>    // 声明了 T, 紧接着的 test()函数却没有使用
    
    void test() {
        cout << "test()调用 " << endl;
    }
    
    void mySwap(T &a, T &b){    // 这个会报错 T, 上面的 T 声明仅对 T 声明下面的第一个函数有效
        cout << " 交换 " << endl;
    }
    
    int main() {
    
        test();  // 这里调用就会报错
    
        test<int>()  // 这随意指定一个 int,就没问题了
    
        system("pause");
    }

1.1.3 函数模板案例

案例描述:

  • 利用函数模板封装一个排序的函数,可以对不同的数据类型数组进行排序
  • 排序规则从大到小,排序算法为选择排序
  • 分别利用 char 数组和 int 数组进行测试
template<typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

template<typename T>  
void mySort(T arr[], int len) {
    for (int i = 0; i < len; ++i) {
        int max = i;
        for (int j = i + 1; j < len; ++j) {
            if (arr[max] < arr[j]) {
                max = j;
            }
        }
        if (max != i) {
            mySwap(arr[max], arr[i]);
        }
    }
}

void test() {
    char cha[] = "dhsjas";
    //int cha[] = { 3,4,2,5,1,6,8,7 };
    int len = sizeof(cha) / sizeof(cha[0]);
    mySort(cha, len);
    for (int i = 0; i < len; ++i) {
        cout << cha[i] << " ";
    }
}

int main() {

    test();

    system("pause");
}

1.1.4 普通函数与函数模板的区别

普通函数与函数模板区别(主要是是否可以发生隐式类型转换):

  • 普通函数调用时,可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换
int addtest1(int a, int b) {
    return a + b;
}

template<typename T>
T addtest2(T a, T b) {
    return a + b;
}

int main() {
    int a = 10;
    char c = 'c'; //(int)c = 99

    cout << addtest1(a, c) << endl;

    // 自动类型推导
    //cout << addtest2(a, c) << endl; 报错

    // 显示指定类型
    cout << addtest2<int>(a, c) << endl;

    system("pause");
}

1.1.5 普通函数与函数模板的调用规则

  • 如果函数模板和普通函数都可以实现,优先使用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 函数模板也可以发生重载
  • 如果函数模板可以产生更好的匹配,优先调用函数模板
int test1(int a, int b);

template<typename T>
void test1(T a, T b) {
    cout << " 调用模板函数 " << endl;
}

int main() {
    int a = 10;
    int b = 20;

    // 报错,调用普通函数,然而普通函数只有声明没有实现
    //test1(a, b);

    // 通过空模板参数列表, 强制调用函数模板
    test1<>(a, b) ;

    // 函数模板可以产生更好的匹配,优先调用函数模板
    char c1 = 'a';
    char c2 = 'b';
    test1(c1, c2);

    system("pause");
}

1.1.6 模板的局限性 / 具体化的模板

有时候通用模板并不能解决所有问题,这时候可以利用具体化的模板,去解决自定义类型的通用化。

class Person {
public:
    Person(string name, int age) {
        this->m_name = name;
        this->m_age = age;
    }

    string m_name;
    int m_age;
};

template<typename T>
bool compare(T a, T b) {
    if (a == b) {
        return true;
    }
    else {
        return false;
    }
};

// 具体化模板
template<> bool compare(Person &p1, Person &p2) {
    if (p1.m_name == p2.m_name && p1.m_age == p2.m_age) {
        return true;
    }
    else {
        return false;
    }
}

int main() {
    Person p1(" 张三 ", 25);
    Person p2(" 张三 ", 21);

    bool ret = compare(p1, p2);

    system("pause");
}

1.2 类模板

1.2.1 类模板语法

类模板作用:建立一个通用类,类中的成员、数据类型可以不具体制定,用一个虚函数类型来代表。

语法:

template<typename T>
class A{...}                   // 类的声明或定义

尖括号 <> 里可以定义多个通用数据类型,以逗号隔开。

template<class NameType,class AgeType>

class Person {
public:
    Person(NameType name, AgeType age) {
        this->m_name = name;
        this->m_age = age;
    }
    void show_Person() {
        cout << this->m_name << endl;
        cout << this->m_age << endl;
    }

    NameType m_name;
    AgeType m_age;
};

void test() {
    Person<string, int> p1("Tom", 23);  //<string,int> 叫模板的参数列表,必须指定

    p1.show_Person();
}

int main() {

    test();

    system("pause");
}

1.2.2 类模板与函数模板区别

类模板与函数模板区别:

  • 类模板没有自动类型推导的使用方式。
  • 类模板在模板参数列表中可以有默认参数。
template<class NameType, class AgeType=int>
class Person {
public:
    Person(NameType name, AgeType age) {
        this->m_name = name;
        this->m_age = age;
    }
    NameType m_name;
    AgeType m_age;
};

// 类模板没有自动类型推导的使用方式
void test1() {
    Person p1("Tom", 23);  // 报错,必须指定类型
}

// 类模板在模板参数列表中可以有默认参数
void test2() {
    Person<string> p2("Tomo", 22);  //template 中已经指定了第二个参数的默认类型
}

1.2.3 类模板中的成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机是有区别的:

  • 普通类中的成员函数一开始就可以创建。
  • 类模板中的成员函数在调用时才创建。
class Person1 {
public:
    void show1() {
        cout << "Person1 成员函数的调用 " << endl;
    }
};

class Person2 {
public:
    void show2() {
        cout << "Person2 成员函数的调用 " << endl;
    }
};

template<class T> 
class Person {
public:
    T obj;

    // 类模板中的成员函数
    void func1() {
        obj.show1();
    }
    void func2() {
        obj.show2();
    }
};

void test() {
    Person<Person1> p;       
    p.func1();             // 这里并不会报错,调用 func1()只会创建 func1()
    p.func2();             // 报错
}

int main() {

    test();

    system("pause");
}

1.2.4 类模板对象做函数参数

一共有三种传入方式:

  1. 指定传入的类型:直接显示对象的数据类型
  2. 参数模板化:将对象中的参数变为模板进行传递
  3. 整个类模板化:将这个对象类型模板化进行传递
template <class T1,class T2>
class Person {
public:
    Person(T1 name, T2 age) {
        this->m_name = name;
        this->m_age = age;
    }
    void show() {
        cout << m_name << endl;
        cout << m_age << endl;
    }
    T1 m_name;
    T2 m_age;
};

// 指定传入的类型      
void test1(Person<string, int>&p) {
    p.show();
}

// 参数模板化
template<class T1, class T2>
void test2(Person<T1, T2>& p) {
    p.show();
}

// 整个类模板化
template<class T>
void test3(T &p) {
    p.show();
    cout << typeid(T).name() << endl;  // 输出
}

int main() {

    Person<string, int>p("Tom", 200);
    test1(p);

    Person<string, int>p2("Tomo", 20);
    test2(p2);

    Person<string, int>p3("July", 50);
    test3(p3);

    system("pause");
}

使用最广泛的是第一种:指定传入的类型。

1.2.5 类模板与继承

当类模板碰到继承时,需要注意以下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定父类中 T 的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定父类中 T 的类型,子类也需要变为类模板
template <class T>
class Base {
    T m;
};

// 在继承父类时,父类模板必须要指定类型
class Son1 :public Base<int> {...};

// 也可以灵活指定,子类也需要变为类模板
template<class T1,class T2>
class Son2 :public Base<T1> {  
public:
    T2 obj;

    Son2() {
        cout << typeid(T1).name() << endl;
        cout << typeid(T2).name() << endl;
    }
};

void test() {
    Son1 s1;

    Son2<int, char> s2;
}

int main() {

    test();

    system("pause");
}

1.2.6 类模板成员函数类外实现

template <class T1, class T2>
class Person {
public:
    Person(T1 name, T2 age);
    void showPerson();

    T1 m_name;
    T2 m_age;
};

// 构造函数类外实现
template<class T1, class T2>
Person<T1,T2>::Person(T1 name, T2 age) {
    this->m_name = name;
    this->m_age = age;
}

// 成员函数类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
    cout << this->m_name << endl;
    cout << this->m_age << endl;
}

1.2.7 类模板成员函数类外实现

当我们写类模板时,成员函数的调用时机是在调用阶段,导致文件编写时链接不到

有两种解决方式:

  • 解决方式 1:直接包含.cpp 源文件
  • 解决方式 2:将声明和实现写到同一个文件中,并更改后缀名为.hpp。hpp 是约定的名称,并不是强制

1.2.8 类模板与友元

全局函数类内实现:直接在类内声明友元即可。

全局函数类外实现:需要提前让编译器知道全局函数的存在。

template <class T1, class T2>
class Person;

// 类外实现
template <class T1, class T2>
void showPerson2(Person<T1, T2> p) {
    cout << p.m_name << endl;
    cout << p.m_age << endl;
}

template <class T1, class T2>
class Person {

    // 全局函数在类内实现
    friend void showPerson(Person<T1, T2> p) {
        cout << p.m_name << endl;
        cout << p.m_age << endl;
    };
    // 全局函数类外实现
    friend void showPerson2<>(Person<T1, T2> p);  // 实现的代码要写在该类的前面

public:
    Person(T1 name, T2 age) {
        this->m_name = name;
        this->m_age = age;
    };

private:
    T1 m_name;
    T2 m_age;
};

void test() {
    Person<string, int> p1("Tom", 25);
    showPerson(p1);   // 全局函数在类内实现
    showPerson2(p1);  // 全局函数在类外实现
}

int main() {

    test();

    system("pause");
}

一般建议全局函数模板类内实现,类外实现太复杂了。

1.2.9 类模板案例

实现一个通用的数组类,要求如下:

  • 可以对内置数据类型以及自定义数据类型的数据进行存储
  • 将数组中的数据存储到堆区
  • 构造函数中可以传入数组的容量
  • 提供对应的拷贝构造函数以及 operator= 防止浅拷贝问题
  • 提供尾插法和尾删法对数组中的数据进行增加和删除
  • 可以通过下标的方式访问数组中的元素
  • 可以获取数组中当前元素个数和数组的容量

首先定义一个数组模板类,文件命名为 Array.hpp:

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

template<class T>
class Array {
public:
    // 有参构造函数,用于初始化数组容量
    Array(int capacity) {
        this->mSize = 0;
        this->mCapacity = capacity;
        this->mAddress = new T[this->mCapacity];
    }
    // 拷贝构造函数
    Array(const Array& arr) {
        this->mCapacity = arr.mCapacity;
        this->mSize = arr.mSize;
        this->mAddress = new T[this->mCapacity];
        for (int i = 0; i < this->mSize; ++i) {
            this->mAddress[i] = arr.mAddress[i];
        }
    }
    // 重载 = 操作符,深拷贝
    Array& operator=(const Array& arr) {
        if (this->mAddress != NULL) {
            delete[] this->mAddress;
            this->mSize = 0;
            this->mCapacity = 0;
        }
        this->mCapacity = arr.mCapacity;
        this->mSize = arr.mSize;
        this->mAddress = new T[this->mCapacity];
        for (int i = 0; i < this->mSize; ++i) {
            this->mAddress[i] = arr.mAddress[i];
        }
        return *this;
    }
    // 尾部插入数据
    void Push_Back(const T& val) {
        if (this->mCapacity == this->mSize) {
            return;
        }
        this->mAddress[this->mSize] = val;
        this->mSize++;
    }
    // 从尾部删除数据
    void Pop_Back() {
        if (this->mSize == 0) {
            return;
        }
        this->mSize--;  // 逻辑删除,让用户访问不到
    }
    T& operator[](int index) {
        return this->mAddress[index];// 暂未考虑越界的问题
    }
    int getCapacity() {
        return this->mCapacity;
    }
    int getSize() {
        return this->mSize;
    }

    ~Array() {
        if (this->mAddress != NULL) {
            delete[] this->mAddress;
            this->mAddress = NULL;
            this->mCapacity = 0;
            this->mSize = 0;
        }
    }

private:
    T* mAddress;    // 指针指向堆区开辟的真实数组
    int mCapacity;  // 数组容量
    int mSize;      // 数组当前的大小
};

测试案例,命名为 ArrayTest.cpp:

#include<iostream>
#include<string>
#include"Array.hpp"
using namespace std;

void printIntArray(Array<int>& arr) {
    for (int i = 0; i < arr.getSize(); ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;
}
// 测试 1
void test1() {

    Array<int> arr1(6);
    Array<int> arr3(100);

    arr1 = arr3;

    for (int i = 0; i < 6; ++i) {
        arr1.Push_Back(i);
    }

    printIntArray(arr1);
    cout << "arr1 的大小:" << arr1.getSize() << endl;
    cout << "arr1 的容量:" << arr1.getCapacity() << endl;

    Array<int> arr2(arr1);
    printIntArray(arr2);
    cout << "arr2 的大小:" << arr2.getSize() << endl;
    cout << "arr2 的容量:" << arr2.getCapacity() << endl;

    arr2.Pop_Back();
    printIntArray(arr2);
    cout << "arr2 的大小:" << arr2.getSize() << endl;
    cout << "arr2 的容量:" << arr2.getCapacity() << endl;

}

// 测试自定义数据类型
class Person {
public:
    Person() {};
    Person(string name, int age) {
        this->m_name = name;
        this->m_age = age;
    }
    string m_name;
    int m_age;
};

void printPersonArray(Array<Person>& arr) {
    for (int i = 0; i < arr.getSize(); ++i) {
        cout << arr[i].m_name + " " << arr[i].m_age << endl;
    }
    cout << endl;
}

// 测试自定义的数据类型
void test2() {

    cout << "\n--------------test2------------" << endl;
    Array<Person> arr(10);

    Person p1(" 孙悟空 ", 999);
    Person p2(" 韩信 ", 24);
    Person p3(" 妲己 ", 23);
    Person p4(" 赵云 ", 26);
    Person p5(" 鲁班 ", 12);

    arr.Push_Back(p1);
    arr.Push_Back(p2);
    arr.Push_Back(p3);
    arr.Push_Back(p4);
    arr.Push_Back(p5);

    printPersonArray(arr);
    cout << "arr 的大小:" << arr.getSize() << endl;
    cout << "arr 的容量:" << arr.getCapacity() << endl;
}

int main() {

    test1();
    test2();

    system("pause");
} 

1.3 写模板的坑

推荐把模板的实现和声明都写在头文件!否则编译会报错。

还有一种方式,可以分离声明与实现:

h 头文件:

template <typename T>
T add(const T &a, const T &b);

cpp 实现:

#include "add.h"
template <typename T>
T add(const T &a, const T &b)
{
    return a + b;
}
template int add(const int &a, const int &b);

最后一行要指定模板的实际类型。

二、STL 初识

2.1 STL 的诞生

  • 长久以来,人们一直希望建立一种可以重复利用的东西
  • C++ 面向对象编程和泛型编程思想,目的就是复用性的提升
  • 大多情况下,数据结构和算法都未有一套标准,导致被迫从事大量重复工作
  • 为了建立数据结构和算法的一套标准,诞生了 STL(Standard Template Library, 标准模板库)

2.2 STL 的介绍

  • STL 从广义上分为:容器 (container), 算法 (algorithm), 迭代器(iterator)
  • 容器 算法 之间通过 迭代器 进行无缝链接
  • STL 几乎所有的代码都采用了模板类或者模板函数

2.3 STL 六大组件

STL 大体分为六大组件,分别是容器,算法,迭代器,仿函数,适配器(配接器),空间配置器

  1. 容器:各种数据结构,如 vector, list, deque, set, map 等,用来存放数据
  2. 算法:各种常用的算法,如 sort, find, copy, for_each 等。
  3. 迭代器:扮演了容器与算法之间的胶合剂
  4. 仿函数,行为类似函数,可作为算法的某种策略
  5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西
  6. 空间配置器:负责用来空间的配置与管理

2.4 STL 中的容器、算法、迭代器

容器:置物之所也。

STL 容器就是将运用最广泛的一些数据结构实现出来,常用的数据结构:数组,链表,树,栈,队列,集合,映射表等。

这些容器分为序列式容器和关联式容器两种:

  • 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置。
  • 关联式容器:二叉树结构,各元素之间没有严格的物理意义上的顺序关系。只能一步步偏移取值,不能跳转

算法:问题值解法也。

有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)。

算法分为:质变算法和非质变算法。

  • 质变算法:是指运算过程中,会更改区间元素的内容,例如拷贝,替换,删除等。
  • 非质变算法:是指运算过程中,不会更改区间内的元素内容,例如查找,计数,遍历,寻找极值等等。

迭代器:容器和算法之间的粘合剂

提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。

每个容器都有自己专属的迭代器。

迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针。

迭代器种类:

种类 功能 支持运算
输入迭代器 对数据的只读访问 只读,支持 ++,==,!=
输出迭代器 对数据的只写访问 只写,支持 ++
前向迭代器 读写操作,并能向前推进迭代器 读写,支持 ++,==,!=
双向迭代器 读写操作,并能向前向后操作 读写,支持 ++,–
随机访问迭代器 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 读写,支持 ++,–,[n],-n,<,<=, >,>=

常用的容器迭代器种类为 双向迭代器 随机访问迭代器

2.5 容器算法迭代器初识

STL 中最常用的容器为 Vector,可以理解为数组。

2.5.1 vector 存放内置数据类型

容器:vector

算法:for_each

迭代器:vector<int>::interator

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

void Mprint(int val) {
    cout << val << endl;
}

void test() {
    vector<int> v;    
    v.push_back(10);  // 插入数据
    v.push_back(20);
    v.push_back(30);
    v.push_back(40);

    // 通过迭代器访问容器中的数据
    vector<int>::iterator itBegin = v.begin();  // 起始迭代器,指向容器的第一个元素
    vector<int>::iterator itEnd = v.end();  // 起始迭代器,指向容器的最后一个元素的下一个位置

    // 第一种遍历方式
    while (itBegin != itEnd) {
        cout << *itBegin << endl;  // 迭代器是一个指针
        itBegin++;
    }

    // 第二种遍历方式
    for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
        cout << *it << endl;
    }

    // 第三种遍历方式,利用 STL 提供的遍历算法
    for_each(v.begin(), v.end(), Mprint);

}

int main() {

    test();

    system("pause");
}

2.5.2 vector 存放自定义数据类型

#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;

class Person {
public:
    Person(string name, int age) {
        this->m_name = name;
        this->m_age = age;
    }
    string m_name;
    int m_age;
};

// 传入自定义的数据类型
void test1() {

    vector<Person> v;

    Person p1("Tom", 23);
    Person p2("Jack", 18);
    Person p3("Judy", 23);
    Person p4("Mark", 23);

    v.push_back(p1);
    v.push_back(p2);
    v.push_back(p3);
    v.push_back(p4);

    for (vector<Person>::iterator it = v.begin(); it != v.end(); ++it) {

        //cout << (*it).m_name << " " << (*it).m_age << endl;  // *it 是 Perosn 类型的数据

        cout << it->m_name << " " << it->m_age << endl; // 推荐这种
    }
}

// 传入自定义的数据类型的指针
void test2() {

    vector<Person*> v;   //v 用来储存指针(指针指向 person 类型)

    Person p1("Tom", 23);
    Person p2("Jack", 18);
    Person p3("Judy", 23);
    Person p4("Mark", 23);

    v.push_back(&p1);
    v.push_back(&p2);
    v.push_back(&p3);
    v.push_back(&p4);

    for (vector<Person*>::iterator it = v.begin(); it != v.end(); ++it) {

        cout << (*it)->m_name << " " << (*it)->m_age << endl;  // *it 是 Perosn 类型的数据    
    }
}

int main() {
    test1();
    test2();
    system("pause");
}

2.5.3 vector 容器嵌套容器

#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;

void test1() {

    vector<vector<int>> v;

    // 创建小容器
    vector<int> v1;
    vector<int> v2;
    vector<int> v3;

    // 向小容器添加数据
    for (int i = 0; i < 3; ++i) {
        v1.push_back(i + 1);
        v2.push_back(i + 2);
        v3.push_back(i + 3);
    }
    // 将小容器插入到大容器中
    v.push_back(v1);
    v.push_back(v2);
    v.push_back(v3);

    // 通过大容器,把所有数据遍历一遍
    for (vector<vector<int>>::iterator it = v.begin(); it != v.end(); ++it) {
        for (vector<int>::iterator jt = (*it).begin(); jt != (*it).end(); ++jt) {
            cout << *jt << " ";
        }
        cout << endl;
    }
}

int main() {
    test1();
    system("pause");
}

三、STL 常用容器

STL 中listvectormap 是三个最常被使用的容器。

STL 中,所有容器拷贝均为 深拷贝。没有浅拷贝的概念。

3.1 string 容器

3.1.1 string 基本概念

本质:string 是 C++ 风格的字符串,而 string 本质是一个类

string 和 char 区别:

  • char * 是一个指针
  • string 是一个类,类内部封装了 char*,管理这个字符串,是一个char* 类型的容量

特点:

  • string 类内部封装了很多成员方法,如查找 find,拷贝 copy,删除 delete,替换 replace,插入 insert
  • string 管理 char* 所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责

3.1.2 string 构造函数

构造函数原型:

  • string(); // 创建一个空的字符串,例如 string str;
  • string(const char* s); // 使用字符串 s 初始化
  • string(const string& str); // 使用一个 string 对象初始化另一个 string 对象
  • string(int n, char c); // 使用 n 个字符 c 初始化

3.1.3 string 赋值操作

string 赋值操作:即给 string 字符串进行赋值

赋值的函数原型:

  • string& operator=(const char* s); //char* 类型字符串,赋值给当前的字符串
  • string& operator=(const string& s); // 把字符串 s 赋值给当前的字符串
  • string& operator=(char s); // 把字符赋值给当前的字符串
  • string& assign(const char* s); // 把字符串 s 赋值给当前的字符串
  • string& assign(const char* s, int n); // 把字符串 s 的前 n 个字符赋值给当前的字符串
  • string& assign(const string& s); // 把字符串 s 赋值给当前的字符串
  • string& assign(int n, char s); // 用 n 个字符 s 赋值给当前的字符串

3.1.4 string 字符串拼接

在字符串末尾拼接字符串

函数原型:

  • string& operator+=(const char* str); // 重载 += 操作符
  • string& operator+=(const char c); // 重载 += 操作符
  • string& operator+=(const string& str); // 重载 += 操作符
  • string& append(const char *s); // 把字符串 s 连接到当前字符串结尾
  • string& append(const char *s, int n); // 把字符串 s 的前 n 个字符连接到当前字符串结尾
  • string& append(const string &s); // 同 operator+=(const string& str)
  • string& append(const string &s, int pos, int n);// 字符串 s 中从 pos 开始的 n 个字符连接到字符串结尾
  • string& append(int n, char c); // 在字符串结尾拼接 n 个 c
// 字符串拼接
void test01(){
    string str1 = " 我 ";
    // += 拼接
    str1 += " 爱玩游戏 ";
    cout << "str1 = " << str1 << endl;

    string str2 = "LOL DNF";
    str1 += str2;
    cout << "str1 = " << str1 << endl;


    string str3 = "I";
     // append 拼接
    str3.append(" love ");
    str3.append("game abcde", 4);

    //append 指定位置拼接
    str3.append(str2, 4, 3); // 从下标 4 位置开始 ,截取 3 个字符,拼接到字符串末尾
    cout << "str3 = " << str3 << endl;
}

3.1.5 string 查找和替换

功能描述:

  • 查找:查找指定字符串是否存在
  • 替换:在指定的位置替换字符串

函数原型:

  • int find(const string& str, int pos = 0) const; // 查找 str 第一次出现位置, 从 pos 开始查找
  • int find(const char* s, int pos = 0) const; // 查找 s 第一次出现位置, 从 pos 开始查找
  • int find(const char* s, int pos, int n) const; // 从 pos 位置查找 s 的前 n 个字符第一次位置
  • int find(const char c, int pos = 0) const; // 查找字符 c 第一次出现位置
  • int rfind(const string& str, int pos = npos) const; // 查找 str 最后一次位置, 从 pos 开始查找
  • int rfind(const char* s, int pos = npos) const; // 查找 s 最后一次出现位置, 从 pos 开始查找
  • int rfind(const char* s, int pos, int n) const; // 从 pos 查找 s 的前 n 个字符最后一次位置
  • int rfind(const char c, int pos = 0) const; // 查找字符 c 最后一次出现位置
  • string& replace(int pos, int n, const string& str); // 替换从 pos 开始 n 个字符为字符串 str
  • string& replace(int pos, int n,const char* s); // 替换从 pos 开始的 n 个字符为字符串 s
// 查找
void test01(){

    string str1 = "abcdefgde";
    //find 找第一次出现的位置
    int pos = str1.find("de");
    if (pos == -1){
        cout << " 未找到 " << endl;
    }
    else{
        cout << "pos = " << pos << endl;
    }
    //rfind 找最后一次出现的位置
    pos = str1.rfind("de");
    cout << "pos = " << pos << endl;
}

// 替换
void test02(){
    string str1 = "abcdefgde";
    str1.replace(1, 3, "1111");    // 从位置 1 开始的共 3 个字符替换为 1111        
    cout << "str1 = " << str1 << endl;
}

3.1.6 string 字符串比较

功能描述:

  • 字符串之间的比较

比较方式:

  • 字符串比较是按字符的 ASCII 码进行对比

= 返回 0

> 返回 1

< 返回 -1

函数原型:

  • int compare(const string &s) const; // 与字符串 s 比较
  • int compare(const char *s) const; // 与字符串 s 比较
void test01(){

    string s1 = "hello";
    string s2 = "aello";

    int ret = s1.compare(s2);

    if (ret == 0) {
        cout << "s1 等于 s2" << endl;
    }
    else if (ret > 0)
    {
        cout << "s1 大于 s2" << endl;
    }
    else
    {
        cout << "s1 小于 s2" << endl;
    }
}

3.1.7 string 字符存取

string 中单个字符存取方式有两种

  • char& operator[](int n); // 通过 [] 方式取字符
  • char& at(int n); // 通过 at 方法获取字符
void test01(){
    string str = "hello world";

    // 获取字符
    for (int i = 0; i < str.size(); i++)
    {
        cout << str[i] << " ";
    }
    cout << endl;

    for (int i = 0; i < str.size(); i++)
    {
        cout << str.at(i) << " ";
    }
    cout << endl;

    // 字符修改
    str[0] = 'x';
    str.at(1) = 'x';
    cout << str << endl;
}

[]:下标操作符 [] 在使用时不检查索引的有效性,如果下标超出字符的长度范围,会示导致未定义行为。

at:函数 at() 在使用时会检查下标是否有效。如果给定的下标超出字符的长度范围,系统会抛出 out_of_range 异常。

3.1.8 string 插入和删除

功能描述:

  • 对 string 字符串进行插入和删除字符操作

函数原型:

  • string& insert(int pos, const char* s); // 插入字符串
  • string& insert(int pos, const string& str); // 插入字符串
  • string& insert(int pos, int n, char c); // 在指定位置插入 n 个字符 c
  • string& erase(int pos, int n = npos); // 删除从 Pos 开始的 n 个字符
// 字符串插入和删除
void test01()
{
    string str = "hello";

    //insert 插入
    str.insert(1, "111");
    cout << str << endl;    //

    //erase 删除
    str.erase(1, 3);  // 从 1 号位置开始 3 个字符, 第二个数字可以不写,意为删除从第 1 个到最后一个
    cout << str << endl;
}

3.1.9 string 子串

功能描述:

  • 从字符串中获取想要的子串

函数原型:

  • string substr(int pos = 0, int n = npos) const; // 返回由 pos 开始的 n 个字符组成的字符串
void test01(){
    string str = "abcdefg";

    // 获取子串
    string subStr = str.substr(1, 3);       // 若第二个数字可以不写,意为取从第 1 个到最后一个
    cout << "subStr = " << subStr << endl;

    string email = "hello@sina.com";
    int pos = email.find("@");
    string username = email.substr(0, pos);
    cout << "username: " << username << endl;
}

3.2 vector 容器

3.2.1 vector 基本概念

vector 数据结构和数组非常相似,也称为单端数组。

vector 与普通数组区别:数组是静态空间,而 vector 可以动态扩展。数组 array 在定义时必须显式或隐式指定其 size;vector 则不用。

动态扩展:并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝至新空间,释放原空间。

vector 容器的迭代器是支持随机访问的迭代器。

3.2.2 vector 构造函数

函数原型:

  • vector<T> v; // 采用模板实现类实现,默认构造函数
  • vector(v.begin(), v.end()); // 将 v[begin(), end()]区间中的元素拷贝给本身。
  • vector(n, elem); // 构造函数将 n 个 elem 拷贝给本身。
  • vector(const vector &vec); // 拷贝构造函数。
#include <vector>

void printVector(vector<int>& v) {

    for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
}

void test01(){

    vector<int> v1; // 无参构造
    for (int i = 0; i < 10; i++)
    {
        v1.push_back(i);
    }
    printVector(v1);

    vector<int> v2(v1.begin(), v1.end());  // 将 v1 的所有数据传给 v2
    printVector(v2);

    vector<int> v3(10, 100);   // 传入 10 个 100 给 V3
    printVector(v3);

    vector<int> v4(v3);  // 拷贝构造
    printVector(v4);
}

vector 数组快速声明与初始化:

// 初始化数组长度
vector<int> v1(10);

// 初始化数组大小和值
vector<int> v2(10, 100);

// 直接赋值
vector<int> v3 = {1,2,3,4,5};

// 使用数组初始化
int arr[] = {1,2,3,4};
vector<int> v4(arr, arr+4);
vector<int> v5(std::begin(arr), std::end(arr));

3.2.3 vector 赋值操作

函数原型:

  • vector& operator=(const vector &vec);// 重载等号操作符
  • assign(beg, end); // 将 [beg, end) 区间中的数据拷贝赋值给本身。
  • assign(n, elem); // 将 n 个 elem 拷贝赋值给本身。
#include <vector>

void printVector(vector<int>& v) {

    for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
}

void test01()
{
    vector<int> v1; 
    for (int i = 0; i < 10; i++)
    {
        v1.push_back(i);
    }
    printVector(v1);

    vector<int>v2;
    v2 = v1;                 // = 号赋值
    printVector(v2);

    vector<int>v3;
    v3.assign(v1.begin(), v1.end());    //assign 赋值
    printVector(v3);

    vector<int>v4;
    v4.assign(10, 100);      //assign 赋数值
    printVector(v4);
}

3.2.4 vector 容量和大小

函数原型:

  • empty(); // 判断容器是否为空

  • capacity(); // 容器的 容量

  • size(); // 返回容器中 元素的个数

  • resize(int num); // 重新指定容器的长度为 num,若容器变长,则以默认值 0 填充新位置。

    ​ // 如果容器变短,则末尾超出容器长度的元素被删除。

  • resize(int num, elem); // 重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置。

    ​ // 如果容器变短,则末尾超出容器长度的元素被删除

void test01()
{
    vector<int> v1;
    for (int i = 0; i < 10; i++)
    {
        v1.push_back(i);
    }
    //printVector(v1);

    //empty 使用
    if (v1.empty())
    {
        cout << "v1 为空 " << endl;
    }
    else
    {
        cout << "v1 不为空 " << endl;
        cout << "v1 的容量 = " << v1.capacity() << endl;
        cout << "v1 的大小 = " << v1.size() << endl;
    }

    //resize 重新指定大小为 15,用 10 补齐
    v1.resize(15, 10);
    //printVector(v1);

    //resize 重新指定大小,超出 5 个元素部分删除
    v1.resize(5);
    //printVector(v1);
}

3.2.5 vector 插入和删除

函数原型:

  • push_back(ele); // 尾部插入元素 ele
  • pop_back(); // 删除最后一个元素
  • insert(const_iterator pos, ele); // 迭代器指向位置 pos 插入元素 ele
  • insert(const_iterator pos, int count,ele);// 迭代器指向位置 pos 插入 count 个元素 ele
  • erase(const_iterator pos); // 删除迭代器指向的元素
  • erase(const_iterator start, const_iterator end);// 删除迭代器从 start 到 end 之间的元素
  • clear(); // 删除容器中所有元素
#include <vector>

void test01()
{
    vector<int> v1;
    // 尾插
    v1.push_back(10);
    v1.push_back(20);
    v1.push_back(30);
    v1.push_back(40);
    v1.push_back(50);
    //printVector(v1);

    // 尾删
    v1.pop_back();
    //printVector(v1);

    // 插入
    v1.insert(v1.begin(), 100);  
    //printVector(v1);               //100 10 20 30 40 50

    v1.insert(v1.begin(), 2, 1000);
    //printVector(v1);

    // 删除
    v1.erase(v1.begin());
    printVector(v1);

    // 清空
    v1.erase(v1.begin(), v1.end());
    v1.clear();
    //printVector(v1);
}

当 vector 容器为空时,必须采用 push_back()方法来插入元素。但如果 vector 容器初始化有值,可以用随机访问的方法重新赋值:

void test() {
    vector<int> m(10,0);        // 初始化 m 容器为 10 个 0

    // 修改元素
    for (int i = 0; i < 10; ++i) {
        m[i] = i;
    }

    // 查看元素
    for (int num : m) {
        cout << num;
    }
}

3.2.6 vector 数据存取

函数原型:

  • at(int idx); // 返回索引 idx 所指的数据
  • operator[idx]; // 返回索引 idx 所指的数据
  • front(); // 返回容器中第一个数据元素
  • back(); // 返回容器中最后一个数据元素
#include <vector>

void test01()
{
    vector<int>v1;
    for (int i = 0; i < 10; i++)
    {
        v1.push_back(i);
    }

    for (int i = 0; i < v1.size(); i++)
    {
        cout << v1[i] << " ";
    }
    cout << endl;

    for (int i = 0; i < v1.size(); i++)
    {
        cout << v1.at(i) << " ";
    }
    cout << endl;

    cout << "v1 的第一个元素为: " << v1.front() << endl;
    cout << "v1 的最后一个元素为: " << v1.back() << endl;
}

3.2.7 vector 互换容器

函数原型:

  • swap(vec); // 将 vec 与本身的元素互换
void test01()
{
    vector<int>v1;
    for (int i = 0; i < 10; i++)
    {
        v1.push_back(i);
    }
    printVector(v1);

    vector<int>v2;
    for (int i = 10; i > 0; i--)
    {
        v2.push_back(i);
    }
    printVector(v2);

    // 互换容器
    cout << " 互换后 " << endl;
    v1.swap(v2);
    printVector(v1);
    printVector(v2);
}

当然 swap 函数更重要的作用是:收缩容量,节省空间

void test02()
{
    vector<int> v;
    for (int i = 0; i < 10000; i++) {
        v.push_back(i);
    }

    cout << "v 的容量为:" << v.capacity() << endl;   // 输出 12138
    cout << "v 的大小为:" << v.size() << endl;       // 输出 10000

    v.resize(3);

    cout << "v 的容量为:" << v.capacity() << endl;   // 输出 12138
    cout << "v 的大小为:" << v.size() << endl;       // 输出 3

    // 收缩容量(内存)
    vector<int>(v).swap(v); //vector<int>(v)匿名对象

    cout << "v 的容量为:" << v.capacity() << endl;   // 输出 3
    cout << "v 的大小为:" << v.size() << endl;       // 输出 3
}

3.2.8 vector 预留空间

减少 vector 在动态扩展容量时的扩展次数。

函数原型:

  • reserve(int len);// 容器预留 len 个元素长度,预留位置不初始化,元素不可访问。
void test01()
{
    vector<int> v;

    // 预留空间
    v.reserve(100000);  // 加与不加这行,后面计算的 num 有差别

    int num = 0;
    int* p = NULL;
    for (int i = 0; i < 100000; i++) {
        v.push_back(i);
        if (p != &v[0]) {
            p = &v[0];
            num++;
        }
    }

    cout << "num:" << num << endl;  // 输出:1
}

3.3 Deque 容器

3.3.1 deque 容器基本概念

deque(音 /dɛk/)容器是一种双端数组,可以对头部和尾部进行插入删除操作。

deque 与 vector 区别:

  • vector 对于头部的插入和删除效率低下,数据量越大,效率越低
  • deque 相对而言,对头部的插入删除速度会比 vector 快
  • vector 访问元素时的速度会比 deque 快,这和内部实现有关。

deque 内部工作原理:

deuqe 内部有个中控器,维护每段缓冲区中的内容,缓冲区存放真实数据。

中控器维护的是每个缓冲区的地址,使得使用 deque 时像一片连续的内存空间。

deque 容器的迭代器是支持随机访问的迭代器。

3.3.2 deque 构造函数

函数原型:

  • deque<T> deqT; // 默认构造形式
  • deque(beg, end); // 构造函数将 [beg, end) 区间中的元素拷贝给本身。
  • deque(n, elem); // 构造函数将 n 个 elem 拷贝给本身。
  • deque(const deque &deq); // 拷贝构造函数
void printDeque(const deque<int>& d)
{
    for (deque<int>::const_iterator it = d.begin(); it != d.end(); it++) {  // 注意这里的 const_iterator,只读迭代器
        cout << *it << " ";

    }
    cout << endl;
}

void test01() {

    deque<int> d1; 
    for (int i = 0; i < 10; i++)
    {
        d1.push_back(i);
    }
    printDeque(d1);

    deque<int> d2(d1.begin(), d1.end());
    printDeque(d2);

    deque<int>d3(10, 100);
    printDeque(d3);

    deque<int>d4 = d3;
    printDeque(d4);
}

总结:deque 容器和 vector 容器的构造方式几乎一致,灵活使用即可

3.3.3 deque 赋值操作

函数原型:

  • deque& operator=(const deque &deq); // 重载等号操作符
  • assign(beg, end); // 将 [beg, end) 区间中的数据拷贝赋值给本身。
  • assign(n, elem); // 将 n 个 elem 拷贝赋值给本身。
void test01()
{
    deque<int> d1;
    for (int i = 0; i < 10; i++)
    {
        d1.push_back(i);
    }
    printDeque(d1);

    deque<int>d2;
    d2 = d1;                //= 号赋值
    printDeque(d2);

    deque<int>d3;
    d3.assign(d1.begin(), d1.end());   //assign 赋值
    printDeque(d3);

    deque<int>d4;
    d4.assign(10, 100);
    printDeque(d4);
}

3.3.4 deque 大小操作

函数原型:

  • deque.empty(); // 判断容器是否为空

  • deque.size(); // 返回容器中元素的个数

  • deque.resize(num); // 重新指定容器的长度为 num, 若容器变长,则以默认值填充新位置。

    ​ // 如果容器变短,则末尾超出容器长度的元素被删除。

  • deque.resize(num, elem); // 重新指定容器的长度为 num, 若容器变长,则以 elem 值填充新位置。

    ​ // 如果容器变短,则末尾超出容器长度的元素被删除。

其用法与 vector 完全一样。

3.3.5 deque 插入和删除

函数原型:

两端插入操作:

  • push_back(elem); // 在容器尾部添加一个数据
  • push_front(elem); // 在容器头部插入一个数据
  • pop_back(); // 删除容器最后一个数据
  • pop_front(); // 删除容器第一个数据

指定位置操作:

  • insert(pos,elem); // 在 pos 位置插入一个 elem 元素的拷贝,返回新数据的位置。

  • insert(pos,n,elem); // 在 pos 位置插入 n 个 elem 数据,无返回值。

  • insert(pos,beg,end); // 在 pos 位置插入 [beg,end) 区间的数据,无返回值。

  • clear(); // 清空容器的所有数据

  • erase(beg,end); // 删除 [beg,end) 区间的数据,返回下一个数据的位置。

  • erase(pos); // 删除 pos 位置的数据,返回下一个数据的位置。

用法类似 vector。比 vector 多的是可以进行头部插入和删除。

3.3.6 deque 数据存取

函数原型:

  • at(int idx); // 返回索引 idx 所指的数据
  • operator[]; // 返回索引 idx 所指的数据
  • front(); // 返回容器中第一个数据元素
  • back(); // 返回容器中最后一个数据元素

其用法与 vector 完全一样。

3.3.7 deque 排序

算法:

  • sort(iterator beg, iterator end) // 对 beg 和 end 区间内元素进行排序
#include <deque>
#include <algorithm>   // 标准算法库

void test01(){

    deque<int> d;
    d.push_back(10);
    d.push_back(20);
    d.push_front(100);
    d.push_front(200);

    printDeque(d);

    // 排序
    sort(d.begin(), d.end());
    printDeque(d);

}

vector 也支持用 sort 进行排序。总结一句话:只要是支持随机访问的迭代器容器都可以用 sort 进行排序。

3.4 案例 - 评委打分

3.4.1 案例描述

有 5 名选手,选手 ABCDE,10 个评委分别对每一位选手打分,去除最高分,去除评委中最低分,取平均分。

实现步骤:

  • 创建五名选手,放到 vector 容器中
  • 遍历 vector 容器,取出来每一位选手,执行 for 循环,可以把 10 个评分打分存到 deque 容器中
  • sort 算法对 deque 容器中分数排序,去除最高分和最低分
  • deque 容器遍历一遍,累加总分
  • 获取平均分

3.4.2 代码实现

#pragma once
#include<iostream>
#include<string>
using namespace std;

#include<vector>
#include<algorithm>
#include <deque>

class Person {
public:
    Person(string name, int score) {
        this->m_Name = name;
        this->m_score = score;
    }
    string m_Name;
    int m_score;
};

void creatPerson(vector<Person>& v) {
    string seed = "ABCDE";

    for (int i = 0; i < seed.size(); ++i) {
        string name = " 选手 " ;
        name += +seed[i];
        int score = 0;
        Person p(name, score);
        v.push_back(p);
    }
}

void setScore(vector<Person>& v) {

    for (vector<Person>::iterator it = v.begin(); it != v.end(); ++it) {

        deque<int> d;  // 用来存放 10 个分数
        for (int i = 0; i < 10; ++i) {
            int score = rand() % 40 + 61;
            d.push_back(score);
        }
        sort(d.begin(), d.end());
        d.pop_back();
        d.pop_front();

        // 计算平均分
        int sum = 0;
        for (deque<int>::iterator dit = d.begin(); dit != d.end(); ++dit) {
            sum += *dit;
        }
        int avg = sum / d.size();

        it->m_score = avg;
    }
}

int main() {
    srand((unsigned int)time(NULL)); // 随机数种子

    vector<Person> v;

    creatPerson(v);

    setScore(v);

    for (vector<Person>::iterator it = v.begin();it!=v.end();++it) {
        cout << it->m_Name << " " << it->m_score << endl;
    }

    system("pause");

    return 0;
}

3.5 stack 容器

3.5.1 基本概念

stack(栈)容器是一种先进后出的数据结构容器,它只有一个出口。

栈内元素只有顶端的元素才可以被外界使用,因此栈不允许有遍历行为。

栈中进入数据称为—入栈push

栈中弹出数据称为—出栈pop

3.5.2 stack 常用接口

构造函数:

  • stack<T> stk // 采用模板类实现,stack 对象的默认构造形式
  • stack(const stack &stk) // 拷贝构造函数

赋值操作:

  • stack& operator=(const stack &stk) // 重载等号操作符

数据存取:

  • push(elem); // 向栈顶添加元素
  • pop(); // 从栈顶移除元素
  • top(); // 返回栈顶元素

大小操作:

  • empty(); // 判断栈是否为空
  • size(); // 返回栈的大小

3.6 queue 容器

3.6.1 基本概念

queue 是一种先进先出的数据结构,它有两个出口:

队列容器允许从一端新增元素,从另一端移除元素。犹如人,嘴巴进,屁股出

队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为

队列中进数据称为—入队push

队列中出数据称为—出队pop

3.6.2 常用接口

构造函数:

  • queue<T> que; //queue 采用模板类实现,queue 对象默认构造形式
  • queue(const queue &que); // 拷贝构造函数

赋值操作:

  • queue& operator=(const queue &que); // 重载等号操作符

数据存取:

  • push(elem); // 往队尾添加元素
  • pop(); // 从队头移除第一个元素
  • back(); // 返回最后一个元素
  • front(); // 返回第一个元素

大小操作:

  • empty(); // 判断容器是否为空
  • size(); // 返回容器的大小

3.7 list 容器

3.7.1 基本概念

list 容器的存储方式为链式存储。

链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。

链表的组成:链表由一系列结点组成。

结点的组成:一个是存储数据元素的数据域,另一个是储存下一个结点地址的指针域。

STL 中的链表是一个 双向循环 链表。

  • 双向 是指每一个结点有两个指针,一个指针 next 指向下一个结点的地址,一个指针 prev 指向上一个结点的地址;
  • 循环 是指第一个结点 prev 指向最后一个结点的地址,最后一个结点 next 指向第一个结点的地址。

由于链表的存储方式并不是连续的内存空间,因此链表 list 中的迭代器只支持前移和后移,属于 双向迭代器

list 的优点:

  • 采用动态存储分配,不会造成内存浪费和溢出
  • 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素

list 的缺点:

  • 链表灵活,但是空间(指针域)和时间(遍历)额外耗费较大。

List 有一个重要的性质,插入和删除操作都不会造成原有 list 迭代器的失效,这在 vector 是不成立的。

3.7.2 list 构造函数

函数原型:

  • list<T> lst; // list 采用采用模板类实现, 对象的默认构造形式:
  • list(beg,end); // 构造函数将 [beg, end) 区间中的元素拷贝给本身。
  • list(n,elem); // 构造函数将 n 个 elem 拷贝给本身。
  • list(const list &lst); // 拷贝构造函数。
#include<list>

void printList(const list<int>&L) {
    for (list<int>::const_iterator it = L.begin(); it != L.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
}

void test1() {
    // 创建 list 容器 --- 默认构造
    list<int> L1;
    L1.push_back(10);
    L1.push_back(20);
    L1.push_back(30);
    L1.push_back(40);

    printList(L1);

    // 创建 list 容器 --- 区间方式构造
    list<int> L2(L1.begin(),L1.end());

    // 创建 list 容器 --- 拷贝构造
    list<int> L3(L2);

    // 创建 list 容器 ---n 个 elem
    list<int> L4(10,100);
    printList(L4);

}

int main() {

    test1();

    system("pause");

    return 0;
}

3.7.3 list 赋值和交换

函数原型:

  • assign(beg, end); // 将 [beg, end) 区间中的数据拷贝赋值给本身。
  • assign(n, elem); // 将 n 个 elem 拷贝赋值给本身。
  • list& operator=(const list &lst); // 重载等号操作符
  • swap(lst); // 将 lst 与本身的元素互换。

3.7.4 list 大小操作

函数原型:

  • size(); // 返回容器中元素的个数

  • empty(); // 判断容器是否为空

  • resize(num); // 重新指定容器的长度为 num,若容器变长,则以默认值填充新位置。

    ​ // 如果容器变短,则末尾超出容器长度的元素被删除。

  • resize(num, elem); // 重新指定容器的长度为 num,若容器变长,则以 elem 值填充新位置。变短则删除。

3.7.5 list 插入和删除

函数原型:

  • push_back(elem); // 在容器尾部加入一个元素
  • pop_back(); // 删除容器中最后一个元素
  • push_front(elem); // 在容器开头插入一个元素
  • pop_front(); // 从容器开头移除第一个元素
  • insert(pos,elem); // 在 pos 位置插 elem 元素的拷贝,返回新数据的位置。
  • insert(pos,n,elem); // 在 pos 位置插入 n 个 elem 数据,无返回值。
  • insert(pos,beg,end); // 在 pos 位置插入 [beg,end) 区间的数据,无返回值。
  • clear(); // 移除容器的所有数据
  • erase(beg,end); // 删除 [beg,end) 区间的数据,返回下一个数据的位置。
  • erase(pos); // 删除 pos 位置的数据,返回下一个数据的位置。
  • remove(elem); // 删除容器中所有与 elem 值匹配的元素。

3.7.6 list 数据存取

函数原型:

  • front(); // 返回第一个元素。
  • back(); // 返回最后一个元素。

3.7.7 list 反转和排序

函数原型:

  • reverse(); // 反转链表
  • sort(); // 链表排序,升序
#include<list>

void printList(const list<int>&L) {
    for (list<int>::const_iterator it = L.begin(); it != L.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
}

bool myCompare(int v1, int v2) {
    return v1 > v2;
}

void test1() {

    list<int> L1;
    L1.push_back(20);
    L1.push_back(10);
    L1.push_back(40);
    L1.push_back(30);

    L1.sort();  // 默认升序排列
    printList(L1);

    L1.sort(myCompare);   // 降序排列
    printList(L1);

}


int main() {

    test1();

    system("pause");

    return 0;
}

3.7.8 排序案例

#include<list>

class Person {

public:
    Person(string name, int age, int height) {
        this->m_Name = name;
        this->m_Age = age;
        this->m_Height = height;
    }
    string m_Name;
    int m_Age;
    int m_Height;
};

void printList(const list<Person>&L) {
    for (list<Person>::const_iterator it = L.begin(); it != L.end(); ++it) {
        cout << it->m_Name << " ";
        cout << it->m_Age << " ";
        cout << it->m_Height << endl;
    }
}

bool sortRule(Person &p1, Person &p2) {

    if (p1.m_Age == p2.m_Age) {
        return p1.m_Height > p2.m_Height;
    }

    return p1.m_Age < p2.m_Age;
}

void test1() {

    list<Person> L1;

    Person p1(" 张飞 ", 23, 178);
    Person p2(" 周瑜 ", 28, 175);
    Person p3(" 孙权 ", 12, 132);
    Person p4(" 曹操 ", 34, 175);
    Person p5(" 关羽 ", 25, 182);
    Person p6(" 刘备 ", 28, 180);

    L1.push_back(p1);
    L1.push_back(p2);
    L1.push_back(p3);
    L1.push_back(p4);
    L1.push_back(p5);
    L1.push_back(p6);

    printList(L1);

    cout << endl;

    L1.sort(sortRule);

    printList(L1);
}

对于自定义数据类型,必须要指定排序规则,否则编译器不知道如何进行排序。

高级排序只是在排序规则上再进行一次逻辑规则制定,并不复杂。

3.8 set/multiset 容器

3.8.1 基本概念

集合容器,所有元素在插入时会自动被排序。set/multiset 属于 关联式容器,底层结构用二叉树实现。

set 和 multiset 区别:

  • set 不允许容器中有重复的元素,multiset 允许容器中有重复的元素
  • set 插入数据的同时会返回插入结果,表示插入是否成功;multiset 无返回值

set 和 multiset 的头文件都是#include<set>

构造函数

  • set<T> st; // 默认构造函数:
  • set(const set &st); // 拷贝构造函数

赋值

  • set& operator=(const set &st); // 重载等号操作符

3.8.2 set 大小和交换

函数原型:

  • size(); // 返回容器中元素的数目
  • empty(); // 判断容器是否为空
  • swap(st); // 交换两个集合容器

3.8.3 set 插入和删除

函数原型:

  • insert(elem); // 在容器中插入元素。
  • clear(); // 清除所有元素
  • erase(pos); // 删除 pos 迭代器所指的元素,返回下一个元素的迭代器。
  • erase(beg, end); // 删除区间 [beg,end) 的所有元素 ,返回下一个元素的迭代器。
  • erase(elem); // 删除容器中值为 elem 的元素。

3.8.5 set 查找和统计

函数原型:

  • find(key); // 查找 key 是否存在, 若存在,返回该键的元素的迭代器;若不存在,返回 set.end();
  • count(key); // 统计 key 的元素个数(整型 int)
#include<set>

void test1() {

    set<int> s1;

    s1.insert(20);
    s1.insert(50);
    s1.insert(10);
    s1.insert(40);
    s1.insert(30);

    set<int>::iterator pos = s1.find(30);

    if (pos != s1.end()) {
        cout << " 找到 " << endl;
    }
    else {
        cout << " 没找到 " << endl;
    }

    int num = s1.count(10);
}

3.8.6 set 返回值

set 插入数据的同时会返回插入结果,表示插入是否成功。返回值是一个 pair 类型的数据

#include<set>

void test1() {

    set<int> s1;

    pair<set<int>::iterator, bool> ret = s1.insert(20);

    if (ret.second) {
        cout << " 插入成功!" << endl;
    }
    else {
        cout << " 插入失败!" << endl;
    }

    pair<set<int>::iterator, bool> ret2 = s1.insert(20);

    if (ret2.second) {
        cout << " 插入成功!" << endl;
    }
    else {
        cout << " 插入失败!" << endl;
    }
}

3.8.7 pair 对组创建

成对出现的数据,利用对组可以返回两个数据。

两种创建方式:

  • pair<type, type> p (value1, value2);
  • pair<type, type> p = make_pair(value1, value2);
pair<string, int> p("Tom", 20);
cout << " 姓名: " <<  p.first << " 年龄: " << p.second << endl;

3.8.8 set 容器排序

学习目标:

  • set 容器默认排序规则为从小到大,掌握如何改变排序规则

主要技术点:

  • 利用仿函数,可以改变排序规则

示例一:内置的数据类型排序

#include<set>

class SortRule {
public:
    bool operator()(int v1, int v2) const {       // 这里加个 const,不然 vs2019 会报错
        return v1 > v2;
    }
};

void printSet(set<int, SortRule>&s) {
    for (set<int, SortRule>::iterator it = s.begin(); it != s.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
}

void test1() {

    set<int, SortRule> s1;
    s1.insert(20);
    s1.insert(30);
    s1.insert(10);
    s1.insert(50);
    s1.insert(40);

    printSet(s1);

}

示例二:自定义数据类型排序

#include<set>
class Person {
public:
    Person(string name, int age, int height) {
        this->m_Name = name;
        this->m_Age = age;
        this->m_Height = height;
    }
    string m_Name;
    int m_Age;
    int m_Height;
};

class SortRule {
public:
    bool operator()(const Person& p1, const Person& p2) const{
        return p1.m_Age > p2.m_Age;
    }
};

void printSet(set<Person, SortRule>&s) {
    for (set<Person, SortRule>::iterator it = s.begin(); it != s.end(); ++it) {
        cout << it->m_Name << " ";
        cout << it->m_Age << " ";
        cout << it->m_Height << endl;
    }
}

void test1() {

    set<Person, SortRule> s1;

    Person p1(" 张飞 ", 25, 180);
    Person p2(" 周瑜 ", 26, 175);
    Person p3(" 刘备 ", 28, 179);
    Person p4(" 孙权 ", 16, 158);
    Person p5(" 关羽 ", 27, 182);

    s1.insert(p1);
    s1.insert(p2);
    s1.insert(p3);
    s1.insert(p4);
    s1.insert(p5);

    printSet(s1);

}

3.9 map/multimap 容器

3.9.1 map 基本概念

map/multimap 属于关联式容器,底层结构是用二叉树实现。

  • map 中所有元素都是 pair
  • pair 中第一个元素为 key(键值),起索引作用,第二个元素为 value(实值)
  • 所有元素都会根据元素的键值自动排序

优点:

  • 可以根据 key 值快速找到 value 值

map/multimap 区别:

  • map 不允许容器中有重复 key 值元素。
  • multimap 允许容器中有重复 key 值元素。

3.9.2 map 构造和赋值

函数原型:

构造:

  • map<T1, T2> mp; //map 默认构造函数:
  • map(const map &mp); // 拷贝构造函数

赋值:

  • map& operator=(const map &mp); // 重载等号操作符
#include<map>

void printMap(map<int, int>& m) {
    for (map<int, int>::iterator it = m.begin(); it != m.end(); ++it) {
        cout << (*it).first << " " << (*it).second << endl;
    }
}

void test1() {

    // 默认构造
    map<int, int>m;
    m.insert(pair<int,int>(1, 10));
    m.insert(pair<int, int>(4, 40));
    m.insert(pair<int, int>(3, 30));
    m.insert(pair<int, int>(2, 200));

    printMap(m);

    // 拷贝构造
    map<int, int>m2(m);
    printMap(m2);

    // 赋值
    map<int, int>m3 = m;
    printMap(m3);

    // 按照 key 取值
    cout << m[2] << endl;  

    // 多层 map
    map<string, map<int, int>> mm;
    mm.insert(make_pair("No1", m));   //mm["No1"]=m
}

map 取值有三种:[],find,at

  • find 有就是有,没有就是没有,需要判断 find 的返回结果,才知道有没有。

  • [] 不管有没有,都是有。因为没有就是 0,字符串就是空。反正就是给它是初始值。如果原先不存在该 key,则插入,如果存在,则覆盖插入。

  • at 用于取值,但它是进行越界检测,这会损失效率。

    相比于 find,能少敲键盘

    相比于[],更安全

    性能会有损失

    所以常用于待查找的 key 都应该存在的情况,如果查找的 key 不存在,则说明程序有问题了。

以上三种虽然都能达到取值的目的,但用法上的些微差别可能导致使用上的 bug,所以一般性的建议如下:

如果纯粹是查找取值,不要用 [] 操作符,因为它会自动创建不存在的元素
正因为上面的原因,[]仅用于非 const 的情形下。在 const map 下使用 [] 会导致编译期错误
如果不确定待查找的值是否存在,且不存在也是合理的,则使用 find,判断返回值是否是 end()
如果待查找的值肯定存在,如果不存在就是逻辑上的错误,则使用 at,处理 out_of_range 异常

所以,对 map 根据 key 取值,不要用[]!!!

3.9.3 map 大小和交换

函数原型:

  • size(); // 返回容器中元素的数目
  • empty(); // 判断容器是否为空
  • swap(st); // 交换两个集合容器

3.9.4 map 插入和删除

函数原型:

  • insert(elem); // 在容器中插入元素。
  • clear(); // 清除所有元素
  • erase(pos); // 删除 pos 迭代器所指的元素,返回下一个元素的迭代器。
  • erase(beg, end); // 删除区间 [beg,end) 的所有元素 ,返回下一个元素的迭代器。
  • erase(key); // 删除容器中值为 key 的元素。

3.9.5 map 查找和统计

函数原型:

  • find(key); // 查找 key 是否存在,若存在,返回该键的元素的迭代器;若不存在,返回 map.end();
  • count(key); // 统计 key 的元素个数,返回 int 整型数据
#include<map>

void test1() {

    map<int, int>m;
    m.insert(make_pair(1, 10));
    m.insert(make_pair(2, 20));
    m.insert(make_pair(3, 30));
    m.insert(make_pair(4, 40));

    map<int, int>::iterator pos = m.find(2);
    cout << pos->second << endl;
}

3.9.6 map 容器排序

map 排序函数与 set 类似。

class SortRule {
public:
    bool operator()(int v1, int v2) const {       // 这里加个 const,不然 vs2019 会报错
        return v1 > v2;
    }
};

void test1() {

    map<int, int, SortRule> m;    //SortRule 指明按照从大到小排列

    m.insert(make_pair(1, 10));
    m.insert(make_pair(2, 20));
    m.insert(make_pair(3, 30));
    m.insert(make_pair(4, 40));
    m.insert(make_pair(5, 50));
}

3.10 案例 - 员工分组

3.10.1 案例描述

  • 公司今天招聘了 10 个员工(ABCDEFGHIJ),10 名员工进入公司之后,需要指派员工在那个部门工作
  • 员工信息有: 姓名 工资组成;部门分为:策划、美术、研发
  • 随机给 10 名员工分配部门和工资
  • 通过 multimap 进行信息的插入 key(部门编号) value(员工)
  • 分部门显示员工信息

3.10.2 代码实现

#include<vector>
#include<map>

class Worker {
public:
    string m_Name;
    int m_Salary;
};

void createWorker(vector<Worker>& v) {
    string nameseed = "ABCDEFGH";
    for (int i = 0; i < nameseed.size(); ++i) {
        Worker worker;
        worker.m_Name = " 员工 ";
        worker.m_Name += nameseed[i];
        worker.m_Salary = rand() % 10000 + 10000;

        v.push_back(worker);
    }
}
void setGroup(vector<Worker>& v, multimap<int, Worker>& m) {

    for (vector<Worker>::iterator it = v.begin(); it != v.end(); ++it) {
        int dpart = rand() % 3 + 1;
        m.insert(pair<int, Worker>(dpart, *it));

    }
}

void showWorkerByGroup(multimap<int, Worker>& m) {

    for (int i = 1; i < 4; ++i) {
        switch (i)
        {
        case 1:
            cout << " 策划部门员工:" << endl;
            break;
        case 2:
            cout << " 美术部门员工:" << endl;
            break;
        case 3:
            cout << " 研发部门员工:" << endl;
            break;
        default:
            break;
        }
        multimap<int, Worker>::iterator pos = m.find(i);
        int num = m.count(i);
        int index = 0;
        for (; pos != m.end() && index < num; ++pos, index++) {
            cout << pos->second.m_Name << " " << pos->second.m_Salary << endl;
        }
        cout << endl;
    }
}

void test1() {

    srand((unsigned int)time(NULL));

    vector<Worker> vWorker;
    createWorker(vWorker);

    multimap<int, Worker>mWorker;
    setGroup(vWorker, mWorker);

    showWorkerByGroup(mWorker);

    /*for (vector<Worker>::iterator it = vWorker.begin(); it != vWorker.end(); ++it) {
        cout << it->m_Name << " " << it->m_Salary << endl;
    }*/    
}

四、STL- 函数对象

4.1 函数对象

4.1.1 函数对象概念

概念:

  • 重载函数调用操作符的类,其对象常称为函数对象。
  • 函数对象使用重载的 () 时,行为类似函数调用,也叫仿函数。

本质:函数对象(仿函数)是一个类,不是一个函数。

4.1.2 函数对象使用

特点:

  • 函数对象在使用时,可以像普通函数那样调用,可以有参数,可以有返回值

    // 函数对象(仿函数)
    class MyAdd {
    public:
        int operator()(int v1, int v2) {
            return v1 + v2;
        }
    };
    
    void test1() {
    
        MyAdd add;
        int ret = add(10, 20);  // 类对象的调用类似函数
        cout << ret << endl;
    }
  • 函数对象超出普通函数的概念,函数对象可以有自己的状态

    class MyPrint {
    public:
        MyPrint() {
            this->count = 0;
        }
        void operator()(string test) {
            cout << test << endl;
            this->count++;
        }
        int count;
    };
    
    void test1() {
    
        MyPrint print;
        print("hello world");
        print("hello world");
        print("hello world");
    
        cout << "print 调用次数:" << print.count << endl;
    
    }

    这里仿函数可以记录自己调用的次数

  • 函数对象可以作为参数传递

    class MyPrint {
    public:
        MyPrint() {
            this->count = 0;
        }
        void operator()(string test) {
            cout << test << endl;
            this->count++;
        }
        int count;
    };
    
    void doPrint(MyPrint& mp, string test) {
        mp(test);
    }
    void test1() {
    
        MyPrint print;
    
        doPrint(print, "Hello C++");   //here
    
    }

4.2 谓词 Pred

返回 bool 类型的仿函数称为谓词:

  • 如果 operator()接受一个参数,那么就叫一元谓词
  • 如果 operator()接受两个参数,那么就叫二元谓词
#include <vector>
#include <algorithm>

//1. 一元谓词
struct GreaterFive{
    bool operator()(int val) {
        return val > 5;
    }
};

void test01() {

    vector<int> v;
    for (int i = 0; i < 10; i++)
    {
        v.push_back(i);
    }

    // 利用 find_if 标准算法
    vector<int>::iterator it = find_if(v.begin(), v.end(), GreaterFive());
    if (it == v.end()) {
        cout << " 没找到!" << endl;
    }
    else {
        cout << " 找到:" << *it << endl;
    }

}

// 二元谓词
class MyCompare
{
public:
    bool operator()(int num1, int num2)
    {
        return num1 > num2;
    }
};
void test(){
    vector<int> v;
    v.push_back(10);
    v.push_back(20);
    sort(v.begin(), v.end(), MyCompare);
}

4.3 内建函数对象

4.3.1 基本概念

STL 内建了一些函数对象,其中包括:

  • 算术仿函数
  • 关系仿函数
  • 逻辑仿函数

用法:

  • 这些仿函数所产生的对象,用法和一般函数完全相同
  • 使用内建函数对象,需要引入头文件#include<functional>

4.3.2 算术仿函数

功能描述:

  • 实现四则运算
  • 其中 negate 是一元运算,其他都是二元运算

仿函数原型:

  • template<class T> T plus<T> // 加法仿函数
  • template<class T> T minus<T> // 减法仿函数
  • template<class T> T multiplies<T> // 乘法仿函数
  • template<class T> T divides<T> // 除法仿函数
  • template<class T> T modulus<T> // 取模仿函数
  • template<class T> T negate<T> // 取反仿函数
#include<functional>
void test1() {

    negate<int> ne;               // 取反
    cout << ne(50) << endl;

    plus<int> p;                  // 加法
    cout << p(10, 20) << endl;
}

4.3.3 关系仿函数

功能描述:

  • 实现关系对比

仿函数原型:

  • template<class T> bool equal_to<T> // 等于
  • template<class T> bool not_equal_to<T> // 不等于
  • template<class T> bool greater<T> // 大于
  • template<class T> bool greater_equal<T> // 大于等于
  • template<class T> bool less<T> // 小于
  • template<class T> bool less_equal<T> // 小于等于
#include<algorithm>
#include<vector>
#include<functional>


void test1() {

    vector<int> v;
    v.push_back(10);
    v.push_back(30);
    v.push_back(20);

    sort(v.begin(), v.end(), greater<int>());    // 内建的 greater<int>()函数对象,改变默认的排序方式

    for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
        cout << *it << endl;
    }
}

4.3.4 逻辑仿函数

功能描述:

  • 实现逻辑运算

函数原型:

  • template<class T> bool logical_and<T> // 逻辑与
  • template<class T> bool logical_or<T> // 逻辑或
  • template<class T> bool logical_not<T> // 逻辑非
#include<algorithm>
#include<vector>
#include<functional>

void test1() {

    vector<bool> v;
    v.push_back(true);
    v.push_back(false);
    v.push_back(true);

    // 利用逻辑非,将容器 v 搬运到容器 v2 中,并执行取反操作
    vector<bool> v2;
    v2.resize(v.size());
    transform(v.begin(), v.end(), v2.begin(), logical_not<bool>());

    for (vector<bool>::iterator it = v2.begin(); it != v2.end(); ++it) {
        cout << *it << endl;
    }
}

此部分内容仅做了解即可,实际使用较少

五、STL 常用算法

概述:

  • 算法主要是由头文件 <algorithm> <functional> <numeric> 组成。

  • <algorithm>是所有 STL 头文件中最大的一个,范围涉及到比较、 交换、查找、遍历操作、复制、修改等等

  • <numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数

  • <functional>定义了一些模板类, 用以声明函数对象。

5.1 常用遍历算法

算法简介:

  • for_each // 遍历容器
  • transform // 搬运容器到另一个容器中

这两个算法包含在 <algorithm> 中。

5.1.1 for_each

功能描述:

  • 实现遍历容器

函数原型:

  • for_each(iterator_beg, iterator_end, _func);

    // 遍历算法 遍历容器元素

    // beg 开始迭代器

    // end 结束迭代器

    // _func 函数或者函数对象

#include<algorithm>
#include<vector>

// 普通函数
void print1(int val) {
    cout << val << " ";
}

// 仿函数
class print2 {
public:
    void operator()(int val) {
        cout << val << " ";
    }
};

void test1() {

    vector<int> v;
    v.push_back(10);
    v.push_back(30);
    v.push_back(20);
    v.push_back(40);

    for_each(v.begin(), v.end(), print1);  // 传入普通函数,遍历出每个元素 

    cout << endl;

    for_each(v.begin(), v.end(), print2());  // 传入仿函数(),遍历出每个元素
}

5.1.2 transform

功能描述:

  • 搬运容器到另一个容器中

函数原型:

  • transform(iterator_beg1, iterator_end1, iterator_beg2, _func);

//beg1 源容器开始迭代器

//end1 源容器结束迭代器

//beg2 目标容器开始迭代器

//_func 函数或者函数对象

#include<algorithm>
#include<vector>

void print1(int val) {
    cout << val << " ";
}

int trans(int v) {
    return v;
}

void test1() {

    vector<int> v;
    v.push_back(10);
    v.push_back(30);
    v.push_back(20);
    v.push_back(40);

    vector<int> v2;
    v2.resize(v.size());  // 先要开辟空间
    transform(v.begin(), v.end(), v2.begin(), trans);

    for_each(v2.begin(), v2.end(), print1);
}

5.2 常用查找算法

算法简介:

  • find(iterator beg, iterator end, value);

    // 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置

    // beg 开始迭代器

    // end 结束迭代器

    // value 查找的元素

  • find_if(iterator beg, iterator end, _Pred);

    // 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置

    // beg 开始迭代器

    // end 结束迭代器

    // _Pred 函数或者谓词(返回 bool 类型的仿函数)

  • adjacent_find(iterator beg, iterator end);

    // 查找相邻重复元素, 返回相邻元素的第一个位置的迭代器

    // beg 开始迭代器

    // end 结束迭代器

  • bool binary_search(iterator beg, iterator end, value);

    // 查找指定的元素,查到 返回 true 否则 false

    // 注意: 在 无序序列中不可用

    // beg 开始迭代器

    // end 结束迭代器

    // value 查找的元素

  • count(iterator beg, iterator end, value);

    // 统计元素出现次数,返回 int 类型数据

    // beg 开始迭代器

    // end 结束迭代器

    // value 统计的元素

  • count_if(iterator beg, iterator end, _Pred);

    // 按条件统计元素出现次数,返回 int 类型数据

    // beg 开始迭代器

    // end 结束迭代器

    // _Pred 谓词

一个 find_if 的例子:

class Greater30 {
public:
    bool operator()(int val) {
        return val > 30;
    }
};

void test1() {

    vector<int> v;
    v.push_back(10);
    v.push_back(30);
    v.push_back(20);
    v.push_back(40);

    vector<int>::iterator it=find_if(v.begin(), v.end(), Greater30());
    if (it == v.end()) {
        cout << " 没找到 " << endl;
    }
    else {
        cout << " 找到 " << endl;
    }
    cout << *it << endl;  // 输出 30,找到第一个满足条件的即停止查找
}

5.3 常用排序算法

算法简介:

  • sort(iterator beg, iterator end, _Pred);

    // 排序

    // beg 开始迭代器

    // end 结束迭代器

    // _Pred 谓词

  • random_shuffle(iterator beg, iterator end);

    // 指定范围内的元素随机调整次序

    // beg 开始迭代器

    // end 结束迭代器

  • merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);

    // 容器元素合并,并存储到另一容器中

    // 注意: 两个容器必须是 有序的

    // beg1 容器 1 开始迭代器
    // end1 容器 1 结束迭代器
    // beg2 容器 2 开始迭代器
    // end2 容器 2 结束迭代器
    // dest 目标容器开始迭代器

  • reverse(iterator beg, iterator end);

    // 反转指定范围的元素

    // beg 开始迭代器

    // end 结束迭代器

5.4 常用拷贝和替换算法

算法简介:

  • copy(iterator beg, iterator end, iterator dest);

    // 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置

    // beg 开始迭代器

    // end 结束迭代器

    // dest 目标起始迭代器

  • replace(iterator beg, iterator end, oldvalue, newvalue);

    // 将区间内旧元素 替换成 新元素

    // beg 开始迭代器

    // end 结束迭代器

    // oldvalue 旧元素

    // newvalue 新元素

  • replace_if(iterator beg, iterator end, _pred, newvalue);

    // 按条件替换元素,满足条件的替换成指定元素

    // beg 开始迭代器

    // end 结束迭代器

    // _pred 谓词

    // newvalue 替换的新元素

  • swap(container c1, container c2);

    // 互换两个容器的元素

    // c1 容器 1

    // c2 容器 2

5.5 常用算术生成算法

注意:

  • 算术生成算法属于小型算法,使用时包含的头文件为 #include <numeric>

算法简介:

  • accumulate(iterator beg, iterator end, value);

    // 计算容器元素累计总和,返回总和

    // beg 开始迭代器

    // end 结束迭代器

    // value 起始值

  • fill(iterator beg, iterator end, value);

    // 向容器中填充元素

    // beg 开始迭代器

    // end 结束迭代器

    // value 填充的值

5.6 常用集合算法

算法简介:

  • set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);

    // 求两个集合的交集

    // 注意: 两个集合必须是有序序列

    // beg1 容器 1 开始迭代器
    // end1 容器 1 结束迭代器
    // beg2 容器 2 开始迭代器
    // end2 容器 2 结束迭代器
    // dest 目标容器开始迭代器,目标容器开辟空间需要从 两个容器中取小值

  • set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);

    // 求两个集合的并集

    // 注意: 两个集合必须是有序序列

    // beg1 容器 1 开始迭代器
    // end1 容器 1 结束迭代器
    // beg2 容器 2 开始迭代器
    // end2 容器 2 结束迭代器
    // dest 目标容器开始迭代器,目标容器开辟空间需要 两个容器相加

  • set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);

    // 求两个集合的差集

    // 注意: 两个集合必须是有序序列

    // beg1 容器 1 开始迭代器
    // end1 容器 1 结束迭代器
    // beg2 容器 2 开始迭代器
    // end2 容器 2 结束迭代器
    // dest 目标容器开始迭代器,目标容器开辟空间需要从 两个容器取较大值

六、异常处理

程序有时会遇到运行阶段错误,导致程序无法正常地运行下去,C++ 为此提供了异常处理工具。其基本思想是:函数 A 在执行过程中发现异常时可以不加处理,而只是”拋出一个异常”给 A 的调用者,函数 B 可以选择捕获 A 拋出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被拋给 B 的调用者,以此类推。如果一层层的函数都不处理异常,异常最终会被拋给最外层的 main 函数。main 函数应该处理异常。如果 main 函数也不处理异常,那么程序就会立即异常地中止。

6.1 基本的异常处理

1、调用 std::abort() 函数

using namespace std;

double cal(double a, double b) {
    if (a == -b) {
        cout << " 两数之和不可以等于 0" << endl;
        abort();       // 直接调用
    }
    else {
        return 1 / (a + b);
    }
}

当运行到 abort(),程序会终止,避免崩溃。

2、返回错误码

可以使用指针 / 引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。

bool cal(double a, double b, double * ans) {
    if (a == -b) {
        *ans = 0.0;
        return false;
    }
    else {
        *ans = 1 / (a + b);
        return true;
    }
}

void test1() {

    double x, y, ans;
    cout << " 请输入 x,y 的值 " << endl;
    cin >> x >> y;

    if (cal(x, y, &ans)) {
        cout << ans << endl;
    }
    else {
        cout << "error code 1" << endl;
    }
}

6.2 异常机制

C++ 异常是对程序运行过程中发生异常情况的一种响应,对异常的处理分为 3 个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用 try 块

C++ 异常处理涉及到三个关键字:try、catch、throw

如果有一个块抛出一个异常,捕获异常的方法会使用 trycatch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

捕获异常

// 捕获异常

try {
   // 保护代码
}
catch(ExceptionName e1)
{
   // 处理 ExceptionName 异常的代码
}
catch(ExceptionName e2)
{
   // 处理 ExceptionName 异常的代码
}

如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 …,如下所示:

try {}
catch (...){}  // 捕获任何异常

抛出异常

我们可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。

double division(int a, int b)
{
   if(b == 0)
   {
      throw "Division by zero condition!";  // 抛出异常
   }
   return (a/b);
}

6.3 案例

#include <iostream>
using namespace std;

double division(int a, int b)
{
   if(b == 0)
   {
      throw "Division by zero condition!";
   }
   return (a/b);
}

int main ()
{
   int x = 50;
   int y = 0;
   double z = 0;

   try {
     z = division(x, y);
     cout << z << endl;
   }catch (const char* msg) {
     cerr << msg << endl;
   }

   return 0;
}

由于我们抛出了一个类型为字符串数组(”Division by zero condition!”)的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char* 接收。

6.4 异常类型

6.4.1 标准异常

C++ 提供了一系列标准的异常,定义在 <exception> 中。

我们可以在程序中使用这些标准的异常。

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。

下面举一个 std::bad_alloc 例子:

#include<iostream>
using namespace std;

int main()
{
    try {
        char* p = new char[0x7fffffff];  // 无法分配这么多空间,会抛出异常
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
    catch (bad_alloc& e) {
        cout << e.what() << endl;
    }
    return 0;
}
// 第 8 行的 catch 捕获到了 bad allocation 输出,第二次 catch 为空(即使就是 bad_alloc 类型的异常)

what() 是异常类提供的一个公共方法,将返回异常产生的原因

异常只捕获一次

6.4.2 自定义异常

可以通过继承和重载 exception 类来定义新的异常。

#include <iostream>
#include <exception>
using namespace std;

class MyException : public exception
{
public:
    const char* what() const throw ()
    {
        return "C++ Exception";
    }
};

int main()
{
    try
    {
        throw MyException();
        cout << "look here" << endl;
    }
    catch (MyException& e)
    {
        cout << "MyException caught" << endl;
        cout << e.what() << endl;
    }
    catch (exception& e)
    {
        // 其他的错误
    }
}
/* 输出
MyException caught
C++ Exception
*/

欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net