文章目录

  • 前言
  • 问题
  • 测试环境
  • 开始测试
    • 无虚函数简单类结构
    • 包含虚函数的类结构
      • 单继承
      • 多继承
      • 菱形继承
      • 虚继承
  • 总结

前言

最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个含有虚函数基类的子类来说,指向虚函数表的指针应该不止一个。

问题

虚函数表的问题是从C++多态的概念引出的,要想实现多态有3个条件:

  1. 存在继承:没有继承就没有多态(运行时),在多态中必须存在有继承关系的父类和子类。
  2. 重写函数:父类中需要定义带有 virtual 关键字的函数,而在子类中重写一个名字和参数与父类中定义完全相同的函数。
  3. 向上转型:将父类的指针和引用指向子类的对象。

满足以上三个条件,当使用父类的指针调用带有 virtual 关键字的函数时,就会产生多态行为。

实现这种多态表现的核心内容就是虚函数表,对于带有 virtual 关键字的函数地址会被放入一个表格,而在类中会有一个指向虚函数表的指针指向这个表格,表明这个表格属于类的一部分。

对于父类来说,这个表格中都是自己类的虚函数,而对于子类来说,首先这个虚函数表包含父类中所有的虚函数,当子类重写某个虚函数时就会用子类重写后的函数地址替换原来父类中定义的函数地址,同时在子类的虚函数表中还会包含子类独有的虚函数。

由此可见虚函数表的不同和复杂性还是体现在子类上,所以之后会分别测试单继承、多继承、菱形继承三种情况下虚函数表的不同,主要看一下虚函数表的个数和内存布局情况。

测试环境

首先来说明一下测试环境,测试工具是VS2013,对于int *p; sizeof(p)的结果是4,说明编译环境是32位的,这个对后面查看内存结构非常关键。

开始测试

使用VS2013查看类的内存布局非常方便,因为类的大小在编译期间就已经确定了,不用运行就可以通过添加编译选项知道类的大小和布局,而指向虚函数表的指针也会占用类的大小,如果说编译的时候确定了类的大小,那从侧面也说明了在编译期间虚函数表实际上也确定了。

使用VS2013查看类的布局时,可以在项目的属性页:“配置属性”–>“C/C++”–>“命令行”中输入以下任意一个命令,

  • /d1reportAllClassLayout :这个选项可以在VS的输出窗口显示所有相关联的类结构,因为一些外部类也会显示,最终的内容会非常多,需要自己辨别有用的信息。
  • /d1reportSingleClassLayoutXXX :这个选项只会在输出窗口显示指定的类结构,只需要将XXX替换成想显示的类的名字即可,缺点就是无法同时显示多个想查看的类。

无虚函数简单类结构

在查看虚函数表的结构之前,先使用之前的编译参数来查看一下简单的类结构,排除虚函数的干扰,能更清楚的了解类成员在类中的布局情况,有一点需要提一下,成员变量会占用类的大小,但是成员函数不会,如果有虚函数,所有的虚函数会被放入一个表格,而在类中放置一个指向虚函数表的指针,来看一下简单代码:

class CBase
{public:void func() {}
public:int m_var1;
};class CDerived : public CBase
{public:void func() {}
public:int m_var2;
};

编译输出的类的内存布局为:

1>  class CBase    size(4):
1>      +---
1>   0  | m_var1
1>      +---
1>
1>  class CDerived  size(8):
1>      +---
1>      | +--- (base class CBase)
1>   0  | | m_var1
1>      | +---
1>   4  | m_var2
1>      +---

从上面的输出内容来看,很清楚的可以看到基类 CBase 的大小 size(4) 占用4个字节,只有一个成员变量 m_var1,在类中偏移量为0的位置,而派生类 CDerived 占用8个字节大小,第一个成员继承自基类 CBasem_var1,在类中偏移量为0的位置,还有一个子类独有的成员变量 m_var2,在类中偏移量为4的位置。

掌握着这种简单类的查看类结构的方法,接下来开始看一下包含虚函数的类的内存布局。

包含虚函数的类结构

查看包含虚函数的类结构相对来说麻烦一点,先来说两个符号,免得一会看见结构发懵,vfptr 表示类中指向虚函数表的指针,通常放在类的起始位置,比成员变量的位置都要靠前, vftable 表示类中引用的虚函数表,在具体分析是还有有一些修饰符,用来表明是谁的虚函数表。

单继承

