编码基本功(一):C++思想及设计模式

CPP

目录

CPP API

自我学习代码路径

设计模式与思想(还需参考muduo优化)

// 智能指针单例

// 饿汉式单例的实现
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{

private:

  CSingleton(){ cout << "单例对象创建!" << endl; };
  CSingleton(const CSingleton &);
  CSingleton& operator=(const CSingleton &);
  ~CSingleton(){ cout << "单例对象销毁!" << endl; };
  static void DestroyInstance(CSingleton *){ cout << "在这里销毁单例对象!" << endl; };//注意这里
  static shared_ptr<CSingleton> sington_; 
  
public:
  static CSingleton& getInsance(){  
    if (!sington_) {
      std::lock_guard<std::mutex> lock(mutex_);
      if (nullptr == sington_) {
        sington_ = std::make_shared<CSingleton>(); // 用make_shared效率更高,代码更简洁
      }
    }
    return sington_;  
  }
  
};
 
#endif




//主文件,用于测试用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"
 
using namespace std;
shared_ptr<CSingleton> CSingleton::myInstance(new CSingleton(), CSingleton::DestroyInstance);
int main()
{
  shared_ptr<CSingleton>  ct1 = CSingleton::getInstance();
  shared_ptr<CSingleton>  ct2 = CSingleton::getInstance();
  shared_ptr<CSingleton>  ct3 = CSingleton::getInstance();
  
  return 0;
}


使用unique_ptr

#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{

private:

  CSingleton(){ cout << "单例对象创建!" << endl; };
  CSingleton(const CSingleton &);
  CSingleton& operator=(const CSingleton &);
  ~CSingleton(){ cout << "单例对象销毁!" << endl; };
  static void DestroyInstance(CSingleton *){ cout << "在这里销毁单例对象!" << endl; };//注意这里
  static unique_ptr<CSingleton> sington_; 
  
public:
  static CSingleton& getInsance(){  
    static std::once_flag flag;
    std::call_once(flag, [&]() {
      sington_.reset(new T);
    });
    return *sington_;  
  }
};

std::unique_ptr<CSingleton> CSingleton::sington_;

设计原则

  • 开放封闭原则:对扩展开放,对修改关闭
  • 里氏代换原则:任何基类可以出现的地方,子类一定可以出现
  • 合成复用原则:尽量使用组合/聚合的方式,而不是使用继承
  • 接口隔离原则:降低类之间的耦合度
  • 迪米特法则:又称最少知道原则(Demeter Principle):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
其它原则
  • 大三律: 如果声明并实现了复制构造函数、复制赋值运算符,或析构函数中的任何一个,就必须同时声明和实现所有这三个。

创建型模式

  • 单例
    • 单例模式的主要优点在于提供了对唯一实例的受控访问并可以节约系统资源;其主要缺点在于因为缺少抽象层而难以扩展,且单例类职责过重。
    • 应用实例: 1. 一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号,否则会出现主键重复,因此该主键编号生成器必须具备唯一性,可以通过单例模式来实现。2. Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
工厂模式
  • 简单工厂模式: 由产品抽象类、具体产品类、工厂类。工厂中可选择创建所有产品。缺点:简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。
  • 工厂方法模式: 现在对该系统进行修改,不再设计一个按钮工厂类来统一负责所有产品的创建,而是将具体按钮的创建过程交给专门的工厂子类去完成,我们先定义一个抽象的按钮工厂类,再定义具体的工厂类来生成圆形按钮、矩形按钮、菱形按钮等,它们实现在抽象按钮工厂类中定义的方法。 即现在不是一个工厂类,而是一个抽象工厂加具体的工厂,不同的具体工厂生产不同产品。当需要生产新产品时,创建新的具体工厂即可。 缺点:在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销
  • 抽象工厂模式:抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。缺点:在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
  • 三者之间关系: 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。
构建器模式(建造者模式)
  • 称呼为构建器模式更好,这个模式更侧重组合,如果将抽象工厂模式看成汽车配件生产工厂 ,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂 ,通过对部件的组装可以返回一辆完整的汽车。
  • 指挥者指挥builder构建Product产品,最终的Product产品在builder手中
  • 应用实例:1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。

结构型模式

  • 适配器模式
    • 将一个接口转换成客户希望的另一个接口。 本质上就是继承原先接口类,然后实现是使用一个成员类的方法实现
    • 应用实例:1.美国电器110V,中国220V,就要有一个适配器将110V转化为220V; 2. 在LINUX 上运行WINDOWS程序
  • 桥接模式
    • 将抽象部分与实现部分分离,使它们都可以独立的变化。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
    • 桥接器里面,将实现类作为一个成员。
    • 应用实例: 不同开关和不同电器,有的开关是按下去,有的开关是点击,但开关都可以用来控制不同电器的关闭和开启。详见附件代码。
  • 享元模式
    • 享元模式通过共享技术实现相同或相似对象的重用。在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象。在享元模式中共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。这个模式不太重要,有用到再看。
  • 外观模式
    • 它向现有的系统添加一个接口,来隐藏系统的复杂性。为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
    • 应用实例: 1、去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
代理模式
  • 代理模式的核心在于间接访问。一个讲代理模式的好网站
  • 在某些情况下,一个客户不想或者不能直接引用一个对 象,此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起到 中介的作用,并且可以通过代理对象去掉客户不能看到 的内容和服务或者添加客户需要的额外服务。通过引入一个新的对象(如小图片和远程代理 对象)来实现对真实对象的操作或者将新的对 象作为真实对象的一个替身,这种实现机制即 为代理模式,通过引入代理对象来间接访问一 个对象,这就是代理模式的模式动机。
  • 代理模式和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
  • 应用实例: 1、Windows 里面的快捷方式。2、买火车票不一定在火车站买,也可以去代售点。
装饰模式
  • 动态地为类的方法增加功能,避免为重新继承每个类来扩增功能的琐碎。即动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
  • 在不想增加很多子类的情况下扩展类或者需要动态扩展类的情况下特别适合用装饰器模式。装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 装饰类和被装饰的类都继承自同一个基类。装饰器类会调用被装饰的类的方法,并额外增加一些功能。
  • 应用实例:变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔。

行为型模式

  • 命令模式
    • 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
    • 定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口。看下面这段代码就懂了。
Receiver * pReceiver = new Receiver();
ConcreteCommand * pCommand = new ConcreteCommand(pReceiver);
Invoker * pInvoker = new Invoker(pCommand);
pInvoker->call();
  • 模板方法模式
    • 在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
    • 意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
  • 中介者模式
    • 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
    • 以聊天为例,所有的对话都通过中介来转发,A说消息发给B,发什么告知中介,中介查找B,找到后调用B的接口让B接收消息。
    • 优点:降低了类的复杂度,将一对多转化成了一对一
    • 缺点:中介者会庞大,变得复杂难以维护
  • 策略模式
    • 看下面这段代码就懂了
        Strategy * s1 = new ConcreteStrategyA();
        Context * cxt = new Context();
        cxt->setStrategy(s1);
        cxt->algorithm();
    
        Strategy *s2 = new ConcreteStrategyB();
        cxt->setStrategy(s2);
        cxt->algorithm();
    
    • 应用举例:旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。
  • 职责链模式
    • 应用举例: 流程审批
    • 看下代码就理解了
状态模式
  • 有限状态机的C++版本
  • 在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
  • 应用实例: TCP的状态转换图
观察者模式(发布订阅模式)
  • 观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
  • 建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。
  • 观察者模式的缺点: 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • 应用实例:订阅者订阅博客,博主更新后,通知给订阅者。
访问者模式
  • 意图:主要将数据结构与数据操作分离。主要解决:稳定的数据结构和易变的操作耦合问题。比如有很多属性,然后通过不同的访问者访问不同的属性。
  • 何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
  • 如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
  • 关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
Reactor模式
  • Reactor 是一种应用在服务器端的开发模式(也有说法称 Reactor 是一种 IO 模式),目的是提高服务端程序的并发能力。
  • 它要解决什么问题呢?传统的 thread per connection 用法中,线程在真正处理请求之前首先需要从 socket 中读取网络请求,而在读取完成之前,线程本身被阻塞,不能做任何事,这就导致线程资源被占用,而线程资源本身是很珍贵的,尤其是在处理高并发请求时。而 Reactor 模式指出,在等待 IO 时,线程可以先退出,这样就不会因为有线程在等待 IO 而占用资源。但是这样原先的执行流程就没法还原了,因此,我们可以利用事件驱动的方式,要求线程在退出之前向 event loop 注册回调函数,这样 IO 完成时 event loop 就可以调用回调函数完成剩余的操作。所以说,Reactor 模式通过减少服务器的资源消耗,提高了并发的能力。当然,从实现角度上,事件驱动编程会更难写,出问题更难调试。
  • 我们用“餐厅”类比的话,对于每个新来的顾客,前台都需要找到一个服务员和厨师来服务这个顾客。服务员给出菜单,并等待点菜;顾客查看菜单,并点菜; 服务员把菜单交给厨师,厨师照着做菜; 厨师做好菜后端到餐桌上这就是传统的多线程服务器。每个顾客都有自己的服务团队(线程),在人少的情况下是可以良好的运作的。现在餐厅的口碑好,顾客人数不断增加,这时服务员就有点处理不过来了。这时老板发现,每个服务员在服务完客人后,都要去休息一下,因此老板就说,“你们都别休息了,在旁边待命”。这样可能 10 个服务员也来得及服务 20 个顾客了。这也是“线程池”的方式,通过重用线程来减少线程的创建和销毁时间,从而提高性能。但是客人又进一步增加了,仅仅靠剥削服务员的休息时间也没有办法服务这么多客人。老板仔细观察,发现其实服务员并不是一直在干活的,大部分时间他们只是站在餐桌旁边等客人点菜。于是老板就对服务员说,客人点菜的时候你们就别傻站着了,先去服务其它客人,有客人点好的时候喊你们再过去。
  • Reactor 模式的核心思想:减少等待。当遇到需要等待 IO 时,先释放资源,而在 IO 完成时,再通过事件驱动 (event driven) 的方式,继续接下来的处理。从整体上减少了资源的消耗。
  • 一个讲述Reactor的好网站

