1、C++11简介
C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
2、列表初始化
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
内置类型的列表初始化
vector<int> v{1, 2, 3};
自定义类型的列表初始化
- 单个对象的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0):_x(x),_y(y)
{}
private:
int _x;
int _y;
};
- 多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
#include<initializer_list>
#include<iostream>
using namespace std;
template<class T>
class Vector
{
public:
Vector(initializer_list<T> l):_capacity(l.size()),_size(0)
{
_array = new T[_capacity];
for(auto e : l)
{
_array[_size++] = e;
}
}
Vector<T>& operator=(initializer_list<T> l)
{
delete[] _array;
_size = 0;
_capacity = l.size();
for(auto e : l)
{
_array[_size++] = e;
}
return *this;
}
private:
T * _array;
size_t _capacity;
size_t _size;
};
int main()
{
Vector<int> v1{1, 2, 3};
Vector<int> v2 = {1, 2};
return 0;
}
3、变量类型推倒
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。
auto使用的前提是,必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。RTTI运行时类型识别的缺陷是降低程序运行的效率。
decltype是根据表达式的实际类型推演出定义变量时所用的类型。
- 推演表达式类型作为变量的定义类型
void test_1()
{
int a = 1;
int b = 2;
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
}
- 推演函数返回值的类型
void * get_memory(size_t size)
{
return malloc(size);
}
void test_2()
{
cout<<typeid(decltype(get_memory)).name()<<endl;
cout<<typeid(decltype(get_memory(1))).name()<<endl;
}
4、基于范围for的循环、final与override、智能指针、array、forward_list和unordered系列(略)
5、委派构造函数
委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更加容易。
初始化列表可以通过类内部成员初始化进行优化,但是构造函数体的重复在C++98中无法解决。委派构造函数将构造函数体中重复的代码提出来,作为一个基础版本,在其他构造函数中调用。
委派构造函数:是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式。
class Info
{
private:
int _type = 0;
char _a = 'a';
private:
void Init(){/*initializer...*/}
public:
Info():_type(0),_a('a')
{Init();}
Info(int type):Info()
{_type = type;}
Info(char a):Info()
{_a = a;}
};
在初始化列表中调用”基准版本”的构造函数称为委派构造函数,而被调用的”基准版本”则称为目标构造函数。构造函数不能同时”委派”和使用初始化列表。
6、默认函数控制
在C++中对于空类编译器会生成一些默认的成员函数,如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
显式缺省函数
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数,由编译器生成。
A() = default;
删除默认函数
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
A(const A&) = delete;
A& operator=(const A&) = delete;
7、右值引用
移动语义
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错。
string getString(char *pstr)
{
string strtmp(pstr);
return strtmp;
}
int main()
{
string s(getString("linux"));
return 0;
}
getString函数返回的临时对象,将s拷贝构造成功之后立马被销毁了(临时对象的空间被释放),再没有其他作用,而s在拷贝构造时,又需要分配空间,一个刚释放一个又申请,有点多此一举。如果将getString返回的临时对象的空间直接交给s,这样s也不需要重新开辟空间了,代码的效率会明显提高。
移动语义与右值引用:将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用。
string(string&& s):_str(s._str)
{}
右值引用
右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。
- 纯右值:纯右值是C++98中右值的概念,用于识别临时变量和一些不跟对象关联的值。比如:常量、一些运算表达式(1+3)等。
- 将亡值:声明周期将要结束的对象。比如:在值返回时的临时对象。
右值引用书写格式:类型&& 引用变量名 = 实体
- 右值引用最常见的一个使用地方就是:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率。
string&& getString(char *pstr)
{
string strtmp(pstr);
return strtmp;
}
int main()
{
string s(getString("linux"));
return 0;
}
- 右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的生命周期。
string getString(char *pstr)
{
return string(pstr);
}
int main()
{
string&& s = getString("linux");
return 0;
}
与引用一样,右值引用在定义时必须初始化。通常情况下,右值引用不能引用左值。
int main()
{
int&& ra;//未初始化,编译失败
int a = 10;
int&& ra = a;//a是左值,编译失败
const int&& ra = 20;//ra是匿名常量20的别名
return 0;
}
std::move()
C++11中,std::move()函数它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。注意:被转化的左值,其生命周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
#include<iostream>
#include<string>
using namespace std;
class person
{
private:
string _name;
string _sex;
size_t _age;
public:
person(char *name, char *sex, size_t age)
:_name(name),_sex(sex),_age(age)
{}
person(const person& p)
:_name(p._name),_sex(p._sex),_age(p._age)
{}
#if 0
person(person&& p)
:_name(p._name),_sex(p._sex),_age(p._age)
{}
#else
person(person&& p)
:_name(move(p._name)),_sex(move(p._sex)),_age(p._age)
{}
#endif
};
person getInstance()
{
person p("Ton", "M", 22);
return p;
}
int main()
{
person p(getInstance());
return 0;
}
注意:move更多的是用在生命周期即将结束的对象上。为了保证移动语义的传递,程序员在编写移动构造函数时,最好使用std::move转移拥有资源的成员。
- 如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现。
string(const string&&);
const person getInstance();
- 在C++11中,无参构造函数/拷贝构造函数/移动构造函数实际上有3个版本
object();
object(const T&);
object(T&&);
- 默认情况下,编译器会为程序员隐式生成一个(如果没有用到则不会生成)移动构造函数。如果程序员声明了自定义的构造函数、移动构造、拷贝构造函数、赋值运算符重载、移动赋值、析构函数,编译器都不会再为程序员生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是按照位拷贝(即浅拷贝)来进行的。因此,在类中涉及到资源管理时,程序员最好自己定义移动构造函数。其他类有无移动构造都无关紧要。在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值,如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义,参数为右值时实施移动语义)。C++11通过forward函数实现完美转发。
#include<iostream>
using namespace std;
void fun(int& x){cout<<"lval ref"<<endl;}
void fun(int&& x){cout<<"rval ref"<<endl;}
void fun(const int& x){cout<<"const lval ref"<<endl;}
void fun(const int&& x){cout<<"const rval ref"<<endl;}
template<typename T>
void perfect_forward(T&& t)
{
fun(forward<T>(t));
}
int main()
{
perfect_forward(10);//lr
int a = 0;
perfect_forward(a);//rr
perfect_forward(move(a));//lr
const int b = 0;
perfect_forward(b);//clr
perfect_forward(move(b));//crr
return 0;
}
8、lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
void test_1()
{
int arr[] = {1, 4, 2, 7, 3, 8, 5, 6, 9, 3, 0, 6};
sort(arr, arr + sizeof(arr)/sizeof(int));
sort(arr, arr + sizeof(arr)/sizeof(int), greater<int>());
for(auto e : arr)
{
cout<<e<<" ";
}
cout<<endl;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则
struct goods
{
string _name;
double _price;
};
struct compare
{
bool operator()(const goods& g1, const goods& g2)
{
return g1._price <= g2._price;
}
};
void test_2()
{
goods g[] = {{"a", 1.2}, {"d", 4.2}, {"b", 2.3} ,{"c", 3.7}};
sort(g, g + sizeof(g)/sizeof(goods), compare());
for(int i = 0; i < sizeof(g)/sizeof(goods); ++i)
{
cout<<g[i]._name<<" ";
}
cout<<endl;
}
lambda表达式语法
void test_3()
{
goods g[] = {{"a", 1.2}, {"d", 4.2}, {"b", 2.3} ,{"c", 3.7}};
sort(g, g + sizeof(g)/sizeof(goods),
[](const goods& g1, const goods& g2)
{
return g1._price <= g2._price;
});
for(int i = 0; i < sizeof(g)/sizeof(goods); ++i)
{
cout<<g[i]._name<<" ";
}
cout<<endl;
}
书写格式:[capture-list](parameters)mutable ->return-type{statement}
- [capture-list]:捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表(可选)。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->return-type:返回值类型(可选)。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
void test_4()
{
[]{};
int a = 3, b = 4;
auto fun0 = [=]{return a + 3;};
cout<<fun0()<<endl;
auto fun1 = [&](int c){b = a + c;};
fun1(10);
cout<<b<<endl;
auto fun2 = [=, &b](int c)->int{return b+= a + c;};
cout<<fun2(10)<<endl;
int x = 10;
auto add_x = [x](int a)mutable->int{x *= 2; return a + x;};
cout<<add_x(10)<<endl;
}
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
lambda表达式捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
父作用域指包含lambda函数的语句块;语法上捕捉列表可由多个捕捉项组成,并以逗号分割,但是捕捉列表不允许变量重复传递,否则就会导致编译错误;在块作用域以外的lambda函数捕捉列表必须为空;在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错;lambda表达式之间不能相互赋值,即使看起来类型相同。
void (*PF)();
void test_5()
{
auto fun1 = []{cout<<"fun1"<<endl;};
auto fun2 = []{cout<<"fun2"<<endl;};
// fun1 = fun2;
auto fun3(fun2);
fun3();
PF = fun2;
PF();
}
函数对象与lambda表达式
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
class Rate
{
private:
double _rate;
public:
Rate(double rate):_rate(rate)
{}
double operator()(double money, int year)
{
return money * year * _rate;
}
};
void test_6()
{
double rate = 0.49;
Rate r1(rate);
cout<<r1(10000.0, 2)<<endl;
auto r2 = [=](double money, int year)->double{return money * year * rate;};
cout<<r2(10000.0, 2)<<endl;
}
在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
9、线程库
C++11中,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件。
#include<iostream>
#include<thread>
using namespace std;
void fun()
{
cout<<"fun thread..."<<endl;
}
int main()
{
thread t(fun);
t.join();
cout<<"main thread..."<<endl;
return 0;
}
线程启动
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给创建线程的函数来实现。
#include<iostream>
#include<thread>
using namespace std;
int main()
{
int a = 12;
int b = 34;
thread t([&](int add_num){a += add_num; b += add_num;}, 50);
t.join();
cout<<a<<" "<<b<<endl;
return 0;
}
线程结束
线程结束时,回收线程资源的两种方法:
- join()
join()会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次只能使用一次join(),当你调用的join()之后joinable()就将返回false了。
#include<iostream>
#include<thread>
using namespace std;
void fun()
{
this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
thread t(fun);
cout<<t.joinable()<<endl;
t.join();
cout<<t.joinable()<<endl;
return 0;
}
- detach()
detach()会从调用线程中分理出新的线程,之后不能再与新线程交互。此时调用joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给C++运行库。同时,C++运行库保证当线程退出时,其相关资源的能够正确的回收。
原子性操作库atomic
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。C++98中传统的解决方式是对共享修改的数据加锁保护。虽然加锁可以解决,但是加锁有一个缺陷就是只要一个线程在保护区时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。
原子操作声明:
#include<atomic>
atomic_类型名 变量名{变量初值};
#include<iostream>
#include<thread>
#include<atomic>
using namespace std;
atomic_long sum{0};
void fun(size_t num)
{
for(size_t i = 0; i < num; ++i)
{
sum++;
}
}
int main()
{
cout<<sum<<endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout<<sum<<endl;
return 0;
}