目录
2.1静态联编
2.2动态联编
2.3虚函数面试题
2.3.1构造函数中使用memset函数
2.3.2this指针与虚函数的调用
2.3.3构造析构函数中调用虚函数
2.3.4动态和静态联编与访问属性和默认值
2.3.5动态创建对象时的析构函数
联编是指计算机程序彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把函数的调用和函数的入口地址相结合的过程。
2.1静态联编
静态联编(早期绑定):静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。
C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。C++语言中,函数重载和函数模板也是静态联编;使用对象名加点”."成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译和链接时确定。(称为静态联编)。
2.2动态联编
动态联编亦称滞后联编或晚期绑定: 动联编是指在程序执行的时候才将函数实现和函数调用关联起来。
C++语言中,使用类类型的引用或指针调用虚函数(成员选择符”->"),则程序在运行时选择虚函数的过程,称为动态联编。
2.3虚函数面试题
2.3.1构造函数中使用memset函数
class Object
{
private:
int value;
public:
Object(int x=0):value(x)
{
memset(this,0,sizeof(Object));
}
void func()
{
cout<<"Object::func: "<<value<<endl;
}
virtual void add(int x)
{
cout<<"Object::add: "<<x<<endl;
}
};
int main()
{
Object obj;
Object* op=&obj;
obj.add(1);//执行完这句正常,打印Object::add: 1
op->add(2);//执行这句时系统崩溃
return 0;
}
原因:
在构建对象时执行memset函数将该对象的所有数据全部清零,包括虚表指针。
由于obj.add(1)是静态联编在编译时就已经绑定调用关系,不会查虚表,所以可以正常执行;
而op->add(2)是动态联编在运行时通过查虚表来确定调用关系,但由于该对象的虚表指针被清零变成空指针,查表时对空指针进行解引用导致系统崩溃。
2.3.2this指针与虚函数的调用
class object
{
private:
int value;
public:
object(int x = 0) : value(x) {}
void print()
{
cout << "object::print"<< endl;
add(1);
}
virtual void add(int x)
{
cout << "object::add: " << x << endl;
}
};
class Base : public object
{
private:
int num;
public:
Base(int x = 0) :object(x + 10), num(x) {}
void show()
{
cout << "Base::show"<< endl;
print();
}
virtual void add(int x)
{
cout << "Base::add: " << x << endl;
}
};
int main()
{
Base base;
base.show();
return 0;
}
结果:
原因:
base对象调用show()方法,先打印Base::show,再通过Base类型的this调用print()方法,公有继承的派生类对象能够调用基类的非私有方法。
进入print函数,其this指针为Object类型,被Base类型的this指针所赋值,即this指针指向base对象,打印Object::print。
通过this指针调用add(1)函数,此时this指针是指向base对象的Object类型指针,由于add()函数是虚函数,并且使用指针进行调用,所以要查表,且查的是Base类型的虚表,形成动态联编,调用Base类型的add方法,打印Base::add:1。
空指针能调用函数体内不对this指针解引用的函数:
class Object
{
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<<"print"<<endl;
}
};
int main()
{
Object* op=nullptr;
op->print();//可以运行成功,打印print
op->func();//可以编译通过,但运行崩溃。
return 0;
}
虽然op指针为空指针且print()函数中的this指针被op所赋值也为空指针,但是能够调用print()函数的原因是使用op指针调用print()函数和执行print()函数的过程中没有对op指针和this指针进行解引用,所以就不会崩溃掉。
调用func()函数崩溃的原因是在func()函数中打印value的值对为空的this指针进行了解引用,导致崩溃。
2.3.3构造析构函数中调用虚函数
class object
{
private:
int value;
public:
object(int x = 0) :value(x)
{
cout << "Create object: "<< endl;
add(12);
}
~object()
{
cout << "Destory object" << endl;
add(23);
}
virtual void add(int x)
{
cout << "object::add: " << x << endl;
}
};
class Base : public object
{
private:
int num;
public:
Base(int x = 0) :object(x + 10), num(x)
{
cout << "Create Base "<< endl;
add(1);
}
~Base()
{
cout << "Destroy Base" << endl;
add(2);
}
virtual void add(int x)
{
cout << "Base::add: " << x << endl;
}
};
int main()
{
Base base;
return 0;
}
结果:
原因:
首先创建对象base时调用Base构造函数但不进入,由于Base继承于Object,所以要去调用Object构造函数,在设置Object类成员属性前设置虚表指针指向Object类的虚表,然后设置value值,再进入Object类构造函数体,打印Create object:。
执行add(12),add()函数是虚函数,用this指针调用需要查虚表,但此时虚表指针指向Object类的虚表,所以调用Obejct类的add()方法,打印object::add:12。
再回到Base类构造函数,首先设置虚表指针指向Base类的虚表,设置num值,进入函数体打印Create object:。
执行add(1)要查虚表,但此时虚表为Base类的,所以调用Base类的add()函数,打印Base::add:1。
销毁base对象时调用Base类的析构函数,在进入函数体之前先重置虚表指针指向Base类虚表,然后打印Destroy Base。
执行add(2)要查虚表,此时虚表指针指向Base类的虚表,所以调用Base类的add()函数打印Base::add:2。
再调用Object类的析构函数,同样在进入函数体之前先重置虚表指针指向Object类的虚表,然后打印Destroy object。
执行add(23)要查虚表,此时虚表指向object类的虚表,所以调用Object类的add()函数打印object::add:23。
总结:
在构造函数、拷贝构造函数或者析构函数中调用虚函数,一定是调用本类的虚函数,如果本类没有重写虚函数,则调用基类的虚函数。在构造、拷贝构造、析构函数中调用虚函数时编译器一律采用静态联编的方式,而不使用动态联编。
2.3.4动态和静态联编与访问属性和默认值
class object
{
public:
virtual void func(int a = 10)
{
cout << "object::func: a " << a << endl;
}
};
class Base : public object
{
private://只在编译时有作用,对于运行时没有作用
virtual void func(int b = 20)
/*虚函数的动态绑定是在运行时根据对象的实际类型来确定的,而默认参数值的解析则在编译时完成。
因此,在动态绑定时,默认参数值已经确定,无法再根据实际对象类型去修改。*/
{
cout << "Base::func: b " << b << endl;
}
};
int main()
{
Base base;
object* op = &base;
op->func();
//base.func();//error,编译不通过
return 0;
}
结果:
问题点1:Base类的func方法为私有访问属性,为什么能在外部函数中调用。
问题点2:调用Base类的func方法时,其默认值为20,为什么打印结果为Object类的func的默认值10。
原因:
首先程序要进行编译,要识别类的成员属性类型、成员函数名、返回类型、参数列表、默认值以及其可访问属性。在编译时确定调用关系Base类的func()方法为私有,使用base对象调用无法编译通过;
op->func()可以编译通过,因为op是object类的指针,其func()函数的可访问属性为公有,所以能够编译通过。
且编译时确定该方法的默认值为object类的func方法的默认值10,因为默认值是在编译时与调用者的类型绑定。
但由于func()为虚方法,在调用时是按照动态联编的方式调用,实际调用的是Base类的func()方法,但默认值与调用者的类型绑定为10。
即便Base类的func()方法为私有,但动态联编不会去考虑可访问属性,因为可访问属性是只在编译期确定的,编译完成后就不会考虑可访问属性。
2.3.5动态创建对象时的析构函数
class object
{
private:
int value;
public:
object(int x = 0) :value(x)
{
cout << "Create object: "<< endl;
}
~object()
{
cout << "Destory object" << endl;
}
virtual void add(int x)
{
cout << "object::add: " << x << endl;
}
};
class Base : public object
{
private:
int num;
public:
Base(int x = 0) :object(x + 10), num(x)
{
cout << "Create Base "<< endl;
}
~Base()
{
cout << "Destroy Base" << endl;
}
virtual void add(int x)
{
cout << "Base::add: " << x << endl;
}
};
int main()
{
object* op = new Base(1);
op->add(1);
delete op;
return 0;
}
结果:
问题:构造的对象为Base类型,在程序结束时要销毁对象,应该要调用基类的析构和派生类的构造函数,但现在只调用基类的构造函数。
原因:
对于delete来说,op的类型为object类型的指针,所以它只会调用object类的析构函数。
如果在Base类中动态申请空间,在其析构函数中释放该空间,现在没有调用Base类的析构函数,这样就会存在内存泄漏。
或者如果在Base类构造函数中打开文件,在析构函数中关闭文件,但没有调用析构函数,这样就会造成文件无法关闭。
解决办法:将析构函数设计为虚函数
当动态创建对象时,需要将析构函数也要设计为虚函数,这样delete时就会查虚表,从而调用派生类的析构函数,调用完派生类的析构函数后会再回到基类中,调用基类的析构函数。
正确的结果: