C++ 学习笔记7--多态(未完待续5/20)

思维导图:
C++ 学习笔记7--多态(未完待续5/20)_第1张图片
C++ 学习笔记7--多态(未完待续5/20)_第2张图片

C++ 学习笔记7--多态(未完待续5/20)_第3张图片
C++ 学习笔记7--多态(未完待续5/20)_第4张图片

多态简单概述:通常是指对于同一个消息、同一种调用,在不同的场合,针对不同的对象下,执行不同的行为 。
更通俗点:警车鸣笛,普通人反应一般,但逃犯听见会大惊失色,拔腿就跑。又比如说,…

文章目录

    • 1、为什么要用多态?
    • 2、虚函数的定义
    • 3、虚函数的实现机制
    • 4、哪些函数不能被设置为虚函数?
    • 5、虚函数的访问
    • 6、纯虚函数
    • 7、抽象类
    • 8、虚析构函数
    • 9、重载、隐藏、覆盖
    • 10、测试虚表的存在
    • 11、带虚函数的多基派生
    • 12、多基派生的二义性
    • 13、虚拟继承
    • 14、效率分析

1、为什么要用多态?

1)如果项目耦合度很高的情况下,维护代码时修改一个地方会牵连到很多地方,会无休止的增加开发成本。而降低耦合度,可以保证程序的扩展性。而多态对代码具有很好的可扩充性
2)C++支持两种多态性:编译时多态和运行时多态:

a)编译时多态:也称为静态多态,我们之前学习过的函数重载、运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为先期联编(early binding)。
b)运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding)。C++通过虚函数来实现动态联编。(我们提到的多态指的就是动态多态

2、虚函数的定义

1)什么是虚函数呢?虚函数就是在基类中被声明为virtual,并在一个或多个派生类中被重新定义的成员函数
2)具体形式:
C++ 学习笔记7--多态(未完待续5/20)_第5张图片
3)如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。派生类要对虚函数进行中可根据需重定义,但是重定义的格式有一定的要求:

与基类的虚函数有相同的参数个数;
与基类的虚函数有相同的参数类型;
与基类的虚函数有相同的返回类型

4)具体代码:

class Base
{
     
public:
  virtual void display()
 {
     
    cout << "Base::display()" << endl;
 }
 
  virtual void print()
 {
     
    cout << "Base::print()" << endl;
 }
};
class Derived
: public Base
{
     
public:
  virtual void display()
 {
     
    cout << "Derived::display()" << endl;
 }
};
void test(Base *pbase)
{
     
  pbase->display();
}
int main()
{
     
  Base base;
  Derived derived;
  test(&base);//调用基类的打印
  test(&derived);//调用派生类的打印
  return 0;
}

3、虚函数的实现机制

1)虚函数的实现是怎样的呢?简单来说,就是通过一张虚函数表(Virtual Fucntion Table)实现的
2)具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针(vfptr),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址。如下图
C++ 学习笔记7--多态(未完待续5/20)_第6张图片
3)当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖(override). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被基类的虚函数覆盖。
4)虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?从上面的例子,可以得出结论:

  1. 基类定义虚函数
  2. 派生类重定义(覆盖、重写)虚函数
  3. 创建派生类对象
  4. 基类的指针指向(引用绑定)派生类对象
  5. 基类指针(引用)调用虚函数

5)防止重写派生类与基类定义不一样,c++11中用override表示派生类对基类函数进行重写,就可以如果写错就报错 ,如:

virtual void print(int) override{
     
	cout<<"printf_derided"<<endl;
}

4、哪些函数不能被设置为虚函数?

  1. 普通函数(非成员函数):定义虚函数的主要目的是为了重写达到多态,所以普通函数声明为虚函数没有意义,因此编译器在编译时就绑定了它。
  2. 静态成员函数:静态成员函数对于每个类都只有一份代码,所有对象都可以共享这份代码,他不归某一个对象所有,所以它也没有动态绑定的必要。(静态函数发生在编译时,虚函数体现多态发生在运行时)
  3. 内联成员函数:内联函数本就是为了减少函数调用的代价,所以在代码中直接展开。但虚函数一定要创建虚函数表,这两者不可能统一。另外,内联函数在编译时被展开,而虚函数在运行时才动态绑定。
  4. 构造函数:这个原因很简单,主要从语义上考虑。因为构造函数本来是为了初始化对象成员才产生的,然而虚函数的目的是为了在完全不了解细节的情况下也能正确处理对象,两者根本不能“ 好好相处 ”。因为虚函数要对不同类型的对象产生不同的动作,如果将构造函数定义成虚函数,那么对象都没有产生,怎么完成想要的动作呢
  5. 友元函数:当我们把一个函数声明为一个类的友元函数时,它只是一个可以访问类内成员的普通函数,并不是这个类的成员函数,自然也不能在自己的类内将它声明为虚函数。(当友元函数是成员函数的时候是可以设置为虚函数的,比如在自己类里面设置为虚函数,但是作为另一个类的友元)

