浅谈C++11新特性

相较于C++98/03,C++11带来了数量可观的新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。


前言

C++11能更好的用于系统开发和库开发,语法更加简化,更加稳固和安全,可以很大程度上提升开发效率


一、列表初始化

1.C++98

可以使用花括号 {} 对数组元素进行统一的列表初始值设定,但对于自定义类型却无法使用 {} 进行初始化。

	int arr[] = { 1, 2, 3, 4, 5 };//C++98支持
	vector<int> iv = { 1, 2, 3, 4, 5 };//C++98不支持无法通过编译

C++11扩大了用花括号 {} 括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户自定义类型,使用初始化列表时,可添加 = ,也可不添加,究其原因是C++11底层增加了initializer_list类型。

2.C++11内置类型和自定义类型的列表初始化

	int x = { 1 };
	int x1{ 1 };
	int x2{ 1 + 2 };
	int arr[]{1, 2, 3, 4, 5};
	int* arr1 = new int[5]{1, 2, 3, 4, 5};//动态数组
	vector<int> iv{ 1, 2, 3, 4, 5 };
class rec
{
public:
	rec(int x = 0, int y = 0) :
		_l(x), _w(y)
	{}
private:
	int _l;
	int _w;
};
int main()
{
	rec r{ 1, 2 };
	return 0;
}

多个对象的列表初始化:
多个对象想要支持列表初始化,需要给该类(模板类)添加一个带有initialler_list类型参数的构造函数即可。

initialler_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()以及size()。

#include <initializer_list>
template <class Type>
class SeqList
{
public:
	SeqList(const initializer_list<Type> &list) :
		size(0), capacity(list.size())
	{
		base = new Type[capacity];
		for (const auto&e : list)
			base[size++] = e;
	}
private:
	Type *base;
	size_t capacity;
	size_t size;
};
int main()
{
	SeqList<int> sq{ 1, 2, 3, 4, 5 };
	return 0;
}

二、变量类型推导

当我们定义变量时,原则上必须先给出变量的实际类型,编译器才允许定义,但有些情况下我们可能不会事先知道实际类型怎么给,或者他的类型比较复杂,在C++11中,我们可以使用auto关键字来根据变量初始化表达式类型推导变量的实际类型。

	auto x = 10;
	int x1 = 10;

在这里插入图片描述
函数参数不可以使用auto来推导


decltype类型推导
如果有时需要根据表达式运行完成后的结果的类型进行推导,因为在编译期间,代码不会运行,所以此时的auto也就无能为力。而decltype是根据表达式的实际类型推演出定义变量时所用的类型。

	int a = 10;
	int b = 5;
	decltype(a + b) c;
	cout << typeid(c).name() << endl;

在这里插入图片描述
也可以推导函数返回值的类型

int add(int a, int b)
{
	return a + b;
}
int main()
{
	cout << typeid(decltype(add(1,2))).name() << endl;
	return 0;
}

在这里插入图片描述


三、范围for循环

	vector<int>iv{ 1, 2, 3, 4, 5 };
	for (auto & e : iv)
	{
		cout << e << " ";
	}

在这里插入图片描述


四、final与override

这两个关键字是帮助用户检测是否进行函数重写。

final:修饰虚函数,表示该虚函数不能再被继承;
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。


五、智能指针

这块内容单独写一篇博客总结


六、新增加容器

静态数组array、forward_list、unordered系列


七、默认成员函数控制

在C++中对于一个空的类,编译器会自动生成一些默认成员函数,例如构造,析构,拷贝构造,赋值重载,移动构造,移动拷贝构造等等,如果类中已经显示定义了,那么编译器将不会生成默认函数,在C++11中程序员可以自行控制是否需要编译器生成这些默认函数。

1.显示缺省函数

在C++11中,可以在默认函数定义或者声明时加上=的default,从而显示的只是编译器生成该函数的默认版本,他、用=default修饰的函数称为显示缺省函数。

class A
{
public:
	A() = default;//等同于A(){}
	A(int a) :_a(a)
	{}
private:
	int _a;
};

int main()
{
	A a1();//调用default修饰的默认构造
	A a2(1);
	return 0;
}

2.删除默认函数

