继承的引出
“IS-A”关系【表示了一种继承关系】
- 相当于,苹果是一种水果,梨是一种梨;苹果是一种水果:苹果具有水果共同有的特点,同时兼有苹果所特有的特点。
“HAS-A”关系:【描述了一种聚集关系(组成关系)】
继承的基本概念
- 作为基础的类(已知的类)-- 基类,又称父类(Base class)
- 由基类经扩充修改形成的新类-- 派生类,又称为子类(Derived class)
C++的继承形式
- C++继承形式主要有单重继承、多重继承等两种
单继承的定义方法
- 单继承派生类的定义格式为:
class <派生类名> : <继承方式> <基类名>
{
<派生类新增加的数据成员>
<派生类新增加的成员函数>
};
- 继承方式,也称为派生类型,包括:
- public(公用的)
- private(私有的)
- protected(受保护的)
【此项是可选的,如果不写此项,则默认为private(私有的)】
举例:基类CRect(长方形),在此基础上通过单继承建立一个派生类CCuboid(立方体):
基类
class CRect
{
protected:
double lenght,width;
public:
CRect(double l,double w);
double getArea();
};
派生类CCuboid(立方体):
class CCuboid: public CRect //声明基类是CRect
{
public: //公有成员如下:
double getVol(); //新增加的成员函数
double getArea(); //重新定义CRect的成员函数
private: //私有成员如下:
double height; //新增加的数据成员
};
多继承的定义方法
多继承派生类的定义格式为:
class <派生类名> : <继承方式1> <基类名1>,<继承方式2> <基类名2>,…
{
<派生类新增加的数据成员>
<派生类新增加的成员函数>
};
如何构造派生类
(1) 从基类接收成员。派生类把基类成员(不包括构造函数和析构函数),毫无选择地全部接收过来。
(2) 调整从基类接收的成员。对于全盘接收过来的基类成员,程序人员可以根据需要作出某些调整。
(3) 声明派生类增加的成员。这部分内容是很重要的,它体现了派生类对基类功能的扩展。
【在声明派生类时,应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的】
继承成员的访问控制规则
- 继承成员的访问控制是由以下两个因素共同决定:
- 基类中该成员的访问控制
- 派生类定义时的继承方式
类定义中“访问控制”的含义
- private(私有):private成员(数据成员、成员函数),只能被本类中的成员函数访问;
- public(公有):public成员(数据成员、成员函数),可以被本类中及类外的函数成员(其它程序)访问。
- protected(保护):保护成员具有private与public的双重角色,对派生类的成员函数而言,它是public,而对类外函数(即全局函数)及非派生类的成员函数而言,它是private。
【简言之:protected成员可以被本类中及其派生类的函数成员访问,其它类则不能访问。】
公有继承
- 在定义一个派生类时将基类的继承方式指定为public的,称为公有继承。
class B: public A
- 采用公有继承方式时,基类中的公有成员和保护成员的访问权限在在派生类中保持不变,而基类的私有成员无论在派生类中,还是在类外都是不可访问。
保护继承
- 在定义一个派生类时将基类的继承方式指定为protected的,称为保护继承。
class B: protected A
- 在保护继承中,基类的私有成员与公有继承时相同。但基类的公用成员和保护成员在派生类中都成了保护成员。
私有继承
- 在声明一个派生类时将基类的继承方式指定为private的,称为私有继承。
class B: private A
- 私有继承中,基类的私有成员与公有继承时相同。但基类的公用成员和保护成员在派生类中则变为私有成员。
派生类的构造函数
- 派生类并不继承基类的构造函数和析构函数,所以派生类需要自己定义构造函数与析构函数;
- 派生类的构造函数必须包含对从基类继承过来的基类成员的初始化;
- 派生类的析构函数必须包含对从基类继承过来的基类成员的撤消。
- 若一个类没有定义构造函数时,C++编译会自动为该类生成一个缺省构造函数,但函数体为空。用这样的类创建对象时,其对象的状态将是不确定的。
1)派生类对象的成员组成:从基类继承过来的成员、新加入的成员
2)派生类构造函数构成:
调用基类构造函数,对继承成员进行初始化;
按常规方法对新加入的数据成员初始化;
3)若调用基类带参数的构造函数,则必须在派生类构造函数的形式参数中为基类构造函数提供实参。
可以在函数体中对派生类新增成员进行初始化
也可以在初始化列表中对继承来的基类成员以及派生类中定义的其它类的子对象初始化。
如何调用基类的无参构造函数
1)调用基类的无参构造函数: 定义派生类构造函数时,如果没有显式的调用基类的构造函数,则C++编译程序自动调用了基类中的无参构造函数。
CCuboid(){} //自动调用了CRect()
CCuboid(double h):height(h){} //自动调用了CRect()
【定义基类时,最好为它定义无参的构造函数,以免继承的时候出错。】
2)调用基类的带参构造函数:
派生类构造函数调用基类的带参数构造函数,必须在初始化列表中显式调用它,并给它提供参数:
- 若是派生类带参数的构造函数,需在自己的形参部分为基类构造函数提供参数;
- 若是派生类无参构造函数,则需为基类构造函数提供一些常量表达式作为参数。
派生类构造函数调用基类构造函数的格式
派生类名::派生类名(基类所需形参,派生类成员所需形参,对象成员形参): 基类名(基类的参数), <对象成员名>(对象成员的参数)
{
派生类成员初始化赋值语句;
}
CCuboid(double l,double w,double h):CRect(l,w),height(h){}
CCuboid():CRect(1,2),height(1){}
派生类构造函数的调用次序
派生类构造函数的调用顺序如下:
- 根据派生类定义顺序依次调用基类构造函数对基类数据成员初始化
- 对象数据成员类的构造函数(如果有对象数据成员的话)
- 派生类构造函数体内的代码
派生类的析构函数
派生类析构函数的执行顺序与派生类构造函数的调用顺序正好相反:
- 派生类析构函数
- 子对象数据成员类的析构函数(如果有子对象数据成员的话)
- 基类的析构函数
派生类对象对成员函数的调用
用公有继承的派生类对象名调用成员函数:
- 派生类的新增成员函数
调用派生类相应的成员函数
- 继承自基类而在子类中没被重写的函数
调用基类相应的成员函数
- 继承自基类而在子类被重写的函数
如果派生类中与调用函数同名的成员函数参数匹配,则该成员函数被调用,否则返回出错信息。编译程序不会再在基类中查找参数匹配的成员函数
构造函数链与析购函数链
class A{
protected:
int *a;
public:
A(int _a){a=new int(_a);cout<<"A's constructor."<<endl;}
void print(){cout<<*a<<endl;}
~A(){delete a;cout<<"A's distructor."<<endl;}
};
class B:public A{
protected:
int *b;
public:
B(int _a,int _b):A(_a){b=new int(_b);cout<<"B's constructor."<<endl;}
void print(){cout<<*a<<" "<<*b<<endl;}
~B(){delete b;cout<<"B's distructor."<<endl;}
};
class C:public B{
protected:
int *c;
public:
C(int _a,int _b,int _c):B(_a,_b){c=new int(_c);cout<<"C's constructor."<<endl;}
void print(){cout<<*a<<" "<<*b<<" "<<*c<<endl;}
~C(){delete c;cout<<"C's distructor."<<endl;}
};
int main()
{
C c(1,2,3);
c.A::print();
c.B::print();
c.print();
return 0;
}
赋值兼容原则
- 两个不同类的类对象一般是不能互相赋值的,但两个具有公有继承关系的对象可赋值。
- 在继承层次结构中,赋值兼容规则是指在公有派生的条件下,任何使用基类对象的地方都可以用其派生类的对象替代。反之不成立。
#include <iostream>
#include <string>
#include <iomanip>
using namespace std;
#include <cstdio>
#include <cstring>
class CPoint
{
protected:
double x,y;
public:
CPoint(double _x=0,double _y=0):x(_x),y(_y){}
void print()
{
cout<<"I am point"<<endl;
}
};
class CCircle:public CPoint
{
protected:
double r;
public:
CCircle(double _r=1,double _x=0,double _y=0)
:CPoint(_x,_y),r(_r){}
void print()
{
cout<<"I am Circle"<<endl;
}
};
class CCylinder:public CCircle
{
double h;
public:
CCylinder(double _x=0,double _y=0,double _r=1,double _h=1)
:CCircle(_r,_x,_y),h(_h){}
void print()
{
cout<<"I am Cylinder"<<endl;
}
};
int main()
{
CPoint p(1,2),*q;
CCircle c(1,2,3);
CCylinder cy(1,2,3,4);
p=c;
p.print();
p=cy;
p.print();
q=&c;
q->print();
return 1;
}
基类对象指针与派生类对象指针的关系
- 两个不同类类对象的指针一般不能互相赋值。
- 若指针指向的两个对象具有公共继承关系,则:
- 派生类对象指针(或引用)可以赋值给基类对象指针(或引用),反之则不然。此时被赋值的基类对象指针只能访问基类的公有成员,而不能访问派生类中新增的成员。
- 可用类型转换运算符将基类指针显式转换为指向派生类的指针来访问派生类的公有成员
多态性
- 在程序中同一符号或名字在不同情况下具有不同解释的现象称为多态性。
- 同一个类中,对应相同的函数名,却执行不同的函数体,即函数重载,属于编译时的多态。
- 派生类中,与基类同名、同参数、同返回类型的函数的不同行为,属于运行时的多态。
- 运算符重载
编译时多态
- 指在编译阶段即可确定下来的多态,主要通过重载机制获得
class CA{
int a;
public:
CA(int _a=1):a(_a){}
int add(int _a);
int add(A &r_a);
int add(double _a);
};
int main()
{
CA a1(1),a2(2);
a1.add(3);
a1.add(a2);
return 1;
}
编译时多态与静态联编
- 联编(binding),也译为绑定,指将程序中出现的标识符与一个存储地址相联系的过程。
- 静态联编,是指这种绑定关系在编译阶段完成,即在编译阶段就必须确定标识符(函数名)与代码之间的对应关系。由于联编过程是在程序运行前完成的,所以又称为早期联编。
- 静态联编能够实现编译时多态。
运行时多态性
- 指必须等到程序运行时才可确定的多态性,主要是通过继承结合动态绑定获得。
运行时多态与动态联编
- 动态联编是指根据目标对象的动态类型(而不是静态类型),在程序运行时(而不是在编译阶段)将函数名绑定到具体的函数实现上
- 动态联编,要在程序运行时动态进行的,所以又称为晚期联编。
- 动态联编可以实现运行时多态。
- 运行时多态性是通过使用虚函数(virtual function)实现的。
如何实现动态联编
- 只有采用指向基类对象的指针或引用来调用虚函数时,才会按动态联编的方式来调用。
- 用普通对象来调用虚函数不能实现动态联编。
- 基类中的虚函数必须具有public或protected访问权限,且派生类必须以公有继承方式从基类派生
class CPoint
{
protected:
double x,y;
public:
CPoint(double x=0,double y=0);
virtual void print(); //step 1,将需要多态的函数设置为虚函数
};
class CCircle:public CPoint
{
protected:
double r;
public:
CCircle(double _r=1,double _x=0,double _y=0);
void printCircle();
void print(){cout<<“CCircle”<<endl;} // 重定义
};
class CCylinder:public CCircle
{
double h;
public:
CCylinder(double _x=0,double _y=0,double _r=1,double _h=1);
void print(){ cout<<“CCylinder”<<endl; } //重定义
};
void printShape(CPoint *point) //step 2:使用指针或引用调用虚函数
{
point->print();
}
class Animal
{public:
void virtual cry()
{cout<<"I am animal"<<endl;}
};
class Dog:public Animal
{
public:
void cry()
{cout<<"I am a dog.汪汪," <<endl;}
};
class Cat:public Animal
{
public:
void cry()
{cout<<"I am a cat ,喵喵" <<endl;}
};
int main()
{
Animal animal;
Dog dog;
Cat cat;
animal=dog; animal.cry();
animal=cat; animal.cry();
Animal *p_animal;
p_animal=&dog; p_animal->cry();
p_animal=&cat; p_animal->cry();
Animal &r_dog=dog; r_dog.cry();
Animal &r_cat=cat; r_cat.cry();
return 0;
}
//输出:
//I am animal
//I am animal
//I am a dog.汪汪,
//I am a cat ,喵喵
//I am a dog.汪汪,
//I am a cat ,喵喵
注意:
- 静态成员函数、内联函数、友元函数和构造函数都不能说明为虚函数。但析构函数可以是虚函数
- 如果基类的析构函数为虚析构函数,则派生类的析构函数也是虚析构函数。
- 在基类设置虚析构函数,目的是在使用delete释放一个基类指针指向的派生类对象时采用动态联编的方式选择正确的派生类析构函数。
- 在程序中最好将析构函数声明为虚函数。
虚析构函数
class Animal
{public:
Animal()
{cout<<"Constructing Animal"<<endl;}
void virtual cry()
{cout<<"I am animal"<<endl;}
virtual~Animal()
{cout<<"Destructing Animal"<<endl;}
};
class Dog:public Animal
{public:
Dog()
{cout<<"Constructing Dog"<<endl;}
void cry()
{cout<<"I am a dog.汪汪," <<endl;}
~Dog()
{cout<<"Destructing Dog"<<endl;}
};
int main()
{
Animal *a=new Dog;
a->cry();
delete a;
}
- A* a=new B;
- delete a;
静态联编:
- delete a 释放的是a的类型
动态联编:
- delete a 释放的是a实际指向的对象
在哪些情况下考虑使用虚函数?
- 首先,成员函数所在的类是否是基类,其次,该成员函数在类的继承后是否将被改动,如果希望改变其功能,一般将它声明为虚函数。
- 考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
纯虚函数和抽象类
- 为了实现接口重用,必须以虚函数的形式在基类中为其派生类定义一个接口。
- 但基类虚成员函数有时无法具体实现。
- 对于这样一些物理上无法实现而逻辑上又不得不存在的抽象的虚函数,可以将其在基类中用不包括任何代码的纯虚函数来定义。
- 而其具体的实现则可在派生类中完成
纯虚函数:
- 在基类中没有具体实现的虚函数。
如果基类中包括有纯虚函数,那么在任何派生类中都必须重定义该函数,因为它们不能直接使用从基类继承下来的虚函数。
抽象类例子:
class Animal //抽象类
{public:
void virtual cry()=0; //纯虚函数
};
class Dog:public Animal
{public:
void cry()
{cout<<"I am a dog.汪汪," <<endl;}
};
class Cat:public Animal
{public:
void cry()
{cout<<"I am a cat ,喵喵" <<endl;}
};
int main()
{ Animal animal; //error
Animal *p_animal; //ok
Dog dog;
Cat cat;
p_animal=&dog;
p_animal->cry();
p_animal=&cat;
p_animal->cry();
Animal &animal=dog; //ok
animal.cry();
}
多重继承
多重继承的派生类定义的一般格式为:
class <派生类名> :<继承方式1> <基类名1>, <继承方式2> <基类名2>, ……
{
<派生类新增的数据成员和成员函数>
};
多重继承派生类的构造函数
(1)功能:
- 对继承成员初始化,对新加成员初始化
(2)定义形式(类似单重继承)
- 当没有显式指明调用基类的哪个构造函数时,系统自动调用基类的缺省构造函数;
- 当调用基类的带参数构造函数时,必须显式指明;并在派生类构造函数的形式参数中为被调用的基类构造函数提供实参;
多重继承派生类构造函数的一般定义格式为:
派生类名::派生类名(基类1形参,基类2形参,… 派生类形参):基类名1(参数), 基类名2(参数), ...基类名n(参数)
{
派生类成员初始化赋值语句;
};
(3)执行
- 先执行基类的构造函数,再调用派生类构造函数中新加入部分;
- 当有多个基类构造函数要执行时,按照派生类定义时基类出现的次序(从左到右)执行;(而不是派生类构造函数定义时基类构造函数出现的次序)
多重继承派生类的析构函数
(1)功能:撤消派生类对象所占用的空间
(2)定义形式:由于析构函数都不带参数,故不必显式指明如何调用基类的析购函数。
(3)执行次序:与构造函数相反
举例:
class A{
protected: int a;
public:
A(int _a=0):a(_a){cout<<"A's constructor-a="<<a<<endl;}
~A(){cout<<"A's distructor-a="<<a<<endl;}
};
class B{
protected: int b;
public:
B(int _b=0):b(_b){cout<<"B's constructor-b="<<b<<endl;}
~B(){cout<<"B's distructor-b="<<b<<endl;}
};
class C: public A,public B{ //定义的顺序
int c;
public:
C(int _c=0):c(_c)
{cout<<"C's constructor-a="<<a<<" b="<<b<<" c="<<c<<endl;}
C(int _a,int _b,int _c):B(_b),A(_a), c(_c) //初始化的顺序
{cout<<"C's constructor-a="<<a<<" b="<<b<<" c="<<c<<endl;}
~C(){cout<<"C's distructor-a="<<a<<" b="<<b<<" c="<<c<<endl;}
};
int main()
{
C c;
C c1(1,2,3);
return 1;
}
多重继承的二义性问题
重复继承
重复继承的二义性
虚基类
- 为了解决多继承时可能发生的对同一基类继承多次而产生的二义性问题,使某个公共基类的成员在其派生类中只产生一个拷贝,可在从基类派生新的类时将这个基类用virtual关键字说明为虚基类
class BASE1 : virtual public BASE
- 虚基类作用--如某个基类被声明为虚基类时,那么在被重复继承时,在派生类对象实例中只存储一个副本(若不声明为虚基类,就会出现多个副本)。
重复继承的构造函数应如何实现
- 使用参数初始化Cperson部分;
- 构造CStudent部分,忽略CStdent用于Cperson构造的部分
- 构造CSTeacher部分,忽略CSTeacher用于Cperson构造的部分
- 构造CStudentOnJob部分
【虚继承,让孙子辈只保留了一份,初始化时,记得调用爷爷辈的构造函数初始化虚继承的变量】
带有虚基类的派生类的构造函数
- 先执行虚基类的构造函数,再执行不是虚基类的基类的构造函数,最后执行构造函数中新加入部分;
- 若有多个虚基类时,依派生类定义时,虚基类出现次序从左至右地执行;
- 当有多个非虚基类时,也依派生类定义时,基类出现次序,从左至右地执行;
举例
class CPerson
{
protected:
string name;
int age;
public:
CPerson() {}
CPerson(string _name,int _age):name(_name),age(_age)
{ cout<<"CPerson Constructor!"<<endl; }
~CPerson()
{ cout<<"CPerson Destructor!"<<endl; }
};
class CStudent : virtual public CPerson
{
protected:
int no; //学号
public:
CStudent(string _name,int _age, int _no):CPerson(_name,_age)
{
no = _no;
cout<<"CStudent Constructor 1!"<<endl;
}
CStudent(int _no) //调用基类CPerson的默认构造函数
{
no = _no;
cout<<"CStudent Constructor 2!"<<endl;
}
~CStudent(){ cout<<"CStudent Destructor!"<<endl; }
};
class CTeacher : virtual public CPerson
{
protected:
string title; //职称
public:
CTeacher(string _name,int _age, string
_title):CPerson(_name,_age)
{
title = _title;
cout<<"CTeacher Constructor 1!"<<endl;
}
CTeacher(string _title) //调用基类CPerson的默认构造函数
{
title = _title;
cout<<"CTeacher Constructor 2!"<<endl;
}
~CTeacher() { cout<<"CTeacher Destructor!"<<endl; }
};
class CStudentOnJob : public CStudent,public CTeacher
{
private:
string research; //研究方向
public:
CStudentOnJob(string _name, int _age, int _no, string _title, string
_r):CPerson(_name,_age),CStudent(_no),CTeacher(_title)
{
research = _r;
cout<<"CStudentOnJob Constructor!"<<endl;
}
void print()
{
cout<<name<<" "<<age<<" "<<no<<" "<<title<<" "<< research<<endl;
}
~CStudentOnJob()
{
cout<<"CStudentOnJob Destructor!"<<endl;
}
};
int main()
{
CStudentOnJob s("liming",26,0001,"lecturer","computer");
s.print();
}