C++多态的实现原理

一、多态的概念

1)多态就是多种形态,C++的多态分为静态多态与动态多态。

静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。

动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

2)在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

如果对象类型是子类,就调用子类的函数;如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。

例:

class Person   
{   
public :       
    virtual void BuyTickets()      
    {           
    cout<<" 买票"<< endl;     
    }  
protected :      
    string _name ;   // 姓名   
};  
  
class Student : public Person   
{   
public :  
     virtual void BuyTickets()    //父类写了virtual,子类可写可不写virtual,意思是一样的。一般情况下都写
     {           
     cout<<" 买票-半价 "<<endl ;  
     }  
protected :  
     int _num ;   //学号  
};  
  
//void Fun(Person* p)   
void Fun (Person& p)   //leaf理解:父类的数据类型(Person),可以接受任何子类类型的赋值。
{  
     p.BuyTickets ();  
}  
  
void Test ()  
 {  
     Person p ;  
     Student s ;  
     Fun(p );        //多态:如果对象类型是子类,就调用子类的函数;如果对象类型是父类,就调用父类的函数
     Fun(s );  
}  

二、多态的实现原理(重点)

一个接口,多种方法

  1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。

  2. 存在虚函数的类都有一个一维的虚函数表叫做虚表当类中声明虚函数时,编译器会在类中生成一个虚函数表。

  3. 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。

  4. 虚函数表是一个存储类成员函数指针的数据结构。

  5. 虚函数表是由编译器自动生成与维护的。

  6. virtual成员函数会被编译器放入虚函数表中。

  7. 当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要在base指针中,找到vptr指针即可)。

  8. vptr一般作为类对象的第一个成员。

三、探索虚表

虚表是通过一块连续的内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在这样一张虚函数表,它就像一张地图,指向了实际调用的虚函数。

例1:

class Base
 {
 public :
     virtual void func1()
    {}
     virtual void func2()
    {}
 private :
     int a ; 
};
 
void Test1 ()
 {
     Base b1;
 }

图示:增加了virtual关键字的类就会有一个虚表,派生类继承以后也会有。

在这里插入图片描述
例2:单继承对象模型

class Base
{
public :
     virtual void func1()
	 {
		 cout<<"Base::func1" <<endl;
	 }
     virtual void func2()
	 {
		 cout<<"Base::func2" <<endl;
	 } 
private :
     int a ;
};
 
class Derive :public Base
 { 
 public :
	virtual void func1()
    {
		cout<<"Derive::func1" <<endl;
	}
     virtual void func3()
	{ 
	cout<<"Derive::func3" <<endl;
	}
    virtual void func4()
	{
		 cout<<"Derive::func4" <<endl;
	}
private :
     int b ;
};
 
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
 {
     cout<<" 虚表地址>"<< VTable<<endl ;
	 
     for (int i = 0; VTable[i ] != 0; ++i)
		{
			 printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]);
			 FUNC f = (FUNC) VTable[i ];
			 f();
		}
     cout<<endl ;
}
 
void Test1 ()
 {
     Base b1 ;
     Derive d1 ;
     int* VTable1 = (int*)(*( int*)&b1 );
     int* VTable2 = (int*)(*( int*)&d1 );
     PrintVTable(VTable1 );
     PrintVTable(VTable2 );
}

可以看到派生类Derive::func1重写基类Base::func1,覆盖了相应虚表位置上的函数。 Base::func2可以看到这里没有看到派生类。Derive中的func3和func4,这两个函数就放在func2的后面,这里没有显示是VS的问题(bug)。

图示:继承后派生类的虚表(函数名相同,子类会覆盖基类的函数)
在这里插入图片描述

例3:多继承对象模型

class Base1
 {
 public :
     virtual void func1()
    {
          cout<<"Base1::func1" <<endl;
    }
     virtual void func2()
    { 
         cout<<"Base1::func2" <<endl;
    }
private :
     int b1 ;
 };
 
class Base2
 { 
public :
     virtual void func1()
    {
          cout<<"Base2::func1" <<endl;
    }
     virtual void func2()
    {
         cout<<"Base2::func2" <<endl;
   }
private :
     int b2 ;
 };
 
class Derive : public Base1, public Base2
 { 
public :
virtual void func1()
    { 
         cout<<"Derive::func1" <<endl;
    }
     virtual void func3()
   { 
         cout<<"Derive::func3" <<endl;
    }
private :
     int d1 ; 
};
 
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
 {
     cout<<" 虚表地址>"<< VTable<<endl ;
 
     for (int i = 0; VTable[i ] != 0; ++i)
    { 
         printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]); 
         FUNC f = (FUNC) VTable[i ];
          f();
    }
    cout<<endl ;
 }
 
void Test1 ()
 { 
    Derive d1 ;
    int* VTable = (int*)(*( int*)&d1 );
    PrintVTable(VTable );
     // Base2虚函数表在对象Base1后面    
    VTable = (int *)(*((int*)&d1 + sizeof (Base1)/4));
     PrintVTable(VTable );
 }

在这里插入图片描述

四、一些考题

为什么调用普通函数比调用虚函数的效率高?
因为普通函数是静态联编的,而调用虚函数是动态联编的。

联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。

静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。

为什么要用虚函数表(存函数指针的数组)?

实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。
同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。
为什么要把基类的析构函数定义为虚函数?

在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.如下代码:

#include<iostream>  
using namespace std;  
  
class Base  
{  
public:  
    Base() {};  
    ~Base()   
    {  
        cout << "delete Base" << endl;  
    };  
};  
  
