面向对象编程的三个基本概念:
---数据抽象:类实现数据抽象;
---继承:派生类继承基类的成员;
---动态绑定:使编译器能够在运行时决定使用基类中定义的函数还是派生类中定义的函数。
动态绑定
通过基类的引用或者指针调用虚函数时,发生动态绑定。引用(或者指针)既可以只想基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或者指针)调用的虚函数在运行时确定,被调用的函数是引用(或者指针)所指向的对象的实际类型所定义的。
继承
派生类可以访问基类的public成员,不可以访问private成员。
private成员可以被基类的成员或者友元访问,但不能被该类型的普通用户访问
protect成员可以像public一样被派生类对象访问,但像private一样 不能被该类型的普通用户访问。
注意:
“派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限”,这句话可以理解为:如下例
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
double ret=price;//正确,通过自身对象访问基类的protected成员
ret=d.price;//正确,通过对象d访问其积累的protected成员
ret=b.price//不正确,不能直接访问基类对象的protected成员
}
构造函数和复制控制
A、派生类的构造函数:
基类部分如果不显示的定义,则调用积累的默认构造函数;
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。但是派生类的构造函数可以通过将派生类包含在构造函数初始化列表来间接初始化继承成员。
B、复制控制
派生类的复制构造函数:
1、显式使用基类的复制构造函数
2、没有显式使用基类的复制构造函数则调用积累的默认构造函数
派生类的赋值操作符:必须对基类部分进行显式赋值
派生类的析构函数:不负责撤销基类对象的成员,每个析构函数只负责自己的成员。因此要在内部显式调用基类的析构函数
C、虚析构函数
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指向对象类型的不同而不同;基类有虚析构函数的话,最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。
虽然把析构函数定义成virtual的可以解决这个问题,但是当其它成员函数都不是virtual函数时,会在基类和派生类引入vtable,实例引入vptr造成运行时的性能损失。如果确定不需要直接而是只通过派生类对象使用基类,可以把析构函数定义为protected(这样会导致基类和派生类外使用自动对象和delete时的错误,因为访问权限禁止调用析构函数),就不会导致以上问题。
如果基类的析构函数为虚函数,则派生类的也是虚函数;
基类析构函数是三法则的一个重要例外。
D、纯虚函数
在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类
去做。这就是纯虚函数的作用。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。
E、多态
编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能,如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
1、非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
2、只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
3、当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
4、如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。
句柄类