C++ 虚函数&纯虚函数&抽象类&接口&虚基类&析构函数&虚析构函数



转至:http://www.cnblogs.com/fly1988happy/archive/2012/09/25/2701237.html

1. 多态

在面向对象语言中,接口的多种不同实现方式即为多态。多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数。

多态性就是允许将子类类型的指针赋值给父类类型的指针,多态是通过虚函数实现的。

多态可以让父类的指针有“多种形态”,这是一种泛型技术。(所谓泛型技术,就是试图使用不变的代码来实现可变的算法)。

2. 虚函数

2.1虚函数定义

在基类的类定义中,定义虚函数的一般形式:

Virtual 函数返回值类型 虚函数名(形参表)
{函数体}

虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public。

2.2 虚函数的作用

虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义(形式同上)。在派生类中定义的函数应与虚函数具有相同的形参个数和形参类型(覆盖),以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

虚函数可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类虚成员函数调用基类指针,则会调用其真正指向的对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向哪个派生类对象,调用时都会调用基类中定义的那个函数。

2.3 实现动态联编需要三个条件:

1)必须把需要动态联编的行为定义为类的公共属性的虚函数;
2)类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来;
3)必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。

2.4 定义虚函数的限制

1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。

2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名同返回值同参数个数同参数类型函数。

2.5 

1)为什么类的静态成员函数不能为虚函数: 

如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(在内存中只有一份拷贝,通过类名或对象引用访问静态成员)本身就是相矛盾的。

2)为什么构造函数不能为虚函数:

因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保vtbl的构建成功呢?

3)虚析构函数

C++开发的时候,用来做基类的类的析构函数一般都是虚函数。当基类中有虚函数的时候,析构函数也要定义为虚析构函数。如果不定义虚析构函数,当删除一个指向派生类对象的指针时,会调用基类的析构函数,派生类的析构函数未被调用,造成内存泄露。
虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这样,当删除指向派生类的指针时,就会首先调用派生类的析构函数,不会有内存泄露的问题了。
一般情况下,如果类中没有虚函数,就不用去声明虚析构函数。当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

2.6虚函数的实现——虚函数表

虚函数是通过一张虚函数表来实现的,简称V-Table。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享,类的每个虚函数成员占据虚函数表中的一行。
在这个表中,主要是一个类的虚函数的地址表。这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。在有虚函数的类的实例中,分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就指明了实际所应该调用的函数。

3. 纯虚函数

许多情况下,在基类中不能对虚函数给出有意义的实现,则把它声明为纯虚函数,它的实现留给该基类的派生类去做。

纯虚函数的声明格式:virtual <函数返回类型说明符> <函数名> ( <参数表> )=0;

纯虚函数的作用是为派生类提供一个一致的接口。

4.抽象类(abstract class)

抽象类是指含有纯虚函数的类(至少有一个纯虚函数),该类不能创建对象(抽象类不能实例化),但是可以声明指针和引用,用于基础类的接口声明和运行时的多态。

抽象类中,既可以有抽象方法,也可以有具体方法或者叫非抽象方法。抽象类中,既可以全是抽象方法,也可以全是非抽象方法。一个继承于抽象类的子类,只有实现了父类所有的抽象方法才能够是非抽象类。

5.接口

接口是一个概念。它在C++中用抽象类来实现,在C#和Java中用interface来实现。

接口是专门被继承的。接口存在的意义也是被继承。和C++里的抽象类里的纯虚函数是相同的。不能被实例化。
定义接口的关键字是interface,例如:   
public interface MyInterface{   
public void add(int x,int y);   
public void volume(int x,int y,int z);   
}  

继承接口的关键字是implements,相当于继承类的extends。需要注意的是,当继承一个接口时,接口里的所有函数必须全部被覆盖。
当想继承多个类时,开发程序不允许,报错。这样就要用到接口。因为接口允许多重继承,而类不允许(C++中可以多重继承)。所以就要用到接口。

