Notas del "Tutorial de C++ moderno" (4)

4 contenedores

4.1 Contenedores Lineales

estándar::matriz

¿Por qué introducir std::array en lugar de usar std::vector directamente?
Ya existe una matriz tradicional, ¿por qué usar std::array?
Primero responda la primera pregunta. A diferencia de std::vector, el tamaño de los objetos std::array es fijo. Si el tamaño del contenedor es fijo, entonces se le puede dar prioridad Utilice el contenedor std::array. Además, debido a que std::vector se expande automáticamente, cuando se almacena una gran cantidad de datos y se elimina el contenedor, el contenedor no devolverá automáticamente la memoria correspondiente al elemento eliminado. En este momento, debe ejecutar manualmente shrink_to_fit () para liberar esta parte de la memoria.

std::vector<int> v;
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 0

// 如下可看出 std::vector 的存储是自动管理的,按需自动扩张
// 但是如果空间不足,需要重新分配更多内存,而重分配内存通常是性能上有开销的操作
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << "size:" << v.size() << std::endl;         // 输出 3
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 4

// 这里的自动扩张逻辑与 Golang 的 slice 很像
v.push_back(4);
v.push_back(5);
std::cout << "size:" << v.size() << std::endl;         // 输出 5
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 8

// 如下可看出容器虽然清空了元素,但是被清空元素的内存并没有归还
v.clear();                                             
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 8

// 额外内存可通过 shrink_to_fit() 调用返回给系统
v.shrink_to_fit();
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 0

El segundo problema es aún más simple.Usar std::array puede hacer que el código sea más "moderno" y encapsula algunas funciones de operación, como obtener el tamaño de la matriz y verificar si no está vacía, y también puede ser fácil de usar. en los algoritmos de contenedor de biblioteca estándar, como std::sort.

Usar std::array es fácil, solo especifica su tipo y tamaño:

std::array<int, 4> arr = {
    
    1, 2, 3, 4};

arr.empty(); // 检查容器是否为空
arr.size();  // 返回容纳的元素数

// 迭代器支持
for (auto &i : arr)
{
    
    
    // ...
}

// 用 lambda 表达式排序
std::sort(arr.begin(), arr.end(), [](int a, int b) {
    
    
    return b < a;
});

// 数组大小参数必须是常量表达式
constexpr int len = 4;
std::array<int, len> arr = {
    
    1, 2, 3, 4};

// 非法,不同于 C 风格数组,std::array 不会自动退化成 T*
// int *arr_p = arr;

Cuando comenzamos a usar std::array, inevitablemente encontraremos una interfaz que debe ser compatible con el estilo C. Aquí hay tres formas:

void foo(int *p, int len) {
    
    
    return;
}

std::array<int, 4> arr = {
    
    1,2,3,4};

// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

// 使用 `std::sort`
std::sort(arr.begin(), arr.end());

std::forward_list

std::forward_list es un contenedor de listas y su uso es básicamente similar al de std::list.

Lo que necesita saber es que, a diferencia de la implementación de la lista doblemente enlazada de std::list, std::forward_list se implementa utilizando una lista enlazada unidireccional, que proporciona inserción de elementos con complejidad O(1), y no Admite acceso aleatorio rápido (esta también es una función de lista vinculada) y el único contenedor en la biblioteca estándar que no proporciona un método de tamaño (). Mayor utilización del espacio que std::list cuando no se requiere iteración bidireccional.

4.2 Contenedores desordenados

Ya estamos familiarizados con los contenedores ordenados std::map/std::set en C++ tradicional.Estos elementos se implementan internamente mediante árboles rojo-negro, y la complejidad promedio de inserción y búsqueda es O(log(size)). Al insertar elementos, el tamaño de los elementos se comparará de acuerdo con el operador < para determinar si los elementos son iguales y se seleccionará una posición adecuada para insertarlos en el contenedor. Al recorrer los elementos en este contenedor, los resultados de salida se recorrerán uno por uno en el orden del operador <.

