Skip to content

Latest commit

 

History

History
1604 lines (1159 loc) · 50.2 KB

C++11新特性.md

File metadata and controls

1604 lines (1159 loc) · 50.2 KB

C++11新特性:

1.列表初始化

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

1.1C++98中{ }的初始化问题

在C++98中,标准允许使用花括号{ }对数组元素进行统一的列表初始值设定

int array1[]={1,2,3,4,5};
int array2[5]={0};

对于一些自定义的类型,却无法使用这样的初始化

vector<int>v{1,2,3,4,5};
//无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,十分不便。

1.2内置类型的列表初始化

int main()
{
    //内置类型变量
    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}};
    return 0;
}

注意:列表初始化可以在{ }前使用等号,和不使用等号没什么区别

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

(1)标准库支持单个对象的列表初始化

class Point
{
 public:
    Point(int x=0,int y=0):
      _x(x),_y(y){}
  private:
    int _x;
    int _y;
};

int main()
{
    Point p{1,2};
    return 0;
}

(2)多个对象的列表初始化

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

注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。

#include<initializer_list>
template<class T>
class Vector
{
  public:
    Vector(initializer_list<T>l):
       _capacity(l.size()),_szie(0)
       {
           _array=new T{_capacity};
           for(auto e:l)
               _array{_size++}=e;
       }//浅拷贝,副本与原容器共享同一份数据
    
     Vector<T>& operator=(initializer_list<T>l)
     {
         delete[]_array;
         size_t i=0;
         for(auto e:l)
             _array[i++]=e;
         return *this;
     }
    //......
  private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

2.变量类型推导

特性:C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给让程序的书写更加方便。将程序中c与it的类型换成auto。

2.1为什么需要类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要什么实际类型,或者类型写起来十分复杂,例如迭代器

2.2decltype类型推导

为什么需要decltype

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

template<class T1,class T2>
    T1 Add(constT1&left,const T2&right)
{
    return left+right;
}
//如果能用加完之后的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)

C++98中确实已经支持RTTI:

  • typeid只能查看类型不能用其结果类定义类型

  • dynamic_cast只能应用于含有虚函数的继承体系中

运行时类型识别的缺陷是降低程序运行的效率

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型

(1)推演表达式类型作为变量的定义类型

#include<iostream>
using namespace std;

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

(2)推演函数返回值的类型

void* GetMemory(size_t size)
{
    return malloc(size);
}

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

总结:

decltype和auto的区别:

  • auto需要执行代码才可推导出类型
  • decltype不需要执行代码,只需推演(并非真正执行)代码即可推导出类型

3.auto关键字、基于范围for的循环

3.1auto的使用细则

3.1.1auto与指针和引用结合起来使用

用auto声明指针类型时,auto和auto*没有区别;但用auto声明引用类型时必须加&

3.1.2在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际值对第一个类型进行推导,然后用推导出来的类型定义其他变量

3.2auto不能推到的场景

//1.auto不能作为函数的参数
void TestAuto(auto a){}//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导

//2.auto不能直接用来声明数组
void TestAuto()
{
    auto b[]={4,5,6};//error
}

//3.为了必变与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
//4.auto在实际中最常见的又是用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用
//5.auto不能定义类的非静态成员变量
//6.实例化模板时不能使用auto作为参数模板

4.多态

4.1定义及实现

4.1.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

在继承中要构成多态的两个条件(缺一不可):

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

4.2虚函数

虚函数:被virtual修饰的类成员函数称为虚函数

4.2.1虚函数重写的两个例外

/*1.协变(基类与派生类虚函数返回值类型不同):

派生类重写基类函数时,与基类虚函数返回值类型不同。原来的返回类型是指向基类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型,这样的类型称为协变返回类型(Covariant returns type)

覆盖的返回值不区分基类或派生类。从语义上理解,一个派生类也是一个基类*/

class A{};
class B:public A{};

class Person
{
 public:
    virtual A*f(){
        return new A;
    }
};
class Student:public Person
{
    virtual B* f(){
        return new B;
    }
};

/*2.析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor*/

class Person{
public:
    virtual ~Person(){
        cout<<"~Person()"<<endl;
    }
};
class Student:public Person{
public:
    virtual ~Student(){
        cout<<"~Student()"<<endl;
    }
};

//只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数
int main()
{
    Person*p1=new Person;
    Person*p2=new Student;
    delete p1;//~Person()
    delete p2;//~Student() ~Person()
    return 0;
}

4.3override和final

特性:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

4.3.1final:修饰虚函数,表示该虚函数不能再被继承

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

4.3.2override:检查派生类虚函数是否重写了基类某个虚函数

class Car
{
public:
    virtual void Drive()
    {}
};
class Benz:public Car
{
public:
    virtual void Drive()override
    {
        cout<<"Benz-舒适"<<endl;
    }
};

4.3.3重载、重写(覆盖)、重定义(隐藏)三个概念的对比

在这里插入图片描述

4.4抽象类(纯虚函数)

在虚函数的后面加上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化处对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

class Car
{
public:
    virtual void Drive()=0;
};
class Benz:public Car
{
public:
    virtual void Drive()
    {
        cout<<"Benz-舒适"<<endl;
    }
};
class BMW:public Car
{
public:
    virtual void Drive()
    {
        cout<<"BMW-操控"<<endl;
    }
};

int main()
{
    Car* pBenz=new Benz;
    pBenz->Drive();//Benz-舒适
    Car* pBMW=new BMW;
    pBMW->Drive();//BMW-操控
    system("pause");
    return 0;
}

接口继承和实现继承:

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的时接口。所以如果不实现多态,不要把函数定义成虚函数。

4.5多态的原理

4.5.1虚函数表

//常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
    virtual void func1()
    {
        cout<<"Func1()"<<endl;
    }
 private:
    int _b=1;
};
int main()
{
    cout<<sizeof(Base);//8
}
//Base对象是8bytes,除了_b成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(v - virtual,f - function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。
  • 重写是语法的叫法,覆盖是原理层的叫法。
  • 虚函数表本质上是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

**注意:**虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的。另外,对象中存的不是虚表,存的是虚表指针。在vs下,虚表存在代码段

重点总结:

基类的虚表生成:

  • 虚表中放置的都是虚函数
  • 按照虚函数在类中声明的先后次序,依次添加到虚表中

派生类的虚表生成:

  • 先将基类中的虚表内容拷贝一份到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

4.6静态多态与动态多态

静态多态(静态绑定,前期绑定,早绑定):在程序编译期间确定了程序的行为(具体调用那个函数)。比如:函数重载、模板

动态多态(动态绑定、后期绑定、晚绑定):在程序运行期间,根据基类指针或者引用指向不同类的对象,调用对应的虚函数(在程序运行时,确定函数的具体行为)

多态常见的面试问题

1.什么是多态?

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

2.什么是重载、重写(覆盖)、重定义(隐藏)?

重载:两个函数在同一定义域;函数名相同,参数不同

重写:两个函数分别在基类和派生类作用域;函数名、参数、返回类型都必须要相同(协变例外);都必须是虚函数

重定义:两个函数分别在基类和派生类作用域;函数名相同;不构成重写就是重定义

3.多态的实现原理?

通过虚函数表,在程序运行期间找到指针指向的对象需要调用的虚函数

4.inline函数可以是虚函数吗?

不能,因为inline函数没有地址,无法把地址放到虚函数表中。

5.静态成员可以是虚函数吗? 不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6.构造函数可以是虚函数吗? 虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数。 因此,构造函数不应该被定义为虚函数。

7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数? 可以,并且最好把基类的析构函数定义成虚函数。

虚析构函数是为了解决这样的一个问题:基类的指针指向派生类对象,并用基类的指针删除派生类对象。

基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。 对象访问普通函数快还是虚函数更快? 普通函数快,因为地址在编译期间指定,单纯的寻址调用。 虚函数调用时,首先找虚函数表,然后找偏移地址进行调用。

8.虚函数表是在什么阶段生成的,存在哪的? 虚函数是在编译阶段就生成的;一般情况下存在代码段(常量区)的:因为虚表中的内容是不允许被修改的。

9.什么是抽象类?抽象类的作用? 抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

5.智能指针

5.1为什么需要只能指针