这种情况的下的子类的虚函数表很简单,在该子类的内存布局上,最开始的位置保存了一个指向虚函数表的指针,虚函数表中包含了从父类继承的虚函数,当子类中重写父类虚函数时会将虚函数表中对应的函数地址替换,最后添加上自己独有的虚函数地址,下面上代码分析一下:

class CBase
{public:void func0() {}virtual void func1() {}virtual void func2() {}
public:int m_var1;
};class CDerived : public CBase
{public:virtual void func2() {}virtual void func3() {}void func4() {}
public:int m_var2;
};

上面这两个类的内存布局情况如下:

1>  class CBase size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | m_var1
1>      +---
1>
1>  CBase::$vftable@:
1>      | &CBase_meta
1>      |  0
1>   0  | &CBase::func1
1>   1  | &CBase::func2
1>
1>  CBase::func1 this adjustor: 0
1>  CBase::func2 this adjustor: 0
1>
1>
1>  class CDerived  size(12):
1>      +---
1>      | +--- (base class CBase)
1>   0  | | {vfptr}
1>   4  | | m_var1
1>      | +---
1>   8  | m_var2
1>      +---
1>
1>  CDerived::$vftable@:
1>      | &CDerived_meta
1>      |  0
1>   0  | &CBase::func1
1>   1  | &CDerived::func2
1>   2  | &CDerived::func3
1>
1>  CDerived::func2 this adjustor: 0
1>  CDerived::func3 this adjustor: 0

看起来是不是比没有虚函数时复杂多了,不过不要着急,从上到下慢慢分析就好了,这次的基类 CBase 大小是8个字节,首先是{vfptr}这个指向虚函数表的指针,在类中的偏移量是0,接下来是成员变量 m_var1,在类中偏移量是4。

然后是 CBase::$vftable@ 表示基类 CBase 的虚函数表,其中第一行 &CBase_meta 看起来怪怪的,这里我们不展开(因为我也没弄太懂),应该是和虚函数表相关的元数据,第二行是一个0,看起来是一个偏移量,这里没有偏移,当出现偏移时我们再试着分析(相信我,马上就会出现),第三行内容 &CBase::func1 是自己类的虚函数,前面有一个0,应该是指该虚函数在虚函数表中索引,第四行也是相同的情况。

接下来出现了两行非常相似的内容,看一下CBase::func1 this adjustor: 0,这句代码中的关键是 adjustor,其实有是一个偏移量,据说涉及到thunk技术,据说“thunk其实就是一条汇编指令,操作码是0xe9,就是jmp,后面紧跟操作数”,这里我们就不展开了,如果后面弄明白了可以单独写一篇总结,到此为止基类的内存结构就分析完了。

继续看派生类 CDerived,它的大小是12个字节,内部结构首先是 {vfptr} 一个指向虚函数表的指针,偏移量为0,m_var1 是从父类继承的成员变量,偏移量为4,而 m_var2 是自己类独有的成员变量,偏移量是8。

然后看派生类对应的虚函数表 CDerived::$vftable@,跳过前两行直接看一下后面几个函数,发现只有 func1 是基类的,而函数 func2func3 都是派生类的,出现这种情况的原因是子类重写了函数 func2func3 ,所以用重写后的函数地址替换了从基类继承的虚函数,造成了目前看到的状况。

最后又出现了两行 adjustor,很奇怪为什么 func1 函数没有 adjustor,貌似这个 adjustor 只对当前类有效,先留个疑问,接下来看一下多继承。

多继承

当多个父类中都包含虚函数的时候,和子类关联的虚函数表就不止一个了,这个情况是可以通过使用sizeof(子类)来简单验证的:

这一部分是在没有VS的情况下预先写下的,本来考虑使用VS展开布局后,这一段就没有什么必要了,但是后来想想还是留着吧,因为这一段使用的g++编译器,64位环境,每个指针占用8个字节,通过不同的环境调试,更加可以证明,多继承下的多个虚函数表的存在性:

class W
{public:long n;
public:void func(){}
};

对于这样的一个简单类,sizeof(W) = 8,类的大小等于成员变量的大小。

class W1
{public:long n1;
public:virtual void func1(){}
};class W2
{public:long n2;
public:virtual void func2(){}
};

对于上面这两个简单的包含虚函数的类,sizeof(W1) = 16,sizeof(W2) = 16,因为每个类都除了一个 long 类型的成员变量以外,还包含了指向虚函数的一个指针,所以类的大小是16个字节。

