C++之组合与继承
组合:将一个类的对象作为另一个类的成员,被称作组合或包含,创建包含对象成员的组合对象时,会执行成员类的构造函数初始化对象成员。成员初始化的次序和成员声明的次序相同,并不考虑它们在初始化列表中的排列顺序。
复用类:
一、如果可以获得源码,修改其源代码
二、如果不能获得源代码,或者担心因修改代码引入错误,这时我们可以应用组合,重新包装一个类对象,对外提供新接口,使之用于特定的环境。
继承:继承是面向对象的三大特征(封装、继承、多态)之一。
继承也是一种复用已有类的机制,在已有类的基础上继承得到新类型,这个新类型自动拥有已有类的特性,并可以修改继承到的特性或者增加自己的新特性。
在C++中,
被继承的已有类称为基类;
继承得到的新类称为派生类;
派生类可以再被继承,这样构成的层次结构称为继承层次
类继承关系的语法形式
class 派生类名 : 基类名表
{
数据成员和成员函数声明
};
基类名表 构成
访问控制 基类名1, 访问控制 基类名2 ,… , 访问控制 基类名n
访问控制 表示派生类对基类的继承方式,使用关键字:public(公有继承),private (私有继承),protected(保护继承)
不论种方式继承基类,派生类都不能直接使用基类的私有成员
派生类的生成过程经历了三个步骤:
一、吸收基类成员(全部吸收(构造、析构除外),但不一定可见)
二、改造基类成员:可以通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽(隐藏)在派生类中不起作用的部分基类成员。
三、添加派生类新成员:仅仅继承基类的成员是不够的,需要在派生类中添加新成员,以保证派生类自身特殊属性和行为的实现。
重名成员:
在改造基类成员时:若派生类定义了与基类同名的成员,在派生类中访问同名成员时屏蔽(hide)了基类的同名成员,若要在派生类中使用基类的同名成员,可显式地使用类名限定符:类名 :: 成员
例:
#include<iostream>
using namespace std ;
class A
{ public:
int a1, a2 ;
A( int i1=0, int i2=0 ) { a1 = i1; a2 = i2; }
void print()
{ cout << "a1=" << a1 << '\t' << "a2=" << a2 << endl ; }
};
class B : public A
{
public:
int b1, b2 ;
B( int j1=1, int j2=1 ) { b1 = j1; b2 = j2; }
void print() //定义同名函数
{ cout << "b1=" << b1 << '\t' << "b2=" << b2 << endl ; }
void printAB()
{
A::print() ; //派生类对象调用基类版本同名成员函数
print() ; //派生类对象调用自身的成员函数
}
};
int main()
{ B b ; b.A::print(); b.printAB(); }
运行结果如下:
派生类构造函数和析构函数的定义规则 :
一、基类的构造函数和析构函数不能被继承
二、如果基类没有定义构造函数或有无参的构造函数, 派生类也可以不用定义构造函数
三、如果基类无无参的构造函数,派生类必须定义构造函数
四、如果派生类的基类也是派生类,则每个派生类只负责直接基类的构造
五、派生类是否定义析构函数与所属的基类无关
派生类与基类构造、析构的顺序:
我们可以通过下面的例子得出结论:
例:
class B
{
public:
B() { cout<<"B()"<<endl; }
~B() { cout<<"~B()"<<endl; }
};
class D : public B
{
public:
D(){ cout<<"D()"<<endl; }
~D() { cout<<"~D()"<<endl; }
};
int main()
{
D d;
return 0;
}
运行结果:
B()
D()
~D()
~B()
多继承:
一个类有多个直接基类的继承关系称为多继承。
多继承声明语法:
class 派生类名 : 访问控制 基类名1 , 访问控制 基类名2 , … , 访问控制 基类名n
{
数据成员和成员函数声明
};
多继承的派生类构造、析构和访问:
多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员,如下面的例子。
执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
析构函数的执行顺序与多继承方式下构造函数的执行顺序完全相反,首先对派生类新增的数据成员进行清理,再对派生类对象成员进行清理,最后才对基类继承来的成员进行清理。
一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。
例:
class Base1
{ public:
Base1(int x) { value = x ; }
int getData() const { return value ; }
protected:
int value;
};
class Base2
{ public:
Base2(char c) { letter=c; }
char getData() const { return letter;}
protected:
char letter;
};
class Derived : public Base1, public Base2
{ friend ostream &operator<< ( ostream &, const Derived & ) ;
public :
Derived ( int, char, double ) ;
double getReal() const ;
private :
double real ;
};
int main()
{ Base1 b1 ( 10 ) ;
Base2 b2 ( 'k' ) ;
Derived d ( 5, 'A', 2.5 ) ;
return 0 ;
}
结果如下:
赋值兼容规则 :
赋值兼容规则指在程序中需要使用基类对象的任何地方,都可以用公有派生类的对象来替代。
赋值兼容规则中所指的替代包括以下的情况:
a 派生类的对象可以赋给基类对象
b 派生类的对象可以初始化基类的引用
c 派生类的对象的地址可以赋给基类类型的指针
赋值兼容的可行性:
通过公有继承,派生类得到了除了构造、析构函数以外的所有成员,且这些成员的访问控制属性也和基类完全相同,这样,它便具备了基类的所有功能。
例:
class Base{
…
};
class Derived:public Base{
…
};
根据赋值兼容规则, 以下几种情况是合法的:
(1) 可以用派生类对象给基类对象赋值。例如:
Base b;
Derived d;
b=d;
这样赋值的效果是,对象b中所有数据成员都将具有对象d中对应数据成员的值。
(2) 可以用派生类对象来初始化基类的引用。例如:
Derived d;
Base &br=d;
(3) 可以把派生类对象的地址赋值给指向基类的指针。例如:
Derived d;
Base *bptr=&d;
这种形式的转换,是在实际应用程序中最常见到的。
(4) 可以把指向派生类对象的指针赋值给指向基类对象的指针。例如:
Derived *dptr,obj; dptr=&obj;
Base *bptr=dptr;
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
赋值兼容应注意的问题:
(1)声明为指向基类的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象。例如:
class B {…};
class D:private B {…};
B b1,*pbl;D d1;
pb1=&b1; //合法,基类B的对象b1和B类的指针
pb1=&d1; //非法,不允许将基类指针指向它的私有派生类对象
(2)允许将一个声明为指向基类的指针指向其公有派生类对象,但是不能将一个声明为指向派生类对象的指针指向其基类的一个对象。
(3) 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类中从基类继承来的成员,而不能直接访问公有派生类的定义的成员。