现代C++教程:高速上手(四)-容器

1、线性容器

std::array与std::vector不同的是,array对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用std::array容器。

由于std::vector是自动扩容的,当存入大量的数据后,并且对容器进行了删除操作,容器并不会自动归还被删除元素相应的内存,这时候需要手动运行shrink_to_fit()释放这部分内存。

std::array C风格接口传参:

void foo(int *p, int len){
    return;
}
std::array<int, 4> arr = {1,2,3,4};
//foo(arr,arr.size()); //非法,无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

//使用std::sort
std::sort(arr.begin(), arr.end());
//升序
std::sort(arr.begin(), arr.end(), [](int a, int b){
    return b > a;
})

std::forward_list是一个列表容器,使用方法和std::list基本类似。和list的双向链表的实现不同,forward_list使用单向链表进行实现,提供了O(1)复杂度的元素插入,不支持快速随机访问,也是标准库容器中唯一一个不提供size()方法的容器。当不需要双向迭代时,具有比list更高的空间利用率。

2、无序容器

传统c++中的有序容器 std::map / std::set,这些元素内部通过红黑树进行实现,插入和搜索的平均复杂度均为O(log(size))。在插入元素时,会根据<操作符比较元素大小并判断元素是否相同,并选择合适的位置插入到容器中。当对这个容器中的元素进行遍历时,输出结果会按照<操作符的顺序来逐个遍历。

而无序容器中的元素是不进行排序的,内部通过Hash表实现,插入和搜索元素的平均复杂度为O(constant),在不关心容器内部元素顺序时,能够获得显著的性能提升。

c++11引入了两组无序容器:std::unordered_map / std::unordered_multimap和std::unordered_set / std::unordered_multiset。
它们的用法和原有的std::map / std::multimap / std::set / std::multiset基本类似。

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>

using namespace std;

int main(){
    unordered_map<int, string> u = {
        {1, "1"},
        {3, "3"},
        {2, "2"}
    };

    map<int, string> v = {
        {1, "1"},
        {3, "3"},
        {2, "2"}  
    };

    cout << "std::unordered_map" << endl;
    for(const auto &n : u){
        cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
    }

    cout <<endl;
    cout << "std::map" << endl;
    for (const auto & n : v){
        cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
    }
}

3、元组

传统c++中的容器,除了std::pair外,似乎没有现成的结构能够用来存放不同类型的数据。但std::pair的缺陷是显而易见的,只能保存两个元素。

元组基本操作

三个核心函数:
1、std::make_tuple: 构造元组
2、std::get:获得元组某个位置的值
3、std::tie:元组拆包

#include <iostream>
#include <tuple>

using namespace std;

auto get_student(int id){
    switch (id)
    {
    case 0:
        return make_tuple(3.8, 'A', "张三");
        break;
    case 1:
        return make_tuple(2.9, 'C', "李四");
        break;
    case 2:
        return make_tuple(1.7, 'D', "王五");
        break;
    default:
        return make_tuple(0.0, 'D', "null");
        break;
    }
}

int main(){
    auto student = get_student(0);
    std::cout << "ID: 0, "
    << "GPA: " << get<0>(student) << ", "
    << "成绩:" << get<1>(student) << ", "
    << "姓名:" << get<2>(student) << "\n";

    double gpa;
    char grade;
    string name;

    //元祖进行拆包
    tie(gpa, grade, name) = get_student(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "成绩:" << grade << ", "
    << "姓名:" << name << "\n";

    return 0;
}

std::get除了使用常量获取元组对象外,c++14增加了使用类型来获取元组中的对象:

std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
std::cout << std::get<std::string>(t) << std::endl;
std::cout << std::get<double>(t) << std::endl; //非法,引发编译期错误
std::cout << std::get<int>(t) << std::endl;

运行期索引

std::get<>依赖一个编译期的常量,所以下面的方式是不合法的:

int index = 1;
std::get<index>(t); //非法

c++17引入了std::variant<>,提供给variant<>的类型模版参数 可以让一个variant<>从而容纳提供的几种类型的变量(在其他语言,例如Python/JavaScrpit等,表现为动态类型):

#include <variant>

template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i){
    if constexpr(n >= sizeof...(T))
        throw std::out_of_range("越界.");
    if(i == n)
        return std::variant<T...>{
            std::in_place_index<n>, std::get<n>(tpl)
        };
    
    return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
}
template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i){
    return _tuple_index<0>(tpl, i);
}
template <typename T0, typename ... TS>
std::ostream & operator<< (std::ostream & s, std::variant<T0, TS...> const & v){
    std::visit([&](auto && x){s<<x;}, v);
    return s;
}

这样我们就能:

int i = 1;
std::cout << tuple_index(student, i) << endl;

元组合并与遍历

还有一个常见的需求就是合并两个元组,这可以通过std::tuple_cat来实现:

auto new_tuple = std::tuple_cat(get_student(1), std::move(t));

要遍历首先我们需要知道一个元组的长度,可以:

template <typename T>
auto tuple_len(T &tpl){
    return std::tuple_size<T>::value;
}

这样就能够对元组进行迭代了:

//迭代
for(int i = 0; i != tuple_len(new_tuple); ++i){
     //运行期索引
    std::cout << tuple_index<i, new_tuple) << std::endl;
}

总结

std::tuple虽然有效,但是标准库提供的功能有限,没办法满足运行期索引和迭代的需求,好在我们还有其他办法可以自行实现。

posted @ 2020-09-19 22:32  Ryan669  阅读(188)  评论(0编辑  收藏