C++复习

C++重新理解虚函数

2018-05-24  本文已影响11人  凉拌姨妈好吃

1. 虚函数的定义

允许派生类重新定义与基类同名的函数,并且可以通过基类指针引用来访问基类或派生类的同名函数

1.1 动态绑定(动态联编)

函数的运行版本由实参决定,直到运行的时候才知道调用了哪个版本的虚函数。
动态绑定只有通过指针或引用调用虚函数时才会发生

Quote base = derived;
base.net_price(20);
//在这里只调用Quote的net_price

switch、if也属于动态联编

2. 虚函数的构造

2.1 派生类中虚函数的构造

派生类中虚函数的参数列表函数名必须相同,返回类型在大多情况下是相同的(当返回类型为基类时,派生虚函数返回类型为自己的派生类)

2.2 override/final说明符

使用override关键字来修饰派生类中的虚函数,表示该函数并没有覆盖已存在的虚函数,如果用户强制覆盖会报错。

struct B{
  virtual void f1() const;
  virtual void f2();
};
struct D:B{
  void f1() const override;
  void f2(int)  override;//编译器会报错,因为B没有f2(int)这样的函数
}

如果用final关键字修饰虚函数,那么它的派生类如果要覆盖该函数会出错

struct B{
  virtual void f2() final;
};
struct D:B{
void f2();//出错,因为f2已经声明为final
}
2.2.1 重载/重写/覆盖
class A{
public:
  void test(int i);
  void test(double i);
  void test(int i, double j);
  void test(double i, int j);
};
class A{
public:
  virtual void fun3(int i){
    cout << "A::fun3() : " << i << endl;
  }
 
};
class B : public A{
public:
  virtual void fun3(double i){
    cout << "B::fun3() : " << i << endl;
  }
};
class A{
public:
  void fun1(int i, int j){
    cout << "A::fun1() : " << i << " " << j << endl;
  }
 
};
class B : public A{
public:
    //隐藏
  void fun1(double i){
    cout << "B::fun1() : " << i << endl;
  }
};
2.3 回避虚函数机制

有时候我们在派生类的虚函数的函数体中想要调用基类的虚函数,那么这时候就一定要加上作用符限定,否则就会调用派生类的虚函数,造成无限循环

3. 纯虚函数

当设计者不希望创建一个基类对象,因为基类里面的函数是没有意义的,那么可以将基类的该函数定义为纯虚函数,那么我们将这些含有纯虚函数的基类称为抽象基类

3.1 抽象基类
3.1.1 为什么有抽象基类

因为纯虚函数不能被调用,所以包含纯虚函数的类是无法实例化的,那么这时候就出现了一个抽象类,它作为多个子类的共同基类,就相当于给多个子类提供一个公共的接口,我们可以通过定义这个公共接口的指针或引用,指向派生类的某个对象,这样就可以通过它来访问派生类对象中的虚函数

3.1.2 抽象基类的几个要点

4. 虚函数表

4.1 虚函数表是如何实现的

先思考一个问题,编译器是在什么时候实现不同对象能调用同名函数绑定关系的?

在创建对象的时候,编译器偷偷给对象加了一个vptr指针。只要我们类中定义了虚函数,那么在实例化对象的时候,就会给对象添加一个vptr指针,类中创建的所有虚函数的地址都会放在一个虚函数表中,vptr指针就指向这个表的首地址。

4.2 在构造函数中定义虚函数会出现什么情况?

看以下代码,思考一下此时虚函数的调用

class Parent{
public:
    Parent(int a=0){
            this->a = a;
            print();}
    virtual void print(){cout<<"Parent"<<endl;}
private:
    int a;
};
class Son:public Parent{
    Son(int a=0,int b=0):Parent(a){
        this->b = b;
        print();}
    virtual void print(){cout<<"Son"<<endl;}
};
void main(int argc, char const *argv[]){
        Son s;
        return 0;
}

两个类中构造函数中,都只会调用自己类中的print()函数
为什么呢?因为Son对象在实例化时,先调用基类构造函数,存在虚函数,将vptr指向基类的虚函数表,调用派生类构造函数,存在虚函数,将vptr指向派生类的虚函数表。所以都只会调用自己类中的虚函数。

如果子类重写了父类的某一虚函数,那么父类的该虚函数就被隐藏,无论以后怎么调用,调用同名虚函数调用的都是子类虚函数

重写前
重写后

为什么析构函数经常定义为虚析构函数

虚析构函数:只有当一个类被定义为基类的时候,才会把析构函数写成虚析构函数。
为什么一个类为基类,析构函数就需要写成虚析构?
假设现在有一个基类指针,指向派生类。此时释放基类指针,如果基类没有虚析构函数,此时只会看指针的数据类型,而不会看指针指向的数据类型,所以此时会发生内存泄露。

4.3 虚继承
4.3.1 为什么会使用虚继承

当类D继承于类B与类C,而类B与类C又继承于公共基类类A,为了避免基类多重拷贝,我们就让类B与类C虚继承于类A

4.3.2 底层原理

每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)
vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间


一个非常容易错的地方!
class parent  
{  
    public:  
    virtual void output();  
};  
void parent::output()  
{  
    printf("parent!");  
}  
       
class son : public parent  
{  
    public:  
    virtual void output();  
};  
void son::output()  
{  
    printf("son!");  
}

son s; 
memset(&s , 0 , sizeof(s)); 
parent& p = s; 
p.output(); 

猜一猜上面会输出什么呢?
编译出错!!!
为什么呢?

memset会将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值
虚函数链表地址也清空了, 所以p.output调用失败。 output函数的地址编程0

上一篇下一篇

猜你喜欢

热点阅读