C++:多态

1 、概念

多态:同一指令,针对不同对象,产生不同行为。

多态的类型

  • 静态多态(静态联编):编译时多态。形式:函数重载、运算符重载、模板。

  • 动态多态(动态联编):运行时多态。形式:虚函数。

多态与虚函数不是等价的,动态多态的体现必须要有虚函数,调用虚函数并不一定体现多态。

2、虚函数

2.1、虚函数的定义

虚函数:在成员函数前加 virtual 关键字,在末尾可添加 override 关键字。

派生类重写基类的虚函数

  • 函数同名
  • 返回值类型相同
  • 参数列表相同:参数类型、参数个数、参数顺序

不能设置为虚函数的函数

  • 普通函数:非成员函数
  • 静态成员函数:编译时绑定。虚函数的调用需要对象,需要 this 指针,而静态函数没有 this 指针,可以不使用对象调用,使用类名+作用域限定符调用
  • 内联成员函数:没有必要,内联函数本身是为了减少函数调用的代价,而虚函数需要创建虚函数表,失去内联的意义。
  • 非成员函数的友元函数
  • 构造函数:
    • 从继承观点来看,构造函数不能被继承,而虚函数可以被派生类重写。
    • 从存储角度,如果构造函数是虚函数,则需用通过虚表来调用,但是对象还没有实例化,没有内存空间,无法通过虚函数指针找到虚表。
    • 从语义角度,构造函数就是为了初始化数据成员,然而虚函数是为了在完全不了解细节情况下也能正确处理对象,虚函数要对不同类型的对象产生不同的动作。如果构造函数是虚函数,那么对象都没有产生,无法完成想要的操作。

2.2、虚函数的实现机制

2.2.1、实现原理
  • 虚函数指针 vfptr:指向虚表
  • 虚函数表(虚表):虚函数的入口地址。注意:多基继承时,只有第一个虚表存放虚函数入口地址,其他虚表存放跳转指令,指向第一个虚表。

在这里插入图片描述

当基类定义虚函数的时候,就会在基类对象的存储布局的前面多一个虚函数指针,指向自己的虚函数表,存放虚函数的入口地址。当派生类继承该基类的时候,把基类的虚函数吸收过来,派生类虚函数指针指向自己的虚函数表,若派生类重写该虚函数,则派生类虚函数表中对应的虚函数的入口地址被覆盖 override。

2.2.2、多态被激活的条件
  • 基类定义虚函数
  • 派生类重定义虚函数
  • 创建派生类对象
  • 基类的指针(引用)指向(绑定)到派生类对象
  • 使用基类指针(引用)调用虚函数
2.2.3、测试虚表

测试虚表的存在,一级指针指向派生类对象(虚表的首地址),二级指针指向虚表中的虚函数,打印虚表。

#include <iostream>
using std::cout;
using std::endl;
 
class Base {
public:
	Base(long base)
	: _base(base)
	{	cout << "Base(long)" << endl;	}

	virtual void func1() {	cout << "Base::func1()" << endl;	}
	virtual void func2() {	cout << "Base::func2()" << endl;	}
	virtual void func3() {	cout << "Base::func3()" << endl;	}
private:
	long _base;		
};

class Derived
: public Base
{
public:
	Derived(long base, long derived)
	: Base(base)
	, _derived(derived)
	{	cout << "Derived(long,long)" << endl;	}

	virtual void func1() {
		cout << "Derived::func1() _derived:" << _derived << endl;
	}
	virtual void func2() {	
        cout << "Derived::func2()" << endl;	
    }
private:
	long _derived;
};