刚好和default相反,在C++11中,在该默认函数声明上加上=delete即可限制该函数的生成,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

	A(const A&) = delete;//禁止生成默认的拷贝构造

注意要避免删除函数和explicit(显示转换)一起使用


八、右值引用

1.what右值引用?

引用可以参考之前的博客引用

使用引用可以提高程序的可读性,为了提高程序运行效率,C++11引入了右值引用,右值暨等号右边的值,右值只能作为等号右边,而不能跑到等号左边。右值引用也是别名,但其只能对右值引用。

	int a = 10;//a为左值
	int &b = a;//常规引用
	const int &c = 10;//常引用
	int &&d = 10;//右值引用
int add(int a, int b)
{
	int value = a + b;
	return value;
}
int main()
{
	int && ret = add(10, 20);
	return 0;
}

在这里插入图片描述

2.左右值的区分

一般可以认为:可以放在等号左边(也能放右侧),或者能够取地址的称为左值,只能放在等号右边,或者不能取地址的称为右值。

区分:
1.普通类型的变量,可以取地址,为左值;
2.const修饰的常量,不可修改,只读类型,理论上应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),所以C++11认为其是左值;
3.如果表达式的运行结果是一个临时变量或者对象,认为是右值;
4.如果表达式运行结果或单个变量是一个引用则认为是左值;
总结:
1.不能简单根据等号两边来判断左右值;
2.能得到引用的表达式一定能够作为引用,否则就用常引用;

C++11对右值进行了严格的区分:
C语言中的纯右值:a+b,100;
将亡值。比如表达式的中间结果,函数按照值的方式返回。

注意:普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值,C++11中右值引用:只能引用右值,一般情况不能直接引用左值。

3.移动语义

解决按值返回对象的缺陷(空间浪费问题)

移动语义暨将一个对象中资源移动到另一个对象中的方式。
举个栗子:

namespace ljl
{
	class String{
	public:
		String(char* str = "")
		{
			if (str == nullptr)
				str = "";
			_str = new char[strlen(str) + 1];
			strcpy(_str, str);
		}
		String(const String & s):
			_str(new char[strlen(s._str)+1])
		{
			strcpy(_str, s._str);
		}
		String operator+(const String &s)
		{
			char* tmp = new char[strlen(_str) + strlen(s._str) + 1];
			strcpy(tmp, _str);
			strcpy(tmp + strlen(_str), s._str);
			String strret(tmp);
			return strret;
		}
	private:
		char *_str;
	};
}

请注意上述代码中的重载+的代码
在这里插入图片描述
引入C++11中移动构造进行改进:将strret中资源转移到临时对象中。

		String(String && s) :_str(s._str)
		{
		//右值引用的移动语义
			s._str = nullptr;
		}

在这里插入图片描述
因为临时对象也是右值,所以继续调动移动构造,构造s3,节省了相对的2个空间,而且提高了程序运行效率。

注意:移动构造函数的参数千万不能设置为const类型,否则资源不会进行转移;在C++11中,编译器会默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显示定义自己的移动构造。

4.右值引用引用左值

在有些场景下,可能需要用右值去引用左值实现移动语义,当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。在C++11中,std::move()函数位于头文件中,他并不搬移任何东西,只有一个功能就是将一个左值强制转换为右值引用,然后实现移动语义。

误用实例:

string s1("hello");
string s2(move(s1));
string s3(s2);

move将s1转化为右值后,在实现s2的拷贝时就会调用移动构造,此时s1的资源就会被转移到s2中,s1称为无效字符串。

正确使用实例:

class Person
	{
	public:
		Person(char * name, char* sex, int age)
			:_name(name), _sex(sex), _age(age)
		{}
		Person(const Person& p)
			:_name(p._name)
			, _sex(p._sex)
			, _age(p._age)
		{}
		/*
		Person(Person&& p)
		:_name(p._name)
		,_sex(p._sex)
		,_age(p._age)
		{}
		*/
		Person(Person && p)
			:_name(move(p._name))
			, _sex(move(p._sex))
			, _age(p._age)
		{}
	private:
		String _name;
		String _sex;
		int _age;
	};
	Person GetP()
	{
		Person p("ljl", "male", 18);
		return p;
	}
}
int main()
{
	ljl::Person p(ljl::GetP());
	return 0;
}

