C++基础之继承

0
10

一、类的继承与类的派生

  • 继承和派生是人们认识客观世界的过程。在程序设计方法中,人们追求代码复用(这是提高软件开发效率的重要手段),将继承和派生用于程序设计方法中,从而有了面向对象程序设计的重要特点。C++对代码复用有很强的支持,
  • “继承”就是支持代码复用的机制之一。
  • 通过已有的类建立新类的过程,叫作类的派生。原来的类称为基类,也称为父类或一般类;新类称为派生类,也称为子类或特殊类。派生类派生自基类,或继承于基类,也可以说基类派生了派生类。派生机制是C++语言及面向对象程序设计方法的重要特征之一。派生类可以再作为基类派生新的派生类,由此基类和派生类的集合称作类继承层次结构。

继承:

  • 使用基类派生新类时,除构造函数和析构函数外,基类的所有成员自动成为派生类的成员,包括基类的成员变量和成员函数。同时,派生类可以增加基类中没有的成员,这同样是指成员变量和成员函数。可以重新定义或修改基类中已有的成员,包括可以改变基类中成员的访问权限。当然派生类需要定义自己的构造函数和析构函数。
  • 使用基类成员是一个重用的过程,在基类之上进行调整,不论是添加新成员还是改造己有的,都是扩充的过程。

覆盖:

  若派生类中定义了一个与基类中同名的成员,则会出现基类与派生类有同名成员的情况,这是允许的。同名的成员既可以是成员变量,也可以是成员函数。这种情况下,若在派生类的成员函数中访问这个同名成员,或通过派生类对象访问这个同名成员时,除非特别指明,访问的就是派生类中的成员,这种情况叫“覆盖”,即派生类的成员覆盖基类的同名成员。覆盖也称为重定义或是重写。对于成员函数来说,派生类既继承了基类的同名成员函数,又在派生类中重写了这个成员函数。这称为函数重定义,也称为同名隐藏。“隐藏”的意思是指,使用派生类对象调用这个名字的成员函数时,调用的是派生类中定义的成员函数,即隐藏了基类中的成员函数。

派生类可以改变基类中成员的访问权限
空类也可以作为基类,也就是说,空类可以派生子类:

class emptyClass{ }; //空基类
class subemptyClass : public emptyClass{ }; //派生类

类的大小

  • 派生类对象中包含基类成员变量,而且基类成员变量的存储位置位于派生类对象新增的成员变量之前。派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小。
  • 对象占用的存储空间包含对象中各成员变量占用的存储空间。出于计算机内部处理效率的考虑,为变量分配内存时,会根据其对应的数据类型,在存储空间内对变量的起始地址进行边界对齐。可以使用sizeof( )函数计算对象占用的字节数。
  • 对象的大小与普通成员变量有关,与成员函数和类中的静态成员变量无关,即普通成员函数、静态成员函数、静态成员变量、静态常量成员变量等均对类对象的大小没有影响。

二、继承关系的特殊性

1、友元

如果基类有友元类或友元函数,则其派生类不会因继承关系而也有此友元类或友元函数。如果基类是某类的友元,则这种友元关系是被继承的。即被派生类继承过来的成员函数,如果原来是某类的友元函数,那么它作为派生类的成员函数仍然是某类的友元函数。总之,基类的友元不一定是派生类的友元;基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数。

#include<iostream>
using namespace std;
class another; //前向引用声明
//基类
class Base {
    private:
      float x;
    public:
      void print(const another &K);
};
//派生类
class Derived:public Base {
    private:
        float y;
};
 //其他类
class another{private:
    int aaa;
public:
    another(){
        aaa=100;
    }
friend void Base::print(const another &K);//基类的成员函数声明为本类的友元
};
void Base::print(const another &K){
    cout<<"Base:"<<K.aaa<<endl; //可以访问私有成员变量
}
int main(){
    Base a;
    Derived d;
    another ano; //aaa 初始化为100
    a.print(ano); //输出为:Base:100
    d.print(ano); //输出为:Base:100
    return 0;
}