// 通过二级指针验证虚函数表的存在,
void test() {
	Derived d(10, 100);
    cout << "----- 打印虚函数表中的虚函数地址 -----" << endl;
	// 一级指针,指向派生类对象的首地址,即虚函数表的地址
    long *pvtable = (long*)&d;
    for(int idx = 0; idx < 3; ++idx) { 
        // 打印虚函数的地址
        cout << pvtable[idx] << endl; 
    } 
    
    cout << "----- 调用虚函数表中的虚函数,不传 this 指针 -----" << endl;
    // 二级指针,指向派生类对象地址的地址,即虚函数的地址
    long **pVtable = (long **)&d; 
    typedef void(* Function)(); 
    for(int idx = 0; idx < 3; ++idx) { 
        // 回调虚函数,没有传this指针
        Function f = (Function)pVtable[0][idx]; 
        f(); 
    } 
    
	cout << "----- 调用虚函数表中的虚函数,传入 this 指针-----" << endl;
	typedef void (*Function2)(Derived*);
	for(int idx = 0; idx < 3; ++idx) { 
        // 回调虚函数,传入this指针
        Function2 f2 = (Function2)pVtable[0][idx];
        f2(&d);
	} 	
} 

int main(void) {
	test();
	return 0;
}

2.3、虚函数的访问

  • 指针访问:基类的指针指向派生类对象,动态联编,体现多态
  • 引用访问:基类的引用绑定派生类对象,动态联编,体现多态
  • 成员函数访问: this 指针调用虚函数,表现动态多态
  • 对象访问:对象调用虚函数,静态联编,不体现多态
  • 构造函数或析构函数:静态联编,只会调用自己的虚函数

测试在构造函数或析构函数中,调用虚函数

#include <iostream>

using std::cout;
using std::endl;

class Grandpa {
public:
    Grandpa() {
        cout << "Grandpa()" << endl;
    }

    virtual void func1() {
        cout << "Grandpa::func1()" << endl;
    }

    virtual void func2() {
        cout << "Grandpa::func2()" << endl;
    }

    ~Grandpa() {
        cout << "~Grandpa()" << endl;
    }
};

class Father: public Grandpa {
public:
    Father() {
        cout << "Father()" << endl;
        func1(); // 构造函数调用虚函数
    }

    virtual void func1() {
        cout << "Father::func1()" << endl;
    }

    virtual void func2() {
        cout << "Father::func2()" << endl;
    }

    ~Father() {
        cout << "~Father()" << endl;
        func2(); // 析构函数调用虚函数
    }
};

class Son: public Father {
public:
    Son() {
        cout << "Son()" << endl;
    }

    virtual void func1() {
        cout << "Son::func1()" << endl;
    }

    virtual void func2() {
        cout << "Son::func2()" << endl;
    }

    ~Son() {
        cout << "~Son()" << endl;
    }
};

int main() {
    Son son; // 栈对象,自动销毁
    return 0;
}

测试结果:构造函数或析构函数中调用虚函数,表现的是静态联编,只会调用自己的虚函数

/* 
构造过程:grandfather -> father -> son 
析构过程:~son-> ~father -> ~grandfather
*/
Grandpa()
Father()
Father::func1() // son还未创建,此时只能调用father的func1
Son() 
~Son()
~Father() 
Father::func2() // son已经销毁,此时只能调用father的func2
~Grandpa()

2.4 、纯虚函数

纯虚函数:只有声明,没有实现,作为函数接口存在。

virtual 返回类型 函数名(参数列表) = 0; 
2.4.1、抽象类

抽象类作为函数接口存在,不能创建对象,但可以创建抽象类的指针和引用

抽象类的形式

  • 纯虚函数:声明了纯虚函数的类,就是抽象类。若派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。
  • protected 修饰构造函数的类。可以派生新类,但不能创建对象。
// 基类定义为抽象类,不能创建基类对象
class Base { 
protected:
    Base(long base): _base(base) {} 
protected: 
    long _base; 
};

class Derived : public Base {
public: 
    Derived(long base, long derived) 
    : Base(base) // 可以调用基类的构造函数,创建派生类
    , _derived(derived) {}
private: 
    long _derived; 
}
2.4.2、开闭原则

面向对象的设计原则:开闭原则

特点:对扩展开放,对修改关闭

测试

#include <math.h>
#include <iostream>
using std::cout;
using std::endl;

// 抽象类:纯虚函数作为函数接口
class Figure {
public:
    virtual void display() const = 0;
    virtual double area() const = 0;
};

// 通过引用访问虚函数,表现动态多态
void func(const Figure &fig) {
    fig.display();
    cout << "'s area is : " << fig.area() << endl;
}