进阶

  • 任何单参数的构造函数都必须是explicit,以避免后台的类型转换
  • 返回值传递:如果返回的对象是类类型的,更好的办法是使用按常量引用返回以节省复制的开销。使用常量返回时,必须确保返回语句中的表达式在函数返回时依然有效。
  • 返回值优化: return Integer (left.i+right.i); 与 Integer tmp(left.i+right.i); return tmp; (Integer 是类名)是完全不一样的前者编译器直接地把这个对象创建在外部返回值的内存单元中,因为不是真正地创建一个局部对象,效率非常高,这种方式常被称为返回值优化。
  • 组合与继承
    • 如果想重用已存在类型作为新类型的内部实现的话最好使用组合(has-a关系)
    • 如果想使新的类型和基类的类型相同(类型一样可确保接口一样),则应使用继承(is-a关系)

CPP语法

引用

  • 声明一个引用时,必须同时对它进行初始化,使它指向一个已存在的对象;一旦一个引用被初始化之后,就不能改为指向其它对象。
    void swap(int &a,int &b); swap(x,y);//引用调用,相当于地址传递,会改变实参。
    int &ri=i; ri=10;(equivalent to i =10)//从而ri就是i的别名。引用必须在定义时就初始化,且以后不可指向其他对象。
    
  • 引用也能用于实现多态。
  • 引用和指针的区别:你也可以把引用看做是通过一个常量指针来实现的,它只能绑定到初始化它的对象上。引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率,缺点是降低了灵活性。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。

结构体

  • 结构体变量声明时可以省略struct (C语言中不行).例 struct stu nem; 可以写成 stu nem
  • 类class和结构体struct的区别
    • class中默认访问权限是private,struct中则是public;class中默认继承是public,而struct默认是public;class和struct如果定义了构造函数,就都不能用大括号进行初始化,如果没有定义,则struct可以用大括号进行初始化,而class只有在所有变量都是public情况下,才可以用大括号初始化。

函数相关

内联函数
  • 内联函数 inline. 使用于规模较小但又使用频繁的函数。对于类中函数,如果将函数说明(函数体)直接放在类中,则也是内联函数。内联函数的定义必须出现在第一次被调用之前,一般不能有循环和switch语句。
带默认形参函数
  • 函数在定义时预先声明默认的形参值,调用时如果给出实参,则用实参初始化形参;如果没有给出实参,则采用预先声明的默认形参值。(在默认值的形参右面不能出现无默认值的形参)int add(int x=5, int y=6) { return x+y;} add(); add(10,20);add(10) 结果为11,30,16.
函数重载
  • 形参必须不同,即个数不同或者类型不同(返回类型对重载无影响)。int add(int x, int y) ; float add(folat x , float y); 以及 int add(int x, int y); int add(int x,int y, int z);都满足。
  • 不能根据一个版本的参数是给定类型,另一个版本的参数是该类型的引用来重载。
  • 一个函数参数是指向type的指针,而另一个函数的参数是指向const type的指针,则这两个函数可以重载;
  • 指向type的引用和指向const type的引用也可以重载,但是type和const type则不行。
  • const关键字可重载

CONST

常量引用
  • 引用会改变实参,但往往不易察觉。如果想利用引用的高效,同时不打算修改传送过来的参数,则使用常量引用 void larger(const int &a, const int &b)
常对象
  • A const a(3,4),常对象必须进行初始化,而且不能被更新,且常对象只能调用它的常成员函数
常成员函数
  • 常成员函数:(语法:类型 函数名(参数表)const)常成员函数可以访问但不能更新(修改)对象的数据成员,也不能调用该类中没有用const修饰的成员函数。(这样就保证了常成员函数绝对不会更改数据成员的值)。比如显示函数等就可以使用常成员函数。
常数据成员
  • 常数据成员 const int a; 类的常数据成员只能通过初始化列表来获得初始值。

类型转换

  • reinterpret_cast<指针类型>(表达式)运算符允许强制转换任何指针类型,其限制是如果要转换的指针类型声明为const,就不能把它转换为不是const的类型。例 long* pnumbe= reinterpret_cast<long*>(pvalue);这是一种最不安全的转换,应尽量不使用。假如将A对象转换为某个类型,则当再次需要使用A时,A已经不同了,以至于不能用于类型的原来目的,除非再次把它转换回来。
  • static_cast<类型>(表达式) 把表达式转换为给定类型, 与C类似,int,double,及基本类型的指针的转换等。
    • 可用于类层次结构中基类和子类之间指针或引用的转换。
    • 进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
    • 进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的
  • const_cast<类型>(表达式) 从const转换成非const或从volatile转换为非volatile。四种类型转换中,只有它能实现这个功能。
  • dynamic_cast<type-id>(expression) 只能应用于多态类类型的指针和引用,即至少包含一个虚函数的类类型,也即type-id和expression都必须包含虚函数。该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
    • 当我们将dynamic_cast用于某种类型的指针或引用时,只有该类型含有虚函数时,才能进行这种转换。否则,编译器会报错。
    • e能成功转换为type*类型的情况有三种:
      • e的类型是目标type的公有派生类:派生类向基类转换一定会成功。
      • e的类型是目标type的基类,当e是指针指向派生类对象,或者基类引用引用派生类对象时,类型转换才会成功,当e指向基类对象,试图转换为派生类对象时,转换失败。
      • e的类型就是type的类型时,一定会转换成功
    • 动态强制转换有两种类型,第一种是沿着类层次结构向下进行强制转换,即从直接或简介基类的指针转化为派生类的指针。
    • 第二种是跨类层次的转换。使用时要检查是否为空。
      carton* pcarton = dynamic_cast<carton*>(pbox)  
      if(pcarton != nullptr) pcarton->surface();
      对于引用,由于没有诸如空引用的情况,这就需要采用捕获bad_cast异常来了解类型转换是否成功。try{…}catch(bad_cast&){…}
      
    • dynamic_cast运算符的主要用途:将基类的指针或引用安全地转换成派生类的指针或引用,来调用派生类的非虚函数
      class Father
      {
      public:
          virtual void print(){cout<<"I'm Father"<<endl;}
          void fantastic(){cout<<"father fantastic"<<endl;}
      };
      class Son:public Father
      {
      public:
          void print(){cout<<"I'm Son"<<endl;}    
          void fantastic(){cout<<"son fantastic"<<endl;}
      };
      int main()
      {
          Son son;
          Father father;
        #if 0
          Father *pFather = &father; 如果运行这个,则转化会失败,即pSon为空
        #else
          Father *pFather = &son;
        #endif
          pFather->print(); 
          Son *pSon = dynamic_cast<Son*>(pFather);
          if(pSon!= NULL) {
              pSon->print();
              pSon->fantastic();
          }
          return 0;
      }
      

友元

  • 通过友元关系,一个普通函数或者类的成员函数可以访问和修改封装于另外一个类中的数据。友元关系是单向的,不能传递的,不被继承的。
  • 友元函数: 友元函数可以是普通函数也可以是其他类的私有和保护成员(在类中声明),在它的函数体中可以通过对象名访问和修改类的私有和保护成员。例:friend float fDist(Point &a,Point &b)
  • 友元类: 若类A为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问和修改B类的私有和保护成员。firend class A;

指针

指向类的非静态成员的指针
  • 指针只能指向公有成员
  • 指向类公有数据成员的指针
    • 声明,类型说明符 类名::*指针名
    • 赋值,指针名=& 类名::数据成员名
    • 引用,对象名.*类成员指针名或对象指针名->*类成员指针名
  • 指向类公有函数成员的指针
    • 声明,类型说明符(类名::*指针名)(参数表)
    • 赋值,指针名=类名::函数成员名
    • 调用,(对象名.*类成员指针名)(参数表)或(对象指针名->*类成员指针名)(参数表)
指向类的静态成员的指针
  • int *const=&Point::countP;(这里countP必须是公有数据) ,void(*gc)()=Point::GetC; 类外指针只能指向公有成员。
this指针
  • this是常量指针
  • this 是一个隐含于 每一个类的成员函数中的特殊指针(包括构造和析构函数),它用于指向正在被成员函数操作的对象。

