Lecture 14 继承——派生类
继承的基本概念
在开发一个新软件时,把现有软件或软件的一部分拿过来用称为软件复用。
目前,不加修改地直接复用已有软件比较困难。已有软件的功能与新软件所需要的功能总是有差别的。解决这个差别有下面的途径:
-
修改已有软件的源代码,它的缺点是:
- 需读懂源代码,可靠性差、易出错,有时源代码难以获得
-
继承机制(Inheritance):
- 在定义一个新的类时,先把已有的一个或多个类的功能全部包含进来,然后在新的类中再给出新功能的定义或对已有类的某些功能重新定义。
- 不需要已有软件的源代码,属于目标代码复用!
基类与派生类
在继承关系中存在两个类:基类(或称父类)和派生类(或称子类)。派生类拥有基类的所有成员,并可以
-
定义新的成员,或
-
对基类的一些成员(成员函数)进行重定义。
继承分为:单继承和多继承
-
单继承:一个类只有一个直接基类。
-
多继承:一个类有多个直接基类。
继承对程序设计的支持
继承机制除了支持软件复用外,它还具有下面的作用:
-
对处理的对象按层次进行分类。
- 有利于问题的描述和解决。
-
对概念进行组合
- 给新类的设计带来便利。
-
支持软件的增量开发。(版本升级)
- 这给软件开发和维护带来便利。
单继承
在定义单继承时,派生类只能有一个直接基类,其定义格式如下:
class <派生类名>:[<继承方式>] <基类名>
{ <成员说明表>
};
-
<派生类名>
为派生类的名字。 -
<基类名>
为直接基类的名字。 -
<成员说明表>
是在派生类中新定义的成员,其中包括对基类成员的重定义。 -
<继承方式>
用于指出从基类继承来的成员在派生类中对外的访问控制
class A //基类
{ int x,y;
public:
void f();
void g();
};
class B: public A //派生类
{ int z; //新成员
public:
void h(); //新成员
};
派生类除了拥有新定义的成员外,还拥有基类的所有成员(基类的构造函数、析构函数和赋值操作符重载函数除外)。例如:
class A //基类
{ int x,y;
public:
void f();
void g();
};
class B: public A //派生类
{ int z; //新成员
public:
void h(); //新成员
};
b.f(); //A类中的f
b.g(); //A类中的g
b.h(); //B类中的h
定义派生类时一定要见到基类的定义。
class A; //声明
class B: public A //Error
{ int z;
public:
void h() { g(); } //Error,编译程序不知道基类中
//是否有函数g以及函数g的原型。
};
......
B b; //Error,编译无法确定b所需内存空间的大小。
如果在派生类中没有显式说明,基类的友元不是派生类的友元;如果基类是另一个类的友元,而该类没有显式说明,则派生类也不是该类的友元。
在派生类中访问基类成员
C++中,派生类不能直接访问基类的私有成员。
class A
{ int x,y;
public:
void f();
void g() { ... x ,y ... }
};
class B: public A
{ int z;
public:
void h()
{ ... x,y ... //Error,x、y为基类的私有成员。
f(); //OK
g(); //OK,通过函数g访问基类的私有成员x和y。
}
};
继承与封装的矛盾
在派生类中定义新的成员函数或对基类已有成员函数重定义时,往往需要直接访问基类的一些private成员(特别是private数据成员),否则新的功能无法实现。
类的private成员是不允许外界使用的(数据封装)!
这样就带来了继承与封装的矛盾。
实际上,有了继承机制以后,一个类的成员有两种被外界使用的场合:
-
通过类的对象(实例)使用
-
在派生类中使用
class A
{ ......
m
};
class B: public A
{ ......
f() { ... m ...} //通过派生类使用A的成员m
};
void g()
{ A a;
... a.m ... //通过A的对象(实例)使用A的成员m
}
在派生类中访问基类成员(protected访问控制)
在C++中,除了public
和private
,还提供了另外一种类成员访问控制:protected
,
-
用
protected
说明的成员不能通过对象使用,但可以在派生类中使用。 -
protected
访问控制缓解了封装与继承的矛盾
C++类向外界提供两种接口:
-
public
:对象的使用者(类的实例用户) -
public
+protected
:派生类
class A
{ protected:
int x,y;
public:
void f();
};
class B: public A
{ ......
void h()
{ f(); //OK
... x ... //OK
... y ... //OK
}
};
void g()
{ A a;
a.f(); //OK
... a.x ... //Error
... a.y ... //Error
}
-
引进protected成员访问控制后,基类的设计者必须要慎重地考虑应该把那些成员声明为protected。
-
一般情况下,应该把今后不太可能发生变动的、有可能被派生类使用的、不宜对实例用户公开的成员声明为protected!
派生类成员标识符的作用域
派生类对基类成员的访问除了受到基类的访问控制的限制以外,还要受到标识符作用域的限制。
对基类而言,派生类成员标识符的作用域是嵌套在基类作用域中的。
class A
{ ......
void f() { g(); } //Error!
......
}
class B:public A
{ ......
void g() { f(); } //OK
}
如果派生类中定义了与基类同名的成员,则基类的成员名在派生类的作用域内不直接可见(被隐藏,Hidden)。访问基类同名成员时要用基类名受限。例如:
class A //基类
{ int x,y;
public:
void f();
void g();
};
class B: public A
{ int z;
public:
void f();
void h()
{ f(); //B类中的f
A::f(); //A类中的f
}
};
即使派生类中定义了与基类同名但参数不同的成员函数,基类的同名函数在派生类的作用域中也是不直接可见的,可以用基类名受限方式来使用之:
class A //基类
{ int x,y;
public:
void f();
void g();
};
class B: public A
{ int z;
public:
void f(int); //不是重载A的f!
void h()
{ f(1); //OK
f(); //Error
A::f(); //OK
}
};
也可以在派生类中使用using
声明把基类中某个的函数名对派生类开放:
class A //基类
{ int x,y;
public:
void f();
void g();
};
class B: public A
{ int z;
public:
using A::f;
void f(int);
void h()
{ f(1); //OK
f(); //OK,等价于A::f();
}
};
基类成员在派生类中对外的访问控制(继承方式)
在C++中,派生类拥有基类的所有成员。问题是:
-
基类的成员在派生类中对外的访问控制是什么?即,
-
派生类的用户能访问基类的哪些成员?
上面的问题由基类的访问控制与继承方式共同决定。继承方式在定义派生类时指定:
class <派生类名>:[<继承方式>] <基类名>
{ <成员说明表>
};
-
继承方式可以是:public、private和protected。
-
默认的继承方式为:private。
继承方式的含义
基类成员 派生类 继承方式 | public | private | protected |
---|---|---|---|
public | public | 不可直接访问 | protected |
private | private | 不可直接访问 | private |
protected | protected | 不可直接访问 | protected |
class A
{ public:
void f();
protected:
void g();
private:
void h();
};
class B: public A
{ //f为public
//g为protected
//h为不可直接访问
public:
void q()
{ f(); //?
g(); //?
h(); //?
}
};
class C: public B
{ public:
void r()
{ f(); //OK
g(); //OK
h(); //Error
q(); //?
}
};
void func()
{ B b;
b.f(); //OK
b.g(); //Error
b.h(); //Error
b.q(); //?
}
?跟B的继承方式无关!
继承方式的调整
class A
{ public:
void f1();
void f2();
void f3();
protected:
void g1();
void g2();
void g3();
};
class B: private A
{ public:
A::f1; //把f1调整为public
A::g1; //把g1调整为public
//是否允许弱化基类的访问控制要视具体的实现而定
protected:
A::f2; //把f2调整为protected
A::g2; //把g2调整为protected
......
};
子类型
对用类型T表达的所有程序P,当用类型S去替换程序P中的所有的类型T时,程序P的功能不变,则称类型S是类型T的子类型。
-
类型T的操作也适合于类型S。
-
在需要T类型数据的地方可以用S类型的数据去替代(类型S的值可以赋值或作为函数参数传给T类型变量)。
在C++中,把类看作类型,把以public方式继承的派生类看作是基类的子类型。
-
对基类对象能实施的操作也能作用于派生类对象。
-
在需要基类对象的地方可以用派生类对象去替代(派生类对象可以赋值或作为函数参数传给基类变量) 。
对于下面的两个类A和B
class A //基类
{ int x,y;
public:
void f() { x++; y++; }
......
};
class B: public A //派生类
{ int z;
public:
void g() { z++; }
......
};
下面的操作是合法的:
A a;
B b;
b.f(); //OK,基类的操作可以实施到派生类对象
a = b; //OK,派生类对象可以赋值给基类对象,属于派生类
//但不属于基类的数据成员将被忽略
A *p = &b; //OK,基类指针变量可以指向派生类对象
......
void func1(A *p);
void func2(A &x);
void func3(A x);
func1(&b); func2(b); func3(b); //OK
下面的操作是不合法的:
A a;
B b;
a.g(); //Error,a没有g这个成员函数。
b = a; //Error,它将导致b有不一致的成员数据
//(a中没有这些数据)。
B *q = &a; //Error,“q->g();”会修改不属于a的数据!
......
void func1(B *p);
void func2(B &x);
void func3(B x);
func1(&a); func2(a); func3(a); //Error
派生类对象的初始化和消亡处理
派生类对象的初始化由基类和派生类共同完成:
-
从基类继承的数据成员由基类的构造函数初始化;
-
派生类的数据成员由派生类的构造函数初始化。
当创建派生类的对象时,
-
先执行基类的构造函数,再执行派生类构造函数。
-
默认情况下,调用基类的默认构造函数,如果要调用基类的非默认构造函数,则必须在派生类构造函数的成员初始化表中指出。
包含成员对象的对象消亡时,
- 先调用本身类的析构函数,执行完后会自动去调用基类的析构函数。
class A
{ int x;
public:
A() { x = 0; }
A(int i) { x = i; }
};
class B: public A
{ int y;
public:
B() { y = 0; }
B(int i) { y = i; }
B(int i, int j):A(i) { y = j; }
};
......
B b1; //执行A::A()和B::B(),b1.x等于0,b1.y等于0。
B b2(1); //执行A::A()和B::B(int),b2.x等于0,b2.y等于1。
B b3(1,2); //执行A::A(int)和B::B(int,int),b3.x等于1,
//b3.y等于2。
对未提供任何构造函数的派生类,编译程序会隐式地为之提供一个默认构造函数,其作用是负责调用基类的构造函数。
对未提供析构函数的派生类,编译程序也会隐式地为之提供一个析构函数,该析构函数的作用是负责调用基类的析构函数。
如果一个类D既有基类B、又有成员对象类M,则
-
在创建D类对象时,构造函数的执行次序为:B->M->D
-
当D类的对象消亡时,析构函数的执行次序为:D->M->B
派生类拷贝构造函数:
-
派生类的隐式拷贝构造函数(由编译程序提供)将会调用基类的拷贝构造函数。
-
派生类自定义的拷贝构造函数在默认情况下则调用基类的默认构造函数。需要时,可在派生类自定义拷贝构造函数的“成员初始化表”中显式地指出调用基类的拷贝构造函数。
class A { ...... };
class B: public A
{ ......
public:
B() { ...... }
B(const B& b):A(b) //调用A类的拷贝构造函数
{ ...... //用b对this的派生类成员进行初始化
}
};
B b1;
B b2(b1);
派生类对象的赋值操作
派生类隐式的赋值操作除了对派生类成员进行赋值外,还将调用基类的赋值操作对基类成员进行赋值。
派生类自定义的赋值操作符重载函数不会自动调用基类的赋值操作,需要在自定义的赋值操作符重载函数中显式地指出。
class A { ...... };
class B: public A
{ ......
public:
B& operator =(const B& b)
{ if (&b == this) return *this; //防止自身赋值。
*(A*)this = b; //调用基类的赋值操作符对基类成员
//进行赋值。也可写成:
//this->A::operator =(b);
...... //对派生类的成员赋值
return *this;
}
};
......
B b1,b2;
b1 = b2;
实例:一个公司中的职员类和部门经理类的设计。
class Employee //普通职员类
{ String name; //String为字符串类。
int salary;
public:
Employee(const char *s, int n=0):name(s)
{ salary = n;
}
void set_salary(int n) { salary = n; }
int get_salary() const { return salary; }
String get_name() const { return name; }
};
const int MAX_NUM_OF_EMPS=20;
class Manager: public Employee //部门经理类
{ Employee *group[MAX_NUM_OF_EMPS];
int num_of_emps;
public:
Manager(const char *s, int n=0): Employee(s,n)
{ num_of_emps = 0;
}
bool add_employee(Employee *e);
bool remove_employee(Employee *e);
int get_num_of_emps() { return num_of_emps; }
};
Manager m("Mark",4000); //创建一个经理对象Mark
cout << "Mark's salary is " << m.get_salary() << '.' << endl; //显示经理Mark的工资
Employee e1("Jack",1000),e2("Jane",2000); //创建职员对象Jack和Jane
m.add_employee(&e1); //把职员Jack纳入经理Mark的管理
m.add_employee(&e2); //把职员Jane纳入经理Mark的管理
cout<< "Number of employees managed by Mark is " << m.get_num_of_emps() << '.' << endl; //显示经理Mark的管理人数
m.remove_employee(&e1); //职员Jack脱离经理Mark的管理
......