自学内容网 自学内容网

C++ 多态

目录

多态概念

编译时多态(静态多态)

运行时多态(动态多态)

多态实现

        条件

        虚函数

        虚函数的覆写

 协变

 析构函数的重写

 override与final的关键字

重载(Overloading)

重写(Overriding,覆盖)

隐藏(Hiding)

纯虚函数与抽象类

 多态的原理

        虚函数表指针

         注意:

​编辑         多态的原理

        动态绑定与静态绑定

虚函数表


多态概念

        多态性是面向对象编程中的一个核心概念,它允许对象通过统一的接口表现出不同的行为。多态性增强了程序的灵活性和可扩展性。

编译时多态(静态多态)

        编译时多态,也称为静态多态或早绑定(early binding),是在编译阶段确定调用哪个函数或方法。这主要通过函数重载和模板来实现。

  • 函数重载:允许在同一个作用域内定义多个同名函数,但这些函数的参数类型或参数个数必须不同。编译器在编译时根据函数调用时提供的参数类型和数量来确定调用哪个版本的函数。

  • 模板:允许程序员编写与类型无关的代码。模板函数或模板类在编译时被实例化,编译器根据提供的类型参数生成具体的函数或类实现。

运行时多态(动态多态)

        运行时多态,也称为动态绑定(dynamic binding)或晚绑定(late binding),是在运行时确定调用哪个函数或方法。这主要通过继承和虚函数来实现。

  • 继承:允许一个类(子类或派生类)继承另一个类(基类或父类)的属性和方法。子类可以重写基类中的虚函数,从而在运行时表现出不同的行为

  • 虚函数:在基类中声明为virtual的成员函数。当通过基类指针或引用调用虚函数时,如果指针或引用实际上指向的是派生类对象,那么将调用派生类中重写的虚函数版本。这种机制允许在运行时根据对象的实际类型来确定调用哪个版本的函数。

多态实现

       多态需要继承关系,在继承关系下调用同一函数产生不同行为。

class A
{
public:
virtual void P(int a = 0)
{
cout << a << "A" << endl;
}
};

class B :public A
{
public:
virtual void P(int a = 1)
{
cout << a <<"B" << endl;
}
};

void C(A* it)
{
it->P();
}

int main()
{
A a;
B b;
C(&a);
C(&b);
return 0;
}

        条件

  • 必须是基类的指针或者引用来调用虚函数
  • 被调用的必须是虚函数且完成了虚函数的重写与覆盖

        如上文给出的对A的virtual void P()是虚函数而且完成了覆写  

        虚函数

虚函数就是在类成员函数前加virtua修饰(非成员函数不可以加virtual)

class A
{
public:
virtual void P(int a = 0)
{
cout << a << "A" << endl;
}
};

class B :public A
{
public:
virtual void P(int a = 1)
{
cout << a <<"B" << endl;
}
};

        虚函数的覆写

虚函数的覆写需要满足

  • 派生类和基类拥有完全相同的虚函数(返回值 参数名 参数列表)

但是注意

  • 派生类继承基类的虚函数时,你参数列表是完全继承基类的(就括号里面的玩意,就是基类里面是 int i = 10,你派生类里面即使int i = 5也会继承成int i = 10)

还有就是 派生类没有virtual不影响派生类的虚函数,但是基类必须有

 

class A
{
public:
virtual void P(int a = 0)
{
cout << a << "A" << endl;
}
};

class B :public A
{
public:
void P(int a = 1)//这里打印出来的a是0 如果按照多态来调用
{
cout << a << "B" << endl;
}
};

 协变

        就是派生类与基类的虚函数在写的时候各自返回对自己的指针或者引用

class A
{
public:
virtual A& P(int a = 0)
{
cout << a << "A" << endl;
return *this;
}
virtual A* G(int a = 0)
{
cout << a << "A" << endl;
return this;
}
};

class B :public A
{
public:
virtual B& P(int a = 1)
{
cout << a << "B" << endl;
return *this;
}
virtual B* G(int a = 1)
{
cout << a << "B" << endl;
return this;
}
};

 析构函数的重写

        基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否有virtual关键字,都构成了与基类的析构函数的重写     

class A
{
public:
~A()//这里不构成虚函数 如果以A*调用只会清除掉A内的玩意,B申请的玩意会清理不完全
{

}
};

class B :public A
{
public:
~B()
{

}
private:
string* T;
};

对于A

class A
{
public:
virtual ~A()
{

}
};

 override与final的关键字

        override是用来检测是否构成了虚函数的重写

        final是拒接重写虚函数

class A
{
public:
virtual void P(int a = 0)
{
cout << a << "A" << endl;
}
virtual void L() final {}
};

class B :public A
{
public:
virtual void P(int a = 1) override
{
cout << a <<"B" << endl;
}
};

​           重载/重写/隐藏特性