5、虚函数的访问

5.1、指针访问

使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。

C++ 学习笔记7--多态(未完待续5/20)_第7张图片

5.2、引用访问(引用的提出就是为了减少指针的使用)

使用引用访问虚函数,与使用指针访问虚函数类似,表现出动态多态特性。不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。因此在使用上有一定限制,但这在一定程度上提高了代码的安全性,特别体现在函数参数传递等场合中,可以将引用理解成一种“受限制的指针”。

void func(Base &ref)
{
     
	ref.print();//动态连接,编译运行起来才知道运行的是谁
}
int main()
{
     
	Base base(10);
	Derived derived(22,33);
	func(base);
	//Base &ref=base;
	func(derived);
	//Base &ref=derived;
}

5.3、对象访问

和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。通过对象名访问虚函数时, 调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。

int main()
{
     
	Base base(10);
	Derived derived(22,33);
	base.print();
	derived.print();
}

5.4、成员函数中访问

在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。

Base base(10);
Derived derived(22,33);

Base *pbase1 = &base;
pbase1->func1();
pbase2->func2();

Base *pbase2 = &derived;
pbase2->func1();
pbase2->func2();

5.5、构造函数和析构函数中访问

构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用“this->虚函数名”的形式来调用,编译器仍将其解释为静态联编的“本类名::虚函数名”。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。

class Grandpa
{
     
public:
  Grandpa()
 {
     
    cout << "Grandpa()" << endl;
 }
 
  ~Grandpa()
 {
     
    cout << "~Grandpa()" << endl;
 }
 
  virtual
  void func1()
 {
     
    cout << "Grandpa::func1()" << endl;
 }
 
  virtual
  void func2()
 {
     
    cout << "Grandpa::func2()" << endl;
 }
};
class Father
: public Grandpa
{
     
public:
Father()
 {
     
    cout << "Father()" << endl;
 func1();
 }
 
  ~Father()
 {
     
    cout << "~Father()" << endl;
 func2();
 }
 
  virtual
  void func1()
 {
     
    cout << "Father::func1()" << endl;
 }
 
  virtual
  void func2()
 {
     
    cout << "Father::func2()" << endl;
 }
};
class Son
: public Father
{
     
public:
  Son()
 {
     
    cout << "Son()" << endl;
 }
 
  ~Son()
 {
     
    cout << "~Son()" << endl;
 }
 
  virtual void func1()
 {
     
    cout << "Son::func1()" << endl;
 }
 
  virtual void func2()
 {
     
    cout << "Son::func2()" << endl;
 }
};
void test()
{
     
  Son son;
}

上面的代码都是调用了第二的Father函数

输出结果
Grandpa()
Father()
void Father::func1() //只能看到自己的func1()
Son()
~Son()
~Father()
void Father::func2() //只能看到本层的func2()
~Granpa()

5.6 动态多态与虚函数等价吗?
不等价,动态多态的体现必须要有虚函数,但是有虚函数并不一定体现动态多态

#include 
using std::cout;
using std::endl;
class A
{
     
public:
    virtual
    void func(int val=1)
    {
     
        cout<<"A->"<<val<<endl;
    }

    virtual void test()
    {
     
        func();
    }

private:
    long _a;
};

class B
: public A
{
     
public:
    virtual void func(int val=10)
    {
     
        cout<<"B->"<<val<<endl;
    }
private:
    long _b; 
};

int main(void){
     
    B b;
    A *p1 = (A*) &b;
    B *p2 = &b;
    p1->func();
    p2->func();
}

它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数
输出结果:

B->1
B->10

6、纯虚函数

1)纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的格式如下:
C++ 学习笔记7--多态(未完待续5/20)_第8张图片
2)设置纯虚函数的意义,就是让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
C++ 学习笔记7--多态(未完待续5/20)_第9张图片
3)声明纯虚函数的目的在于,提供一个与派生类一致的接口
具体代码:

class Figure
{
     
public:
  virtual void display() const = 0;
  virtual double area() const = 0;
};
class Circle
: public Figure
{
     
public:
//C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的
  explicit Circle(double radius)
 : _radius(radius)
 {
     
   
 }
 
  void display() const
 {
     
    cout << "Circle";
 }
 
  double area() const
 {
     
    return 3.14159 * _radius * _radius;
 }
private:
  double _radius;
  };
class Rectangle
: public Figure
{
     
public:
  Rectangle(double length, double width)
 : _length(length)
 , _width(width)
 {
     
   
 }
 
  void display() const
 {
     
    cout << "Rectangle";
 }
 
  double area() const
 {
     
    return _length * _width;
 }
private:
  double _length;
  double _width;
};
class Triangle
: public Figure
{
     
public:
  Triangle(double a, double b, double c)
 : _a(a)
 , _b(b)
 , _c(c)
 {
     
   
 }
 
  void display() const
 {
     
    cout << "Triangle";
 }
 
  //海伦公式计算三角形的面积
  double area() const
 {
     
    double p = (_a + _b + _c) / 2;
    return  sqrt(p * (p - _a) * (p - _b) * (p - _c));
 }  
private:
  double _a;
  double _b;
  double _c;
};

7、抽象类

1)一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数,该类便为抽象类。一个抽象类只能作为基类来派生新类,不能创建抽象类的对象
2)和普通的虚函数不同,在派生类中一般要对基类中纯虚函数进行重定义。如果该派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。这说明只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类才不再是抽象类
3)除此以外,还有另外一种形式的抽象类。对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中作为其对象成员都不能创建该类的对象,但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类。
4)代码

class Base
{
     
protected:
  Base(long base)
 : _base(base)
 {
     
    cout << "Base()" << endl;
 } 
protected:
  long _base;
};
class Derived
: public Base
{
     
public:
  Derived(long base, long derived)
 : Base(base)
 , _derived(derived)
 {
     
    cout << "Derived(long, long)" << endl;
 }
 
  void print() const
 {
     
    cout << "_base:" << _base
      << ", _derived:" << _derived << endl;
 }
private:
  long _derived;
};
void test()
{
     
  Base base(1);//error
  //如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中作为其对象成员都不能创建该类的对象
  Derived derived(1, 2);
  //但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类
}

输出结果

Base()
Derived(long, long)

8、虚析构函数

1)虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候
2)代码

class Base
{
     
public:
  Base(const char *pbase)
 : _pbase(new char[strlen(pbase) + 1]())
 {
     
    cout << "Base(const char *)" << endl;
    strcpy(_pbase, pbase);
 }
 
  /*virtual*/
  ~Base()
 {
     
    if(_pbase)
   {
     
      delete [] _pbase;
      _pbase = nullptr;
   }
   
 cout << "~Base()" << endl;
 }
   
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);
 }
 
  ~Derived()
 {
     
    cout << "~Derived()" << endl;
    if(_pderived)
   {
     
      delete [] _pderived;
      _pderived = nullptr;
   }
 }
private:
 char *_pderived;
};
void test()
{
     
  Base *pbase = new Derived("hello", "wuhan");
  pbase->print();
  delete pbase;
}

3)注意:

上面的代码只释放了基类的内存,但是派生类new出来的堆空间并没有释放,类的析构函数是虚函数,只要派生类自己定义了自己的析构函数,编译器就认为是一种重写(自定义),目的-》为了解决内存泄漏的问题(类的析构函数定义为虚构函数)
处理方法:
①法一:类型转换为派生类对象,太麻烦
delete dynamic_cast(pbase);
②法二:基类的析构函数是虚函数,派生类自动变为析构函数,而且派生类的析构函数自己自定义了,执行析构函数会自动执行派生类的析构函数
virtual ~Base()
基类将于派生类的析构函数相同
delete pbase;
//pbase->~Base()
//pbase->~destructor()

9、重载、隐藏、覆盖

1)重载:发生在同一个作用域中,函数名称相同,但参数的类型、个数、顺序不同(参数列表不一样)。
2)覆盖:发生在基类与派生类中,同名虚函数,参数列表亦完全相同。
3)隐藏:发生在基类与派生类中,指的是在某些情况下,派生类中的函数屏蔽了基类中的同名函数。(同名数据成员也有隐藏),如果派生类和基类都定义了,会直接调用派生类的,掩盖住基类的
4)隐藏的例子:

//针对于隐藏的例子
class Base
{
     
public:
  Base(int m)
 : _member(m)
 {
     
    cout << "Base(int)" << endl;
 }
  void func(int x)
 {
     
    cout << "Base::func(int)" << endl;
 }
 
  ~Base()
 {
     
    cout << "~Base()" << endl;
 }
protected:
  int _member;
};
class Derived
: public Base
{
     
public:
  Derived(int m1, int m2)
 : Base(m1)
 , _memeber(m2)
 {
     
    cout << "Derived(int, int)" << endl;
 }
 
  void func(int *)
 {
     
  cout << "_member: " << _member << endl;
    cout << "Derived::func(int*)" << endl;
 }
 
  ~Derived()
 {
     
    cout << "~Derived()" << endl;
 }
private:
  int _member;
};

10、测试虚表的存在

1)从前面的知识讲解,我们已经知道虚表的存在,但之前都是理论的说法,我们是否可以通过程序来验证呢?答案是肯定的。接下来我们看看下面的例子:(由程序验证虚表的存在)
2)代码

class Base
{
     
public:
  Base(long data1): _data1(data1)
 {
     
   
 }
 
  virtual
  void func1()
 {
     
    cout << "Base::func1()" << endl;
 }
 
  virtual
  void func2()
 {
     
    cout << "Base::func2()" << endl;
 }
 
  virtual
  void func3()
 {
     
    cout << "Base::func3()" << endl;
 }
protected:
  long _data1;
};
class Derived
: public Base
{
     
public:
  Derived(long data1, long data2)
 : _data1(data1)
 , _data2(data2)
 {
     
 }
 
  virtual
  void func1()
 {
     
    cout << "Derived::func1()" << endl;
 }
 
  virtual
  void func2()
 {
     
    cout << "Derived::func2()" << endl;
 }
private:
  long _data2;
};
void test()
{
     
  Derived derived(10, 100);
  long **pVtable = (long **)&derived;
 
  typedef void(* Function)();
  for(int idx = 0; idx < 3; ++idx)
 {
     
    Function f = (Function)pVtable[0][idx];
    f();
 }
}

注意:虚表存在于只读区域

11、带虚函数的多基派生

class Base1
{
     
public:
Base1()
 : _iBase1(10)
 {
     
   
 }
 
virtual
  void f()
 {
     
    cout << "Base1::f()" << endl;
 }
virtual
  void g()
 {
     
 cout << "Base1::g()" << endl;
 }
virtual
  void h()
 {
     
    cout << "Base1::h()" << endl;
 }
private:
int _iBase1;
};
class Base2
{
     
public:
Base2()
 : _iBase2(100)
 {
     
   
 }
  virtual void f()
 {
     
    cout << "Base2::f()" << endl;
 }
virtual
  void g()
 {
     
    cout << "Base2::g()" << endl;
 }
virtual
  void h()
 {
     
    cout << "Base2::h()" << endl;
 }
private:
int _iBase2;
};
class Base3
{
     
public:
Base3()
 : _iBase3(1000)
 {
     
   
 }
  virtual
  void f()
 {
     
    cout << "Base3::f()" << endl;
 }
virtual
  void g()
  {
     
    cout << "Base3::g()" << endl;
 }
virtual
  void h()
 {
     
    cout << "Base3::h()" << endl;
 }
private:
int _iBase3;
};
class Derived
: public Base1
, public Base2
, public Base3
{
     
public:
Derived()
 : _iDerived(10000)
 {
     
   
 }
void f()
 {
     
    cout << "Derived::f()" << endl;
 }
virtual
  void g1()
 {
     
    cout << "Derived::g1()" << endl;
 }
private:
int _iDerived;
};
void test()
{
     
Derived d;
Base2 *pBase2 = &d;
Base3 *pBase3 = &d;
Derived *pDerived = &d;
pBase2->f();
cout << "sizeof(d) = " << sizeof(d) << endl;
cout << "&Derived = " << &d << endl;
cout << "pBase2 = " << pBase2 << endl;
cout << "pBase3 = " << pBase3 << endl;
}
//结论:多重继承(带虚函数)
//1. 每个基类都有自己的虚函数表
//2. 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中
//3. 内存布局中,其基类的布局按照基类被声明时的顺序进行排列
//4. 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是
// 真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的
// 对应的虚函数的地址,而只是一条跳转指令

12、多基派生的二义性

13、虚拟继承

14、效率分析

你可能感兴趣的:(c++,多态,c++)