Thinking in C++【14】多态性和虚函数

1.早捆绑与晚捆绑

把函数体与函数调用相联系称为捆绑

  • 早捆绑:捆绑在程序运行之前(由编译器和连接器)完成
  • 取一个对象的地址,并将其作为基类的地址来处理,这称为向上类型转换
enum note {
    
     middleC, Csharp, Eflat };
class Instrument {
    
    
public:
    void play(note) const {
    
    
        cout << "Instrument::play\n" << endl;
    }
};
class Wind : public Instrument {
    
    
public:
    void play(note) const {
    
    
        cout << "Wind::play\n" << endl;
    }
};
void tune(Instrument& i) {
    
    
    i.play(middleC);
}
int main() {
    
    
    Wind flute;
    tune(flute);
}

输出: Instrument::play
原因:编译器在只有Instrument地址时并不知道要调用的正确函数

注意: 向上转型需要添加取地址符"&"
1)void tune(Instrument& i) {}
2)Base& base = derived4;

  • 晚捆绑(动态捆绑、运行时捆绑): 捆绑发生在运行时
    为了实现晚捆绑,需要在基类中声明这个函数时使用virtual关键字。晚捆绑只对virtual函数起作用。而且只在使用含有virtual函数的基类的地址时发生
enum note {
    
     middleC, Csharp, Eflat };
class Instrument {
    
    
public:
    virtual void play(note) const {
    
    
        cout << "Instrument::play\n" << endl;
    }
};
class Wind : public Instrument {
    
    
public:
    void play(note) const {
    
    
        cout << "Wind::play\n" << endl;
    }
};
void tune(Instrument& i) {
    
    
    i.play(middleC);
}
int main() {
    
    
    Wind flute;
    tune(flute);
}
  • 仅仅在声明的时候需要使用关键字virtual,定义时不需要;
  • 如果一个函数在基类中声明为virtual,那么在所有的派生类中它都是virutual的。
  • 若基类中的virtual声明的函数,在第j层子类中并无重写,则编译器会自动地调用继承层次中“最近”的定义。

2.C++实现晚捆绑的机制

