理解C++继承与多态

理解C++继承与多态

类与类之间的关系

类与类之间的关系可分为3种:代理、继承、组合

代理(Proxy)

代理类是被代理类的接口的功能子集,代理类中实现了被代理类的部分功能

理解C++继承与多态_第1张图片

组合(Composition)

一个类或者多个类是另外一个类的一部分(a part of/has a)

理解C++继承与多态_第2张图片

继承(Inheritance)

子类是父类的一种,并且子类具有新的属性(a kind of/is a)

理解C++继承与多态_第3张图片

继承

派生类从基类中继承的数据包含3个方面:

  1. 派生类继承了基类的成员变量
  2. 派生类继承了基类的成员方法
  3. 派生类继承了基类的作用域

其中第三点是重要且容易被忽视的点,因为派生类继承了基类的作用域,所以当派生类和基类中存在同名成员时,可以依据作用域进行区分

class Base {
public:
    int data;
};
class Derive :public Base {
public:
    int data;
};
Derive d;
d.data = 1;
d.Derive::data = 1;//等同于d.data=1
d.Base::data = 1;

继承方式与访问限定符

继承方式有3种:public,protected,private,使用不同的继承方式进行继承,基类中的成员到了派生类中会有不同的访问权限,具体如下

继承方式 基类的public成员 基类的protected成员 基类的private成员
public public protected 不可见
protected protected protected 不可见
private private private 不可见

总的来说,基类中的成员被继承到派生类之后,其访问限定符的权限小于等于原权限。

基类的私有成员无论以何种方式继承,到派生类中均不可见,在派生类的类内和类外均不可访问,基类不想让派生类访问到的数据可以被设置为私有。

class Base {
private:
	int base_private_data;
};
class Derive :public Base {
public:
	void Demo() {
		base_private_data = 0;//error,基类的私有成员不可见
		cout << base_private_data << endl;//error
	}
private:
	int derive_data;
};
Derive d;
d.base_private_data = 0;//error,基类的私有成员不可见,在派生类的内部与外部均不可访问

基类的私有成员在虽然在派生类中不可见,但的确被派生类继承下去了,可以通过派生类对象的大小确定这一点

cout << sizeof(Derive) << endl;//8

默认继承方式

在继承时可以不指定继承方式,此时采用默认继承方式:当派生类为class时,默认继承方式为private,当派生类为struct时,默认继承方式是public(默认继承方式与基类无关,只与派生类是struct或class有关)

struct Base {
public:
	int a;
};
class Derive :Base {//相当于class Derive:private Base
public:
	int b;
};
class A {
public:
	int a;
};
struct B :A {//相当于struct B:public A
public:
	int b;
};

构造函数与析构函数

派生类对象的构造函数是如何工作的?

派生类对象在实例化时,会自动调用构造函数,派生类的构造函数会按照下面的顺序进行一系列的初始化工作:

  1. 调用基类的默认构造函数,如果基类不存在默认构造函数,则必须在初始化列表显示调用基类的构造函数
  2. 初始化派生类自己的成员变量
class Base {
public:
	Base(int data):base_data(data){
		cout << "Base" << endl;
	}
private:
	int base_data;
};
class Derive :public Base {
public:
	Derive(int b_data, int d_data) :Base(b_data), derive_data(d_data) {
		cout << "Derive" << endl;
	}
private:
	int derive_data;
};
Derive d(0,0);
/*
结果:
Base
Derive
*/

派生类对象的析构函数是如何工作的?

派生类对象的析构函数会首先对派生类中资源进行清理,在析构函数的"{"结束后会自动调用基类的析构函数对基类中的资源进行清理。

class Base {
public:
	Base() {
		cout << "Base" << endl;
		b_ptr = new int;
		*b_ptr = 0;
	}
	~Base() {
		cout << "~Base" << endl;
		delete b_ptr;
	}
private:
	int* b_ptr;
};
class Derive :public Base {
public:
	Derive() {
		cout << "Derive" << endl;
		d_ptr = new int;
		*d_ptr = 0;
	}
	~Derive() {
		cout << "~Derive" << endl;
		delete d_ptr;
	}//在"{"结束后会自动调用基类的析构函数
private:
	int* d_ptr;
};
Derive d;
/*
结果:
Base
Derive
~Derive
~Base
*/

在派生类的析构函数中不要显示的调用基类的析构函数,否则可能出现重复delete的错误行为。

总结:派生类对象实例化时,先调用基类的构造函数,在调用派生类的构造函数,派生类对象析构时,先调用派生类的析构函数,在自动调用基类的析构函数。

拷贝构造函数与赋值运算符重载

派生类对象默认的拷贝构造函数会自动调用基类的拷贝构造函数,并对派生类成员完成浅拷贝,派生类对象默认的赋值函数会自动调用基类的赋值函数。

class Base {
public:
	Base() = default;
	Base(const Base& b) {
		cout << "Base(const Base& b)" << endl;
		base_data = b.base_data;
	}
	Base& operator=(const Base& b) {
		cout << "Base& operator=(const Base& b)" << endl;
		if (&b != this) {
			base_data = b.base_data;
		}
		return *this;
	}
private:
	int base_data = 0;
};
class Derive :public Base {
public:
	Derive() = default;
private:
	int der_data = 0;
};
Derive d1;
Derive d2(d1);//Base(const Base& b)
d1 = d2;//Base& operator=(const Base& b)

