【七、多态】动/静态联编、虚析构函数、虚函数(虚函数表与VPTR指针)、重写与重定义

简介: 【七、多态】动/静态联编、虚析构函数、虚函数(虚函数表与VPTR指针)、重写与重定义

前言

面向对象有三大特点:封装、继承、多态。封装可以把属性和方法封装在一个类中,这样当类对象做函数参数时即可以属性也可以使用方法;继承可以实现对代码的复用;而多态则实现了接口和功能的解耦合。


一、问题引出,为什么要有多态?

当我们在子类和父类定义了同名函数时(函数重写),并把子类对象传给父类指针或引用时,我们调用该同名函数,最终都是调用父类的成员函数,这是由于类型兼容性原则导致的(使用public继承时,子类可以当作父类使用)。但是我们现在希望编译器能够自动判断我们传入的是子类还是父类,并调用相应的成员函数。

class Parent
{
public:
  void func()
    {
        ;
    }
};
class Child
{
public:
  void func()
    {
        ;
    }
};
//void test(Parent& p)
void test(Parent* p)
{
    //p.func();
    p->func(); //传入子类对象,依然调用父类的 func 成员
}
int main()
{
    Child c;
    // test(&c);
    test(c);
    return 0;
}

二、多态的基础知识

1.类型兼容性原则

类型兼容性原则是指:当派生类是公有制继承时,派生类的对象可以当作基类的对象使用(但是基类不能当派生类使用)。这包括

子类对象传递给父类的引用;

子类对象传递给父类的指针;

用子类对象给父类对象赋值;

子类对象初始化父类;

#include <iostream>
using namespace std;
class A
{
public:
  void ptint_a()
  {
    cout << "===a===" << endl;
  }
protected:
private:
};
class B : public A
{
public:
  void ptint_b()
  {
    cout << "===b===" << endl;
  }
protected:
private:
};
void func1(A* a)
{
  a->ptint_a();
}
void func2(A& a)
{
  a.ptint_a();
}
/*
void func2(B& b)
{
  b.ptint_b();
}*/
void func3(B& b)
{
  b.ptint_b();
}
int main()
{
  A a1;
  B b1;
  A a2 = b1; //子类对象初始化父类//会调用拷贝构造函数
  func1(&b1); //子类对象传递给父类的指针
  func2(b1);//会优先匹配 func2(B& b)//子类对象传递给父类的引用
  //func3(a1); 基类不能当派生类用
  a1 = b1; //可以直接用子类对象给父类对象赋值
  system("pause");
  return 0;
}

2.重载重写重定义

  • 重载
    同一个类中,函数名相同,参数类型、个数不同;
  • 重写
    在子类与父类中,父类与子类的函数原型完全相同;
    有 virtual 关键字,虚函数重写,多态;
    无 virtual 关键字,重定义;
  • 名称覆盖
    当父类和子类有相同的函数名、变量名出现,发生名称覆盖,子类的函数名,覆盖了父类的函数名。
#include <iostream>
using namespace std;
class MyClassA
{
public:
  void PrintFunc()
  {
    cout << "MyClassA 无参函数" << endl;
  }
  void PrintFunc(int a)
  {
    cout << "MyClassA 一个参数" << endl;
  }
  virtual void PrintFunc(int a, int b)
  {
    cout << "MyClassA 两个参数" << endl;
  }
};
class MyClassB : public MyClassA
{
public:
  void PrintFunc(int a)
  {
    cout << "MyClassB 一个参数" << endl;
  }
  virtual void PrintFunc(int a, int b)
  {
    cout << "MyClassB 两个参数" << endl;
  }
};
/*  MyClassA 和 MyClassB 的函数 void PrintFunc(int a)  属于重写(重定义)
* MyClassA 和 MyClassB 的函数 virtual void PrintFunc(int a, int b)  属于虚函数重写(多态)
* MyClassA 的 void PrintFunc() 在子类中被名称覆盖,子类中无法直接引用
* MyClassA 中的三个 PrintFunc() 属于函数重载
* MyClassB 中的两个 PrintFunc() 属于函数重载
*/
int main()
{
  MyClassB b1;
  //b1.PrintFunc(); //错误  C2661 “MyClassB::PrintFunc”: 没有重载函数接受 0 个参数 
  //因为子类中也有名为 PrintFunc 的函数,所以他把父类中的同名函数覆盖了,编译器只会在
  //子类中查找 PrintFunc 函数,当找不到匹配的参数时,就会报没有重载函数
  b1.MyClassA::PrintFunc();
  system("pause");
  return 0;
}

3.动态联编与静态联编

C++与C语言都是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象。从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

