C++11新特性总结

2023-11-17

目录

一. 统一的列表初始化 {}  适用于各种STL容器

二. 类型推导 auto 和 decltype的出现

三. 右值引用移动语义  (特别重要的新特性)

 四.  万能引用  +  完美转发

五. 可变参数模板  (参数包)

六. emplace_back 的出现和对比分析 push_back接口  emplace_back 是 结合这 可变模板参数出现的

七. Lambda表达式

八. 包装器 (适配器) (function包装器)

九. 线程库

线程函数参数 

十. 原子操作

条件变量引入以及条件变量对象和互斥对象配合实现一个案例

十一. 总结本章


一. 统一的列表初始化 {}  适用于各种STL容器

  • C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
	int _x;
	int _y;
};
int main()
{
	int x1 = 1;
	int x2{ 2 };
	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };
	// C++11中列表初始化也可以适用于new表达式中
	int* pa = new int[4]{ 0 };
	return 0;
}
  • 创建对象时也可以使用列表初始化方式调用构造函数初始化
struct Date {
	int _year;
	int _month;
	int _day;
};

int main() {
	int a{ 2 };			//支持使用{}的统一初始化了
	vector <int > intv{ 1, 2, 3, 5 };
	vector <Date > datev{ { 2001, 10, 9 }, Date{ 2001, 10, 21 } };
	//上述的这些方式都是支持的了   {}  其实是调用的构造函数
	return 0;
}
  • 为什么可以支持 {}  这种方式来调用各种容器的构造函数? 
  • 本质原因:      支持了 std::initializer_list 作为参数的构造函数的产生

 在容器中构造函数中的出现例子:

  • 从此以后终于知道为啥引入   initializer_list头文件之后就可以进行各种容器的{} 初始化形式了.

光光知道还不够, 我们一定要去看看它的底层实现是怎样的, 如下

namespace tyj {
	template <class T>
	class vector {
		typedef T* iterator;
		typedef const T* const_iterator;
	public:
		//如何进行一个初始化, 范围形式的初始化
		vector(initializer_list<T>& l) {
			_start = new T[l.size()];
			_finish = _start + l.size();
			_endofstorage = _start + l.size();
			iterator sit = _start;
			//然后就是范围形式的构造了
			//如下是方式1: 基于范围的实现
			/*for (auto& e : l) {
				*sit++ = e;
			}*/
			//然后是第二种形式, 使用迭代器进行赋值, 其实也就是上述范围的赋值的底层
			typename initializer_list<T>::iterator lit = l.begin();
			while (lit != l.end()) {
				*sit++ = *lit++;
			}
		}
		//针对这个 operator = 赋值运算符的重载 还是复用上述的构造函数
		vector<T>& operator=(initializer_list<T> l) {
			vector<T> tmp(l);
			std::swap(_start, tmp._start);
			std::swap(_finish, tmp._finish);
			std::swap(_endofstorage, tmp._endofstorage);
			return *this;
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

int main() {
	//测试上述的东西:
	//断点测试, 进去查看其中的内存即可
	tyj::vector<int> intv = initializer_list<int>{ 1, 2, 3, 4, 5 };
	return 0;
}

二. 类型推导 auto 和 decltype的出现

  • auto 关键字的作用在编译阶段对于=右边的对象进行自动的类型推导
int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcpy;
	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	return 0;
}
  • 既然有了auto 可以自动推导=右边的类型, 为啥需要decltype呢????
  • decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导. 使用形式如下:     如下包含了万能引用, 完美转发lambda表达式等等知识点, 后序会一一讲解清除
    template<class T, class U> 
    auto Add(T&& t, U&& u) 
    		->decltype(std::forward<T>(t) +std::forward<T>(u)) {
    	return std::forward<T>(t) +std::forward<T>(u);
    }
    
    int main() {
    	auto func = [](int a, double b)->decltype(a + b){ return a + b; };
    	cout << Add(2, 5);
    	while (1);
    	return 0;
    }

三. 右值引用移动语义  (特别重要的新特性)