class WW : public W1, public W2
{public:long nn;
public:virtual void func(){}
};

而继承了 W1W2 这两个父类的子类 WW 在继承了两个成员变量 n1n2 之外,还有自己的成员变量 nn,三个变量占用字节24个,而计算类 WW 的的大小 sizeof(W1) = 40,也就是说除了成员变量24个字节,还剩余了16个字节的空间没有着落,我们知道它至少包含一个指向虚函数表的指针,占用8个字节的大小,还剩8个字节没有找到用处,从此处分析应该还有一个指向虚函数表的指针,具体的情况可以看一下内存分布。

接下来和单继承的分析方法一样,写代码编译查看布局:

class CBase0
{public:void func0() {}virtual void func1() {}virtual void func2() {}virtual void func3() {}
public:int m_var0;
};class CBase1
{public:void func0() {}virtual void func2() {}virtual void func3() {}virtual void func4() {}
public:int m_var1;
};class CDerived : public CBase0, public CBase1
{public:virtual void func1() {}virtual void func2() {}virtual void func4() {}virtual void func5() {}void func6() {}
public:int m_var2;
};

上面3个类描述了一个简单的多继承的情况,之所以写这么多函数就是构建一种,既有虚函数覆盖,又有单独不被覆盖的情况,下面展示了这段代码的内存布局。

1>  class CBase0    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | m_var0
1>      +---
1>
1>  CBase0::$vftable@:
1>      | &CBase0_meta
1>      |  0
1>   0  | &CBase0::func1
1>   1  | &CBase0::func2
1>   2  | &CBase0::func3
1>
1>  CBase0::func1 this adjustor: 0
1>  CBase0::func2 this adjustor: 0
1>  CBase0::func3 this adjustor: 0
1>
1>
1>  class CBase1    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | m_var1
1>      +---
1>
1>  CBase1::$vftable@:
1>      | &CBase1_meta
1>      |  0
1>   0  | &CBase1::func2
1>   1  | &CBase1::func3
1>   2  | &CBase1::func4
1>
1>  CBase1::func2 this adjustor: 0
1>  CBase1::func3 this adjustor: 0
1>  CBase1::func4 this adjustor: 0
1>
1>
1>  class CDerived  size(20):
1>      +---
1>      | +--- (base class CBase0)
1>   0  | | {vfptr}
1>   4  | | m_var0
1>      | +---
1>      | +--- (base class CBase1)
1>   8  | | {vfptr}
1>  12  | | m_var1
1>      | +---
1>  16  | m_var2
1>      +---
1>
1>  CDerived::$vftable@CBase0@:
1>      | &CDerived_meta
1>      |  0
1>   0  | &CDerived::func1
1>   1  | &CDerived::func2
1>   2  | &CBase0::func3
1>   3  | &CDerived::func5
1>
1>  CDerived::$vftable@CBase1@:
1>      | -8
1>   0  | &thunk: this-=8; goto CDerived::func2
1>   1  | &CBase1::func3
1>   2  | &CDerived::func4
1>
1>  CDerived::func1 this adjustor: 0
1>  CDerived::func2 this adjustor: 0
1>  CDerived::func4 this adjustor: 8
1>  CDerived::func5 this adjustor: 0

内容很多,前面两个基类 CBase0CBase1 的布局很简单,参照之前的分析很容易看懂,直接从派生类看起吧。

我们发现派生类 CDerived 中确实有两个指向虚函数表的指针,接下来看一下这两个虚函数表,这个虚函数表和前面遇到的格式一样,除了第一行的元数据,第二行的诡异偏移量0,剩下的虚函数指针有的是从基类继承来的,有的是被当前派生类覆盖的,还有派生类自己独有的。

而第二个虚函数表就有点意思了,首先是少了 &CDerived_meta 这一行,然后偏移量终于不是0了,而是-8,从派生类 CDerived 的内存布局上来看,以下开始大胆假设,至于小心求证的部分放到以后来做(看自己的进步状态了)。

第二个指向虚函数表的指针是不是距离类的起始偏移量是8,我猜这个-8的意思就是指的这个偏移量,这个值有可能被后面使用,第二行出现了 &thunk: this-=8; goto CDerived::func2,其中包含 thunk 字样,表示这个 func2 不归我管,你去-8偏移量的那个虚函数表里找一找。

还有一点你有没有发现 func5 这个函数只在第一个虚函数表中出现,而没有出现在第二个虚函数表中,这也是一个规则,自己独有的虚函数放到第一个虚函数表中,这可能也是为什么只有第一个虚函数表包含元数据行。

