cpp_7 线程
线程是进程中的一个实体,是CPU调度的基本单位。一个进程中可以包含多个线程,每个线程都有自己的执行路径和执行状态。线程共享进程的资源,如内存空间、文件描述符等,但每个线程有自己的栈空间和寄存器上下文。
线程可以并发执行,多个线程可以同时运行在多个CPU上或者在同一个CPU上通过时间片轮转的方式交替执行。线程之间可以共享数据,可以通过共享内存或消息传递等方式进行通信。
线程的优势是能够提高程序的并发性和响应速度,可以同时执行多个任务,提高程序的性能。然而,线程之间的共享资源也会带来一些问题,如竞争条件和死锁等,需要通过同步机制来解决。
线程是在同一时间并行执行代码片段。
C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。
头文件:#include <thread>
线程类:std::thread
创建线程/构造函数
用于创建线程对象。
thread() noexcept;
默认构造函数。构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
有参构造函数
1 2 template < class Function, class ... Args >explicit thread (Function&& fx, Args&&... args) ;
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数 。 使用类的非静态成员函数作为参数 时,在此之前需要创建类对象,确保类对象的声明周期比线程长,因为类的普通成员函数可能访问类对象的成员变量,如果对象销毁了,内存可能发生泄漏。格式如下:
1 2 3 4 5 6 7 8 9 10 class Test { public : void func (string str, int num) {} }; int main{ Test t; thread t1 (&Test::func, &t, "str" , 5 ) ; }
thread(const thread& ) = delete;
拷贝构造函数被删除,不允许线程对象之间的拷贝。
thread(thread&& other ) noexcept;
有移动构造函数,将线程other的资源所有权转移给新创建的线程对象。
赋值函数
1 2 thread& operator = (thread&& other) noexcept ; thread& operator = (const other&) = delete ;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。
注意
代码示例:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <iostream> #include <thread> #include <windows.h> using namespace std;void func (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } } class mythread1 { public : void operator () (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } } }; class mythread2 { public : static void func (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } } }; class mythread3 { public : void func (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } } }; int main () { auto f = [](int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } }; mythread3 myth; thread t6 (&mythread3::func, &myth, 3 , "我是一只傻傻鸟。" ) ; cout << "任务开始。\n" ; for (int ii = 0 ; ii < 10 ; ii++) { cout << "执行任务中......\n" ; Sleep (1000 ); } cout << "任务完成。\n" ; t6. join (); }
线程资源的回收 虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。
回收子线程的资源有两种方法:
在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。 分离后,需要主程序留有时间执行子线程。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。
示例:
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 #include <iostream> #include <thread> #include <windows.h> using namespace std;void func (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; Sleep (1000 ); } } int main () { thread t1 (func, 3 , "我是一只傻傻鸟。" ) ; thread t2 (func, 8 , "我有一只小小鸟。" ) ; t1. detach (); t2. detach (); Sleep (12000 ); }
this_thread 的全局函数 C++11提供了命名空间this_thread
来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。
都在 std 命名空间下。即:std::this_thread::get_id() 等
get_id()
该函数用于获取线程ID,thread类也有同名的成员函数(t1.get_id()
也可以)。
1 2 3 thread::id get_id () noexcept ;cout << this_thread::get_id () << endl;
sleep_for() 类似 sleep,但使用的是 chrono 库的 duration 作为参数。
该函数让线程休眠一段时间。
1 2 3 4 template <class Rep , class Period > void sleep_for (const chrono::duration<Rep,Period>& rel_time) ; sleep_for (std::chrono::second (1 ));
sleep_until()
该函数让线程休眠至指定时间点。(可实现定时任务) 需要将字符串时间转换为时间点格式。
1 2 template <class Clock , class Duration > void sleep_until (const chrono::time_point<Clock,Duration>& abs_time) ;
yield()
该函数让线程主动让出自己已经抢到的CPU时间片。yield()
的作用是让当前线程放弃执行,将执行机会让给其他线程,但不会使线程进入阻塞状态。具体来说,yield()
的作用是告诉操作系统当前线程愿意放弃其执行权,但并不能保证操作系统会立即调度其他线程。
thread类其它的成员函数
不在 this_thread 命名空间下,而是成员函数。
1 2 void swap (std::thread& other) ; static unsigned hardware_concurrency () noexcept ;
示例:
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 #include <iostream> #include <thread> using namespace std;void func (int bh, const string& str) { cout << "子线程:" << this_thread::get_id () << endl; for (int ii = 1 ; ii <= 3 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; this_thread::sleep_for (chrono::seconds (1 )); } } int main () { thread t1 (func, 3 , "我是一只傻傻鸟。" ) ; thread t2 (func, 8 , "我有一只小小鸟。" ) ; cout << "主线程:" << this_thread::get_id () << endl; cout << "线程t1:" << t1. get_id () << endl; cout << "线程t2:" << t2. get_id () << endl; t1. join (); t2. join (); }
call_once 函数
在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次。
在多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。
头文件:#include <mutex>
函数原型:
1 2 template < class callable, class ... Args > void call_once ( std::once_flag& flag, Function&& fx, Args&&... args ) ;
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。
使用方法:
创建一个类型为 once_flag 的全局变量 onceflag(随便什么名字)。
在被调用的线程函数中使用 call_once 函数,并传入对应参数。
示例:
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 <iostream> #include <thread> #include <mutex> using namespace std;once_flag onceflag; void once_func (const int bh, const string& str) { cout << "once_func() bh= " << bh << ", str=" << str << endl; } void func (int bh, const string& str) { call_once (onceflag,once_func,0 , "各位观众,我要开始表白了。" ); for (int ii = 1 ; ii <= 3 ; ii++) { cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; this_thread::sleep_for (chrono::seconds (1 )); } } int main () { thread t1 (func, 3 , "我是一只傻傻鸟。" ) ; thread t2 (func, 8 , "我有一只小小鸟。" ) ; t1. join (); t2. join (); }
native_handle函数
用于返回 linux 中原生的线程 id,以便可以使用线程库 pthread.h 中的函数操作线程。
C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。
为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。
示例:
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 <iostream> #include <thread> #include <pthread.h> using namespace std;void func () { for (int ii=1 ;ii<=10 ;ii++) { cout << "ii=" << ii << endl; this_thread::sleep_for (chrono::seconds (1 )); } } int main () { thread tt (func) ; this_thread::sleep_for (chrono::seconds (5 )); pthread_t thid= tt.native_handle (); pthread_cancel (thid); tt.join (); }
线程安全
前面在多个线程使用 cout 输出内容时,会出现乱序,是由于 cout 是全局对象,因此导致冲突。
在单核cpu中,不会有冲突。
背景:
如:合租时,要使用卫生间。
如何保证线程安全
volatile 关键字:只保证了顺序,不能保证线程安全
原子操作(原子类型)
线程同步(也称为锁)
线程同步是指 多个线程协同工作,协商如何使用共享资源。
互斥锁 头文件:#include <mutex>
C++ 11 中有四种互斥锁:
mutex 类
多人排队等待一个公共卫生间。
lock() 加锁
互斥锁有锁定和未锁定两种状态。
如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。
如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
unlock() 解锁
只有持有锁的线程才能解锁。
尝试加锁try_lock()
有多个公共卫生间,这个锁住,那就判断尝试别的也可以。
如果互斥锁是未锁定状态,则加锁成功,函数返回true。
如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)
示例:
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 <thread> #include <mutex> using namespace std;mutex mtx; void func (int bh, const string& str) { for (int ii = 1 ; ii <= 10 ; ii++) { mtx.lock (); cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl; mtx.unlock (); this_thread::sleep_for (chrono::seconds (1 )); } } int main () { thread t1 (func, 1 , "我是一只傻傻鸟。" ) ; thread t2 (func, 2 , "我是一只傻傻鸟。" ) ; thread t3 (func, 3 , "我是一只傻傻鸟。" ) ; thread t4 (func, 4 , "我是一只傻傻鸟。" ) ; thread t5 (func, 5 , "我是一只傻傻鸟。" ) ; t1. join (); t2. join (); t3. join (); t4. join (); t5. join (); }
timed_mutex 类
如同:等待一个卫生间超过5分钟,则换别的卫生间。
判断在一定时间是否能获取锁,若是不行,则切换到其它函数。
增加了两个成员函数:
1 2 bool try_lock_for (时间长度) ;bool try_lock_until (时间点) ;
recursive_mutex 类
普通的互斥锁必须在解锁后才能加锁,同一个线程内也不例外。
递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。
即普通互斥锁不能在未解锁的情况下二次加锁,递归互斥锁可以。
示例:
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 #include <iostream> #include <mutex> using namespace std;class AA { recursive_mutex m_mutex; public : void func1 () { m_mutex.lock (); cout << "调用了func1()\n" ; m_mutex.unlock (); } void func2 () { m_mutex.lock (); cout << "调用了func2()\n" ; func1 (); m_mutex.unlock (); } }; int main () { AA aa; aa.func2 (); }
lock_guard 模板类 lock_guard是模板类,可以简化互斥锁的使用,也更安全。
lock_guard的定义如下:
1 2 3 4 5 template <class Mutex >class lock_guard { explicit lock_guard (Mutex& mtx) ; }
lock_guard 的构造函数初始化参数是某一种类型的互斥锁。
lock_guard在构造函数中加锁,在析构函数中解锁。
lock_guard采用了RAII 思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。
示例:
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 <thread> #include <mutex> using std::cout;using std::endl;int a = 0 ;std::mutex mt; void add () { for (int i = 0 ; i < 1000000 ; i++) { std::lock_guard<std::mutex> mlock (mt) ; a++; } }; int main () { std::thread t1 (add) ; std::thread t2 (add) ; t1. join (); t2. join (); cout << a << endl; return 0 ; }
条件变量-生产消费者模型
条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
为了保护共享资源,条件变量需要和互斥锁一起使用。
条件变量最常用的就是实现 生产/消费者模型(高速缓存队列)
生产/消费者模型 生产/消费者模型示意图:
如客服收集用户需求,然后提出工单,工人收到工单处理需求。
生产者先把需要处理的数据放在缓存队列(仓库)中,然后向消费者发送通知。
消费者接收到通知后,从缓存队列中把数据拿出来进行处理。
示例代码:
持有锁时间越短,效率越高。第 32 行,通过大括号增加了一个作用域,使得unique_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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <iostream> #include <string> #include <thread> #include <mutex> #include <deque> #include <queue> #include <condition_variable> using namespace std;class AA { mutex m_mutex; condition_variable m_cond; queue<string, deque<string>> m_q; public : void incache (int num) { lock_guard<mutex> lock (m_mutex) ; for (int ii=0 ; ii<num ; ii++) { static int bh = 1 ; string message = to_string (bh++) + "号超女" ; m_q.push (message); } m_cond.notify_one (); } void outcache () { while (true ) { string message; { unique_lock<mutex> lock (m_mutex) ; while (m_q.empty ()) m_cond.wait (lock); message = m_q.front (); m_q.pop (); } this_thread::sleep_for (chrono::milliseconds (1 )); cout << "线程:" << this_thread::get_id () << "," << message << endl; } } }; int main () { AA aa; thread t1 (&AA::outcache, &aa) ; thread t2 (&AA::outcache, &aa) ; thread t3 (&AA::outcache, &aa) ; this_thread::sleep_for (chrono::seconds (2 )); aa.incache (3 ); this_thread::sleep_for (chrono::seconds (3 )); aa.incache (5 ); t1. join (); t2. join (); t3. join (); }
条件变量 C++11的条件变量提供了两个类:
头文件:#include <condition_variable>
condition_variable
:只支持与普通mutex搭配,效率更高。
condition_variable_any
:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。
condition_variable类 notify_one 和 notify_all,如果生产的数据只有一个,用前者合适,生产的数据有多个,则用后者(可以调用多线程执行操作)。
wait() 函数
wait()
函数的作用不只是等待生产者的唤醒信号,还会进行解锁操作,使得多个线程都可以获得锁,然后阻塞等待生产者的唤醒信号,最后等到了信号后进行加锁操作以便执行对后面共享变量的操作。
对传入的互斥锁进行解锁
阻塞等待生产者的唤醒信号
等到了信号后给互斥锁加锁
1 2 3 4 5 6 7 8 9 10 11 1 )condition_variable () 默认构造函数。2 )condition_variable (const condition_variable &)=delete 禁止拷贝。3 )condition_variable& condition_variable::operator =(const condition_variable &)=delete 禁止赋值。4 )notify_one () 通知一个等待的线程。5 )notify_all () 通知全部等待的线程,使得所有线程处理获取的数据。6 )wait (unique_lock<mutex> lock) 阻塞当前线程,直到通知到达。7 )wait (unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。8 )wait_for (unique_lock<mutex> lock,时间长度)9 )wait_for (unique_lock<mutex> lock,时间长度,Pred pred)10 )wait_until (unique_lock<mutex> lock,时间点)11 )wait_until (unique_lock<mutex> lock,时间点,Pred pred)
unique_lock 模板类 template <class Mutex> class unique_lock
是模板类,模板参数为互斥锁类型。
unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。
普通互斥锁需要转换为 unique_lock 才能用于条件变量。因为lock_guard 没有 lock() 和 unlock() 函数,而 unique_lock 有,因为 wait 函数需要解锁和加锁两个功能。
unique_lock 除了增加作用域,也可以调用 unlock() 函数手动解锁。
示例代码 while() 循环有虚假唤醒的可能,即因为没有抢到数据,就是虚假唤醒了线程。
除了用循环避免虚假唤醒,还可以用 wait() 函数的重载函数 wait(unique_lock<mutex> lock,Pred pred)
循环的阻塞当前线程,直到通知到达且谓词满足。
示例代码:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <iostream> #include <string> #include <thread> #include <mutex> #include <deque> #include <queue> #include <condition_variable> using namespace std;class AA { mutex m_mutex; condition_variable m_cond; queue<string, deque<string>> m_q; public : void incache (int num) { lock_guard<mutex> lock (m_mutex) ; for (int ii=0 ; ii<num ; ii++) { static int bh = 1 ; string message = to_string (bh++) + "号超女" ; m_q.push (message); } m_cond.notify_all (); } void outcache () { while (true ) { unique_lock<mutex> lock (m_mutex) ; m_cond.wait (lock, [this ] { return !m_q.empty (); }); string message = m_q.front (); m_q.pop (); cout << "线程:" << this_thread::get_id () << "," << message << endl; lock.unlock (); this_thread::sleep_for (chrono::milliseconds (1 )); } } }; int main () { AA aa; thread t1 (&AA::outcache, &aa) ; thread t2 (&AA::outcache, &aa) ; thread t3 (&AA::outcache, &aa) ; this_thread::sleep_for (chrono::seconds (2 )); aa.incache (2 ); this_thread::sleep_for (chrono::seconds (3 )); aa.incache (5 ); t1. join (); t2. join (); t3. join (); }
原子类型
头文件:#include <atomic>
原子操作由CPU指令提供支持,是轻量级的锁,不是完全没有锁。
C++11提供了atomic<T>
模板类(结构体),用于支持原子类型,模板参数可以是bool、char、int、long、long long、指针类型(不支持浮点类型和自定义数据类型)。
原子操作由CPU指令提供支持,它的性能比锁和消息传递更高 ,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。
构造函数:
1 2 3 atomic () noexcept = default ; atomic (T val) noexcept ; atomic (const atomic&) = delete ;
赋值函数:
1 atomic& operator =(const atomic&) = delete ;
常用函数:
是CPU指令提供的标准操作。
1 2 3 4 5 6 7 void store (const T val) noexcept ; T load () noexcept ; T fetch_add (const T val) noexcept ; T fetch_sub (const T val) noexcept ; T exchange (const T val) noexcept ; T compare_exchange_strong (T &expect,const T val) noexcept ; bool is_lock_free () ;
注意点:
示例:
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 #include <iostream> #include <atomic> using namespace std;int main () { atomic<int > a = 3 ; cout << "a=" << a.load () << endl; a.store (8 ); cout << "a=" << a.load () << endl; int old; old = a.fetch_add (5 ); cout << "old = " << old <<",a = " << a.load () << endl; old = a.fetch_sub (2 ); cout << "old = " << old << ",a = " << a.load () << endl; atomic<int > ii = 3 ; int expect = 4 ; int val = 5 ; bool bret = ii.compare_exchange_strong (expect, val); cout << "bret=" << bret << endl; cout << "ii=" << ii << endl; cout << "expect=" << expect << endl; }
可调用对象的绑定器和包装器 C11 的三神器:智能指针、移动语义和可调用对象的绑定器和包装器。
可调用对象
在C++中,可以像函数一样调用的有:**普通函数、类的静态成员函数、仿函数、lambda函数、类的非静态成员函数、可被转换为函数的类的对象 ** ,统称可调用对象或函数对象。
可调用对象有类型,可以用指针存储它们的地址,可以被引用(类的成员函数除外)
普通函数
C++ 中,函数是一种数据类型,函数的实体被看成对象,而不只是一段代码。函数是对象,可以调用的对象,或者叫函数对象。
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 <iostream> using namespace std;using Fun = void (int , const string&); Fun show; int main () { show (1 , "我是一只傻傻鸟。" ); void (*fp1)(int , const string&) = show; void (&fr1)(int , const string&) = show; fp1 (2 , "我是一只傻傻鸟。" ); fr1 (3 , "我是一只傻傻鸟。" ); Fun* fp2 = show; Fun& fr2 = show; fp2 (4 , "我是一只傻傻鸟。" ); fr2 (5 , "我是一只傻傻鸟。" ); } void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; }
类的静态成员函数
类的静态成员函数和普通函数本质上是一样的,把普通函数放在类中而已。
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 <iostream> using namespace std;using Fun = void (int , const string&); struct AA { static void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; int main () { AA::show (1 , "我是一只傻傻鸟。" ); void (*fp1)(int , const string&) = AA::show; void (&fr1)(int , const string&) = AA::show; fp1 (2 , "我是一只傻傻鸟。" ); fr1 (3 , "我是一只傻傻鸟。" ); Fun* fp2 = AA::show; Fun& fr2 = AA::show; fp2 (4 , "我是一只傻傻鸟。" ); fr2 (5 , "我是一只傻傻鸟。" ); }
仿函数
仿函数的本质是类,重载小括号 () 运算符,调用的代码像函数。
仿函数的类型就是类的类型。
普通类创建的对象不是可调用对象,因为它不能像函数一样直接调用(调用成员函数不一样,是可以的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std;struct BB { void operator () (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; int main () { BB bb; bb (11 , "我是一只傻傻鸟。" ); BB ()(12 , "我是一只傻傻鸟。" ); BB& br = bb; br (13 , "我是一只傻傻鸟。" ); }
lambda 函数
lambda函数的本质是仿函数,仿函数的本质是类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> using namespace std;int main () { auto lb = [](int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; }; auto & lr = lb; lb (1 , "我是一只傻傻鸟。" ); lr (2 , "我是一只傻傻鸟。" ); }
类的非静态成员函数
与其他可调用对象不同,所以才需要可调用对象的包装器和绑定器。这是最重要的可调用对象。
类的非静态成员函数有地址,但是,只能通过类的对象才能调用它,所以,C++对它做了特别处理。
普通函数有函数类型,而类的非静态成员函数只有指针类型,没有引用类型,不能引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> using namespace std;struct CC { void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; int main () { CC cc; cc.show (14 , "我是一只傻傻鸟。" ); void (CC::* fp11)(int , const string&) = &CC::show; (cc.*fp11)(15 , "我是一只傻傻鸟。" ); using pFun = void (CC::*)(int , const string&); pFun fp12 = &CC::show; (cc.*fp12)(16 , "我是一只傻傻鸟。" ); }
可以转换为函数指针的类对象
不重要。
类可以重载类型转换运算符operator 数据类型() ,如果数据类型是函数指针或函数引用类型,那么该类实例也将成为可调用对象。
它的本质是类,调用的代码像函数。
在实际开发中,意义不大。
类中的这个转换函数只能返回普通全局函数和类的静态成员函数,不能返回类的非静态成员函数,而普通全局函数和类的静态成员函数本身就是可调用对象,直接调用就行,没必要多此一举。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> using namespace std;void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } struct DD { using Fun = void (*)(int , const string&); operator Fun () { return show; } }; int main () { DD dd; dd (17 , "我是一只傻傻鸟。" ); }
包装器
用于统一模板中的写法。
std::function
模板类是一个通用的可调用对象的包装器,用简单的、统一的方式处理可调用对象。
头文件:#include <functional>
1 2 template <class _Fty >class function ……
_Fty 是可调用对象的类型(只有一个模板参数),格式:返回类型(参数列表)。
注意:
示例:
包装器的使用:
仿函数:可以使用匿名对象,也可以使用已有对象。
lambda 函数:可以预先定义一个lambda表达式,也可以直接给一个没有名字的。
类的非静态成员函数:function<void(CC&,int, const string&)> fn11 = &CC::show;
模板参数的第一个参数是类对象的引用,调用时的第一个参数是类对象。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include <iostream> #include <functional> using namespace std;void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } struct AA { static void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct BB { void operator () (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct CC { void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct DD { using Fun = void (*)(int , const string&); operator Fun () { return show; } }; int main () { using Fun = void (int , const string&); void (*fp1)(int , const string&) = show; fp1 (1 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn1 = show; fn1 (1 , "我是一只傻傻鸟。" ); void (*fp3)(int , const string&) = AA::show; fp3 (2 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn3 = AA::show; fn3 (2 , "我是一只傻傻鸟。" ); BB bb; bb (3 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn4 = BB (); fn4 (3 , "我是一只傻傻鸟。" ); auto lb = [](int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; }; lb (4 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn5 = lb; fn5 (4 , "我是一只傻傻鸟。" ); CC cc; void (CC:: * fp11)(int , const string&) = &CC::show; (cc.*fp11)(5 , "我是一只傻傻鸟。" ); function<void (CC&,int , const string&)> fn11 = &CC::show; fn11 (cc,5 , "我是一只傻傻鸟。" ); DD dd; dd (6 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn12 = dd; fn12 (6 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fx=dd; try { if (fx) fx (6 , "我是一只傻傻鸟。" ); } catch (std::bad_function_call e) { cout << "抛出了std::bad_function_call异常。" ; } }
适配器(绑定器)bind
头文件:#include <functional>
std::bind()模板函数是一个通用的函数适配器(绑定器),它用一个可调用对象及其参数,生成一个新的可调用对象,以适应模板。也就是把一个可调用对象改头换面,变成一个新的可调用对象。
函数原型:
1 2 template < class Fx , class ... Args > function<> bind (Fx&& fx, Args&...args);
Fx
:需要绑定的可调用对象(可以是前两节课介绍的那六种,也可以是function对象)。
args
:绑定参数列表,可以是左值、右值和参数占位符 std::placeholders::_n
,如果参数不是占位符,缺省为值传递,std:: ref
(参数)则为引用传递。
std::bind()
返回 std::function
的对象。
std::bind()
的本质是仿函数。
bind()
第一个参数传入可调用对象(表示将可调用对象绑定给包装器对象),后面传入占位符。
bind 基本用法示例:
函数参数顺序不同,形成重载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <functional> using namespace std;void show (int bh, const string& message) { cout << "亲爱的" << bh << "号," << message << endl; } int main () { function<void (int , const string&)> fn1 = show; function<void (int , const string&)> fn2 = bind (show, placeholders::_1, placeholders::_2); fn1 (1 , "我是一只傻傻鸟。" ); fn2 (1 , "我是一只傻傻鸟。" ); function<void (const string&, int )> fn3 = bind (show, placeholders::_2, placeholders::_1); fn3 ("我是一只傻傻鸟。" , 1 ); function<void (const string&)> fn4 = bind (show, 3 , placeholders::_1); fn4 ("我是一只傻傻鸟。" ); function<void (int , const string&,int )> fn5 = bind (show, placeholders::_1, placeholders::_2); fn5 (1 , "我是一只傻傻鸟。" , 88 ); }
绑定六种可调用对象示例:
仿函数和lambda函数:可以直接传入定义了的对象,也可以传入匿名对象或者lambda函数,你懂的。
类的非静态成员函数:
function<void(CC&, int, const string&)> fn11 = bind(&CC::show, placeholders::_1, placeholders::_2, placeholders::_3);
这样的话,在调用包装器对象时,还需要将类对象传入。
function<void(int, const string&)> fn11 = bind(&CC::show,&cc,placeholders::_1, placeholders::_2);
可以预先将类对象的地址传入,这样在调用时就不需要额外传入类对象了,也就可以实现模板化了。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <iostream> #include <functional> using namespace std;void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } struct AA { static void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct BB { void operator () (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct CC { void show (int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; } }; struct DD { using Fun = void (*)(int , const string&); operator Fun () { return show; } }; int main () { function<void (int , const string&)> fn1 = bind (show, placeholders::_1, placeholders::_2); fn1 (1 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn3 = bind (AA::show, placeholders::_1, placeholders::_2); fn3 (2 , "我是一只傻傻鸟。" ); function<void (int , const string&)> fn4 = bind (BB (), placeholders::_1, placeholders::_2); fn4 (3 , "我是一只傻傻鸟。" ); auto lb = [](int bh, const string& message) { cout << "亲爱的" << bh << "," << message << endl; }; function<void (int , const string&)> fn5 = bind (lb, placeholders::_1, placeholders::_2); fn5 (4 , "我是一只傻傻鸟。" ); CC cc; function<void (int , const string&)> fn11 = bind (&CC::show,&cc,placeholders::_1, placeholders::_2); fn11 (5 , "我是一只傻傻鸟。" ); DD dd; function<void (int , const string&)> fn12 = bind (dd, placeholders::_1, placeholders::_2); fn12 (6 , "我是一只傻傻鸟。" ); }
可变函数和参数的实现
使用了可变参数模板,并且为了提高可用性使用了完美转发。
代码示例类似 thread 的构造函数:
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 <iostream> #include <thread> #include <functional> using namespace std;void show0 () { cout << "亲爱的,我是一只傻傻鸟。\n" ; } void show1 (const string& message) { cout << "亲爱的," << message << endl; } struct CC { void show2 (int bh, const string& message) { cout << "亲爱的" << bh << "号," << message << endl; } }; template <typename Fn, typename ...Args>auto show (Fn&& fn, Args&&...args) -> decltype (bind(forward<Fn>(fn), forward<Args>(args)...)) { cout << "表白前的准备工作......\n" ; auto f = bind (forward<Fn>(fn), forward<Args>(args)...); f (); cout << "表白完成。\n" ; return f; } int main () { show (show0); show (show1,"我是一只傻傻鸟。" ); CC cc; auto f = show (&CC::show2,&cc, 3 ,"我是一只傻傻鸟。" ); f (); }
回调函数
在消息队列和网络库的框架中,当接收到消息(报文)时,回调用户自定义的函数对象,把消息(报文)参数传给它,由它决定如何处理。
回调函数也可以说是一个通用函数,将外部其它的函数传入,让外部函数进行业务处理。将外部可调用对象绑定到回调函数也就是注册回调函数。
回调函数的参数是需要处理的数据。
现代C++中,回调函数一般用类的成员函数,足够强大。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <iostream> #include <string> #include <thread> #include <mutex> #include <deque> #include <queue> #include <condition_variable> #include <functional> using namespace std;void show (const string& message) { cout << "处理数据:" << message << endl; } struct BB { void show (const string& message) { cout << "处理表白数据:" << message << endl; } }; class AA { mutex m_mutex; condition_variable m_cond; queue<string, deque<string>> m_q; function<void (const string&)> m_callback; public : template <typename Fn, typename ...Args> void callback (Fn && fn, Args&&...args) { m_callback = bind (forward<Fn>(fn), forward<Args>(args)..., std::placeholders::_1); } void incache (int num) { lock_guard<mutex> lock (m_mutex) ; for (int ii = 0 ; ii < num; ii++) { static int bh = 1 ; string message = to_string (bh++) + "号超女" ; m_q.push (message); } m_cond.notify_all (); } void outcache () { while (true ) { unique_lock<mutex> lock (m_mutex) ; m_cond.wait (lock, [this ] { return !m_q.empty (); }); string message = m_q.front (); m_q.pop (); cout << "线程:" << this_thread::get_id () << "," << message << endl; lock.unlock (); if (m_callback) m_callback (message); } } }; int main () { AA aa; BB bb; aa.callback (&BB::show, &bb); thread t1 (&AA::outcache, &aa) ; thread t2 (&AA::outcache, &aa) ; thread t3 (&AA::outcache, &aa) ; this_thread::sleep_for (chrono::seconds (2 )); aa.incache (2 ); this_thread::sleep_for (chrono::seconds (3 )); aa.incache (5 ); t1. join (); t2. join (); t3. join (); }
如何取代虚函数 C++虚函数在执行过程中会跳转两次(先查找对象的函数表,再次通过该函数表中的地址找到真正的执行地址),这样的话,CPU会跳转两次,而普通函数只跳转一次。
CPU每跳转一次,预取指令要作废很多,所以效率会很低。(百度)
为了管理的方便(基类指针可指向派生类对象和自动析构派生类),保留类之间的继承关系。
包装器和绑定器可以实现虚函数的功能,并且不会有性能的损失。
包装器和绑定器不要求两个类是否有继承关系,但是保留是为了方便处理基类的指针。
示例:
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 <iostream> #include <functional> using namespace std;struct Hero { function<void ()> m_callback; template <typename Fn, typename ...Args> void callback (Fn&& fn, Args&&...args) { m_callback = bind (forward<Fn>(fn), forward<Args>(args)...); } void show () { m_callback (); } }; struct XS :public Hero { void show () { cout << "西施释放了技能。\n" ; } }; struct HX :public Hero { void show () { cout << "韩信释放了技能。\n" ; } }; int main () { int id = 0 ; cout << "请输入英雄(1-西施;2-韩信。):" ; cin >> id; Hero* ptr = nullptr ; if (id == 1 ) { ptr = new XS; ptr->callback (&XS::show, static_cast <XS*>(ptr)); } else if (id == 2 ) { ptr = new HX; ptr->callback (&HX::show, static_cast <HX*>(ptr)); } if (ptr != nullptr ) { ptr->show (); delete ptr; } }