new 和 delete

  • 动态内存分配
    • int *point; point=new int(2); /*动态分配用于存放int类型数据的内存空间,并将初值2存入该空间,然后将首地址赋给指针point*/
    • 运算符delete用来删除由new建立起来的对象,格式为 delete 指针名。如果是用new建立的数组,用delete删除是要在指针名前加上"[ ]" 例 Point *Ptr=new Point[2]; delete [ ] Ptr; int *p=new int[10]; delete [ ]p
    • float (*cp)[25][10]; cp=new float [30][25][10]; cp[i][j][k]等价于采用数组名[i][j][k]调用一个三维数组[30][25][10]的元素。
    • C++继承了C中的动态存储管理函数。
  • 深入理解new和delete
    • new有三种形态:new operator , operator new, placement new. new opeartor 即是我们熟悉的那个new,
    • 当调用new operator时,它做了三件事:
      • 它从堆中划分一块区域,自动调用构造函数,最后返回指向该区域的指针。第一步内存申请是通过opeartor new(仅仅是分配内存,不初始化) 完成的。
      • 第二步关于调用什么构造函数则是通过placement new 来决定的,placement new 主要是用来定位构造函数的,可以通过它来选择合适的构造函数。如果要在一块已经获得的内存里创建一个对象,那就应该调用placement new。例:int a[10]; X *xp = new(a) X(47) // X at location a xp->X::~(); 简化版例子,如需使用,详见C++编程思想p685或查找资料
    • 当使用new operator申请一块内存(opeartor new处理)失败时,opeartor new首先会调用一个错误处理函数new_handler进行相应的处理,如果处理成功则返回地址,否则转而去调用另外一个new_handler,然后继续前面的过程,直到new_handler中调用exit()或类似的函数,使程序结束或者new_handler中抛出异常,std::bad_alloc是C++标准中规定的标准行为,所以推荐使用try{p=new int[SIZE];}catch(std:bad_alloc){……}的处理方式。
    try{
        int*pStr=new string[SIZE];
        //processing codes
    }catch(const ::std::bad_alloc &e){
        /*processing codes*/
        return-1;
    }   // 不要使用C中的if(pStr == NULL), 可以使用 int *pSTR = (::std::nothrow)new int[SIZE];
    
    
    • 如果想对一个void*类型指针进行delete操作, 要注意这将可能成为一个程序错误, 除非指针所指的内容是非常简单的, 因为它将不执行析构函数。如果在程序中发现内存丢失的情况, 那么就搜索所有的delete语句并检查被删除指针的类型。 如果是void*类型, 则可能发现了引起内存丢失的某个因素( 因为C++还有很多其他的引起内存丢失的因素)

构造函数

  • 若未声明构造函数, 则编译器会生成一个默认的构造函数, 这有可能导致某些成员未被初始化或被初始化为不恰当的值。构造函数中不可直接调用另外一个构造函数(如果仅仅为了一个构造函数重用另一个构造函数的代码,那么完全可以把构造函数中的公共部分抽取出来定义一个成员函数(推荐为private),然后在每个需要这个代码的构造函数中调用该函数即可, 也可以考虑使用默认形参)。
  • 常量和引用,必须通过参数列表进行初始化
  • 在构造函数调用返回之前,虚函数表尚未建立,不能支持虚函数机制,所以构造函数不允许设为虚
  • 在初始化成员变量时,出于对方法通用性及高效性的考虑,强烈推荐采用成员变量初始化列表的方式初始化变量
  • 构造的顺序 构造是从类层次的最根处开始,而在每一层,首先会调用基类构造函数(调用顺序按照它们被继承时说明的顺序), 然后调用成员对象构造函数(按照声明顺序), 最后才是类自己的构造函数。调用析构函数则严格按照构造函数相反的次序。另一个有趣现象是,对于成员对象,构造函数调用的次序完全不受构造函数的初始化表达式表中的次序影响。 该次序是由成员对象在类中声明的次序所决定的。

拷贝构造函数

  • 拷贝构造函数在三种情况下被调用:
    1. 当用一个类的对象去初始化该类另一个对象时(Point B(A) 或 Point B = A) , 注意如果是Point A, B; B=A 则调用的是 = 运算符函数(如果有,没有就默认的);
    2. 函数形参和实参结合时;
    3. 返回值是类的对象,函数执行完返回调用者时。
  • 深拷贝与浅拷贝: 见代码(需要深拷贝的情况主要出现在类的数据成员有指针的情况下)

类的成员

类的静态成员
  • 如果某个属性为整个类所共享,不属于任何一个具体对象,则采用static关键字来声明为静态成员。静态成员由该类的所有对象共同维护和使用,从而实现同一类不同对象之间的数据共享。静态数据成员具有静态生存期,只能通过类名对它进行访问,一般用法是类名::标识符。静态数据成员应该通过非内联函数来访问,而且访问静态数据成员的函数,其函数体应该与静态成员的初始化写在同一个源文件程序中。在类的定义中仅仅对静态数据成员进行声明,必须在文件作用域的某个地方使用类名限定对静态数据成员进行定义,这时也可以初始化
类的静态成员函数
  • 静态成员函数主要用于访问一个类中的静态数据成员,维护对象之间共享的数据
  • 除具备类的静态成员的性质外,静态成员函数可以直接访问该类的静态数据和静态函数成员,而访问非静态数据和非静态成员必须通过参数传递的方式得到对象名,然后通过对象访问。void Point::f(Point a){cout<<x; /*错误*/ cout<<a.x; /*正确*/},但最好不要这样使用,因为如果需要这样的话普通函数更好,没必要使用静态函数 ,。既可以使用类名,也可以使用对象名来调用静态成员函数(Point::GetC( ) )

继承与派生

  • 派生类将继承基类公有成员中除构造和析构函数之外的所有成员(私有和保护不可继承)。
  • 无论怎样的继承方式,基类的私有成员都不可被派生类的成员或对象直接访问,也就是说虽然派生类继承了基类的私有成员,但访问还是要通过基类名::成员函数(参数表)访问,派生类就像个没实权的继承人。
  • 公有和保护成员可以直接被派生类访问;
  • 公有继承时,基类的公有和保护成员的属性不变(仍作为派生类的公有成员和保护成员);
  • 私有继承时,公有和保护都变私有;
  • 保护继承,公有和保护都变保护。保护的属性与私有类似。
派生类的构造函数和析构函数
  • 如果基类声明了带有形参表的构造函数时,派生类就应当声明构造函数
  • 子类只能通过在构造函数里通过初始化表达式表调用基类构造函数
  • 构造的顺序 构造是从类层次的最根处开始,而在每一层,首先会调用基类构造函数(调用顺序按照它们被继承时说明的顺序), 然后调用成员对象构造函数(按照声明顺序), 最后才是类自己的构造函数。调用析构函数则严格按照构造函数相反的次序。另一个有趣现象是,对于成员对象,构造函数调用的次序完全不受构造函数的初始化表达式表中的次序影响。 该次序是由成员对象在类中声明的次序所决定的。
  • 虽然常常需要在初始化表达式表中做显式构造函数调用, 但并不需要做显式的析构函数调用, 因为对于任何类型只有一个析构函数, 并且它并不取任何参数。析构函数会自动调用,不需要显式调用
派生类的拷贝构造函数
  • 例: C类是B类的派生类C::C(C&c1):B(c1){…}(这里B中放c1是由于类型兼容原则,可以由派生类的引用去初始化基类的引用)
作用域分辨(名字隐藏/覆盖)
  • 如果派生类声明了一个和某个基类成员同名(只需要名字相同)的新成员,派生的新成员就隐藏基类的成员,直接使用成员名只能访问派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,派生类的函数就隐藏基类的函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。这时如果要访问基类的被隐藏的成员,就需要使用作用域分辨符和基类名来限定。格式为针对数据成员的 基类名::成员名; 和针对函数成员的 基类名::成员名(参数表), 无法通过子类无法访问(编译器会报错),就好像子类中没有该成员一样。
类型兼容原则(向上类型转换)
  • 是指在需要基类对象的任何地方,都可以用公有派生类的对象来代替,在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。派生类对象可以赋值给基类对象、可以初始化基类对象的引用、派生类对象的地址也可以赋给基类指针。 替代之后,派生类对象就可以作为基类的对象使用,但仅仅只是发挥出基类的作用,例:如果基类Instructment和派生类Wind中有一个同名函数比如play(),则当Wind w; Instructment *ip = &w; ip->play();只能调用Instructment::play(),而不是Wind::play(). 且只能访问从基类继承的成员。
虚基类
  • 当某类的部分或全部直接基类是从另一个共同的基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称,因此派生类就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来惟一标识。例A是基类,B0、B1是A的派生类,C是B0和B1的派生类,A中有一个函数fun(),则B0、B1两者有一个同样的函数fun(),则C中有两个fun()函数,C在使用fun时必须这样使用,C c1; c1.B0::fun();
    使用虚基类可以解决上述问题,它可以将共同基类设置为虚基类(格式为class 派生类名:virtual 继承方式 基类名),设置是将B0,B1以(class 派生类名:virtual 继承方式 基类名)从继承的,然后C以继承方式 B0, 继承方式 B1 来继承。这样从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。建立一个对象时,如果这个对象含有从虚基类继承的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化,,而且,只有最远派生类的构造函数会调用类的构造函数,该派生类的其他基类(B0、B1)都不会。

多态

虚函数
  • 当派生类中声明一个与基类相同的函数时,如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,问题是访问到的只是从基类继承来的同名成员,而不是派生类中新增的同名函数。虚函数可以解决上述问题,可以使得通过基类指针访问到的是派生类的同名函数,这样通过基类指针(引用也可以,简单赋值不行)可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程中的多态
  • 重写继承来的虚函数时,如果函数有默认的形参值,千万不要重新定义不同的值,因为通过基类指针来访问派生类的函数时,默认形参值是基类的而非派生类的。
  • 在构造函数调用返回之前,虚函数表尚未建立,不能支持虚函数机制,所以构造函数不允许设为虚
  • 在每个带有虚函数的类中, 编译器秘密地放置一个指针,称为vpointer, 指向这个对象的VTABLE(在VTABLE中, 编译器放置特定类的虚函数的地址)。 当通过基类指针做虚函数调用时( 也就是做多态调用时) , 编译器静态地插入能取得这个vpointer并在VTABLE表中查找函数地址的代码, 这样就能调用正确的函数并引起晚捆绑的发生。
  • 静态函数不能是虚函数,虚函数必须是非静态的成员函数
  • 不要在构造/析构函数中调用虚函数。因为这种调用不会如你所愿,既无法让你获得梦想的多态,还会带来了一系列令人头疼的问题。请记住:如果在构造函数或析构函数中调用了一个类的虚函数,那它们就变成普通函数了,失去了多态的能力。换句话说就是,对象不能在生与死的过程中让自己表现出“多态”
  • 虚函数在子类中改变访问缺权限,并且子类即使是private,指向子类的基类指针仍然能够访问。
