(三)继承与多态

继承,Inheritance

基类和派生类内涵相似,派生类作为基类的特殊对象。如雇员-经理。
保持已有类的特性而构造新类的过程称为继承。
在已有类的基础上新增自己的特性产生新类的过程称为派生。
为了实现代码重用,当新的问题出现,而对原有程序进行改造。
定义继承对象时,需要特别声明:

class BC{
}
class DC:public BC
{

}

派生类的构造函数和析构函数不得继承。
派生类也可以被基类指针所指向。
但只能访问基类的对象和方法。

DC *p;
p=&BC;

基类成员在派生类中的外部能见度:

基类成员在派生类中的外部能见度

名称隐藏

如果派生类中添加了成员与基类的成员重名,则基类成员将被隐藏。
不过依然可以访问:

d1.h();//派生类
d1::BC.h();//基类

调整可访问性

分为在public中用using语句和在private中。

class BC {
public: 
    void setX(float a) { x = a;};
private:
    float x;
};
class  DC : public BC{
public: 
    void setY(float b) { y = a;};
private:
    float y;
    using BC::sertX; 
};
int main(){
    DC d;
    d.setY(1.7);   // ok
    d.setX(2.3);  //Error, SetX is private in DC
    /*
    不过d.BC::setX()可以访问,不知道为啥??
    */
}
class Base {
public:
    void foo(int){};
};
class Derived1 : public Base {
public:
    void foo(const char*){};
};
class Derived2 : public Base {
public:
    using Base::foo;
    void foo(const char*){};
};
int main() {
    Derived1 d1;
    d1.foo("abc");
    d1.foo(42);  // error
    d1.Base::foo(42); // ok

    Derived2 d2;
    d2.foo("abc");
    d2.foo(42);  // OK
}

受保护成员

受保护成员只在自己的类和派生类中是可见的,如以下代码,有错误。

#include<iostream>
using namespace std;
class BC{
public:
    void set_x(int a)
    {x=a;}
protected:
    int get_x() const
    {
        return x;
    }
private:
    int x;
};
int main()
{
    BC b;
    b.get_x();//在主函数中,是类外代码访问,不可见。
}

class BC {
  protected:
    int get_w( ) const;
  //……
};
class DC : public BC {
public:
  int get_val( ) const { return get_w( ); }
  void base_w( const BC& b ) const {
     cout << b.get_w( ) << endl;
  }
};

继承方式不会影响基类成员在派生类中的能见度。

继承下的构造函数

派生类的构造函数可以用初始化列表。

class BC{
public:
BC(){x=y=1;}
private:
int x,y;
};
class DC:public BC{
public:
DC():BC(),S("派生类"),{}
//BC(),再执行S(),再执行DC()
private:
string S;
}

先执行基类构造,再初始化链表,再执行派生类。
也可以改成:

DC():S(""),BC(){}

基类和子类构造函数形式下的关系:
在这里插入图片描述
总结:子类构造函数尽量指出当基类有多个构造函数时,调用哪个。
唯一一个不允许的是,子类无构造函数,而基类有构造函数却无缺省构造。这样编译器不会给出缺省构造,编译错误。

继承下的析构函数

基类构造函数先执行,派生类构造函数后执行;
派生类析构函数先执行,基类析构函数后执行。

虚继承

问题的提出:

class BC0 {
    public:   int K;
};
class BC1 : public BC0 {
    public:  int x;
};
class BC2 : public BC0 {
     public:   int x;
};
class  DC : public BC1, public BC2{
};
/*
在DC类中。两个同名成员K,两个x,怎么区分?
*/

虚继承使得数据只被继承一份。