为了更好的解决 内存泄漏 所以出现了智能指针

1.当我们malloc出来的内存没有去释放时就会出现问题,就会存在内存泄漏问题

2.异常安全问题:如果malloc和free之间如果存在抛异常,那么还是有内存泄漏

3.当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄漏(没有将基类的析构函数定义为虚函数)

5.2内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏比跟不上指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费

**内存泄漏的危害:**长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

内存泄漏分类:

  • 堆内存泄露(Heap Leak):

    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应free或者delete掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  • 系统资源泄漏:

    指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源浪费,严重可导致系统效能降低,系统执行不稳定。

如何避免内存泄漏:

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记者匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  • 采用RAII思想或者智能指针来管理资源。
  • 有些公司内部规范使用内部实现的私有内存管理库。这套库自导内存泄漏检测的功能选项。
  • 出问题了使用内存泄露工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

5.3智能指针的使用及原理

5.3.1RAII(资源获取就是初始化)

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之再对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做有两大好处:

  • 不需要显示地释放资源
  • 对象所需的资源在其生命周期内保持有效
#include<iostream>
#include<vector>
using namespace std;

//使用RAII思想设计的SmartPtr类
template<class T>
class SmatrPtr
{
public:
    SmartPtr(T*ptr=nullptr)
        :_ptr(ptr){}
    ~SmartPtr(){
        if(_ptr)
            delete _ptr
    }
 private:
    T*_ptr;
};

void MergeSort(int *a,int n)
{
    int* tmp=(int*)malloc(sizeof(int)*n);
    
    //将tmp指针委托给了sp对象
    SmartPtr<int>sp(tmp);
    //_MergeSort(a,0,n-1,tmp);
}

int main()
{
    try{
        int a[5]={4,5,2,3,1};
        MergeSort(a,5);
    }
    catch(const exception& e)
    {
        cout<<e.what()<<endl;//bad allocation
    }
    return 0;
}

5.4智能指针的原理

上述的SmartPtr还不能称其为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所有所指空间中的内容,因此:AutoPtr模板类中还得需要将*、->重载下,才可让其像指针一样去使用。

template<class T>
class smart_ptr
{
    T*m_ptr;
 public:
    smart_ptr(T*pre=nullptr)
        :m_ptr(ptr){}
    ~smart_ptr(){
        if(m_ptr){
            delete[]m_ptr;
            m_ptr=nullptr;
        }
    }
    
    T& operator*(){
        return *m_ptr;
    }
    T* operator->(){
        return m_ptr;
    }
    T& operator[](int i){
        return m_ptr[i];
    }
};

class Test
{
public:
    int m_a;
};

5.4.1std::auto_ ptr (C++98)

auto_ptr实现原理:管理权转移思想

template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = NULL)
        : _ptr(ptr)
    {}
    ~AutoPtr() {
        if (_ptr) 
        	delete _ptr;
    }

    // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后令ap与其所管理资源断开联系, 
    // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    AutoPtr(AutoPtr<T>& ap)
        : _ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }

    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        // 检测是否为自己给自己赋值 
        if(this != &ap)
        {
            // 释放当前对象中资源            
            if(_ptr){
                delete _ptr;
            }
            // 转移ap中资源到当前对象中 
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }

    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
private:
    T* _ptr;
};

5.4.2std::unique_ptr(C++11)

unique_ptr实现思路:防拷贝,不让拷贝和赋值

template<class T>
class UniquePtr
{
public:
	UniquePtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	// C++98防拷贝的方式:只声明不实现+声明成私有 
	//UniquePtr(UniquePtr<T> const &);
	//UniquePtr & operator=(UniquePtr<T> const&);

	// C++11防拷贝的方式:delete
	UniquePtr(UniquePtr<T> const&) = delete;
	UniquePtr& operator=(UniquePtr<T> const&) = delete;

	T* _ptr;
};

5.4.3std::shared_ptr(C++11)

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

#include <iostream>
#include <memory>
using namespace std;

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;

	~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;	// 1
	cout << node2.use_count() << endl;	// 1

	node1->_next = node2;
	node2->_prev = node1;

	cout << node1.use_count() << endl;	// 2
	cout << node2.use_count() << endl;	// 2

	return 0;
}

