• C++虚函数


    虚函数和多态

    首先明确一个空类产生的对象的大小为1B,即使是一个空类,其实例化的对象至少占用1B的内存空间

    class A {
    };
    
    int main() {
        A a;
        cout << sizeof(a) << endl;		//1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后我们向类A中加入两个普通成员函数,对象的大小还是1B:

    class A {
    public:
        void func1() {}
        void func2() {}
    };
    
    int main() {
        A a;
        cout << sizeof(a) << endl;		//1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这说明A的普通成员函数,并不会占用类对象的内存空间。然而一旦我们向A中加入一个虚函数,其对象的大小变为8B:

    class A {
    public:
        void func1() {}
        void func2() {}
        virtual void vfunc() {}
    };
    
    int main() {
        A a;
        cout << sizeof(a) << endl;		//8
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    原因:当引入虚函数后,编译器在类中插入了一个虚函数表指针vptr,类似于下面的伪码:

    class A{
    public:
    	void* vptr;
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而vtpr是占用类对象的内存空间的:

    (gdb) p a
    $1 = (A) {_vptr.A = 0x7ff771b64520 <vtable for A+16>}
    
    • 1
    • 2

    当类A中至少包含一个虚函数,编译器会为类A产生一个虚函数表vtbl,这个虚函数表会一直伴随着类A,包括其装入内存

    虚函数表指针被赋值的时机:执行A的构造函数时,让对象的虚函数指针指向类A的vtbl

    每个类对象的虚函数表指针vptr指向这个类的虚函数表vtbl,编译器在编译期间在类A的构造函数内安插vptr的赋值语句,类似于下面的伪码:

    class A {
    public:
    	A() {
    		vptr = &A::vtbl;	//编译器做的
    		...
    	}
    	void* vptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    考虑如下的A类对象的内存布局:

    class A {
    public:
        void func1() {}
        void func2() {}
        virtual void vfunc() {}
        virtual void vfunc2() {}
        virtual ~A() {}
    private:
    	int m_a;
    	int m_b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    (gdb) p a
    $1 = (A) {_vptr.A = 0x7ff77a105520 , m_a = 0, m_b = 1}
    
    • 1
    • 2

    虚析构的作用

    注意上面的例子中把A的析构函数设为虚函数,在实际开发中,这样做的目的是保证当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用,否则其只会调用基类的析构函数,如果派生类中有指针成员持有堆区内存,就得不到释放而造成内存泄漏

    例如,这是正确的形式:

    #include 
    using namespace std;
    
    class Base {
    public:
        virtual ~Base() {
            cout << "Base dtor" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        ~Derived() {
            cout << "Derived dtor" << endl;
        }
    
    };
    
    int main () {
        Base* pb = new Derived();
        delete pb;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当delete父类指针时,子类的dtor也一同被调用:

    $ g++ virtual-dtor.cpp 
    $ ./a.out 
    Derived dtor
    Base dtor
    
    • 1
    • 2
    • 3
    • 4

    如果去掉父类dtor前的virtual,则只会调用父类的dtor:

    #include 
    using namespace std;
    
    class Base {
    public:
        ~Base() {
            cout << "Base dtor" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        ~Derived() {
            cout << "Derived dtor" << endl;
        }
    
    };
    
    int main () {
        Base* pb = new Derived();
        delete pb;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    $ g++ virtual-dtor.cpp 
    $ ./a.out 
    Base dtor
    
    • 1
    • 2
    • 3

    多态:当通过父类指针指向子类对象,或通过父类引用绑定子类对象,调用父类中的虚函数,实际调用的是对应子类的虚函数

    在这里插入图片描述

    class Base {
    public:
        virtual void myvirfunc() {}
    };
    
    int main() {
        Base* pb = new Base();
        pb->vfunc();	//this is polymorphic
    
        Base b;
        b.vfunc();		//this is not polymorphic
    
        Base* pb2 = &b;
        pb2->vfunc();	//this is polymorphic
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    多态的表现:

    1. 程序中既存在父类也存在子类,父类中必须含有虚函数,子类中也必须重写父类中的虚函数
    2. 父类指针指向子类对象,或者父类引用绑定子类对象
    3. 当通过父类的指针或引用,调用子类中重写的虚函数时,就能看出多态性的表现了

    下面的调用全是多态调用:

    class Base {
    public:
        virtual void myvirfunc() {}
    };
    
    class Derive : public Base {
    public:
        virtual void myvirfunc() {}
    };
    
    int main() {
        //父类指针指向子类对象
        Derive d;
        Base* pb = &d;
        pb->myvirfunc();
    
        Base* pb2 = new Derive();
        pb2->myvirfunc();
    
        //父类引用绑定子类对象
        Derive d2;
        Base& rb = d2;
        rb.myvirfunc();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    存在继承关系时虚函数表指针指向:

    考虑下面的继承关系和重写:

    class Base {
    public:
    	virtual void f() {}
    	virtual void g() {}
    	virtual void h() {}
    };
    
    class Derive :public Base {
    public:
    	virtual void g() {}		//rewrite g()
    };
    
    int main(int argc, char* argv[]) {
    	Base b;
    	Derive d;
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    设父类有f(), g(), h()这三个虚函数,子类重写了父类中的g()虚函数,则父子类对象的虚函数表指针和虚函数表如下:

    在这里插入图片描述

    延申思考题:

    1. 当进行多重继承时,子类对象有几个虚函数表指针?子类对象有几个虚函数表?
    2. 虚基类表指针在对象内存中的布局?

    用C语言模拟多态

    对于下面的多态实例:

    #include 
    using namespace std;
    
    class ISpeaker {
    protected:
        int b;
    public:
        ISpeaker (int bb) : b(bb) {}
        virtual void speak() = 0;
    };
    
    class Dog : public ISpeaker {
    public:
        Dog() : ISpeaker(0) {}
        virtual void speak() override {
            printf("woof %d\n", b);
        }
    };
    
    class Human : public ISpeaker {
    private:
        int c;
    public:
        Human() : ISpeaker(1), c(2) {}
        virtual void speak() override {
            printf("hello %d\n", c);
        }
    };
    
    int main(){
        ISpeaker* d = new Dog();
        ISpeaker* h = new Human();
    
        d->speak();
        h->speak();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述
    其用C语言可以描述如下:

    #include 
    using namespace std;
    
    extern "C" {
    	//虚函数表类型
        struct vft {
            void (*speak) (void* ptr);
        };
    	//Dog的speak方法
       void Dog_speak (void* ptr) {
            void* p = ptr + sizeof(vft*);
            int bb = *((int*)p);
            printf("woof %d\n", bb);
        }
    	//Human的speak方法
        void Human_speak (void* ptr) {
            void* p = ptr + sizeof(vft) + sizeof(int);
            int cc = *((int*)p);
            printf("hello %d\n", cc);
        }
    	//Dog的虚函数表,其speak函数指针指向Dog_seapk
        const static vft Dog_vft= {
            .speak = Dog_speak
        };
    	//Human的虚函数表,其speak函数指针指向Human_speak
        const static vft Human_vft = {
            .speak = Human_speak
        };
    	//基类型内存模型
        struct ISpeaker {
            const vft* vptr;
            int b;
        };
    	//Dog类型内存模型
        struct Dog {
            const vft* vptr;
            int b;
        };
    	//Human类型内存模型
        struct Human {
            const vft* vptr;
            int b;
            int c;
        };
     
        Dog* Dog_constructor() {
            Dog* d =(Dog*)malloc(sizeof(Dog));
            d->vptr = &Dog_vft;
            d->b = 0;
            return d;
        }
    
        Human* Human_constructor() {
            Human* h =(Human*)malloc(sizeof(Human));
            h->vptr = &Human_vft;
            h->b = 1;
            h->c = 2;
            return h;
        }   
    
        int main () {
            ISpeaker* s1 = (ISpeaker*)Dog_constructor();
            ISpeaker* s2 = (ISpeaker*)Human_constructor();
            
            s1->vptr->speak(s1);
            s2->vptr->speak(s2);
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
  • 相关阅读:
    查看MySQL的初始密码
    保研/考研简历中数模竞赛的经历,就该这样写
    Python从入门到高手的80行代码
    【服务器学习】hook模块
    简要归纳UE5 Lumen全局光照原理
    P1~P2 模板起源
    王学岗Linux系统的使用和Linux命令
    将传统应用带入浏览器的开源先锋「GitHub 热点速览」
    【国产替代】航空电子通信总线航空电子通信总线产品为MIL-STD-1553和ARINC 429等协议提供原生支持
    T113-S3-LCD-RGB显示屏调试
  • 原文地址:https://blog.csdn.net/DanielSYC/article/details/126648840