关闭

内联和虚函数区别

标签: 编译器虚拟机class汇编iostreamcompiler
254人阅读 评论(0) 收藏 举报

当一个函数是内联和虚函数时,会发生代码替换或使用虚表调用吗? 为了弄清楚内联和虚函数,让我们将它们分开来考虑。通常,一个内联函数是被展开的。

class CFoo {
private:
int val;
public:
int GetVal() { return val; }
int SetVal(int v) { return val=v; }
};
这里,如果使用下列代码:
CFoo x;
x.SetVal(17);
int y = x.GetVal();
那么编译器产生的目标代码将与下面的代码段一样:
CFoo x;a
x.val = 17;
int y = x.val;
    你当然不能这么做,因为val是个私有变量。内联函数的优点是不用函数调用就能隐藏数据,仅此而已。

    虚函数有多态性,意味着派生的类能实现相同的函数,但功能却不同。假设 GetVal 被声明为虚函数,并且你有第二个 以不同方法实现的类 CFoo2:
class CFoo2 : public CFoo {
public:
// virtual in base class too!
virtual int CFoo2::GetVal() { return someOtherVal; }
};
    如果 pFoo是一个 CFoo 或 CFoo2 指针,那么,无论 pFoo 指向哪个类 CFoo 或 CFoo2,成员函数 pFoo->GetVal 都能调用成功。

    如果一个函数既是虚拟函数,又是内联函数,会是什么情况呢?记住,有两种方式建立内联函数,

第一种是在函数定义中使用关键字 inline,如:
inline CFoo::GetVal() { return val; }
第二种是在类的声明中编写函数体,就象前面的 CFoo2::GetVal 一样。所以如果将虚函数体包含在类的声明中,如:
class CFoo {
public:
virtual int GetVal() { return val; }
};
    编译器便认为这个函数 GetVal 是内联的,同时也是虚拟的。那么,多态性和内联特性如何同时工作呢?

    编译器遵循的第一个规则是无论发生什么事情,多态性必须起作用。如果有一个指向 CFoo 对象的指针,pFoo->GetVal 被保证去调用正确的函数。一般情况下,这就是说函数 GetVal 将被实例化为非内联函数,并有vtable(虚表)入口指向它们。但这并不意味着这个函数不能被扩展!再看看下面的代码:
CFoo x;
x.SetVal(17)
int y = x.GetVal()
    编译器知道x是 CFoo,而不是CFoo2,因为这个堆对象是被显式声明的。x肯定不会是CFoo2。所以展开 SetVal/GetVal 内联是安全的。如果要写更多的复杂代码:
CFoo x;
 CFoo* pfoo=&x;
 pfoo->SetVal(17);
 int y = pfoo->GetVal();
 ...
CFoo2 x2;
 pfoo = &x2;
 pfoo->SetVal(17); //etc.
编译器知道 pfoo 第一次指向x,第二次指向x2,所以展开虚拟函数也是安全的。

    你还可以编写更复杂的代码,其中,pfoo 所指的对象类型总是透明的,但是大多数编译器不会做任何更多的分析。即使在前面的例子中,某些编译器将会安全运行,实例化并通过一个虚表来调用。实际上, 编译器总是忽略内联需要并总是使用虚表。唯一绝对的规则是代码必须工作;也就是说,虚函数必须有多态行为。
    通常,无论是显式还是隐式内联,它只是一个提示而已,并非是必须的,就象寄存器一样。编译器完全能拒绝展开一个非虚内联函数,C++编译器常常首先会报 错:“内联中断-函数太大”。如果内联函数调用自身,或者你在某处传递其地址,编译器必须产生一个正常(外联?)函数。内联函数在DEBUG BUILDS中不被展开,可设置编译选项来预防。
    要想知道编译器正在做什么,唯一的方法是看它产生的代码。对于微软的编译器来说,你可以用-FA编译选项产生汇编清单。你不必知道汇编程序如何做。我鼓励你完成这个实验;这对于了解机器实际所做的事情机器有益,同时你可学习许多汇编列表中的内容。
    有关内联函数的东西比你第一次接触它时要复杂得多。有许多种情况强迫编译器产生正常函数:递归,获取函数地址,太大的那些函数和虚函数。但是如果编译器决定实例化你的内联函数,就要考虑把函数放在什么地方?它进入哪个模块?
    通常类在头文件中声明,所以如果某个cpp包含foo.h,并且编译器决定实例化CFoo::GetVal,则在cpp文件中将它实例化成一个静态函数。 如果十个模块包含foo.h,编译器产生的虚函数拷贝就有十个。实际上,可以用虚表指向不同类型的GetVal拷贝,从而是相同类型的对象只产生拷贝。一 些链接器能巧妙地在链接时排除冗余,但一般你是不能指望他来保证的。
    我们得出的结论是:最好不要使用内联虚函数,因为它们几乎不会被展开,即便你的函数只有一行,你最好还是将它与其它的类函数一起放在模块(cpp文件) 中。当然,开发者常常将简短的虚函数放在类声明中-不是因为他们希望这个函数被展开为内联,而是因为这样做更方便和可读性更强。