6.虚基类

在派生类继承基类时,加上一个virtual关键词则为虚拟基类继承,如:
class derive : virtual public base
{
};

虚基类是相对于它的派生类而言的,它本身可以是一个普通的类。只有它的派生类虚继承它的时候,它才称作虚基类,如果没有虚继承的话,就称为基类。比如类B虚继承于类A,那类A就称作类B的虚基类,如果没有虚继承,那类B就只是类A的基类。
虚继承主要用于一个类继承多个类的情况,避免重复继承同一个类两次或多次。
例如 由类A派生类B和类C,类D又同时继承类B和类C,这时候类D就要用虚继承的方式避免重复继承类A两次。

7. 抽象类VS接口

一个类可以有多个接口,只能继承一个父类??

抽象类可以有构造方法,接口中不能有构造方法;

抽象类中可以有普通成员变量,接口中没有普通成员变量;

接口里边全部方法都必须是abstract的,抽象类的可以有实现了的方法;

抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型;

抽象类中可以包含静态方法,接口中不能包含静态方法;

抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

8. 虚函数VS纯虚函数

虚函数
引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。
纯虚函数
引入原因:
1)同“虚函数”;
2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
纯虚函数就是基类只定义了函数体,没有实现过程。
纯虚函数相当于接口,不能直接实例话,需要派生类来实现函数定义;
有的人可能在想,定义这些有什么用?
比如你想描述一些事物的属性给别人,而自己不想去实现,就可以定义为纯虚函数。说的再透彻一些,比如盖楼房,你是老板,你给建筑公司描述清楚你的楼房的特性,多少层,楼顶要有个花园什么的,建筑公司就可以按照你的方法去实现了,如果你不说清楚这些,可能建筑公司不太了解你需要楼房的特性。用纯需函数就可以很好的分工合作了。

二者的区别:

1> 类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,编译器就可以使用后期绑定来达到多态了;
纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。

2>虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现;

3>虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于“介面继承”,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,介面类中只有纯虚函数的;

4>带纯虚函数的类叫抽象类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。

9.析构函数VS虚析构函数

1.为什么要定义虚析构函数?

如果有一个带有虚函数功能的类,则它需要一个虚析构函数,原因如下:
1)如果一个类有虚函数功能,它经常作为一个基类使用;
2)如果它是一个基类,它的派生类经常使用new来分配;
3)如果一个派生类对象使用new来分配,并且通过一个指向它的基类的指针来控制,那么它经常通过一个指向它的基类的指针来删除它(如果基类没有虚析构函数,结果将是不确定的,实际发生时,派生类的析构函数永远不会被调用);
基类有虚析构函数的话,最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。

2.声明为保护(protected)的析构函数

如果在一个派生类中定义了基类以外的成员对象,且基类析构函数不是virtual修饰的,那么当基类指针或引用指向派生类对象并析构(例如自动对象在函数作用域结束时;或者通过delete)时,会调用基类的析构函数而导致派生类定义的成员没有被析构,产生内存泄露等问题。
虽然把析构函数定义成virtual的可以解决这个问题,但是当其它成员函数都不是virtual函数时(即基类中没有虚函数),会在基类和派生类引入vtable,实例引入vptr造成运行时的性能损失。如果确定不需要直接而是只通过派生类对象使用基类,
可以把析构函数定义为protected(这样会导致基类和派生类外使用自动对象和delete时的错误,因为访问权限禁止调用析构函数),就不会导致以上问题。

3.构造函数、析构函数声明为私有和保护时的用法

从语法上来讲,一个函数被声明为protected或者private,那么这个函数就不能从“外部”直接被调用了。
对于protected的函数,子类的“内部”的其他函数可以调用之;
而对于private的函数,只能被本类“内部”的其他函数说调用;