2、静态属性

如果基类中的成员是静态的,则在其派生类中,被继承的成员也是静态的,即其静态属性随静态成员被继承。
如果基类的静态成员是公有的或是保护的,则它们被其派生类继承为派生类的静态成员。访问这些成员时,通常用“<类名>::<成员名>”的方式引用或调用。无论有多少个对象被创建,这些成员都只有一个拷贝,它为基类和派生类的所有对象所共享。

#include<iostream>
using namespace std;
// 基类
class Base {
    private:
        float x;
    public:
        static int staV;
    Base(){ 
        staV++;
    }
};
int Base::staV=0;
//派生类
class Derived: public Base {
    private:
        float y;
    public:
        Derived( ){ 
            staV++;
        }
};
int main(){
    Base a;
    cout<< "Base:" <<a.staV<<endl; //输出1
    Derived d;
    cout<< "Derived:" << d.staV<<endl; //输出3(创建子类对象会调用父类构造器,然后调用自身的构造器)
    return 0;
}

3、继承之间的访问关系

  • 派生类和基类中都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中的公有成员变量,但不能直接访问基类中的私有成员变量。也就是说,不能在派生类的函数中,使用“基类对象名.基类私有成员函数(实参)”,或是“基类对象名.基类私有成员变量”,或是“基类名::基类私有成员”的形式访问基类中的私有成员。
  • 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层,基类在外层。如果派生类声明了一个和基类某个成员同名的新成员,派生的新成员就隐藏了外层同名成员,直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用基类名和作用域分辨符来限定。
#include<iostream>
using namespace std;
class CB{
    public:
        int a;
        CB(int x){
            a=x;
        }
        void showa(){
            cout<<"Class CB--a="<<a<<endl;
        }
};
class CD:public CB{
    public:
        int a; //与基类a同名
        //x用来初始化基类的成员变量a
        CD(int x,int y):CB(x) { 
            a=y; 
        }
        //与基类showa同名        
        void showa() {
            cout<<"Class CD--a="<<a<<endl; 
        }
        //访问派生类a            
        void print2a(){ 
            cout<<"a=" << a<<endl; 
            //访问基类a
            cout<<"CB::a="<<CB::a<<endl;
        }
};
int main(){
    CB CBobj(12);
    CBobj.showa();//Class CB--a=12
    CD CDobj(48,999);
    CDobj.showa(); //访问派生类的showa ()  Class CD--a=999
    CDobj.CB::showa(); //访问基类的showa ()  Class CB--a=48
    cout<<"CDobj.a="<<CDobj.a<<endl; // CDobj.a=999
    cout<<"CDobj.CB::a="<<CDobj.CB::a<<endl;//CDobj.CB::a=48
}

4、protected访问范围说明符

  • 定义类时,类成员可以使用protected访问范围说明符进行修饰,从而成为“保护成员”。保护成员的访问范围比私有成员的访问范围大,能访问私有成员的地方都能访问保护成员。此外,基类中的保护成员可以在派生类的成员函数中被访问。
  • 在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。将基类中成员变量的访问方式修改为protected后,在派生类中可以直接访问。

三、多重继承

  • C++允许从多个类派生一个类,即一个派生类可以同时有多个基类。这称为多重继承。相应地,从一个基类派生一个派生类的情况,称为单继承或单重继承
  • 派生类继承了基类名1、基类名2、……、基类名n的所有成员变量和成员函数,各基类名前面的继承方式说明符用于限制派生类中的成员对该基类名中成员的访问权限,其规则与单继承情况一样。
  • 多重继承情况下如果多个基类间成员重名时,按如下方式进行处理:对派生类而言,不加类名限定时默认访问的是派生类的成员;而要访问基类重名成员时,要通过类名加以限定。