class Father
{
public:
    virtual void print(){cout<<"I'm Father"<<endl;}
};
class Son:public Father
{
private:
    void print(){cout<<"I'm Son"<<endl;}
};
int main()
{
    Son son;
    Father *pFather = &son;
    pFather->print(); //不会报错
    return 0;
}
/*说明:虚函数编译时的访问权限仅仅是和调用者(指针、引用)的类型有关,编译器只要知道这个类型有一个可以访问的虚函数就行了,并不查看具体对象类型中该虚函数的访问权限*/
  • 虚函数的名字隐藏, 和其它非虚函数一样, 也会被隐藏
class Father
{
public:
    virtual void print(){cout<<"I'm Father"<<endl;}
    virtual void print(int val) { cout <<"I'm Father Val:" <<val <<endl;}
};
class Son:public Father
{
public:
    void print(){cout<<"I'm Son"<<endl;}    
};
int main()
{
    Son son;
    Father *pFather = &son;
    pFather->print();
    //son.print(1)  不注释的话编译器会报错 ,父类的其它同名函数被隐藏
    return 0;
}
虚析构函数
  • 当用基类的指针指向派生类时,如果基类的析构函数不为虚函数,则删除基类指针时,只会调用基类的析构函数,不会调用派生类的析构函数,造成内存泄漏。当将基类的析构函数设置为虚析构函数后,当删除指向派生类的基类指针时,基类和派生类的析构函数都会被调用。
  • 任何时候我们的类中都要有一个虚函数,我们应当立即增加一个虚析构函数( 即使它什么也不做) 。 这样,我们保证在后面不会出现问题。
纯虚函数
  • 是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本。格式:virtua 函数类型 函数名 (参数表)= 0
抽象类
  • 带有纯虚函数的类是抽象类。抽象类不能实例化,既不能定义一个抽象类的对象,但是我们可以声明一个抽象类的指针和引用。通过指针或引用,我们就可以访问派生类对象,进而访问派生类的成员。

类的大小

  • 类的大小计算汇总
  • 类的大小,与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数、静态成员函数、静态数据成员、静态常量数据成员,均对类的大小无影响;
  • 虚函数对类的大小有影响,是因为虚函数表指针带来的影响;
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响

模板

类模板
  • 模板体看起来类似于普通的类定义,只是在许多地方都使用了T,在实例化类模板,生成一个类定义时,T就会被用于实例化模板的类型代替。如果需要在类外定义其成员函数,则需要采用template <模板参数表> 类型名 类名<T>::函数名(参数表)形式,例:template <typename T> Array <T> :: Array(const Array& theArray), Array 是一个类名。模板类的成员函数必须是函数模板。声明Array类时 格式:Array<double> D ,或Array<double>D(2),后一种声明时还初始化了。无非将Array变为Array<double>,其他声明也类似。但声明之后,就可以像正常类使用了,比如这里D要调用一个成员函数,直接D.成员函数名
函数模板
  • 例:template <class T> T max(T a , T b) (class 或typename均可); 和普通函数一样,可以先声明后定义,也可以立即定义。可以显式指定模板参数(隐式就是编译器根据实参来确定返回类型),比如当a为int,b为long时编译器就不能生成合适的函数,这时可以显式指定模板参数:max<long>(a,b) ; 如果max(&a,&b)会出得到不想要的结果,可通过模板说明来处理(略)或者重新写一个针对max(&a,&b)的函数,在使用时会优于重载函数。带有多个参数的模版:template<class Treturn, class Targ> T return max (Targ, Targ b); 因为编译器不能推断返回值的类型T return,所以必须指定该类型。max<int>(1.4, 2.4); 还可以指定T returnT arg max<double,double>(1.4, 3.5). 函数模板中参数的定义是非常重要的,如果返回类型定义为第二个参数,函数调用中就必须总是指定两个参数。

运算符重载

  • 当是重载自增和自减运算符时,且在类中定义时,则后置时重载函数中必须有一个int形参(只写一个int就可以),而前置时则无形参。详见代码。运算符也可以重载为类的友元函数,参数的个数和原操作数个数相同,这时在函数形参表中形参从左到右的顺序就是运算符操作数的顺序。当运算符重载为类的成员函数时,函数的参数的个数比原来的操作数个数要少一个(后置++和后置—除外),少了的操作数就是该对象本身, 且该对象本身对应左边的操作数。

RTTI

  • 运行时类型识别,它提供了运行时确定对象类型的方法。两个重要的 RTTI 运算符的使用方法,它们是 typeid 和 dynamic_cast ,typeid的主要作用就是让用户知道当前的变量是什么类型的。用法示例:typeid(对象名).name(), 不能存储typeid操作的结果。 必须像例子描述的那样来使用它。它不能与void型指针一起工作,因为一个void*真实的意思是“无类型信息”。 RTTI最主要的副作用是﹕程序员可能会利用RTTI来支持其「复选」(multiple-selection)方法﹐而不使用虚函数(virtual function)方法。虽然这两种方法皆能达到多态化(polymorphism) ﹐但使用复选方法﹐常导致违反著名的「开放╱封闭原则」(open/closed principle) ,优点参考java的RTTI

异常处理

  • 不推荐使用异常处理
  1. try块后面必须紧跟catch块,所抛出的异常对象不能是指向try块的局部对象的指针。
  2. try块中一旦抛出异常,则try中抛出异常语句后面的语句将不再执行。
  3. 抛出异常的代码(throw语句)只需要逻辑上位于try块中即可。也就是说,如果try块中调用一个函数,在该函数中抛出的任何异常都会被该try块的某个catch块捕获。一个函数中有throw语句,则应在try中调用该函数。
  4. 嵌套的try块:内存的catch处理程序可以捕获在内层try块中抛出的异常。而外层的try块可以捕获外层的以及内层未捕获的异常。
  5. 对于类类型的异常,会自动进行类型转换(throw和catch),以匹配异常类型和处理程序的参数类型。出现以下情形就是匹配:
  6. 参数类型(catch)和异常类型(throw)完全匹配,忽略const
  7. 参数类型是异常类型的基类或间接基类,或是异常类的直接或间接基类的引用,忽略const
  8. 异常和参数都是指针,异常类型可以自动转化为参数类型,忽略const
  9. 在处理程序捕获一个异常时,可以重新抛出该异常,让外层try块的处理程序捕获它(内层不可以捕获)。语句为throw 不包括throw表达式。如果catch块写成catch(…)表示该块处理所有异常。
  10. C++的异常处理必须确保当程序的执行流程离开一个作用域的时候,对于属于这个作用域的所有由构造函数建立起来的对象(非指针), 它们的析构函数一定会被调用。但当构造函数没有正常结束时不会调用相关联的析构函数,
  11. 标准库异常所有的标准异常类归根结底都是从exception类派生的
  12. 但是异常也并非十全十美的。作为C++的一个高级特性,异常处理会带来一定的开销。据有关统计表明:使用异常,运行期间在不发生异常的情况下,性能可能会下 降5%~14%。其次,虽然异常处理能将异常处理代码与一般的普通代码分离开来,增强了代码可读性,但是同时它也破坏了程序的结构性,增加了代码管理和调 试的难度。因为函数返回点可能在你意料之外,光凭查看代码是很难评估程序控制流的。再次,要想轻松编写正确的异常安全代码,需要大量的支撑机制配合,所以使用异常就意味着要付出更多的代价。

C++11

