C++编程(三)—— C++11

文章目录

  • 绑定器和函数对象
    • 函数对象(仿函数)
      • c函数指针
      • 函数对象
    • 绑定器
    • lambda表达式
    • 用什么类型表示lambda表达式?
  • 关键词与语法
    • auto
    • nullptr
    • 右值引用
  • 智能指针
  • 容器
    • set和map
    • unordered_set和unordered_map
    • 数组
    • 链表
  • 语言级别支持的多线程编程
    • thread
    • 子线程如何结束
    • 主线程如何处理子线程
    • 线程间的互斥
    • 线程的同步通信机制(条件变量)
    • 总结


绑定器和函数对象

函数对象(仿函数)

函数对象 = C语言中的函数指针
C++编程(三)—— C++11_第1张图片

c函数指针

其中comp为函数指针。
通过C语言的函数指针调用函数,不能内联,效率低(因为有函数调用开销)

template<typename T>
bool myless(T a, T b)
{
	return a < b;
}

template<typename T, typename Compare>
bool compare(T a, T b, Compare comp)
{
	return comp(a,b);      // 函数指针调用函数
}
int main()
compare(10, 20, myless<int>);

函数对象

1、通过函数对象对用operator(),可以省略函数的调用开销,比通过函数指针调用函数效率高(函数指针不能inline)

2、因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多可用的信息。

3、使用函数对象时,不需要事先定义一个类。

template<typename T>
class myless(T a, T b)
{
public:
	bool operator()(T a, T b)   // 二元函数对象(两个参数)
	{
		return a < b;
	}
}

template<typename T, typename Compare>
bool compare(T a, T b, Compare comp)
{
	return comp(a,b);      // operator() (a,b)
}

int main()
compare(10, 20, myless<int>());

例子1:优先级队列:

priority_queue<int, vector<int>, greater<int>> ay;

当你使用 greater 时,你实际上是在创建一个 greater 的实例而不是调用一个函数。在这种情况下,不需要使用括号。括号通常用于调用函数。

例子2:有序set:
set默认小到大,当需要他从大到小时,可以传入一个函数对象,如下:

set<int,  greater<int>> set1;

例子3:排序算法:

std::sort(vec.begin(), vec.end(), std::greater<int>());

greater 是一个函数对象类,它定义了一个函数调用运算符 operator(),用于比较两个元素的大小。

然而,如果你要使用函数对象来进行比较操作,你需要使用括号来调用该函数对象的函数调用运算符 operator()。这个函数调用运算符是函数对象类中的一个成员函数,用于执行比较操作。例如:

greater<int> comp;
bool result = comp(5, 10);  // 调用函数对象的函数调用运算符

在上面的示例中,comp 是greater 的一个实例,通过调用函数对象的函数调用运算符,可以将 5 和 10 进行比较。

需要注意的是, std::greater() 是一个二元函数对象,可以被用于比较两个值的大小。
如果 std::greater() 是一个一元函数对象,它将无法正确地用作 std::sort 的比较器。因为 std::sort 需要一个接受两个参数的比较函数对象来进行排序操作

但是在优先队列的模板参数中,我们只需提供函数对象类型,而不需要调用它的函数调用运算符。优先队列会在内部使用这个函数对象进行比较操作。因此,在优先队列的模板参数中,我们只需提供函数对象的类型 greater,而不需要调用它。

绑定器

函数绑定器(Function binders)是一种函数适配器

bind1st和bind2nd在STL中主要用于二元函数对象,将其中的一元绑定成一个固定的量,成为一元函数变量。
bind1st和bind2nd+二元函数对象 =》 一元函数对象

greater为例,如果我现在想在一个已经排序完成的数组vec中在正确的位置插入70,可以使用bind1st绑定

70按顺序插入vec容器中--》找一个小于70的数字
operator()(const T &val)
greater   a > b
less      a < b

auto it1 = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));auto it1 = find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));
if(it1 != vec.end())
{
	vec.insert(it1, 70);
}

bind是bind1st和bind2nd的升级,他们本身也是函数对象。

#include 
#include 

void printValues(int a, int b) {
    std::cout << "a: " << a << ", b: " << b << std::endl;
}

int main() {
    // 使用 std::bind1st
    auto printA = std::bind1st(std::ptr_fun(printValues), 42);
    printA(10);  // 输出:a: 42, b: 10

    // 使用 std::bind2nd
    auto printB = std::bind2nd(std::ptr_fun(printValues), 20);
    printB(30);  // 输出:a: 30, b: 20

    // 使用 std::bind
    auto printC = std::bind(printValues, std::placeholders::_1, std::placeholders::_2);
    printC(50, 60);  // 输出:a: 50, b: 60

    return 0;
}

lambda表达式

lambda表达式底层就是函数对象的实现。
语法:
[捕获外部变量] (形参列表) -> 返回值 {操作代码}
C++编程(三)—— C++11_第2张图片
lambda表达式省去了定义类的步骤

返回值:
如果返回值不需要,那么"->返回值“是可以省略的。