通常使用的场景如下:
1)如果你不想让外面的用户直接构造一个类(假设这个类的名字为A)的对象,而希望用户只能构造这个类A的子类,那你就可以将类A的构造函数/析构函数声明为protected,而将类A的子类的构造函数/析构函数声明为public。

例如:

复制代码
class A
{ 
   protected: A(){}
   public:
   ....
};
calss B : public A
{
   public: B(){}
   ....
};

A a; // error
B b; // ok
复制代码

2)如果将构造函数/析构函数声明为private,那只能这个类的“内部”的函数才能构造这个类的对象了。
例如:

复制代码
class A
{
   private:
     A(){  }
     ~A(){ }

   public:
     void Instance()//类A的内部的一个函数
     {
        A a;
     }
};
复制代码

上面的代码是能通过编译的。上面代码里的Instance函数就是类A的内部的一个函数。Instance函数体里就构建了一个A的对象。
但是,这个Instance函数还是不能够被外面调用的。为什么呢?
如果要调用Instance函数,必须有一个对象被构造出来。但是构造函数被声明为private的了。外部不能直接构造一个对象出来。

A aObj; // 编译通不过
aObj.Instance();

但是,如果Instance是一个static静态函数的话,就可以不需要通过一个对象,而可以直接被调用。

复制代码
class A
{
   private:
     A():data(10){ cout << "A" << endl; }
     ~A(){ cout << "~A" << endl; }

   public:
     static A& Instance()
     {
         static A a;
         return a;
     }
 
     void Print()
     {
         cout << data << endl;
     }

  private:
     int data;
};

A& ra = A::Instance();
ra.Print();
复制代码

上面的代码其实是设计模式singleton模式的一个简单的C++代码实现。



