八、继承:让某个类的对象获得另一个类的对象的特性。通过继承可实现代码重用,即从已存在的类派生出的一个新类将自动具有原来那个类的特性。
类的继承还具有:(1)单向性;A类为B类的基类(父类),则派生类(子类)B继承了父类A中的属性和方法,在B类中可访问A类的属性和方法,但在父类A中则不能访问子类的任何属性和方法。同时单向又体现为子类B继承了父类A,则A类不能再继承B类。(2)传递性;A类为B类的基类,B类为C类的基类,则基类A中的属性和方法通过子类B同时又传给了子类C,同理C类也不能作为A类的基类。(3)可重用性;通过继承,基类代码得到重用,而不用重写。
C++规定派生类中对象成员(其它类的对象)初值的设定应在初始化列表中进行,所以派生类的构造函数定义(派生类头文件中构造函数的声明格式与普通类相同)可写成:函数类型 派生类名::派生类名(形参表):基类1(参数表),基类2(参数表),...,基类n(参数表),对象成员1(参数表),对象成员2(参数表),...,对象成员n(参数表) {...}
在派生类数据成员通常有3类:继承于基类的数据成员,派生类中自身定义的数据成员以及派生类中其它类的对象。在上述形式中,由于基类在派生类中常常是隐常的,所以通过调用基类构造函数来对基类的数据成员初值化,这也称为基类拷贝或基类子对象。在上述构造函数定义中的对象成员为派生类中定义的其它类的对象(构造函数中的形参初始化)。
当然在初始化列表中也可对派生类自身数据成员初始化,但必须为“数据成员名(参数表)”形式。对于基类拷贝成员的初始化次序,若为单继承,优先初始化上层类的对象;若为多继承,则取决于派生类声明指定继承时的基类的先后次序,与初始化列表中的次序无关。对于派生类自身数据成员的初始化次序与初始化列表中的次序无关,取决于在派生类中声明的先后次序。
继承方式:公有继承(public),私有继承(private),保护继承(protected)。
公有继承:
基类对象可访问基类公有成员,不可访问保护成员和私有成员。
派生类成员可访问基类的公有成员和保护成员,它们作为派生类的成员时,保持原来的状态;若在派生类中有同名成员存在,则访问基类成员时,基类成员名前面必须加上“基类名::”来指定成员所属的类;派生类成员不可访问基类的私有成员。
派生类对象只能访问基类的公有成员,不可访问保护成员和私有成员;对于同名成员,在基类成员名前面加上“基类名::”来指定成员所属的类,如OB.OA::m或OB-> OA::m。
总之,公有继承时,基类对象与派生类对象可访问基类的公有成员,而派生类的成员函数则可访问基类的公有成员和保护成员。
私有继承:
基类对象可访问基类公有成员,不可访问保护成员和私有成员。
派生类成员可访问基类的公有成员和保护成员,它们作为派生类的成员时,作为派生类的私有成员,因此不能被这个派生类的子类所访问;若在派生类中有同名成员存在,则访问基类成员时基类成员名前面必须加上“基类名::”来指定成员所属的类;派生类成员不可访问基类的私有成员。
派生类对象不能访问基类任何成员(公有成员也不能访问)。
总之,私有继承时,基类对象可访问基类公有成员,派生类对象不能访问基类所有成员,而派生类的成员函数则可访问基类的公有成员和保护成员(但不能再往下继承)。
保护继承:
基类对象可访问基类公有成员,不可访问保护成员和私有成员。
派生类成员可访问基类的公有成员和保护成员,它们作为派生类的成员时,作为派生类的保护成员(与私有继承的唯一区别),且不能被这个派生类的子类所访问;若在派生类中有同名成员存在,则访问基类成员时基类成员名前面必须加上“基类名::”来指定成员所属的类;派生类成员不可访问基类的私有成员。
派生类对象不能访问基类任何成员(公有成员也不能访问)。
总之,保护继承时,基类对象可访问基类公有成员,派生类对象不能访问基类所有成员,而派生类的成员函数则可访问基类的公有成员和保护成员(但不能再往下继承)。
多重继承与虚继承:虚继承是为解决多重继承而出现的,是多重继承中特有的概念。例如,类D继承于类B和类C,而类B和类C又都继承于类A,即A类是B类和C类的基类,而B类和C类同时是D类的基类。为了节省内存空间 ,可以将B,C对A的继承定义为虚拟继承,则A就成了虚拟基类。用代码表示如下:
class A ;
class B:public virtual A;//虚拟继承
class C:public virtual A;//虚拟继承
class D:public B,public C;
最后说明,在面向对象的程序设计中,继承和多重继承一般是指公共继承,很少用私有继承和保护继承。
九、多态:允许将子类类型的指针赋值给父类类型的指针。多态性是通过虚函数实现的,是面向对象编程的核心概念。虚函数就是允许被其子类重新定义的成员函数,而子类重新定义父类虚函数的做法,叫做覆盖或重写。
覆盖是指子类重新定义父类的虚函数的做法。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,但很少有编译器支持)。
重载是指编写一个与已有函数同名但参数表不同(个数或类型不同)的函数。即指允许存在多个同名函数, 但这些函数的参数表不同。重载的概念并不属于面向对象编程。它的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对编译器来说是这样的)。函数重载是静态联编的具体实现方式。调用重载函数时,编译根据调用时参数类型与个数在编译时实现静态联编,将调用地址与函数名进行绑定。所以说对于重载函数的调用,在编译期间就确定了,是静态的,也就是说它们的函数标识符与内存地址在编译期就绑定了(早绑定)。因此重载与多态无关。
在静态联编的方式下,同一成员函数(参数类型及个数相同,函数返回类型也相同)在基类与派生类中的不同版本是不会在运行时根据程序代码的指定进行自动绑定的,必须通过类的虚函数机制,才能实现基类和派生类中的成员函数不同版本的动态联编。动态联编是指函数标识符与内存地址在程序运行时动态绑定,而不在编译时静态绑定,所以又称晚绑定。虚函数是用关键字virtual来修饰基类中的public或protected的成员函数。当在派生类中进行重新定义后(函数覆盖),就可在此派生类中具有该成员函数的不同版本。在程序运行过程中,依据基类对象指针所指向的派生类对象,或通过基类引用对象所引用的派生类对象,才能确定哪个派生类中重新定义的成员函数被激活,从而实现动态联编。虚函数虽然只是在一般函数定义前加上virtual,但必须是类中成员函数;可把析构函数定义为虚函数,但不能把构造函数定义为虚函数,通常在释放基类中及其派生类中动态申请的存储空间时,也要把析构函数定义为虚函数,以便实现撤销对象时的多态性;虚函数在派生类重新定义时,参数个数和类型以及函数类型必须和基类的虚函数完全匹配,这一点与函数重载完全不同;虚函数派生下去仍是虚函数,且可省略virtual关键字。
当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(所调子类的虚函数的地址无法确定)。因此,这样的函数地址是在函数运行期间绑定的(晚绑定)。
基类与派生类中的每个虚函数都在虚函数表中(vtable)占有一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存它的入口地址)。当一个包含虚函数的对象(注意,不是指针类型对象)被创建时,它在头部附加一个指针,指向vtable中相应的位置。调用虚函数的时候,不管是用什么指针调用的,它先根据 vtable找到入口地址再执行,从而实现了动态联编。而普通函数只有简单地跳转到一个固定的地址。
十、友元
只有类的成员函数才能访问类的私有成员,程序中的其它函数是无法访问私有成员的。非成员函数可以访问类中的公有成员,不能访问其私有成员。
友元是一种定义在类外部的普通函数,但它需要在类中说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。友元不是成员函数,但它可以访问类中的私有成员。友元可以提高程序的运行效率。但是它破坏了类的封装性和隐藏性,使非成员函数也可以访问类的私有成员。友元可以是一个函数,称为友元函数;友元也可以是一个类,称为友元类。
十一、this指针:this指针是类中一个特殊指针,当类实例化(用类定义对象)时,this指针指向对象自己,而在类的声明时指向类本身。
十二、关于C++中class里面 static变量问题(即面向对象的程序设计中的static),与C中的static全局变量和局部变量含义不同(C是面向过程的程序设计)
class B:
{
public:
static int a;
};
B b;
int _tmain(int argc, _TCHAR* argv[])
{
b.a = 10;
system("pause");
return 0;
}
编译出现错误:
Error error LNK2001: unresolved external symbol "public: static int B::a"
解决方法:必须初始化,初始化的时候要在类外;如下:添加int B::a = 0;也可在类中初始化,在类中写成static int a=0;
静态变量是属于类,而不是属于类对象的。也就是说不管你有多少个类对象,静态成员只有一个。 它的初始化,不能依赖于创建类对象时用于初始化对象的构造函数。
静态数据成员也遵从 public/private/protected访问规则。类中的那个static int a; 只是声明,定义要放在外面int B::a = 0; // 此处可以初始化 。如果a是公有的,B::a就可以访问了(读取和修改)。 静态成员的提出是为了解决数据共享的问题。实现共享有许多方法,如:设置全局性的变量或对象是一种方法。但是,全局变量或对象是有局限性的。在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。
使用静态数据成员可以节省内存,因为它是所有对象所公有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
同全局变量相比,使用静态数据成员有两个优势:静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;可以实现信息隐藏,静态数据成员可以是private成员,而全局变量不能;
静态数据成员的使用方法和注意事项如下:
1、静态数据成员在定义或说明时前面加关键字static。
2、静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
<数据类型><类名>::<静态数据成员名>=<值>
这表明:
(1) 初始化可在类外进行,而前面不加static,以免与一般静态变量或对象相混淆。当然也可在类中初始化。
(2) 初始化时不加该成员的访问权限控制符private,public等。
(3) 初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员。
3、静态数据成员是静态存储的,它是静态生存期,必须对它进行初始化。
4、引用静态数据成员时,采用如下格式:<类名>::<静态成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员。
与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this 是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数和静态数据成员。而非静态成员函数(普通函数)可以访问静态数据成员和调用其余的静态成员函数。
关于静态成员函数,可以总结为以下几点:
1、出现在类体外的函数定义不能指定关键字static;
2、静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
3、非静态成员函数可以任意地访问静态成员函数和静态数据成员;
4、静态成员函数不能访问非静态成员函数和非静态数据成员;
5、由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
6、调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,
也可以直接使用如下格式:<类名>::<静态成员函数名>(<参数表>)调用类的静态成员函数。