联编是指一个程序模块、代码之间互相关联的过程。

静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。比如重载函数使用静态联编(在编译的时候就决定了怎么执行,函数怎么调用)。

动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。比如switch 语句和 if 语句是动态联编的例子(在程序执行的过程中才知得调用哪一个,根据类型决定)。

通过对重写函数加 virtual 关键字来实现动态联编。

三、多态案例

#include <iostream>
using namespace std;
class GetArea //抽象类//抽象类不能建立对象
{
public:
  virtual double get_area() = 0; //纯虚函数
};
class Square : public GetArea
{
public:
  Square(int a, int b)
  {
    this->a = a;
    this->b = b;
  }
public:
  virtual double get_area()
  {
    return a * b;
  }
private:
  int a;
  int b;
};
class Circular : public GetArea
{
public:
  Circular(int r)
  {
    this->r = r;
  }
public:
  virtual double get_area()
  {
    return 3.14 * r * r;
  }
private:
  int r;
};
class Triangle : public GetArea
{
public:
  Triangle(int h, int l)
  {
    this->h = h;
    this->l = l;
  }
public:
  virtual double get_area()
  {
    return 0.5 * h * l;
  }
private:
  int h;
  int l;
};
void PrintArea(GetArea& obj)//同一调用语句表现出不同状态
{
  cout << obj.get_area() << endl;
}
int main()
{
  Square    s(3, 4);
  Circular  c(10);
  Triangle  t(5, 2);
  PrintArea(s);
  PrintArea(c);
  PrintArea(t);
  system("pause");
  return 0;
}

C语言实现多态效果的案例可见

C语言实现多态效果

四、虚函数

1.虚函数表与 vptr 指针

如果类中有虚函数,那么在使用该类定义对象的时候,会创建一个虚函数表,虚函数表存放了虚函数的入口地址,而 vptr 指向这个虚函数表,父类的vptr指针指向父类的虚函数表,子类的vptr指针指向子类的虚函数表(虚函数的入口地址都在这个虚函数表中)。动态联编就是这么实现的,根据vptr指针决定调用哪个虚函数,当遇到虚函数调用时,会根据vptr指针找到虚函数表,并在虚函数表中查找函数原型。

vptr指针初始化是分步实现的

1.使用子类定义一个子类对象,会初始化子类的vptr指针

2.当父类的构造函数执行时,子类的 vptr 指针会指向父类的虚函数表

3.当父类的构造函数执行完毕时,子类的 vptr 指针指向子类的虚函数表

通过一个例子可以证明vptr指针的分步初始化

#include <iostream>
using namespace std;
class Class1
{
public:
  Class1()
  {
    print_func();
    cout << "class 1 构造" << endl;
  }
  virtual void print_func()
  {
    cout << "class 1" << endl;
  }
};
class Class2 : public Class1
{
public:
  Class2()
  {
    print_func();
    cout << "class 2 构造" << endl;
  }
  virtual void print_func()
  {
    cout << "class 2" << endl;
  }
};
int main()
{
  Class2 c2;
  system("pause");
  return 0;
}

编译运行

可以看到,在Class1构造函数中调用了Class1的print_func函数,这说明此时vptr指针指向了Class1的虚函数表;在Class2构造函数中调用了Class2的print_func函数,说明此时vptr指针指向了Class2的虚函数表。

2.虚析构函数

虚析构函数是指,在父类析构函数加 virtual 关键字,通过父类指针,执行所有子类的析构函数,释放所有子类内存。

应用场景:一个函数的参数是基类指针,在主调函数 new 分配了内存,需要在被调函数中使用完该对象后 delete

释放内存,我在调用这个函数时传入了一个派生类对象,如果基类中析构函数没有加 virtual 关键字,就会静态联编,delete