右值引用、移动语义和完美转发
  • 等号左边的就是左值,等号右边的就是右值, 这个值能不能放等号左边,能放就是左值,否则就是右值。另一种方法是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。比如a = b+c, &a是允许的操作,但&(b+c)这样的操作则不会通过编译,则a是一个左值,b+c是一个右值。

  • 在C++11 中右值分两种,一个是将亡值,一个是纯右值。其中纯右值就是C++98中的概念。而将亡值则是C++11新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转化为T&&的类型转换函数的返回值。

  • 左值引用和右值引用

    • C++98的引用(即T&)我们称之为左值引用,T&&称之为右值引用,因为前者只能指向左值,而后者只能指向右值。可以使用is_rvalue_reference、is_lvalue_reference、is_reference 来判断。
    • 无论左值引用还是右值引用,都必须在声明时初始化。
    • T&& a = ReturnRvalue(); 这个表达式中,ReturnRvalue()返回的右值在表达式结束后,其生命也就终结了,而通过右值引用的声明,该右值又“重获新生”,其生命周期将与右值引用类型变量a的生命期一样。相比与如下的声明方式T b = ReturnRvalue() 前者少一次对象的析构及一次对象的构造。
    • 非常量的左值引用(bool& result = true)不能绑定到右值,编译器会报错,但常量的左值引用(如const bool& result = true)可以绑定到右值,且会延长右值的生命期。
    • 涉及型别推导的T&& 称之为万能引用。
  • 引用折叠

    • 如果任一引用为左值引用,则结果为左值引用,否则(即两个皆为右值引用),结果为右值引用。
  • 区分万能引用和右值引用

    • 如果 T&& 既可以指向左值引用,又可以指向右值引用,即涉及到类型推导,则称之为万能引用,这里名称不一定是T。完美转发中的T&&就是万能引用,auto&& var也是万能引用,但Widgt&& vartemplate<typename T> void f(Widget&& param)都是右值引用,void f(Widget&& param)也是右值引用
  • 在C++11中,我们用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源(所谓资源,就是指new出来的东西),而当我们用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符来移动资源,从而避免拷贝,提高效率

  • C++11新增移动构造函数赋值构造函数

    ClassName(ClassName&& T); // 这里不能加cosnt
    ClassName& operator(ClassName &&T);
    
  • 编译器生成默认移动构造函数或赋值构造函数的条件极其苛刻,如下,这里的声明的意思就是显示声明并实现了:

    1. 该类未声明任何复制操作
    2. 该类未声明任何移动操作,移动构造函数或赋值构造函数其中任何一个声明了,另外一个编译器都不会默认生成。
    3. 该类未声明任何析构函数
  • C++11中还增加了成员函数的引用修饰词,示例代码如下:

    // 对于如下定义的代码:
    class Widget{
    public:
        using DataType = std::vector<double>;
        DataType& data() { return values; }
    
    private:
        DataType values;
    };
    
    // 如果按如下方法调用, 这里就是vals1就是拷贝构造的
    Widget w;
    auto vals1 = w.data();
    
    // 但如果按照如下方式调用,则没必要进行拷贝,比较好的做法是移动而非复制
    auto vals2 = makeWidget().data();
    
    /*  可以使用引用修饰词来解决, 最终版本如下, 调用哪个取决于调用的对象是左值还是右值,这里makeWidget().data()中,makeWidget()是一个右值,调用右值版本,而w.data()中,w是左值,调用左值版本。*/
    class Widget{
    class Widget{
    public:
        using DataType = std::vector<double>;
        DataType& data() &   //对于左值Widgets类型,返回左值
        { return values; }
    
        DataType& data() &&   //对于右值Widgets类型,返回右值
        { return std::move(values); }
    
    private:
        DataType values;
    };
    
移动语义
  • 就是移为己用, 避免了析构和拷贝构造,提升了效率。
  • 需要注意的是析构函数还是会调用的,只是内容已经被置为空了,析构时其实并不真正地释放资源。以指针为例,进行移动语义时,指针指向的内容被转移,然后指针被置空,后面调用析构时,只是释放空指针,并不会造成资源的释放。
  • 由于被移动的对象资源已经被移走了,所以后续就不能再使用该对象的资源了。同样以指针为例,那就不能通过*ptr获取指针内容了,会运行错误。
  • 针对右值引用使用std::move, 针对万能引用实施std::forward
  • std::move 会强制转化右值,如果调用该函数将一个左值转化为右值并用于移动语义,那调用后,就不能够再使用被转化的左值了,因为由于移动语义,它的资源已经被置空了。**std::move**不能将常量转化为右值
    class Test {
        int * arr{nullptr};
        public:
        Test():arr(new int[5000]{1,2,3,4}) {
            cout << "default constructor" << endl;
        }
        Test(const Test & t) {
            cout << "copy constructor" << endl;
            if (arr == nullptr) arr = new int[5000];
            memcpy(arr, t.arr, 5000*sizeof(int));
        }
        /*不能使用const Test && t, 因为需要对t进行修改*/
        Test(Test && t): arr(t.arr) {
            cout << "move constructor" << endl;
            t.arr = nullptr;
        }
        ~Test(){
            cout << "destructor" << endl;
            delete [] arr;
        }
    };

    Test createTest() {
        return Test();
    }

    int main() {
        Test t(createTest());
    }

    /* 我们会发现,打印的结果为:
        default constructor
        move constructor
        destructor
        move constructor
        destructor
        destructor
    */
    /*gcc是一个丧心病狂的编译器,他会强制进行RVO返回值优化。如果你不做任何设置直接用GCC编译运行上面的代码,你将看到的是:
            default constructor
    这个时候不要怀疑写错了。请直接在gcc后面添加编译参数-fno-elide-constructors*/
    /*如果这里没有Test(Test && t) 函数,则就要进行繁杂的深拷贝*/
  • 移动语义一个典型的应用场景:高性能的置换函数
    template<class T>
    void swap(T& a,  T& b)
    {
        T tmp(move(a));
        a = move(b);
        b= move(tmp);
    }
    
完美转发
  • 所谓完美转发,是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。比如:
     template <typename T>
     void IamForwarding(T t){
         IrunCodeActually(t);
     }
     /*这里就希望传入IamForwarding的是左值对象,IrunCodeActually就能获得左值对象,传入是右值对象,就能获得右值对象,而不产生额外的开销,就好像转发者不存在一样。*/
    
     /*我们考虑一个例子*/
     template <typename T>
     void func(T t) {
         cout << "in func" << endl;
     }
    
     template <typename T>
     void relay(T&& t) {
         cout << "in relay" << endl;
         func(t);
     }
    
     int main() {
         relay(Test());
     }
    
     /*那么现在我们来运行一下这个程序,我们会看到,结果与我们预想的似乎并不相同:
    
     default constructor
     in relay
     copy constructor
     in func
     destructor
     destructor
     我们看到,在relay当中转发的时候,调用了复制构造函数,也就是说编译器认为这个参数t并不是一个右值,而是左值。这个的原因已经在上一节将结果了,因为它有一个名字。那么如果我们想要实现我们所说的,如果传进来的参数是一个左值,则将它作为左值转发给下一个函数;如果它是右值,则将其作为右值转发给下一个函数,我们应该怎么做呢?
     */
    
  • 引用折叠TR有可能是类型T的引用或是类型T,即可能是T&,T&&或T的别名, 假定函数test(TR x), 当传递给函数的参数有可能是T、T& 、T&&, 则x的实际类型是什么呢? 实际上,会发生引用折叠,这个规则是总是优先将其折叠为左值引用。而模板对类型的推导规则就比较简单,当转发函数的实参是类型X的一个左值引用,那么模板参数被推导为X&,而转发函数的实参是类型X的一个右值引用的话,那么模板的参数就被推导为X&&类型。
  • 根据上述,最完美的转发函数写法如下:
    template<class A>
    void G(A &&a)
    {
        F(forward<A>(a));
    }
    template <typename T>
    void IamForwarding(T&& t){
        IrunCodeActually(forward<T>(t));
    }
    
    // forward的作用将一个对象转发给另一个对象,同时保留该对象的左值性或右值性。
    // 与std::move()相区别的是,move()会无条件的将一个参数转换成右值,而forward()则会保留参数的左右值类型。
    // forward函数模板源码如下:
    template <class _Ty>
    _NODISCARD constexpr _Ty&& forward(
        remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
        return static_cast<_Ty&&>(_Arg);
    }
    
    template <class _Ty>
    _NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
        static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
        return static_cast<_Ty&&>(_Arg);
    }
    /*
    第一个forward function template中,模板参数是一个左值。当调用该函数的实参为左值时,这个template就将forward an lvalue as lvalue.当调用该函数的实参为右值时,这个template就将forward an lvalue as rvalue.
    第二个forward function template中,模板参数是一个右值。当调用该函数的实参为左值时,这个template就将执行static_assert这一句,不进行转换。当调用该函数的实参为右值时,这个template就将forward an rvalue as rvalue.
    */
    
  • 测试完美转发函数代码
    //所以我们的代码应该是这样:
    
    template <typename T>
    void func(T t) {
        cout << "in func " << endl;
    }
    
    template <typename T>
    void relay(T&& t) {
        cout << "in relay " << endl;
        func(std::forward<T>(t));
    }
    /*现在运行的结果就成为了:
    default constructor
    in relay
    move constructor
    in func
    destructor
    destructor
    而如果我们的调用方法变成:*/
    
    int main() {
        Test t;
        relay(t);
    }
    
    /*那么输出就会变成:
        default constructor
        in relay
        copy constructor
        in func
        destructor
        destructor
    完美地实现了我们所要的转发效果。*/
    
类型推导
模板类型推导
  • 来自Effective Modern C++ 条款一
    tmplate<typename T>
    void f(ParamType param);
    
    f(expr)
    
  • 型别推导的过程就是通过expr来推导ParamType和T、分为三种情况
    • 情况1:ParamType是一个指针或引用,但不是一个万能引用。
      • 若expr具有引用型别,先将引用部分忽略。
      • 而后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别。
      template<typename T>
      void f(T& param);      // param是个引用, 也有可能是 T* param
      
      /*
      又声明了下列变量
      int x = 27;
      const int cx = x;
      const int& rx = x;
      在各次调用中,对param和T的类型推导结果如下:
      f(x);   // T的类别是int, param的类别是int&
      f(cx);  // T的类别是const int, param 的类别是 const int&
      f(rx);  // T的类别是const int , param 的类别是 const int&
      */
      
    • 情况2:ParamType是一个万能引用
      • 如果expr是一个左值,T和ParamType都会被推导为左值引用。
      • 如果expr是一个右值,则应用“常规”(即情况1中的)规则
    • 情况3:ParamType既非指针也非引用
      • 若expr具有引用型别,则忽略其引用部分。
      • 忽略expr的引用性后,若expr是个const对象,也忽略之。若其是一个volatile对象,也忽略之。
