C++11新特性总结

  • Post author:
  • Post category:其他




1. 列表初始化


1.1 内置类型的列表初始化

	// 内置类型变量
	int x1 = { 10 };
	int x2{ 10 };
	int x3 = 1 + 2;
	int x4 = { 1 + 2 };
	int x5{ 1 + 2 };
	
	// 数组
	int arr1[5]{ 1,2,3,4,5 };
	int arr2[]{ 1,2,3,4,5 };
	
	// 动态数组,在C++98中不支持
	int* arr3 = new int[5]{ 1,2,3,4,5 };

	// 标准容器
	vector<int> v{ 1,2,3,4,5 };
	map<int, int> m{ {1,1}, {2,2,},{3,3},{4,4} };


1.2 自定义类型的列表初始化

class Point
{
public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	{}
private:
	int _x;
	int _y;
};
int main()
{
	Point p[10]{ { 1, 2 } ,{ 1, 2 }, { 1, 2 }, { 1, 2 } };
	return 0;
}



2.变量类型推导

  • auto可以根据变量初始化表达式类型推导变量的实际类型。但是auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。


decltype类型推导

  • decltype是根据表达式的实际类型推演出定义变量时所用的类型
void* GetMemory(size_t size) {
	return malloc(size);
}
int main()
{
	int a = 10;
	int b = 20;

	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a + b) c;
	cout << typeid(c).name() << endl;

	// 如果没有带参数,推导函数的类型
	cout << typeid(decltype(GetMemory)).name() << endl;
	// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
	cout << typeid(decltype(GetMemory(0))).name() << endl;
	return 0;
}



3. 范围for循环

vector<int> arr{ 1,2,3,4,5,6,7,8,9 };
for (auto e : arr)
	cout << e << " ";



4. final与override


1. final:修饰虚函数,表示该虚函数不能再被重写

class Car 
{ 
public:    
	virtual void Drive() final {} 
};
 
class Benz :public Car 
{ 
public:    
	//编译报错
	virtual void Drive() {cout << "Benz-舒适" << endl;} 
};


2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{ 
public:    
	virtual void Drive(){} 
};
 
class Benz :public Car { 
public:
	//如果没有构成重写,编译时会报错    
	virtual void Drive(int num) override {cout << "Benz-舒适" << endl;} 
}; 



5. 智能指针


详情请看我另一篇博客



6. 新增加容器—静态数组array、forward_list以及unordered系列


unordered系列



7. 委派构造函数

  • 委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更加容易。
  • 所谓委派构造函数:就是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式。
class Info {
public:
	// 目标构造函数
	Info() 
		: _type(0)
		, _a('a')
	{
		InitRSet();
	}
	// 委派构造函数
	Info(int type) 
		: Info()
	{
		_type = type;
	}
	// 委派构造函数
	Info(char a) 
		: Info()
	{
		_a = a;
	}
private:
	void InitRSet() { 
		//初始化其他变量 
	}
private:
	int _type = 0;
	char _a = 'a';
	//...
};
  • 在初始化列表中调用”基准版本”的构造函数称为委派构造函数,而被调用的”基准版本”则称为目标构造函数。

  • 注意

    :构造函数不能同时”委派”和使用初始化列表。



8. 默认成员函数控制


8.1 显式缺省函数

  • 在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A {
public:
	A(int a) 
		: _a(a)
	{}
	// 显式缺省构造函数,由编译器生成
	A() = default;
	// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
private:
	int _a;
};

A& A::operator=(const A& a) = default;


8.2 删除默认函数

  • 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class A {
public:
	A(int a) 
		: _a(a)
	{}
	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator=(const A&) = delete;
private:
	int _a;
};



9. 右值引用


9.1 右值引用概念

  • 为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用
int Add(int a, int b) {
	return a + b;
}
int main()
{
	const int&& ra = 10;

	// 引用函数返回值,返回值是一个临时变量,为右值
	int&& rRet = Add(10, 20);
	return 0;
}


9.2 左值与右值

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
	return g_a;
}
int main()
{
	int a = 10;
	int b = 20;
	// a和b都是左值,b既可以在=的左侧,也可在右侧,
	// 说明:左值既可放在=的左侧,也可放在=的右侧
	a = b;
	b = a;

	const int c = 30;
	// 因为可以对c取地址,因此c严格来说不算是右值
	cout << &c << endl;
	//无法将右值引用绑定到左值
	//const int&& t = c;

	// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	//b + 1 = 20;

	GetG_A() = 100;
	return 0;
}


区分左值与右值,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。


总结:

  1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断。


C++11对右值进行了严格的区分:

  • C语言中的纯右值,比如:a+b, 100
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。


9.3 引用与右值引用比较

  • 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
  • C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
	// 10纯右值,本来只是一个符号,没有具体的空间,
	// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	int&& r1 = 10;
	r1 = 100;
	
	int a = 10;
	// 编译失败:右值引用不能引用左值
	//int&& r2 = a; 


9.4 值的形式返回对象的缺陷

  • 如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
