C++标准编程:虚函数与内联

  我们曾经在讨论C++的时候,经常会问到:“虚函数能被声明为内联吗?”现在,我们几乎听不到这个问题了。现在听到的是:“你不应该使print成为内联的。声明一个虚函数为内联是错误的!”

  这种说法的两个主要的原因是(1)虚函数是在运行期决议而内联是一个编译期动作,所以,我们将虚函数声明为内联并得不到什么效果;(2)声明一个虚函数为内联导致了函数的多分拷贝,而且我们为一个不应该在任何时候内联的函数白白花费了存储空间。这样做很没脑子。

  不过,事实并不是这样。我们先来看看第一个:许多情况下,虚拟函数都被静态地决议了——比如在派生类虚拟函数中调用基类的虚拟函数的时候。为什么这样做呢?封装。一个比较明显的例子就是派生类析构函数调用链。所有的虚析构函数,除了最初触发这个析构链的虚析构函数,都被静态的决议了。如果不将基类的虚析构函数内联,我们无法从中获利[a]。这和不内联一个虚拟析构函数有什么不同吗?如果继承体系层次比较深并且有许多这样的类的实例要被销毁的话,答案是肯定的。

  再来看另外一个不用析构函数的例子,想象一下设计一个图书馆类。我们将MaterialLocation作为抽象类LibraryMaterial的一个成员。将它的print成员函数声明为一个纯虚函数,并且提供函数定义:它输出MaterialLocation。

  class LibraryMaterial {

  private:

  MaterialLocation _loc; // shared data

  // …

  public:

  // declares pure virtual function

  inline virtual void print( ostream& = cout ) = 0;

  };

  // we actually want to encapsulate the handling of the

  // location of the material within a base class

  // LibraryMaterial print() method - we just don’t want it

  // invoked through the virtual interface. That is, it is

  // only to be invoked within a derived class print() method

  inline void

  LibraryMaterial::

  print( ostream &os ) { os 《 _loc; }

  接着,我们引入一个Book类,它的print函数输出Title, Author等等。在这之前,它调用基类的print函数(LibraryMaterial::print())来显示书本位置(MaterialLocation)。如下:

  inline void

  Book::

  print( ostream &os )

  {

  // ok, this is resolved statically,

  // and therefore is inline expanded …

  LibraryMaterial::print();

  os 《 "title:" 《 _title

  《 "author" 《 _author 《 endl;

  }

  AudioBook类,派生于Book类,并加入附加信息,比如旁述,音频格式等等。这些东西都用它的print函数输出。再这之前,我们需要调用Book::print()来显示前面的信息。

  inline void

  AudioBook::

  print( ostream &os )

  {

  // ok, this is resolved statically,

  // and therefore is inline expanded …

  Book::print();

  os 《 "narrator:" 《 _narrator 《 endl;

  }

  这和虚析构函数调用链的例子一样,都只是最初调用的虚函数没有被静态决议,其它的都被原地展开。This unnamed hierarchical design pattern is significantly less effective if we never declare a virtual function to be inline.

  那么对于第二个原因中代码膨胀的问题呢?我们来分析一下,如果我们写下:

  LibraryMaterial *p =

  new AudioBook( "Mason & Dixon",

  "Thomas Pynchon", "Johnny Depp" );

  // …

  p->print();

  这个print实例是内联的吗?不,当然不是。这样不得不通过虚拟机制在运行期决议。这让print实例放弃了对它的内联声明了吗?也不是。这个调用转换为下面的形式(伪代码):

  // Pseudo C++ Code

  // Possible transformation of p->print()

  ( *p->_vptr[ 2 ] )( p );

  where 2 represents the location of print within the associated virtual function table.因为调用print是通过函数指针_vptr[2]进行的,所以,编译器不能静态的决定这个调用地址,并且,这个函数也不能内联。

  当然,虚函数print的内联实体(definition)也必须在某个地方表现出来。 即是说,至少有一个函数实体是在virtual table调用的地址原地展开的。编译器是如何决定在何时展开这个函数实体呢?其中一个编译(implementaion)策略是当virtual table生成的同时,生成这个函数实体。这就是说对于每一个派生类的virtual table都会生成一个函数实体。

  在一个可应用的类[b]中有多少vitrual table会被生成呢?呵呵,这是一个好问题。C++标准中对虚函数行为进行了规定,但是没有对函数实现进行规定。由于virtual table没有在C++标准中进行规定,很明显,究竟这个virtual table怎样生成,和究竟要生成多少个vitrual table也没有规定。多少个?当然,我们只要一个。Stroustrup的cfront编译器,很巧妙的处理了这些情况。( Stan and Andy Koenig described the algorithm in the March 1990 C++ Report article, "Optimizing Virtual Tables in C++ Release 2.0.")

  Moreover, the C++ Standard now requires that inline functions behave as though only one definition for an inline function exists in the program even though the function may be defined in different files。新的规则要求编译器只展开一个内联虚函数。如果一点被广泛采用的话,虚函数的内联导致的代码膨胀问题就会消失。

  [译注:C++ Standard: 9.3.8, Member function of local class shall be defined inline in their class defination, if they are defined at all]

  ============================

  译注:

  [a]函数调用开销,调用基类虚函数的时候至少要经过两次间接过程(S. B.Lippman: 《Inside the C++ Object Model》)

  [b]一个产品类(?)

  总结:

  就是虚函数inline在调用链等地方很有用~

  即使没有加入inline声明,作为一个好编译器,都会优化(虚析构函数)

  在很长的函数调用链中,最好将链中基类的函数inline,这样节约开销

  至于在什么地方inline,由编译器决定,因为C++标准没有规定

  新C++标准(可能没有通过)中,规定了,inline化只对产品类有效,且只动作一次

  保证代码不过度膨胀

  inline动作是在产品类实例化同时,和vtable生成一起。

