编译器根据指针的类型,来确定调用哪个类的普通成员函数
编译器根据基类类型指针指向的对象类型,来确定调用哪个类的虚函数
对象的自洽性:
1)对同样的函数调用,各个类的对象都会做出恰当的响应。
编译器仅根据指针的类型来确定调用哪个类的普通成员函数,
即,通过基类类型指针调用普通成员函数,只能调用基类的成员函数:
1)即便这个基类类型的指针指向了子类对象,调用的也为基类的成员函数。
2)一旦调用子类所特有的成员函数,将引发编译错误。
// selfconst.cpp 非虚的世界(没有虚函数的程序)
#include
using namespace std;
class Shape {
public:
void Draw( ) { cout << "Shape::Draw" << endl; }
private:
int m_x;
int m_y;
};
class Rect : public Shape {
public:
void Draw( ) { cout << "Rect::Draw" << endl; }
private:
int m_rx;
int m_ry;
};
class Circle : public Shape {
public:
void Draw( ) { cout << "Circle::Draw" << endl; }
void foo( ) { }
private:
int m_radius;
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
cout << "--------利用 对象 调用 非虚的成员函数---------" << endl;
// 哪个类对象 就调用 哪个类的普通成员函数(对象的自恰性)
Shape s;
s.Draw( ); // Shape::Draw
Rect r;
r.Draw( ); // Rect::Draw
Circle c;
c.Draw( ); // Circle::Draw
cout << "------利用 指针 调用 非虚的成员函数----------" << endl;
// 利用 基类类型的指针 只能调用 基类的普通成员函数
Shape* ps = &s;
ps->Draw( ); // Shape::Draw
// 即便 基类类型指针 指向的是子类对象,调用仍然为基类的普通成员函数
ps = &r;
ps->Draw( ); // Shape::Draw
ps = &c;
ps->Draw( ); // Shape::Draw
// 如果调用 子类所特有成员函数,将报告编译器错误
// ps->foo( );
// 编译器 简单而粗暴根据 指针本身的类型 来确定到底调用哪个类的普通成员函数
return 0;
}
class 类名 {
virtual 返回类型 函数名 ( 形参表 ) { ... }
};
一个类中,除了构造函数和静态成员函数外,任何函数都可以被声明为虚函数。
覆盖:
1)如果子类的成员函数和基类的虚函数具有相同的函数签名,则该成员函数就也是虚函数,无论其是否带有virtual关键字。
2)与基类的虚函数构成覆盖关系。
通过基类类型指针调用虚函数:
1)如果基类型指针指向基类对象,则调用基类的原始版本虚函数。
2)如果基类型指针指向子类对象,则调用子类的覆盖版本虚函数。
// bdv.cpp 虚的世界(有虚函数的程序)
#include
using namespace std;
class Shape {
public:
virtual void Draw( ) = 0; // 虚函数(原始版本)
private:
int m_x;
int m_y;
};
class Rect : public Shape {
public:
void Draw( ) { cout << "Rect::Draw" << endl; } // 虚函数(编译器补virtual),
//与基类Draw函数构成覆盖关系(覆盖版本)
private:
int m_rx;
int m_ry;
};
class Circle : public Shape {
public:
virtual void Draw( ) { cout << "Circle::Draw" << endl; } // 虚函数(编译器
//不补virtual),与基类的Draw函数构成覆盖关系(覆盖版本)
private:
int m_radius;
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
cout << "--------利用 对象 调用 虚的成员函数---------" << endl;
// 哪个类对象 就调用 哪个类的虚成员函数(对象的自恰性)
// Shape s;
// s.Draw( ); // Shape::Draw
Rect r;
r.Draw( ); // Rect::Draw
Circle c;
c.Draw( ); // Circle::Draw
cout << "------利用 指针 调用 虚的成员函数----------" << endl;
// Shape* ps = &s;
// ps->Draw( ); // Shape::Draw (不是多态)
Shape* ps = &r;
ps->Draw( ); // Rect::Draw(多态)
ps = &c;
ps->Draw( ); // Circle::Draw(多态)
// 根据 指针指向的对象的类型 来确定到底调用哪个类的虚成员函数
return 0;
}
静态多态:在编译阶段就已经绑定了函数地址,主要体现是重载、模板。
动态多态:利用虚函数实现,在运行期间绑定,主要体现是给父类指针传递不同的类型,调用的函数也会不同。
如果子类提供了对基类虚函数的有效覆盖,那么通过一个基类型指针(指向子类对象),或者基类型引用(引用子类对象),调用该虚函数,实际调用的将是子类中的覆盖版本,而非基类中的原始版本,这种现象称为动态多态。
动态多态的重要意义在于,一般情况下,调用哪个类的成员函数是由指针或引用本身的类型决定的,而当多态发生时,调用哪个类的成员函数是由指针或引用的实际目标对象的类型决定的。
1)基类中定义虚函数,子类提供覆盖版本
2)基类型指针(指向子类对象)或基类型引用(引用子类对象),调用该虚函数
调用虚函数的指针也可以是基类中的this指针,同样能满足多态的2个必要条件,
但在构造和析构函数中除外。
// this.cpp 多态 和 this指针
#include
using namespace std;
class Base {
public:
void foo( /* Base* this */ ) {
cout << "foo函数中调用的为: ";
this->vfun();
}
Base( /* Base* this */ ) {
cout << "构造函数中调用的为: ";
this->vfun();
}
~Base( /* Base* this */ ) {
cout << "析构函数中调用的为: ";
this->vfun();
}
virtual void vfun() { cout << "Base::vfun()" << endl; } // 原始版本
};
class Derived : public Base {
public:
Derived( ) {
//【Base();】定义 基类子对象,利用 基类子对象.Base()
}
~Derived() {
// 对于 基类子对象,利用 基类子对象.~Base()
}
void vfun() { cout << "Derived::vfun()" << endl; } // 覆盖版本
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
Derived d; // 定义d,利用d.Derived()
d.foo(); // foo( &d )
return 0;
} // d.~Derived()
动态绑定(非编译,而是程序执行时的操作):
当编译器看到通过指针或引用,调用虚函数的语句时,并不急于生成有关函数跳转的指令,相反,编译器会用一段代码替代该语句,这段代码在运行时才能被执行,完成如下操作:
1)确定指针或引用的目标对象所占内存空间
2)从目标对象所占内存中间中找到虚表指针
3)利用虚表指针找到虚函数表
4)从虚函数表中获取所调用虚函数的入口地址
5)根据入口地址,调用该虚函数
// vftable.cpp 多态揭秘 -- 虚函数表
#include
using namespace std;
class A { // 编译器根据A类的信息,将制作一张虚函数表 A::foo的地址 A::bar的地址
public:
virtual void foo() { cout << "A::foo" << endl; }
virtual void bar() { cout << "A::bar" << endl; }
};
class B : public A { //编译器根据B类的信息,将制作一张虚函数表 B::foo的地址 A::bar的地址
public:
void foo() { cout << "B::foo" << endl; }
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
A a; // |虚表指针|-->编译器根据A类的信息制作的虚函数表
cout << "a对象的大小:" << sizeof(a) << endl; // 8
B b; // |虚表指针|-->编译器根据B类的信息制作的虚函数表
cout << "b对象的大小:" << sizeof(b) << endl; // 8
void(**pfunc)() = *((void(***)())&a); // 从a中获取虚表指针
pfunc[0](); // A::foo
pfunc[1](); // A::bar
void(**pfunc2)() = *((void(***)())&b); // 从b中获取虚表指针
pfunc2[0](); // B::foo
pfunc2[1](); // A::bar
A* pa = &b;
pa->foo(); // 编译器在编译期间 并不知道 调用 哪个类的foo函数
// 1. 根据pa获取b对象所占内存空间
// 2. 从b对象所占内存空间中 获取 虚表指针
// 3. 根据 虚表指针 找到 编译器根据B类的信息制作的虚函数表
// 4. 从 虚函数表中 获取 虚函数的入口地址
// 5. 利用 函数指针 调用 虚函数
// 调用普通成员函数执行效率高
return 0;
}
动态绑定对性能的拖累:
1) 虚函数表本身会增加进程内存空间的开销
2)与普通函数调用相比虚函数调用要多出几个步骤,增加运行时间的开销
3)无法内联:动态绑定会妨碍编译器通过内联来优化代码
故,只有在确实需要多态特性的场合才建议使用虚函数,负责尽量使用普通函数。
练习:设计一个通用的线程类。
// tread.cpp 多态练习:设计一个通用的线程类
#include
#include
#include
#include
using namespace std;
// 设计一个“通用”的线程类
class Thread {
public:
void start( /* Thread* this */ ) {
pthread_create(&m_tid, NULL, threadfunc, this );
}
static void* threadfunc( void* arg ) {
// 子线程需要的执行操作,就不应该有类的设计者提供
// 子线程需要的执行操作,就应该由用户来提供
// 我们线程处理函数 调用 用户提供的操作
Thread* p = (Thread*)arg;
p->run( );
}
virtual void run( ) { }
private:
pthread_t m_tid;
};
// 以上代码模拟类的设计者
// -------------------------
// 以下代码模拟类的使用者
class MyThread : public Thread {
public:
MyThread( int sec, char ch ) : m_sec(sec), m_ch(ch) {}
void run( ) {
for( ;; ) {
usleep( 1000*m_sec );
cout << m_ch << flush;
}
}
private:
int m_sec;
char m_ch;
};
int main( void ) {
MyThread t1(500,'+'), t2(1000,'*');
t1.start( );
t2.start( );
getchar( );
return 0;
}
运行时类型信息,RTTI。
用于将基类类型的指针或引用,转换为其子类类型的指针或引用。
前提是子类必须从基类多态继承,即基类包含至少一个虚函数。
优于静态类型转换:动态类型转换会对所需转换的基类指针或引用做检查,如果其指向的对象的类型与所要转换的目标类型一致,则转换成功,否则转换失败。
针对指针的动态类型转换,以返回NULL空指针表示失败;
针对引用的动态类型转换,已抛出bad_cast异常表示失败。
// dynamic_pre.cpp 静态类型转换 和 动态类型转换
#include
using namespace std;
class A {};
class B : public A {};
class C : public B {};
class D {};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
B b;
A* pa = &b; // B*-->A* (子类型指针-->基类型指针)可以隐式
cout << "----------------static_cast----------------" << endl;
B* pb = static_cast(pa); // 即安全又合理
cout << "A* pa--->B* pb: " << pb << endl;
C* pc = static_cast(pa); // 不安全 (静态转换没有检查出来)
cout << "A* pa--->C* pc: " << pc << endl;
// D* pd = static_cast(pa); // 不合理 (静态转换检查出来)
// cout << "A* pa--->D* pd: " << pd << endl;
return 0;
}
// dynamicCast.cpp
// 静态类型转换 和 动态类型转换
// 将 基类类型指针 转换为 子类类型指针
// 将 基类类型引用 转换为 子类类型引用
#include
using namespace std;
class A { // 编译器根据A类信息,将制作一张虚函数表 "A"...|A::foo的地址
virtual void foo() {}
};
class B : public A { // 编译器根据B类信息,将制作一张虚函数表 "B"...|A::foo的地址
};
class C : public B { // 编译器根据C类信息,将制作一张虚函数表 "C"...|A::foo的地址
};
class D { // 编译器根据D类信息,不制作虚函数表
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
B b; // |虚表指针|--->编译器根据B类信息制作的虚函数表
A* pa = &b; // B*-->A* (子类型指针-->基类型指针)可以隐式
cout << "----------------dynamic_cast(运行期间做类型转换)----------------" << endl;
B* pb = dynamic_cast(pa); // pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout << "A* pa--->B* pb: " << pb << endl;
C* pc = dynamic_cast(pa); // pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout << "A* pa--->C* pc: " << pc << endl;
D* pd = dynamic_cast(pa); // pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout << "A* pa--->D* pd: " << pd << endl;
cout << "----------------static_cast(编译期间做类型转换)----------------" << endl;
pb = static_cast(pa); // A*-->B* 的反向 B*-->A*可以隐式
cout << "A* pa--->B* pb: " << pb << endl;
pc = static_cast(pa); // A*-->C* 的反向 C*-->A*可以隐式
cout << "A* pa--->C* pc: " << pc << endl;
// pd = static_cast(pa); // A*-->D* 的反向 D*-->A*不可以隐式
// cout << "A* pa--->D* pd: " << pd << endl
return 0;
}
#include <typeinfo>
返回type_info类型对象的常引用——
type_info类的成员函数 name(),返回类型名的字符串
type_info类支持 == 和 != 操作符,可直接用于类型相同与否的判断
当其作用于基类类型的指针或引用,的目标对象时——
1)若基类不包含虚函数,typeid所返回类型信息由该指针或引用本身的类型决定。
2)若基类包含至少一个虚函数,即存在多态继承,typeid所返回类型信息由该指针或
引用的实际目标对象的类型决定。
// typeinfo.cpp
// typeid操作符 - 获取对象的类型信息
// - 无法获取 对象本身的常属性 信息
#include
#include
using namespace std;
class A { // 编译器根据A类信息, 将制作一张虚函数表 "A"...|A::foo的地址
// virtual void foo() {}
};
class B : public A { // 编译器根据B类信息,将制作一张虚函数表 "B"...|A::foo的地址
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
B b; // |虚表指针|--->编译器根据B类信息制作的虚函数表
A* pa = &b;
A& ra = b;
cout << typeid(*pa).name() << endl;//pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout << typeid(ra).name() << endl;//ra->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
int m;
const type_info& rty = typeid(m);
// 1. 获取m的类型信息(类名,类大小,类版本...)
// 2. 创建一个type_info类 对象
// 3. 将 m的类型信息 保存到 type_info类对象的 各个"私有"成员变量中
// 4. 返回 type_info类对象的 常引用
string rn = rty.name();
cout << "m的类型: " << rn << endl;
const int n=10;
cout << "n的类型: " << typeid(n).name() << endl;
cout << ( typeid(m) == typeid(n) ) << endl;
cout << ( typeid(m) != typeid(n) ) << endl;
return 0;
}
任何时候,为基类定义一个虚析构函数总是无害的。
delete一个基类指针(指向子类对象)时,
1)基类析构函数是普通函数:
实际被调用的仅仅是基类的析构函数
基类的析构函数只负责析构子类对象中的基类子对象
基类的析构函数不会调用子类的析构函数
在子类中分配的动态资源无法得到释放:
2)基类析构函数是虚函数:
实际被调用的是子类的析构函数
子类的析构函数将首先释放子类对象自己的成员,然后调用基类的析构函数
基类的析构函数释放该子类对象的基类部分
实现完美的资源释放:
// deletebd.cpp
// 虚 析构函数 - 当delete基类类型指针(指向子类对象)时,能够正确调用子类的析构函数
#include
#include
#include
#include
using namespace std;
class A {
public:
A() : m_a(open("./cfg1", O_CREAT|O_RDWR,0644)) {
//【int m_a=open(...);】定义m_a,初值为文件描述符-->文件表等内核结构(动态资源)
cout << "A() is invoked - 打开了cfg1文件" << endl;
}
virtual ~A() { // 虚函数(原始版本)
close( m_a );
cout << "~A() is invoked - 关闭了cfg1文件" << endl;
// 释放 m_a 本身所占内存空间
}
private:
int m_a;
};
class B : public A {
public:
B() : m_b(open("./cfg2", O_CREAT|O_RDWR,0644)) {
//【A();】定义基类子对象,利用基类子对象.A()
//【int m_b=open(...);】定义m_b,初值为文件描述符-->文件表等内核结构(动态资源)
cout << "B() is invoekd - 打开了cfg2文件" << endl;
}
~B() { // 虚函数(覆盖版本)
close( m_b );
cout << "~B() is invoked - 关闭了cfg2文件" << endl;
// 对于 基类子对象, 利用 基类子对象.~A()
// 释放 基类子对象/m_b 本身所占内存空间
}
private:
int m_b;
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
A* p = new B; // 定义 B类堆对象,利用 B类堆对象.B()
delete p; // p->虚析构函数(~B()) 销毁B类堆对象
return 0;
} // 释放p本身所占内存空间
没有分配任何动态资源的类,无需定义析构函数。
没有定义析构函数的类,编译器会为其提供一个缺省析构函数,但该函数不是虚函数。
因此为了保证delete一个指向子类对象的基类指针时,能正确调用子类的析构函数,就必须把基类的析构函数定义为虚函数,即使它是一个空函数。
任何时候,为基类定义一个虚析构函数总是无害的。
class 类名{
virtual 返回类型 函数名 ( 形参表 ) = 0;
}
拥有纯虚函数的类称为抽象类
抽象类不能实例化为对象
抽象类的子类如果不对基类中的全部纯虚函数有效覆盖,那么该子类也是抽象类
全部由纯虚函数构成的抽象类称为纯抽象类或接口。
// abstract.cpp 纯虚函数 和 抽象类
#include
using namespace std;
class A { // 抽象类
public:
void bar() {}
virtual void foo() = 0; // 纯虚函数
};
class B : public A {
public:
void foo() {
// ...
}
};
// 模拟类的设计者(类库、别人设计的类、自己设计的类)
// --------------------------------
// 模拟用户(使用类的人)
int main( void ) {
// A a;
// new A;
B b;
new B;
return 0;
}