atuo关键字
  • auto类型推导和模板型别推导是一模一样的
  • 唯一的区别是auto型别推导会假定用大括号括起的初始化表达式代表一个std::initializer_list, 但模板类型推导却不会。解决方法之一就是如果使用auto就不使用大括号初始化。
  • 以const auto& rx = x 为例,这里x等价于expr , const auto&等价于 ParamType, rx等价于param
  • 优先选用auto,而非显式型声明, 理由如下:
    1. 可以向未初始化的变量带来的问题挥手告别
    2. 可以用它来表示只有编译器才能掌握的型别
    3. 显式指定型别可能导致你既不想要,也没想到的隐式型别转换。使用auto可以防止这个问题。
{}大括号初始化
  • 在C++的三种初始化表达式的写法中,只有大括号适用于所有场合。大括号可以阻止隐式窄化型别转换,还对最令人苦恼之解析语法免疫(例如,假定Widget构造函数无参,则Widget w() 是错误的,编译器会认为声明一个函数, 只能写成 Widget w, 而用大括号可以,即Widget w{}, 表示调用无参的构造函数构造w)
  • 如果使用大括号初始化物来初始化一个使用auto声明的变量,那么推导出来的型别就会成为std::initializer_list。编译器只要有任何可能把一个采用大括号初始化语法的调用语句解读为带有std::initializer_list型别形参的构造函数(即{A,B,C,...}),则编译器就会选用这种解释。只有在找不到任何办法把大括号初始化物中的实参转换为std::initializer_list模板中的型别时,编译器才会退而去检查普通的重载协议。
    • 示例: std::vector<int> v{10,0}, 如果是小括号,则表示10个元素,每个都是0, 但由于是大括号,则优先推导为std::initializer_list,结果就是两个元素,一个为10,一个为0.
  • 可以坚持使用C98的方法来初始化,即小括号(), 只在必须使用时,才使用大括号{},如创建容器时逐个指定值.
decltype
  • 当用decltype(e)来获取类型时,推导规则为以下四条:
    1. 如果e是一个没有带括号的标记符表达式(定义的变量名、数组名、类名等)或者类成员访问表达式(即类成员),那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译时错误。
    2. 否则,假设e的类型是T, 如果e是一个将亡值,那么decltype(e)为T&&
    3. 否则,假设e的类型是T, 如果e是一个左值,则decltype(e) 为 T&
    4. 否则,假设e的类型是T, 则decltype(e)为T
    //示例代码如下:
    int i = 4;
    int arr[5] = {0};
    int *ptr = arr;
    
    struct s{ double d; }s;
    
    void Overloaded(int);
    void Overloaded(char);
    
    int&& rvalRef();
    const bool Func(int);
    
    // 规则1
    decltype(arr) var1;        // int[5], 标记符表达式
    decltype(ptr) var2;        // int *  , 标记符表达式
    decltype(s.d) var4;        // double, 成员访问表达式
    decltype(Overloaded) var5   // 无法编译通过
    
    // 规则2, 将亡值
    decltype(RvalRef()) var6 = 1;  // int &&
    
    // 规则3
    decltype(true?i:i) var7 = i;  // int&, 返回i的左值
    decltype((i)) var8 = i;  // int&, 带圆括号的左值
    decltype(++i) var9 = i;   // int&, ++i 返回i的左值
    decltype(arr[3]) var10 = i;   // int&, [] 操作返回左值
    decltype(*ptr) var11 = i;    // int&, *操作返回左值
    decltype("str") var12 = "str";    //const char(&)[90], 字符串字面常量为左值
    
    // 规则4
    decltype(1) var13;   // int,  除字符串常量外,其它常量为右值
    decltype(i++) var14;  // int , i++返回右值
    decltype((Func(1)))   // const bool,  圆括号可以忽略
    
    
  • 在C++11中, decltype 最主要的用处可能就是用来声明一个函数模板,在这个函数模板中返回值的类型取决于参数的类型。
    // 示例如下:
    
    /* 此处使用了 C++11 的尾随返
    回类型技术,即函数的返回值类型在函数参数之后声明(“->”后边)。尾随返回类型的一个优势
    是在定义返回值类型的时候使用函数参数。例如在函数 authAndAccess 中,我们使用
    了 c 和 i 定义返回值类型。在传统的方式下,我们在函数名前面声明返回值类
    型, c 和 i 是得不到的,因为此时 c 和 i 还没被声明。*/
    
    template<typename Container, typename Index> // 有缺陷,后面再修改
    auto authAndAccess(Container& c, Index i)-> decltype(c[i])  
    {
        authenticateUser();
        return c[i];
    }
    
    /*对使用 auto 来表明函数返回类型的情况,编译器使用模板类型推导。但是这
    样是回产生问题的。正如我们所讨论的,对绝大部分对象类型为 T 的容器, [] 操作子返回
    的类型是 &T , 然而条款一提到,在模板类型推导的过程中,初始表达式的引用会被忽略。*/
    
    /*为了让 authAndAccess 按照我们的预期工作,我们需要为它的返回值使用 decltype 类型推
      导,即指定 authAndAccess 要返回的类型正是表达式 c[i] 的返回类*/
      
    // 正确的写法如下:
    template<typename Container, typename Index>  // final C++11 version
    auto  authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    
    /* C++ 的拥护者们预期
    到在某种情况下有使用 decltype 类型推导规则的需求,并将这个功能在 C++14 中通
    过 decltype(auto) 实现。这使这对原本的冤家(decltype 和 auto ) 在一起完美地发挥作
    用: auto 指定需要推导的类型, decltype 表明在推导的过程中使用 decltype 推导规则*/
    
    template<typename Container, typename Index> // final C++14 version
    decltype(auto) 
    authAndAccess(Container&& c, Index i) 
    {
        authenticateUser();
        return std::forward<Container>(c)[i];
    }
    
查看类型推导的方法
  • IDE, 复杂情况下会出错
  • 编译器诊断信息
  • 运行时输出,typeid(x).name, 可能不准确
  • Boost库,准确
基于范围的for循环
  • 用于数组时,数组的大小必须是能够确定的
  • 用于迭代器时,不是迭代器,而是解引用后的对象, 见如下代码
int main(){
    vector<int> v = {1, 2, 3, 4, 5};
    for(auto i=v.begin(); i != vec.end(); ++i){
        cout<<*i<<endl;
    }

    for(auto e:v){
        cout<< e <<endl;  // 这里是e,而不是*e
    }
    return 0;
}
lambda函数
  • lambda简单介绍:多了一个捕获列表的无名内联函数。
  • [capture list] (parameter list) -> return type { function body }捕获列表,参数列表(可省略),返回值类型(可省略),函数体。
  • 只有按照值或按照引用捕获两种方法。 [=]代表全部采用值传递方式捕获所有父作用域的变量,包括this. [&]代表全部采用引用传递方式捕获所有父作用域的变量, 包括this. []表示捕获任何变量
  • 只能捕捉父作用域,不能捕捉爷爷及以上的作用域,即只能是处于同一层的作用域。自然,也不能捕捉全局变量。 也不能捕捉父作用域的静态变量。
bool cmp(int a, int b){
    return  a < b;
}

int main(){
    vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
    vector<int> lbvec(myvec);

    sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
    cout << "predicate function:" << endl;
    for (int it : myvec)
        cout << it << ' ';
    cout << endl;

    sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });   // Lambda表达式
    cout << "lambda expression:" << endl;
    for (int it : lbvec)
        cout << it << ' ';

    return 0;
}
/*在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。*/

// 另外一个例子:
/* find_if 的作用: Returns an iterator to the first element in the range [first,last) for which pred returns true. If no such element is found, the function returns last.*/
int main(){
    vector<int> vec={1, 3, 6, 7, 7, 8};
    int val = 7;
    std::vector<int>::iterator iter = find_if(vec.begin(), vec.end(), [val](int i){
        return i == val;
    })
    // use iter to ...
}
  • 避免默认捕获模式,显式写出需要捕获的变量,而非捕获所有
  • 如果不需要考虑效率,则按值优于按引用
智能指针
  • 当一个函数终止时,本地变量都将从栈内存中删除,一个指针ps占据的内存将被释放。如果ps指向的内存(假定ps申请了堆内存)也被释放,那就可以避免内存泄漏。智能指针可以实现这种功能,它们可以让指针释放时,它所指向的内存也将被释放。智能指针可以提高内存的使用效率,帮助防止内存泄漏。
  • 参考链接https://my.oschina.net/hevakelcj/blog/465978https://blog.csdn.net/Xiejingfa/article/details/50772571
  • C++98提供auto_ptr, C++11摒弃了这种方案,提供了另外两种方案unique_ptr and shared_ptr.
    • 如果程序要使用多个指向同一对象的指针,应该选择shared_ptr(本质是引用计数)。
    • 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr
    • weak_ptr是为了解决shared_ptr的嵌套引用而存在的
auto_ptr
  • auto_ptr是独占性的,不允许多个auto_ptr指向同一个资源;
  • 复制auto_ptr对象时,把指针指传给复制出来的对象,原有对象的指针成员随后重置为nullptr。这常常造成问题, 如下代码所示
void runGame(){
    std::auto_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物
    monster1->doSomething();//怪物做某种事
    std::auto_ptr<Monster> monster2 = monster1;//转移指针
    monster2->doSomething();//怪物做某种事
    monster1->doSomething();//Oops!monster1智能指针指向了nullptr,运行期崩溃。
}
  • auto_ptr不够方便——没有移动语义的后果,比如auto_ptr不能够作为函数的返回值和函数的参数,也不能在容器中保存autp_ptr。 而这些unique_ptr都可以做到。因为C++11之后有了移动语义的存在,这里调用的是移动构造函数。因为移动语义它可以接管原来对象的资源,同时让原来对象的资源置为空。
unique_ptr
  • 是一种更严格的、更自私的智能指针,当它自身被析构时,它所指向的对象也一并被析构。 并且还有一个性质:它不允许除它以外的智能指针指向同一对象(也就是没有实现 copy 方法),不过可以通过 move 来实现转移所有权,也就是转移到另一个 unique_ptr 来管理对象。
  • unique_ptr 可以作为函数返回值,因为这实际上是移动语义
  • release()只转移控制权,并不释放内存,而reset和=nullptr操作会释放原来的内存
int main()
{
    unique_ptr<int> up1(new int(11));
    // unique_ptr<int> up2 = up1 //错误!!! 无法通过编译
    cout << *up1 <<endl;  // 11
    unique_ptr<int> up3 = move(up1); //现在up3是数据唯一的unique_ptr智能指针
    cout<<*up3<<endl  // 11
    // cout<<*up1<<endl  // 运行时错误
    up3.reset(); // 显式释放内存
    up1.reset(); // 不会导致运行错误
}