时间: 2024-09-20 00:08:38

C++标准编程:虚函数与内联的相关文章

c++-纯虚析构函数的内联实现

问题描述 纯虚析构函数的内联实现 C++ 纯虚析构函数在VS里面可以内联实现,如: class CBase { public: CBase(); virtual ~CBase() = 0 {}; } 这样写在VS里没有问题,但在Eclipse里面这样写C++编译通不过 解决方案 这是C++ 11的写法吧,你用的编译器不一定支持. 解决方案二: 估计是编译器的版本问题,你查查那两个IDE编译器的具体版本的信息. 解决方案三: 编译器不支持,那你就只能分开写了,写成非inline方式 解决方案四:

拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

 1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: #include<iostream> //如果声明已经定义,边不会生成 class classA { private: int a; int b; public: //拷贝构造的规则,有两种方式实现初始化 //1.一个是通过在后面:a(x),b(y)的方式实现初始化 //2.第二种初始化的方式是直

C++中的虚函数和动态联编

  面向对象程序设计的基本观点是用程式来仿真大千世界,这使得它的各种根本特性非常人性化,如封装.继承.多态等等,而虚函数就是c++中实现多态性的主将.为了实现多态性,c++编译器也革命性地提供了动态联编(或叫晚捆绑)这一特征. 虚拟函数亦是mfc编程的关键所在,mfc编程主要有两种方法:一是响应各种消息,进行对应的消息处理.二就是重载并改写虚拟函数,来实现自己的某些要求或改变系统的某些默认处理. 虚函数的地位是如此的重要,对它进行穷根究底,力求能知其然并知其所以然 对我们编程能力的提高大有好处.

C++如何处理内联虚函数

当一个函数是内联和虚函数时,会发生代码替换或使用虚表调用吗? 为了弄 清楚内联和虚函数,让我们将它们分开来考虑.通常,一个内联函数是被展开的 . 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(); 那么编译器产生的目标代码将与下

C++编程中队内联函数的理解和使用_C 语言

函数调用过程c++经过编译生成可执行程序文件exe,存放在外存储器中.程序启动,系统从外存储器中将可执行文件装载到内存中,从入口地址(main函数起始处)开始执行.程序执行中遇到了对其他函数的调用,就暂停当前函数的执行,并保存下一条指令的地址作为从被调函数返回后继续执行的入口点,保存现场.然后转到被调函数的入口地址执行被调函数.遇到return语句或者被调函数结束后,恢复先前保存的现场,从先前保存的返回地址处继续执行主调函数的其余部分. 内联函数函数调用需要进行现场保护,以便在函数调用之后继续进

C++中内联函数的定义和使用

引入内联函数的目的是为了解决程序中函数调用的效率问题. 函数是一种更高级的抽象.它的引入使得编程者只关心函数的功能和使用方 法,而不必关心函数功能的具体实现:函数的引入可以减少程序的目标代码,实 现程序代码和数据的共享.但是,函数调用也会带来降低效率的问题,因为调用 函数实际上将程序执行顺序转移到函数所存放在内存中某个地址,将函数的程序 内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去前 要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执 行.因此,函数调

C++中的内联函数(inline)

在c++中,为了解决一些频繁调用的小函数大量消耗栈空间或者是叫栈内存的问题,特别的引入了inline修饰符,表示为内联函数. 可能说到这里,很多人还不明白什么是栈空间,其实栈空间就是指放置程序的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,如果频繁大量的使用就会造成因栈空间不足所造成的程序出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭. 下面我们来看一个例子: #include <iostream>#include <string>using na

如何调试C/C++内联函数及过程

这篇文章描述了一种持续的权衡方法,可以在调试程序和使之运行 更快之间取得正确的平衡.这篇文章同时还描述了如何调试内联函数及过程. 编写一个运行得快的程序并不容易.编译器可以帮助将一个程序转换成运行得更加快,但权衡是转换之后的程序与初始的程序会有所不同.在某些作了积极优化转换(aggressive optimization transform)的情形里,从人类的角度看转换后的程序与初始程序相比较已经几乎没有可读的一致性了. 因此,调试有问题的已优化程序会更加困难.这是一个问题,因为为了获得最大化的

深入探讨:宏、内联函数与普通函数的区别_C 语言

内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数.    内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定:而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患.    使用内联函数时,应注意以下问题:    1)内联函数的定