如果派生类中维护了堆区数据,在进行拷贝与赋值时需要进行深拷贝的操作,那么派生类的拷贝构造函数与赋值函数就要复杂一些,需要在拷贝构造函数的初始化列表显示的调用基类的拷贝构造函数,在赋值函数内显示调用基类的赋值函数

class Base {
public:
	Base() = default;
	Base(const Base& b) {
		cout << "Base(const Base& b)" << endl;
		base_data = b.base_data;
	}
	Base& operator=(const Base& b) {
		cout << "Base& operator=(const Base& b)" << endl;
		if (&b != this) {
			base_data = b.base_data;
		}
		return *this;
	}
private:
	int base_data = 0;
};
class Derive :public Base {
public:
	Derive(int v=10) :ptr(new int(v)) {}//先调用基类的默认构造函数
	Derive(const Derive& d) :Base(d)/*切片*/, ptr(new int(*d.ptr)) {}
	Derive& operator=(const Derive& d) {
		Base::operator=(d);//指定作用域
		delete ptr;
		ptr = new int(*d.ptr);
		return *this;
	}
	~Derive() { delete ptr; }
private:
	int* ptr;
};

由于基类与派生类的operator=()函数的函数名相同,构成隐藏,所以在派生类的赋值函数中调用基类的赋值函数时需要指定作用域,否则会导致栈溢出

派生类与基类的同名成员

派生类与基类存在同名成员变量时,这2个同名成员变量构成隐藏(overhide)关系,当使用派生类对象进行访问时,默认访问到的是派生类中的成员,如果想要访问基类中的成员,需要指定基类的作用域。

一般认为派生类与基类的同名非静态成员变量构成隐藏,若派生类与基类中存在同名的静态成员变量,或成员变量同名,但一个为静态,一个为非静态,此时要想访问到基类中的成员,依旧需要加上作用域

class Base {
public:
    inline static int data = 5;
};
class Derive :public Base {
public:
    inline static int data = 0;
};
Derive d;
cout << d.data << endl;//0
cout << d.Base::data << endl;//5

派生类与基类存在同名成员函数时,这两个同名成员函数可能构成隐藏关系或覆盖(override)关系.

同名函数之间有3种可能的关系:重载、隐藏、覆盖

  • 重载

    函数重载有3个必要条件:

    1. 函数名相同
    2. 参数不同,可以是个数不同、类型不同、顺序不同
    3. 同一作用域

    其中第三个条件是比较重要且容易忽视的条件,如果2个函数之间满足前2个条件,但不满足第3个条件,那么这2个函数不构成重载,例如

    namespace Aspace {
    	void Demo(){}
    }
    namespace Bspace {
    	void Demo(){}
    }
    

    这两个Demo函数虽然函数名和参数相同,但是作用域不同,因此不构成重载

  • 覆盖(重写)

    函数构成重写的必要条件:

    1. 函数名相同
    2. 参数与返回值相同(对于返回值而言,协变除外)
    3. 2个函数一个在父类作用域,一个在子类的作用域
    4. 父类作用域中的那个函数必须被virtual修饰,为虚函数

    例如

    class Base {
    public:
    	virtual void Demo() {
    		cout << "Base" << endl;
    	}
    };
    class Derive :public Base {
    public:
    	void Demo() {
    		cout << "Derive" << endl;
    	}
    };
    
  • 隐藏

    2个同名函数,一个在父类作用域,一个在子类作用域,只要不构成重写,就构成隐藏,2个函数构成隐藏的常见场景如下

    class Base {
    public:
        void Demo() {
            cout << "Base" << endl;
        }
    };
    class Derive :public Base {
    public:
        void Demo() {
            cout << "Derive" << endl;
        }
    };
    Derive d;
    d.Demo();//Derive
    d.Base::Demo();//Base
    

    除此之外,还存在一些不太常见的场景,在这些场景下,函数依旧构成隐藏

    1. 基类中存在重载的函数

      class Base {
      public:
          void Demo() {
              cout << "Base" << endl;
          }
          void Demo(int){
              cout << "Base int" << endl;
          }
      };
      class Derive :public Base {
      public:
          void Demo() {
              cout << "Derive" << endl;
          }
      };
      Derive d;
      

      Base::Demo与Base::Demo(int)构成函数重载,Derive::Demo与Base::Demo和Base::Demo(int)均构成隐藏,此时不能通过d.Demo(1)直接调用Base::Demo(int),必须指定作用域 d.Base::Demo(1)

    2. 基类中存在同名的虚函数

      class Base {
      public:
          virtual void Demo() {
              cout << "Base" << endl;
          }
          virtual void Demo(int){
              cout << "Base int" << endl;
          }
      };
      class Derive :public Base {
      public:
          void Demo() {
              cout << "Derive" << endl;
          }
      };
      Derive d;
      

      Base::Demo与Base::Demo(int)构成函数重载,Derive::Demo与Base::Demo构成重写,Derive::Demo与Base::Demo(int)构成隐藏

    3. 派生类中的函数为虚函数

      class Base {
      public:
          void Demo() {
              cout << "Base" << endl;
          }
          void Demo(int){
              cout << "Base int" << endl;
          }
      };
      class Derive :public Base {
      public:
          virtual void Demo() {
              cout << "Derive" << endl;
          }
      };
      Derive d;
      

      此时派生类中的Demo函数与Base中的2个Demo函数都构成隐藏