最后一点,我们发现对于函数 func4 来说 adjustor 终于不是0了,而值变成了8,仿佛在说这个虚函数只在偏移量的为8的位置。

菱形继承

对于这一部分,并没有太多新的内容,只是简单的菱形继承中,最初的基类在最终的子类中会包含两份,而虚函数的样子并没有太大的不同,接下来简单看一下代码和对应的内存布局即可,因为菱形继承并不被提倡,所以也不用花太多时间来分析这个问题。

class CSuper
{public:virtual void func0() {}virtual void func1() {}
public:int m_var;
};class CBase0 : public CSuper
{public:virtual void func1() {}virtual void func2() {}
public:int m_var0;
};class CBase1 : public CSuper
{public:virtual void func1() {}virtual void func3() {}
public:int m_var1;
};class CDerived : public CBase0, public CBase1
{public:virtual void func1() {}virtual void func3() {}virtual void func4() {}
public:int m_var2;
};
1>  class CSuper    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | m_var
1>      +---
1>
1>  CSuper::$vftable@:
1>      | &CSuper_meta
1>      |  0
1>   0  | &CSuper::func0
1>   1  | &CSuper::func1
1>
1>  CSuper::func0 this adjustor: 0
1>  CSuper::func1 this adjustor: 0
1>
1>
1>  class CBase0    size(12):
1>      +---
1>      | +--- (base class CSuper)
1>   0  | | {vfptr}
1>   4  | | m_var
1>      | +---
1>   8  | m_var0
1>      +---
1>
1>  CBase0::$vftable@:
1>      | &CBase0_meta
1>      |  0
1>   0  | &CSuper::func0
1>   1  | &CBase0::func1
1>   2  | &CBase0::func2
1>
1>  CBase0::func1 this adjustor: 0
1>  CBase0::func2 this adjustor: 0
1>
1>
1>  class CBase1    size(12):
1>      +---
1>      | +--- (base class CSuper)
1>   0  | | {vfptr}
1>   4  | | m_var
1>      | +---
1>   8  | m_var1
1>      +---
1>
1>  CBase1::$vftable@:
1>      | &CBase1_meta
1>      |  0
1>   0  | &CSuper::func0
1>   1  | &CBase1::func1
1>   2  | &CBase1::func3
1>
1>  CBase1::func1 this adjustor: 0
1>  CBase1::func3 this adjustor: 0
1>
1>
1>  class CDerived  size(28):
1>      +---
1>      | +--- (base class CBase0)
1>      | | +--- (base class CSuper)
1>   0  | | | {vfptr}
1>   4  | | | m_var
1>      | | +---
1>   8  | | m_var0
1>      | +---
1>      | +--- (base class CBase1)
1>      | | +--- (base class CSuper)
1>  12  | | | {vfptr}
1>  16  | | | m_var
1>      | | +---
1>  20  | | m_var1
1>      | +---
1>  24  | m_var2
1>      +---
1>
1>  CDerived::$vftable@CBase0@:
1>      | &CDerived_meta
1>      |  0
1>   0  | &CSuper::func0
1>   1  | &CDerived::func1
1>   2  | &CBase0::func2
1>   3  | &CDerived::func4
1>
1>  CDerived::$vftable@CBase1@:
1>      | -12
1>   0  | &CSuper::func0
1>   1  | &thunk: this-=12; goto CDerived::func1
1>   2  | &CDerived::func3
1>
1>  CDerived::func1 this adjustor: 0
1>  CDerived::func3 this adjustor: 12
1>  CDerived::func4 this adjustor: 0

虚继承

解决菱形继承的一个常用的办法就是改为虚继承,实际上虚继承中就是将从最基类中继承的公共部分提取出来放在最子类的末尾,然后在提取之前的位置用一个叫做vbptr的指针指向这里。

之前看到过一种说法:

虚继承内部实现也相当复杂,似乎破坏了OO的纯洁性

至于复杂不复杂,看看后面的内存布局就很清楚了,那是相当复杂,其中出现了各种偏移,简单了解下就行了,如果不是维护老代码,谁现在还写这样的结构。