分析:
在这里插入图片描述

5.完美转发

完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另外一个函数。
完美就是,函数模板在向其他函数传递自身形参时,如果相应实参是左值,他就应该被转发为左值,如果相应实参是右值,它就应该被转发为右值。
其目的就是保留在其他函数针对转发而来的参数的左右值属性进行不同处理(例如参数为左值是拷贝语义,而当参数为右值时进行移动语义)

C++11通过forward函数来实现完美转发。

6.右值引用的应用

1.实现移动语义;
2.给中间临时变量取别名;
3.实现完美转发。

九、lambda表达式

1.C++98

在C++98中如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法(需要引用算法头文件#include< algorithm>)。

int main()
{
	int arr[] = { 4, 3, 5, 2, 1 };
	int n = sizeof(arr) / sizeof(arr[0]);
	for (auto& e : arr)
		cout << e << " ";
	cout << endl;
	sort(arr, arr + n);//默认升序
	for (auto& e : arr)
		cout << e << " ";
	cout << endl;
	sort(arr, arr + n, greater<int>());//利用仿函数进行降序排序
	for (auto& e : arr)
		cout << e << " ";
	cout << endl;
	return 0;
}

在这里插入图片描述
但是当待排序元素为自定义类型时,需要用户定义排序时的比较规则:

struct Goods
{
	string _name;
	double _price;
};
struct Compare1
{
	bool operator()(const struct Goods  &g1, const struct Goods &g2)
	{
		return g1._price < g2._price;
	}
};

int main()
{
	Goods gds[] =
	{
		{ "苹果", 2.1 },
		{ "香蕉", 3 },
		{ "橙子", 2.5 },
	};
	int n = sizeof(gds) / sizeof(gds[0]);
	sort(gds,gds+n,Compare1());
	return 0;
}

在这里插入图片描述
由上我们可以看到,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

2.lambda表达式

说白了lambda表达式省略了我们之前自定义的仿函数类。

sort(gds, gds + n, [](const struct Goods &g1,
		const struct Goods &g2)->bool
	{return g1._price < g2._price; });

上面的代码和C++98的效果一样。

表达式语法:
在这里插入图片描述
匿名的函数对象

int main()
{
	auto fun = [](int a, int b)->int {return a + b; };
	cout << fun(10, 20) << endl;
	cout << typeid(fun).name()<<endl;
	return 0;
}

在这里插入图片描述
注:返回值类型在明确的情况下,可以省略由编译器推导,在無返回值时刻直接省略。如果不需要传递参数,则可以将()直接省略。因此最简单的lambda表达式为[]{}。

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

捕获列表说明:
捕获列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。
[var]:表示值传递方式捕捉变量var;

int main()
{
	int x = 10;
	int y = 20;
	auto fun = [x, y]//需要捕获这两个变量,除非变量是全局的
	{
		return x + y;
	};
	cout << fun(x,y) << endl;
	return 0;
}

[&var]表示引用传递捕捉变量var;
[&]:表示引用传递捕捉所有父作用域中的变量(包括this);
[this]:表示值传递方式捕捉当前的this指针;

class A
{
public:
	int fun(int x, int y)
	{
		auto f = [this](int x, int y)->int
		{
			return this->a + this->b + x + y;
		};
		return f(x, y);
	}
private:
	int a = 1;
	int b = 2;
};
int main()
{
	A a;
	cout << a.fun(10, 20) << endl;
	return 0;
}

在这里插入图片描述
注:
父作用域包含lambda函数的语句块;
语法上捕捉列表可有多个捕捉项组成,并以逗号分隔;
捕捉列表不允许变量重复传递,否则就编译错误;
在块作用域以外的lambda函数捕捉列表必须为空;
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会出错;
lambda表达式之间不能相互赋值。

十、线程库

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差,C++11中最重要的特性就是对线程进行支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入原子类的概念,要是用标准库中的线程,必须包含 < thread > 头文件。

thread th;//创建一个没有关联任何线程函数的线程
函数名功能
thread()构造一个线程对象,没有关联任何线程函数,暨么有启动任何线程;
thread(fun,args1,agrs2…)构造一个线程对象,并关联线程函数fun,args1和args2为线程函数的参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程
join()该函数调用后会阻塞主线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程“死活”就与主线程无关

解释:
1.当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
①.函数指针;
②.lambda表达式;
③.函数对象。

void thread_fun()
{
	for (int i = 0; i < 10; ++i)
	{
		cout << "子线程" << endl;
	}
}
int main()
{
	thread th1(thread_fun);//创建一个关联thread_fun函数的线程
	for (int i = 0; i < 10; ++i)
	{
		cout << "主线程" << endl;
	}
	th1.join();//阻塞主线程
	return 0;
}
void thread_fun1(int a)
{
	cout << "线程1" <<"  "<< a << endl;
}
class threadfun3
{
public:
	void operator()()
	{
		cout << "线程3" << endl;
	}
};
int main()
{
	thread th1(thread_fun1,10);
	thread th2([]{cout << "线程2" << endl; });
	threadfun3 th;
	thread th3(th);
	th1.join();
	th2.join();
	th3.join();
	cout << "主线程" << endl;
	return 0;
}

在这里插入图片描述

void thread_fun()
{
	cout << "子线程 id" << this_thread::get_id() << endl;
}
int main()
{
	cout << "主线程 id" << this_thread::get_id() << endl;
	thread th(thread_fun);
	th.join();
	return 0;
}

在这里插入图片描述

void thread_fun()
{
	cout << "子线程 id" << this_thread::get_id() << endl;
}
int main()
{
	cout << "主线程 id" << this_thread::get_id() << endl;
	thread th(thread_fun);
	th.detach();//分离线程
	cout << "子线程 id" << th.get_id() << endl;
	//th.join();
	return 0;
}

在这里插入图片描述

注意:主线程和子线程竞争执行;
主线程在所有子线程退出之后才退出

thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行

	thread th(thread_fun);
	thread th1 = move(th);//利用move函数将th变为右值
	th1.join();//因为已经转移到th1所以等待th1

可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效——①采用无参构造函数构造的线程对象;②线程对象的状态已经转移给其他线程对象;③线程已经调用jion或者detach结束。

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因此其实际引用的是线程栈中的拷贝,而不是外部实参。
如果想要修改参数值,则按照之前我们对函数参数的理解可以传地址也就是线程参数为指针类型。

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

class Test
{
public:
	void fun()
	{
		cout << "Test::fun()" << endl;
	}
public:
	int m_a = 10;
};

int main()
{
	Test t;
	thread th(&Test::fun,&t);//传t对象地址(this),传类成员函数地址
	th.join();
	return 0;
}

join与detach

线程库给我们两种回收线程所使用的资源的选择:

1.join()方式
主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程对象只能使用一次join(),否则程序会崩溃。

注意:当主线程在被join阻塞前结束,则该子线程会造成资源泄漏。因此,采用join方式结束线程时,应注意其位置。为了避免因此问题而导致程序异常,可采用RAII的方式对线程对象进行封装。

class mythread
{
public:
	explicit mythread(thread &t) :m_t(t)
	{}
	~mythread()
	{
		if (m_t.joinable())//当线程可用时
			m_t.join();//
	}
	mythread(mythread const&) = delete;//不能拷贝
	mythread& operator=(const mythread &) = delete;//不能赋值
private:
	thread &m_t;
};

2.detach()方式
detach()函数被调用后,新线程与线程对象分离,不在被线程对象所表达,就不能通过线程对象控制线程,但线程会在后台运行,其所有权将会交给C++运行库。同时,C++运行库保证,当线程退出时,其相关资源能够正确的回收。

因此:线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式将线程与线程对象分离。

线程安全问题

因为线程之间是竞争执行,倘若共享数据不是只读的,当多个线程同时修改共享数据时,就会产生很多潜在的麻烦。比如说,主线程创建了两个子线程,两个子线程共享主线程的资源,当其中一个子线程对资源进行修改时,另外一个线程恰恰在读取数据。

long shared = 0;//全局数据

void fun(size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		shared++;
	}
}
int main()
{
	cout << "修改前:" << shared << endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();
	cout << "修改后: " << shared << endl;
	return 0;
}

