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);