关键字virtual告诉编译器不要进行早捆绑,而应当自动安装对于实现晚捆绑必需的所有机制。

  • 具体:典型的编译器对每个包含虚函数的类创建一个表(VTABLE),在VTABLE中放置特定的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针,称为vpointer(VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时(即多态调用),编译器静态地插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,如此调用正确的函数并引起晚捆绑的发生。

3.存放类型信息

  • 无论类里有多少个virtual虚函数,编译器只在这个结构中插入单个指针(VPTR);
  • 编译器只为成员变量分配内存,成员函数在代码区,不占用内存;
  • 如果类无成员变量,编译器会为其插入哑成员,使其内存所占大小为1
class NoVirtual{
    
    
    int a;
    //情况二
    //int b;
    //情况三:
    // int a;
public:
    void x() const {
    
    }
    int i() const {
    
     return 1; }
};
class OneVirtual {
    
    
    int a;
public:
    virtual void x() const {
    
    }
    int i() const {
    
     return 1; }
};
class TwoVirtual {
    
    
    int a;
public:
    virtual void x() const {
    
    }
    virtual int i() const {
    
     return 1; }
};
int main() {
    
    
    cout << "int:" << sizeof(int) << endl;
    cout << "void*:" << sizeof(void*) << endl;
    cout << "NoVirtual:" << sizeof(NoVirtual) << endl;
    cout << "OneVirtual:" << sizeof(OneVirtual) << endl;
    cout << "TwoVirtual:" << sizeof(TwoVirtual) << endl;
}

情况1:
int:4
void*:8
NoVirtual:4
OneVirtual:16
TwoVirtual:16

情况2:
int:4
void*:8
NoVirtual:8
OneVirtual:16
TwoVirtual:16

情况3:
int:4
void*:8
NoVirtual:1
OneVirtual:16
TwoVirtual:16


4.纯虚函数

  • 当类中全是纯虚函数,则我们称之为纯抽象类
    //纯虚函数语法:
virtual void f() = 0; 

纯虚函数告诉编译器在VTABLE中为函数保留一个位置,但在这个特定位置中不放地址。

class Instrument {
    
    
	virtual void play(note) const = 0;
	virtual string what() const = 0;
	virtual void adjust(int) = 0;
}

5.纯虚函数的定义

  • 纯虚函数的定义不能在类内部定义,但能够在类外定义;
  • 子类继承纯虚函数,必须重写纯虚函数;
class Pet {
    
    
    virtual void speak() const = 0;
    virtual void eat() const = 0;
    //Inline pure virtual definitions illegal
    //! virtual void sleep()  const = 0 {}
};

// OK, not defined inline
void Pet::speak() const {
    
    
    cout << "Pet::eat()\n" << endl;
}

void Pet::eat() const {
    
    
    cout << "Pet::eat()\n" << endl;
}

class Dog : public Pet {
    
    
public:
    void speak() const {
    
    
        Pet::speak();
    }
    void eat() const {
    
    
        Pet::eat();
    }
};
int main() {
    
    
    Dog simba;
    simba.speak();
    simba.eat();
}

6.对象切片

  • describe()接受的是一个Pet对象(而不是指针或地址),所以describe()的任何调用都将引起一个与Pet大小相同的对象压栈并在调用后清除。在这个过程中,编译器只拷贝这个对象对应于Pet的部分,切除这个对象的派生部分。
  • 注意:即使程序没有用到基类的成员变量,也要确保基类有默认构造器或者后续会被赋值,否则编译器会报错”无初始化“
    在这里插入图片描述
//
//  main.cpp
//  NUAATestJan5
//
//  Created by chenjunyi on 2021/1/5.
//  Copyright © 2021 chenjunyi. All rights reserved.
//


#include <iostream>
#include <string>
using namespace std;

class Pet{
    
    
    string pname;
public:
    Pet(const string& petName) : pname(petName){
    
    };
    virtual string name() const {
    
     return pname; }
    //virtual string name() const = 0;
    //virtual string desciption() const = 0;
    virtual string desciption() const {
    
    
        return "This is " + pname;
    };
};

class Dog : public Pet {
    
    
    string favoriteActivity;
public:
    Dog(const string& name, const string& activity) : Pet(name), favoriteActivity(activity){
    
    }
    string name() const {
    
     return dogname; }
    string desciption() const {
    
    
        return Pet::name() + " likes to " + favoriteActivity;
    }
    
};

void describe(Pet p){
    
     //slices the object
    cout << p.desciption() << endl;
}

int main() {
    
    
    Pet p("Alfred");
    Dog d("Fluffy", "sleep");
    describe(p);
    describe(d);
}


7.虚函数的重载和重新定义

  • 编译器不允许改变重新定义过的函数的返回值(若f()不是虚函数,则可以)。因为编译器必须保证我们能够多态地通过基类调用函数,且不发生冲突。
class Base {
    
    
public:
	virtual int f() const {
    
     return 1; }
	virtual void f(string s) {
    
     cout << "Hello world!!" << endl;} 
}

class Derived3 : public Base {
    
    
	//Cannot change the return type;
	void f() const {
    
     return 4; }
}
  • 将派生类向上转型为基类,派生类重写过的成员函数会替代基类的成员函数
class Base {
    
    
public:
	virtual int f() const {
    
     return 1; }
	virtual void f(string s) {
    
     cout << "Hello world!!" << endl;} 
}

class Derived4 : public Base {
    
    
public:
	int f() const {
    
     return 55; } 
	int g() const {
    
     return 100; }
}

int main() {
    
    
	Derived4 d;
	Base& base = d;
	base.f();  // return 55
	base f("hello");   //cout << "Hello world!!" << endl;
}

在这里插入图片描述

  • 总结:在重写的时候,派生类的代码区A会替代基类的代码区B;而在向上转型的时候,编译器会去除代码区的派生部分,而将重写的代码段1替换基类相应的代码段2

8.向下类型转换

  • dynamic_cast<>()
Dog* d1 = dynamic_cast<Dog*>(b);

猜你喜欢

转载自blog.csdn.net/qq_43118572/article/details/112553344