class String
{
public:
	String(const char* str = "");
	String(const String& s);
	String& operator=(const String& s);
	String operator+(const String& s)
	{
		char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(pTemp, _str);
		strcpy(pTemp + strlen(_str), s._str);
		String strRet(pTemp);
		return strRet;
	}
	~String();
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2("world");
	String s3(s1 + s2);
	return 0;
}
  • 在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?


9.5 移动语义

  • C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

    在这里插入图片描述
  • 在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:
String(String&& s)
 : _str(s._str)
{ 
 s._str = nullptr;
}
  • 因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。


注意:

  1. 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
  2. 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。


9.6 右值引用引用左值

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


注意:

  1. 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
  2. STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
String s1("hello world");
String s2(move(s1));

  • 注意

    :以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。

使用move的一个例子:

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


9.7 完美转发


完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x) {
	cout << "int" << endl;
}
void Func(char x) {
	cout << "char" << endl;
}
//...
template<class T>
void PerfectForward(T t) {
	Func(t);
}
  • PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美。
  • 完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销。所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。
  • 这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

C++11通过forward函数来实现完美转发, 比如:

void Fun(int &x) { 
	cout << "int &x" << endl; 
}
void Fun(int &&x) { 
	cout << "int &&x" << endl; 
}
void Fun(const int &x) { 
	cout << "const int &x" << endl; 
}
void Fun(const int &&x) { 
	cout << "const int &&x" << endl; 
}
template<typename T>
void PerfectForward(T &&t) { 
	Fun(std::forward<T>(t)); 
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const左值
	PerfectForward(std::move(b)); // const右值
	return 0;
}


9.8 右值引用作用

  1. 实现移动语义(移动构造与移动赋值)
  2. 给中间临时变量取别名
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
string&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
  1. 实现完美转发



10. lambda表达式


10.1 lambda表达式语法


lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

  1. lambda表达式各部分说明

    • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
    • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
    • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
    • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
    • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。


注意

: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;

	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;
	cout << b << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	cout << x << endl;
  1. 捕获列表说明

    捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

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


注意:


a. 父作用域指包含lambda函数的语句块

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

  • 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空。

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

f. lambda表达式之间不能相互赋值,即使看起来类型相同

void(*PF)();
int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };
	
	// 编译失败--->提示找不到operator=()
	//f1 = f2; 

	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();

	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();
	return 0;
}


10.2 仿函数与lambda表达式

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);

	// lamber
	auto r2 = [=](double monty, int year)->double {return monty * rate*year; };
	r2(10000, 2);
	return 0;
}
  • 函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

在这里插入图片描述

在这里插入图片描述

  • 实际在底层编译器对于lambda表达式的处理方式,完全就是按照仿函数的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。



11. 线程库


11.1 thread类的简单介绍

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


注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
std::thread t1;

//输出为0, 没有线程
cout << t1.get_id() << endl;
  1. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

    • 函数指针
    • lambda表达式
    • 函数对象
void ThreadFunc(int a) {
	cout << "Thread1: " << a << endl;
}
class TF
{
public:
	void operator()(int a)
	{
		cout << "Thread3: " << a << endl;
	}
};
int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);

	// 线程函数为lambda表达式
	thread t2([](int a) {
		cout << "Thread2: " << a << endl; 
	}, 20);

	// 线程函数为函数对象
	TF tf;
	thread t3(tf, 30);

	t1.join();
	t2.join();
	t3.join();
	cout << "Main thread!" << endl;
	return 0;
}
  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。
  2. 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

    • 采用无参构造函数构造的线程对象
    • 线程对象的状态已经转移给其他线程对象
    • 线程已经调用join或者detach结束


11.2 线程函数参数


线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

void ThreadFunc1(int& x) {
	x += 10;
}
void ThreadFunc2(int* x) {
	*x += 10;
}
int main()
{
	int a = 10;
	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
	//thread t1(ThreadFunc1, a);
	//t1.join();
	//cout << a << endl;

	// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	cout << a << endl;

	// 地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;
}

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


11.3 join与detach

  • join()方式

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

  • detach()方式

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

    detach()函数一般在线程对象创建好之后就调用,因为如果不是join()等待方式结束,那么线程对象可能会在新线程结束之前被销毁掉而导致程序崩溃。因为std::thread的析构函数中,如果线程的状态是jionable,std::terminate将会被调用,而terminate()函数直接会终止程序。

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


11.4 原子性操作库(atomic)

  • 虽然加锁可以解决线程安全问题,但是加锁有一个缺陷就是:只要一个线程在对临界资源修改时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
  • 因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。头文件

    <atomic>



    在这里插入图片描述
atomic_long sum{ 0 };
void fun(size_t num) {
	for (size_t i = 0; i < num; ++i)
		sum ++; // 原子操作
}
  • 在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
  • 原子类型通常属于”资源型”数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atomic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
	atomic<int> a1(0);
	// 编译失败
	//atomic<int> a2(a1); 
	atomic<int> a2(0);
	// 编译失败
	//a2 = a1; 


11.5 lock_guard与unique_lock

  • 有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
  • C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
  • lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
  • 与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:


    • 上锁/解锁操作

      :lock、try_lock、try_lock_for、try_lock_until和unlock

    • 修改操作

      :移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)

    • 获取属性

      :owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。



版权声明:本文为qq_42837885原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。