C++11较C++98的新特性:
7.新增加容器—静态数组array、forward_list以及unordered系列
1.C++的发展过程和历史
C++语言发展大概可以分为三个阶段:
第一阶段从80年代到1995年。这一阶段C++语言基本上是传统类型上的面向对象语言,并且凭借着接近C语言的效率,在工业界使用的开发语言中占据了相当大份额;
第二阶段从1995年到2000年,这一阶段由于标准模板库(STL)和后来的Boost等程序库的出现,泛型程序设计在C++ 中占据了越来越多的比重性。当然,同时由于Java、C#等语言的出现和硬件价格的大规模下降,C++受到了一定的冲击;
第三阶段从2000年至今,由于以Loki、MPL等程序库为代表的产生式编程和模板元编程的出现,C++ 出现了发展历史上又一个新的高峰,这些新技术的出现以及和原有技术的融合,使C++已经成为当今主流程序设计语言中最复杂的一员。
以下是C++ 发展年代列表:
(1).1967 年,Simula 语言中第一次出现了面向对象 (OO) 的概念,但由于当时软件规模还不大,技术也还不太成熟,面向对象的优势并未发挥出来。
(2).1980 年,Smalltalk-80 出现后,面向对象技术才开始发挥魅力。
(3).1979 年,Bjarne Stroustrup 借鉴 Simula 中 “Class” 的概念,开始研究增强 C 语言,使其支持面向对象的特性。 B.Stroustrup 写了一个转换程序 “Cfront” 把 C++ 代码转换为普通的 C 代码,使它在各种各样的平台上立即投入使用。 1983 年,这种语言被命名为 C++
(4).1986 年,B.Stroustrup 出版了 《The C++ Programming Language》第一版,这时 C++ 已经开始受到关注, B.Stroustrup 被称为 C++ 之父(Creator of C++)。
(5).1989 年,负责 C++ 标准化的 ANSI X3J16挂牌成立。1990 年,B.Stroustrup 出版了 《The Annotated C++ Reference Manual》(简称 ARM),由于当时还没有 C++ 标准,ARM 成了事实上的标准。
(6).1990 年, Template(模板) 和 Exception(异常) 加入到了 C++ 中, 使 C++ 具备了泛型编程(Generic Programming)和更好的运行期错误处理方式。
(7).1991 年,负责 C++ 语言国际标准化的技术委员会工作组 ISO/IEC JTC1/SC22/WG21 召开了第一次会议,开始进行 C++ 国际标准化的工作。从此,ANSI 和 ISO 的标准化工作保持同步,互相协调。
(8).1993 年,RTTI(运行期类型识别) 和 Namespace(名字空间) 加入到 C++ 中。1994 年, C++ 标准草案出台。 B.Stroustrup 出版了《The Design and Evolution of C++》(简称 D&E)。
本来,C++ 标准已接近完工,这时 STL(标准模板库) 的建议草案被提交到标准委员会,对 STL 标准化的讨论又一次推迟了 C++ 标准的出台。
(9).1998 年,ANSI 和 ISO 终于先后批准 C++ 语言成为美国国家标准和国际标准。
(10).2000 年,B.Stroustrup 推出了 《The C++ Programming Language》特别版(Special Edition),书中内容根据 C++ 标准进行了更新。
(11).在2003年C++ 标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++ 03这个名字已经取代了C++ 98,称为C++ 11之前的最新C++ 标准名称。不过由于TC1主要是对C++ 98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++ 98/03标准。
(12).2011年,C++ 11 则带来了数量可观的变化,其中包含了约140个新特性,以及对C++ 03标准中约600个缺陷的修正,这使得C++ 11更像是从C++ 98/03中孕育出的一种新语言。相比较而言,C++ 11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
语言的发展是一个逐步递进的过程,C++ 是直接从 C 语言发展过来的,而 C 语言是从 B 语言发展过来的,B 语言是 BCPL 的一个解释性后代,BCPL 是 Basic CPL。其中最有趣的是 CPL 中 C 的由来,由于当时这个语言是剑桥大学和伦敦大学合作开发的,在伦敦的人员加入之前,C 表示剑桥,伦敦人员加入之后,C 表示 Combined 组合。还有一种非正式的说法,C 表示 Christopher,因为 Christopher 是 CPL 背后的主要动力。
最初导致C++诞生的原因是在Bjarne博士等人试图去分析UNIX的内核的时候,这项工作开始于1979年4月,当时由于没有合适的工具能够有效的分析由于内核分布而造成的网络流量,以及怎样将内核模块化。同年10月,Bjarne博士完成了一个可以运行的预处理程序,称之为Cpre,它为C加上了类似Simula的类机制。在这个过程中,Bjarne博士开始思考是不是要开发一种新的语言,当时贝尔实验室对这个想法很感兴趣,就让Bjarne博士等人组成一个开发小组,专门进行研究。
当时不是叫做C++,而是C with class,这是把它当作一种C语言的有效扩充。由于当时C语言在编程界居于老大的地位,要想发展一种新的语言,最强大的竞争对手就是C语言,所以当时有两个问题最受关注:C++ 要在运行时间、代码紧凑性和数据紧凑性方面能够与C语言相媲美,但是还要尽量避免在语言应用领域的限制。在这种情况下,一个很自然的想法就是让C++ 从C语言继承过来,但是我们的Bjarne博士更具有先见之明,他为了避免受到C语言的局限性,参考了很多的语言,例如:从Simula继承了类的概念,从Algol68继承了运算符重载、引用以及在任何地方声明变量的能力,从BCPL获得了//注释,从Ada得到了模板、名字空间,从Ada、Clu和ML取来了异常。
2.列表初始化
1.在C++ 98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
2.但是,C++ 98对于一些自定义的类型,却无法使用这样的初始化,如对vector对象初始化,但是C++ 11可以。
vector<int> v{1,2,3,4,5};
3.C++ 11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。对于自定义类型只要初始化列表和构造函数的参数列表个数相同,就能用列表正常初始化,如下:
#include <iostream>
using namespace std;
class Point2D
{
public:
Point2D(int x = 0, int y = 0) : _x(x), _y(y)
{}
private:
int _x;
int _y;
};
class Point3D
{
public:
Point3D(int x = 0, int y = 0, int z = 0) : _x(x), _y(y), _z(z)
{}
private:
int _x;
int _y;
int _z;
};
int main()
{
Point2D p{ 1,2 };
Point2D p1(1, 2);
Point3D p2{ 1, 2, 3 };
Point3D p3(1, 2, 3);
return 0;
}
4.也可重载一个参数是列表的构造函数,从而达到用列表正常初始化自定义类型,只不过参数列表中的值都是同一种类型。
template<class type>
class Point2D
{
public:
Point2D() = delete;
Point2D(initializer_list<type> list)
{
auto e = list.begin();
_x = *e;
++e;
_y = *e;
}
private:
int _x;
int _y;
};
class Point3D
{
public:
Point3D() = delete;
Point3D(initializer_list<int> list) {
initializer_list<int>::iterator it = list.begin();
_x = *it++;
_y = *it++;
_z = *it;
}
private:
int _x;
int _y;
int _z;
};
int main()
{
Point2D<int> p{ 1,2 };
Point3D p2{ 1, 2, 3 };
return 0;
}
对于自己实现的顺序表,希望用列表初始化,便可以重载一个参数是列表的构造函数
template<class Type>
class SeqList
{
public:
SeqList(size_t sz) : capacity(sz), size(0)
{
base = new Type[capacity];
}
SeqList(initializer_list<Type> list) : capacity(list.size()), size(0)
{
base = new Type[capacity];
for (const auto& e : list)
base[size++] = e;
}
~SeqList()
{
if (base != nullptr)
delete[]base;
base = nullptr;
capacity = size = 0;
}
private:
Type* base;
size_t capacity;
size_t size;
};
int main()
{
SeqList<int> sq{ 1,2,3,4,5,6,7,8,9,10 };
return 0;
}
initializer_list类具有begin,end,size公有成员方法
4.C++ 98不能对new的空间用初始化列表初始化,C++ 11可以,如下代码:
int *pa = new int[5] {1, 2, 3, 4, 5};
3.变量类型推导
auto
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂
不知道需要实际类型怎么给,比如说求和后不知道会不会越界,便可以变量类型自动推导
short a = 32670;
short b = 32670;
short c = a + b; //-196
cout<<c<<endl;
cout<<typeid(c).name()<<endl;
auto x = a + b;
cout << x << endl; //65340
cout << typeid(x).name() << endl; //int
short的取值范围是[-32768, 32767], c越界,而x不会,aoto关键字会推导出x的类型是:int,auto会在保证不越界的情况下自动推导变量类型
类型写起来特别复杂,比如定义和使用集合的迭代器(集合有正向和反向迭代器)
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s = { 3,6,9,3,2,1,5,8 };
//set<int>::iterator it = s.begin();
//set<int>::reverse_iterator rit = s.rbegin();
auto it = s.begin();
auto rit = s.rbegin();
cout << typeid(it).name() << endl;
cout << typeid(rit).name() << endl;
while (it != s.end())
cout << *it++ << " ";
cout << endl;
while (rit != s.rend())
cout << *rit++ << " ";
cout << endl;
return 0;
}
正向迭代器和反向迭代器的类型分别是 :set::iterator,set::reverse_iterator,写起来比较麻烦,因此可以用auto关键字自动推导
注意:
auto是在程序运行期间推断类型的,缺陷是降低程序运行效率
aoto声明的变量必须初始化,否则无法编译通过。
//如下代码无法编译通过
int main() {
auto a;
cout << typeid(a).name() << endl;
return 0;
}
decltype
typeid只能查看变量类型,而不能将结果当作类型名去声明其它变量
,再者auto只能在程序运行期间推断类型效率低。因此出现了decltype关键字,decltype是根据表达式的实际类型推演出定义变量所用的类型,而且是在编译期间推断类型的。
decltype会推导出表达式的合适类型,并且结果可以用于定义其它变量
int main() {
short a = 32670;
short b = 32670;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a + b) c;
cout << typeid(c).name() << endl;
return 0;
return 0;
}
decltype推演函数返回值的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() << endl;
return 0;
}
注意:只写函数名推演的是函数类型,带参数推演的是函数返回值的类型,并且两种推导函数都是没有被执行的。
RTTI运行时类型识别(Run-Time Type Identification) 在C++98中就已经支持了,例如: typeid和dynamic_cast(都是在程序运行时推断)
typeid是获取变量类型名称,文章前面已经使用过了,就不再赘述
dynamic_cast可以获取目标对象的引用或指针,如下:
在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof,以及C#的as、is运算符类似,C++ 提供了dynamic_cast函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast,dynamic_cast提供了类型安全检查,是一种基于能力查询(Capability Query)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast。
T1 obj;
T2* pObj = dynamic_cast<T2*>(&obj);//转换为T2指针,失败返回NULL
T2& refObj = dynamic_cast<T2&>(obj);//转换为T2引用,失败抛出bad_cast异常
基本用法
dynamic_cast可以获取目标对象的引用或指针:
T1 obj;
T2* pObj = dynamic_cast<T2*>(&obj);//转换为T2指针,失败返回NULL
T2& refObj = dynamic_cast<T2&>(obj);//转换为T2引用,失败抛出bad_cast异常
在使用时需要注意:被转换对象obj的类型T1必须是多态类型,即T1必须公有继承自其它类,或者T1拥有虚函数(继承或自定义)。若T1为非多态类型,使用dynamic_cast会报编译错误。下面的例子说明了哪些类属于多态类型,哪些类不是:
//A为非多态类型
class A{
};
//B为多态类型,类内部定义了虚函数
class B{
public: virtual ~B(){}
};
//D为多态类型,公有继承A
class D: public A{
};
//E为非多态类型
class E : private A{
};
//F为多态类型,继承了B类中的虚函数
class F : private B{
}
在多态类型间转换,分为3种类型:
1.子类向基类的向上转型(Up Cast)
2.基类向子类的向下转型(Down Cast)
3.横向转型(Cross Cast)
向上转型是多态的基础,需不要借助任何特殊的方法,只需用将子类的指针或引用赋给基类的指针或引用即可,当然dynamic_cast也支持向上转型,而其总是肯定成功的。而对于向下转型和横向转型来讲,其实对于dynamic_cast并没有任何区别,它们都属于能力查询。为了理解方便,我们不妨把dynamic_cast视为cross cast:
class Shape {
public: virtual ~Shape();
virtual void draw() const = 0;
};
class Rollable {
public: virtual ~Rollable();
virtual void roll() = 0;
};
class Circle : public Shape, public Rollable {
void draw() const;
void roll();
};
class Square : public Shape {
void draw() const;
};
//横向转型失败
Shape *pShape1 = new Square();
Rollable *pRollable1 = dynamic_cast<Rollable*>(pShape2);//pRollable为NULL
//横向转型成功
Shape *pShape2 = new Circle();
Rollable *pRollable2 = dynamic_cast<Rollable*>(pShape2);//pRollable不为NULL
在C++11中,auto是RTTI的,decltype属于编译期间推断的
4.范围for循环
当用for和auto结合去访问常见的容器中的元素时,容器必须支持begin(),end(), 以及迭代器的++操作(也就是必须支持迭代器才行), 如下:
int main()
{
vector<int> v = {1,2,3,4,5,6,7,8,9,10};
//being() end() ++it;
for(const auto &e : v)
cout<<e<<" ";
cout<<endl;
return 0;
}
5.final与override
final:修饰虚函数,表示该虚函数不能再被继承 override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。 可以参考前面的文章:
6.智能指针
如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏。这种问题就叫异常不安全,通过智能指针就能解决。
内存泄漏分类堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针的原理:RAIIRAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
智能指针支持的标准VC6.0平台支持 C++ 98 中的auto_ptr
VC6.0中的实现:
template<class _Ty>
class auto_ptr {
public:
typedef _Ty element_type;
explicit auto_ptr(_Ty *_P = 0) _THROW0()
: _Owns(_P != 0), _Ptr(_P) {}
auto_ptr(const auto_ptr<_Ty>& _Y) _THROW0()
: _Owns(_Y._Owns), _Ptr(_Y.release()) {}
auto_ptr<_Ty>& operator=(const auto_ptr<_Ty>& _Y) _THROW0()
{if (this != &_Y)
{if (_Ptr != _Y.get())
{if (_Owns)
delete _Ptr;
_Owns = _Y._Owns; }
else if (_Y._Owns)
_Owns = true;
_Ptr = _Y.release(); }
return (*this); }
~auto_ptr()
{if (_Owns)
delete _Ptr; }
_Ty& operator*() const _THROW0()
{return (*get()); }
_Ty *operator->() const _THROW0()
{return (get()); }
_Ty *get() const _THROW0()
{return (_Ptr); }
_Ty *release() const _THROW0()
{((auto_ptr<_Ty> *)this)->_Owns = false;
return (_Ptr); }
private:
bool _Owns; //标志是否占有资源
_Ty *_Ptr; //接收堆上新开辟的空间的地址
};
vS开始支持g++(Linux),其中有auto_ptr
//auto_ptr
#include <iostream>
#include <memory>
using namespace std;
/*
memory是C++空间配置器以及new delete定义的头文件,里面定义了空间配置器,
new delete以及一些用于调用构造函数的函数。
*/
class test {
public:
test() {
}
void func() {
cout << "test::func is called" << endl;
}
};
int main() {
int* p = new int(10);
auto_ptr<int> ap(p);
cout << *ap << endl;
*ap = 100;
cout << *p << endl;
test* tP = new test;
auto_ptr<test> ap2Test(tP);
tP->func();
return 0;
}
VS2013之后C++ 11 smart_ptr unique_ptr shared_ptr weak_ptr
C++库中的智能指针都定义在memory这个头文件中#include
VS2013中的实现
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
: _Myptr(_Ptr)
{ // construct from object pointer
}
auto_ptr(_Myt& _Right) _THROW0()
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
auto_ptr(auto_ptr_ref<_Ty> _Right) _THROW0()
{ // construct by assuming pointer from _Right auto_ptr_ref
_Ty *_Ptr = _Right._Ref;
_Right._Ref = 0; // release old
_Myptr = _Ptr; // reset this
}
template<class _Other>
operator auto_ptr<_Other>() _THROW0()
{ // convert to compatible auto_ptr
return (auto_ptr<_Other>(*this));
}
template<class _Other>
operator auto_ptr_ref<_Other>() _THROW0()
{ // convert to compatible auto_ptr_ref
_Other *_Cvtptr = _Myptr; // test implicit conversion
auto_ptr_ref<_Other> _Ans(_Cvtptr);
_Myptr = 0; // pass ownership to auto_ptr_ref
return (_Ans);
}
template<class _Other>
_Myt& operator=(auto_ptr<_Other>& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
template<class _Other>
auto_ptr(auto_ptr<_Other>& _Right) _THROW0()
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right
}
_Myt& operator=(_Myt& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
_Myt& operator=(auto_ptr_ref<_Ty> _Right) _THROW0()
{ // assign compatible _Right._Ref (assume pointer)
_Ty *_Ptr = _Right._Ref;
_Right._Ref = 0; // release old
reset(_Ptr); // set new
return (*this);
}
~auto_ptr() _NOEXCEPT
{ // destroy the object
delete _Myptr;
}
_Ty& operator*() const _THROW0()
{ // return designated value
#if _ITERATOR_DEBUG_LEVEL == 2
if (_Myptr == 0)
_DEBUG_ERROR("auto_ptr not dereferencable");
#endif /* _ITERATOR_DEBUG_LEVEL == 2 */
return (*get());
}
_Ty *operator->() const _THROW0()
{ // return pointer to class object
#if _ITERATOR_DEBUG_LEVEL == 2
if (_Myptr == 0)
_DEBUG_ERROR("auto_ptr not dereferencable");
#endif /* _ITERATOR_DEBUG_LEVEL == 2 */
return (get());
}
_Ty *get() const _THROW0()
{ // return wrapped pointer
return (_Myptr);
}
_Ty *release() _THROW0()
{ // return wrapped pointer and give up ownership
_Ty *_Tmp = _Myptr;
_Myptr = 0;
return (_Tmp);
}
void reset(_Ty *_Ptr = 0)
{ // destroy designated object and store new pointer
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}
private:
_Ty *_Myptr; // the wrapped object pointer
};
C++11的unique_ptr是参照Boost库scoped_ptr来实现的
unique_ptr的实现原理:简单粗暴的防拷贝
C++98防拷贝的方式:拷贝构造和赋值都只声明不实现并且声明成私有
C++11 delete拷贝构造和赋值方法
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象
销毁一个对象引用计数就减一
通过任何一个引用同一份资源的shared_ptr实例访问的都是同一个引用计数
//C++11 shared_ptr
#include <iostream>
#include <memory>
using namespace std;
int main()
{
int* p = new int(10);
int* q = new int(20);
shared_ptr<int> sp1(p);
cout << *sp1 << endl;
cout << "use_count = " << sp1.use_count() << endl;
{
shared_ptr<int> sp2 = sp1;
cout << "use_count = " << sp1.use_count() << endl;
}
cout << "use_count = " << sp1.use_count() << endl;
sp1.reset(q); //重定向引用的资源
cout << *sp1 << endl;
return 0;
}
多线程中shared_ptr对引用计数的改变要保证操作的原子性
并且对临界资源的访问要保证原子性:加锁或者使用原子类型
以下演示对引用计数的改变保证操作的原子性,对临界资源的访问没有保证原子性
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// 1.演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
// 2.演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,main函数的n改大一些概率就变大了,就容易出现了。
// 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程安全问题,将代码中的SharedPtr换成shared_ptr进行测试,可以验证库的shared_ptr,发现结论是一样的。
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
~SharedPtr() { Release(); }
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
// sp1 = sp2
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
int UseCount() { return *_pRefCount; }
T* Get() { return _ptr; }
void AddRefCount()
{
// 加锁或者使用加1的原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release()
{
bool deleteflag = false;
// 引用计数减1,如果减到0,则释放资源
_pMutex->lock();
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
deleteflag = true;
}
_pMutex->unlock();
if (deleteflag == true)
delete _pMutex;
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
mutex* _pMutex; // 互斥锁
};
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
:_year(year), _month(month), _day(day)
{ cout << "Date()" << endl; }
~Date() { cout << "~Date()" << endl; }
int _year;
int _month;
int _day;
};
void SharePtrFunc(SharedPtr<Date>& sp, size_t n)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
SharedPtr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
SharedPtr<Date> p(new Date);
cout << p.Get() << endl;
const size_t n = 999999;
thread t1(SharePtrFunc, ref(p), n);
thread t2(SharePtrFunc, ref(p), n);
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
return 0;
}
循环引用
#include <iostream>
#include <memory>
using namespace std;
template<class T>
struct ListNode
{
public:
~ListNode()
{
cout << "~ListNode()" << endl;
}
T data;
shared_ptr<ListNode<T>> m_prev; //ListNode *m_prev;
shared_ptr<ListNode<T>> m_next; //ListNode *m_next;
};
int main()
{
shared_ptr<ListNode<int>> node1(new ListNode<int>);
shared_ptr<ListNode<int>> node2(new ListNode<int>);
cout << "use_count 1 = " << node1.use_count() << endl;
cout << "use_count 2 = " << node2.use_count() << endl;
node1->m_next = node2;
node2->m_prev = node1;
cout << "use_count 1 = " << node1.use_count() << endl; //2
cout << "use_count 2 = " << node2.use_count() << endl; //2
return 0;
}
node1->m_next = node2;
node2->m_prev = node1;
互相引用,同时等待对方释放资源,造成死锁。实际上,直到程序退出析构函数也是没有被调用的。
解决办法:weak_ptr
#include <iostream>
#include <memory>
using namespace std;
template<class T>
struct ListNode
{
public:
~ListNode()
{
cout << "~ListNode()" << endl;
}
T data;
weak_ptr<ListNode<T>> m_prev; //ListNode *m_prev; //弱指针 * ->
weak_ptr<ListNode<T>> m_next; //ListNode *m_next;
};
int main()
{
shared_ptr<ListNode<int>> node1(new ListNode<int>);
shared_ptr<ListNode<int>> node2(new ListNode<int>);
cout << "use_count 1 = " << node1.use_count() << endl;
cout << "use_count 2 = " << node2.use_count() << endl;
node1->m_next = node2;
node2->m_prev = node1;
cout << "use_count 1 = " << node1.use_count() << endl; //2
cout << "use_count 2 = " << node2.use_count() << endl; //2
return 0;
}
发现使用weak_ptr后,再就没有发生内存泄漏了。可以用vld库监视内存有没有泄漏。
删除器
如果不是new的,比如malloc或者是文件指针和套接字,用智能指针去管理的话就要自定义删除器,智能指针除了第一个真正管理内存空间的指针外,是有第二个具有默认值的参数的,可以给仿函数,可以是lambda表达式,当然也可以是函数地址
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//内置类型删除器
template<class T>
struct freeMalloc{
void operator()(T* ptr) {
cout << "free type ptr : " << ptr << endl;
free(ptr);
}
};
//文件指针删除器
struct freeFile {
void operator()(FILE* fp) {
cout << "free file ptr : " << fp << endl;
fclose(fp);
}
};
template<typename U>
auto freeMal = [](U* p)->void {free(p); };
auto freeFil = [](FILE* fp)->void {fclose(fp); };
int main()
{
//如果不是new的,比如malloc或者是文件指针和套接字
int *p = (int *)malloc(sizeof(int));
#if 0
shared_ptr<int> is(p, freeMalloc<int>()); //仿函数
#else
shared_ptr<int> is(p, freeMal<int>); //lambda
//shared_ptr<int> is(p, [](int* p)->void {free(p); } );
#endif
FILE* fp = fopen("test.txt", "w");
#if 0
shared_ptr<FILE> fs(fp, freeFile()); //仿函数
#else
shared_ptr<FILE> fs(fp, freeFil ); //lambda
//shared_ptr<FILE> fs(fp, [](FILE* fp)->void {fclose(fp); } );
#endif
return 0;
}
删除器可以用auto定义好lambda然后传auto定义的名字
或者直接在实参列表写lambda表达式,两种方式是等价的如下:
shared_ptr<int> is(p, [](int* p)->void {free(p); } ); //lambda
shared_ptr<FILE> fs(fp, [](FILE* fp)->void {fclose(fp); } ); //lambda
智能指针构造方法第二个参数也可以是函数地址
定义一个文件指针删除器
void freeFi(FILE* fp) {
fclose(fp);
}
用函数地址作为第二个参数去构造智能指针
shared_ptr<FILE> fs(fp, freeFi); //函数地址
第三方库Boost库中的有六种指针
scoped_ptr,scoped_array,shared_ptr,shared_array,weak_ptr,intrusive_ptr
使用方式:
scoped_ptr:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
int main() {
int* p = new int(10);
boost::scoped_ptr<int> sptr(p);
cout << *sptr << endl;
//scoped_ptr禁止拷贝构造和赋值操作
//boost::scoped_ptr<int> sptr1(sptr);
//智能指针置空
sptr.reset();
return 0;
}
scoped_array:
//scoped_array
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
int main() {
int* pa = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
boost::scoped_array<int> spa(pa);
for (int i = 0; i < 10; ++i)
cout << spa[i] << " ";
cout << endl;
//禁止拷贝构造和赋值
//boost::scoped_array<int> spa1(spa);
return 0;
}
shared_ptr:
vS2013中可以正常执行,VS2019中 无法引用 函数 “boost::shared_ptr::shared_ptr(const boost::shared_ptr &) [其中 T=int]” (已隐式声明) – shared_ptr拷贝构造函数是已删除的函数
#include <iostream>
#include <boost/shared_ptr.hpp>
using namespace std;
int main() {
int* p = new int(1);
boost::shared_ptr<int> sp(p);
cout << *p << endl;
cout << "use_count" << sp.use_count() << endl;
//允许拷贝构造
boost::shared_ptr<int> sp1(sp);
cout << "use_count" << sp.use_count() << endl;
//允许赋值
boost::shared_ptr<int> sp2;
sp2 = sp;
cout << "use_count" << sp.use_count() << endl;
return 0;
}
shared_array用法与shared_ptr类似
weak_ptr是为了配合shared_ptr而引入的一种智能指针,其没有重载operator*和->,它最大的作用是协助shared_ptr工作。
//weak_ptr
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
using namespace std;
int main() {
int* p = new int(1);
boost::shared_ptr<int> sp(p);
cout << "use_count " << sp.use_count() << endl;
boost::weak_ptr<int> wp(sp);
cout << "use_count " << sp.use_count() << endl;
return 0;
}
用shared_ptr拷贝构造的weak_ptr并不会导致shared_ptr引用系数的增加
weak_ptr用于解决循环引用的问题
循环引用
使用Boost库,也是通过weak_ptr解决的
总结:
7.新增加容器---静态数组array、forward_list以及unordered系列
静态数组array
int main()
{
//原生态的数组类型
int ar[] = { 1,2,3,4,5,6,7,8,9,10 };
//ar[0];
//*(ar+offset)
int n = sizeof(ar) / sizeof(ar[0]);
//array容器
array<int, 10> br { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; ++i)
cout << br[i] << " ";
cout << endl;
//at,front,back,data
cout<<"pos : 1 is "<< br.at(1)<<endl; //返回下标对应的元素
cout <<"first element: "<< br.front() << endl; //第一个元素
cout <<"last element: "<< br.back() << endl; //最后一个元素
cout << "底层数组的地址 " << br.data() << endl; //direct access to the underlying array
int* arr = br.data();
for (int i = 0; i < 10; ++i)
cout << arr[i] << " ";
cout << endl;
//判空求容量以及最大容量
cout << br.empty() << endl;
cout << br.size() << endl;
cout << br.max_size() << endl;
array<int, 10> br1;
br1.fill(5); //填充方法
br1.swap(br); //交换方法
//交换完成后打印:
auto e = br.begin();
while (e != br1.end())
cout << *e++<<" ";
cout << endl;
for (int i = 0; i < br.size(); ++i)
cout << br[i] << " ";
cout << endl;
return 0;
}
array是模板类,定义时需要提供元素的类型和数组大小
array与vector的区别是,vector可以动态增长,array静态初始化后就不变了,array成为一个类的好处是不用手动计算元素个数
当然也有重载[],at,front,back,data .正向(常)和反向(常)迭代器.判空求容量以及最大容量.填充方法和交换方法
forward_list
#include<forward_list>的forward_list是单向不循环链表
#include头文件的list是双向循环链表
int main() {
forward_list<int> fl;
fl.push_front(3);
fl.push_front(2);
fl.push_front(1);
//如果非要尾插:
forward_list<int>::iterator itEnd;
auto tmp = fl.begin();
while (tmp != fl.end()) {
itEnd = tmp;
tmp++;
}
fl.insert_after(itEnd, 666);
for (auto e : fl) {
cout << e << " ";
}
cout << endl;
//获取第一个元素front
cout<<fl.front()<<endl;
//第一个节点的前面
forward_list<int>::iterator bbit = fl.before_begin();
bbit++;
cout << *bbit << endl;
//assign
forward_list<int> fl2;
fl2.push_front(6);
fl2.push_front(5);
fl2.push_front(4);
forward_list<int>::iterator it = fl2.begin();
forward_list<int>::iterator it2 = it;
it2++; it2++;
fl.assign(it, it2); //[iterator1,iterator2)
for (auto e : fl) {
cout << e << " ";
}
cout << endl;
return 0;
}
获取第一个元素front
forward_list只有头插方法,没有尾插方法,以及如下的结点操作方法
clear,insert_after,erase_after,push_front,pop_front和find方法.
对forward_list对象的操作方法swap,merge,remove,reverse,sort.
只有正向(常)迭代器和before_begin和cbefore_begin.
判空及求最大容量empty max_size
undered系列容器
8.默认成员函数控制
用default在函数尾声明,让系统依然生成默认的成员函数
用delete在函数尾声明,不允许系统生成默认的成员函数
class Test
{
public:
Test() = delete;
Test(int data) : m_data(data)
{}
Test(const Test &) = delete;
private:
int m_data;
};
void main()
{
Test t0;
Test t(0);
Test t1 = t; //error
}
当用户自定义了构造函数,系统便不会生成默认的构造函数,要想构造未初始化的对象,可以:
1.写上默认的构造函数
Test(){
}
2.给有参数的构造函数的参数都给默认值
Test(int data = 0) : m_data(data){
}
3.用default在函数尾声明一下,让系统依然生成默认的构造函数
Test() = default;
想禁止拷贝构造的行为可以:
1.将拷贝构造函数成私有成员函数
private:
Test(const Test &){
...
}
2.用delete在函数尾声明一下,让系统不生成默认的构造函数
Test(const Test &) = delete;
用default和delete是最省事的方式
避免删除函数和explicit一起使,当然一块使用是可以正常编译运行的。默认函数都没有生成,隐式类型转换也就是无稽之谈,写上是多此一举。
9.右值引用
C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性。
C++11中右值引用主要有以下作用:
-
实现移动语义(移动构造与移动赋值)
-
给中间临时变量取别名:
C语言中左值与右值:
可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值
C语言中一般情况下:
-
普通类型的变量,因为有名字,可以取地址,都认为是左值。
-
const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
-
如果表达式的运行结果是一个临时变量或者对象,认为是右值。
-
如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
-
不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量
-
能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
C语言中的纯右值,比如:a+b,100
将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
一个右值引用的例子:
int Add(int a, int b){
return a + b;
}
int main(){
const int&& ra = 10;
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
}
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其语法上只能对右值引用。并且普通引用只能引用左值,C ++98的const引用既能引用左值又能引用右值。
按值的形式返回对象的缺陷
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值。
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
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)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
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()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1 + s2);
return 0;
}
以上代码中:
String s3(s1 + s2);相当于String s3 = (s1 + s2);
会调用+运算符重载函数,这里是按值返回结果的
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;
}
出了函数,栈空间将被释放,包括创建的局部变量strRet。由于是按值返回的,在teturn前会用局部变量strRet拷贝构造一个临时匿名对象,假如叫做tmp保存结果。函数返回后,匿名对象tmp就会拷贝构造s3,等s3构造好了之后,临时对象就会被销毁,执行流程就是这样,在这里不考虑编译器底层的优化。
上述执行过程效率不高,还对空间造成了极大的浪费
可以借助右值引用实现移动语义来改进:
给上述代码增加移动构造方法:
String(String&& s) : _str(s._str)
{
s._str = nullptr;
}
实际上就变成了一种浅拷贝的方式,String s3(s1 + s2);执行这一句调用+运算符重载函数,按值返回局部变量strRet之前,会调用移动构造函数,函数的初始化列表_str(s._str)相当于将局部变量strRet的数据成员_str,赋值给临时匿名对象的数据成员_str,出移动构造函数前s._str = nullptr;将局部变量strRet的_str赋空,也就相当于将局部变量strRet的堆空间移动给了临时匿名对象。而用临时匿名对象去拷贝构造s3的时候也是相当于将临时匿名对象的堆空间移动给了s3。临时匿名对象依然会构造,但减少了深拷贝对堆空间的浪费。
总结:
实际上移动的是new出来的空间, 也就是堆上某些空间的所属者
move函数:
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于#include头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
还是使用上面定义的String类
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
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(String&& s) :_str(s._str) {
s._str = nullptr;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
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()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main() {
String s1("hello");
//String s2 = s1;
/*用s1拷贝构造s2,调用的是普通的拷贝构造方法也就是进行了深拷贝,
要想让其调用移动构造方法进行浅拷贝,那么就得让=右边是右值才行*/
String s2 = move(s1);
return 0;
}
String s2 = s1;
用s1拷贝构造s2,调用的是普通的拷贝构造方法也就是进行了深拷贝
要想让其调用移动构造方法进行类似浅拷,那么就得让=右边是右值才行修改如下:
String s2 = move(s1);
move经常用于拷贝构造中:
如果对象的数据成员还是对象,可以在移动构造中继续使用move,从而达到完全拷贝构造。
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
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(String&& s) :_str(s._str) {
s._str = nullptr;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
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()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
class Person
{
public:
Person(const char* name, const char* sex, int 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
private:
String _name;
String _sex;
int _age;
};
Person GetTempPerson()
{
Person p("prety", "male", 18);
return p;
}
int main()
{
Person p(GetTempPerson());
return 0;
}
完美转发(在协议中称为透传):
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
完美转发是通过#include中的forward函数实现的
也就是传给模板函数的参数是左值还是左值,是右值还是右值
#include <iostream>
using namespace std;
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>
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;
}
10.lambda表达式
首先了解一下仿函数:
所谓的仿函数(functor),是通过重载()运算符模拟函数形为的类。
因此,这里需要明确两点:
1.仿函数不是函数,它是个类;
2.仿函数重载了()运算符,使得它的使用可以像函数调用那样子(代码的形式好像是在调用函数)。
调用库函数sort可能会用到仿函数
#include <iostream>
#include <functional> //包含仿函数的库
using namespace std;
int main()
{
int array[] = { 4,1,8,5,3,7,0,9,2,6 };
int n = sizeof(array) / sizeof(array[0]);
//参数:首元素地址,首元素地址+偏移量,仿函数(决定排序方式,默认值是less<int>()从小到大排序)
//sort(array, array + n); //等价于:sort(array, array + n, less<int>());
sort(array, array+n, greater<int>());
for (int i = 0; i < n; ++i)
cout << array[i] << " ";
cout << endl;
return 0;
}
除了调用sort对内置类型排序外,也有可能有对自定义类型排序的需求
如下:对自定义类型Student按体重排序
struct Student
{
char name[10];
int weight;
};
class stuCompareOfClass{
public:
bool operator()(const Student& s1, const Student& s2)
{
return s1.weight < s2.weight;
//return strcmp(s1.name, s2.name) >= 0;
}
};
struct stuCompareOfStruct {
bool operator()(const Student& s1, const Student& s2)
{
return s1.weight < s2.weight;
//return strcmp(s1.name, s2.name) >= 0;
}
};
int main()
{
Student stu[] = {
{"杨同学", 45}, {"张同学", 40},
{"刘同学",60},{"王同学",55}
};
int n = sizeof(stu) / sizeof(stu[0]);
sort(stu, stu+4, stuCompareOfClass()); //仿函数
sort(stu, stu+4, stuCompareOfStruct()); //仿函数
sort(stu, stu + 4, [] (const Student& s1, const Student& s2)->bool {return s1.weight < s2.weight; }); //lambda表达式
return 0;
}
用class和struct都可以实现仿函数,注意用class的时候重载()的成员函数一定要给public属性。向以上的这种使用方式会很不方便,对于不同的自定义类型调用sort排序,每次都要重新实现一个类,因此C++ 11诞生了lambda表达式
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要
直接调用,可借助auto将其赋值给一个变量
[capture-list] (parameters) mutable->returntype {statement};
lambda表达式各部分说明:
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的
mutable给私有数据成员加上mutable,就允许const成员方法对这个私有数据成员修改
class Test
{
public:
Test(int a = 0) : m_a(a)
{}
public:
void fun(int a)const
{
m_a = a;
}
private:
mutable int m_a;
};
void main()
{
Test t(100);
t.fun(1);
}
在lanbda表达式中,不加mutable默认是const,也就是捕捉列表捕捉的对象及变量都是是const的,加上就是mutable的
[capture-list]捕捉列表[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
int main() {
int a = 10;
int b = 20;
auto fun = [] (int x)->int {
return x + a;
}
return 0;
}
以上代码会报错:封闭函数局部变量不能在lambda体中引用,除非其位于捕获列表中。这实际上是闭包的原因,但是全局变量在lambda 体中是可见的
要让以上代码正常执行
按值传参可以:[=]捕捉父作用域所有变量(包括this),也可以[a]只捕捉需要的变量。
引用传参可以:[&]捕捉父作用域所有变量(包括this),也可以[&a]只捕捉需要的变量
注意:无论是按值捕获还是引用捕获,捕捉父作用域的变量只是位于lambda之前定义的, 并且无法在 lambda 中捕获带有静态存储持续时间的变量,也就是不可以捕捉全局变量。当lanbda表达式是mutable的,引用捕捉的变量,在lambda体中修改后会反映到父作用域,也就是实参需要被修改的时候可以用引用捕获
语法上捕捉列表可由多个捕捉项组成,并以逗号分割
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c.
捕捉列表不允许变量重复传递,否则就会导致编译错误。
其它组成部分和普通函数作用类似
总结:
在块作用域以外的lambda函数捕捉列表必须为空
lambda表达式之间不能相互赋值,但是允许拷贝构造
可以将lambda表达式赋值给相同类型的函数指针
int main() {
auto f1 = [] {cout << "Hello C++." << endl; };
auto f2 = [] {cout << "Hello Linux." << endl; };
f1 = f2; //error:不能相互赋值
auto f3 = f2; //right:允许拷贝构造
f3();
void (*funcPtr)(); //定义一个函数指针
funcPtr = f1; //将lambda表达式赋值给相同类型的函数指针
funcPtr();
return 0;
}
仿函数与lambda的渊源:
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
//仿函数
Rate rt(2.5);
cout << rt(1000, 2) << endl; //rt(1000,2) ==> rt.operator()(1000, 2);
//lambda表达式
double rate = 2.5;
auto rt1 = [rate](int money, int year)->double {return money * year * rate; };
cout << typeid(rt1).name() << endl;
cout << rt1(1000, 2) << endl;
return 0;
}
可以看出来,从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
11.线程库
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,...) | 构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否是有效的,joinable代表的是一个正在执行中的线程 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与主线程分离,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
join
主线程在子线程创建成功后得用join阻塞等待, 回收子线程资源,不回收资源程序会奔溃
#include <iostream>
#include <thread>
using namespace std;
void thread_fun(int n, int m)
{
for (int i = 0; i < n; ++i)
cout << "This is Child Thread." << endl;
}
int main()
{
int n = 5;
int m = 10;
thread th(thread_fun, n, m);
cout << th.get_id() << endl;
cout << th.get_id() << endl;
for (int i = 0; i < 10; ++i)
cout << "This is Main Thread." << endl;
th.join(); //主线程阻塞等待,回收子线程资源,不回收资源程序会奔溃
cout << "Main End." << endl;
return 0;
}
detach
主线程在子线程创建成功后不用join阻塞等待回收子线程资源,立刻调用detach进行线程分离,使子线程称为后台线程
#include <iostream>
#include <thread>
using namespace std;
void thread_fun(int n, int m)
{
for (int i = 0; i < n; ++i)
cout << "This is Child Thread." << endl;
}
int main()
{
int n = 5;
int m = 10;
thread th(thread_fun, n, m);
th.detach();
cout << th.get_id() << endl;
cout << th.get_id() << endl;
for (int i = 0; i < 10; ++i)
cout << "This is Main Thread." << endl;
cout << "Main End." << endl;
return 0;
}
注意:
1.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
2.创建的线程对象的执行流全部在相关联的线程函数中
3.get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
4.给线程关联线程函数:
函数指针,lambda表达式和函数对象(仿函数)
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([] {cout << "Thread2" << endl; });
// 线程函数为函数对象,仿函数
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
5.thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象
不允许拷贝构造以及赋值
void treadFunc() {
cout << "I am child tread!" << endl;
}
int main() {
thread td1(treadFunc);
thread td2 = td1; //拷贝构造:error
thread td3;
td3 = td1; //赋值: error
return 0;
}
移动构造和移动赋值(注意不能对同一个线程对象多次移动构造或者赋值。进行了移动构造和移动赋值的线程对象,实际没有对应任何线程,已经没有资源了,因此不能对其进行线程阻塞等待了)
void treadFunc() {
cout << "I am child tread!" << endl;
}
int main() {
thread td1(treadFunc);
thread td2 = move(td1); //移动构造:error
thread td3(treadFunc);
thread td4;
td4 = move(td3); //移动赋值: error
td2.join();
td4.join();
return 0;
}
6.可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象:移动构造和移动赋值
线程已经调用jion或者detach结束
7.线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的
普通的按值传参和引用传参并不会改变实参的值
普通的按值传参
void func(int data) {
data += 10000; //线程函数的参数是以值拷贝的方式拷贝到线程栈空间
cout << "child thread data: " << data << endl;
}
int main() {
int data = 10;
thread td(func, data);
td.join();
cout << "main thread data: "<<data << endl;
return 0;
}
引用传参
VS2019将引用传参不加ref当成了错误,以下代码无法编译通过
在老一些的编译器中VS2013中以下代码任然可以正常执行
void func(int& data) {
data += 10000;
cout << "child thread data: " << data << endl;
}
int main() {
int data = 10;
thread td(func, data);
td.join();
cout << "main thread data: " << data << endl;
return 0;
}
要改变可以用按址传参和引用配合ref实现
按址传参
void func(int* data) {
*data += 10000;
cout << "child thread data: " << *data << endl;
}
int main() {
int data = 10;
thread td(func, &data);
td.join();
cout << "main thread data: " << data << endl;
return 0;
}
引用配合ref
void func(int &data) {
data += 10000;
cout << "child thread data: " << data << endl;
}
int main() {
int data = 10;
thread td(func, ref(data));
td.join();
cout << "main thread data: " << data << endl;
return 0;
}
8.如果是类成员函数作为线程参数时,必须将this作为线程函数参数。
9.join很容易用错
void ThreadFunc() { cout << "ThreadFunc()" << endl; }
bool DoSomething() { return false; }
int main()
{
std::thread t(ThreadFunc);
if (!DoSomething())
return -1;
t.join();
return 0;
}
//程序无法运行到join就会结束,也就是子线程的资源没有被回收主线程已经退出,程序结束
解决上述问题的办法有:代理和线程分离
代理
为了管理内存等资源,C++程序员通常采用RAII机制(资源获取即初始化,Resource Acquisition Is Initialization),在使用资源的类的构造函数中申请资源,然后使用,最后在析构函数中释放资源。实际上借助的是代理模式。
class agent {
public:
agent(thread & th):_th(th) {
}
~agent() {
if (_th.joinable())
_th.join();
}
private:
thread &_th;
};
void ThreadFunc() { cout << "ThreadFunc()" << endl; }
bool DoSomething() { return false; }
int main()
{
thread t(ThreadFunc);
agent a(t);
if (!DoSomething())
return -1;
return 0;
}
以上代理函数中的析构函数可以使用lambda表达式
class agent {
public:
agent(thread& th) :_th(th) {
}
~agent() {
auto lam = [this]()->void {this->_th.join(); };
if (_th.joinable()) lam();
}
private:
thread& _th;
};
分离
detach():该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会交给c++ 运行库。同时,C++ 运行库保证,当线程退出时,其相关资源的能够正确的回收。
void ThreadFunc() { cout << "ThreadFunc()" << endl; }
bool DoSomething() { return false; }
int main()
{
thread t(ThreadFunc);
t.detach();
if (!DoSomething())
return -1;
return 0;
}
9.多线程最主要的问题是共享数据(临界资源)带来的问题(即线程安全)。当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
#include <iostream>
#include <thread>
using namespace std;
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl; //结果小于2000w,并且每次的结果都不一样
return 0;
}
这是因为打破了操作的原子性,t1和t2同时对sum操作便会浪费掉sum ++的总次数,具体原因需要汇编和指令以及计算机底层的知识,不过多分析。(有兴趣可以追更笔者后期更新)
解决以上问题的方法就是同步机制:
四种互斥量:
(1)mutex互斥量的成员函数有:lock,unlock和try_lock
(2)recursive_mutex:允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的unlock(),除此之外,std::recursive_mutex 的特性和std::mutex 大致相同。
(3)timed_mutex:比 mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
try_lock_for()接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
(4)recursive_timed_mutex
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
unsigned long sum = 0L;
mutex mu;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i) {
mu.lock();
sum++;
mu.unlock();
}
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl; //结果2000w
return 0;
}
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁(解锁失败)。
因此C++ 11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++ 11引入的原子操作类型,使得线程间数据的同步变得非常高效。
原子类型名称 | 对应的内置类型名称 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_charl6_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
atmoic<T> t; // 声明一个类型为T的原子类型变量t
#include
#include
#include
using namespace std;
atomic_ulong sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum++;
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl; //结果小于2000w
return 0;
}
通过验证我们发现
atomic<unsigned long> sum(0L);
和
atomic_ulong sum = 0L;
是等价的
猜想底层:
#define atomic atomic_ulong;
10.保证一段代码的原子性:
C++ 98中是加锁
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int number = 0;
mutex g_lock;
int ThreadProc1()
{
for (int i = 0; i < 100; i++)
{
g_lock.lock();
++number;
cout << "thread 1 :" << number << endl;
g_lock.unlock();
}
return 0;
}
int ThreadProc2()
{
for (int i = 0; i < 100; i++)
{
g_lock.lock();
--number;
cout << "thread 2 :" << number << endl;
g_lock.unlock();
}
return 0;
}
int main()
{
thread t1(ThreadProc1);
thread t2(ThreadProc2);
t1.join();
t2.join();
cout << "number:" << number << endl;
system("pause");
return 0;
}
上述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。
因此:C++ 11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
lock_guard
lock_gurad 是 C++11 中定义的模板类
定义如下:
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_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
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int number = 0;
mutex g_lock;
int ThreadProc1()
{
for (int i = 0; i < 100; i++)
{
lock_guard<mutex> lck(g_lock);
++number;
cout << "thread 1 :" << number << endl;
}
return 0;
}
int ThreadProc2()
{
for (int i = 0; i < 100; i++)
{
g_lock.lock();
--number;
cout << "thread 2 :" << number << endl;
g_lock.unlock();
}
return 0;
}
int main()
{
thread t1(ThreadProc1);
thread t2(ThreadProc2);
t1.join();
t2.join();
cout << "number:" << number << endl;
system("pause");
return 0;
}
lock_gurad实际上也是通过RAII机制,对其管理的互斥量进行了封装,以代理模式实现锁的管理,在构造函数中加锁,析构函数中自动解锁。
在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
缺陷:太单一,用户没有办法对该锁进行控制,从而有了unique_lock
unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。
与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所管理的互斥量的指针)。
#include <iostream>
#include <mutex>
using namespace std;
mutex mu;
int main() {
unique_lock<mutex> ug1(mu);
unique_lock<mutex> ug2 = move(ug1); //移动构造
unique_lock<mutex> ug3(mu);
unique_lock<mutex> ug4;
ug4 = move(ug3); //移动赋值
return 0;
}
unique_lock