循环引用分析:

  • node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。

  • node1的_next指向node2,node2的_prev指向node1,引用计数变成2。

  • node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

  • 也就是说_next析构了,node2就释放了。

  • 也就是说_prev析构了,node1就释放了。

  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

在这里插入图片描述

#include <iostream>
#include <memory>
using namespace std;

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时
// weak_ptr的_next和_prev不会增加 node1和node2的引用计数。
struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_ptr<ListNode> _next;
    ~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;

    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

//1
//1
//1
//1
//~ListNode()
//~ListNode()

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题

#include <iostream>
using namespace std;

// 仿函数的删除器 
template<class T> 
struct FreeFunc {
    void operator()(T* ptr)
    {
        cout << "free:" << ptr << endl;
        free(ptr);
    }
};
template<class T>
struct DeleteArrayFunc {
    void operator()(T* ptr)
    {
        cout << "delete[]" << ptr << endl;
        delete[] ptr;
    }
};

int main() {
    FreeFunc<int> freeFunc;
    shared_ptr<int> sp1((int*)malloc(4), freeFunc);//第二个参数是析构的方式
    
    DeleteArrayFunc<int> deleteArrayFunc;
    shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

    return 0;
}

//delete[]007904D0
//free : 00795108

5.5C++11和boost中智能指针的关系

  • C++ 98 ,产生了第一个智能指针auto_ptr.

  • C++ boost,给出了更实用的scoped_ptr和shared_ptr和weak_ptr.

  • C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。

  • C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

要点总结:

  • 当一个函数中出现大量return,那么在每次return之前都要加入delete,这种操作非常麻烦且容易出错,智能指针就是用来解决这个问题的。

  • 智能指针是个类模板,主要功能是假装自己是个指针,通过析构函数,解决上面的问题。

  • 智能指针就是用来更加方便、安全的使用临时new出来的变量或对象的。

三种指针在拷贝时的区别:

  • auto_ptr:旧的给新的,旧的失效
  • unique_ptr:不允许创建新的
  • shared_ptr:随意拷贝
  • weak_ptr:解决shared_ptr循环引用的问题

6.STL

6.1容器

在这里插入图片描述

6.2算法

STL中的算法主要分为两大类:与数据结构相关算法(容器中的成员函数)和通用算法(与数据结构不相干)。STL中通用算法总共有70多个,主要包含: 排序,查找,排列组合,数据移动,拷贝,删除,比较组合,运算等

6.3迭代器

迭代器是一种设计模式,让用户通过特定的接口访问容器的数据,不需要了解容器内部的底层数据结构。

每个容器的迭代器应该由容器设计者负责提供,然后容器按照约定给出统一的接口即可

6.4适配器

**适配器:**又接着配接器,是一种设计模式。简单地说:需要的东西就在眼前,但是由于接口不对而无法使用,需要对其接口进行转化以方便使用。即:将一个类的接口转换成用户希望的另一个类的接口,使原本接口不兼容的类可以一起工作。

STL中适配器总共有3种类型:

  • 容器适配器 - stack(栈)和queue(队列)
  • 迭代器适配器 - 反向迭代器

6.5仿函数

仿函数:一种具有函数特征的对象,调用者可以像调用函数一样使用该对象,为了能够“行为类似函数”,该对象所在类必须自定义函数调用运算符operator(),重载该运算符后,就可以在仿函数对象的后面加上一对小括号,以此调用仿函数所定义的operator()操作。

仿函数一般配合算法,作用就是:提高算法的灵活性

6.6空间配置器

空间配置器:为各个容器高效地管理空间(空间的申请和回收)。

为什么需要空间配置器

模拟实现vector、list、map、unordered_map等容器时,所有需要空间的地方都是通过new申请的,虽然代码可以正常运行,但是有以下不足之处:

  • 空间申请与释放需要用户自己管理,容易造成内存泄漏
  • 频繁向系统申请小块内存块,容易造成内存碎片
  • 频繁向系统申请小块内存,影响程序运行效率
  • 直接用malloc与new申请,每块空间前有额外空间浪费
  • 申请空间失败怎么应对
  • 代码结构混乱,代码复用率不高
  • 未考虑线程安全问题