Los elementos en un contenedor desordenado no están ordenados y se implementan internamente a través de una tabla Hash. La complejidad promedio de insertar y buscar elementos es O (constante). Cuando no se tiene en cuenta el orden de los elementos dentro del contenedor, se pueden lograr mejoras significativas en el rendimiento. obtenido.

Los dos conjuntos de contenedores desordenados introducidos por C++11 son: std::unordered_map/std::unordered_multimap y std::unordered_set/std::unordered_multiset.

Su uso es básicamente similar al original std::map/std::multimap/std::set/set::multiset. Dado que ya estamos familiarizados con estos contenedores, no daremos ejemplos uno por uno. Comparemos directamente std ::mapa y std::unordered_map:

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

int main() {
    
    
    // 两组结构按同样的顺序初始化
    std::unordered_map<int, std::string> u = {
    
    
        {
    
    1, "1"},
        {
    
    3, "3"},
        {
    
    2, "2"}
    };
    std::map<int, std::string> v = {
    
    
        {
    
    1, "1"},
        {
    
    3, "3"},
        {
    
    2, "2"}
    };

    // 分别对两组结构进行遍历
    std::cout << "std::unordered_map" << std::endl;
    for( const auto & n : u)
        std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";

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

La salida es:

std::unordered_map
Key:[2] Value:[2]
Key:[3] Value:[3]
Key:[1] Value:[1]

std::map
Key:[1] Value:[1]
Key:[2] Value:[2]
Key:[3] Value:[3]

4.3 Tuplas

Conociendo Python, deberías conocer el concepto de tuplas. Mirando los contenedores en C++ tradicional, a excepción de std::pair, parece que no existe una estructura prefabricada que pueda usarse para almacenar diferentes tipos de datos (generalmente definimos la estructura nosotros mismos). Pero el defecto de std::pair es obvio, solo puede guardar dos elementos.

operación básica

Hay tres funciones principales para el uso de tuplas:

std::make_tuple: construye una tupla
std::get: obtiene el valor de una cierta posición en la tupla
std::tie: desempaqueta la tupla

#include <tuple>
#include <iostream>

auto get_student(int id)
{
    
    
    // 返回类型被推断为 std::tuple<double, char, std::string>

    if (id == 0)
        return std::make_tuple(3.8, 'A', "张三");
    if (id == 1)
        return std::make_tuple(2.9, 'C', "李四");
    if (id == 2)
        return std::make_tuple(1.7, 'D', "王五");
    return std::make_tuple(0.0, 'D', "null");
    // 如果只写 0 会出现推断错误, 编译失败
}

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

    double gpa;
    char grade;
    std::string name;

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

std::get Además de usar constantes para obtener objetos de tupla, C++14 agrega el uso de tipos para obtener objetos en tuplas:

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<3>(t) << std::endl;

índice de tiempo de ejecución

Si lo piensa detenidamente, puede encontrar el problema con el código anterior.std::get<> se basa en una constante de tiempo de compilación, por lo que el siguiente método es ilegal:

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

¿Entonces lo que hay que hacer? La respuesta es que al usar std::variant<> (introducido en C++17), el parámetro de plantilla de tipo proporcionado a variant<> permite que variant<> acomode varios tipos de variables proporcionadas (en otros lenguajes, como Python /JavaScript, etc., se comporta como un tipo dinámico):

#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;
}

Esta voluntad:

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

Fusión y recorrido de tuplas

La fusión se puede hacer con std::tuple_cat:

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

¿Cómo iterar rápidamente sobre una tupla? Pero acabamos de presentar cómo indexar una tupla por un número no constante en tiempo de ejecución, luego el recorrido se vuelve simple. Primero, necesitamos saber la longitud de una tupla, que puede ser:

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

Esto permite iterar sobre tuplas:

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

Supongo que te gusta

Origin blog.csdn.net/YuhsiHu/article/details/131971877
Recomendado
Clasificación