  • 故名思意, 对左值的引用就是左值引用, 对于右值的引用就是右值引用
  • 定义左值和右值的区别,   可否进行取地址, 可以取地址的就是左值, 不可以取地址的就是右值

定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址, 所以本质还是左值.

int main() {
	int a = 10;

	int& b = a;		//此处是左值引用
	int&& c = 2;	//此处是右值引用
	const int& d = 100;		//突然发现此处也是可行的?
	const int& e = a;
	int&& h = std::move(a);	//std::move 作用 将左值引用转换为右值引用
	//先引出结论: const 左值引用既可以引用左值也可以引用右值
	//右值引用 就只能引用右值不可以引用左值
	return 0; 
}
  • const 左值引用既可以引用左值也可以引用右值. 
  • std::move() 方法可以将左值转换为右值

注意点: 我们不可以对于右值进行一个取地址, 但是一旦给右值取别名之后, 会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。     

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	rr2 = 5.5;  // 报错
	return 0;
}

左值引用作为参数和返回值完美解决了深拷贝的问题, 但是存在一些情况像是局部对象的返回, 就没办法以左值引用返回, 这个时候需要进行深拷贝, 于是右值引用的出现使得这个问题的解决成为可能, 有没有什么办法可以将局部对象作为返回值的这个深拷贝也优化掉?????

首先移动构造的本质:    本质是一种资源窃取, 资源转移.....

比如  return str;          str  如果是一个局部对象的话,   它出不了函数, 一旦函数调用结束, 就会随着栈帧一起释放掉, 但是它的底层存在  char* _str 这样一个字符串数组的成员. 要是一同释放掉着实浪费, 我们是否可以将其利用起来??????        

移动构造的本质就是  将  即将返回的局部对象的所有底层的堆区资源进行转移  窃取, 反正函数调用结束你即将消亡, 然鹅我拷贝构造做深拷贝正好需要的也是这个, 于是将这个即将消亡的 str 的 底层的 堆区 资源转移,进行移动构造出新的对象 

void swap(string& s) {
	::swap(_str, s._str);
	::swap(_size, s._size);
	::swap(_capacity, s._capacity);
		
}   //提供swap方法方便转移资源
string(string&& s) 
	:_str(nullptr)
	,_size(0)
	,_capacity(0) {		
	swap(s);    //直接通过交换, 转移财产, 我的全是空, 和你换
    //反正你即将死亡, 不如将你的资源换给我助我快速构造加以利用
	std::cout << "string(string&& s) ---移动构造" << std::endl;
}

string& operator= (string&& s) {
		swap(s);
		std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl;
		return *this;
}

tyj::string to_string(int val) {
	tyj::string s;		
	bool flag = 0;
	if (val < 0) {
		val *= -1;
		flag = 1;
	}
	while (val) {
		s.push_back(val % 10);
		val /= 10;
	}
	if (flag) s.push_back('-');
	std::reverse(s.begin(), s.end());
	return s;			// 返回即将消亡的局部对象, 如何优化掉这个深拷贝?
}

图解分析, 存在移动构造和不存在移动构造的区别:

 移动构造相对于拷贝构造: 比较区别????

移动构造和拷贝构造本质都是构造一个对象:    只是两者采取的构造方式不一样, 拷贝构造的话如果是深拷贝, 也就是底层存在堆区数据, 存在指针, 就需要新开堆区空间, 且需要进行堆区数据的拷贝,  效率低...     移动构造, 我还是需要堆区空间存储数据, 但是我不自己新开辟, 我直接将拷贝对象的堆区资源转移过来成为我的即可.,....  不需要new 空间 +  数据转移, 效率提高

注意:  移动构造 和  拷贝构造相比,  它的高效仅仅体现在深拷贝 上面, 如果不存在深拷贝. 仅仅只是栈区数据的拷贝, 两者效率是相同的

深拷贝:   存在堆区空间的拷贝....     也就是存在底层存储数据的空间的拷贝

移动构造高效就高效在了这个底层存储数据空间的获取上面, 不是从新申请空间  + 拷贝数据的方式来获取的, 而是直接的获取对方的现有空间  + 数据

全部代码如下, 可以测试上述推论:     分别测试存在移动语义和不存在的情况看看调用如何??

namespace tyj {
	class string {
		typedef char* iterator;
		typedef const char* const_iterator;
	public:
		const_iterator begin() const {
			return _str;
		}
		const_iterator end() const {
			return _str + _size;
		}
		iterator begin() {
			return _str;
		}
		iterator end() {
			return _str + _size;
		}
		string(const char* s = "") 
			: _size(strlen(s))
			, _capacity(_size)
			, _str(new char[_size]) {
			//std::cout << "string(const char* s) ---构造对象" << std::endl;
		}
		//提供一个swap 函数 一切都是为了方便后序的资源转移拷贝构造等等复用代码
		void swap(string& s) {
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0) {
			//复用代码
			string tmp(s._str);
			swap(tmp);
			std::cout << "string(const string& s) --- 深拷贝" << std::endl;
		}

		string& operator=(const string& s) {
			//复用拷贝构造代码
			string tmp(s);
			swap(tmp);
			std::cout << "string& operator=(string s) --- 深拷贝" << std::endl;
			return *this;
		}

		//然后右值引用出现了, 出现了另外一种方式, 叫做移动构造
		string(string&& s) 
			:_str(nullptr)
			,_size(0)
			,_capacity(0) {		
			swap(s);
			std::cout << "string(string&& s) ---移动构造" << std::endl;
		}

		string& operator= (string&& s) {
			swap(s);
			std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl;
			return *this;
		}
		
		void reserve(size_t n) {		//扩容
			if (n <= _capacity) return;
			char* pTemp = new char[n];
			_capacity = n;
			memcpy(pTemp, _str, _size + 1);	//拷贝_size + 1个过去, 结束字符也拷贝过去
			delete[]_str;
			_str = pTemp;
		}

		void push_back(char c) {
			if (_size == _capacity) {
				reserve(_capacity > 0 ? (_capacity << 1) : 8);
			}
			_str[_size++] = c;		//放入数据
			_str[_size] = 0;		//后序制结束
		}

	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};