SGI-STL空间配置器实现原理

SGI-STL以128作为小块内存与大块内存的分界线,将空间配置器分为两级结构,一级空间配置器处理大块内存,二级空间配置处理小块内存。

一级空间配置器

一级空间配置器原理:直接对malloc与free进行了封装,并增加了C++中set_new_handle思想

二级空间配置器

二级空间配置器专门负责处理小于128字节的小块内存。如何才能提升小块内存的申请与释放?SGI-STL采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度与高效管理

STL中一级容器(vector/deque/list)是指,容器元素本身是基本类型,非组合类型

7.委派构造函数

7.1构造函数冗余造成重复

特性:委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更容易。

7.2委派构造函数

委派构造函数:指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式

在初始化列表中调用“基准版本”的构造函数称为委派构造函数,而被调用的“基准版本”则称为目标构造函数

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

8.默认函数控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造。移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器生成,有时候又不生成,容器造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

8.1显示缺省函数

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

8.2删除默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

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

9.右值引用

9.1移动语义

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错。

移动语义:将临时对象的资源转移到s2,整个过程开辟一段内存

将一个对象中资源移动到另一个对象中的方式,称之为移动语义。在C++11中如果需要实现移动语义,必须使用右值引用。

9.2C++11中的右值

右值引用:对右值的引用。C++11中,右值由两个概念组成:纯右值和将亡值。

纯右值:

纯右值是C++98中右值的概念,用于识别临时变量和一些不跟对象关联的值。比如:常量、一些运算表达式(1+3)等

将亡值:

声明周期将要结束的对象。比如:在值返回时的临时对象

9.3右值引用

9.4std::move

功能:将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。

注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁

为了保证移动语义的传递,程序员在编写移动构造函数时,最好使用std::move转移拥有资源的成员为右值

9.5完美转发

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

总结:

  • 什么是右值?

    可以取地址的统统是左值,否则是右值

  • move forward

    move是将一个值强制转换成右值

    forward是将一个值转换为左值/右值

    右值引用可以延长一个临时对象的声明周期

    右值引用做返回值参考引用和指针,不能返回临时变量的右值引用

  • 移动构造函数

    允许使用右值进行构造

    右值构造所用到的对象往往都是临时对象,所以可以直接将其资源转移,以节省时间,所以不要将左值通过move转成右值后去构造,否则该左值的资源将被转移,无法再使用。

  • 类引用折叠

    右值引用+右值引用=右值引用

    左值引用+右值引用=左值引用

  • int&& &&=int&&

    int&& & / int& &&=int&

10.lambada表达式:匿名函数

10.1

