程序设计实习(4) —— 多态

虚函数

在类的定义中, 前面有 virtual 关键字的成员函数就是 虚函数. virtual 只用在类定义里的函数声明中, 写函数体时不用.

派生类的指针可以赋给基类指针. 通过基类指针调用基类和派生类中的同名同参 函数时:

  • 若该指针指向一个基类的对象, 那么被调用是基类的虚函数;
  • 若该指针指向一个派生类的对象, 那么被调用的是派生类的虚函数.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class CBase {
public:
    virtual void someVirtualFunction() { cout << "base function" << endl; }
};
class CDerived : public CBase {
public:
    virtual void someVirtualFunction() { cout << "derived function" << endl; }
};
int main() {
    CDerived ODerived;
    CBase *p = &ODerived;
    p->someVirtualFunction(); // derived function
    return 0;
}

派生类的对象可以赋给基类引用. 类似于指针, 通过基类引用调用基类和派生类中的同名同参虚函数也是多态的.

1
2
3
4
5
6
int main() {
    CDerived ODerived;
    CBase &r = ODerived;
    r.someVirtualFunction(); // derived function
    return 0;
}

多态不能针对对象. 派生类中的虚函数的访问权限可以是 public, protected, private. 但是, 基类中的虚函数的访问权限不能是 private, 即使派生类中的虚函数的访问权限是 public. 反过来是可以的, 而且可以正常多态.

实现原理

采用了动态联编的技巧. 每一个有虚函数的类 (或其派生类) 都有一个虚函数表,该类的任何对象中都放着虚函数表的指针. 虚函数表中列出了该类的虚函数地址. 多出来的 4 个字节就是用来放虚函数表的地址的. 在编译时, 调用语句被编译成对虚函数表的索引, 而不是对函数的直接调用.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Base {
public:
    int i;
    virtual void Print() { cout << "Base:Print" << endl; }
};
class Derived : public Base {
public:
    int n;
    virtual void Print() { cout << "Drived:Print" << endl; }
};
int main() {
    Derived d;
    cout << sizeof(Base) << "," << sizeof(Derived);
    return 0;
}

32 位系统下, 这个程序的输出是 8,12.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class A {
public:
    virtual void func() { cout << "A::func "; }
};
class B : public A {
public:
    virtual void func() { cout << "B::func "; }
};
int main() {
    A a;
    A *pa = new B();
    pa->func(); // 多态, B::func
    // 64 位程序
    long long *p1 = (long long *)&a;
    long long *p2 = (long long *)pa; // 篡改了虚函数表指向
    *p2 = *p1;
    pa->func(); // A::func
    return 0;
}

虚析构函数

在非构造/析构函数中调用虚函数时, 调用的是当前对象的虚函数, 而不是基类的虚函数, 是多态. 在构造/析构函数中调用虚函数时, 调用的是当前类的虚函数, 编译时确定, 不是多态.

通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数. 为了解决这个问题, 可以把基类的析构函数声明为虚函数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class son { 
public: 
    virtual ~son() { cout << "bye from son" << endl;}
};
class grandson : public son {
public:
    ~grandson() { cout << "bye from grandson" << endl; }
};
int main() {
    son *pson;
    pson = new grandson();
    delete pson;
    return 0;
}

此时, 会先调用派生类的析构函数, 再调用基类的析构函数. 另外注意, 构造函数不能是虚函数.

纯虚函数和抽象类

如果在虚函数后面加上 = 0, 则该虚函数是纯虚函数. 纯虚函数不可以有函数体, 只有声明.

1
2
3
4
5
class A {
public:
    virtual void print() = 0; // 纯虚函数
    void fun() { cout << "fun"; }
}

一个类中有纯虚函数的类叫 抽象类. 抽象类不能实例化, 只能作为基类, 不过可以作为指针或引用类型. 在抽象类的成员函数内可以调用纯虚函数, 但是在构造函数或析构函数内部不能调用纯虚函数.

本文遵循 CC BY-NC-SA 4.0 协议
使用 Hugo 构建