	//来一个函数, 方便测试右值引用使用案例
	tyj::string to_string(int val) {
		tyj::string s;		
		bool flag = 0;
		if (val < 0) {
			val *= -1;
			flag = 1;
		}
		while (val) {
			s.push_back(val % 10);
			val /= 10;
		}
		if (flag) s.push_back('-');
		std::reverse(s.begin(), s.end());
		return s;			
	}	
}

int main() {
	tyj::string s1 = tyj::to_string(123456);
	tyj::string s2;
	s2 = tyj::to_string(23456);
	return 0;
}
  • STL 容器中全部都是怎加了移动构造和移动赋值的:  如下图:

 就连 STL的  push_back 等等这种接口上都是同样增添了右值引用版本的:

 

 四.  万能引用  +  完美转发

万能引用:  就是 既可以引用左值  也可以引用右值      模板中的&& 万能引用

为了引出完美转发  首先先看如下的一段代码

void f(int& a) {
	std::cout << "左值引用" << endl;
}

void f(const int& a) {
	std::cout << "const 左值引用" << endl;
}

void f(int&& a) {
	std::cout << "右值引用" << endl;
}

void f(const int&& a) {
	std::cout << "const 右值引用" << endl;
}

template <class T>
void PerfectForward(T&& t) {
	f(t);
}

int main() {
	PerfectForward(2);
	int a = 10;
	PerfectForward(a);
	PerfectForward(move(a));
	return 0;
}
  •   结果不尽人意,  全部都是左值引用?????   为啥  仅仅经过了一次参数  t  接收之后t 退化了 退化成了左值?????
  • 前面我们学过 右值一旦 被引用之后就可以取地址了, 其实也就自然退化为左值了, 这个时候需要调用 std::forward<类型>()  进行完美转发, 保持之前的类型属性不退化
void PerfectForward(T&& t) {
	//先尝试一下不是完美转发
	//f(t);
	//然后进行完美转发
	f(std::forward<T>(t));	//转发之后效果就恢复正常了
}
  • 万能引用  + 完美转发,  在 过程中保持住  右值属性不退化
  • 接下来就是完美转发在实际案例中的使用场景
template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};

//如下是实际的测试案例
template<class T>
class List
{
	typedef ListNode<T> Node;	
public:
	List()
	{
		_head = new Node;			//搞一个虚拟头部
		_head->_next = _head;
		_head->_prev = _head;		//双向循环
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));	//完美转发
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<tyj::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	while (1);
	return 0;
}

//上述所有的 传入 && 右值引用作为参数的地方后序进一步传参全部需要使用forward<>()完美转发
//完美转发保持之前原有的类型属性不变, 如果不使用完美转发效果就是后序全部变成左值引用退化了
//可以取地址的就是左值了, 不可以取地址的才是右值, 右值一旦被变量接收其实也就退化成左值了
//如果想要继续保持右值的属性就需要完美转发

五. 可变参数模板  (参数包)

  • C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
  • 简单的理解可以理解为一个参数包, 可以支持传入数量不固定的参数, 而且还是模板, 使用起来更加的灵活
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

模板参数包的简单使用.  第一种解包方式, 递归解包

//设置递归终点, 当参数包解包完全, 适配空包
void ShowList() {
	cout << endl;
}
//递归形式调用解包, 每一次解出一个参数
template<class T, class ...Args > 
void ShowList(T& val, Args... args) {
	cout << val << endl;
	ShowList(args...);
}
int main() {
	ShowList("dsadsa", 2, 5, 6, "edsad");
	return 0;
}
  • 解包方式2:  利用数组结合 {}  初始化的方式 进行解包
template<class T>
int PrintArg(T& val) {
	cout << val << endl;
	return 0;
}
template<class ...Args>
void ShowList(Args... args) {
	int arr[] = { PrintArg(args)... };
}
int main() {
	ShowList(1, 43, 6, 7, 8, "dfsads", "dsaw", 'a');
	return 0;
}

六. emplace_back 的出现和对比分析 push_back接口  emplace_back 是 结合这 可变模板参数出现的

int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的tyj::string,来试试
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实效率也还好, 差别不算很大
	std::list< std::pair<int, tyj::string> > mylist;
	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));
	mylist.push_back(make_pair(30, "sort"));
	mylist.push_back({ 40, "sort" });
	return 0;
}

七. Lambda表达式

 如下, 先进行一个简单的使用

struct Cars {
	int carnum;
	int price;
	string name;
};

struct cmp {
	bool operator()(Cars& c1, Cars& c2) {
		return c1.price < c2.price;
	}
};

bool cmp2(const Cars& c1, const Cars& c2) {
	return c1.price < c2.price;
}
int main() {
	auto fun = [] {};		//这个是最为简单的lambda表达式啥都不干
	fun();					//调用, 使用方式像极了无参仿函数调用
	auto add = [](double a, double b)->double { return a + b; };
	cout << add(2.7, 3.7) << endl;
	//然后是常用方式:  代替 仿函数使用
	Cars cars[] = { 
	  {100, 150000, "长城"}
	, {55, 20000, "宝马摩托"}
	, {455, 1000, "小电瓶"}
	, {1000, 500, "自行车"}};
	//形式1:
	sort(cars, cars + sizeof(cars) / sizeof(Cars)
		, [](Cars& c1, Cars& c2)->bool {return c1.price < c2.price; });
	//形式2:
	sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp());	//传入匿名可调用对象
	//形式3:
	sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp2);	//传入函数指针
	return 0;
}
  • 上述我们利用了 lambda 表达式来代替了仿函数的使用, 这个也是lambda表达式的常用形式之一, 在很多时候都可以见到上述的这种使用场景...  但是有没有思考过为什么吗? 
  • 可调用对象(仿函数). 函数指针, lambda表达式   底层处理是否是类似 或者说甚至一样的???
  • 其实 lambda表达式的底层处理就是完全处理成了仿函数的.

  • 其实本质上 lambda的底层只是有很多的修饰, 如果把修饰看成是类名 本质就完全是类的 operator() 的重载 仿函数的底层处理形式了

  •  侯杰老师在讲解 lambda底层的时候也曾阐述 lambda 的底层处理就是按照仿函数, 当作类来进行处理的

