C++11:新特性(右值引用、移动语义、lambda表达式、线程库)

前言

C++98是C++标准的第一个版本,以模板的方式重写C++标准库,引入了STL(标准模板库)。C++11则带来了数量可观的变化,其中包含了约140个新特性(正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库、右值引用和移动语义、lamber表达式),以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。

相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率

一:列表初始化

在C++98中,标准允许使用花括号{ }对数组元素进行初始化。但对于一些自定义的类型(vector、list、map)却无法使用这样的方式进行初始化。导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。

C++11 扩大了初始化列表的使用范围,使其可用于所有的内置类型和用户自定义的类型 ,使用初始化列表时,可添加等号(=),也可不添加。

代码示例:

int main(){
	// C++11
	vector<int> v1 = { 1, 2, 3, 4, 5, 6 };
	vector<int> v2{1, 2, 3, 4, 5, 6};

	list<int> l1 = { 1, 2, 3, 4, 5, 6 };
	list<int> l2{ 1, 2, 3, 4, 5, 6 };

	map<string, int> m1 = { { "苹果", 1 }, { "西瓜", 2 } };
	map<string, int> m2{ { "苹果", 1 }, { "西瓜", 2 } };
}

容器可以支持大括号列表初始化,本质是给类增加了一个带有 initializer_list 类型参数的构造函数,可以接收大括号列表的内容。

二:变量类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,这里就需要变量的类型推导来使程序通过编译,并且更加的整洁。

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

2.1 decltype类型推导

decltype类型推导是一种运行时类型识别(RTTI)

C++98中已经支持了RTTI,typeid只能查看类型不能用其结果类定义类型,dynamic_cast只能应用于含有虚函数的继承体系中,运行时类型识别的缺陷是降低程序运行的效率。

代码示例:

int main(){
	int a = 10;
	int b = 20;
	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a+b) c;
	cout<<typeid(c).name()<<endl;
	return 0;
}

注意:函数若带参数则推导函数返回值的类型,若函数没有带参数则推导函数的类型。

三:基于范围的for循环

C++98中遍历一个数组,我们需要说明这个数组的范围来进行遍历操作,但对于一个有范围的集合来讲,由程序员来说明循环的范围是没有必要的,并且还容易出错

C++11中就引入了基于范围的for循环,for循环后的括号由 “:” 分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

代码演示:

void TestFor(){
	int array[] = { 1, 2, 3, 4, 5 };
	for(auto& e : array)
		e *= 2;
	for(auto e : array)
		cout << e << " ";
	return 0;
}

原理:

基于范围的for循环会被编译器替代成为迭代器,也就是支持迭代器就支持范围for(原生指针也被认为迭代器)

四:final与override

final修饰类,类就变成了最终类,不能被继承。final修饰虚函数,则这个虚函数不能被重写。

override修饰子类重写的虚函数,检查是否完成重写,若不满足重写的条件则报错。

五:新容器

C++98中的容器: string、vector、list、deque、map、set、bitmap、stack、queue、priority_queue。

C++11中的新容器:
array(定长数组):存储数据的空间在栈上,栈上的空间并不充足,并且数组定长。
forward_list(单链表):不支持反向迭代,不支持尾插尾删、不支持在当前位置的前驱插入。
unordered_map / unordered_set:底层是哈希表,效率高于map和set。

六:默认成员函数控制

6.1 显示缺省函数

有时声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象,C++11让程序员可以控制默认成员函数是否需要编译器生成。

在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;

int main(){
	A a1(10);
	A a2;
	a2 = a1;
	return 0;
}

6.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;
};

注意:避免删除函数和explicit一起使用。

七:右值引用

C++98中就提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。

左值: 通常是变量。
右值: 通常是常量,表达式,或者函数返回值等临时对象。

代码示例:

普通左值引用不能引用右值,const左值引用可以引用右值