class CSuper
{public:virtual void func0() {}virtual void func1() {}
public:int m_var;
};class CBase0 : virtual public CSuper
{public:virtual void func1() {}virtual void func2() {}
public:int m_var0;
};class CBase1 : virtual public CSuper
{public:virtual void func1() {}virtual void func3() {}
public:int m_var1;
};class CDerived : public CBase0, public CBase1
{public:virtual void func1() {}virtual void func3() {}virtual void func4() {}
public:int m_var2;
};
1>  class CSuper    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | m_var
1>      +---
1>
1>  CSuper::$vftable@:
1>      | &CSuper_meta
1>      |  0
1>   0  | &CSuper::func0
1>   1  | &CSuper::func1
1>
1>  CSuper::func0 this adjustor: 0
1>  CSuper::func1 this adjustor: 0
1>
1>
1>  class CBase0    size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | m_var0
1>      +---
1>      +--- (virtual base CSuper)
1>  12  | {vfptr}
1>  16  | m_var
1>      +---
1>
1>  CBase0::$vftable@CBase0@:
1>      | &CBase0_meta
1>      |  0
1>   0  | &CBase0::func2
1>
1>  CBase0::$vbtable@:
1>   0  | -4
1>   1  | 8 (CBase0d(CBase0+4)CSuper)
1>
1>  CBase0::$vftable@CSuper@:
1>      | -12
1>   0  | &CSuper::func0
1>   1  | &CBase0::func1
1>
1>  CBase0::func1 this adjustor: 12
1>  CBase0::func2 this adjustor: 0
1>
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>            CSuper      12       4       4 0
1>
1>
1>  class CBase1    size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | m_var1
1>      +---
1>      +--- (virtual base CSuper)
1>  12  | {vfptr}
1>  16  | m_var
1>      +---
1>
1>  CBase1::$vftable@CBase1@:
1>      | &CBase1_meta
1>      |  0
1>   0  | &CBase1::func3
1>
1>  CBase1::$vbtable@:
1>   0  | -4
1>   1  | 8 (CBase1d(CBase1+4)CSuper)
1>
1>  CBase1::$vftable@CSuper@:
1>      | -12
1>   0  | &CSuper::func0
1>   1  | &CBase1::func1
1>
1>  CBase1::func1 this adjustor: 12
1>  CBase1::func3 this adjustor: 0
1>
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>            CSuper      12       4       4 0
1>
1>
1>  class CDerived  size(36):
1>      +---
1>      | +--- (base class CBase0)
1>   0  | | {vfptr}
1>   4  | | {vbptr}
1>   8  | | m_var0
1>      | +---
1>      | +--- (base class CBase1)
1>  12  | | {vfptr}
1>  16  | | {vbptr}
1>  20  | | m_var1
1>      | +---
1>  24  | m_var2
1>      +---
1>      +--- (virtual base CSuper)
1>  28  | {vfptr}
1>  32  | m_var
1>      +---
1>
1>  CDerived::$vftable@CBase0@:
1>      | &CDerived_meta
1>      |  0
1>   0  | &CBase0::func2
1>   1  | &CDerived::func4
1>
1>  CDerived::$vftable@CBase1@:
1>      | -12
1>   0  | &CDerived::func3
1>
1>  CDerived::$vbtable@CBase0@:
1>   0  | -4
1>   1  | 24 (CDerivedd(CBase0+4)CSuper)
1>
1>  CDerived::$vbtable@CBase1@:
1>   0  | -4
1>   1  | 12 (CDerivedd(CBase1+4)CSuper)
1>
1>  CDerived::$vftable@CSuper@:
1>      | -28
1>   0  | &CSuper::func0
1>   1  | &CDerived::func1
1>
1>  CDerived::func1 this adjustor: 28
1>  CDerived::func3 this adjustor: 12
1>  CDerived::func4 this adjustor: 0
1>
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>            CSuper      28       4       4 0

总结

  1. 虚函数表是用来实现多态的核心内容。
  2. 多继承很强大但是不要滥用,当多个基类都含有虚函数时,派生类会有多个指向虚函数表的指针。
  3. 忘记菱形继承吧,为了取消二义性引入虚继承,结果造成内存分布复杂而又难以理解,大道至简,回归本质吧!

