文章

c++ 常用标准库函数用法

c++ 常用标准库函数用法

本文以 demo 的形式列出了 c++ 常用的标准库函数的用法, 标准包含且不限于 c++11、c++14、c++17、c++20。

std::priority_queue

  • 标准:c++98。
  • 说明:它是基于堆实现的,可以快速地插入新元素,并能够快速地取出当前优先级最高(或最低)的元素。优先级队列在 头文件中定义,是标准模板库的一部分。 使用优先级队列之前,需要包含 头文件,并且可以自定义存储的元素类型和元素之间的比较方式。如果不指定比较方式,默认使用 std::less ,元素会按照从大到小的顺序排列,最大元素优先。
  • 例子: 1、不指定优先级的比较方式,采用默认的 std::less ,元素会按照从大到小的顺序排列,最大元素优先。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;
    pq.push(30);
    pq.push(20);
    pq.push(50);
    pq.push(40);
    // 循环取出并打印元素,直到队列为空
    while (!pq.empty()) {
        // 访问当前优先级最高的元素
        std::cout << pq.top() << " ";
        pq.pop();
    }
    std::cout << "\n";
    return 0;
}

上面的代码最终会输出如下内容:

1
50 40 30 20

2、让优先级队列按照从小到大的顺序排列,可以通过定义自己的比较函数或者使用 std::greater 来实现,下面我采用 std::greater 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <queue>
#include <functional> // for std::greater

int main() {
    // 创建一个int类型的优先级队列,元素按照从小到大的顺序排列
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;
    pq.push(30);
    pq.push(20);
    pq.push(50);
    pq.push(40);
    // 循环取出并打印元素,直到队列为空
    while (!pq.empty()) {
        std::cout << pq.top() << " ";
        pq.pop();
    }
    std::cout << "\n";
    return 0;
}

上面的代码最终会输出如下内容:

1
20 30 40 50

3、下面是一个使用重载 < 操作符的例子,我定义了一个简单的 Person 类,根据人的年龄来决定其在优先级队列中的优先级。年龄较大的人具有较高的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <queue>
#include <string>

struct Person {
    std::string name;
    int age;

    Person(const std::string &n, int a)
        : name(n), age(a) {}
    // 重载 < 操作符
    bool operator<(const Person& other) const {
        // 注意:优先级队列是按照元素的 < 操作符的相反顺序排列的,
        // 因此这里使用 > 来比较,以确保年龄大的人优先级更高
        return age > other.age;
    }
};

int main() {
    std::priority_queue<Person> pq;
    pq.push(Person("Alice", 30));
    pq.push(Person("Bob", 25));
    pq.push(Person("Charlie", 35));
    pq.push(Person("Diana", 32));
    // 循环取出并打印元素,直到队列为空
    while (!pq.empty()) {
        Person p = pq.top();
        std::cout << p.name << " - " << p.age << std::endl;
        pq.pop();
    }
    std::cout << "\n";
    return 0;
}

上面的代码最终会输出如下内容:

1
2
3
4
Bob - 25
Alice - 30
Diana - 32
Charlie - 35

4、通过提供一个仿函数来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <queue>

struct MyObject {
    int value;
    MyObject(int v) : value(v) {}
};

class Compare {
public:
    bool operator()(const MyObject & a, const MyObject & b) {
        // 定义优先级:值较小的对象优先级更高
        return a.value > b.value;
    }
};

int main() {
    std::priority_queue<MyObject, std::vector<MyObject>, Compare> pq;
    pq.push(MyObject(30));
    pq.push(MyObject(20));
    pq.push(MyObject(50));
    pq.push(MyObject(40));
    // 循环取出并打印元素,直到队列为空
    while (!pq.empty()) {
        std::cout << pq.top().value << " ";
        pq.pop();
    }
    std::cout << "\n";
    return 0;
}

上面的代码最终会输出如下内容:

1
20 30 40 50

std::call_once

  • 标准:c++11。
  • 说明:用于保证多线程环境下某个函数只被调用一次。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <mutex>
#include <thread>

static std::once_flag flag;

void Test() {
    std::call_once(flag, []() {
        // 该代码片段只会被调到一次
        std::cout << "I am Test" << std::endl;
      }
    );
}

