代码改变世界

C++继承和多态

2015-09-20 10:17  Fururur  阅读(713)  评论(0编辑  收藏  举报

继承

访问控制

基类的成员函数可以有public、protected、private三种访问属性。

类的继承方式有public、protected、private三种。

公有继承

当类的继承方式为public时,基类的public成员和protected成员的访问属性在派生类中不变,而基类的private成员无法直接访问。

私有继承

当类的继承方式为private时,基类的public成员和protected成员都以私有成员的身份出现在派生类中,而基类的private成员无法直接访问。

保护继承

当类的继承方式为protected时,基类的public成员和protected成员都以保护成员的身份出现在派生类中,而基类的private成员无法直接访问。

私有继承和保护继承的比较

在直接派生类中,所有成员的访问属性是完全相同的。当派生类继续派生时,会有以下区别。

  • 如果是私有继承,那么间接子类无法访问基类的所有成员。
  • 如果是保护继承,那么基类的成员在间接子类中可能是保护的或者是私有的,由第二次继承的方式决定。

类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类来替代。
规则如下:

  • 派生类的对象可以隐式转换为基类对象。a = b;
  • 派生类的对象可以初始化为基类的引用。A& a = b;
  • 派生类的指针可以隐式转换为基类的指针。A* p = &b;

代替之后,派生类的对象就可以作为基类对象来使用了,但只能使用从基类继承过来的成员。一般通过oop的多态来提现他的作用。

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

构造函数

构造派生类的对象时,就要对基类的成员对象和新增对象进行才初始化。
派生类的构造函数一般形式:

派生类名::派生类名(参数列表) :基类名1(基类名1初始化参数表),...,成员变量1(成员变量1初始化参数表)
{
    派生类构造函数的其他初始化操作;
}

派生类构造函数执行的顺序:

  1. 调用基类构造函数,调用顺序是按照他们被继承的顺序(从左向右)
  2. 对派生类新增的成员对象初始化,调用顺序按照他们在类中申明的顺序
  3. 执行派生类的构造函数体。

示例代码和运行结果:

#include<iostream>
using namespace std;
class Base1{
public:
        Base1(int i){cout<<"Constructing Base1 "<<i<<endl;}
        ~Base1(){cout<<"Destructing Base1 "<<endl;}
};
class Base2{
public:
    Base2(int j){cout<<"Constructing Base2 "<<j<<endl;}
    ~Base2(){cout<<"Destructing Base2 "<<endl;}
};
class Base3{
public:
    Base3(){cout<<"Constructing Base3* "<<endl;}
    ~Base3(){cout<<"Destructing Base3 "<<endl;}
};
class Derived:public Base2,public Base1,public Base3
{
public:
    Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b)
    { }
private:
    Base1 member1;
    Base2 member2;
    Base3 member3;
};
int main()
{
    Derived obj(1,2,3,4);
    return 0;
}
运行结果:
Constructing Base2 2
Constructing Base1 1
Constructing Base3*
Constructing Base1 3
Constructing Base2 4
Constructing Base3*
Destructing Base3
Destructing Base2
Destructing Base1
Destructing Base3
Destructing Base1
Destructing Base2

Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b){ }
Base3只有默认的构造函数,不需要传递参数,所以基类Base3和Base3的对象在派生类中的初始化列表中不必列出。注意运行的顺序!

注:有关于构造函数初始化列表的相关知识可以参见http://www.cnblogs.com/graphics/archive/2010/07/04/1770900.html

拷贝构造函数

派生类如果没有编写拷贝构造函数,系统会生成一个隐含的拷贝构造函数,这个函数会自动调用基类的拷贝构造函数,然后对派生类新增的成员对象一一复制。

如果要编写,需要为基类的拷贝构造函数传递参数。

Derived::Derived(const Derived& v):Base(V){...}

析构函数

派生类的析构函数只要负责派生类中新增的非对象成员进行清理。系统会自动调用基类及成员对象的析构函数。但执行顺序与构造函数的顺序相反。

  1. 执行析构函数函数体。
  2. 对派生类新增的类类型成员对象进行清理。
  3. 对基类继承过来的成员进行清理。

代码见构造函数执行结果。

派生类成员的标识与访问

作用域分辨符

作用域分辨符,就是“::”,他可以限定要访问的成员所在的类的名称。

类名::成员名
类名::成员名(参数表)

规则:

  1. 如果派生类中申明了与基类成员函数同名的新函数,即使函数的参数列表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。
  2. 如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名的成员,在这种情况下,派生类成员将隐藏所有基类的成员。

多继承同名隐藏实例1