int main()
{
    unique_ptr<int> up2(new int(22));
    up2.reset(new int(23));  // 释放指向22的内存,并转为指向23
    cout<<*up2<<endl;   // 23
    up2.reset();        // 释放23
    if(!up2) {printf("up2 is empty()\n");} // 输出up2 is empty()
}

shared_ptr
  • shared_ptr会在循环引用时会造成问题
    #include <iostream>
    #include <memory>
    #include <vector>
    using namespace std;
    class ClassB;
    class ClassA
    {
    public:
        ClassA() { cout << "ClassA Constructor..." << endl; }
        ~ClassA() { cout << "ClassA Destructor..." << endl; }
        shared_ptr<ClassB> pb;  // 在A中引用B
    };
    class ClassB
    {
    public:
        ClassB() { cout << "ClassB Constructor..." << endl; }
        ~ClassB() { cout << "ClassB Destructor..." << endl; }
        shared_ptr<ClassA> pa;  // 在B中引用A
    };
    
    int main() {
        shared_ptr<ClassA> spa = make_shared<ClassA>();
        shared_ptr<ClassB> spb = make_shared<ClassB>();
        spa->pb = spb;
        spb->pa = spa;
        // classA和classB的资源不会得到释放,因为最终引用计数为1
    }
    
weak_ptr
  • 为了解决类似这样的问题,C++11引入了weak_ptr,来打破这种循环引用。
    weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。
  • 当我们创建一个weak_ptr时,需要用一个shared_ptr实例来初始化weak_ptr,由于是弱共享,weak_ptr的创建并不会影响shared_ptr的引用计数值.
    int main() {
        shared_ptr<int> sp(new int(5));
        cout << "创建前sp的引用计数:" << sp.use_count() << endl;             // use_count = 1
    
        weak_ptr<int> wp(sp);
        cout << "创建后sp的引用计数:" << sp.use_count() << endl;             // use_count = 1
    }
    
  • 既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr。
    class A
    {
    public:
        A() : a(3) { cout << "A Constructor..." << endl; }
        ~A() { cout << "A Destructor..." << endl; }
    
        int a;
    };
    
    int main() {
        shared_ptr<A> sp(new A());
        weak_ptr<A> wp(sp);
        sp.reset();
    
        if (shared_ptr<A> pa = wp.lock())
        {
            cout << pa->a << endl;
        }
        else
        {
            cout << "wp指向对象为空" << endl;
        }
    }
    
  • 除此之外,weak_ptr还提供了expired()函数来判断所指对象是否已经被销毁
  • 如何使用weak_ptr, weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。
  • 最后,我们来看看如何使用weak_ptr来改造最前面的代码,打破循环引用问题
    class ClassB;
    class ClassA
    {
    public:
        ClassA() { cout << "ClassA Constructor..." << endl; }
        ~ClassA() { cout << "ClassA Destructor..." << endl; }
        weak_ptr<ClassB> pb;  // 在A中引用B
    };
    
    class ClassB
    {
    public:
        ClassB() { cout << "ClassB Constructor..." << endl; }
        ~ClassB() { cout << "ClassB Destructor..." << endl; }
        weak_ptr<ClassA> pa;  // 在B中引用A
    };
    
    int main() {
        shared_ptr<ClassA> spa = make_shared<ClassA>();
        shared_ptr<ClassB> spb = make_shared<ClassB>();
        spa->pb = spb;
        spb->pa = spa;
        // 函数结束,思考一下:spa和spb会释放资源么?
    }
    
注意事项
  • 为了避免隐式转换,智能指针不能使用赋值的方式初始化, 即unque_ptr up1 = new int[2] 是错误的, 只能写成unique_ptr up1(new int(11))(语法上就限定了不能使用), 指向数组时不能使用*和->,不是指向数组时不能使用[].
  • 对于数组类型,不要使用unique_ptr, 用array、vector、string是更好的选择。
  • uniqe_ptr可以赋值给shared_ptr, 当unique_ptr是右值或转化成右值的时候
  • 不使用new分配内存时,不能使用auto_ptrshared_ptr(new[]也不能); 不使用new 或new[]分配内存时,不能使用unique_ptr
  • 优先使用make_uniquemake_shared, 示例auto spws(std::make_shared<Widget>()); 因为只会引发一次内存分配,控制块和对象的构造函数一起分配,并且书写上更简单。但是make函数无法自定义析构函数
    • 使用make的缺点:托管对象所占用的内存直到与其关联的控制块也被析构时才会被释放,因为同一动态分配的内存块同时包含了两者。
