Notizen zum „Modern C++ Tutorial“ (4)

4 Behälter

4.1 Lineare Container

std::array

Warum std::array einführen, anstatt std::vector direkt zu verwenden?
Es gibt bereits ein traditionelles Array. Warum std::array verwenden?
Beantworten Sie zunächst die erste Frage. Im Gegensatz zu std::vector ist die Größe von std::array-Objekten festgelegt. Wenn die Containergröße festgelegt ist, kann ihr Priorität eingeräumt werden Verwenden Sie den std::array-Container. Da std::vector außerdem automatisch erweitert wird, gibt der Container beim Speichern großer Datenmengen und beim Löschen des Containers nicht automatisch den Speicher zurück, der dem gelöschten Element entspricht. Zu diesem Zeitpunkt müssen Sie Shrink_to_fit manuell ausführen (), um diesen Teil des Speichers freizugeben.

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

Das zweite Problem ist noch einfacher: Die Verwendung von std::array kann den Code „moderner“ machen und einige Betriebsfunktionen kapseln, z. B. das Abrufen der Größe des Arrays und die Überprüfung, ob es nicht leer ist, und kann auch benutzerfreundlicher sein in den Standard-Bibliothekscontaineralgorithmen wie std::sort.

Die Verwendung von std::array ist einfach. Geben Sie einfach Typ und Größe an:

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;

Wenn wir std::array verwenden, werden wir unweigerlich auf eine Schnittstelle stoßen, die mit dem C-Stil kompatibel sein muss. Hier gibt es drei Möglichkeiten:

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 ist ein Listencontainer und seine Verwendung ähnelt im Wesentlichen der von std::list.

Was Sie wissen müssen, ist, dass std::forward_list im Gegensatz zur Implementierung der doppelt verknüpften Liste von std::list mithilfe einer einseitig verknüpften Liste implementiert wird, die das Einfügen von Elementen mit O(1)-Komplexität ermöglicht und dies nicht tut unterstützt schnellen Direktzugriff (dies ist auch eine Funktion für verknüpfte Listen) und ist der einzige Container in der Standardbibliothek, der keine size()-Methode bereitstellt. Höhere Speicherplatznutzung als std::list, wenn keine bidirektionale Iteration erforderlich ist.

4.2 Ungeordnete Container

Wir sind bereits mit dem geordneten Container std::map/std::set in traditionellem C++ vertraut. Diese Elemente werden intern durch rot-schwarze Bäume implementiert, und die durchschnittliche Komplexität des Einfügens und Suchens beträgt O(log(size)). Beim Einfügen von Elementen wird die Größe der Elemente anhand des <-Operators verglichen, um festzustellen, ob die Elemente gleich sind, und eine geeignete Position zum Einfügen in den Container ausgewählt. Beim Durchlaufen der Elemente in diesem Container werden die Ausgabeergebnisse einzeln in der Reihenfolge des <-Operators durchlaufen.

Die Elemente in einem ungeordneten Container werden nicht sortiert und intern über eine Hash-Tabelle implementiert. Die durchschnittliche Komplexität beim Einfügen und Suchen von Elementen beträgt O (konstant). Wenn die Reihenfolge der Elemente im Container keine Rolle spielt, können erhebliche Leistungsverbesserungen erzielt werden erhalten.

Die beiden von C++11 eingeführten Sätze ungeordneter Container sind: std::unordered_map/std::unordered_multimap und std::unordered_set/std::unordered_multiset.

Ihre Verwendung ähnelt im Wesentlichen dem ursprünglichen std::map/std::multimap/std::set/set::multiset. Da wir mit diesen Containern bereits vertraut sind, werden wir keine Beispiele einzeln angeben. Vergleichen wir std direkt ::map und 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";
}

Die Ausgabe ist:

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 Tupel

Wenn Sie Python kennen, sollten Sie das Konzept von Tupeln kennen. Wenn Sie sich die Container in traditionellem C++ ansehen, mit Ausnahme von std::pair, scheint es, dass es keine vorgefertigte Struktur gibt, die zum Speichern verschiedener Datentypen verwendet werden kann (normalerweise definieren wir). die Struktur selbst). Der Fehler von std::pair liegt jedoch auf der Hand: Es können nur zwei Elemente gespeichert werden.

Grundbetrieb

Es gibt drei Kernfunktionen für die Verwendung von Tupeln:

std::make_tuple: Konstruieren Sie ein Tupel.
std::get: Ermitteln Sie den Wert einer bestimmten Position im Tupel.
std::tie: Entpacken Sie das Tupel

#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 Zusätzlich zur Verwendung von Konstanten zum Abrufen von Tupelobjekten fügt C++14 die Verwendung von Typen zum Abrufen von Objekten in Tupeln hinzu:

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;

Laufzeitindex

Wenn Sie sorgfältig darüber nachdenken, finden Sie möglicherweise das Problem mit dem obigen Code. std::get<> basiert auf einer Konstante zur Kompilierungszeit, daher ist die folgende Methode illegal:

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

Was also tun? Die Antwort ist, dass bei Verwendung von std::variant<> (eingeführt in C++17) der für „variant<>“ bereitgestellte Typvorlagenparameter es einem „variant<>“ ermöglicht, mehrere Arten von bereitgestellten Variablen aufzunehmen (in anderen Sprachen, z. B. Python). /JavaScript usw. verhält sich wie ein dynamischer Typ):

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

Dieser Wille:

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

Zusammenführen und Durchlaufen von Tupeln

Das Zusammenführen kann mit std::tuple_cat erfolgen:

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

Wie kann man schnell über ein Tupel iterieren? Aber wir haben gerade vorgestellt, wie man ein Tupel zur Laufzeit durch eine nicht konstante Zahl indiziert, dann wird das Durchlaufen einfach. Zuerst müssen wir die Länge eines Tupels kennen, die sein kann:

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

Dies ermöglicht die Iteration über Tupel:

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

Guess you like

Origin blog.csdn.net/YuhsiHu/article/details/131971877