C++学习笔记系列之继承多态
一、移动语义
1、右值引用
有一种机制,可以在语法层面识别出临时对象,在使用临时对象构造新对象(拷贝构造)的时候,将临时对象所持有的资源『转移』到新的对象中,就能消除这种不必要的拷贝。
2、左值和右值
左值和右值都是针对表达式而言的, 左值是指表达式结束后依然存在的持久对象
右值是指表达式结束时就不再存在的临时对象
区分: 能对表达式进行取地址,则为左值 否则为右值
- 非const左值引用不能绑定到右值上
- const左值引用能绑定到右值上
- const左值引用同时可以绑定到左值上
- 因此,const左值引用作为函数的形参时,无法判断传过来的实参是左值还是右值
3、右值引用
- 右值引用只能绑定到右值上,不能绑定到左值上。
- 移动构造函数,string( string && rhs) 右值引用。
- 常量右值引用没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着『修改』)。
4、移动语义
- 问题:临时对象,带来的不必要的资源浪费。
- 需求:为了提高程序的执行效率,需要直接将临时对象的内容直接转移到新对象之中。
- 问题准换: 需要在语法层面识别出一个临时对象(右值)
在C++11之前,只有const左值引用能够绑定到右值,const int & ref = 10 ; 但是它也能绑定到左值,故当const作为函数形参时,实参不能区分是左值还是右值,故提出了----右值引用-->只能绑定到右值,
int && ref = 10 ;
String(String && rhs); //移动构造函数
String & operator=(String
&& rhs);//移动赋值运算符函数
- std : : move函数的作用就是强制将一个左值转换成右值引用。并且,具有移动语义的函数会优先执行。
- 使用移动语义的特点,该语句之后该对象不会再使用了。
- 编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数。
- 传递进来的实参是右值,在函数体内有了名字,就变成了左值。
5、资源管理 ---RAII
- 利用对象生命周期管理程序资源(包括内存、文件句柄、锁等)的技术。
- 关键:要保证资源的释放顺序与获取顺序严格相反
- 在构造时初始化资源, 或托管已构造的资源
- 析构时释放资源
- 一般不允许复制或赋值(对象语义) - 值语义
- 提供若干访问资源的方法
- RAII的本质是用 栈对象 来管理资源,因为栈对象在离开作用域时,会自动调用析构函数
6、资源管理---智能指针 RAII(Resource Acquisition Is Initialization) 资源获取即初始化时机
智能指针(Smart Pointer) --->利用栈对象来管理资源。
1、 是存储指向动态分配(堆)对象指针的类
2、在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象
C++11提供了以下几种智能指针,位于头文件<memory>,它们都是类模板
- auto_ptr 被弃用的原因 --> 在语法上来说是可以表达复制语义的,但实现上发生了所有权的转移,该智能指针存在缺陷。
- std::unique_pt
-
- (独享型--不能进行复制或赋值 --> 即在它的内里面没有提供复制构造函数和赋值运算符函数),可以有移动语义。具有移动(std::move)语义(即提供了移动构造函数和移动赋值运算符函数),可做为容器元素 时(需要用移动语义方式添加元素,添加完后,原对象不再被使用),可以指定自定义的删除器。
- std::shared_ptr c++11
-
- 共享型,能复制和赋值
- 从语义上来讲,可以将对象语义(不能被复制或赋值)准换成值语义(就是能够被复制和赋值)。
-
- ifstream ifs; 本身不能复制
- shared_ptr<ifstream> sp(&ifs);
- shared_ptr<ifstream> sp2(sp); //可以复制了。
- shared_ptr是一个引用计数智能指针,用于共享对象的所有权
- 1.引进了一个计数器shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块
- 2.析构函数中不是直接释放指针对应的内存块,如果shared_count大于0则不释放内存只是将引用计数减1,只有计数等于0时释放内存
- 3.复制构造与赋值操作符只是提供一般意义上的复制功能,并且将引用计数加1.
- 问题:循环引用(可能发生内存泄漏) --->通过weak_ptr 解决
- std::weak_ptr c++11
-
- 为了解决循环引用问题而诞生
- std::shared_ptr是强引用智能指针
- std::weak_ptr 是弱引用智能指针
- 强引用,只要有一个引用存在,对象就不能被释放
- 弱引用,并不增加对象的引用计数,但它只知道对象是否存在。
- 如果存在,提升为shared_ptr成功,提升操作由lock方法完成;否则,提升失败
- 通过weak_ptr访问对象的成员的时候,要提升为shared_ptr
- shared_ptr的误用
- 1、同时将一个原生的裸指针交给不同的智能指针进行托管。
-
- Point *pt = new Point(1,2); std::shared_ptr<Point> p1(pt);//在类之外对对象进行托管
std::shared_ptr<Point> p2(pt);
- 2、
-
- shared_ptr<Point> p1(new Point(1, 2));
shared_ptr<Point> p2(new Point(3, 4));
p2.reset(p1.get());
class std::enable_shared_from_this 方法shared_from_this()
- g++ -std=c++11 xx.cc
二 、继承与派生
1、继承的概念:
在既有类的基础上定义新的类,而不用将既有类的内容重新书写一遍,这称为“继承”(inheritance),既有类称为“基类”或“父类”,在它的基础上建立的类称为“派生类”或“子类 。
MFC已经被微软弃用。
ABC的时代 A ----> 人工智能AI
B ----> Big Data
C ----> 云的时代
尽量向这些内容上靠。
2、继承的定义形式
class 派生类 : 派生方式 基类 {…}; //public, protected, private
派生类生成过程包含3个步骤:
-
- 吸收基类的成员
- 改造基类的成员
- 添加自己新的成员
3、不能被继承(继承的局限性)
-
- 构造函数
- 析构函数
- 用户重载的new 、delete运算符
-
- operator new /operator delete
- 用户重载的=运算符 (对象的复制)
- 友元关系(单向性,不具备传递性)
- 继承时,若不写关键字,默认私有继承。
- 保护成员不能直接通过对象进行访问,但是它对其子类、派生类进行开放
- 无论何种继承,基类的私有成员都不能在派生类内部直接访问。
- 采用public的继承方式称为接口继承。privated的继承方式叫实现继承。protected的继承称保护继承。
- 采用的protected进行继承时,除了基类的private成员不能在派生类直接访问外,其他成员都可以在类内部直接访问。基类public成员在派生类内部变成了protected了
- protected继承时,除了基类的私有成员在整个继承体系中不能直接访问外,其他的非私有成员都可以在派生类内部进行访问。
- 派生类只有一个基类时,称为单基派生,在实际运用中,C++同时支持多个基类,这种方法称为多基派生或多重继承。
-
- class 派生类名:继承方式1 基类名1,继承方式2 基类名2,…,继承方式n 基类名n { private:
-
- 新增私有成员列表;
- public:
- 新增公开成员列表;
- };
- 成员名的二义性 ----> 作用域限定符 ::
- 对于存储二义性,采用虚拟继承解决, class C: virtual public A
重要基础: ---> 三种继承方式的访问权限
- 对于派生类创建的对象,只有调用通过pubic继承的基类的public的成员,其他情况都不能访问
- 对于基类的私有成员,无论以何种方式进行继承,在派生类内都不能访问。
- 对于基类的非私有成员,不论以何种方式继承,在派生类内部都能直接访问。
- 如果派生类采用了私有继承,其基类的非私有成员在这一层级的访问权限就变成private,之后的派生类均不能再雷内部直接访问顶层内部的非私有成员,如果才用的protected继承,在继承体系之中一直都可以访问基类的非私有成员。
4、派生类的构造和销毁
派生类对象的创建总原则: 先完成基类部分的初始化,在完成派生类的初始化。
- 派生时,构造函数和析构函数是不能继承的,构造时,先基类在派生类。
- 系统首先通过派生类的构造函数来调用基类的构造函数,完成基类成员的初始化,而后对派生类中新增的成员进行初始化。
- 如果派生类没有显式定义构造函数而基类有,则基类必须拥有默认构造函数。
- 当派生类有显示定义构造函数时,基类部分的初始化,如果你想调用基类的有参构造函数,必须要在派生类构造函数的初始化列表中显式调用基类的有参构造函数。
- 调用顺序为:
-
- 完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成。
- 调用基类的构造函数完成基类成员的初始化。
- 若派生类中含对象成员、const成员或引用成员,则必须在初始化表中完成其初始化。
- 派生类构造函数体执行。
- 隐藏机制,当派生类与基类有同名函数时,基类的同名函数会被隐藏,只会执行派生类的同名函数,即使,基类的参数有变化,也会被隐藏。可以通过加上域名限定符,来调用被隐藏的基类成员函数。
5、派生类的析构
- 当对象被删除时,派生类的析构函数被执行。析构函数同样不能继承。
- 在执行派生类析构函数时,基类析构函数会被自动调用(与虚函数)。
- 执行顺序是先执行派生类的析构函数,再执行基类的析构函数,这和执行构造函数时的顺序正好相反。
6、多基派生类
- 各基类构造函数的执行顺序与其在初始化表中的顺序无关,而是由定义派生类时继承指定的基类顺序决定的。
- 析构函数的执行顺序同样是与构造函数的执行顺序相反。
- 覆盖:
-
- oversee 隐藏:父子类,函数名称相同,不带 virtual关键字的函数
-
- 2个函数参数相同,但基类函数不是虚函数。若是虚函数,并且通过对象名去调用的时候,也可以认为是隐藏,通过指针或引用去调用,叫覆盖。
- 2个函数参数不同,无论基类函数是否是虚函数,基类函数都会被屏蔽。
- override 覆盖:父子类,函数的名称、返回值 类型、参数的类型个数都相同有virtual关键字
- overload 重载:同一个类,函数名称相同,参 数不同(类型,顺序,个数)
7、基类与派生类对象间的相互转换
- 可以把派生类的对象赋值给基类的对象
- 可以把派生类的对象赋值给基类的引用
- 可以声明基类的指针指向派生类的对象 (向上转型)
-
- 向下转型:一般不能进行,只有当sizeof(基类) == sizeof (派生类)
也就是说如果函数的形参是基类对象或者基类对象的引用或者基类对象的指针类型,在进行函数调用时,相应的实参可以是派生类对象 。
- 如果用户定义了基类的拷贝构造函数或者赋值运算符=,而没有显式定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。
- 定义了派生类有显式定义拷贝构造函数或者重载了派生类的对象赋值运算符=,而基类也有显式定义,派生类对象初始化新的派生类,或者在派生类对象间赋值时,只会调用派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,只有显式调用基类的拷贝构造或赋值运算符函数。
二 、多态
1、概述
通常是指对于同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为 。
2、分类
静态多态性, 和动态多态性。
静态多态性 :函数重载和运算符重载。编译器可以在编译过程中完成这种联编。
动态多态性:在程序运行时完成选择,通过虚函数来实现动态联编。 继承 + 虚函数 --->实现
3、虚函数 ---> 通过虚(函数)表 实现
- 函数原型前加一个关键字virtual即可。当成员函数成为虚函数后,则该对象的存数布局就会多一个虚函数指针,它指向一个虚函数表,表中存放虚函数入口地址。
- 如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数;即使在派生类中省略了virtual关键字,也仍然是虚函数。
- 虚函数的作用:
-
- 不加virtual时,具体调用哪个版本的disp()只取决于指针本身的类型,和指针所指对象的类型无关。
- 而加virtual时,具体调用哪个版本的disp()不再取决于指针本身的类型,而是取决于指针所指对象的类型。
- 派生类重定义虚函数格式
-
- 与基类的虚函数有相同的参数个数;
- 与基类的虚函数有相同的参数类型;
- 与基类的虚函数有相同的返回类型。
- 虚函数的访问
-
- 通过对象名调用虚函数,不会展现虚函数的性质,此时虚函数退化成一个普通函数,直接通过函数名去访问。静态联编。
- 使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;
- 使用指针访问 虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。
- 引用访问类似。
- 在类内的非虚函数成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。
- 构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。
4、虚函数指针
- 如果类中包含有虚成员函数,在用该类实例化对象时,对象的第一个成员将是一个指向虚函数表(vftable)的指针(vfptr)。虚函数表记录运行过程中实际应该调用的虚函数的入口地址。
- 动态多态性(虚函数机制)被**的条件:
-
- 基类要定义一个虚函数
- 派生类要覆盖该虚函数
- 通过基类的指针或者引用指向派生类对象
- 该指针或引用调用虚函数。
- 在构造函数或析构函数之中调用虚函数才用的是静态联编。
- 构造函数为什么不能是虚函数?
-
- 答:根据虚函数的使用条件来说,只有先创建对象,在对象创建的过程之中才有了虚函数指针(vfptr),然后才能通过它去调用虚函数。故虚函数的使用时建立在对象的创建,及构造函数之后的。
5、纯虚函数与抽象类
- 首先必须是成员函数, 没有给出实现的函数,用作借口。定义了纯虚函数的类成为抽象类,不能实例化。其实现交给派生类实现,若有多个纯虚函数,须全部实现,否则,该派生类也是抽象类。 形式: 返回值类型 函数名(函数参数) = 0
- 当在基类中无法为虚函数提供任何有实际意义的定义时,可以将该虚函数声明为纯虚函数,它的实现留给该基类的派生类去做。
class 类名
{
virtual 类型 函数名 (参数表)=0;
…
};
-
- 纯虚函数不能被直接调用创建对象,仅提供一个与派生类一致的接口。
- 一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数,该类便为抽象类。一个抽象类只能作为基类来派生新类,不能创建抽象类的对象,但可声明一个指向抽象类的指针。
- 拥有纯虚函数的累称为抽象类
- 使用多态
-
- 继承 + 虚函数 实现多态(面向对象的编程)
- bind + function 实现多态(基于对象的编程方式) --> function/bind 的救赎
- 只定义了protected型构造函数的类也是抽象类,不能直接实例化的类。
- 构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。
- 一旦基类的虚构函数称为虚函数之后,派生类的析构函数会自动称为虚函数。即使没有给出virtual关键字。
6、virtual 继承 (虚拟继承-虚函数)
- 同样使用 virtual 关键字 ---> 存在、间接和共享 这三种特征
- 虚拟继承如何表现:
-
- 存在即表示虚继承体系和虚基类确实存在
- 间接性表现在当访问虚基类的成员时同样也必须通过某种间接机制来完成(通过虚基表来完成)
- 共享性表现在虚基类会在虚继承体系中被共享,而不会出现多份拷贝
- 虚继承:在继承定义中包含了virtual关键字的继承关系 虚基类:在虚继承体系中的通过virtual继承而来的基类
语法:
class Subclass : public virtual Baseclass
{
public: //...
private: //...
protected: //...
};
其中Baseclass称之为Subclass的虚基类; 而不是说Baseclass就是虚基类
7、虚拟继承的一些特性
注:以下结论均可通过vs的图形化调试验证。
- 一:单个虚拟继承,不带虚函数
-
- 虚拟继承与继承的区别:
-
- 1、虚基类处于对象内存的末尾
- 2、多了一个虚基指针 (vbptr)
- 二:单个虚拟继承,带虚函数
-
- 如果派生类拥有自己的虚函数(并不是覆盖),它产生的虚函数指针是位于虚基指针的前面的 。
- 或者说,一个类如果有自己的虚函数,它在内存中的布局一定是位于最开始的位置,原因是为了提高(或者说是派生类的)访问虚函数的速度 。
- 三:多重继承(带虚函数)
-
- 1. 每个基类都有自己的虚表
- 2. 派生类的虚成员函数被加入到第一个基类的虚函数表中
- 3. 内存布局中,其基类布局依次按其声明时的顺序进行排列
- 4. 派生类会覆盖掉基类的对应的虚函数,第一个虚函数表中的被覆盖的虚函数地址是真实的;之后的虚函数表中的对应的 被覆盖的虚函数所存放的并不是虚函数的地址,而是一条跳转指令
- 四:多重继承具有共同的基类 vs 虚拟继承 /* 虚基指针所指向的虚基表的内容:
-
- 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
- 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
8、虚基派生的构造函数和析构函数
- 从虚基类直接或间接派生出来的子类的构造函数初始化列表均有对该虚基类构造函数的调用,这样就保证虚基类的唯一副本只被初始化一次。即虚基类的构造函数只被执行一次。
- 继承机制下析构函数的调用顺序: 先调用派生类的析构函数,然后调用派生类中成员对象的析构函数 ,再调用普通基类的析构函数 最后调用虚基类的析构函数 。
9、效率
10、扩展阅读
0. 扩展阅读
http://blog.****.net/myan/article/details/5928531
http://www.cnblogs.com/leoo2sk/archive/2009/04/09/1432103.html
1、参考阅读 http://blog.****.net/haoel/article/details/3081328
http://blog.****.net/haoel/article/details/3081385
三、总结
1、概述
- 客观世界 程序世界
类到对象 实例化
对象到类 抽 象
- 对象的管理方式:
-
- 单个对象 ---> 智能指针
- 多个对象 ----> 数据结构 + 算法 实现 (STL -->模板)
- OOP (面向对象的编程)
四、面向对象的设计
1、概述
-
-
- 面向对象分析(OOA)
- 面向对象设计(OOD)
- 面向对象编程(OOP)
-
经验:在平常的时候,没有必要提前告诉他(不然预期太高),可以学其他的东西。在紧急的时候,可以适当表现一下。 --- > 以后需要学习的事情。
- Unifed Modeling Language(UML), 又称统一建模语言或标准建模语言
- UML为软件开发提供了一些标准的图例(10),统一开发思想,从而促进团队协作 。
- 类与类之间的关系
-
- 继承(泛化) 垂直的 (A is B)空心的三角箭头
- 依赖 ---> 虚线箭头 下面四种都是横向的(水平的) A use B 例子:master 和 pat
-
- 1. 从语义上来说是 A use B,是偶然的,临时的,并非固定的, 发生在函数调用时。
- 2. B作为A的成员函数参数
- 3. B作为A的成员函数的局部变量
- 4. A的成员函数调用B的静态方法
- 关联 下面三种都是 A has B
-
- 双向的关联关系 一条直线
- 单向的关联关系 一方只知道一方存在 ->
- 关系是固定的 A has B
- 彼此并不负责对方的生命周期
- 一般使用指针或者引用
- 聚合 --->空心的菱形箭头
-
- 1、比较强的一种关联关系
- 2. 对象之间的关系表现为分为整体和局部
- 3. 整体部分并不负责局部对象的销毁
- 组合 --->实心菱形箭头
-
- 1.更强的一种关联关系
- 2. 对象之间的关系表现为分为整体和局部
- 3. 整体部分负责局部对象的销毁
- 总结: ----> 很重要的
-
-
- 继承 ---> 继承关系
- 传参数 ---> 依赖关系
- 指针或引用使用另一个类 --> 关联关系
- 整体和局部 ---> 聚合关系
- 对象成员(强相关) ---> 组合关系
-
- 关系之间的比较
-
- 继承体现的是类与类之间的纵向关系
- 其他四种体现的是类与类之间的横向关系
- 耦合强弱: 依赖 < 关联 < 聚合 < 组合
- 从语义上来看
-
- 继承(A is B)
- 关联、聚合、组合(A has B)
- 依赖(A use B)
- 当组合与依赖结合时,可以替代继承
- 组合+依赖(基于对象) vs 继承(面向对象)
- 设计原则
-
- 强调模块间保持低耦合、高内聚的关系 。
- SOLID的5原则
- 单一职责原则(Single Responsibility Principle)
-
- 一个类,最好只做一件事,只有一个引起它变化的原因。原则的核心就是解耦和增强内聚性。
- 开闭原则(Open Closed Principle)
-
- 软件实体(类,模块,函数等等)应当对扩展开放,对修改闭合。
- 能在不修改类的前提下扩展一个类的行为。
- 核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。
- 里氏替换原则(Liscov Substitution Principle)
-
- 子类必须能够替换其基类。
- 主要着眼于抽象和多态建立在继承的基础上
- 接口分离原则(Interface Segregation Principle)
-
- 接口应该是内聚的,应该避免“胖”接口
- 依赖倒置原则(Dependency Inversion Principle)
-
- 面向接口编程,依赖于抽象
- a.高层模块不依赖于底层模块,二者都同依赖于抽象;
- b.抽象不依赖于具体,具体依赖于抽象。
- web服务器:
-
- CS : qq 360杀毒软件
- BS : WIndows: IIS asp(小型)
-
-
- Apachae(html/php) Nginx(html/php) (中小型)
- Tomcat/WebLogic(jsp) (大型)
- hadoop spark ( 大数据 )
- Linux内核
-