在判断2个同名函数之间的关系时,需要严格对照上述规则,否则当存在虚函数时容易判断错误。

基类与派生类对象的赋值

派生类可以赋值给基类,称为从下向上的转换,其中"下"是派生类,"上"是基类,基类不能赋值给派生类,因为派生类中有的成员基类中不一定有。

class Base {
public:
	int a;
};
class Derive :public Base {
public:
	int b;
};
Base b1, b2;
Derive d1, d2;
b1 = d1;//派生类可以赋值给基类,从下向上的转换
d2 = b2;//基类不能赋值给派生类

即使派生类中没有新增的成员,编译器也不支持将基类赋值给派生类

class Base {
public:
	int a;
};
class Derive :public Base {

};
Base b;
Derive d;
d = b;//error

基类指针或引用可以引用派生类对象,访问派生类中的基类成员(成员变量+成员方法),派生类的指针或引用不能指向基类成员,否则在进行访问时可能出现内存错误。

class Base {
public:
	int a;
};
class Derive :public Base {
public:
	int b;
};
Base b;
Derive d;
Base& ref = d;//基类引用可以引用派生类,访问基类成员
ref.a = 10;
Derive* ptr = (Derive*)&b;//错误行为
ptr->a = 10;
ptr->b = 20;//error,内存错误

继承与友元

友元关系不能被继承,基类的友元不能被继承到派生类中

class Base {
	friend void Demo();
private:
	int x = 10;
};
class Derive :public Base {
private:
	int y = 20;
};
void Demo() {
	Base b;
	cout << b.x << endl;//ok
	Derive d;
	cout << d.x << endl;//ok
	cout << d.y << endl;//error
}

可以理解为基类的友元关系仅在基类作用域有效,在派生类作用域无效,Demo只能访问成员x,不能访问成员y。值得注意的是,虽然基类的private成员在派生类中不可见,但是基类的友元可以访问,因此在Demo函数中可以访问d.x(因为x位于基类的作用域)

继承与静态成员

在整个继承体系中,静态成员变量只有一份,可以利用此特点来统计继承体系中实例化出的对象的数量。

class Base {
public:
	Base() { cnt++; }
public:
	inline static int cnt = 0;
};
class Derive :public Base {
public:
	int x;
};
Base b;
Derive d;
cout << &b.cnt << endl;//00B7D3D4
cout << &d.cnt << endl;//00B7D3D4
cout << &Base::cnt << endl;//00B7D3D4
cout << &Derive::cnt << endl;//00B7D3D4
cout << Base::cnt << endl;

静态成员函数

派生类中的成员函数可以与基类中的静态成员函数构成隐藏

class Base {
public:
	static void Demo(int x) { cout << "Base" << endl; }
};
class Derive :public Base {
public:
	void Demo() { cout << "Derive" << endl; }
};
Derive d;
d.Demo();
d.Base::Demo(1);//因为构成隐藏,必须通过作用域调用

多继承

与Java不同,C++中允许使用多继承,一个派生类可以继承多个基类,在多继承体系下,需要把握好对象的内存模型。

理解C++继承与多态_第4张图片

先继承的基类,其成员数据被置于低地址处,高地址处存放派生类自己的数据(由于当前机器是小端存储,因此显示的数据需要从右向左读取)

菱形继承

C++允许多继承,因此可以进行菱形继承,其对象的内存布局如下

理解C++继承与多态_第5张图片

菱形继承存在数据冗余的问题,Base中的成员在Derive中存在2份,除此之外,菱形继承还存在数据访问时的二义性问题

d.base_data = 10;//不明确

虚继承

虚继承通过使用虚基表指针解决了菱形继承的问题,使用虚继承,就没有菱形继承的数据冗余和二义性问题。

理解C++继承与多态_第6张图片

Middle1和Middle2通过virtual的方式继承Base类,Base称为虚基类,在Middle1和Middle2的内存模型中,存在一个虚基表指针,存放在对象的头4/8字节,然后是Middle1和Middle2自身的成员,最后是继承的Base中的成员。虚基表指针指向虚基表,虚基表中存储偏移量,通过偏移量即可找到Base的数据成员。

理解C++继承与多态_第7张图片

注意到虚基表指针指向的虚基表中,第一个位置为0,第二个位置才是偏移量,其中第一个位置与虚函数和多态有关,由于当前探讨的是一般情况,继承体系中不包含虚函数,因此第一个位置为0

多态

虚函数

基类中不包含虚函数时,基类的大小为基类中所含成员变量的大小,且此时用基类的指针或引用调用成员函数时,只与指针和引用本身的类型有关,与其指向的对象无关。

class Base {
public:
	void Demo() { cout << "Base" << endl; }
protected:
	int b_data = 0;
};
class Derive :public Base {
public:
	void Demo() { cout << "Derive" << endl; }
private:
	int d_data = 5;
};
Derive d;
Base* ptr = &d;
ptr->Demo();//Base
Base& ref = d;
ref.Demo();//Base
cout << typeid(ptr).name() << endl;//class Base*
cout << typeid(*ptr).name() << endl;//class Base
cout << typeid(ref).name() << endl;//class Base