class Derived : public Base  
{  
public:  
    Derived() {};  
    ~Derived()  
    {  
        cout << "delete Derived" << endl;  
  
    };  
};  
int main()  
{  
    //操作1  
    Base* p1 = new Derived;  
    delete p1;  
    //因为这里子类的析构函数重写了父类的析构函数,虽然子类和父类的析构函数名不一样,  
    //但是编译器对析构函数做了特殊的处理,在内部子类和父类的析构函数名是一样的。  
    //所以如果不把父类的析构函数定义成虚函数,就不构成多态,由于父类的析构函数隐藏了子类  
    //的析构函数,所以只能调到父类的析构函数。  
    //但是若把父类的析构函数定义成虚函数,那么调用时就会直接调用子类的析构函数,  
    //由于子类析构先要去析构父类,在析构子类,这样就把子类和继承的父类都析构了  
  
    system("pause");  
}  

为什么子类和父类的函数名不一样,还可以构成重写呢?
因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。

五、虚析构函数(防止没有析构到子类导致内存泄漏)

我们知道,有时会让一个基类指针指向用 new 运算符动态生成的派生类对象;同时,用 new 运算符动态生成的对象都是通过 delete 指向它的指针来释放的。如果一个基类指针指向用 new 运算符动态生成的派生类对象,而释放该对象时是通过释放该基类指针来完成的,就可能导致程序不正确。

例如下面的程序:

#include <iostream>
using namespace std;
class CShape  //基类
{
public:
    ~CShape() { cout << "CShape::destrutor" << endl; }
};
class CRectangle : public CShape  //派生类
{
public:
    int w, h;  //宽度和高度
    ~CRectangle() { cout << "CRectangle::destrutor" << endl; }
};
int main()
{
    CShape* p = new CRectangle;
    delete p;  //只根据 p 的类型是 CShape * 来决定应该调用 CShape 类的析构函数,这样就无法析构到,子类的析构函数
    return 0;
}

程序的输出结果如下:

CShape::destrutor

输出结果说明,delete p;只引发了 CShape 类的析构函数被调用,没有引发 CRectangle 类的析构函数被调用。这是因为该语句是静态联编的,编译器编译到此时,不可能知道此时 p 到底指向哪个类型的对象,它只根据 p 的类型是 CShape * 来决定应该调用 CShape 类的析构函数。

按理说,delete p;会导致一个 CRectangle 类的对象消亡,应该调用 CRectangle 类的析构函数才符合逻辑,否则有可能引发程序的问题。

例如,假设程序需要对 CRetangle 类的对象进行计数,如果此处不调用 CRetangle 类的析构函数,就会导致计数不正确。

再如,假设 CRectangle 类的对象在存续期间进行了动态内存分配,而释放内存的操作都是在析构函数中进行的,如果此处不调用 CRetangle 类的析构函数,就会导致被释放的对象中动态分配的内存以后再也没有机会回收。

综上所述,人们希望delete p;这样的语句能够聪明地根据 p 所指向的对象执行相应的析构函数。实际上,这也是多态。为了在这种情况下实现多态,C++ 规定,需要将基类的析构函数声明为虚函数,即虚析构函数。

改写上面程序中的 CShape 类,在析构函数前加 virtual 关键字,将其声明为虚函数:

class CShape{
public:
    virtual ~CShape() { cout << "CShape::destrutor" << endl; }
};

则程序的输出变为:

CRectangle::destrutor
CShape::destrutor

说明 CRetangle 类的析构函数被调用了。实际上,派生类的析构函数会自动调用基类的析构函数。

只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数。

一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。

析构函数可以是虚函数,但是构造函数不能是虚函数。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在信号处理领域,DOA(Direction of Arrival)估计是一项关键技术,主要用于确定多个信号源到达接收阵列的方向。本文将详细探讨三种ESPRIT(Estimation of Signal Parameters via Rotational Invariance Techniques)算法在DOA估计中的实现,以及它们在MATLAB环境中的具体应用。 ESPRIT算法是由Paul Kailath等人于1986年提出的,其核心思想是利用阵列数据的旋转不变性来估计信号源的角度。这种算法相比传统的 MUSIC(Multiple Signal Classification)算法具有较低的计算复杂度,且无需进行特征值分解,因此在实际应用中颇具优势。 1. 普通ESPRIT算法 普通ESPRIT算法分为两个主要步骤:构造等效旋转不变系统和估计角度。通过空间平移(如延时)构建两个子阵列,使得它们之间的关系具有旋转不变性。然后,通过对子阵列数据进行最小二乘拟合,可以得到信号源的角频率估计,进一步转换为DOA估计。 2. 常规ESPRIT算法实现 在描述中提到的`common_esprit_method1.m`和`common_esprit_method2.m`是两种不同的普通ESPRIT算法实现。它们可能在实现细节上略有差异,比如选择子阵列的方式、参数估计的策略等。MATLAB代码通常会包含预处理步骤(如数据归一化)、子阵列构造、旋转不变性矩阵的建立、最小二乘估计等部分。通过运行这两个文件,可以比较它们在估计精度和计算效率上的异同。 3. TLS_ESPRIT算法 TLS(Total Least Squares)ESPRIT是对普通ESPRIT的优化,它考虑了数据噪声的影响,提高了估计的稳健性。在TLS_ESPRIT算法中,不假设数据噪声是高斯白噪声,而是采用总最小二乘准则来拟合数据。这使得算法在噪声环境下表现更优。`TLS_esprit.m`文件应该包含了TLS_ESPRIT算法的完整实现,包括TLS估计的步骤和旋转不变性矩阵的改进处理。 在实际应用中,选择合适的ESPRIT变体取决于系统条件,例如噪声水平、信号质量以及计算资源。通过MATLAB实现,研究者和工程师可以方便地比较不同算法的效果,并根据需要进行调整和优化。同时,这些代码也为教学和学习DOA估计提供了一个直观的平台,有助于深入理解ESPRIT算法的工作原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值