捕获:
[ ]:不捕获任何外部变量
[=]:以传值的方式捕获外部所有变量
[&]:以传引用的方式捕获外部所有变量(形参修改了,外部也修改)
[this]:捕获外部的this
[=, &a]:以传值的方式捕获外部所有变量,但是a变量以传引用的方式捕获
[a, &b]:以传值的方式捕获外部变量a,以传引用的方式捕获外部变量b

int a = 10; int b = 20;
auto func3 = [a, b] () mutable  
// 加上mutable使得a,b作为捕获参数可以在lambda表达式内部修改
// 但是因为a,b是作为值进行传递的,所以a和b的值并不会发生改变 
{
	int tmp = a;
	a = b;
	b = tmp;
}
func3(); 

请注意,[ ]中的参数属于函数对象的成员变量ma, mb,如下所示:
C++编程(三)—— C++11_第3张图片
而重载括号运算符函数为常量函数,不能修改成员变量的值,所以一般[ ]中的参数在函数代码中为不能改变。

但如果是传的引用,就可以直接修改而不用加mutable,如下:

int a = 10; int b = 20;
auto func3 = [&] () 
// 加上&使得a,b可以作为引用传递,所以a和b的值会发生改变 
{
	int tmp = a;
	a = b;
	b = tmp;
}
func3(); 

例1:
前面提到,要在一个有序数组中找到第一个小于70的数字,如下:

auto it1 = find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));

因为less是一个二元函数对象,所以需要bind2nd达到 a < 70 的效果。
但是这样的代码比较冗余,于是C++11提供了lambda表达式来简化代码,如下:

auto it1 = find_if(vec.begin(), vec.end(), [] (int val)->bool {return val < 48;} );

[ ]:捕获外部变量
(…):小括号重载函数的形参列表(一个int val形参,一元函数对象)
bool:函数返回值
{…}:函数代码

例2:
for_each可以遍历容器的所有元素,可以自行添加合适的函数对象,对容器的元素进行过滤。
下面的例子为过滤vec中的偶数。

for_each(vec.begin(), vec.end(), 
		 [](int val)->void
		 {
		 	if(val % 2 == 0) cout << val << ' ';
		 }
		);

例3:
从大到小排序

sort(vec.begin(), vec.end(), 
	 [] (int a, int b)->bool
	 {
	 	return a > b;
	 }
	);

函数对象的缺点:
函数对象使用在泛型算法参数传递、比较性质、自定义操作(优先级队列)
如果一个地方使用函数对象,就得定义出一个类,而且是只有小括号重载函数的简短类。灵活性太差。

lambda表达式的好处:

把lambda作为函数对象传入表达式,是非常精悍的。

用什么类型表示lambda表达式?

auto func3 = [&] () 
// 加上&使得a,b可以作为引用传递,所以a和b的值会发生改变 
{
	int tmp = a;
	a = b;
	b = tmp;
}

不用auto:

// function  在<>中:  <返回类型(输入类型1,输入类型2)>
map<int, function<int(int,int)>> caculateMap; 
caculateMap[1] = [] (int a, int b)->int { return a+b; };
.....

智能指针:
如果我想用智能指针管理FILE*,但是FILE的内存需要用fclose(FILE*)而不是delete,所以需要进行自定义删除函数。

unique ptr<FILE, function<void(FILEx)>>
	ptr1(fopen("data.txt""w")[] (EILE *pf) fclose (pf); });

优先级队列:

1、题目:347.找出topK的元素
方式一:根据函数对象的调用,根据键值对的第二位来进行大小的比较排序,如下:定义:

priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

仿函数mycomparison如下:

class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
};

但是这个不灵活,且需要定义一个类,很冗余。

方式二:lambda表达式

using FUNC = function<bool(pair<int, int>& , pair<int, int>&)>;
        priority_queue<pair<int, int>, vector<pair<int, int>>, FUNC> pri_que
                                     ( [] (pair<int, int>& lhs, pair<int, int>& rhs)->bool
                                     {
                                     	return lhs.second > rhs.second;
                                     } );

priority_queue<pair<int, int>, vector<pair<int, int>>, 
			   decltype
			   (
			   [](const auto& lhs, const auto& rhs) 
			   {
   			   		return lhs.second > rhs.second;
			   }
			   )> pri_que;

decltype获取 Lambda 表达式的类型。

2、自定义Data类,让priority_queue比较其中成员变量的大小。
C++编程(三)—— C++11_第4张图片

using FUNC = function<bool(Data&, Data&)>;
priority_queue<Data, vector<Data>, FUNC> maxHeap
												( [] (Data& d1, Data& d2)->bool
												{
													return d1.ma > d2.ma;  //自定义规则
												}
												);