基类中包含虚函数时,情况就不一样了

class Base {
public:
	virtual void Demo() { cout << "Base" << endl; }
protected:
	int b_data = 0;
};
class Derive :public Base {
public:
	void Demo() { cout << "Derive" << endl; }
private:
	int d_data = 5;
};
Derive d;
Base* ptr = &d;
ptr->Demo();//Derive
Base& ref = d;
ref.Demo();//Derive
cout << typeid(ptr).name() << endl;//class Base*
cout << typeid(*ptr).name() << endl;//class Derive
cout << typeid(ref).name() << endl;//class Derive
cout << "sizeof(Base)=" << sizeof(Base) << endl;//8
cout << "sizeof(Derive)=" << sizeof(Derive) << endl;//12

基类中包含虚函数时,其大小是8,此时用基类指针或引用调用该函数,与其指向的对象有关,不在和指针与引用本身的类型相关,并且此时打印ref和*ptr时,结果为指向对象的类型。

虚函数表指针

当一个类中包含虚函数时(包括通过继承得来的虚函数),该类实例化出的对象有且仅有一个虚函数表指针,对象的大小为成员变量的大小加上虚函数表指针的大小,以上面包含虚函数Demo的Base类和Derive类为例,其内存布局如下:

理解C++继承与多态_第8张图片

虚函数表指针被置于对象的头部,即低地址位置,虚函数表指针指向虚函数表,虚函数表中存放的是当前类中虚函数的地址。

理解C++继承与多态_第9张图片

虚函数的覆盖

基类中存在虚函数,并且派生类实现了同名覆盖函数,那么派生类虚函数表中同名覆盖函数的地址会将基类同名方法的地址覆盖,Derive的虚函数表:

理解C++继承与多态_第10张图片

如果基类中存在虚函数,但是派生类没有对其进行重写,那么在派生类的虚函数表中保存的依旧是基类的虚函数地址,若派生类中存在自己特有的虚函数,其地址会被添加到派生类虚函数表末尾位置。

哪些方法不能实现为虚函数?

虚函数的特点:

  1. 虚函数的地址会被设置进当前类的虚函数表
  2. 虚函数的调用依赖于对象,可以是对象直接调用,也可以是在运行时通过虚函数表指针找到虚函数并调用

虚函数的上述特点,决定了以下函数不能被声明虚函数:

  • 构造函数,构造函数负责对象的初始化工作,只有当对象初始化完毕之后,才可以调用虚函数

    class Foo {
    public:
    	virtual Foo() {}//error,不允许使用virtual
        virtual Foo(const Foo& f){}//error,不允许使用virtual
    };
    
  • 静态成员函数,静态成员函数与对象无关,不能被声明为虚函数

    class Foo {
    public:
    	//error,仅非静态成员函数可以是虚拟的
    	virtual static void Demo(){}
    };
    
  • 普通全局函数,普通全局函数与对象无关,不能被声明为虚函数

    //error,类声明外部的说明符无效
    virtual void Demo(){}
    
  • 内联函数,内联函数没有地址,从理论上来说也不能被声明为虚函数,但是由于inline是建议性关键字,在将被inline修饰的函数声明为virtual时,编译器会自动忽略其内联属性,使得inline virtual修饰的成员函数能够编译通过

    class Foo {
    public:
        //可以编译通过,inline被编译器自动忽略
    	inline virtual void Demo() {}
    };
    

特殊成员函数

  1. 拷贝构造函数可以被声明为虚函数吗?

    拷贝构造函数也属于构造函数,用于初始化对象,不能被声明为虚函数。

  2. 赋值运算符重载函数可以被声明为虚函数吗?

    赋值运算符重载函数没有违背虚函数的特征,可以被声明为虚函数。

    class Base {
    public:
    	virtual Base& operator=(const Base& f) {
    		b_data = f.b_data;
    	}
    private:
    	int b_data;
    };
    class Derive :public Base {
    public:
    	virtual Derive& operator=(const Derive& d) {
    		d_data = d.d_data;
    		Base::operator=(d);
    	}
    private:
    	int d_data;
    };
    

    如果基类的赋值函数被声明为虚函数,派生类的赋值函数与基类的赋值函数之间的关系是隐藏而不是重写

  3. 析构函数可以被声明为虚函数吗?

    析构函数可以被声明为虚函数,并且建议将基类的析构函数声明为虚函数,在一些特殊场景下可以避免出现内存泄漏

动态绑定与静态绑定

静态绑定:在编译时期就能确定具体的行为

动态绑定:在运行时才能确定具体的行为

C++中实现静态绑定的机制:函数重载、模版

函数重载

理解C++继承与多态_第11张图片

模版

理解C++继承与多态_第12张图片

C++中实现动态绑定的机制为虚函数,当方法不是虚函数时,在编译期间可以直接根据对象的类型、指针的类型、引用的类型确定调用的函数,当方法是虚函数时,需要在运行时通过对象的虚函数表指针找到对应的虚函数并进行调用,此时通过指针或引用调用虚函数就与指针和引用本身的类型无关。

Base类实现了虚函数,Base*指向Derive对象,其解引用的类型变成了Derive,这与虚函数表和C++的RTTI机制密切相关。

