多态与虚函数(补)
- 静态联编与动态联编的深层次理解
- 多态底层原理
- 示例
- 示例一
- 示例二
- 示例三
- 示例四
- 对象与内存
- 虚析构函数
- 构造函数为什么不能是虚函数?
静态联编与动态联编的深层次理解
我们首先看下面一段代码
class object {
private: int value;
public:
object(int x = 0) : value(x) {}
virtual void add() { cout << "object: :add()" << endl; }
virtual void fun() { cout << "object: :fun()" << endl; }
virtual void print() const
{
cout << "object::print()" <<endl;
}
};
class Base : public object {
private:
int num;
public:
Base(int x = 0) :object(x), num(x + 10) {}
virtual void add() { cout << "Base: :add()" << endl; }
virtual void fun() { cout << "Base: :fun()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
};
class Test : public Base {
private:
int count;
public:
Test(int x = 0) :Base(x), count(x + 10) {}
virtual void add() { cout << "Test: :add()" << endl; }
virtual void print() const
{
cout << "Test: :print()" << endl;
}
virtual void show() { cout << "Test: :show()" << endl; }
};
大家可以试着画一下上面三个类的虚表,此处我就不画了
我们试着想一下在上面这三个类型的基础上运行一下代码是否可以运行通过
int main() {
object* op = nullptr;
Test test;
op = &test;
op->show();
return 0;
}
有的同学可能会说这是可以运行通过的,因为我们的op指向了test对象,这就是错误的理解,该程序在编译时期就会出现错误,为什么呢?
我们知道编译器在编译时是按照类型识别的,而在编译识别的时候op是我们的objecct类型,而其类型中不存在所谓的show函数,所以呢就会在编译时期爆索,有的同学可能还会说但是我们的op指向了test,这是在运行时进行的,我们编译都通过不了更别提运行时了呀,要怎么运行通过呢?只能通过强转来实现,将op强转为test类型 ((Test*)op)->show();,这种方法不建议大家使用,因为会出现不确定因素导致程序崩溃,比如将以上代码改为
int main() {
object* op = nullptr;
object obj;
Test test;
op = &obj;
((Test*)op)->show();
return 0;
}
这样就会崩掉,因为obj中没有show函数。
多态底层原理
在上次我们讲述了多态底层是通过虚表指针查虚表来实现多态的,而虚表指针是如何查表的呢?
我们通过汇编语言来理解一下,
这就是运行时的多态,而编译时的多态便是在编译时通过类型名绑定了对象,也就确定了要对ecx赋值的值,不存在查表这一系列操作。这也就是动态联编和静态联编的区别,注意对指针解引用再次通过对象点调用函数仍然是动态联编,((*ob).add())
示例
示例一
我们观察以上代码会出现什么问题?
在我们使用memset函数时对this指针进行了操作,这就使得我们该对象的虚表指针也被置为0了,也就是说虚表指针成为了野指针,导致了虚表无法被查找,无法通过指针调用虚函数。所以我们对this指针操作需要谨慎。
示例二
我们阅读上面代码,想一下运行结果是什么,是为什么这么调用呢?
我们在调用show函数时,函数中调用print函数使用的是this指针调用,很显然调用的是基类的print函数,但是传过去的this指针是Base指针,而在print函数中调用了add函数,这里也是使用this指针调用,而传过来的指针是Base指针,所以我们查虚表也是查的Base的虚表。
运行结果:
示例三
观察上面代码,想一想运行结果是什么?
很显然我们创建Base对象的时候首先会创建obj对象,所以首先会调用obj的构造函数,此时虚表指针指向obj的虚表,所以add(12)调用的是obj的add函数,然后创建完成之后创建Base对象,此时虚表指针指向Base虚表,所以此处的add查找的是Base的add,然后析构开始进入Base的析构函数(重置虚表指针),在~Base的时候调用add调用的是本类型的add函数,然后析构基类,在析构派生类的时候虚表指针已经被重置,指向了obj的虚表,所以析构基类时查找的也是obj的虚表。注意:::析构函数是静态联编,按照其类型名析构,防止编写程序时派生类对象已经析构了还通过该对象查找该类的虚表。(虚表在数据区)
示例四
思考一下以上代码会出现什么样的情况?
我们会发现在Base类中虚函数是私有的,我们会思考 在Base中func函数是私有的,可不可以调用。我们从程序主函数开始看,op是obj类型的,而obj中的func是共有的,所以呢编译时是可以通过的,我们将编译器给骗过去了,而运行的时候又是动态联编,因为编译时我们确定看初始值形参a=10,所以呢在动态联编的时候我们又是从虚表中进行查找不会经过private这一步,所以呢我们仍调用的是Base类型中的func函数,但是b的值变成了10,这就是我们编译时确定了的形参。输出结果:
这个实例将动态联编和静态联编的使用达到了极致,希望大家可以都可以理解。
对象与内存
class object {
private: int value;
public:
object(int x = 0) : value(x) {}
void func(int a = 10) {
cout << "object::func a" <<a<<" value:" << value << endl;
}
void print() const
{
cout << "object::print:" <<endl;
}
};
int main() {
object* op = nullptr;
//object obj;
//op = &obj;
op->print();
return 0;
}
我们观察上面代码,才可是否可以运行?
答案是可以运行的,因为在print函数中,不存在对this指针的使用,直接输出。
而通过op调用func函数时就会报错,this指针为空。
我们对以上代码进行了修改如下:
class object {
private: int value;
public:
object(int x = 0) : value(x) {}
void func(int a = 10) {
cout << "object::func a" <<a<<" value:" << value << endl;
}
virtual void print() const//2
{
cout << "object::print:" <<endl;
}
};
int main() {
object obj(11);//3
object* op = (object*)malloc(sizeof(object));//1
(*op) = obj;//3
new(op) object(obj);//4
op->print();
op->func();
return 0;
}
我们首先知道当使用op调用print函数时会出现崩溃的情况,因为不存在this指针为nullptr,所以呢我们加入了1操作,申请了空间给op指针,这样我们就可以运行出结果,只是输出的value是随机值,因为我们只申请了空间但是没有赋值。为此我们进行了2操作,将print改成了虚函数,这样呢程序就会崩溃,因为不存在虚表指针来调用虚函数。然后我们添加了3操作,希望可以创建一个对象,通过对象与对象之间赋值来改变,但是这样呢程序仍然会崩溃,因为默认的对象与对象之间的赋值函数不会对虚表指针也进行赋值。所以我们使用了定位new,也就是通过系统操作把新的obj对象赋值给op,这样也就实现了对虚表指针的赋值,程序也就不会崩溃。
虚析构函数
首先我们给出一段代码,思考下面代码出现的问题。
class object {
private: int value;
public:
object(int x = 0) : value(x) {}
~object() {}
virtual void print(int x) //2
{ cout << "object::print:" <<x<<endl; }
};
class Base :public object {
int num;
public:
Base(int x = 0) :object(x + 10), num(x) { }
~Base() {}
void print(int x) { cout << "Base::print:" << x << endl; }
};
int main() {
object* op = new Base(10);
op->print(1);
delete op;
return 0;
}
上面代码我们会发现在调用print的时候调用的是Base的print函数,但是我们delete时就会只析构object对象导致出现内存泄漏,怎么解决这种问题呢?就是将obj的析构函数设置成虚析构函数,就可以实现动态析构。
根据赋值兼容规则,可以用基类的指针指向派生类的对象,如果使用基类指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,可能存在内存泄漏问题。也就是上面代码出现的问题。
总结:在实现运行时多态,不管怎样调用析构函数都必须保证不出错,所以必须把析构函数定义为虚函数。类中没有虚函数就不要把析构函数定义为虚。
构造函数为什么不能是虚函数?
- 构造函数的用途:创建对象,初始化对象中的属性,类型转换。
- 在类中定义了虚函数就会有一个虚函数表,对象模型中就含有一个指向虚表的指针。在定义对象时构造函数设置虚表指针指向虚表。
- 构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
- 如果构造函数可以定义为虚构造函数,使用指针调用虚析构造函数,如果编译器采用静态联编,构造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数,相当于已经实例化的对象在调用构造函数这是不允许的,对象的构造函数只能执行一次。
- 如果指针可以调用需构造函数,通过查虚表调用构造函数,那么指针为nullptr就会出现错误。
- 构造函数在编译时确定,如果是虚函数,编译器怎么知道你想构建是继承树上的哪一种呢?