C++高频知识点(二十七)
文章目录
- 131. lamda表达式是怎么获取循环索引的?
- 方法 1:手动维护索引变量
- for_each 函数
- 仿函数
- 方法 2:使用带索引的 for 循环
- 132. 多线程中,生产者消费者之间如何通信?
- 代码例子
- 133. 详细谈谈 condition_variable
- wait 方法详解
- 示例代码1
- 示例代码2
- 虚假唤醒问题
- 134. C++中的“std::move”有什么作用?如何使用?
- move 只针对复杂类型
- (1)移动语义中的std::move
- (2)容器优化中的std::move
- 注意事项
- 135. C++中使用std::thread创建线程有几种方式?
- 1. 创建线程执行普通函数
- 2.使用 lambda 表达式创建线程
- 3. 传递参数给线程函数
- 4. 使用成员函数创建线程
- 为什么成员函数要用 &?
- 指向 printMessage 的成员函数指针:
- 函数名通常是函数的地址,但对于成员函数来说,情况有所不同。
- 1. 普通函数的函数名是地址
- 2. 成员函数的函数名和地址的区别
- 5. 使用成员函数并传递参数
131. lamda表达式是怎么获取循环索引的?
方法 1:手动维护索引变量
如果你需要在使用 Lambda 表达式时访问索引,可以在循环中定义并更新一个索引变量,并将其传递给 Lambda 表达式。
在 std::for_each 中获取索引:
#include <iostream>
#include <vector>
#include <algorithm>int main() {std::vector<int> numbers = {10, 20, 30, 40, 50};int index = 0; // 定义索引变量// 使用 for_each 遍历,并通过捕获索引std::for_each(numbers.begin(), numbers.end(), [&index](int num) {std::cout << "Index: " << index << ", Value: " << num << std::endl;++index; // 手动增加索引});return 0;
}
for_each 函数
template <class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);
first:容器中要遍历的起始迭代器。
last:容器中要遍历的结束迭代器,指向容器的尾后元素。遍历会从 first 到 last 之前的元素。
f:一个函数或者函数对象(可以是 Lambda 表达式、普通函数或仿函数),这个函数会被应用到每个元素上。
仿函数
仿函数(也称为函数对象)是指一个实现了 operator() 运算符的对象,这样的对象可以像普通函数一样被调用。简而言之,仿函数是一个可以被调用的对象,它在 C++ 中被用作可调用对象的一种形式。
#include <iostream>class Adder {
private:int value; // 状态,存储当前加的数public:// 构造函数初始化 valueAdder(int v) : value(v) {}// 重载 operator(),使对象成为一个可调用对象int operator()(int x) {return x + value;}
};int main() {Adder add5(5); // 创建一个仿函数对象,value 为 5std::cout << "Result: " << add5(10) << std::endl; // 调用仿函数对象,相当于调用 add5.operator()(10)return 0;
}
方法 2:使用带索引的 for 循环
另一种方法是在基于范围的 for 循环中手动维护索引。
示例:范围 for 循环和 Lambda
#include <iostream>
#include <vector>int main() {std::vector<int> numbers = {10, 20, 30, 40, 50};for (int index = 0; index < numbers.size(); ++index) {auto lambda = [index](int value) {std::cout << "Index: " << index << ", Value: " << value << std::endl;};lambda(numbers[index]);}return 0;
}
132. 多线程中,生产者消费者之间如何通信?
一般来讲:在 C++ 中,可以通过 互斥锁(std::mutex)和 条件变量(std::condition_variable)来实现生产者和消费者之间的通信。这种方法能有效防止缓冲区的竞争访问并协调线程等待和唤醒的行为。
代码例子
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>std::queue<int> buffer;
const unsigned int BUFFER_SIZE = 10; // 缓冲区最大容量std::mutex mtx;
std::condition_variable cv;void producer(int id) {int value = 0;while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return buffer.size() < BUFFER_SIZE; }); // 等待缓冲区有空位// 生产数据并放入缓冲区buffer.push(value);std::cout << "Producer " << id << " produced: " << value++ << std::endl;lock.unlock();cv.notify_all(); // 通知消费者可以消费数据}
}void consumer(int id) {while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return !buffer.empty(); }); // 等待缓冲区有数据// 消费数据并从缓冲区移除int value = buffer.front();buffer.pop();std::cout << "Consumer " << id << " consumed: " << value << std::endl;lock.unlock();cv.notify_all(); // 通知生产者可以生产数据}
}int main() {std::thread p1(producer, 1);std::thread p2(producer, 2);std::thread c1(consumer, 1);std::thread c2(consumer, 2);p1.join();p2.join();c1.join();c2.join();return 0;
}
133. 详细谈谈 condition_variable
std::condition_variable 是 C++11 标准引入的一个同步原语,用于线程间的协调。它提供了一种机制,允许一个线程等待特定条件的满足,并在另一个线程中通知或唤醒它。
wait 方法详解
1.wait一旦被调用,它首先去获得锁,然后去检测一下第二个参数的值,如果为True,那么wait函数就返回了(等于wait什么都没做),代码继续往下执行。
2.如果第二个参数为false,则该线程进入阻塞状态(立刻进入睡眠状态),睡眠之前会释放锁,一旦入睡,不会自己醒来,除非被人叫醒(被 notify)。
3.notify 只能唤醒由同一个条件变量 wait 而导致的 在睡眠中的线程。
4.睡眠中的线程 ,如果被叫醒,它首先去获得锁,并且去再去判断第二个条件的值,如果为false,则放弃锁,继续睡;为true 则醒来返回(不睡了),执行后边的代码 。
示例代码1
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>std::mutex mtx;
std::condition_variable cv;
bool ready = false;void wait_for_ready() {std::unique_lock<std::mutex> lock(mtx);std::cout << "俺马上要调用wait...\n";// Wait until ready is truecv.wait(lock, [] {std::cout << "谓词检查! ready=="<<ready << '\n';return ready;});std::cout << "线程结束等待, ready = " << ready << std::endl;
}void set_ready() {std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟工作{std::lock_guard<std::mutex> lock(mtx);ready = true;std::cout << "Ready 变成了 true, notifying..." << '\n';}cv.notify_one(); //这里做通知
}int main() {// 从这之后 cout 打印 bool 都是 true/falsestd::cout << std::boolalpha;std::thread t1(wait_for_ready);std::thread t2(set_ready);t1.join();t2.join();return 0;
}
示例代码2
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool ready = false;void print_message(int id) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return ready; }); // 等待 ready 变为 truestd::cout << "Thread " << id << " is running!" << std::endl;
}void set_ready() {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作std::unique_lock<std::mutex> lock(mtx);ready = true;lock.unlock();cv.notify_all(); // 通知所有等待的线程
}int main() {std::thread t1(print_message, 1);std::thread t2(print_message, 2);std::thread notifier(set_ready);t1.join();t2.join();notifier.join();return 0;
}
虚假唤醒问题
在多线程编程中,虚假唤醒是一个可能的问题,即线程被唤醒时,条件未必真的满足。因此,wait 必须始终与一个循环一起使用,以重新检查条件是否满足。这样可以确保线程只有在条件为真时才继续执行。
虚假唤醒通常发生在多线程编程中,特别是在使用条件变量(如 std::condition_variable)时。当某个线程因条件变量的 wait 方法进入等待状态,且被其他线程唤醒时,有可能会发生所谓的“虚假唤醒”。
虚假唤醒的原因包括:
线程被唤醒,但条件不满足:即使 wait 被唤醒,可能仍然没有满足特定的条件。这时需要再次检查条件,而不是继续执行。 否则,程序会发生错误。
系统调度器的行为:操作系统的调度器有时可能会唤醒线程,即便条件没有变化,这种唤醒并不意味着条件满足,只是调度器调度线程执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool ready = false;void print_message(int id) {std::unique_lock<std::mutex> lock(mtx);// 使用 while 循环检查条件,以防止虚假唤醒// 意思是,如果条件不成立,你TMD就别唤醒我,打扰老子睡觉while (!ready) {cv.wait(lock); // 等待 ready 变为 true}std::cout << "Thread " << id << " is running!" << std::endl;
}void set_ready() {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作std::unique_lock<std::mutex> lock(mtx);ready = true;lock.unlock();cv.notify_all(); // 通知所有等待的线程
}int main() {std::thread t1(print_message, 1);std::thread t2(print_message, 2);std::thread notifier(set_ready);t1.join();t2.join();notifier.join();return 0;
}
134. C++中的“std::move”有什么作用?如何使用?
std::move 的本质是一个类型转换函数,它将传入的左值强制转换为右值引用,从而允许将资源从一个对象“移动”到另一个对象,而不是进行拷贝
move 只针对复杂类型
std::move 适用于能够被移动的类型,主要是那些管理动态资源(如内存、文件句柄等)的类型,特别是可以通过转移资源而不需要复制的类型。它的作用是将对象的资源从一个地方“转移”到另一个地方,从而避免不必要的复制,提高效率。
不适用于:基本类型(如 int、float 等),这些类型本身不管理资源,移动和拷贝效果一样,因此 std::move 对它们没有实际意义。
#include <iostream>
#include <string>
using namespace std;class myclass {
public:myclass(int value, string msg) {this->value = value;this->msg = msg;cout << "这是构造" << endl;}void show() {cout << "这里是显示" << this->value << "-" << this->msg << endl;}private:int value;string msg;
};int main() {myclass obj1(1, "hello");myclass obj2 = move(obj1);obj2.show();obj1.show();return 0;
}
(1)移动语义中的std::move
std::move常用于调用移动构造函数或移动赋值运算符。
#include <iostream>
#include <vector>
using namespace std;class MyClass {
private:int* data; // 动态资源
public:// 构造函数MyClass(int value) : data(new int(value)) {cout << "Constructed with value: " << *data << endl;}// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data) {other.data = nullptr; // 转移后释放原对象资源cout << "Move Constructed" << endl;}// 析构函数~MyClass() {if (data) {cout << "Destroyed with value: " << *data << endl;delete data;} else {cout << "Destroyed empty object" << endl;}}
};int main() {MyClass obj1(10); // 调用构造函数MyClass obj2(std::move(obj1)); // 调用移动构造函数return 0;
}
(2)容器优化中的std::move
在STL容器中,std::move避免拷贝,提高性能。
#include <iostream>
#include <vector>
#include <string>
using namespace std;int main() {vector<string> vec;string str = "Hello";cout << "Before move, str: " << str << endl;vec.push_back(std::move(str)); // 使用 std::move 将 str 移动到 vec 中cout << "After move, str: " << str << endl; // str 内容被“移动”cout << "vec[0]: " << vec[0] << endl;return 0;
}
注意事项
错误示例:
std::string s = "test";
std::string moved = std::move(std::move(s)); // 不必要的 std::move
135. C++中使用std::thread创建线程有几种方式?
使用 std::thread 创建一个线程,需要将一个可调用对象(函数、函数指针、lambda 表达式、成员函数等)传递给 std::thread 构造函数。
template< class F, class... Args >
explicit thread( F&& f, Args&&... args );
F&&:这是一个万能引用(universal reference),能够根据传入的参数决定它是否是左值或右值。当你定义一个函数模板时,使用 F&& 来捕获参数,这样它既可以接受左值,也可以接受右值。允许完美转发。
1. 创建线程执行普通函数
#include <iostream>
#include <thread>void printHello() {std::cout << "Hello from thread!" << std::endl;
}int main() {// 创建线程并启动std::thread t(printHello);// 等待线程完成t.join(); // 或 t.detach(),取决于需求std::cout << "Main thread ends!" << std::endl;return 0;
}
- std::thread t(printHello); 会创建一个新线程,在新线程中执行 printHello 函数。
- t.join(); 会让主线程等待子线程执行完毕,直到 printHello 函数执行完。
- 如果你调用 t.detach();,子线程将与主线程分离,并在后台继续执行,但主线程不会等待它。
2.使用 lambda 表达式创建线程
#include <iostream>
#include <thread>int main() {std::thread t([]() {std::cout << "Hello from thread!" << std::endl;});t.join();std::cout << "Main thread ends!" << std::endl;return 0;
}
3. 传递参数给线程函数
如果你的线程函数需要参数,你可以通过构造 std::thread 时传递参数。线程函数的参数会自动传递给被调用的函数或 lambda。
#include <iostream>
#include <thread>void printNumbers(int a, int b) {std::cout << "Number1: " << a << ", Number2: " << b << std::endl;
}int main() {int x = 5, y = 10;std::thread t(printNumbers, x, y); // 将 x 和 y 作为参数传递给 printNumberst.join();return 0;
}
4. 使用成员函数创建线程
如果你需要在类的成员函数中创建线程,可以使用成员函数,但要特别注意对象和方法的绑定。
#include <iostream>
#include <thread>class MyClass {
public:void printMessage() {std::cout << "Hello from MyClass method!" << std::endl;}
};int main() {MyClass obj;// 通过成员函数创建线程std::thread t(&MyClass::printMessage, &obj); // 传递对象指针t.join();return 0;
}
为什么成员函数要用 &?
成员函数是类的一部分,依赖于对象实例:成员函数和普通函数不同,它们是绑定到某个类的对象上的,因此在调用时需要知道该函数在哪个对象上执行。&MyClass::printMessage 表示一个指向成员函数的指针,需要通过某个对象来调用。
this 指针:成员函数内部需要通过 this 指针访问类的成员,而 this 是指向当前对象的指针。因此,成员函数必须和某个特定对象绑定,才能正确执行。
成员函数指针:在 C++ 中,成员函数和普通函数的指针类型是不同的。普通函数指针的类型就是函数本身的类型(例如 void (*f)())。而成员函数指针的类型包含了类的类型信息(例如 void (MyClass::*f)()),并且需要与特定对象关联。因此,必须使用 & 来取得成员函数的指针。
因此
普通函数可以直接传递函数名,因为它不依赖于特定对象。
成员函数需要通过 & 来获取指向成员函数的指针,因为它依赖于特定的对象实例(通过 this 指针)。C++ 编译器需要明确知道哪个对象来调用该成员函数。
指向 printMessage 的成员函数指针:
void (MyClass::*ptr1)(); // 指向没有参数的成员函数指针
ptr1 = &MyClass::printMessage;
void (MyClass::*ptr1)(); 声明了一个指针 ptr1,它指向 MyClass 类中返回类型为 void、没有参数的成员函数。
&MyClass::printMessage 获取 printMessage 成员函数的地址,并赋给 ptr1
函数名通常是函数的地址,但对于成员函数来说,情况有所不同。
1. 普通函数的函数名是地址
对于普通(非成员)函数,函数名就是指向该函数的指针。也就是说,函数名本身就是指向函数的地址,可以直接作为指针来使用。
#include <iostream>
using namespace std;void printMessage() {cout << "Hello from function!" << endl;
}int main() {void (*funcPtr)() = printMessage; // 函数名即函数的地址funcPtr(); // 通过指针调用函数return 0;
}
在这个例子中,printMessage 就是函数的地址。你可以将它赋值给函数指针 funcPtr,然后通过指针来调用函数。
2. 成员函数的函数名和地址的区别
对于成员函数,它的函数名不是简单的函数地址。成员函数与类的对象绑定,并且需要一个 this 指针来访问类的成员变量。因此,成员函数的指针并不等于函数的地址,它需要特定的成员函数指针类型来指向。
#include <iostream>
using namespace std;class MyClass {
public:void printMessage() {cout << "Hello from MyClass!" << endl;}
};int main() {MyClass obj;// 通过成员函数指针获取地址void (MyClass::*funcPtr)() = &MyClass::printMessage;// 通过对象调用成员函数(obj.*funcPtr)(); // 通过对象调用成员函数return 0;
}
5. 使用成员函数并传递参数
如果成员函数带有参数,依然可以通过 std::thread 传递。
#include <iostream>
#include <thread>class MyClass {
public:void printNumbers(int a, int b) {std::cout << "Number1: " << a << ", Number2: " << b << std::endl;}
};int main() {MyClass obj;int x = 5, y = 10;// 传递对象指针及参数std::thread t(&MyClass::printNumbers, &obj, x, y);t.join();return 0;
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!