class Rectangle: public Figure {
public:
    Rectangle(double length = 0, double width = 0)
    : _length(length)
    , _width(width)
    { cout << "Rectangle(double = 0, double = 0)" << endl;}

    void display() const override {
        cout << "Rectangle ";
    }

    double area() const override {
        return _length * _width;
    }

    ~Rectangle() {
        cout << "~Rectangle()" << endl;
    }

private:
    double _length;
    double _width;
};

class Circle: public Figure {
public:
    Circle(double radius = 0)
    : _radius(radius) 
    { cout << "Circle(double = 0)" << endl; }

    void display() const override {
        cout << "Circle ";
    }

    double area() const override {
        return _radius * _radius * 3.14159;
    }

    ~Circle() {
        cout << "~Circle()" << endl;
    }
private:
    double _radius;
};

class Triangle
: public Figure
{
public:
    Triangle(double a = 0, double b = 0, double c = 0)
    : _a(a)
    , _b(b)
    , _c(c)
    { cout << "Triangle(double = 0, double = 0, double = 0)" << endl; }

    void display() const override {
        cout << "Triangle " ;
    }

    double area() const override {
        double tmp = (_a + _b + _c)/2;
        return sqrt(tmp * (tmp - _a) * (tmp - _b) * (tmp - _c));
    }

    ~Triangle() {
        cout << "~Triangle()" << endl;
    }

private:
    double _a;
    double _b;
    double _c;
};

int main(int argc, char **argv) {
    Rectangle rectangle(10, 12);
    Circle circle(10);
    Triangle triangle(3, 4, 5);

    cout << endl;
    func(rectangle);
    func(circle);
    func(triangle);
    
    return 0;
}

2.5、虚析构函数

多态的问题:如果一个基类的指针指向派生类的对象,当 delete 该指针释放派生类对象,系统只会执行基类的析构函数,不会执行派生类的析构函数,发生内存泄漏。

为了防止内存泄漏,只要基类中定义了虚函数,必须将基类的析构函数设置为虚函数,派生类的析构函数自动成为为虚函数。

基类和派生类的函数名虽然看起来不同,不符合重写规则,但实际上每个类只有一个析构函数,编译器将析构函数名统一解释为 destructor,实现重写。

例:父类是虚析构函数,子类重写了父类的析构函数,当 delete base 指针时 pbase->~destructor即重写后的子类析构函数,子类析构后,调用父类的析构函数。

#include <string.h>
#include <iostream>

using std::cout;
using std::endl;

class Base {
public:
    Base(const char *pbase)
    : _pbase(new char[strlen(pbase) + 1]())
    {
        cout << "Base(const char *)" << endl;
        strcpy(_pbase, pbase);
    }
    // 将基类的析构函数声明为虚函数:~destructor
    virtual ~Base() {
        cout << "~Base()" << endl;
        if(_pbase) {
            delete [] _pbase;
            _pbase = nullptr;
        }
    }
private:
    char *_pbase;
};

class Derived: public Base {
public:
    Derived(const char *pbase, const char *pderived)
    : Base(pbase)
    , _pderived(new char[strlen(pderived) + 1]())
    {
        cout << "Derived(const char *, const char *)" << endl;
        strcpy(_pderived, pderived);
    }
	// 重写发生在派生类的析构函数: ~destructor
	// 基类的析构函数虚化 -> 派生类的析构函数虚化 -> 名字相同发生重写
    ~Derived() {
        cout << "~Derived()" << endl;
        if(_pderived) {
            delete [] _pderived;
            _pderived = nullptr;
        }
    }
private:
    char *_pderived;
};
int main() {
    Base *pbase = new Derived("hello", "world");
    
    // 执行析构函数 pbase->~destructor()
    // 先执行派生类对象的析构函数,再执行基类(Base*)的析构函数
    delete pbase;
        
    return 0;
}

2.6、重载 覆盖 隐藏

  • 重载:同一个作用域,函数名相同,参数列表不同。

  • 覆盖 | 重定义 | 重写:基类与派生类中的虚函数,函数名相同,参数列表相同。

  • 隐藏:基类与派生类,函数名相同,派生类屏蔽了基类的同名数据成员。使用基类的作用域才能访问到其同名函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值