maxHeap.push(Data(10,20);
maxHeap.push(Data(10,30);  // 非常灵活,对Data的成员变量可以设置任意规则进行比较

关键词与语法

详情见:博客

auto

根据右值,推导出右值的类型

nullptr

指针专用。(之前的NULL指针和整数是混用的)

右值引用

move移动语义函数和forward类型完美转发函数

智能指针

详情见:博客

shared_ptr和weak_ptr

容器

详情见:博客

set和map

增删插的效率很高
红黑树为底层,有序容器

unordered_set和unordered_map

哈希表为底层,增删插的效率为O(1)

数组

array
vector 建议使用

链表

farword_list双向链表
list 建议使用

语言级别支持的多线程编程

概念见:博客

之前都是使用的操作系统的接口,如:
createThread pthread_create clone
这样对跨平台很不友好,兼容性差。

thread

int main
{
	// 创建了一个线程对象,传入一个线程函数,新线程就开始运行了
	std::thread t1(threadHandle1, 2);
	
	xxxxx  主程序
	
	// 主线程等待子线程结束,主线程继续往下运行
	//t1.join();
	
	//把子线程设置为分离线程
	t1.detach();
	
	cout << "main thread done!" << endl;
	当主线程运行完成时,如果查看当前进程还有未运行完成的子线程,进程就会异常终止
	return 0;
}

子线程如何结束

子线程函数运行完成,线程结束

主线程如何处理子线程

1、主线程等待子线程结束 join

2、把子线程设置为分离线程,不管子线程 detach

线程间的互斥

竞争态条件,有不是线程安全的代码=》临界区代码段=》保障原子操作=》互斥锁mutex
C++编程(三)—— C++11_第5张图片
所以临界区代码需要=》操作=》互斥锁
锁+双重判断
C++编程(三)—— C++11_第6张图片

线程的同步通信机制(条件变量)

场景:线程1必须依赖线程2的通知

例子:生产者消费者模型
1、创建线程安全的lockQueue
C++编程(三)—— C++11_第7张图片

std::mutex mtx; // 定义互斥锁,做线程间的互斥操作
std::condition_variable cv; // 定义条件变量,做线程间的同步通信操作

// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue   // 对queue重新封装一下
{
public:
	void put(int val) // 生产物品
	{
		//lock_guard guard(mtx); // scoped_ptr   不能同时使用两把锁
		unique_lock<std::mutex> lck(mtx); // unique_ptr
		while (!que.empty())
		{
			// que不为空,生产者应该通知消费者去消费,消费完了,再继续生产
			// 生产者线程进入#1等待状态(阻塞状态),并且#2把mtx互斥锁释放掉 消费者线程就能抢到这把锁   不释放锁  无法消费
			cv.wait(lck);  // lck.lock()  lck.unlock
		}
		que.push(val);
		/* 
		notify_one:通知另外的一个线程的
		notify_all:通知其它所有线程的
		通知其它所有的线程,我生产了一个物品,你们赶紧消费吧
		其它线程得到该通知,就会从等待状态 =》 阻塞状态 =》 获取互斥锁才能继续执行
		*/
		cv.notify_all(); 
		cout << "生产者 生产:" << val << "号物品" << endl;
	}
	int get() // 消费物品
	{
		//lock_guard guard(mtx); // scoped_ptr
		unique_lock<std::mutex> lck(mtx); // unique_ptr
		while (que.empty())
		{
			// 消费者线程发现que是空的,通知生产者线程先生产物品
			// #1 进入等待状态 # 把互斥锁mutex释放
			cv.wait(lck);
		}
		int val = que.front();
		que.pop();
		cv.notify_all(); // 通知其它线程我消费完了,赶紧生产吧
		cout << "消费者 消费:" << val << "号物品" << endl;
		return val;
	}
private:
	queue<int> que;
};
void producer(Queue *que) // 生产者线程  生产10个物品
{
	for (int i = 1; i <= 10; ++i)
	{
		que->put(i);
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}
void consumer(Queue *que) // 消费者线程
{
	for (int i = 1; i <= 10; ++i)
	{
		que->get();
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}
int main()
{
	Queue que; // 两个线程共享的队列

	std::thread t1(producer, &que);
	std::thread t2(consumer, &que);

	t1.join(); // 主线程等到两个子线程执行完  继续执行
	t2.join();
    
	return 0;
}


总结

线程间互斥 : 临界区 原子类型 互斥锁 信号量

线程间同步 : 条件变量 信号量

1、std::mutex

std::mutex mtx;
mtx.lock()
mtx.unlock()

2、lock_guard
只能在简单的临界区代码段的互斥操作中,不可能用在函数参数传递或者返回函数中。
因为函数参数传递或者返回过程中都会用到拷贝构造和赋值,但是lock_guard拷贝构造和赋值(引用和右值引用)都被delete了。

lock_guard<std::mutex> guard(mtx)

3、unique_lock
不仅能在简单的临界区代码段的互斥操作中,还能用在函数参数传递(函数调用)中。
虽然unique_lock底层左值引用的拷贝构造和赋值被delete,但是它提供了右值引用的拷贝构造和赋值

unique_lock<std::mutex> lck(mtx)

4、条件变量

std::condition_variable cv;
cv.wait(lck);   //guard输入不了,因为lock_guard没有拷贝构造
// 使线程进入等待状态,并且把lck.unlock可以把mtx释放掉
cv.notify_all();
// 通知再cv上等待的线程,条件成立了,起来干活了
// 收到通知=》从等待状态到阻塞状态=》获取互斥锁了=》线程继续执行
notify_one();
// 通知再cv上等待的一个线程

你可能感兴趣的:(c++,开发语言)