针对对于自定义对象的 sort 还有一点点小小的技巧, 可以在我们需要 sort 的自定义类中去重载一下operator < 函数,  直接不需要在自己传入排序规则了.......

why???   上述 重载一下 operator < 就可以达到重建排序规则   ()  的效果

  •  因为默认调用的其实就是 less<>{}  说白了就是  operator <   验证如下代码:   可以达到同上述一毛一样的效果.
struct Cars {
	int carnum;
	int price;
	string name;

	bool operator<(Cars& c) const {
		return price < c.price;
	}
};
int main() {
	Cars cars[] = {
	  {100, 150000, "长城"}
	, {55, 20000, "宝马摩托"}
	, {455, 1000, "小电瓶"}
	, {1000, 500, "自行车"} };
	sort(cars, cars + sizeof(cars) / sizeof(Cars));
	return 0;
}

八. 包装器 (适配器) (function包装器)

  • function 是 C++中的类模板, 也是一个包装器.
  • 说到包装器, 首先就要思考              函数指针, 仿函数, Lambda表达式
  • 上章就提到了 三者底层可能差不大多, 使用的情景也是各有雷同, 包装器 其实就可以算是将上述三者进行一个统一, 适配成一个东西    如下 : function 包装器可以实现对三者的统一包装
//函数指针
int add1(int a, int b) {
	return a + b;
}

//仿函数
struct Add {
	int operator()(int a, int b) {
		return a + b;
	}
	int a, b;
};
int main() {
	auto add2 = [](int a, int b){ return a + b; };	//当然可以在()->指定后置返回类型
	//auto add2 = [](int a, int b)->int { return a + b; };	
	function<int(int, int) > func1 = add1;		//函数名
	function<int(int, int) > func2 = Add();		//函数对象
	function<int(int, int) > func3 = add2;		//lambda表达式
	std::cout << func1(3, 5) << std::endl;
	std::cout << func2(3, 5) << std::endl;
	std::cout << func3(3, 5) << std::endl;
	while (1);
	return 0;
}
  • 思考一个问题,  为什么需要 function 这个包装器, 直接使用三者不可以吗???

包装器的好处????    统一了可调用对象的类型, 并且指定了参数和返回值类型
1. 简化了函数指针这样的复杂指针的使用, 函数指针复杂难以理解
2. 方便了作为参数时候的传入
3. 仿函数是一个类名没有指定参数和返回值需要知道就需要去看这个operator () 重载获取
4. lambda 在语法层, 看不到类型, 底层存在类型, 但是也是lambda_uuid, 也很难看

我觉得function 出现的 最最重要的原因就是有了一个确切的类型,  使用简单方便, 

解决函数指针使用复杂的问题, 解决仿函数不能指定参数类型的问题,  要知道参数类型还要跑去看哪个 operator()     以及解决    lambda没有具体类型的问题. 

实际案例:

150. 逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。

有效的算符包括 +-*/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

未使用 function代码:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i=0;i<tokens.size();i++){
            if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
                int rhs=st.top();
                st.pop();
                int lhs=st.top();
                st.pop();
                switch(tokens[i][0]){
                    case '+':st.push(lhs+rhs);break;
                    case '-':st.push(lhs-rhs);break;
                    case '*':st.push(lhs*rhs);break;
                    case '/':st.push(lhs/rhs);break;
                }
                continue;
            }
            st.push(stoi(tokens[i]));         
        }
        //然后就是最后的结果了
        return st.top();
    }
};

使用 function 的代码:

class Solution {
    //使用包装器进行复用???  如何利用包装器??
    //需要的是 function 和  对应的 op 对应起来...
    //如何对应 使用的就是map 对应  map<string, function<int(int, int) > > opmap
    //逆波兰表达式:  左 右 op
    //遇到 op 的时候 说明前面的就是 l + r
    //每一个运算结果需要重新入栈
public:
    int evalRPN(vector<string>& tokens) {
        stack<int > numst;
        map<string, function<int(int, int) > > opmap = {
            {"+", [](int a, int b)->int{ return a + b;}} ,
            {"-", [](int a, int b)->int{ return a - b;}} ,
            {"*", [](int a, int b)->int{ return a * b;}} ,
            {"/", [](int a, int b)->int{ return a / b;}} 
        };
        for (auto& e : tokens) {
            if (e == "+" || e == "-" || e == "*" || e == "/") {
                int r = numst.top(); numst.pop();
                int l = numst.top(); numst.pop();       //先提取的是r 后 l
                numst.push(opmap[e](l, r));
            } else {
                numst.push(stoi(e));
            }
        }
        return numst.top();
    }
};

