C++面向对象程序设计(五)
1.继承和派生的概念
继承:在定义一个新的类B时,如果该类与某个已有的类A相似(B拥有A的全部特点),吧么就可以把A作为一个基类,而把B作为基类的一个派生类(也称为子类)。
派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。
派生类一经定义后,可以独立使用,不依赖于基类。
派生类拥有基类的远不成员函数和成员变量,不论是public,protected,private。
在派生类的各个成员函数中,不能访问基类中的private成员。
写法:
class 派生类名:public 基类名{ };
class CStudent{
private:
string sName;
int nAge;
public:
bool isThreeGood(){};
void SetName(const string &name){sName=name;}
};
class CUndergraduateStudent:public CStudent{
private:
int nDepartment;
public:
bool is ThreeGood(){};//覆盖
bool CanBaoYan(){};
};
class CGraduatedStudent:public CStudnet{
private:
int nDepartment;
char szMentorName[20];
public:
int CountSalary(){};
};
- 派生类对象的内存空间
派生类对象的体积等于基类对象的体积加上派生类对象自己的成员变量的体积。在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。
#include <iostream>
#include <string>
using namespace std;
class CStudnet{
private:
string name;
string id;
char gender;
int age;
public:
void PrintInfo();
void SetInfo(const string & name_,const string &id_,int age_,char gender_);
string GetName(){return name;}
};
class CUndergraduateStudent:public CStudnet{
private:
string department;
public:
void QUalifiedForBaoYan(){
cout<<"qualified for baoyan"<<endl;
}
void PrintInfo(){
CStudnet::PrintInfo();
cout<<"Department:"<<department<<endl;
}
void SetInfo(const string &name_,const string &id_,int age_,char gender_,const string&department_){
CStudnet::SetInfo(name_,id_,age_,geender);
department=department_;
}
};
void CStudent::PrintfInfo(){
cout<<"Name:"<<name<<endl;
cout<<"ID:"<<id<<endl;
cout<<"Age:"<<age<<endl;
cout<<"Gender:"<<gender<<endl;
}
void CStudnet::SetInfo(const string&name_,const string & id,int age_,char gender_){
name=name_;
id=id_;
age=age_;
gender=gender_;
}
int main(){
CUndergraduateStudnet s2;
s2.SetInfo("Harry Potter","118829212",19,'M',"Computer Science");
cout<<s2.GetName()<<" ";
s2.QualifiedForBaoYan();
s2.printInfo();
return 0;
}
习题:
如果多种事物,有一些共同的特点,又有各自不同的特点,如何编写类来代表这些事物比较合适?(C)
A)为每种事物独立编写一个类,各类之间互相无关
B)写一个类代表其中一种事物,代表其他事物的类,都从这个类派生出来
C)概括所有事物的共同特点,写一个基类。然后为每种事物写一个类,都从基类派生而来
D)一共就写一个类,包含所有事物的所有特点,然后用一个成员变量作为标记来区分不同种类的事物
2.继承关系和复合关系
继承:“是”关系:
- 基类A,B是基类A的派生类
- 逻辑上要求:“一个B对象也是一个A对象”
符合:“有”关系
- 类C中“有”成员变量k,k是类D的对象,则C和D是复合关系
- 一般逻辑上要求:C对象是C对象的固有属性或组成部分
举一个栗子🌰
由CMan派生出CWoman是够合适呢?
这当然是不合理的,因为“一个女人也是一个男人”在逻辑上不成立。
比较好的做法是概括男人和女人的共同特点,写一个Chuman类,代表“人”然后CMan和CWoman都从CHuman派生。
由点类派生出圆类?
考虑“一个圆是不是一个点”,逻辑上不成立,所以我们使用复合关系,而非继承关系。(符合关系更合理)
每一个圆对象里都包含有一个“点”类的对象,这个点就是圆心。
习题:
以下哪种派生关系是合理的(A)
A)从虫子类派生出飞虫类
B)从点类派生出圆类
C)从狼类派生出狗类
D)从爬行动物派生出哺乳动物类
- 复合关系的使用综合
如果要写一个小区养狗管理程序,需要写一个“业主”类,还需要写一个“狗”类。
而狗是有主人的,主人就是业主(假定一个狗只有一个主人,但一个主人最多可以有10条狗)
- 先来分析以下是否合理
class CDog;
class CMaster{
CDog dogs[10];
};
class CDog{
CMaster m;//这样的定义属于循环定义,是错误的
};
//详细解释:
//一个CMaster占10个CDog的体积,一个CDog占1个Cmager体积,又一个Cmaster占10个CDog的体积,所以无法计算具体体积,编译就会报错
- 再来看以下是否合理
//为狗类设一个业主类的成员对象
//为业主类设一个狗类的对象指针数组
class CDog;
class CMaster{
CDog *dogs[10];
};
class CDog{
CMaster m;//如果修改一条狗的的主人的信息,那么其他狗的主人信息也要修改,不好
};
- 再来看一个设计是否合理
//为狗类设计一个业主类的对象指针
//为业主类设计一个狗类的对象数组
class CMaster;//CMaster必须提前声明,不能先写CMaster类后写CDog类。
class CDog{
CMaster *pm;
};
class CMaster{
CDog dogs[10];
};
//狗的对象都包含在主人中,对狗的操作必须要经过主人,狗失去了自由,所以仍不是一个比较好的做法
- 正确的做法
//为狗类设计一个业主类的对象指针
//为业主类设计一个狗类的对象指针数组
class CMaster;//CMaster必须提前声明,不能先写CMaster类后写CDog类
class CDog{
CMaster m;
};
class CMaster{
CDog *dogs[10];
};
3.覆盖和保护成员
- 派生类覆盖基类成员
派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员,要在派生类中访问由基类定义的同名成员时,要使用作用域符号::。
注:一般来说,基类和派生类不定义同名成员变量,但是定义同名成员函数很常见,以下栗子就方便理解,但不建议这么写
class base {
int j;
public:
int il;
void func();
};
class derived:public base{
public:
int i;
void access();
void func();
};
void derived::access(){
j=5;//错误,不可访问
i=5;//派生类的i
base::i=5;//基类的i
func();//派生类的func
base::func();//基类的func
}
习题:
派生类和基类有同名同参数表的成员函数,这种现象:(D)
A)叫重复定义,是不允许的
B)叫函数的重载
C)叫覆盖,在派生类中基类的同名函数就没用了
D)叫覆盖,体现了派生类对从基类继承得到的特点的修改
以下说法正确的是:(B)
A)派生类可以和基类有同名的成员函数,但是不能有同名成员变量(错误,可以)
B)派生类的成员函数中,可以调用基类的同名同参数表的成员函数(正确)
C)派生类和基类的同名成员函数必须参数表不同,否则就是重复定义(错误)
D)派生类和基类的同名成员变量存放在相同的存储空间(错误)
- 类的保护成员
private、public、protected存取权限
- 基类的private成员:
- 基类的成员函数
- 基类的友元函数
- 基类的public成员:
- 基类的成员函数
- 基类的友元函数
- 派生类的成员函数
- 派生类的友元函数
- 其他的函数
- 基类的protected成员
- 基类的成员函数
- 基类的友元函数
- 派生类的成员函数可以访问当前对象的基类的保护成员
class Father{
private:int nPrivate;
public:int nPublic;
protected:int nProtected;
};
class Son:public Father{
void AccessFather(){
nPublic=1;//ok
nPrivate=1;//wrong,不能访问父类私有成员
nProtected=1;//ok
Son f;
f.nProtected=1;//wrong,f不是当前对象,是protected成员,但是不能访问
}
};
int main(){
Father f;
Son s;
f.nPublic=1;//ok
s.nPublic=1;//ok
f.nProtected=1;//error,只能通过成员函数来修改
f.nPrivate=1;//error
s.nProtected=1;//error
s.nPrivate=1;//error,
return 0;
}
4.派生类的构造函数
class Bug{
private:
int nLegs; int nColor;
public:
int nType;
Bug(int legs,int color);
void PrintBug(){};
};
class FlyBug:public Bug{
int nWings;
public:FlyBug(int legs,int color,int wings);
};
Bug::Bug(int legs,int color){
nLegs=legs;
nColor=color;
}
FlyBug::FlyBug(int legs,int color,int wings){
nLegs=legs;//错误,基类private成员不能访问
nColor=color;//错误
nType=1;//ok
nWings=wings;
}
FlyBug::FlyBug(int legs,int color,int wings):Bug(legs,color){
nWings=wings;
}
int main(){
FlyBug fb(2,3,4);
fb.PrintBug();
fb.nType=1;
fb.nLegs=2;//error nLegs是private成员
return 0;
}
在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
调用基类构造函数的两种方式:
-
显示方式:在派生类的构造函数中,为基类的构造函数提供参数
derived::derived(arg_derived-list):base(arg_base-list)
-
隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数。
派生类的解析函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
class Base{
public:
int n;
Base(int i):n(i){
cout<<"Base"<<n<<" constructed"<<endl;
}
~Base(){
cout<<"Base"<<n<<" destructed"<<endl;
}
};
class Derived:public Base{
public:
Derived(int i):Base(i){
cout<<"Derived constructed"<<endl;
}
~Derived(){
cout<<"Derived destructed"<<endl;
}
};
int main(){
Derived Obj(3);
return 0;
}
//Base 3 constructed
//Derived constructed
//Derived destructed
//Base 3 destructed
- 包含成员对象的派生类的构造函数写法
class Bug{
private:
int nLegs; int nColor;
public:
int nType;
Bug(int legs,int color);
void PrintBug(){};
};
class Skill{
public:
Skill(int n){};
};
class FlyBug:public Bug{
int nWings;
Skill sk1,sk2;
public:
FlyBug(int legs,int color,int wings);
};
FlyBug::FlyBug(int legs,int color,int wings):Bug(legs,color),sk1(5),sk2(color),nWings(wings){}
- 封闭派生类对象的构造函数执行顺序
在创建派生类的对象时
(1)先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员。
(2)再执行成员对象类的析构函数,用以初始化派生类对象中的成员对象。
(3)最后执行派生类自己的构造函数。
在派生类对象消亡时:
(1)先执行派生类自己的析构函数
(2)再一次执行各成员对象类的析构函数
(3)最后执行基类的析构函数
析构函数的调用顺序与构造函数的调用顺序相反。
5.public继承的赋值兼容规则
class base{};
class derived:public base{};
base b;
derived d;
(1)派生类的对象可以赋值给基类对象
b=d;
(2)派生类对象可以初始化基类的引用
base & br=d;
(3)派生类对象的地址可以赋值给基类指针
base *pb=&d;
如果派生方式是private或者protected,则以上三条不行
class base{};
class derived:protected base{};
base b;
derived d;
protected继承时,基类的public成员和protected成员全部成为派生类的protected成员。
private继承时,基类的public成员成为派生类的private成员,基类的protected成员成为派生类的不可访问成员。
protected和private的继承不是“是”的关系。
- 基类与派生类的指针强制转换
- 在公有派生的情况下,派生类对象的指针可以直接赋值给基类指针
Base * ptrBase=&objDerived;
ptrBase指向的是一个Derived对象
*ptrBase可以看作是一个Base类的对象,访问它的public成员直接通过ptrBase即可,但不能通过ptrBase访问objDerived对象中属于Derived类而不属于Base类的成员。
-
即使基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类没有而派生类中有的成员。
-
通过强制指针类型转换,可以把ptrBase转换成Derived类的指针
Base *ptrBase=&objDerived;
Derived *ptrDerived=(Derived *)ptrBase;
//程序员要保证ptrBase指向的是一个Derived类的对象,否则很容易出错
#include <iostream>
using namespace std;
class Base{
protected:
int n;
public:
Base(int i):n(i){
cout<<"Base"<<n<<" constructed"<<endl;
}
~Base(){
cout<<"Base"<<n<<" destructed"<<endl;
}
void Print(){
cout<<"Base:n="<<n<<endl;
}
};
class Derived::public Base{
public:
int v;
Derived(int i):Base(i),v(2*i){
cout<<"Derived constructed"<<endl;
}
~Derived(){
cout<<"Derived detructed"<<endl;
}
void Func(){};
void Print(){
cout<<"Derived:v="<<v<<endl;
cout<<"Derived:n="<<n<<endl;
}
};
int main(){
Base objBase(5);
Derived objDerived(3);
Base * pBase=&objBase;
//pBase->Func();Base类没有Func函数
//pBase->v=5;错误.Base类没有v变量
pBase->Print();
//Derived *pDerived=&objBase;错误,只能是基类的类型,不能是派生类对象指针指向基类
Derived *pDerived=(Derived*)(&objBase);
pDerived->Print();//神勇,可能出现不可预期的错误,v不是基类的成员变量
pDerived->v=128;//往别人的空间里写入数据,会产生问题。
objDerived.Print();
return 0;
}
- 直接基类和间接基类
类A是类B的直接基类,类B是类C的直接基类,那么类A是类C的间接基类。
在声明派生类时,只需要列出它的直接基类
-
派生类沿着类的层次自动向上继承它的间接基类
-
派生类的成员包括
派生类自己定义的成员
直接基类中的所有成员
所有间接基类的全部成员
#include <iostream>
using namespace std;
class Base{
public:
int n;
Base(int i):n(i){
cout<<"Base"<<n<<"constructed"<<endl;
}
~Base(){
cout<<"Base"<<n<<"destructed"<<endl;
}
};
class Derived:public Base{
public:
Derived(int i):Base(i){
cout<<"Derived constructed"<<endl;
}
~Derived(){
cout<<"Derived destructed"<<endl;
}
};
class MoreDerived:public Derived{
public:
MoreDerived():Derived(4){
cout<<"More Derived constructed"<<endl;
}
~MoreDerived(){
cout<<"More Derived Destructed"<<endl;
}
};
int main(){
MoreDerived obj;
return 0;
}
习题:
以下说法正确的是:(C)
A)派生类对象生成时,派生类的构造函数先于基类的构造函数执行。(后于)
B)派生类对象消亡时,基类的析构函数先于派生类的析构函数执行。(后于)
C)如果基类有无参构造函数,则派生类的构造函数就可以不带初始化类表。
D)在派生类的构造函数中部可以访问基类的成员变量。(public或者protected可以访问,但是private成员还需要调用基类的构造函数才能间接访问)。