class Base {
public:
    virtual ~Base() {}
};
class Derive :public Base {
public:
    int data;
};
Base b;
Derive d;
Base* b1 = &b;
Base& r1 = b;
cout << typeid(b1).name() << endl;//class Base*
cout << typeid(*b1).name() << endl;//class Base
cout << typeid(r1).name() << endl;//class Base
Base* b2 = &d;
Base& r2 = d;
cout << typeid(b2).name() << endl;//class Base*
cout << typeid(*b2).name() << endl;//class Derive
cout << typeid(r2).name() << endl;//class Derive

RTTI

C++中的RTTI(Run-Time Type Information)即运行时类型信息,是一种在程序运行时获取和操作对象类型信息的机制,RTTI允许在程序运行期间检查对象的实际类型,而不仅仅只是在编译时处理,RTTI在处理多态、基类指针与引用时有很好的效果,可以动态的确定对象的实际类型

C++中的RTTI主要通过typeid运算符dynamic_cast运算符来完成.

typeid运算符用于获取表达式的类型信息,返回一个std::type_info对象,当基类中包含虚函数时,typeid会在运行时从对象的虚函数表中获取到对象RTTI的类型信息。

class Base {
public:
	virtual Base(){}
};
class Derive :public Base {
public:
	int y;
};
Base* p = new Derive;
if (typeid(*p) == typeid(Base)) {
    cout << "points to Base"<<endl;
}
else {
    cout << "points to Derive" << endl;//√
}
delete p;

若基类不包含虚函数,那么typeid只会获取到指针和引用本身的类型信息,因为运行时类型信息只有在虚函数表中才存在。

class Base {
public:
	int x;
};
class Derive :public Base {
public:
	int y;
};
Derive d;
Base& r = d;
if (typeid(r) == typeid(Base)) {
    cout << "Base" << endl;//√
}
else {
    cout << "Derive" << endl;
}

dynamic_cast运算符用于执行安全的向下转型(downcasting),dynamic_cast允许将基类的指针或引用转化为派生类的指针或引用,如果转化不合法,dynamic_cast返回空指针或引用,dynamic_cast会在运行时检查对象的实际类型,从而确定基类指针或引用到底是指向基类对象还是派生类对象,进而判断向下转型(downcasting)是否安全。

在使用dynamic_cast时要确保基类中存在虚函数(即存在虚函数表),dynamic_cast会在运行时获取虚函数表中的类型信息。

class Base {
public:
	virtual void Demo() {}
};
class Derive :public Base {
public:
	int x;
};
try {
    Base b;
    Derive d;
    Base& r1 = b;
    Base& r2 = d;
    Derive& r3 = dynamic_cast<Derive&>(r2);//downcasting成功
    Derive& r4 = dynamic_cast<Derive&>(r1);//downcasting失败,抛出std::bad_cast异常
}
catch (const std::bad_cast& e) {
    cout << "转型失败" << endl;
}
catch (...) {
    cout << "其它错误" << endl;
}

dynamic_cast将基类引用向下转化(downcasting)为派生类引用时,如果失败,会抛出std::bad_cast异常,将基类指针向下转化为派生类指针时,如果失败,会返回空指针。

class Base {
public:
	virtual void Demo() {}
};
class Derive :public Base {
public:
	int x;
};
Base b;
Derive d;
Base* p1 = &b;
Base* p2 = &d;
Derive* p3 = dynamic_cast<Derive*>(p2);
if (p3) {
    cout << "downcasting success" << endl;//√
}
else {
    cout << "downcasting fail" << endl;
}
Derive* p4 = dynamic_cast<Derive*>(p1);
if (p4) {
    cout << "downcasting success" << endl;
}
else {
    cout << "downcasting fail" << endl;//×
}

使用dynamic_cast(Base_ref/Base_ptr)时,必须保证Base类是多态类型,否则会编译报错,因为此时dynamic_cast无法从虚函数表中提取出类型信息

动态绑定

运行时多态与指针、引用调用虚函数相关,使用基类指针指向基类对象基类指针指向派生类对象派生类指针指向派生类对象,在调用虚函数时,都会发生多态,在运行时根据虚函数表指针找到虚函数的地址并进行调用.

理解C++继承与多态_第13张图片

在示例中,Derive没有重写基类的虚函数,此时Derive的虚函数表中存放的就是Base虚函数的地址,通过指针或引用调用时,最终都是调用的Base的虚函数。

vtable for Derive:
        .quad   0
        .quad   typeinfo for Derive
        .quad   Base::Demo()
vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::Demo()

通过汇编可以发现,虚函数表中的确保存了运行时类型信息。

使用对象调用虚函数不会发生多态

运行时多态与指针和引用调用虚函数相关,使用对象调用虚函数不会发生多态,使用对象调用虚函数的行为属于编译时决议。

理解C++继承与多态_第14张图片

抽象类和纯虚函数

纯虚函数:没有具体实现的虚函数称为纯虚函数,包含纯虚函数的类称为抽象类,抽象类不能实例化对象,一般用作接口使用。

class Base {
public:
	virtual void Demo() = 0;//纯虚函数
};
Base b;//error,抽象类不能实例化对象

派生类必须重写基类的纯虚函数函数,否则派生类也无法实例化对象