九. 线程库

 简单的用起来

int main() {
	size_t n = 100;
	thread t1([n]{
		for (size_t i = 0; i < n; i += 2) {
			cout << i << endl;
		}
	});
	cout << t1.get_id() << endl;		//线程id  
	thread t2([n]{
		for (size_t i = 1; i < n; i += 2) {
			cout << i << endl;
		}
	});
	cout << t2.get_id() << endl;		//线程id  
	t1.join();
	t2.join();			//主线程阻塞等待子线程的死亡
	while (1);			//等待他们结束
	return 0;
}
  • . 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的 状态。
  •  当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针    lambda表达式     函数对象
void TFun() {
	cout << "函数指针" << endl;
}
struct TF {
	void operator()() {
		cout << "函数对象" << endl;
	}
};
int main() {
	thread t1(TFun);		//传入函数指针
	TF tf;
	thread t2(tf);			//可调用对象(仿函数)
	thread t3([]() {cout << "Lambda" << endl; });
	t1.join();
	t2.join();
	t3.join();              //join 主线程挂起等待三个线程结束返回
	return 0;
}
  • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个 线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用jion或者detach结束

线程函数参数 

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。   (线程函数参数传入是以值拷贝的形式拷贝到栈空间中的, 所以既是是引用类型, 在线程中修改后外部实参也是无法修改的)   如何处理这个问题, 如下代码解释

std::ref();    使用这个函数转换之后传入的线程函数参数才是真正的引用, 线程中改变, 外面也会改变

class Fun {
public:
	void operator()() {
		cout << "operator()" << endl;
	}
};

void ThreadFunc1(int& x)
{
	x += 10;
}
void ThreadFunc2(int* x)
{
	*x += 10;
}

int main() {
//测试一波:
	int a = 10;
	thread t1(ThreadFunc1, a);			//传入a 
	cout << a << endl;					//?? a 是否改变?
	//上述发现 a 没有改变
	//如何可以使得传入的数据不需要进行拷贝, 而是原有数据?
	 如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t3(ThreadFunc1, std::ref(a));//才不会传入拷贝本
	cout << a << endl;
	thread t2(ThreadFunc2, &a);			//这样看一看???
	cout << a << endl;					//a改变了, 因为这个传入的是地址进去
	t1.join(); 
	t2.join();
	return 0;
}

十. 原子操作

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
//多线程对于共享数据的写操作带来的问题...    

unsigned long num = 0L;			//先定义全局的数据
void tf(size_t n) {
	for (size_t i = 0; i < n; ++i) {
		num += 1;
	}
}

int main() {
	thread t1(tf, 10000000);
	thread t2(tf, 10000000);
	t1.join();
	t2.join();	
	cout << num << endl;
	return 0;
}
  • 发现一个大问题, 上述根本无法获取我们想要的结果甚至, 每一次运行结果都是不一样的

解决上述问题的方式1: 在 C++98 中采取的是加锁的方式实现避免函数的重入问题, 

lock();

操作临界资源  (写入操作)

unlock();

unsigned long num = 0L;			//先定义全局的数据
mutex mtx;
void tf(size_t n) {
	for (size_t i = 0; i < n; ++i) {
		mtx.lock();
		num += 1;
		mtx.unlock();
	}
}

int main() {
	thread t1(tf, 10000000);
	thread t2(tf, 10000000);
	t1.join();
	t2.join();	
	cout << num << endl;
	return 0;
}

加锁确实是可以解决上述的问题, 但是不停的解锁解锁, 效率会变得特别低,  时间消耗也会大大增加, 不停的加锁解锁, 虽然也解决了问题, 保护了临界资源..  但是程序运行时延性大大增加, 而且对于锁控制不好还会死锁, 于是C++11 搞出来一个原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

atomic_long num{ 0 };//定义全局的原子操作数据

void tf(size_t n) {
	for (size_t i = 0; i < n; ++i) {
		num += 1;
	}
}
int main() {
	thread t1(tf, 10000000);
	thread t2(tf, 10000000);
	t1.join();
	t2.join();
	cout << num << endl;
	return 0;
}

有了原子操作数据, 确实针对这些数据的操作不再需要加锁保护了, 但是如果是一段代码段的原子操作, 就还是不得不使用锁来实现, 但是只要设计到锁就可能发生死锁, C++11为了预防死锁, C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。

  • unique_lock   和  lock_guard  都是对于锁的一种封装模板类, 实现对于锁的管理, 

  •  然后接下来是一个简单的实现
// RAII
namespace tyj
{
	template<class Lock>
	class lock_guard
	{
	public:
		lock_guard(Lock& lock)
			:_lock(lock)
		{
			_lock.lock();
			cout << "加锁" << endl;
		}

		/*void lock()
		{
		_lock.lock();
		}

		void unlock()
		{
		_lock.unlock();
		}*/
        //对于lock_guard是没有上述操作的, 它仅仅只是做垃圾回收
        //出作用域自动回收锁, 调用析构解锁
		~lock_guard()
		{
			_lock.unlock();
			cout << "解锁" << endl;
		}