重载(Overloading)

        定义
        重载是指在同一个作用域内,允许存在多个同名但参数列表不同的函数或运算符。这些重载的函数或运算符通过参数列表的不同来区分。

        特点

  • 函数名相同。
  • 参数列表不同(参数数量或参数类型不同)。
  • 返回类型可以不同,但返回类型不是重载的决定因素。
  • 可以是成员函数、全局函数或友元函数。
  • 编译器在编译时根据参数列表来确定调用哪个重载的函数或运算符。

重写(Overriding,覆盖)

         定义
        重写(覆盖)是指在派生类中重新定义基类中的虚函数。这意味着当通过基类指针或引用来调用虚函数时,如果指针或引用实际上指向的是派生类对象,那么将调用派生类中重写的虚函数版本。

        特点

  • 基类函数必须是虚函数(用virtual关键字声明)。
  • 派生类中的函数必须与基类中的虚函数具有相同的函数名、返回类型和参数列表。
  • 派生类函数可以修改基类虚函数的实现。
  • 在C++11及以后的版本中,可以使用override关键字来显式指定重写基类虚函数,这有助于编译器检查重写是否正确。

隐藏(Hiding)

        定义
        隐藏是指派生类中的函数或成员变量隐藏了基类中的同名函数或成员变量。当通过派生类对象或派生类指针来访问这些被隐藏的成员时,将访问派生类中的版本,而不是基类中的版本。

        特点

  • 隐藏可以发生在成员函数、成员变量以及类型定义(如嵌套类)上。
  • 当派生类中的函数或成员变量与基类中的同名成员具有相同的名称时,基类中的成员将被隐藏。
  • 隐藏不同于重写,因为隐藏不涉及虚函数和动态绑定。隐藏是静态的,即在编译时就已经确定访问的是哪个成员。
  • 隐藏可能会导致意外的行为,因为它可能会使程序员误以为他们正在访问基类中的成员,而实际上他们正在访问派生类中的隐藏成员。因此,在编写继承代码时应小心避免隐藏。

纯虚函数与抽象类

  • 只需要在虚函数后面给个 ‘=0’ 就构成了纯虚函数
  • 纯虚函数不需要定义实现,只需要声明
  • 包含纯虚函数的类是抽象类
  • 继承抽象类的派生类不重写纯虚函数就还是抽象类
  • 抽象类不能实例化
class A//无法实例化
{
public:
virtual void T() = 0 {cout << "我是抽象类" << endl;}//无法调用
};

class B :public A
{
public:
virtual void T() { cout << "我不是抽象类"<<endl;}
};

int main()
{
B b;
b.T();
return 0;
}

 多态的原理

        虚函数表指针

虚函数表

  • 当类中包含虚函数时,编译器会生成一个虚函数表。这个表包含了类中所有虚函数的地址。
  • 每个类都有一个独立的虚函数表,即使它们之间是通过继承关系相关的。

虚函数表指针

  • 在每个包含虚函数的类的对象实例中,编译器会插入一个指向该类虚函数表的指针(_vfptr)。
  • 这个指针通常位于对象的开始位置(但也可能根据平台的不同位于对象的末尾)。

        这个虚函数表的位置在每个类里面的成员变量里,而且是先完全继承基类的,然后再修改复写的,也就是没有覆写的和基类的指向同一个函数地址,覆写的则指向自己的函数地址

         注意:

        虚数数表指针也占空间,大小是指针的大小

class A
{
public:
virtual void T(){}

int _a;
};

class B:public A
{
public:
virtual void T() { cout << "我覆写啦"; }

int _b;
char _c;
};

int main()
{
cout << sizeof(A) << ' ' << sizeof(B) << endl;
return 0;
}

        这个_vfptr是可以被看到的

         多态的原理

        多态是通过,根据你提供的ptr判断执行的

  • 当指向的基类的时候,基类通过自己的_vfptr去指向虚函数表,然后指向要对应调用的虚函数,这里指向的都是父类
  • 当指向派生类的时候,基类的指针对派生类产生了切割,但是这个指针指向的是派生类的_vfptr指向的虚函数表,也就是派生类自己的,派生类通过自己的虚函数表找到覆写的虚函数执行

        动态绑定与静态绑定

  • 对不满足多态条件的函数,是编译时绑定,也就是静态绑定
  • 满足多态的是运行时绑定,是动态绑定 

        特殊:

 

class A
{
public:
virtual void T(){}
virtual void G(){}//这个没有覆写 
int _a;
};

class B:public A
{
public:
virtual void T() { cout << "我覆写啦"; }
virtual void L() {}//这没有前置的函数
int _b;
char _c;
};

 

  • 覆写的引用的是 B::T 
  • 没覆写的引用的 A::G
  • 这个L也是再_vfptr内 vs没有展示出来  引用的是B::L

虚函数表

  • 基类的虚函数表存放基类的虚函数地址,而且各个实例化都是独立的

  • 派生类的虚函数表寄存基类和自己的虚函数地址

  • 在vs中虚函数表的最后一个地址下面内容是0x00000000作为标记(g++不会标记)

  • 虚函数储存的知识地址,真正的代码放在代码段内 


原文地址:https://blog.csdn.net/forccct/article/details/145266436

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!