#include<iostream>
using namespace std;
class CB1{
    public:
        int a; 
        CB1 (int x){ a=x; }
        void showa(){ cout<<"ClassCB1==>a="<<a<<endl; }
};
class CB2{
    public:
        int a; 
        CB2 (int x){ a=x; }
        void showa(){ cout<<"ClassCB1==>a="<<a<<endl; }
};
//多重继承,两个基类
class CD:public CB1,public CB2 {
public:
    int a; //与两个基类成员变量a重名
    CD(int x,int y,int z):CB1(x) ,CB2(y){ a=z; }
    //与两个基类成员函数showa()重名
    void showa() { 
        cout<<"Class CD==>a="<<a<<endl; 
    }
    void print3a(){ 
        cout<<"a="<<a<<endl;
        cout<<"CB1::a="<<CB1::a<<endl;
        cout<<"CB2::a="<<CB2::a<<endl;
    }
};
int main(){
    CB1 CB1obj(11);
    CB1obj.showa();//ClassCB1==>a=11
    CD CDobj(101,202,909);
    CDobj.showa(); //调用派生类的showa()  //Class CD==>a=909
    CDobj.CB1::showa(); //调用基类的showa() //ClassCB1==>a=101
    cout<<"CDobj.a="<<CDobj.a<<endl;//访问派生类成员a  //CDobj.a=909
    cout<<"CDobj.CB2::a="<<CDobj.CB2::a<<endl; //CDobj.CB2::a=202
}

2、多重继承的二义性

⚫ 如果派生类中新增了同名成员,则派生类成员将隐藏所有基类的同名成员。使用“派生类对象名.成员名”或“派生类对象指针->成员名”的方式可以唯一标识和访问派生类新增成员。这种情况下,不会产生二义性。
⚫ 如果派生类中没有新增同名成员,当满足访问权限时,使用“派生类对象名.成员名”或“派生类对象指针->成员名”方式时,系统无法判断到底是调用哪个基类的成员,从而产生二义性。为了避免二义性,必须通过基类名和作用域分辨符来标识成员。
⚫ 当要访问派生类对象中的某个变量时,添加“基类::”作为前缀,指明需要访问从哪个基类继承来的,从而可以排除二义性。

3、继承权限控制

设计继承类时,需要使用继承方式说明符指明派生类的继承方式。继承方式说明符可以是public(公有继承)、private(私有继承)或protected(保护继承)

四、类型兼容

⚫类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,也称为赋值兼容规则。在公有派生的情况下,有以下3条类型兼容规则。

  • 1)派生类的对象可以赋值给基类对象。
  • 2)派生类对象可以用来初始化基类引用。
  • 3)派生类对象的地址可以赋值给基类指针,即派生类的指针可以赋值给基类的指针。

⚫上述3条规则反过来是不成立的。例如,不能把基类对象赋值给派生类对象。在进行替代之后,派生类对象就可以作为基类的对象使用了,但只能使用从基类继承的成员。

⚫如果类B为基类,类D为类B的公有派生类,则类D中包含了基类B中除构造函数、析构函数之外的所有成员。这时,根据类型兼容规则,在基类B的对象可以出现的任何地方,都可以用派生类D的对象来替代。假设有以下的声明:

class B{…}
class D : public B{…}
B b1, *pb1;
D d1;

⚫这时,派生类对象可以隐含转换为基类对象,即用派生类对象中从基类继承来的成员变量的值,逐个为基类对象的成员变量的值进行赋值。b1=d1;
⚫派生类的对象也可以用来初始化基类对象的引用,即:B &rb=d1;
⚫派生类对象的地址可以隐含转换为指向基类的指针,即派生类对象的地址赋给基类指针:pb1=&d1;
⚫由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理。因为当函数的形参为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),从而没有必要为每一个类设计单独的模块,大大提高了程序的效率。

五、私有继承

六、保护继承

  保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。这样,派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法直接访问它们。