内联虚函数,编译器将把它看做普通的虚函数。

 


/*向上类型转换和拷贝构造函数拷贝构造函数拷贝构造函数

  如果允许便以其为派生类生成拷贝构造函数,

  它将首先自动调用基类的拷贝构造函数,

  然后再是各成员对象的拷贝构造函数

  */

  #include <iostream>

  using namespace std;

  class Parent{

  int i;

  public:

  Parent(int ii):i(ii){

  cout << "Parent(int ii)\n";

  }

  Parent(const Parent& b):i(b.i){

  cout << "Parent(const Parent&)\n";

  }

  Parent():i(0){cout << "Parent()\n";}

  friend ostream& operator<<(ostream& os,const Parent& b){

  return os << "Parent: " << b.i << endl;

  }

  };

  class Member{

  int i;

  public:

  Member(int ii):i(ii){

  cout << "Member(int ii)\n";

  }

  Member(const Member& m):i(m.i){

  cout << "Member(const Member&)\n";

  }

  friend ostream& operator<<(ostream& os,const Member& m){

  return os << "Member: " << m.i << endl;

  }

  };

  class Child:public Parent{

  int i;

  Member m;

  public:

  Child(int ii):Parent(ii),i(ii),m(ii){

  cout << "Child(int ii)\n";

  }

  Child(const Child& c):Parent(c),i(c.i),m(c.m){//无论何时我们在创建自己的拷贝构造函数时,都要正确地调用基类拷贝构造函数

  cout << "Child(Child&)\n";

  }

  friend ostream& operator<<(ostream& os,const Child& c){

  return os << (Parent&)c << c.m

  << "Child: " << c.i << endl;

  }

  };

  int main(){

  Child c(2);

  cout << "calling copy-constructor: " << endl;

  Child c2 = c;

  cout << "Values in c2:\n" << c2;

  return 0;

  }

[原]C++:对象切片及拷贝构造函数 C++提供了继承机制和虚拟,并通过(且只能通过)指向同一类族的指针或者引用来实现多态,否则多态不起作用。原因之一就是这里要说一下的著名的对象切片(Object slicing)问题。

无虚拟机制的继承的切片问题
首先,类中毫无疑问地需要有继承和虚拟。没有这两者,就不存在多态(注意,重载并不属于多态——个人理解,欢迎来搞)。由于虚拟机制的复杂性,先用一个小例子来说明一下只有继承时的切片问题。假定有两个类:
class MyBase
{
public:
    void Get(){};
    void Set(){};
public:
    int b;
};

class DerivedMyBase: public MyBase
{
public:
    void Print(){};
    void GetD(){};
};
如果有下面的语句:
DerivedMyBase aDMB;
MyBase aMB = aDMB;

那么,通过aMB来访问d或者Print()就是非法的:
// Illegal to access GetD() or Print() through aMB
aMB.GetD();
aMB.Print();

这是因为在将aDMB拷贝给aMB时发生了对象切片,在aMB对象中只有MyBase的信息,所有的关于DerivedMyBase类的信息都被切片了。在“MyBase aMB = aDMB;”还涉及到默认拷贝构造函数的问题,下文会详细描述。

这仅仅是最简单的一种情况。要注意区分下面这种情况:
DerivedMyBase aDMB;
MyBase * pMB = &aDMB;

通过pMB来访问d或者Print()仍然是非法的:
// Illegal to access GetD() or Print() through pMB
pMB->GetD(); // Of course one can use dynamic_cast<> to make this call legal.
pMB->Print();