class Base {
public:
	virtual void Demo() = 0;//纯虚函数
};
class Derive :public Base {
public:
	int x;
};
Derive d;//error,必须重写纯虚函数才能实例化对象

析构函数设置为虚函数

建议将析构函数设置为虚函数,在使用基类指针指向new出来的派生类对象对象时,如果没有将基类的析构函数设置为虚函数,那么在delete时可能导致内存泄漏

class Base {
public:
	Base() :p(new int(0)) {}
	~Base() {
		cout << "~Base" << endl;
		delete p;
	}
private:
	int* p;
};
class Derive :public Base {
public:
	Derive() :c(new char(0)) {}
	~Derive() {
		cout << "~Derive" << endl;
		delete c;
	}
private:
	char* c;
};
Base* ptr = new Derive;
delete ptr;//只调用了Base的析构函数

new Derive时,先调用Derive的构造函数,Derive的构造函数中会调用Base的构造函数,在进行delete时,会调用析构函数,由于不构成多态,依据指针本身的类型决定调用的析构函数,ptr的类型是Base*,因此只调用Base的析构函数,c指针管理的内存没有释放,造成内存泄漏。把基类的析构函数设置为虚函数就不存在上述问题,基类的析构函数设置为虚函数,此时delete ptr,会依据指针指向的对象的类型决定调用的函数,由于析构函数的函数名统一被编译器处理为destructor,因此基类的析构函数与派生类的析构函数构成重写,delete ptr就会调用派生类的析构函数,派生类析构函数的}结束后又会自动调用基类的析构函数,避免了内存泄露.

new与delete

派生类存在虚函数,基类没有虚函数,使用基类指针指向new出来的派生类对象,delete基类指针时的错误

理解C++继承与多态_第15张图片

基类没有虚函数,派生类中存在虚函数,使用基类指针指向派生类会发生偏移,此时指针中存储的数据并非派生类对象的起始地址,这与Base* p=new Derive是相同的原理,p中保存的数据并不是new出来的Derive的起始地址,存在一个虚函数表指针大小的偏移,此时进行delete p由于释放的位置与起始位置之间存在偏移,会发生错误.

理解C++继承与多态_第16张图片

虚函数表

虚函数表所在的区域

理解C++继承与多态_第17张图片

虚函数表的地址与字符常量区的地址十分接近,虚函数表位于字符常量区。

一个类只有一个虚函数表,所有对象共享

虚函数表的写入时间:虚函数表在编译期间已经写入完毕,在编译时虚函数表中已经填充好了虚函数的地址,可以通过下面的测试佐证这一点:

理解C++继承与多态_第18张图片

在new Base后,调用了Base的构造函数,在构造函数中调用了clear函数,将this指向的对象进行清空,虚函数表指针被置为0x00000000,p->show属于运行时多态,在运行时尝试依据虚函数表指针找到虚函数并调用,但是由于虚函数表指针在编译时已经设置好了,运行时调用clear先进行了一次清空操作,因此p->show在运行时尝试寻找虚函数表就会报错.

虚函数表和虚函数表指针在编译期间即可确定,虚函数表指针甚至可以在运行时修改(虚函数表不能在运行时修改,因为虚函数表位于字符常量区)

观察下面一段示例

class Base{
public:
	Base() {
		clear();
	}
	void clear() { 
		memset(this, 0, sizeof(*this));
	}
	virtual void show(){
		cout << "Base::" << b_data << endl;
	}
	virtual ~Base(){}
private:
	int b_data=1;
};
class Derive : public Base{
public:
	Derive(){
		cout << "Derive()" << endl;
	}
	void show(){
		cout << "Derive::" << d_data << endl;
	}
private:
	int d_data=2;
};
Base* p = new Derive;
p->show();
delete p;

p指向的Derive的虚函数表指针最终会是0x00000000吗?

理解C++继承与多态_第19张图片

new Derive时,调用Derive的构造函数,Derive的构造函数会先调用Base的构造函数初始化基类部分,Base的构造函数调用clear时会将数据清空,Base的构造函数调用完毕,后续还会初始化派生类部分!在这个过程中派生类会在构造函数的初始化列表重写虚函数表指针,使其指向派生类的虚函数表,因此虚函数表指针由0x00000000变为一个有效值,p->show发生运行时多态,直接通过派生类的虚函数表指针找到派生类的虚函数表,进行虚函数的调用。

多继承的虚函数表

class Base1 {
public:
	virtual void Demo1() {}
	virtual void Demo2() {}
};
class Base2 {
public:
	virtual void Demo1() {}
	virtual void Demo2() {}
};
class Derive :public Base1, public Base2 {
public:
	virtual void Demo1() {}
	virtual void Demo3() {}
};

多继承时,Derive中有2个虚函数表指针,Derive中新增的虚函数位于第一个虚函数表指针指向的虚函数表。

vtable for Derive:
        .quad   0
        .quad   typeinfo for Derive
        .quad   Derive::Demo1()
        .quad   Base1::Demo2()
        .quad   Derive::Demo3() #Derive中新增加的虚函数位于第一个虚函数表
        .quad   -8
        .quad   typeinfo for Derive
        .quad   non-virtual thunk to Derive::Demo1()
        .quad   Base2::Demo2()