//lambda表达书写格式:[capture-list](parameters)mutable->return-type{statement}
1.lambda表达式各部分说明
    
    [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
    
    (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
    
    mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
        
    ->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时对此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
        
    {statement}:函数体。在该函数体内,除了亦可使用其参数外,还可以使用所有捕获到的变量。
        
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};该lambda函数不能做任何事情。
        
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
        
 
2.捕获列表说明
    捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用
        [var]:表示值传递方式捕捉变量var
        [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
        [&var]:表示引用传递捕捉变量var
        [&]:表示引用传递捕捉所有父作用域的变量(包括this)
        [this]:表示值传递方式捕捉当前的this指针
        
注意:
        a.父作用域指包含lambda函数的语句块
        b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割
        c.捕捉列表不允许变量重复传递,否则就会导致编译错误。
        d.在块作用域以外的lambda函数捕捉列表必须为空
        e.在块作用域中的lambada函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译错           误
        f.lambda表达式之间不能相互赋值,即使看起来类型相同

10.2函数对象与lambda表达式

函数对象,又称为仿函数,即可以像函数一样使用的对象。

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

11.线程库

11.1thread类的简单介绍

C++11最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件,该头文件声明了std::thread线程类。

11.2线程的启动

C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程进行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreaddex()创建线程函数来实现(注意:该函数是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针。

11.3线程的结束

启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择

  • 加入式: join() 主线程需要等子线程完成后再继续

    join():会主动地等待线程的终止。在调用进程中join(),当新的线程终止时,join()会清理相关的资源,然后返回,调用线程再继续向下执行。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程的对象每次你只能使用一次join(),当你调用join()之后joinable()就将返回false了。

  • 分离式:detach() 主线程无需等待子线程,所以可能会导致副线程无法完成,程序就已经结束

    detach():会从调用线程中分离出新的线程,之后不能再与新线程交互。此时调用joinable()必然是返回false。分离的线程会在后台运行,其所有权和控制权将会交给C++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。

    注意:必须在thread对象销毁前做出选择,这是因为线程可能在你加入或分离线程之前,就已经结束了,之后如果再去分离它,线程可能会在thread对象销毁之后继续运行下去。

//使用lambda表达式作为线程函数创建线程
int main()
{
    int n1=500;
    int n2=600;
    thread t([&](int addNum)){
        n1+=addNum;
        n2+=addNum;
    },500);
    t.join();
    cout<<n1<<' '<<n2<<endl;//1000 1100
    return 0;
}
//使用普通函数作为线程函数创建线程

#include<iostream>
#include<thread>
using namespace std;

void function_1() {
	cout << "www.oxox.work" <<endl;
}

int main()//主线程 
{
    //join()
	thread t1(function_1);//t1的初始化线程就开始进行了
	t1.join();//主线程会等t1线程结束后再继续运行,t1和主线程是两个互不影响的线程
    
    //detach()
    thread t1(function_1);
    t1.detach();//网站无法输出,原因:主线程速度太快,还没等t1完成就已经结束了
    //t1.join();编译报错,一个线程一旦被detach(),就不能再join()
    if(t1.joinable())//join()之前可进行判断
    {
        t1.join();
    }
    
	return 0;
}
#include<iostream>
#include<thread>
using namespace std;

void function_1(){
    cout<<"www.oxox.work"<<endl;
}

int main(){
    thread t1(function_1);//t1线程开始运行
    
    try{//如果没有try和catch,当抛出异常时,t1.join()无法执行。加上后,无论是否抛出异常,t1.join()都会被执行
        for(int i=0;i<100;i++)
       {
        cout<<"from main:"<<i<<endl;
       }
    }
    catch(...)
    {
        t1.join();
        throw;
    }
    
    return 0;
}
//使用类作为线程函数创建线程
#include<iostream>
#include<thread>
using namespace std;

class Fctor{
public:
    void operator()(string& msg){
        for(int i=0;i>-100;i--)
            cout<<"from t1:"<<msg<<endl;
            msg="Jomocool";
    }
};

int main(){
    //Fctor fct;
    string s="Jomo";
    thread t1((Fctor()),ref(s); // thread t1(fct);
                      //引用传递,不加ref的话仍然是值传递
                      //thread t1((Fctor()),move(s));将s从主线程移动到移动线程,更加安全可靠
            
    cout<<this_thread::get_id()<<endl;//输出主线程的ID
    cout<<t1.get_id()<<endl;//输出t1线程的ID
             
    try{
        for(int i=0;i<100;i++)
         cout<<"from main:"<<s<<endl;//当t1线程执行第一次后,这行代码将会一直输出from mian:Jomocool
    }
              
    catch(...){
        t1.join();
        throw;
    }
    
    thread::hardware_concurrency();//告诉我们最多可以使用多少个并发进程编程
    return 0;
}
//线程赋值
thread t1(function_1);
thread t2=move(t1);
t2.join();//t1.join()要改成t2.join()
//数据竞争与互斥对象
#include<iostream>
#include<thread>
#include<mutex>
#include<fstream>
using namespace std;

//mutex mu;//互斥对象

/*void shared_print(string msg,int id)//当一个线程正在输出即调用打印函数时,另一个线程将会等待,这样将使输出结                                       果更加整齐
{
    //mu.lock();
    lock_guard<mutex>guard(mu);
    cout<<msg<<id<<endl;
    //mu.unlock();//加锁必解锁
}*/
//不推荐使用lock()和unlock(),因为如果抛出异常,将会一直锁住

class LogFile
{
 public:
    LogFile(){
        f.open("log.txt");
    }
    void shared_print(string id,int value)
    {
        lock_guard<mutex>locker(m_mutex);
        f<<"From"<<id<<":"<<value<<endl;
    }
 protected:
 private:
    mutex m_mutex;
    ofstream f;
}

void function_1(logFile& log){
    for(int i=0;i>-100;i--)
        log.shared_print("From t1:",i);
}

int main(){
    LogFile log;
    thread t1(function_1,ref(log));
    for(int i=0;i<100;i++)
        log.shared_print("From main:",i);
    
    t1.join();
    
    return 0;
}
//死锁
//数据竞争与互斥对象
#include<iostream>
#include<thread>
#include<mutex>
#include<fstream>
using namespace std;

class LogFile
{
    mutex m_mutex;
    mutex m_mutex2;
    
 public:
    LogFile(){
        f.open("log.txt");
    }
    void shared_print(string id,int value)
    {
       /*lock(m_mutex,m_mutex2);
        lock_guard<mutex>locker(m_mutex,adopt_lock);
        lock_guard<mutex>locker2(m_mutex2,adopt_lock);*/
        
        lock_guard<mutex>locker(m_mutex);
        lock_guard<mutex>locker2(m_mutex2);
        f<<"From"<<id<<":"<<value<<endl;
    }
      void shared_print2(string id,int value)
    {
      /*lock(m_mutex,m_mutex2);
        lock_guard<mutex>locker(m_mutex,adopt_lock);
        lock_guard<mutex>locker2(m_mutex2,adopt_lock);*/
          
        lock_guard<mutex>locker2(m_mutex2);
        lock_guard<mutex>locker(m_mutex);
        f<<"From"<<id<<":"<<value<<endl;
    }
}

void function_1(logFile& log){
    for(int i=0;i>-100;i--)
        log.shared_print("From t1:",i);
}

int main(){
    LogFile log;
    thread t1(function_1,ref(log));
    for(int i=0;i<100;i++)
        log.shared_print2("From main:",i);
    
    t1.join();
    
    return 0;
}
//两个线程并没有输出全部的结果,因为造成了死锁

避免死锁:
    第一种解决方案:确保每个lock_guard的顺序都是相同的
    第二种解决方案:用std::lock(m_mutex,m_mutex2)//你有多少个互斥对象就放多少个,有lock就不用管顺序
//unique lock和lazy initialization

class LogFile
{
    mutex m_mutex;
    //mutex m_mumtex_open;
    once_flag m_flag;
    ofstream f;
 public:
    LogFile(){
        //f.open("log.txt");
    }
    void shared_print(string id,int value)
    {
        /*unique_lock<mutex>locker(m_mutex_open,defer_lock);
        if(!f.is_open())
        {
            f.open("log.txt"); 
        }*/
        call_once(,_flag,[&](){f.open("log.txt")});
        unique_lock<mutex>locker(m_mutex,defer_lock);
        //...
        locker.lock()
        f<<"From"<<id<<":"<<value<<endl;
        locker.unlock();
        //...
        
        locker.lock();
        
        unique_lock<mutex>locker2=move(locker);
    }
};

void function_1(logFile& log){
    for(int i=0;i>-100;i--)
        log.shared_print("From t1:",i);
}

int main(){
    LogFile log;
    thread t1(function_1,ref(log));
    for(int i=0;i<100;i++)
        log.shared_print("From main:",i);
    
    t1.join();
    
    return 0;
}
//条件变量 

deque<int>q;
mutex mu;
condition_variable cond;

void function_1(){
    int count=10;
    while(count>0){
        unique_lock<mutex>locker(mu);
        q.push_front(count);
        locker.unlock();
        //cond.notify_one();
        cond.notify_all();//通知线程2运行
        this_thread::sleep_for(chrono::seconds(1));
        count--;
    }
}

void function_2(){
    int data=0;
    while(data!=1){
        unique_lock<mutex>locker(mu);
        cond.wait(locker,[](){return !q.empty()});//等待下一个数据填充到q中
        data=q.back();
        q.pop_back();
        locker.unlock();
        cout<<"t2 got a value from t1:"<<data<<endl;
    }
}

int main(){
    thread t1(function_1);
    thread t2(function_2);
    t1.join();
    t2.join();
}
//Future,Promise,async()
//包含头文件<future>
//代码更加简洁了

void factorial(future<int>&f){
    int res=1;
    
    int N=f.get();
    for(int i=N;i>1;i--)res*=i;
    cout<<"Result is:"<<res<<endl;
    return res;
}
int main(){
    int x;
    
    //主线程获取子线程的变量
    future<int>fu=async(launch::deferred/*不创建子线程*/ | launch::async/*创建子线程   */,factorial);
    x=fu.get();//get()只能被调用一次,但get()被调用时,factorial()将在同一个线程被调用
    cout<<"Get from child"<<x<<endl;
    
    //子线程获取主线程的变量
    promise<int>p;//有promise,就一定要set_value(),否则会抛出异常
    future<int>f=p.get_future();
    future<int>fu=async(launch::async,factorial,ref(f));
    p.set_value(4);
    
    //多线程
    promise<int>p;
    future<int>f=p.get_future();
    shared_future<int>sf=f.share();//sf是由fshare而来,所以有很多副本
    future<int>fu=async(launch::async,factorial,sf);//factorial()参数列表改成shared_future<int>
    future<int>fu2=async(launch::async,factorial,sf);
    future<int>fu3=async(launch::async,factorial,sf);

    
    p.set_value(4);
    return 0;
}

//future,promise不能被copy,只能被move
//使用可调用对象

class A
{
public;
    void f(int x,char c){}
    int operator()(int N){return 0;}
}

void foo(int x){}

int main(){
    A a;
    
    thread t1(a,6);//传递a的拷贝给子线程
    thread t2(ref(a),6);//传递a的引用给子线程
    thread t3(move(a),6);//a在主线程中将不再有效
    thread t4(A(),6);//传递临时创建的A对象给子线程
    
    thread t5(foo,6);//传递全局函数给子线程
    thread t6([](int x){return x*x;},6);//传递lambda表达式给子线程
    
    thread t7(&A::f,a,8,'w');//传递A的拷贝的成员函数给子线程
    thread t8(&A::f,&a,8,'w');//传递A的地址的成员函数给子线程
    
    thread
    
    async(launch::async,a,6);//async()也适用上面的传递方式
    
    return 0;
}
//packaged_task

int factorial(int N){
    int res=1;
    for(int i=N;i>1;i--)
        res*=i;
    cout<<"Result is:"<<res<<endl;
    return res;
}

deque<packaged_task<int()>>task_q;
mutex mu;
condition_variable cond;

void thread_1()
{
    packaged_task<int()>t;
    {
        unique_lock<mutex>locker(mu);
        cond.wait(locker,[]{return !task_q.empty();});//保证push_back在front之前调用
        t=move(task_q.front());
    }
    t();
}

int main(){
    thread t1(thread_1);
    packaged_task<int()>t(bind(factorial,6);
    future<int>ret=t.get_future();//获得与packaged_task共享状态相关联的future对象
                         
    {
         lock_guard<mutex>locker(mu);
         task_q.push_back(move(t);
    }
    cond.notify_one();
    int value=ret.get();//等待任务完成并获取结果 
                          
    t1.join();
                                                   
    return 0;
}
//回顾和时间约束

int factorial(int N){
    int res=1;
    
    for(int i=N;i>1;i--)
        res*=i;
    cout<<"Rusult is:"<<res<<endl;
    
    return res;
}

deque<packaged_task<int()>>task_q;
mutex mu;
condition_variable cond;

int main(){
    //
    thread t1(factorial,6);
    this_thread::sleep_for(chrono::milliseconds(3));
    chrono::steady_clock::time_point tp=chrono::steady_clock::now()+chrono::milliseconds(4);
    this_thread::sleep_until(tp);
    //
    mutex mu;
    unique_lock<mutex>locker(mu);
    locker.try_lock_for(chrono::milliseconds(3));
    locker.try_locker_until(tp);
    //
    condition_variable cond;
    cond.wait_for(locker,chrono::milliseconds(3));
    cond.wait_until(tp);
    //
    promise<int>p;
    future<int>f=p.get_future();
    f.wait_for(chrono::milliseconds(3));
    f.wait_until(tp);
    
    return 0;
}

11.4原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。

image-20220719115158893