int main(){
	// 普通类型引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用不能引用左值,但右值引用可以引用move后的左值

int main(){
	// 右值引用不能引用左值
	int a = 0;
	int x = 1;
	int y = 2;
	int&& n = x + y;
	// int&& m = a;
	// 右值引用可以引用move后的左值
	int&& m = move(a);
}

既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?让我们一起来看看下面的内容。

7.1 移动语义

C++11将右值分为:纯右值和将亡值

纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象

右值引用将亡值进行深拷贝的代价就会有些大,需要将将亡值与普通值进行区分,以减少没必要的拷贝。

代码演示:

class String{
public:
	String(const char* str = ""){
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	// s2(s1)
	String(const String& s){
		cout<<"String(const String& s)-深拷贝-效率低"<<endl;

		_str = new char[strlen(s._str) + 1];
		strcpy(_str, s._str);
	}

	// s3(右值-将亡值)
	String(String&& s)
		:_str(nullptr)
	{
		// 传过来的是一个将亡值,反正你都要亡了,我的目的是跟你有一样大的空间,一样的值
		// 不如把你的控制和只给我
		cout << "String(String&& s)-移动拷贝-效率高" << endl;
		swap(_str, s._str);
	}

	String& operator=(const String& s){
		if(this != &s){
			char* newstr = new char[strlen(s._str)+1];
			strcpy(newstr,s._str);
			delete[] _str;
			_str = newstr;
		}
		return *this;
	}

	String& operator=(String&& s){
		swap(_str,s._str);
		return *this;
	}
	
	~String(){
		delete[] _str;
	}
private:
	char* _str;
};

String f(const char* str){
	String tmp(str);
	return tmp; // 这里返回实际是拷贝tmp的临时对象
}

int main(){
	String s1("左值");
	String s2(s1);                      // 参数是左值
	String s3(f("右值-将亡值"));        // 参数是右值-将亡值(传递给你用,用完我就析构了)

	return 0;
}

结论:所有做深拷贝的类,都可以加两个右值引用做参数的移动拷贝和移动赋值。

左值引用和右值引用本质的作用都是减少拷贝提高效率。
右值引用可以弥补左值引用不足的地方,右值引用做参数和做返回值减少拷贝的本质是利用了移动构造和移动赋值。

左值引用:
做参数:void push(T x) -> void push(const T& x) 减少传参过程中的拷贝
做返回值:T func() -> T& func() 减少返回过程中的拷贝(返回对象出了作用域不存在了就不能传引用)

右值引用:
做参数:void push(T&& x) 使得push内部不再将x拷贝构造到容器空间上,而是采用移动构造
做返回值:解决的是传值返回接收返回值的问题,利用移动构造减少拷贝

7.2 完美转发

右值引用会在第二次之后的参数传递过程中右值属性丢失,下一层调用会全部识别为左值。

这时候C++11就引入了完美转发的概念,完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

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

void Fun(int &x){cout << "lvalue ref" << endl;}
void Fun(int &&x){cout << "rvalue ref" << endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}

template<typename T>
// C++11 通过forward函数实现完美转发
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main(){
	PerfectForward(10); // rvalue ref
	int a;
	PerfectForward(a); // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref
	const int b = 8;
	PerfectForward(b); // const lvalue ref
	PerfectForward(std::move(b)); // const rvalue ref
	return 0;
}

八:lambda表达式

每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C11语法中出现了Lambda表达式。

lambda表达式其实是一个匿名函数

8.1 lambda表达式语法

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

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

代码示例:

// lambda表达式
// [捕捉列表](参数列表)multable->返回值类型{函数体}
#include
using namespace std;

// 捕捉:传值捕捉 传引用捕捉
// 传值捕捉的对象是不能被改变的,需要加上mutable属性就可以改变了。

int main(){
	int a = 0, b = 1;
	// 实现a+b的lambda表达式
	auto add1 = [](int x, int y)->int{return x + y; };
	cout << add1(a, b) << endl;

	auto add2 = [a, b]()->int{return a + b; };
	cout << add2() << endl;

	// 实现a和b的交换
	auto swap1 = [](int& x, int& y){
		int z = x;
		x = y;
		y = z;
	};
	swap1(a, b);

	auto swap2 = [&a, &b]()mutable{
		int c = a;
		a = b;
		b = c;
	};
	swap2();

	return 0;
}

8.2 lambda表达式原理

函数对象又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

C++11:新特性(右值引用、移动语义、lambda表达式、线程库)_第1张图片
实际在底层编译器对于lambda表达式的处理方式,完全就是按照仿函数的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),operator()的参数和实现,就是我们写的lambda表达式的参数和实现

九:线程库

在C++11之前,涉及到多线程的问题,往往是和平台有关系的,windows和linux下各有自己的接口,这使得代码的可移植性比较差

C++98中如果想使得多线程的程序在Windows和Linux下都能运行,那么可以用条件编译(#ifdef _WIN32) 来解决这个问题。

C++11中提供了线程库,这使得C++在并行编程时不需要依赖第三方库,可跨平台,面向对象封装的类(每个线程都是一个类对象)原理其实是在封装库时使用了条件编译,底层分别调用了不同平台的线程API

多线程最主要的问题就是共享数据带来的线程安全问题,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

#include 
#include 
using namespace std;

void fun(size_t num){
	for (size_t i = 0; i < num; ++i)
	sum++;
}

int main(){
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	return 0;
}

C++98中传统的解决方式:
可以对共享修改的数据可以加锁保护。虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

C++11中引入了原子操作:
不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。可以使用atomic类模板,定义出需要的任意原子类型。

代码实现:

#include
#include
#include
using namespace std;

// 仿函数
atomic<int> x = 0;
struct Add{
	void operator()(int n){
		for (int i = 0; i <= n; i++){
			++x;
		}
	}
};
int main(){
	Add add;
	thread th1(add, 1000000);
	thread th2(add, 1000000);

	th1.join();
	th2.join();
	return 0;
}

//lambda表达式
int main(){
	atomic<int> x = 0;
	auto add = [&x](int n){
		for (int i = 0; i < n; i++){
			++x;
		}
	};
	thread th1(add, 1000000);
	thread th2(add, 1000000);

	th1.join();
	th2.join();
	return 0;
}

你可能感兴趣的:(C++,c++11,右值引用,移动语义,lambda,线程库)