课程介绍 第1章 预备知识  1.1 汇编语言的由来及其特点   1 机器语言   2 汇编语言   3 汇编程序   4 汇编语言的主要特点   5 汇编语言的使用领域  1.2 数据的表示和型   1 数值数据的表示   2 非数值数据的表示   3 基本的数据型  1.3 习题 第2章 CPU资源和存储器  2.1 寄存器组   1 寄存器组   2 通用寄存器的作用   3 专用寄存器的作用  2.2 存储器的管理模式   1 16位微机的内存管理模式   2 32位微机的内存管理模式  2.3 习题 第3章 操作数的寻址方式  3.1 立即寻址方式  3.2 寄存器寻址方式  3.3 直接寻址方式  3.4 寄存器间接寻址方式  3.5 寄存器相对寻址方式  3.6 基址加变址寻址方式  3.7 相对基址加变址寻址方式  3.8 32位地址的寻址方式  3.9 操作数寻址方式的小结  3.10 习题 第4章 标识符和表达式  4.1 标识符  4.2 简单内存变量的定义   1 内存变量定义的一般形式   2 字节变量   3 字变量   4 双字变量   5 六字节变量   6 八字节变量   7 十字节变量  4.3 调整偏移量伪指令   1 偶对齐伪指令   2 对齐伪指令   3 调整偏移量伪指令   4 偏移量计数器的值  4.4 复合内存变量的定义   1 重复说明符   2 结构型的定义   3 联合型的定义   4 记录型的定义   5 数据型的自定义  4.5 标号  4.6 内存变量和标号的属性   1 段属性操作符   2 偏移量属性操作符   3 型属性操作符   4 长度属性操作符   5 容量属性操作符   6 强制属性操作符   7 存储单元别名操作符  4.7 表达式   1 进制伪指令   2 数值表达式   3 地址表达式  4.8 符号定义语句   1 等价语句   2 等号语句   3 符号名定义语句  4.9 习题 第5章 微机CPU的指令系统  5.1 汇编语言指令格式   1 指令格式   2 了解指令的几个方面  5.2 指令系统   1 数据传送指令   2 标志位操作指令   3 算术运算指令   4 逻辑运算指令   5 移位操作指令   6 位操作指令   7 比较运算指令   8 循环指令   9 转移指令   10 条件设置字节指令   11 字符串操作指令   12 ASCII-BCD码调整指令   13 处理器指令  5.3 习题 第6章 程序的基本结构  6.1 程序的基本组成   1 段的定义   2 段寄存器的说明语句   3 堆栈段的说明   4 源程序的结构  6.2 程序的基本结构   1 顺序结构   2 分支结构   3 循环结构  6.3 段的基本属性   1 对齐型   2 组合型   3 别   4 段组  6.4 简化的段定义   1 存储模型说明伪指令   2 简化段定义伪指令   3 简化段段名的引用  6.5 源程序的辅助说明伪指令   1 模块名定义伪指令   2 页面定义伪指令   3 标题定义伪指令   4 子标题定义伪指令  6.6 习题 第7章 子程序和库  7.1 子程序的定义  7.2 子程序的调用和返回指令   1 调用指令   2 返回指令  7.3 子程序的参数传递   1 寄存器传递参数   2 存储单元传递参数   3 堆栈传递参数  7.4 寄存器的保护与恢复  7.5 子程序的完全定义   1 子程序完全定义格式   2 子程序的位距   3 子程序的语言型   4 子程序的可见性   5 子程序的起始和结束操作   6 寄存器的保护和恢复   7 子程序的参数传递   8 子程序的原型说明   9 子程序的调用伪指令   10 局部变量的定义  7.6 子程序库   1 建立库文件命令   2 建立库文件举例   3 库文件的应用   4 库文件的好处  7.7 习题 第8章 输入输出和中断  8.1 输入输出的基本概念   1 I/O端口地址   2 I/O指令  8.2 中断   1 中断的基本概念   2 中断指令   3 中断返回指令   4 中断和子程序  8.3 中断的分   1 键盘输入的中断功能   2 屏幕显示的中断功能   3 打印输出的中断功能   4 串行通信口的中断功能   5 鼠标的中断功能   6 目录和文件的中断功能   7 内存管理的中断功能   8 读取和设置中断向量  8.4 习题 第9章 宏  9.1 宏的定义和引用   1 宏的定义   2 宏的引用   3 宏的参数传递方式   4 宏的嵌套定义   5 宏与子程序的区别  9.2 宏参数的特殊运算符   1 连接运算符   2 字符串整体传递运算符   3 字符转义运算符   4 计算表达式运算符  9.3 与宏有关的伪指令   1 局部标号伪指令   2 取消宏定义伪指令   3 中止宏扩展伪指令  9.4 重复汇编伪指令   1 伪指令REPT   2 伪指令IRP   3 伪指令IRPC  9.5 条件汇编伪指令   1 条件汇编伪指令的功能   2 条件汇编伪指令的举例  9.6 宏的扩充   1 宏定义形式   2 重复伪指令REPEAT   3 循环伪指令WHILE   4 循环伪指令FOR   5 循环伪指令FORC   6 转移伪指令GOTO   7 宏扩充的举例   8 系统定义的宏  9.7 习题 第10章 应用程序的设计  10.1 字符串的处理程序  10.2 数据的分统计程序  10.3 数据转换程序  10.4 文件操作程序  10.5 动态数据的编程  10.6 COM文件的编程  10.7 驻留程序  10.8 程序段前缀及其应用   1 程序段前缀的字段含义   2 程序段前缀的应用  10.9 习题 第11章 数值运算协处理器  11.1 协处理器的数据格式   1 有符号整数   2 BCD码数据   3 浮点数  11.2 协处理器的结构  11.3 协处理器的指令系统   1 操作符的命名规则   2 数据传送指令   3 数学运算指令   4 比较运算指令   5 超越函数运算指令   6 常数操作指令   7 协处理器控制指令  11.4 协处理器的编程举例  11.5 习题 第12章 汇编语言和C语言  12.1 汇编语言的嵌入  12.2 C语言程序的汇编输出  12.3 一个具体的例子  12.4 习题 附录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值