由于没有虚拟机制,多态在这里仍然不起作用,然而,这里并没有对象切片的发生。因为DerivedMyBase是一个MyBase,所以“MyBase * pMB = &aDMB;”是合法的。而pMB仅仅是一个指针,通过该指针引用的是aDMB,但编译器对于该指针应用对象的了解仅限于MyBase,对于DerivedMyBase类的信息一无所知——这也就是在实践中通常将基类作为抽象类来实现多态的原因,此时派生类中的所有不属于基类的信息都无法通过基类指针或引用来获取,因为编译器在解析该指针或引用指向的内存区时是按照基类的信息来解释的。

对象切片的机理
那么,对象切片是如何发生的?简而言之,是由compiler向拷贝构造函数中插入的代码来做。由于在“MyBase aMB = aDMB;”中由编译器生成的拷贝构造函数不需要对虚拟机制进行额外的处理,此时依照bitwise copy,所有属于DerivedMyBase的信息都丢掉了。而在“ MyBase * pMB = &aDMB;”中,根本就不需要调用copy ctor,所以切片不会发生。

下面,为MyBase和DerivedMyBase加入虚拟机制,看看情况有什么变化:
class MyBase
{
public:
    virtual void Get(){};
    virtual void Set(){};
public:
    int b;
};

class DerivedMyBase: public MyBase
{
public:
    void Print(){};
    void GetD(){};
};

首先编译器会在你的ctor或者编译器为你生成的ctor中加入对虚拟机制的处理代码,这也使得默认拷贝构造函数及对象切片问题变得异常复杂。——此处虚拟机制包括virtual函数和virtual基类。

memberwise copy和bitwise copy
首先说一下深拷贝(memberwise copy)和浅拷贝(bitwise copy)的问题。一般来说,自己定义的copy ctor对于对象的拷贝会有严格的、符合语义的定义(人为错误、破坏因素除外)。然而,无论是自定义的还是默认的ctor,编译器都会插入对虚拟机制的处理代码,这就保证对象切片和拷贝正确的发生——可能会出乎你的意料,但符合C++的语法语义。

虚拟机制与拷贝方式
当类中没有虚拟机制、没有其他类对象的成员时(只包含built-in类型、指针或者数组),默认copy ctor进行的是bitwise copy,这会导致对象切片的发生。然而,当类中有虚拟机制,或者有其他类对象成员时,默认copy ctor采用的是memberwise copy,并且会对虚拟机制进行正确的拷贝。

因为包含虚拟机制的类在定义一个对象时,编译器会向ctor中添加初始化vtable和vbaseclasstable(依赖于具体编译器)的代码,这样可以保证vtable中的内容与类型完全匹配。也就是说MyBase和DerivedMyBase有这相似的VTABLE,但不是完全相同——例如DerivedMyBase中还可以定义自己的virtual函数,这样它的VTABLE就会有更多表项。

而多态的实现是通过将函数调用解析为VTABLE中的偏移量来实现。pMB->Get()可能会被编译器解析成:
(*pMB->__vtable[Offset_of_Get])();

而当MyBase作为虚基类时,访问其中的数据成员可能就是:
pMB->__vBaseClassMyBase->b;

那么,当“aMB = aDMB;”,copy ctor会执行memberwise copy,正确的初始化aMB的VTABLE,而不是仅仅将aDMB的VTABLE拷贝过来。如果是bitwise copy,aMB对象中的VTABLE将是aDMB的,aMB.Get()调用的将是DervieMyBase定义的Get(),这显然是不符合语义和逻辑的。

总而言之
对象切片和copy ctor是一个很复杂的东西,在有虚拟机制的情况下两者是紧密结合在一起的。因为对象切片和拷贝构造函数的问题,不通过指针或者引用无法达到多态的目的。

还有一个问题是赋值拷贝的问题,这个机制更复杂,因此Lippman建议不要再虚基类中使用数据成员。C#和java禁止了多重继承,并将interface作为一个单独的东西,消除了赋值拷贝带来的复杂性。关于赋值拷贝的问题,有机会再讨论。

PS:上述代码均能由g++ 3.4.4编译。对于C++的复杂性,想必很多人都有切身感受。
顺祝ChinaUnix的所有朋友们新年快乐,万事大吉,新年发大财!

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1954次
    • 积分:52
    • 等级:
    • 排名:千里之外
    • 原创:3篇
    • 转载:2篇
    • 译文:0篇
    • 评论:0条
    文章分类
    文章存档