时只执行基类的析构函数,而没有执行派生类析构函数,造成了派生类对象中内存泄漏。虚析构函数会执行动态联编,把基类和派生类的析构函数都执行一遍。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class MyClassA
{
public:
  MyClassA(const char* str)
  {
    this->pA = new char[strlen(str) + 1];
    strcpy(this->pA, str);
    cout << "A 构造函数" << endl;
  } 
  //~MyClassA()
  virtual ~MyClassA()//虚析构函数
  {
    if (this->pA != NULL)
    {
      delete[] this->pA;
    }
    this->pA = NULL;
    cout << "A 析构函数 " << endl;
  }
private:
  char* pA;
};
class MyClassB : public MyClassA
{
public:
  MyClassB(const char* str) : MyClassA(str)
  {
    this->pB = new char[strlen(str) + 1];
    strcpy(this->pB, str);
    cout << "B 构造函数" << endl;
  }
  ~MyClassB()
  {
    if (this->pB != NULL)
    {
      delete[] this->pB;
    }
    this->pB = NULL;
    cout << "B 析构函数 " << endl;
  }
private:
  char* pB;
};
class MyClassC : public MyClassB
{
public:
  MyClassC(const char* str) : MyClassB(str)
  {
    this->pC = new char[strlen(str) + 1];
    strcpy(this->pC, str);
    cout << "C 构造函数" << endl;
  }
  ~MyClassC()
  {
    if (this->pC != NULL)
    {
      delete[] this->pC;
    }
    this->pC = NULL;
    cout << "C 析构函数 " << endl;
  }
private:
  char* pC;
};
void FuncTest(MyClassA* p)
{
  delete p;
}
int main()
{
  MyClassC* c = new MyClassC("hello C++");
  FuncTest(c);
  //delete c; //也会调用 C B A 的析构函数
  system("pause");
  return 0;
}

正常析构函数只会执行A(做函数参数的类)的析构函数,因为静态联编,编译器编译时并不知道具体要用哪个类的析构函数,所以只能选择函数参数的类的析构函数执行

将基类的析构函数加 virtual 关键字,即虚析构函数,将会依次调用 C B A(基类和所有派生类)的析构函数;动态联编,在执行时根据指针具体传入哪个类的对象决定如何调用


系列文章

六、继承


相关文章
|
3月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
存储 Cloud Native 编译器
C++ 虚函数表和虚函数表指针的创建时机
C++ 虚函数表和虚函数表指针的创建时机
|
存储 编译器 C++
【C++要笑着学】虚函数表(VBTL) | 观察虚表指针 | 运行时决议与编译时决议 | 动态绑定与静态绑定 | 静态多态与动态多态 | 单继承与多继承关系的虚表(二)
虚表是编译器的实现,而非C++的语言标准。上一章我们学习了多态的概念,本章我们深入探讨一下多态的原理。文章开头先说虚表指针,观察编译器的查表行为。首次观察我们先从监视窗口观察美化后的虚表 _vfptr,再透过内存窗口观察真实的 _vfptr。我们还会探讨为什么对象也能切片却不能实现多态的问题。对于虚表到底存在哪?我们会带着大家通过一些打印虚表的方式进行比对!铺垫完虚表的知识后,会讲解运行时决议与编译时决议,穿插动静态的知识点。文章的最后我们会探讨单继承与多继承的虚表,多继承中的虚表神奇的切片指针偏移问题,这块难度较大,后续我们会考虑专门讲解一下,顺带着把钻石虚拟继承给讲了
378 1
【C++要笑着学】虚函数表(VBTL) | 观察虚表指针 | 运行时决议与编译时决议 | 动态绑定与静态绑定 | 静态多态与动态多态 | 单继承与多继承关系的虚表(二)
|
存储 编译器 C++
【C++要笑着学】虚函数表(VBTL) | 观察虚表指针 | 运行时决议与编译时决议 | 动态绑定与静态绑定 | 静态多态与动态多态 | 单继承与多继承关系的虚表(一)
虚表是编译器的实现,而非C++的语言标准。上一章我们学习了多态的概念,本章我们深入探讨一下多态的原理。文章开头先说虚表指针,观察编译器的查表行为。首次观察我们先从监视窗口观察美化后的虚表 _vfptr,再透过内存窗口观察真实的 _vfptr。我们还会探讨为什么对象也能切片却不能实现多态的问题。对于虚表到底存在哪?我们会带着大家通过一些打印虚表的方式进行比对!铺垫完虚表的知识后,会讲解运行时决议与编译时决议,穿插动静态的知识点。文章的最后我们会探讨单继承与多继承的虚表,多继承中的虚表神奇的切片指针偏移问题,这块难度较大,后续我们会考虑专门讲解一下,顺带着把钻石虚拟继承给讲了
632 0
【C++要笑着学】虚函数表(VBTL) | 观察虚表指针 | 运行时决议与编译时决议 | 动态绑定与静态绑定 | 静态多态与动态多态 | 单继承与多继承关系的虚表(一)
|
Go
静态联编,动态联编,类指针之间的关系,虚函数与多态性,纯虚函数,虚析构函数
1.静态联编,是程序的匹配,连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。 2.动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编。switch语句和if语句是动态联编的例子。 #include&lt;iostream&gt; void go(int num) { } void go(char *str) { } //class //::在一个类中 cl
1245 0
|
5月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
1月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
22 0
|
2月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
|
3月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
|
3月前
|
C语言
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)