Skip to content

多态、析构、虚函数

  1. 一个类如果使用了虚函数,请把他的析构函数也改为虚函数。
    2.保证至少有一个虚函数不是内联实现的。

1 多态含义

多态,字面上看多态就是多种形态。

类存在继承关系且多种层次时,会用到多态。多态具体指:调用成员函数时,根据当前的类型执行不同的函数。(动态调用,程序跑起来后才知道调用什么)  

如果一个语言无法实现多态,只能算是“基于对象的程序设计语言”而非“面向对象的程序设计语言”。

多态诞生目的:降低程序运行效率,提高开发人员效率。

2 如何实现多态-虚函数

在类中使用 virtual 关键字修饰函数。

class A {
  public:
    virtual void Execute() {
        cout << "A::Execute" << endl;
    }
};
class B : public A {
  public:
    virtual void Execute() {
        cout << "B::Execute" << endl;
    }
};

int main() {
    A  a;
    B b;
    A *pa = &a;
    B *pb = &b;
    pa->Execute();
    pa = pb;
    pa->Execute();
    return 0;
}

3 纯虚函数

需要在基类中定义虚函数,以便派生时更好使用。但是基类有没什么好写的,就可以使用纯虚函数。

virtual void Execute() = 0;

4 析构函数

正常逻辑是 delete 应该调用类本身的析构函数,而不是基类的虚函数。如果析构函数不用虚函数的话,会有各种 BUG。

  • 引用计数,因为调用错了析构函数导致delete后计数错误  
  • 类内动态分配的内存,调用错误的析构函数就内存泄露了

c++ 派生类会自动调用基类的析构函数。

一个类如果使用了虚函数,请把他的析构函数也改为虚函数。

5 虚函数必须在外部实现

保证有一个虚函数不是内联实现的就行了。

析构虚函数如果定义在内部,会发生警告

class A {
  public:
    virtual ~A(){}
    virtual void Execute() {
        cout << "A::Execute" << endl;
    }
};
 warning: 'A' has no out-of-line virtual method definitions; 
its vtable will be emitted in every translation unit

定义在外部则正常。

class A {
  public:
    virtual ~A();
    virtual void Execute() {
        cout << "A::Execute" << endl;
    }
};
A::~A() {}

原因就设计到虚函数是如何实现的了,定义了虚函数编译器会自动生成虚函数表,用来对应虚函数。如果虚函数所有方法是内联的,编译器不知道在那个 cpp 文件中生成虚函数表,他会在所有用到这个类的 cpp 中搞一个副本并链接。增大了.O 文件。

6 构造函数不能是虚函数

从使用角度来说:构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用。具体实现看下文。

7 虚函数实现原理

类的实例化就是给每一个实例在内存中分配一块地址。空类的大小是 0,实例化时编译器会给一个字节。

class A {
  public:
    virtual ~A();
};
A::~A() {}

class AA {
  public:
    ~AA() {}
};
int main() {
    std::cout << sizeof(A) << ", " << sizeof(AA) << std::endl;
    // 结果 8, 1 (gcc 64)
}

看到如果类中有虚函数,多 8 个字节(gcc 64)。任何有虚函数的类,都会多一个地址的大小,存放虚函数表的位置。为了高效,这个放地址的位置在这个类的存储空间最前面。

每一个有虚函数的类都有一个虚函数表,这个类的所有对象都放着这个表的指针。

编译器自动添加到构造函数中的,所以构造函数不能使虚函数

程序编译时候会给每个有虚函数的类生成一个虚函数表放在. O 里,程序运行后拷贝到内存里。

class A {
  public:
    int i;
    virtual ~A();
};
A::~A() {}

https://www.cnblogs.com/phpandmysql/p/10853354.html

8 虚函数类的调用

  1. 取 a 指针指向的地址的前 8 个字节,如果 a 指向的是类 A 这个地址指向位置就是类 A 的虚函数表,如果 a 指向的是类 B 这个地址指向的位置就是类 B 的虚函数。
  2. 在虚函数表里找到要调用函数的地址。
  3. 调用函数。
class A {
  public:
    virtual ~A();
    virtual void fun() {
        std::cout << "A::fun" << std::endl;
    }
};
A::~A() {}

class B: public A {
  public:
    virtual ~B();
    virtual void fun() {
        std::cout << "B::fun" << std::endl;
    }
};
B::~B() {}

int main() {
    A *a;
    B b;
    a = new A;
    a->fun();
    a = &b;
    a->fun();
    a = new A;
    a->fun();
    return 0;
}

/* 结果
A::fun
B::fun
A::fun
*/

https://www.zhihu.com/question/425173545/answer/1520796610

单继承时,如何知道 a 指向的是类 A 还是类 B?
* 每个虚函数在虚函数表中有个索引
* 虚函数调用被编译器改成了:(*p->vptr[x])(p)。(其中 pthisx 是这个虚函数在虚函数表中位置)

这样就可以做到在程序运行是,调用那个函数取决于 vptr[x] 具体指向哪里。

https://www.cnblogs.com/phpandmysql/p/10853354.html

可以看到有继承关系的所有虚函数的 x 都是一样的。