并发API
  • C++11中原子类型,满足顺序一致性。也可以指定不要求一致性,而让编译器去优化(某些情况下,编译器可能会先去执行后面的代码,再执行前面的代码).
  • atomic类型不能赋值和拷贝
  • atomic实现原子操作的效率比互斥锁更高
  • 对并发使用automic, 对特种内存使用volatile, 即那种需要对一个变量用多条连续语句进行赋值的情况。 volatile跟并发程序没有任何关系。
  • 优先选用基于任务而非基于线程,对于一个函数,可以通过线程调用,也可以通过任务的方式调用,因为后者会得到一个返回值,fut.get()即可获取,示例如下:
    int doAsycWork();
    std::thread t(doAsycWOrk); // 基于线程
    auto fut = std::async(doAsyncWork); // 基于任务
    
  • 调用std::asyc, 请指定std::launch::async, 不然可能会以异步或同步方式两种可能执行。见EMC++条款36
  • std::thread 必须指定是join或detach,不然程序会异常终止,最好是对象析构时自动调用设定好的方式(join还是detach), 即RAII.见MFC++条款37.
    class ThreadRAII{
    public:
        enum class DctorAction {join, detach};
        ThreadRAII(std::thread&& t, DtorAction a)
        : action(a),t(std::move(t)) {}
    
        ~ThreadRAII()
        {
            if(t.joinable()){
                if(action == DtorAction::join){
                    t.join();
                }else{
                    t.detach();
                }
            }
        }
    
        std::thread& get(){return t;}
    private:
        DtorAction action;
        std::thread t;
    }
    
    //使用示例:
        ThreadRAII t(
            std::thread([]{
                //bla bla
            }),
            ThreadRAII::DtorAction::join
        );
    
  • 指涉到经由std::aysnc启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束。并且任务如果没结束,该期值不会被析构。只有上述一种情况会这样。示例代码如下:
    void task(std::promise& prom)
    {
        int x = 0;
        /* 这里一般一个非常耗时耗cpu的操作如查找,计算等,在此过程中得到x的最终值,这里我们直接赋值为10 */
        x = 10;
        prom.set_value(10);         //设置共享状态的值,同时promise会设置为ready
    }
    
    void print_int(std::future& fut) {
        int x = fut.get(); //如果共享状态没有设置ready,调用get会阻塞当前线程
        std::cout << "value: " << x << '\n';
    }
    
    int main ()
    {
        std::promise prom;            // 生成一个 std::promise 对象.
        std::future fut = prom.get_future();     // 和 future 关联.
        std::thread th1(task, std::ref(prom));      // 将 prom交给另外一个线程t1 注:std::ref,promise对象禁止拷贝构造,以引用方式传递
        std::thread th2(print_int, std::ref(fut));   // 将 future 交给另外一个线程t.
        /*主线程这里我们可以继续做一大堆我们想做的事,不用等待耗时的task线程,也不会因为等待task的执行结果而阻塞*/
    
        th1.join();
        th2.join();
        return 0;
    
其它
nullptr
  • nullptr是指针空值(不是指针类型,是一个空值,用于赋值给指针,任何指针都可以赋这个值),而void*是无类型指针,就是可以指向其它任何类型的指针。NULL并不是指针空值,NULL可能被定义为字面常量或者是无类型(void*)指针常量.
  • 还有一个指针空值类型,即nullptr_t, 可以在支持nullptr的头文件cstddef中找到如下定义 typedef decltype(nullptr) nullptr_t;
  • NULL与nullptr比较是否可行(windows上TDM-GCC测试可行char *ptr = NULL;if(ptr == nullptr)结果为true ) 0不可以与nullptr比较是否相等(部分老旧编译器可以)
优先选用限定作用域的枚举类型
  • C++11新增的枚举类型,enum class 声明,也叫枚举类或强枚举类型,示例如下
enum class Color{ balck, white, red };
Color c = white;         //错误!,范围内并无名为“white”的枚举量
Color c = Color::white   // 正确
  • 强枚举类型不支持隐式转换到整数型别、不会命名污染
优先选用别名声明,而非typedef
  • 使用using UPtrMapSS = std::unqiue_ptr<std::unordered_map<std::string,std::string>>; 而非
    typedef std::unqiue_ptr<std::unordered_map<std::string,std::string>> UPtrMapSS
  • 这样做的好处只在模板的时候体现
只要有可能使用constexpr, 就使用它
  • 这东西用得少
  • 这里的有可能的意思是,后续绝对不需要更改了,不然太麻烦了
    constexpr int pow(int base, int exp) noexcept
    {
        auto result = i;
        for(int i=0;i<exp; ++i) result *= base;
    
        return result;
    }
    // 则pow(x,y) 返回值是一个常量,只要x和y是常量或constexpr
    
    class Point{
    public:
        constexpr Point(double x=0, double y=0) noexcept
        :x(xVal),y(yVal)
        {}
    
        constexpr double xValue() const noexcept { return x; }
        void setX(double newX) noexcept { x = newX; }
        constexpr Point p1(9.4,27.7);
    }
    
只要函数不会发射异常,就为其加上noexcept声明
优先选用删除函数 = delete,而非private未定义函数
为意在改写的函数添加override声明,必须是虚函数
  • C++11新增的函数引用修饰词也必须相同

STL

函数指针与仿函数
  • 仿函数其实就是重载了operator() 的类
  • 当STL中需要一个函数地址时,既可以给函数指针,也可以给仿函数,示例如下,这里给myfunction或myclass都是可以的,当然最好的是给lamda表达式。
    // for_each example
    #include <iostream>     // std::cout
    #include <algorithm>    // std::for_each
    #include <vector>       // std::vector
    
    void myfunction (int i) {  // function:
      std::cout << ' ' << i;
    }
    
    struct myclass {           // function object type:
      void operator() (int i) {std::cout << ' ' << i;}
    } myobject;
    
    int main () {
      std::vector<int> myvector;
      myvector.push_back(10);
      myvector.push_back(20);
      myvector.push_back(30);
    
      std::cout << "myvector contains:";
      for_each (myvector.begin(), myvector.end(), myfunction);
      std::cout << '\n';
    
      // or:
      std::cout << "myvector contains:";
      for_each (myvector.begin(), myvector.end(), myobject);
      std::cout << '\n';
    
      return 0;
    }
    
priority_queue
  • priority_queue<Type, Container, Functional>Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。如果不写后两个参数,那么容器默认用的是vector(这里为什么可以用vector,请参考堆排序),比较方式默认用operator<,也就是优先队列是大顶堆,队头元素最大。
  • 优先输出小数据 priority_queue<int, vector<int>, greater<int> > p;
  • 如果要更改其中的compare函数,需要写一个类或结构体来重载()运算符,当然也可以用默认的std::less, 但可能并没有相应的能够比较类型的<函数,如要通过比较一个结构体中某个数据来排序,这时less中的<号就失效了,因为它是针对某种特定的类型。或者仅仅只是写一个函数重载<,让它能够通过less来比较。PS: comp中两个参数a,b如果是a<b返回true(less就是这样的) ,则 b在top
    #include<iostream>
    #include<queue>
    #include<cstdlib>
    using namespace std;
    struct Node{
        int x,y;
        Node(int a=0, int b=0):x(a), y(b) {}
    };
    
    struct cmp{
        bool operator()(Node a, Node b){
            if(a.x == b.x)    return a.y > b.y;
            return a.x > b.x;
        }
    };
    
    int main(){
        priority_queue<Node, vector<Node>, cmp>p;
        for(int i=0; i<10; ++i)
            p.push(Node(rand(), rand()));
        while(!p.empty()){
            cout<<p.top().x<<' '<<p.top().y<<endl;
            p.pop();
        }
        return 0;
    }
    
迭代器失效问题
  • 在使用 list、set 或 map遍历删除某些元素时可以这样使用,这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响
    std::list< int> List;
    std::list< int>::iterator itList;
    for( itList = List.begin(); itList != List.end(); )
    {
          if( WillDelete( *itList) )
              List.erase( itList++); //不可以删除后再++, 因为erase后,当前迭代器失效了。
          else
              itList++;
    }
    
    • 对于序列式容器(如vector,deque),删除当前的iterator会使后面所有元素的iterator都失效,这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置
    for (iter = cont.begin(); iter != cont.end();)
    {
           if (shouldDelete(*iter))
              iter = cont.erase(iter);
           else
              ++iter;
    }
    
  • 推荐vector、deque都通过 if(delete(iter)) iter = erase(iter); else iter++的方式来迭代处理,其它的如map等通过上面的例子处理,如果是C++11则都用iter = erase(iter)来处理
push_back()和emplace_back()
  • 引入了右值引用,转移构造函数后,push_back()右值时就会调用构造函数和转移构造函数,如果可以在插入的时候直接构造,就只需要构造一次即可。这就是c++11 新加的emplace_back, emplace_back利用了完美转发实现这个功能。
deque
  • deque系由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。受到分段连续线性空间的字面影响,我们可能以为deque的实现复杂度和vector相比虽不中亦不远矣,其实不然。主要因为,既是分段连续线性空间,就必须有中央控制,而为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为繁琐。deque的实现代码分量远比vector或list都多得多。
map与unordered_map的区别
  • map: map内部实现了一个红黑树,该结构具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素,因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作。
  • unordered_map: unordered_map内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的
  • unordered_map哈希表的建立比较耗费时间,但因为内部实现了哈希表,因此其查找速度非常的快O(1), 对于那些有顺序要求的问题,用map会更高效一些
  • unorder_map是将给如的键值放入哈希表中,就是映射为一个地址,然后从这个地址去取键值对应的元素值,即unorder_map[key]
set
  • 与map 类似, 都是红黑树实现的
    • set elements in a set are unique,会排序,默认是数值小(优先级低)的放前面(less), 有insert和erase等函数 set主要通过insert来加入新元素
    • 如果要访问set中的第1个元素,可通过 *setName.begin() 获得.
    • Multisets are containers that store elements following a specific order, and where multiple elements can have equivalent values(与set的区别). 会排序,默认是数值小(优先级低)的放前面,(less), 有insert,erase函数
pair
  • pairstd::pair <std::string, double> p, p.first p.second,可以将pair作为一个类似于int的类型,如vector<pair<string, string>> tickets

比较函数等
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>

using namespace std;

typedef struct Fruits{
  string name;
  int id;
  int weight;
}Fruits;

bool Compare(Fruits&a,Fruits&b){
  //if(a.name!=b.name){
  //  return a.name<b.name;
  //}
  if(a.weight!=b.weight){
    return a.weight<b.weight;
  }
  else{
    return a.id<b.id;
  }
}
int main(){
  int N;
  while(cin>>N){
    vector<Fruits> ft;
    ft.clear();
    for(int i=0;i<N;i++){
      string tmname="";
      int iid,wweight;
      cin>>tmname>>iid>>wweight;
      Fruits fft;
      fft.name=tmname;
      fft.id=iid;
      fft.weight=wweight;
      ft.push_back(fft);
    }
    sort(ft.begin(),ft.end(),Compare);  // 从小到大排序
    for(auto it=ft.begin();it!=ft.end();it++){
      cout<<it->name<<' '<<it->id<<' '<<it->weight<<endl;
    }
  }
  return 0;
}

数据结构

AVL树

  • 二叉查找树:对于树中的每个结点X,它的左子树中所有项的值小于X中的项,而它的右子树中所有项的值大于X中的项。因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn) 。但二叉树若退化成了一棵具有n个结点的线性链后,则此些操作最坏情况运行时间为O(n)
  • 平衡二叉树(AVL树):是其每个结点的左子树和右子树的深度之差不超过1的二叉查找树。因为这个性质,它的深度和logN是同数量级的。由此,它的平均查找长度也和logN同数量级。

红黑树

  • 红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)

红黑树与AVL比较

  • 红黑树属于平衡二叉树。说它不严格是因为它不是严格控制左、右子树高度或节点数之差小于等于1。
  • 显然,avl树要比红黑树更平衡,因此avl树的查找效率更高。
  • 红黑树的插入操作统计上比avl树要好
  • 删除操作红黑树的平均效率也比avl树高, 插入和删除本质上是调整树的结构

B+树

  • B+树的优势:
    1. 单一节点存储更多的元素,使得查询的IO次数更少。
    2. 所有查询都要查找到叶子节点,查询性能稳定。
    3. 所有叶子节点形成有序链表,便于范围查询。
  • b+树图文详解,看这个就懂了
  • B树与B+树
    1. B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
    2. B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
    3. B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
    4. B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
    5. B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

其它

备忘

  • A的ASCII码为65; a的ASCII码为97,0的ASCII码为48.
  • 包含所有C++头文件的头文件<bits/stdc++.h> , 一般竞赛评测用计算机1秒接受的时间复杂度约为10^7条语句。
  • 满二叉树(深度为k,且有2^(k-1)个结点,就是非叶子节点都有左右子树),完全二叉树(如果给结点编号,则编号位置对应于满二叉树的编号,或者定义为只有最下面的两层结点度能够小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树)
  • 一个空类所占字节数为1; 一个虚继承的空类所占字节为4字节(C++类中有虚函数的时候有一个指向虚函数的指针(vptr),在32位系统分配指针大小为4字节。无论多少个虚函数,只有这一个指针,4字节) ;类中函数和静态类型不占类的字节(因为函数保存在代码段内); 子类的大小是本身成员变量的大小加上父类的大小。父类子类共享一个虚函数指针
  • vector<int>::reverse_iterator r = s.rbegin(); for(r; r != s.end() ; r++)
  • set和map之类的iterator指针只能执行自增运算(如 *(itr-1)会报错),不能进行四则运算 ; vector的iterator可以
  • string.substr(pos,pos2-pos)所表示的子字符串是从pos(包括)到pos2(不包括);pos2(包括pos2)到pos3(包括pos3)的长度为pos3-pos2+1
  • C++中的this是常量指针
  • int _access( const char *path, int mode ); windows VS下判断路径名或文件名是否存在,mode取0

好的编程习惯

  • 先写测试,后写代码
  • 先形成清晰思路(全局观),后写代码,三思再编
  • 充分考虑代码的诸多问题,提高代码的鲁棒性
  • 多写注释,注释非常重要,一种简要的注释是将这一步做什么写清楚,如acf作者代码中的注释
  • Google 开源项目风格指南中文版

参考资料

  1. Effective C++:改善程序与设计的55个具体做法
  2. More Effective C++:35个改善编程与设计的有效方法
  3. Effective Modern C++:改善C++11与C++14的42个具体做法
  4. C++编程思想
  5. 深入理解C++11:C++11新特性解析与应用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值