int main() {
    std::thread t1([&]() {
        while (true) {
            Test();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    std::thread t2([&]() {
        while (true) {
            Test();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    std::thread t3([&]() {
        while (true) {
            Test();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

std::mutex

  • 标准:c++11。
  • 说明:用于实现线程间的互斥访问,确保共享资源的安全性。在使用时,一旦 A 线程 lock 后,其他线程如果调用 lock 便会阻塞等待,直到 A 线程 unlock,其他线程如果不想阻塞等待,可以调用 try_lock, 它会尝试lock,失败后便返回。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <mutex>
#include <iostream>
#include <thread>
#include <unistd.h>

std::mutex mtx;

int main() {
    std::thread t1([](){
        sleep(2); // 休眠2秒,让t2线程先拿到锁
        while (true) {
            mtx.lock(); // t2或者t3持有锁未解锁t1就会一直阻塞
            std::cout<< "t1已上锁\n";
            mtx.unlock();
            std::cout<< "t1已解锁\n";
        }
    });

    std::thread t2([](){
        // 上锁后永远不解锁
        mtx.lock();
        while (true){
            std::cout<< "t2已上锁\n";
            sleep(1);
        }
        mtx.unlock();
    });
    
    std::thread t3([](){
        while (true) {
            // 尝试上锁,立刻返回,不阻塞,如果成功返回true,失败返回false
            auto ret = mtx.try_lock();
            if (ret) {
                std::cout<< "t3已上锁\n";
                mtx.unlock();
                std::cout<< "t3已解锁\n";
            } else {
                std::cout<< "t3上锁失败\n";
            }
            sleep(1);
        }
    });

    t1.join();
    t2.join();
    t3.join();
    return 0;
}

std::lock_guard

  • 标准:c++11。
  • 说明:用于在作用域内自动管理互斥量的上锁和解锁操作,确保在离开作用域时正确释放互斥量,避免忘记解锁而导致的死锁问题。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <mutex>
#include <iostream>

std::mutex mtx;

// 使用 lock_guard 前 unlock无法被调用
# if 0
void test_lock() {
    mtx.lock();
    if (true) {
        std::cout << "exit \n";
        mtx.unlock();
        return;
    }
    mtx.unlock();
}
#endif

// 使用 lock_guard 后不用担心 未 unlock 的问题
void test_lock() {
    std::lock_guard<std::mutex> glck(mtx);
    if (true) {
        std::cout << "exit" << std::endl;
        return;
    }
}

int main() {
    test_lock();
    return 0;
}

std::shared_mutex

  • 标准:c++17
  • 说明:它允许多个线程同时读取共享数据,但在写入数据时要求独占访问权。这种读写锁的设计旨在提高在多线程环境下对共享数据进行读取操作时的效率,因为它减少了锁的竞争,允许更高的并发度。当某个线程需要写入时,它会等待所有读操作完成后才进行,确保数据一致性和线程安全。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <any>
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>

class DataCenter {
private:
    std::unordered_map<std::string, std::any> datas_;
    mutable std::shared_mutex rw_mutex_;

public:
    DataCenter() {}
    ~DataCenter() {}

    template <typename ValueType>
    void put(const std::string &key, const ValueType &value) {
        std::unique_lock lock(rw_mutex_);
        datas_[key] = value;
    }
    template <typename ValueType>
    auto get(const std::string &key) const -> ValueType {
        std::shared_lock lock(rw_mutex_);
        if (datas_.count(key) == 0) {
            std::cout << "[DC] key not found, key: " << key << std::endl;
            return {};
        }
        try {
            return std::any_cast<ValueType>(datas_.at(key));
        } catch (...) {
            std::cout << "[DC] type error, key: " << key << std::endl;
            return {};
        }
    }

    void remove(const std::string &key) {
        std::unique_lock lock(rw_mutex_);
        datas_.erase(key);
    }
};

std::unique_lock

  • 标准:c++11。
  • 说明:与 std::lock_guard 类似,也可以自动管理互斥量的上锁和解锁操作,但提供了更高级的锁定机制,支持可延迟和可中断的锁定、多个互斥量的锁定等特性,更加灵活和可控。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <mutex>
#include <iostream>

std::mutex mtx;
bool flag = false;

void test_lock() {
    std::unique_lock<std::mutex> ulck(mtx);
    // ulck 构造函数里自动 lock ,防止多个线程对这个值同时修改
    flag = true;
    // 下面的代码不需要互斥访问时,提前unlock
    ulck.unlock();
    // do nothing
    // 如果又出现需要互斥访问的代码片段时,也可以再次 lock
    // 最后 ulck 出作用域时会在析构函数里 unlock
}
int main() {
    test_lock();
    return 0;
}

std::recursive_mutex

  • 标准:c++11。
  • 说明:与 std::mutex 类似,但允许同一个线程多次对互斥量进行加锁操作,而不会导致死锁。这意味着线程可以在持有互斥量的情况下再次对其进行加锁,而不会被阻塞。递归互斥量非常适用于需要在嵌套函数或递归调用中使用互斥量的情况,确保线程安全性的同时保持灵活性。
  • 例子:

使用 std::mutex 时, 当出现下面这种情况就会陷入死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::mutex mtx;

void func1() {
    mtx.lock();
    // do something
    mtx.unlock();
}

void func2() {
    mtx.lock();
    func1();
    // do something
    mtx.unlock();
}

递归锁可以解决这个问题,它允许在同一线程反复加锁,只要加锁次数和解锁次数相等就不会陷入死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <mutex>
#include <thread>
#include <iostream>

std::recursive_mutex rmtx;

void func1() {
    mtx.lock();
    std::cout << "func1" << std::endl;
    mtx.unlock();
}

void func2() {
    mtx.lock();
    std::cout << "func2" << std::endl;
    func1();
    mtx.unlock();
}

int main() {
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
    return 0;
}

std::condition_variable

  • 标准:c++17。
  • 说明:用于实现线程间的同步与通信。它可以与 std::mutex 配合使用,通过等待和通知的机制,实现线程的阻塞和唤醒操作,使线程能够在特定条件满足时进行等待,或者在条件变量发生变化时进行通知,从而实现线程间的协调与同步。
  • 例子:

常用成员函数

1
2
3
4
5
wait(); // 阻塞当前线程直到条件满足被唤醒
wait_for(); // 阻塞当前线程,直到唤醒条件变量或在指定的超时时间之后
wait_until(); // 阻塞当前线程,直到唤醒条件变量或直到达到指定的时间点为止
notify_one(); // 通知一个正在等待的线程
notify_all(); // 通知所有正在等待的线程

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;

void func() {
    std::unique_lock<std::mutex> ulck(mtx);
    std::cout << "子线程等待中..." << std::endl;
#if 1 // 一直阻塞等待
    cv.wait(ulck);
#elif 0// 只等待20秒
    cv.wait_for(ulck, std::chrono::seconds(20) == std::cv_status::timeout);
#else // 等到指定的时间点
    cv.wait_until(ulck, std::chrono::system_clock::now() + std::chrono::seconds(1));
#endif
    std::cout << "子线程等待结束" << std::endl;
}

int main() {
    std::cout << "创建子线程" << std::endl;
    std::thread t(func);
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "子线程可以往下执行" << std::endl;
    cv.notify_one();
    t.join();
    return 0;
}

std::any

  • 标准:c++17。
  • 说明:它提供了一种类型安全的机制,可以在运行时存储和访问任意类型的值。通过 它我们可以将任意类型的值存储在一个统一的容器中,并在需要时进行类型转换和访问,从而实现更灵活的编程。可以基于 void* 实现,也可以基于继承特性实现。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
#include <any>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::any var = 88;
    std::any str = "hahaha";
    Person p = {
        "老王",
        88
    };
    std::any info = p;
    std::cout << std::any_cast<int>(var) << std::endl;
    std::cout << std::any_cast<std::string>(str) << std::endl;
    auto ret = std::any_cast<Person>(info);
    std::cout << "姓名: "
              << ret.name
              << " 年龄: "
              << ret.age
              << std::endl;
    return 0;
}

std::variant

  • 标准:c++17。
  • 说明:和 union 的用途类似。
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <variant>
#include <string>
#include <vector>

struct MyType {
    std::string name;
    int age;
};

using Value = std::variant<int, double, std::string, bool, MyType>;

struct Print {
    void operator()(int i) {
        std::cout << i << std::endl;
    }
    void operator()(double i) {
        std::cout << i << std::endl;
    }
    void operator()(std::string i) {
        std::cout << i << std::endl;
    }
    void operator()(bool i) {
        std::cout << i << std::endl;
    }
    void operator()(MyType i) {
        std::cout << i.name << i.age << std::endl;
    }
};

int main() {
    MyType t;
    t.name = "xiong";
    t.age = 27;
    std::vector<Value> test = {
        88,
        99.9,
        "Hello world",
        true,
        t
    };
    for (const auto &it : test) {
        std::cout << it.index() << std::endl;
        std::visit(Print{}, it);
    }
    std::cout << std::get<0>(test[0]) << std::endl;
    std::cout << std::get<1>(test[1]) << std::endl;
    std::cout << std::get<2>(test[2]) << std::endl;
    std::cout << std::get<3>(test[3]) << std::endl;
    std::cout << std::holds_alternative<std::string>(test[2]) << std::endl;

    return 0;
}
本文由作者按照 CC BY 4.0 进行授权