在这里插入图片描述
按理说应该会加到2000000,为什么会造成上面这种现象?
举个栗子,当shared为0时,在t1线程对shared进行++的时候,恰巧t2线程抢夺了cpu打断了t1的++操作而自己对shared进行++,此时shared为1,然后t1又争抢回cpu资源,进行++,但t1依然认为此时的shared为0,对其++之后为1。

C++98对上述问题采取的措施是对共享资源进行加锁保护:

mutex t;//初始化互斥锁
long shared = 0;//全局数据

void fun(size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		t.lock();//上锁
		shared = shared+1;
		t.unlock();//解锁
	}
}

在这里插入图片描述
互斥锁可以解决问题,但是加锁有一个缺陷就是:只要一个线程对共享资源进行操作时,其他线程会被阻塞,会影响程序运行效率。而且如果控制不好锁的使用,会造成死锁。

在C++11中引入了原子操作——不可被中断的一个或一系列的操作,C++11引入原子操作类型(仅对一些基础整型如int,char,long等),使得线程间的数据的同步变得非常高效。

#include <atomic>
atomic_long shared = {0};//初始化原子类型

在C++11中,我们还可以使用atomic类模板定义出需要的任意原子类型。 但是不建议,因为此时的效率不如互斥锁。

lock_guard、unique_lock