		lock_guard(const lock_guard<Lock>& lock) = delete;

	private:
		Lock& _lock;
	};
}
  • 只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数 成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁 问题。
  • 向比较  lock_guard 而言, unique_lock 提供了更多的操作
  1. 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  2. 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)

条件变量引入以及条件变量对象和互斥对象配合实现一个案例

 要求 : 支持两个线程交替打印,一个打印奇数,一个打印偶数

int main() {
	mutex mtx;	  //定义锁 为后面的完成要求做准备
	bool flag = 1;//flag = 1 打印偶数  flag = 0 打印奇数 配合 condion_variable使用
	condition_variable _cond;	//定义条件变量为后序相互耦合关联式打印埋伏笔
	thread t1([&](){
		unique_lock<mutex> _lock(mtx);
		int i = 0;
		while (i < 100) {
			while (!flag) _cond.wait(_lock);//不满足flag 一直等
			//说明满足了
			cout << "i: " << i << endl;
			flag = 0;		   //修改让t2去打印
			_cond.notify_one();//唤醒t2打印奇数了
			i += 2;
		}
	});

	thread t2([&](){
		unique_lock<mutex> _lock(mtx);
		int j = 1;
		while (j < 100) {
			while (flag) _cond.wait(_lock);//满足flag 说明这个时候在打印偶数
			//说明满足了
			cout << "j: " << j << endl;
			flag = 1;		   //修改让t2去打印
			_cond.notify_one();//唤醒t2打印奇数了
			j += 2;
		}
	});
	t1.join();
	t2. join();
	return 0;
}

