类与类之间的关系:组合(一个类包含了其他类)、依赖(一个类使用了其他类)和继成(一个类继承了其他类)。
一、类的组合
1、类的组合关系
某一个或几个类的对象作为其他某个类的成员,例:
class A
{
...
};
class B
{
...
};
class C
{
int x;
char y;
A a;
B b;
public:
...
};
其中,x和y是普通的数据成员,a和b是对象成员。C类包含了A类和B类的对象,此时称C与A和B之间的关系为组合关系。
2、对象成员的构造与析构
对象与它内部的对象成员有相同的生命周期。当创建一个对象时,先调用对象成员的构造函数再调用对象自身的构造函数,析构时顺序完全相反。
如果对象成员的构造函数需要传递参数,那么这个参数传递的过程需要放在新类构造函数的初始化表中来解决,格式如下:
新类构造函数(新类构造函数实际参数表, 成员对象实际参数表):成员对象名(成员对象实际参数表)
例:
class A
{
int x;
public:
A(int a) {x = a;}
};
class B
{
int y;
A a1, a2;
public:
B(int a, int b, in c):a1(a), a2(b)
{
y = c;
}
};
对象成员构造函数的调用顺序与初始化表中的出现次序无关,它始终与对象成员在类中定义的次序一致。
二、类的依赖
一个对象中某一个函数的实现需要另一个对象,例:
class A
{
...
};
class B
{
int x;
public:
B(...);
void Print(A a, A b);
}
三、类的继承与派生
1、派生类的定义
一个新定义的类具有某个或某些旧类的功能与成员,但它又不完全与旧类相同,而是额外添加了一些功能和成员。旧类成为基类(父类),新类称为派生类(子类)。在C++中一个基类可以派生出多个派生类,一个派生类也可以有多个基类,派生类也可以作为基类继续派生出新的派生类。
派生类定义语法格式如下:
class <派生类名>:[继承方式] <基类名1> [, [继承方式] <基类名2>, ... , [继承方式] <基类名n>]
{
<派生类新增的数据成员和成员函数定义>
};
说明:
①派生类的定义方式与普通类定义类似,只是要在派生类名称与类体之间必须给出继承方式与基类名,对于单一继承只有一个基类名,对于多重继承,有多个基类名,彼此之间以逗号分隔;
②继承方式指明派生类是以什么方式继承基类,共有3种:public(公有)、private(私有)和protected(保护),如果缺省则默认为私有继承方式。
引入继承机制的优势是提高代码的可重用性。
三种不同的继承方式会导致基类成员的访问属性在派生类中会发生不同的变化。
(1)公有继承
基类公有成员——>派生类公有成员
基类保护成员——>派生类保护成员
基类私有成员不可被继承,在派生类中不可见。
(2)保护继承
基类公有成员——>派生类保护成员
基类保护成员——>派生类保护成员
基类私有成员不可被继承,在派生类中不可见。
(3)私有继承
基类公有成员——>派生类私有成员
基类保护成员——>派生类私有成员
基类私有成员不可被继承,在派生类中不可见。
总结:基类的私有成员不可以被继承,因此在派生类中无法访问。Protected继承和Private继承改变了基类成员的访问属性,限制了这些成员的进一步派生,因而在实际编程中极少使用。
2、派生类对象的构造与析构
(1)派生类对象的构造函数
对一个派生类而言,新增加成员的初始化可以在派生类的构造函数中完成,其基类成员的初始化则必须在基类的构造函数中完成。C++中,这个工作需要借助派生类构造函数对基类构造函数的调用来实现,具体如下:
①如果基类构造函数没有参数,那么系统将默认调用基类的没有参数的构造函数;
②如果基类构造函数需要传递参数,那么要在派生类的构造函数初始化列表中对基类构造函数进行调用。
基类构造函数需传递参数时,派生类构造函数的语法格式如下:
<派生类名>(<总形式参数表>):<基类名1>(<参数表1>), <基类名2>(<参数表2>)[, ...<基类名n>(<参数表n>), <其它初始化项>]
{
[<派生类自身数据成员的初始化>]
}
说明:
①派生类只需负责直接基类构造函数的调用。若直接基类构造函数不需要提供参数,则无需在初始化列表中列出,但实质上也会自动调用基类构造函数的;
②基类构造函数的调用通过初始化列表来完成。当创建一个对象时,实际调用次序为声明派生类时各基类出现的次序,而不是各基类构造函数在初始化表中的顺序。
③其它初始化项包括对象成员、常成员和引用成员等。另外,普通数据成员的初始化也可以放在这里进行。
定义一个派生类对象时,构造函数的调用顺序为:基类构造函数、派生类对象成员构造函数(按定义顺序)、派生类构造函数;析构函数的调用顺序与此相反。
(2)派生类对象的析构函数
派生类析构函数只能完成对新增加数据成员的清理工作,而基类的则应由基类的析构函数完成。由于析构函数没有参数,因此派生的析构函数默认直接调用了基类的析构函数。
3、同名冲突及其解决方案
(1)派生类与基类的同名冲突
派生类中的新成员名称与基类中的某个成员同名,此时同名覆盖将发生作用:无论是派生类成员函数还是派生类对象访问同名成员,如果不加任何标识,访问的都是派生类新定义的同名成员。而派生类成员函数或派生类对象若要访问基类中的同名成员,则必须在同名成员前加上“基类名::”进行限定。
说明:
①派生类指针或引用访问的是派生类的同名成员;
②基类引用成为派生类对象别名时,访问的是基类中的同名成员;
③基类指针指向派生类对象时,访问的是基类中的同名成员;
④若通过基类指针或引用访问派生类同名成员,设计下一章虚函数。
(2)多个直接基类引发的同名冲突
一个派生类的多个直接基类中有同名成员,访问时在成员前指明基类名即可访问该基类成员。如:
class A
{
int a;
public:
...
};
class B
{
int b;
public:
...
};
class C:public A, public B
{
int c;
public:
...
void Print()
{
A::a ...;
B::b ...;
}
};
(3)共同祖先基类多重拷贝引发的同名冲突
设Base类中有成员a,那么在Derived类中将会有两个成员a。
以上问题的一种解决方法时通过Base1::a或Base2::a的方式来区分两个a;
但是,在Derived类中出现两个a通常与实际不符,因此C++中提供了另外一种解决方案:虚基类。
虚基类的定义通过关键字“virtual”来实现,语法格式如下:
class 派生类名:virtual 继承方式 基类名
{
...
};
或
class 派生类名:继承方式 virtual 基类名
{
...
};
virtual确保虚基类的构造函数至多被调用一次。程序运行时,系统会进行检查:如果虚基类的构造函数还没有被调用过,那就调用一次,如果已经被调用过了,那就忽略此次调用。
根据C++规定,只有最后一层派生类对虚基类构造函数的调用发挥作用,例:
class A
{
int a;
public:
...
};
class B1:virtual public A
{};
class B2:virtual public A
{};
class C:public B1, public B2
{
int c;
public:
C(int xc, int xb1, int xb2, int xa):B1(xb1), B2(xb2), A(xa)
{...}
};
上例中,虚基类成员a的值最终为xa。
此时创建一个对象时,其完整的构造函数调用顺序是:所有虚基类构造函数——>所有直接基类构造函数——>所有对象成员的构造函数——>派生类自己的构造函数;析构函数与之相反。
4、赋值兼容规则
所谓赋值兼容规则是指需要使用基类的地方可以使用其公有派生类来代替,换言之,公有派生类可以当成基类来使用。公有派生类继承了基类中除构造函数、析构函数以外的所有非私有成员,且访问权限也完全相同,因此当外界需要基类时,完全可以用它来代替。
赋值兼容常见的4种情形:
①基类对象=公有派生类对象, 基类对象获得基类成员部分,派生类中新增的成员不能被基类对象访问;
②指向基类对象的指针=公有派生类对象的地址,利用赋值后的指针可以访问派生类中的基类成员;
③指向基类对象的指针=指向公有派生类对象的指针,利用赋值后的指针可以访问原指针所指向对象的基类成员;
④基类的引用=公有派生类对象,即派生类对象可以初始化基类的引用,赋值后的引用只可以访问基类成员部分,不可以访问派生类新增成员。
注意:使用赋值兼容时,必须是公有派生类!!!