在多线程环境下,如果想要保证某个变量的安全性,只需要将其设置成原子类型即可,但是如果我们只需要保证一段代码的安全性,那么就只能通过锁来控制线程安全。

mutex t;
long n;
void fun1()
{
	for (int i = 0; i < 1000000; ++i)
	{
		t.lock();
		++n;
		t.unlock();
	}

}
void fun2()
{
	for (int i = 0; i < 1000000; ++i)
	{
		t.lock();
		--n;
		t.unlock();
	}
}

Mutex的种类

在C++11中,Mutex总共封装了四个互斥量的种类:
1.std::mutex
这是最基本的互斥量(互斥锁),该类的对象之间不能拷贝,也不能进行移动,赋值。mutex最常用的三个函数:lock()上锁、unlock()解锁、try_lock()尝试上锁,跟lock的区别是,lock如果已经对临界资源上锁,那么其他线程再执行到lock时会被阻塞等待,直到别的线程解锁。try_lock是如果已经有线程存在于try_lock和unlock形成的临界区,那么其他线程在执行到try_lock时会直接返回加锁失败。

2.std::recursive_mutex
允许同一个线程对互斥量多次上锁(递归上锁),来获得对互斥量对象的多层所有权,释放互斥量是需要调用与该锁层次深度相同次数的unlock()。

3.std::timed_mutex
相较于mutex增加了两个成员函数:
try_lock_for:在一段时间如果没有获得锁则阻塞,如果在此期间其他线程释放锁,则可以加锁,如果超时则返回加锁失败;
try_lock_until():以一个时间点为参数,在该时间未到之前如果没有加锁成功则阻塞,如果在此期间其他线程释放锁,则加锁,如果超时则失败返回。

守卫锁lock_guard

template<class _Mutex>
class lock_guard
{
public:
	//在构造lock_guard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		:_MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	//在构造lock_guard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex & _Mtx, adopt_lock_t)
		:_MyMutex(_Mtx)
	{}
	~lock_guard()_NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁时,只需要实例化一个对象,出作用域前,对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

守卫锁的缺陷:太单一,用户无法对该锁进行控制,因此C++11有提供了unique_lock.

唯一锁unique_lock

也是通过RAII的方式对锁进行封装,并且也是以独占所有权的方式管理mutex对象的上锁解锁操作,暨其对象之间不能发生拷贝,在构造(或移动(move)赋值)时,其对象需要传递一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入的Mutex对象的加锁解锁。
unique_lock操作:
lock、unlock、try_lock、try_lock_for、try_lock_until、移动赋值,交换(swap)、释放(release返回所管理的互斥量的指针,并释放所有权)、owns_lock(检测是否上了锁)、operator bool、mutex(返回当前对象所管理的互斥量的指针)。

  • 18
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值