class Base1{
public:
    int var;
    void fun(){cout<<"Member of Base1"<<endl;}
};
class Base2{
public:
    int var;
    void fun(){cout<<"Member of Base2"<<endl;}
};
class Derived:public Base1,public Base2{
public:
    int var;
    void fun(){cout<<"Member of Derived"<<endl;}
};
int main()
{
    Derived d;
    Derived* p = &d;
    d.var = 1;//访问Derived类的成员
    d.fun();//访问Derived类的成员
 
    d.Base1::var = 2;//作用域分辨符标识
    d.Base1::fun();//访问Base1基类的成员
 
 
    p->Base2::var = 3;
    p->Base2::fun();
    return 0;
}

如果派生类中没有申明同名的成员,那么“对象名.成员名”就无法访问到任何成员。

如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中会产生同名的现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来限定。

#include<iostream>
using namespace std;
class Base0{
public:
    int var0;
    void fun0(){cout<<"Member of Base1"<<endl;}
};
 
 
class Base1:public Base0{
public:
    int var1;
};
 
 
class Base2:public Base0{
public:
    int var2;
};
 
 
class Derived:public Base1,public Base2{
public:
    int var;
    void fun(){cout<<"Member of Derived"<<endl;}
};
 
 
int main()
{
    Derived d;
    d.Base1::var0 = 2;
    d.Base1::fun0();
 
 
    d.Base2::var0 = 3;
    d.Base2::fun0();
    return 0;
}

在这种情况下,派生类对象在内存中就同时拥有成员var0的两份同名副本。然而其实我们只需要一份就够了。此时可以引入虚基类。

虚基类

将共同基类设为虚基类,这时从不同路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射。

class Base0{
public:
    Base0(int var):var0(var){}
    int var0;
    void fun0(){cout<<"Member of Base1"<<endl;}
};
 
 
class Base1:virtual public Base0{
public:
    Base1(int var):Base0(var){}
    int var1;
};
 
 
class Base2:virtual public Base0{
public:
    Base2(int var):Base0(var){}
    int var2;
};
 
 
class Derived:public Base1,public Base2{
public:
    Derived(int var):Base0(var),Base1(var),Base2(var){}
    int var;
    void fun(){cout<<"Member of Derived"<<endl;}
};

多态

操作符 重载

牵扯的内容较多,单独成篇。

虚函数

虚函数必须是非静态的成员函数,它经过派生后,就可以实现运行过程的多态。

一般虚函数成员

虚函数的申明语法:

virtual 函数类型 函数名(形参表)

虚函数只能在类的定义中的函数原型声明中,而不能在成员函数实现的时候。

运行时多态需要满足的三个条件:

  1. 满足赋值兼容规则。
  2. 需要声明虚函数。
  3. 由成员函数来调用或者通过指针、引用来访问虚函数。

虚函数需要动态绑定,一般不声明会inline函数

在派生类中的函数满足:

  1. 与基类虚函数同名。
  2. 与基类虚函数参数个数和参数类型相同。
  3. 返回值相同。

这时,派生类的虚函数会覆盖基类的虚函数,同时,派生类的虚函数还会隐藏基类中同名函数的所有其他重载形式。

用基类指针如果要调用被覆盖的成员函数,可以用作用域来限定。

基类的构造函数和析构函数调用虚函数时,不会调用派生类的虚函数。这是由于基类在构造时或者在析构事,对象已经不再是派生类的对象了。

虚函数的默认形参值是静态绑定的,也就是说默认形参值只能来自基类。

虚析构函数

析构函数设置成虚函数后,在使用指针引用时可以动态绑定,保证基类指针可以调用适当的析构函数对不同的对象进行清理。

class Base{
public:
    ~Base();
};
Base::~Base(){
    cout<<"base destructor"<<endl;
}

class Derived:public Base
{
public:
    Derived();
    ~Derived();
private:
    int *p;
};
Derived::Derived(){
    p = new int(0);
}

Derived::~Derived(){
    cout<<"Derived Destructor"<<endl;
    delete p;
}

void fun(Base* b){
    delete b;
}

int main()
{
    Base* b = new Derived();
    fun(b);
    return 0;
}

输出为:

base destructor

说明通过基类指针删除派生类的对象时是调用基类的析构函数。派生类的析构函数没有被执行,所以派生类中的动态分配的内存没有被释放,造成内存泄露。
此时应该使用虚析构函数:

class Base{
public:
    virtual ~Base();
};

输出为:

Derived Destructor`
base destructor

纯虚函数和抽象类

纯虚函数

纯虚函数是一个在基类声明的虚函数。
virtual 函数类型 函数名(形参表) = 0

声明纯虚函数之后,基类中就可以不再给出函数的实现部分,具体的函数体由派生类给出。

细节:
基类中仍然允许对纯虚函数给出实现,即使给出,也必需由派生类覆盖,否则无法实例化。如果把析构函数声明为纯虚函数,必须要给出函体,因为派生类的析构函数在执行完后要调用基类的纯虚函数。

抽象类

带有纯虚函数的类是抽象类。

抽象类不能实例化。