vtable for Base2:
        .quad   0
        .quad   typeinfo for Base2
        .quad   Base2::Demo1()
        .quad   Base2::Demo2()
vtable for Base1:
        .quad   0
        .quad   typeinfo for Base1
        .quad   Base1::Demo1()
        .quad   Base1::Demo2()

经典例题

  1. 输出什么?

    class Base {
    public:
    	Base() { Demo(); }
    	virtual void Demo() { cout << "Base" << endl; }
    };
    class Derive :public Base {
    public:
    	virtual void Demo() { cout << "Derive" << endl; }
    };
    Derive d;
    

    构造函数中调用虚函数为编译时决议,直接call Demo,输出"Base"

  2. 输出什么?

    class Base {
    public:
    	virtual void Demo() { cout << "Base" << endl; }
    	virtual ~Base() { Demo(); }
    };
    class Derive :public Base {
    public:
    	virtual void Demo() { cout << "Derive" << endl; }
    	~Derive() { Demo(); }
    };
    Base* d = new Derive;
    delete d;
    

    析构函数中调用虚函数也是编译时决议,call Demo,输出"Derive"、“Base”

  3. 输出什么?

    class Base {
    public:
    	virtual void Demo() { cout << "Base" << endl; }
    	virtual ~Base() { Func(); }
    	virtual void Func() { Demo(); }
    };
    class Derive :public Base {
    public:
    	virtual void Demo() { cout << "Derive" << endl; }
    	virtual void Func() { cout << "Func" << endl; }
    };
    Base* d = new Derive;
    delete d;
    

    Derive的析构函数结束后,虚函数表指针指向Base的虚函数表,析构函数中调用Func是直接call Func,Func中调用Demo是call rdx寄存器,此时rdx寄存器中保存的是Base::Demo的地址,输出"Base"

  4. 能运行成功吗?

    class Base {
    public:
    	virtual void Demo() { cout << "Base" << endl; }
    };
    class Derive :public Base {
    private:
    	virtual void Demo() { cout << "Derive" << endl; }
    };
    Base* p = new Derive;
    p->Demo();
    delete p;
    

    访问限定符private、public、protected仅在编译阶段发挥作用,控制外接对类成员的访问,p的类型是Base*,在编译阶段,只能看到Base类中的成员,Base::Demo,因此可以编译通过,但是到了运行阶段,发生动态多态,虚函数表指针指向派生类的虚函数表,调用派生类的Demo函数,所以输出"Derive"

    注意

    • 派生类中只要实现了与基类同函数头的函数,不论访问限定符如何,都构成重写
    • 访问限定符只在编译阶段发挥作用,运行阶段无效
  5. 能运行成功吗?

    class Base {
    protected:
    	virtual void Demo() { cout << "Base" << endl; }
    };
    class Derive :public Base {
    public:
    	virtual void Demo() { cout << "Derive" << endl; }
    };
    Base* p = new Derive;
    p->Demo();
    delete p;
    

    编译出错,p的类型是Base*,在编译阶段只能看到Base中的成员,Base中的Demo访问限定符为protected,因此编译失败

  6. 输出什么?

    class Base {
    public:
    	virtual void Demo(int a=10) { cout << a << endl; }
    };
    class Derive :public Base {
    private:
    	virtual void Demo(int a=20) { cout << a << endl; }
    };
    Base* p = new Derive;
    p->Demo();
    delete p;
    

    虚函数的继承是接口继承,Derive类继承了Base虚函数的接口,重写了虚函数的实现,在Base的接口中,默认参数a=10,因此输出"10"

    理解C++继承与多态_第20张图片

  7. 输出什么?

    class A{
    public:
    	A(const char* s) { cout << s << endl; }
    	~A() {};
    };
    class B : virtual public A{
    public:
    	B(const char* s1, const char* s2):A(s1){cout << s2 << endl;}
    };
    class C : virtual public A{
    public:
    	C(const char* s1, const char* s2):A(s1){cout << s2 << endl;}
    };
    class D : public B, public C{
    public:
    	D(const char* s1, const char* s2, const char* s3, const char* s4):B(s1, s2), C(s1, s3), A(s1){cout << s4 << endl;}
    };
    D* p = new D("class A", "class B", "class C", "class D");
    delete p;
    

    D在进行构造时,先调用B、C的构造函数,在调用A的构造函数,最后调用D自己的构造函数,B、C在调用构造函数时,也是先调用A的构造函数,由于是虚继承,因此A的构造函数只会被调用一次,输出的结果是"class A class B class C class D"

  8. 下面代码中p1、p2与p3的大小关系

    class Base1{
    public:
    	int b1;
    };
    class Base2{
    public:
    	int b2;
    };
    class Derive : public Base1, public Base2{
    public:
    	int d;
    };
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    

    存在指针的偏移,所以(p1=p3)

    理解C++继承与多态_第21张图片

  9. 输出什么?

    class A{
    public:
    	virtual void Demo(int val = 1){cout << "A->" << val << endl;}
        virtual void F(){Demo();}
    };
    class B : public A{
    public:
    	void Demo(int val = 0){cout << "B->" << val << endl;}
    };
    B* p = new B;
    p->F();
    

    实现动态多态的前提:基类的指针或引用调用虚函数,且对象已经被初始化完成,且还未开始销毁,p调用F函数时,对象已经初始化完成,F中this调用Demo,属于基类指针调用虚函数,构成动态多态,F中Demo的汇编如下:

    A::F():
            push    rbp
            mov     rbp, rsp
            sub     rsp, 16
            mov     QWORD PTR [rbp-8], rdi
            mov     rax, QWORD PTR [rbp-8]
            mov     rax, QWORD PTR [rax]
            mov     rdx, QWORD PTR [rax]
            mov     rax, QWORD PTR [rbp-8]
            mov     esi, 1 #默认参数在编译时其效果
            mov     rdi, rax
            call    rdx
            nop
            leave
            ret
    

    可知在F中调用Demo使用的参数是1,接着call rdx,实际调用的函数体是B类中Demo的函数体,不过由于this的类型是A*,使用的默认参数在编译时就已经确定,所以结果是"B->1"

  10. 输出什么?

    class A{
    public:
    	virtual void Demo(int val = 1){cout << "A->" << val << endl;}
    };
    class B : public A{
    public:
    	void Demo(int val = 0){cout << "B->" << val << endl;}
    };
    B* p = new B;
    p->Demo();
    

    函数的默认参数在编译时起效果,在编译时,p的类型是B*,因此默认参数取B中的默认参数0,输出"B->0"

    如何理解接口继承?

    普通函数的继承是实现继承,虚函数的继承是一种接口继承,派生类继承了基类虚函数的接口,重写其实现,运行时多态是调用函数具体的实现,至于函数的参数,可以在编译时确定,也可以在运行时确定,在第9题中,p->F(),F中调用Demo,在编译时this的类型是A*,又由于没有显示传参,因此采用A中Demo的默认参数,只不过在运行时call rdx寄存器调用B中Demo的实现。在第10题中,p->Demo(),p是B类型的指针,因此采用B中Demo的默认参数。

    函数的默认参数在编译时根据类型确定,而具体调用的函数在运行时根据对象确定。

  11. pE、pF、pG的关系

    class ClassE {
    public:
    	virtual ~ClassE() {};
    	virtual void FunctionE() {};
    };
    class ClassF {
    public:
    	virtual void FunctionF() {};
    };
    class ClassG : public ClassE, public ClassF {
    public:
    	//nothing
    }; 
    ClassG aObject; //语句1
    ClassE* pE = &aObject;
    ClassF* pF = &aObject;
    ClassG* pG = &aObject;
    

    pF>(pE=pG)

    理解C++继承与多态_第22张图片

  12. 下面选项正确的是:

    class ClassE {
    public:
    	virtual ~ClassE() {};
    	virtual void FunctionE() {};
    };
    class ClassF {
    public:
    	virtual void FunctionF() {};
    };
    class ClassG : public ClassE, public ClassF {
    public:
    	//nothing
    }; 
    ClassG aObject; //语句1
    ClassE* pE = &aObject;
    ClassF* pF = &aObject;
    ClassG* pG = &aObject;
    ClassE* pE2;
    

    A:pE2=static_cast(pF);

    B:void*pVoid=static_cast(pF); pE2=static_cast(pVoid);

    C:pE2=pF;

    D:pE2=static_cast(static_cast(pF));

    分析:E与F是完全不同的类型,且没有继承关系,A选项错误;void*类型可以接收任意类型的指针,B选项第一条语句编译没问题,第二条语句编译起来也没有问题,但是此时pE2的指向错误;C选项与A选项类似,错误;D选项先将pF指针静态转化为G类型指针会发生偏移,接着在转化为E类型指针是没有问题的。

    关于static_cast

    static_cast可以用于基类与派生类指针之间的转化(可以将基类指针转化为派生类指针),但是static_cast不会检查这种转化是否安全,如图

    理解C++继承与多态_第23张图片

    static_cast不能用于不相关类型指针之间的转化:

    int* p = new int;
    char* c = static_cast<char*>(p);//类型转换无效
    

    通过调试观察此题的B选项和D选项:

    理解C++继承与多态_第24张图片

    在将pVoid赋值给pE2时,pE2的指向(红色部分)已经错误,void*类型指针在进行赋值时不会发生偏移!将pF赋值给tmp时,会发生偏移,属于将基类指针赋值给派生类指针,但是因为此时基类指针pF指向的就是派生类,因此没有问题,tmp偏移到低地址位置,将tmp赋值给pE2,属于将派生类指针赋值给基类,此时偏移量为0,pE2指向正确位置

  13. 将第12题中的语句1改为ClassG aObject=new ClassG;,则下面哪一个语句是不安全的?

    A:delete pE

    B:delete pF

    C:delete pG

    答案是B,因为pF的析构函数不是虚函数

  14. 判断语句E和F的正确性

    class Base {
    	//nothing
    };
    class Derived : public Base {
    	//nothing
    };
    Base* pB = new Base();
    if (Derived* pD = static_cast<Derived*>(pB)) {
        //语句E
    }
    Derived* pD = new Derived();
    if (Base* pB = static_cast<Base*>(pD)) {
        //语句F
    }
    

    语句E是错误的,基类指针转化为派生类指针应该使用dynamic_cast,并且基类必须要有虚函数以供获取向下转型(downcasting)时的类型信息,语句F是正确的

你可能感兴趣的:(c++,开发语言)