class A{};
class B:private A{};//私有继承
class C:protected A{};//保护继承

七、派生类的构造函数和析构函数

  • 派生类并不继承基类的构造函数,所以需要在派生类的构造函数中调用基类的构造函数,以完成对从基类继承的成员变量的初始化工作。具体来说,派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类成员变量。
  • 在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。
定义派生类构造函数的一般格式如下:
派生类名::派生类名(参数表):基类名1(基类1 初始化参数表),…,基类名m(基类m初始化参数表),成员对象名1(成员对象1 初始化参数表),…,成员对象名n(成员对象n 初始化参数表){
    类构造函数函数体 //其他初始化操作
}

派生类构造函数执行的一般次序如下:

  • 1)调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
  • 2)对派生类新增的成员变量初始化,调用顺序按照它们在类中声明的顺序。
  • 3)执行派生类的构造函数体中的内容。
  • 构造函数初始化列表中基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的,无论它们的顺序怎样安排,基类构造函数的调用和各个成员变量的初始化顺序都是确定的。
#include<iostream>
using namespace std;
//基类
class BaseClass {
    protected:
        int v1,v2;
    public:
    BaseClass();
    BaseClass(int,int);
    ~BaseClass();
};
BaseClass::BaseClass(){ 
    cout<<"BaseClass 无参构造函数"<<endl;
}
BaseClass::BaseClass(int m,int n){ 
    v1=m;
    v2=n;
    cout<<"BaseClass 2个参数构造函数"<<endl;
}
BaseClass::~BaseClass(){ 
    cout<<"BaseClass析构函数"<<endl; 
}
//公有继承的派生类
class DerivedClass:public BaseClass { 
    int v3;
    public:
        DerivedClass();
        DerivedClass(int);
        DerivedClass(int,int,int);
        ~DerivedClass();
};
DerivedClass::DerivedClass(){
    cout<<"DerivedClass无参构造函数"<<endl;
}
DerivedClass::DerivedClass(int k):v3(k){
    cout<<"DerivedClass 带1个参数构造函数"<<endl;
}
DerivedClass::DerivedClass(int m,int n,int k):BaseClass(m,n),v3(k){
    cout<<"DerivedClass 带3个参数构造函数"<<endl;
}
DerivedClass::~DerivedClass(){
    cout<<"DerivedClass析构函数"<<endl;
}
int main(){
    cout<<"无参对象的创建"<<endl;
    BaseClass baseCla; //基类对象
    DerivedClass derivedCla; //派生类对象
    return 0;
}
无参对象的创建
BaseClass 无参构造函数
BaseClass 无参构造函数
DerivedClass无参构造函数
DerivedClass析构函数
BaseClass析构函数
BaseClass析构函数

八、复制构造函数

  • 对于一个类,如果程序中没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象执行复制。
  • 如果要为派生类编写复制构造函数,一般也需要为基类相应的复制构造函数传递参数,但并不是必须的。
#include<iostream>
using namespace std;
class CBase{
public:
    CBase(){}
    CBase(CBase & c){
        cout<<"CBase::复制构造函数"<<endl;
    }
    CBase & operator=(const CBase &b){
        cout<<"CBase::operator="<<endl;
        return *this;
    }
};
class CDerived:public CBase{
    public:
        CDerived(){
            cout<<"CDerived::复制构造函数"<<endl;
        }
};
int main(){
    CDerived d1,d2;
    CDerived d3(d1);
    //d3初始化过程中会调用类CBase的复制构造函数
    d2=d1; //会调用类CBase重载的"="运算符
    return 0;
}

九、派生类的构造函数和析构函数

派生类并不继承基类的构造函数,所以需要在派生类的构造函数中调用基类的构造函数,以完成对从基类继承的成员变量的初始化工作。具体来说,派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类成员变量。
在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。

<

发布回复

请输入评论!
请输入你的名字