十一. 总结本章

  • 首先本章介绍了初始化参数列表{} 进行统一的初始化
  • {} 的本质是一个类型 叫做 initializer_list , 支持使用{} 构造的本质是支持传入initializer_list做参数的构造函数
  • 然后引入了右值引用, 可以取地址的是左值, 右值是不可以去地址的值, 一旦给右值取别名, 右值就会退化, 就会分配空间 + 地址 退化为左值
  • 然后通过右值引用引出移动构造,  移动构造相比拷贝构造好处体现在深拷贝上面, 他和深拷贝不同的是不需要重新开底层的存储空间  + 转移数据, 直接窃取右值的底层空间
  • 模板右值引用: 万能引用,   引用接收之后所有的右值会退化为左值, 想要保持住右值属性不退化, 需要进行  std::forward<>()完美转发, 保持右值属性
  • 然后是 lambda表达式的引出, [捕获列表](参数列表)->后置返回类型{函数体} 且lambda表达式的底层处理就是 类的可调用对象      operator() 运算符重载 
  • function 包装器 对于 函数指针  仿函数    lambda表达式的统一封装....   包装:  好处, 使用起来更加方便,  指定好了参数和返回值类型, 作为参数传入也更加方便灵活...
  • thread 线程类库,  C++11 支持的线程库参数的传入以值拷贝形式, 要想传入的是真正的引用 必须进行 std::ref()处理
  • 原子操作:   创建了一套原子操作数据类型 atomic_long 等等以atomic开头的支持原子操作的数据类型, 相比 使用mutex 更加高效, 且不会死锁
  • 但是由于对于代码段的原子操作,   原子操作的数据类型   无能为力, 只能使用 mutex, 使用锁为了避免死锁, C++11  产生了 锁的管理模板类  unique_lock 和 lock_guard 进行管理锁, 在 对象结束的时候调用析构解锁, 不至于一直死锁
  • 因为一直使用锁, 效率极低, 所以 可以使用  condition_variable 配合锁使用完成一些特殊的要求, 以及提高效率, 不至于让系统一直不停的加锁解锁,  因为加锁解锁 耗费CPU资源
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++11新特性总结 的相关文章

  • JSON.Net 反序列化返回“null”

    我正在使用 JSON Net 反序列化 JSON 字符串 JSON 字符串是 string testJson Fruits Apple color red size round Orange Pro
  • 如何使用 ASP.NET MVC 编辑多选列表?

    我想编辑一个如下所示的对象 我希望用 UsersGrossList 中的一个或多个用户填充 UsersSelectedList 使用 mvc 中的标准编辑视图 我只得到映射的字符串和布尔值 下面未显示 我在 google 上找到的许多示例都
  • c# 从另一个类中的另一个静态事件引发事件

    需要帮助从另一个班级调用事件 我有已声明事件的课程 public class MxPBaseGridView GridView public event AddNewItemsToPopUpMenuEventHandler AddNewIt
  • 锁定 ASP.NET 应用程序变量

    我在 ASP NET 应用程序中使用第三方 Web 服务 对第 3 方 Web 服务的调用必须同步 但 ASP NET 显然是多线程的 并且可能会发出多个页面请求 从而导致对第 3 方 Web 服务的同时调用 对 Web 服务的调用封装在自
  • 叮当错误?命名空间模板类的朋友

    以下代码在 clang 下无法编译 但在 gcc 和 VS 下可以编译 template
  • 司机和提供商之间的区别

    数据库中的驱动程序和提供程序有什么区别 有没有解释一下 不胜感激 样本 ADO NET driver for MySQL vs providerName System Data EntityClient 来自 MSDN 论坛 驱动程序是安装
  • C 中“complex”的默认类型

    根据我读过的文档 C99 和更高版本的支持float complex double complex and long double complex作为复杂类型 但是 此代码在使用时编译时不会发出警告gcc Wall Wextra inclu
  • C# 编译器数字文字

    有谁知道 C 编译器数字文字修饰符的完整列表 默认情况下 声明 0 使其成为 Int32 声明 0 0 使其成为 Double 我可以在末尾使用文字修饰符 f 来确保某些内容被视为 Single 例如像这样 var x 0 x is Int
  • 静态类与类的实例

    我有一个静态类 用于访问我的公共属性 整个应用程序的全局属性 和我在应用程序运行期间使用的方法 例如 我在静态类中设置了一些属性 并且在应用程序运行时我可以从属性中获取值 但我可以使用单例模式创建非静态类并以相同的方式使用它 问题 对于我的
  • 在 C++11 中移出 stdpriority_queue 的元素

    最小的工作示例 include
  • 如何使用 Roslyn 通过扩展方法、静态类中的方法以及带有 ref/out 参数的方法来访问调用

    我正在致力于创建一个开源项目 用于创建 NET UML 序列图 该项目利用名为 js sequence diagrams 的 javascript 库 我不确定 Roslyn 是适合这项工作的工具 但我想我应该尝试一下 所以我整理了一些概念
  • 如何在win32中使用GetSaveFileName保存文件?

    我编写此代码是为了获取 fileName 来保存我的文件 include stdafx h include
  • 用于连接 DataTable 上的动态列的动态 LINQ

    我目前遇到的情况不确定如何继续 我有两个从数据库填充的数据表 我还有一个可用的列名称列表 可用于将这两个数据表连接在一起 我希望编写一组 LINQ 查询 这些查询将 显示两个数据表中的行 内部联接 用于从一个数据表更新另一个数据表 显示一个
  • 如何使用 CSI.exe 脚本参数

    当你运行csi exe 安装了 Visual Studio 2015 update 2 您将得到以下语法 Microsoft R Visual C Interactive Compiler version 1 2 0 51106 Copyr
  • 为什么 f(i = -1, i = -1) 是未定义的行为?

    我正在读关于违反评估顺序 http en cppreference com w cpp language eval order 他们举了一个令我困惑的例子 1 如果标量对象上的副作用相对于同一标量对象上的另一个副作用是无序的 则行为未定义
  • 如何在dll级别读取app.config? [复制]

    这个问题在这里已经有答案了 我在一个解决方案中有一个控制台应用程序项目和库项目 dll The 图书馆项目有 app config 文件 我在其中存储我在库中使用的一些键值对 控制台应用程序引用此 dll 我有另一个 app config
  • Xamarin.Forms UWP 项目中标题栏和选项卡之间令人恼火的空白

    我几乎是新手Xamarin Forms我正在开发一个相当简单的跨平台应用程序 该应用程序在 Android 中显示得足够好 但在 UWP 中却出现了一个愚蠢的空白 该项目由一个 TabbedPage 组成 其中包含 4 个 Navigati
  • 如何将 int 作为“void *”传递给线程启动函数?

    我最初有一个用于斐波那契变量数组的全局变量 但发现这是不允许的 我需要进行基本的多线程处理并处理竞争条件 但我无法在 pthread 创建中将 int 作为 void 参数提供 我尝试过使用常量指针 但没有成功 由于某些奇怪的原因 void
  • 如何在c linux中收听特定接口上的广播?

    我目前可以通过执行以下操作来收听我编写的简单广播服务器 仅广播 hello int fd socket PF INET SOCK DGRAM 0 struct sockaddr in addr memset addr 0 sizeof ad
  • SQL Server“未找到网络路径”在不同环境中随机且不频繁地发生

    类似 如果不是同一个问题 随机遇到网络路径未找到异常 https stackoverflow com questions 38696448 network path not found exception encountered rando