单继承、多继承、菱形继承的虚函数表相关推荐

  1. C++ - 多继承方式会产生多个虚函数表

    1.多重继承可能产生多个虚函数表 代码示例:会产生多个虚函数表 #include <iostream> #include <string>using namespace std ...

  2. Cpp 对象模型探索 / 多重继承虚函数表分析

    一.源码 #include <iostream>class Base1 { public:virtual void func11(){std::cout << "Ba ...

  3. C++单个类的所有对象是否共享虚函数表的验证

    今天偶然看到这个面试题,第一感觉是既然所有对象共享虚函数代码,那么虚函数指针对所有对象就是一样的,因此虚函数表也就是一样的,没有必要为每个对象复制一份一模一样的虚函数表.所以做个验证,在这儿记录一下. ...

  4. 9-4:C++多态之单继承和多继承中的虚函数表

    文章目录 (1)单继承中的虚函数表 (2)多继承中的虚函数表 (1)单继承中的虚函数表 如下继承体系中,fun1函数重写,fun2未被重写,B类中fun3和fun4也被定义为了虚函数 #include ...

  5. c/c++入门教程 - 2.4.6 继承、公共继承、保护继承、私有继承、virtual虚继承(概念、语法、方式、构造和析构顺序、同名成员处理、继承同名静态成员处理、多继承语法、菱形继承、钻石继承)

    目录 4.6 继承 4.6.1 继承的基本语法 4.6.2 继承方式 4.6.3 继承中的对象模型 4.6.4 继承中构造和析构顺序 4.6.5 继承同名成员处理方式 4.6.6 继承同名静态成员处理 ...

  6. 单继承与多继承的虚函数表

    我们大部分时候使用的多态虚表实在public继承的基础上实现的 这句话可以怎么说了? 自己简单的思考逻辑认为基类中的成员或者成员函数子类中都会重新拷贝一份,也就是如果派生类中没有定义基类的某虚函数重写 ...

  7. 虚继承:解决菱形继承问题

    文章目录 一.菱形继承 二.菱形继承问题 三.虚继承:解决菱形继承问题 ```羊驼类继承的实际是两个虚基类指针 vbptr (virtual base pointer),``` 四.虚继承代码 一.菱 ...

  8. C++ 多态(二) : 虚函数、静态绑定、动态绑定、单/多继承下的虚函数表

    文章目录 ⏰1.多态的原理--虚函数表

  9. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  10. C++虚函数,虚函数表,虚继承,虚继承表

    一.虚函数 类中用virtual关键字修饰的函数. 作用:主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的 ...

最新文章

  1. mysql 5.7 xbackup_CentOS 7 下 MySQL 5.7 配置 Percona Xtrabackup
  2. 互联网寒冬前端社招面试
  3. js图片轮换显示实例(转载)
  4. php编写开机启动脚本,设置 msyql php-fpm 开机自动启动脚本
  5. hadoop(一) 基本介绍
  6. zookeeper+kafka+logstash+elasticsearc+kibana
  7. string 找出所有数字 index_发现规律,解决整数转罗马数字
  8. mysql运行状态监控研究内容_如何监控mysql主从的运行状态shell脚本实例介绍
  9. 【iOS开发-74】解决方式:Xcode6下利用preference保存数据,终于的plist文件在哪里?...
  10. 循环增加li id_循环老化对于锂离子电池中锂和电解液分布的影响
  11. 【好玩的代码雨(附源代码
  12. Scintilla教程(5): 选中
  13. 项目管理-----整合项目资源
  14. VS中进行C#编码时智能提示由英文切换为中文
  15. 吟诵,不为吟诵 - 徐健顺
  16. 【Linux】查看linux是centos还是ubuntu的方法
  17. 在c语言中int i k d,c语言int *pInt=(int *)d; 什么意思?
  18. 使用再生龙制作linux系统镜像及还原,再生龙软件备份和还原linux系统
  19. (五)美赛写作篇:Summary官方模板+论文模板
  20. IT 人能在一线城市里生活一辈子吗?

热门文章

  1. Java医院管理系统完整代码_医院管理系统(JAVA代码)
  2. LAMP兄弟连ThinkPHP笔记
  3. 计算机网络技术 虚拟仿真教学实验,虚拟仿真实验教学优秀课件
  4. 最新最全的微信小程序入门学习教程,微信小程序零基础入门到精通
  5. ftl模板引擎遍历list
  6. 如何备份光猫html文件夹,华为光猫分区备份,还原,制作固件教程
  7. 什么是ESAM安全模块
  8. PHP 使用 hprose RPC 服务 系列文章之三——Laravel5.8中使用Hprose
  9. 在OpenWrt系统的路由器NETGEAR WNDR4300上安装KMS服务器vlmcsd
  10. surfacepro4黑苹果触屏_surface pro 4 黑苹果 (surface book 1适用)