class BC0{
public: int K;
};
class BC1:virtual public BC0{
public: int x;
};
class BC2:virtual public BC0{
public: int x;
class DC:public BC1,public BC2{
public: inr x;
};

多态

当通过指针或引用访问派生类一个对象时,这个休想可以被看做一个基类对象。

虚函数

虚函数(Virtual functions)允许程序员在基类中声明一个函数,然后在各个派生类中对其进行重定义(redefined). 编译器与装入器将保证对象及其所调用函数的正确对应。
这里的 redefine 叫做 Overriding(覆盖,又称重置)。
这是C++语言中唯一的一种动态绑定(Dynamic binding)机制。
静态绑定,绑定工作在编译连接阶段完成。
因为绑定过程是在程序开始执行之前进行的,因此也称为早期绑定或前绑定。
在编译、连接过程中,系统就可以根据类型匹配等特征确定程序中操作调用与执行该操作代码的关系,即确定了某一个同名标识到底是要调用哪一段程序代码。
C++中,除虚函数外,其他函数均是静态绑定的。
重载函数是静态绑定。
动态绑定:绑定工作在程序运行阶段完成的。

class A {
public:
 	virtual void Get( );
};
class B : public A{ 
public:
	virtual void Get( );  
};

void MyFunction( A * pa ){
	pa->Get( );
}
pa->Get( ) 调用A::Get( ),还是B::Get( )?编译时无法确定,因为不知道MyFunction被调用时,形参会对应于一个 A 对象还是B对象。
所以只能等程序运行到 pa->Get( )了,才能决定到底调用哪个Get( )。
动态联编的实现需要如下3个条件:
(1)调用的函数是虚函数;
(2)调用虚函数操作的是指向对象指针或者对象引用;或者是由成员函数调用虚函数;
(3)派生类型关系的建立。

虚函数关键字 virtual。
虚函数必须是非静态的成员函数。
在这个类的派生类中,可以重新定义这个虚拟函数的实现代码,也可以不重新定义(继承上一层的虚拟函数的实现代码)。
这个虚拟函数的接口不能在重新定义时改变。

注意:多态针对的是指针和引用。实体对象的定义不适合规律。
class TradesPerson {
  public:
    virtual void sayHi() { cout << "Just hi." << endl; }
    void run() { cout << "Base::run " << endl; }
};
class Tinker : public TradesPerson {
  public:
    virtual void sayHi() { cout << "Tinker." << endl;}
    void run() { cout << "Tinker::run " << endl; }
};
class Tailor : public TradesPerson {
  public:
    void sayHi() { cout << "Tailor." << endl; }
    void run() { cout << "Tailor::run " << endl; }
};
int main( ) {
  TradesPerson* p;
  int which ;
  cout << "1 == TradesPerson, 2 == Tinker, 3 == Tailor ";
    cin >> which;
  switch( which ){
    case 1: p = new TradesPerson; break;
    case 2: p = new Tinker; break;
    case 3: p = new Tailor; break;
  }
  p->sayHi();    p->run();    delete p;
  return 0;
}
这个代码无论如何run()函数的答案都是Base::run.
因为根据多态的概念,这是用基类指针所定义的。所以,对于不是虚函数的函数,只认基类的资料。

虚表:每个多态类有一个虚表(virtual table)
虚表中有当前类的各个虚函数的入口地址
每个对象有一个指向当前类的虚表的指针(虚指针vptr)

在这里插入图片描述
对虚函数的限制:
静态函数成员不能是虚函数。
类的构造函数不能是虚函数。
当类的虚函数在类外定义时,不能有virtual关键字。换句话说,关键字只出现在声明中。
析构函数通常定义为虚函数。

析构函数为什么通常为虚函数

当用基类指针定义派生类对象时,析构函数要写成虚函数。

#include<iostream>
using namespace std;
#include <iostream>
using namespace std;
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 Constructor
而期望的结果是:
Derived Constructor
Base Constructor
不管析构函数是不是虚函数,将析构函数设为虚函数是希望在通过基类指针删除派生类对象时调用派生类的析构函数。

如果析构函数不是虚函数,派生类对象没有Vptr,编译器会调用基类的析构函数(在编译时就确定了)。

  这样,用户在派生类析构函数中填写的代码就不会被调用,派生类成员对象的析构函数也不会被调用。不过,派生类对象空间还是会被正确释放的,堆管理程序知道对象分配了多少空间。
#include<iostream>
using namespace std;
class bc{
public:

    virtual void tell(){cout<<1<<endl;}
    virtual void setf(){cout<<2<<endl;}
};
class dc:public bc{
public:
    void tell(){cout<<3<<endl;}
    void seth(){cout<<4<<endl;}
};
int main()
{
    bc *p;
    p=new dc;
    p->tell();
    p->setf();
}
当签名相同的函数同时出现在bc和dc中,如果不加virtual,基类指针只能访问基类函数;若加,则内容被dc里改写。

名字重载:

class C {
  public:
    C( ) {}         // default constructor
    C( int x ) {}  // convert constructor
};
void  print( double d );
void  print( char * );
int  main( ){
   C c1,  c2(26);
   print( 100.123);    print(100.123);
}

名字覆盖:
也就是虚函数的重写相同函数签名的函数体。
名字隐藏:

#include<iostream>
using namespace std;
class B {
  public:
   void m( int  x ) { cout<<1<<endl;}
};
class  D : public B {
 public:
   void m ( ) {cout<<2<<endl;}
};
int main()
{
    D k;
    k.m(5);
    k.m();//不过可以用k.B::m(50)来访问。
}
若基类和派生类有非虚函数的同名函数,则基类函数被隐藏。
条件是同名,而不是相同签名!

抽象基类:至少有一个纯虚函数。

RTTI,运行时类型识别

操作符dynamic_cast,在运行期间检测类型转换是是否安全。
dynamic_cast(expression)
将基类指针转换为派生类指针,也可以转换引用。
若指针或引用所指对象的实际类型与转换的目的类型兼容,成功进行。
否则,若指针转换则得到空指针;如引用转换就抛出异常。

#include <iostream>
using namespace std;
class Base {
public:
	virtual void fun1() { cout << "Base::fun1()" << endl; }
	virtual ~Base() { }
};
class Derived1: public Base {
public:
	virtual void fun1() { cout << "Derived1::fun1()" << endl; }
	virtual void fun2() { cout << "Derived1::fun2()" << endl; }
};
class Derived2: public Derived1 {
public:
	virtual void fun1() { cout << "Derived2::fun1()" << endl; }
	virtual void fun2() { cout << "Derived2::fun2()" << endl; }
}; 
void fun(Base *b) {
	b->fun1();
	//尝试将b转换为Derived1指针
	Derived1 *d = dynamic_cast<Derived1 *>(b);
	//判断转换是否成功
	if (d != 0) d->fun2();
}
int main() {
	Base b;
	fun(&b);
	Derived1 d1;
	fun(&d1);
	Derived2 d2;
	fun(&d2);
	return 0;
}
运行结果:
Base::fun1()
Derived1::fun1()
Derived1::fun2()
Derived2::fun1()
Derived2::fun2()

Derived1 *d = dynamic_cast<Derived1 *>(b);
相当于对dynamic_cast<*>(公共汽车)询问:公共汽车是车吗?
如果是,转换成功。
这就是能够转换成d2
指针的原因。
本句前面的Derived1相当于占位符,对对d2的转换不起作用。

static_cast没有运行时类型检查保证转换的安全性。

typeid是关键字,获取类型的相关信息。
参数可以是表达式或类型说明符。
类型信息用type_info对象表示
type_info是typeinfo头文件中声明的类;
typeid的结果是type_info类型的常引用;
可以用type_info的重载的“==”、“!=”操作符比较两类型的异同;
type_info的name成员函数返回类型名称,类型为const char *。

#include <iostream>
#include <typeinfo>
using namespace std;
class Base {
public:
   virtual ~Base() { }
};
class Derived: public Base { };
void fun(Base *b) {
    //得到表示b和*b类型信息的对象
    const type_info &info1 = typeid(b);
    const type_info &info2 = typeid(*b);
    cout << "typeid(b): " << info1.name() << endl;
    cout << "typeid(*b): " << info2.name() << endl;
    if (info2 == typeid(Base)) //判断*b是否为Base类型
        cout << "A base class!" << endl;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值