随机推荐

  • 计算机组成原理期末考试题

    计算机组成原理期末考试试题及答案 一 选择题 1 完整的计算机系统应包括 D A 运算器 存储器和控制器 B 外部设备和主机 C 主机和实用程序 D 配套的硬件设备和软件系统 2 计算机系统中的存储器系统是指 D A RAM存储器 B RO
  • 交换机端口安全

    文章目录 一 802 1X认证 1 定义和起源 2 认证方式 本地认证 远程集中认证 3 端口接入控制方式 基于端口认证 基于MAC地址认证 二 端口隔离技术 1 隔离组 2 隔离原理 3 应用场景 首先可以看下思维导图 以便更好的理解接下
  • MinIO文件服务器快速部署和案例演示

    MinIO文件服务器 文章目录 前言 服务器部署 依赖配置 SpringBoot配置 方法模板类 方法使用示例 易错点和注意点 MinIO的文件上传时文件类型设置的坑 前言 这个MinIO服务用起来比较简单 配置和使用都非常快 1 博客案例
  • css设置滚动条样式

    废话不多说 直接上代码 1 设置全局滚动条样式 webkit scrollbar 滚动条整体样式 width 4px 高宽分别对应横竖滚动条的尺寸 height 1px webkit scrollbar thumb 滚动条里面小方块 bor
  • 传智书城项目修改(修改图片、文本),使用eclipse修改, 以及解决出现的乱码问题

    在开始本教程先 请确保已经导入项目并能在网页中显示 导入项目参考http t csdn cn Bl0JX 一 修改首页顶部 首页效果如下 在项目文件中找到head jsp文件 在右侧的布局文件代码中修改首页的文字和首页logo 修改logo
  • weston设置

    weston设置 屏幕旋转180度方法 修改标题栏位置 启动配置文件 屏幕旋转180度方法 编辑 etc xdg weston weston ini文件 增加如下语句 output name DSI 1 transform 180 其中na
  • buuctf-Web1

    在登陆界面用sql注入的各种手段都没用 注册进去看看 抓包后发现数据包有点像xss 也能使它弹窗 但是好像没什么用 用标题1 会有关于sql语句的弹窗 但是如果抓包就不会出现 也能说明是单引号闭合 1 就不行 能知道是sql注入也不容易 闭
  • 用Python制作一个随机抽奖小工具

    大家好 我是才哥 最近在工作中面向社群玩家组织了一场活动 需要进行随机抽奖 参考之前小明大佬的案例 再结合自己的需求 做了一个简单的随机抽奖小工具 今天我就来顺便介绍一下这个小工具的制作过程吧 先看效果 1 核心功能设计 针对随机抽奖的小工
  • 自动控制原理知识点梳理——1. 自动控制的一般概念 & 2. 控制系统的数学模型

    目录 1 自动控制的一般概念 1 1知识梳理 逻辑图 2 控制系统的数学模型 2 1知识梳理 逻辑图 2 2补充内容 2 2 1传递函数的零点和极点 2 2 2典型环节及其传递函数 2 2 3相同的特征多项式和开环传递函数定义 2 2 4由
  • minecraft正版多人服务器,我的世界:“服务器的潜规则”,有的保护玩家,也有的打破平衡...

    多人联机 陪伴多数玩家的青春 点点滴滴 都是付出的时间与精力 如今 MC的 服务器 玩法愈来愈丰富 规则越来越多 那么 服务器内究竟有何潜规则呢 服务器控制台权限 gt OP权限 多数玩家都有这样的认为 OP权限在服务器中处在顶尖地位 那到
  • 税务大比武网络攻防复习(完整版)

    目录 信息化建设与管理 计算机终端设备 通信与网络 数据管理与应用 软件开发 计算与存储 基础设施保障 网络安全 网络安全基础 网络安全管理 密码学 软件开发安全 主机 数据库 中间件安全 网络与通信安全 网络攻击 信息收集 口令攻击和软件
  • uniapp小程序练手项目并上线

    如题 做一个自己的小程序 并在各大小程序开发者平台上线 背景 因没有小程序开发经验 且前端知识掌握得不好 作为一个小程序爱好者 总想有一款自己的小程序 同时也想有一款自己的app 虽然目前还没有实现 在整体难度上来说 小程序的实现比app的
  • 对数器的使用----bug测试,文章中含有测试源码

    当我们选择排序没有出现问题时 我们可以发现一个随机数组是有序的 会打印出 选择排序没有毛病 说明此时选择排序没有毛病 package cn Text public class LogarithmicDetector TODO 选择排序 pu
  • 2023省赛 飞机降落(dfs)

    看数据量 fact 10 3628800 直接暴力dfs include
  • firefly 搭建

    基本内容在http blog csdn net wangqiuyun article details 11150503里都有 在此仅记录搭建工程中遇到的问题及实际项目添加的工具 mysql安装 最好用5 6 服务启动不成功 基本是配置的问题
  • vue插件开发以及发布到npm流程——消息提示插件

    最近有兴趣学习了一下vue插件开发 以及发布到npm上 项目里可以直接使用自己开发的插件 以下文章以开发一个消息提示的组件为例 记录一下开发的流程 一 vue项目创建配置 创建vue项目的指令不多说 直接以下指令 这里我选择的vue2进行开
  • 校园二手物品交易系统微信小程序设计

    系统简介 本网最大的特点就功能全面 结构简单 角色功能明确 其不同角色实现以下基本功能 服务端 后台首页 可以直接跳转到后台首页 用户信息管理 管理所有申请通过的用户 商品信息管理 管理校园二手物品中所有的商品信息 非常详细 违规投诉管理
  • c语言源码解释,C语言一些细节注意(源码+解释)

    最近可能要回归底层开发设计 所以又看了看C的一些东西 顺便对一些问题进行了代码确认 现将代码贴出 希望对各位网友有所帮助 只是为了测试 没有按照什么规范写 代码风格比较烂 哈哈哈哈 大家见谅了 O
  • org.apache.hadoop.hbase.client.RetriesExhaustedException: Can't get the locations

    Hbase API操作表时报出异常 Exception in thread main org apache hadoop hbase client RetriesExhaustedException Can t get the locati
  • C++11新特性总结

    目录 一 统一的列表初始化 适用于各种STL容器 二 类型推导 auto 和 decltype的出现 三 右值引用移动语义 特别重要的新特性 四 万能引用 完美转发 五 可变参数模板 参数包 六 emplace back 的出现和对比分析