一、背景知识(一些基本概念)

虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。

纯虚函数(Pure Virtual Function):基类中没有实现体的虚函数称为纯虚函数(有纯虚函数的基类称为虚基类)。 C++ “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函 数。通过动态赋值,实现调用不同的子类的成员函数(动态绑定)。正是因为这种机制,把析构函数声明为“虚函数”可以防止在内存泄露。

实例:

#include <iostream>
using namespace std;class base_class
{
public: base_class() { } virtual ~base_class() { } int normal_func() { cout << "This is base_class's normal_func()" << endl; return 0; } virtual int virtual_fuc() { cout << "This is base_class's virtual_fuc()" << endl; return 0; } }; class drived_class1 : public base_class { public: drived_class1() { } virtual ~drived_class1() { } int normal_func() { cout << "This is drived_class1's normal_func()" << endl; return 0; } virtual int virtual_fuc() { cout << "This is drived_class1's virtual_fuc()" << endl; return 0; } }; class drived_class2 : public base_class { public: drived_class2() { } virtual ~drived_class2() { } int normal_func() { cout << "This is drived_class2's normal_func()" << endl; return 0; } virtual int virtual_fuc() { cout << "This is drived_class2's virtual_fuc()" << endl; return 0; } }; int main() { base_class * pbc = NULL; base_class bc; drived_class1 dc1; drived_class2 dc2; pbc = &bc; pbc->normal_func(); pbc->virtual_fuc(); pbc = &dc1; pbc->normal_func(); pbc->virtual_fuc(); pbc = &dc2; pbc->normal_func(); pbc->virtual_fuc(); return 0; }

输出结果:

This is  base_class's normal_func()
This is  base_class's virtual_fuc() This is base_class's normal_func() This is drived_class1's virtual_fuc() This is base_class's normal_func() This is drived_class2's virtual_fuc()

假如将 base_class 类中的 virtual_fuc() 写成下面这样(纯虚函数,虚基类):

// 无实现体
virtual int virtual_fuc() = 0;

那么 virtual_fuc() 是一个纯虚函数,base_class 就是一个虚基类:不能实例化(即不能用它来定义对象),只能声明指针或者引用。读者可以自行测试,这里不再给出实例。

虚函数表(Virtual Table,V-Table):使用 V-Table 实现 C++ 的多态。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向 这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

编译器应该保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。

这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

二、无继承时的虚函数表

#include <iostream>
using namespace std;class base_class
{
public: virtual void v_func1() { cout << "This is base_class's v_func1()" << endl; } virtual void v_func2() { cout << "This is base_class's v_func2()" << endl; } virtual void v_func3() { cout << "This is base_class's v_func3()" << endl; } }; int main() { // 查看 base_class 的虚函数表  base_class bc; cout << "base_class 的虚函数表首地址为:" << (int*)&bc << endl; // 虚函数表地址存在对象的前四个字节 cout << "base_class 的 第一个函数首地址:" << (int*)*(int*)&bc+0 << endl; // 指针运算看不懂?没关系,一会解释给你听 cout << "base_class 的 第二个函数首地址:" << (int*)*(int*)&bc+1 << endl; cout << "base_class 的 第三个函数首地址:" << (int*)*(int*)&bc+2 << endl; cout << "base_class 的 结束标志: " << *((int*)*(int*)&bc+3) << endl; // 通过函数指针调用函数,验证正确性 typedef void(*func_pointer)(void); func_pointer fp = NULL; fp = (func_pointer)*((int*)*(int*)&bc+0); // v_func1()  fp(); fp = (func_pointer)*((int*)*(int*)&bc+1); // v_func2()  fp(); fp = (func_pointer)*((int*)*(int*)&bc+2); // v_func3()  fp(); return 0; }

输出结果:

base_class 的虚函数表首地址为:0x22ff0c
base_class 的 第一个函数首地址:0x472c98
base_class 的 第二个函数首地址:0x472c9c base_class 的 第三个函数首地址:0x472ca0 base_class 的虚函数表结束标志: 0 This is base_class's v_func1() This is base_class's v_func2() This is base_class's v_func3()

简单的解释一下代码中的指针转换:

  • &bc:获得 bc 对象的地址。
  • (int)&bc: 类型转换,获得虚函数表的首地址。这里使用 int 的原因是函数指针的大小的 4byte,使用 int 可以使得他们每次的偏移量保持一致(sizeof(int) = 4,32-bit机器)。
  • (int)&bc:解指针引用,获得虚函数表。
  • (int)(int*)&bc+0:和上面相同的类型转换,获得虚函数表的第一个虚函数地址。
  • (int)(int*)&bc+1:同上,获得第二个函数地址。
  • (int)(int*)&bc+2:同上,获得第三个函数地址。
  • ((int)(int)&bc+3):获得虚函数表的结束标志,所以这里我解引用了。和我们使用链表的情况是一样的,虚函数表当然也需要一个结束标志。
  • typedef void(*func_pointer)(void):定义一个函数指针,参数和返回值都是 void。

对于指针的转换,我就解释这么多了。下面的文章,我不再做解释,相信大家可以举一反三。如果你觉得很费解的话,我不建议继续去看这篇文章了,建议你去补一补基础(《C和指针》是一本很好的选择哦!)。 通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

三、单一继承下的虚函数表

3.1 子类没有重写父类的虚函数

(陈皓文章中用了“覆盖”一词,我觉得太合理,但是我又找不到更合理的词语,所以就用一个句子代替了。^-^)

#include <iostream>
using namespace std;class base_class
{
public: virtual void v_func1() { cout << "This is base_class's v_func1()" << endl; } virtual void v_func2() { cout << "This is base_class's v_func2()" << endl; } virtual void v_func3() { cout << "This is base_class's v_func3()" << endl; } }; class dev_class : public base_class { public: virtual void v_func4() { cout << "This is dev_class's v_func4()" << endl; } virtual void v_func5() { cout << "This is dev_class's v_func5()" << endl; } }; int main() { // 查看 dev_class 的虚函数表  dev_class dc; cout << "dev_class 的虚函数表首地址为:" << (int*)&dc << endl; cout << "dev_class 的 第一个函数首地址:" << (int*)*(int*)&dc+0 << endl; cout << "dev_class 的 第二个函数首地址:" << (int*)*(int*)&dc+1 << endl; cout << "dev_class 的 第三个函数首地址:" << (int*)*(int*)&dc+2 << endl; cout << "dev_class 的 第四个函数首地址:" << (int*)*(int*)&dc+3 << endl; cout << "dev_class 的 第五个函数首地址:" << (int*)*(int*)&dc+4 << endl; cout << "dev_class 的虚函数表结束标志: " << *((int*)*(int*)&dc+5) << endl; // 通过函数指针调用函数,验证正确性 typedef void(*func_pointer)(void); func_pointer fp = NULL; for (int i=0; i<5; i++) { fp = (func_pointer)*((int*)*(int*)&dc+i); fp(); } return 0; }

输出结果:

dev_class 的虚函数表首地址为:0x22ff0c
dev_class 的 第一个函数首地址:0x472d10
dev_class 的 第二个函数首地址:0x472d14 dev_class 的 第三个函数首地址:0x472d18 dev_class 的 第四个函数首地址:0x472d1c dev_class 的 第五个函数首地址:0x472d20 dev_class 的虚函数表结束标志: 0 This is base_class's v_func1() This is base_class's v_func2() This is base_class's v_func3() This is dev_class's v_func4() This is dev_class's v_func5()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

可以看出,v-table中虚函数是顺序存放的,先基类后派生类。

3.2 子类有重写父类的虚函数

#include <iostream>
using namespace std;class base_class
{
public: virtual void v_func1() { cout << "This is base_class's v_func1()" << endl; } virtual void v_func2() { cout << "This is base_class's v_func2()" << endl; } virtual void v_func3() { cout << "This is base_class's v_func3()" << endl; } }; class dev_class : public base_class { public: virtual void v_func1() { cout << "This is dev_class's v_func1()" << endl; } virtual void v_func2() { cout << "This is dev_class's v_func2()" << endl; } virtual void v_func4() { cout << "This is dev_class's v_func4()" << endl; } virtual void v_func5() { cout << "This is dev_class's v_func5()" << endl; } }; int main() { // 查看 dev_class 的虚函数表  dev_class dc; cout << "dev_class 的虚函数表首地址为:" << (int*)&dc << endl; cout << "dev_class 的 第一个函数首地址:" << (int*)*(int*)&dc+0 << endl; cout << "dev_class 的 第二个函数首地址:" << (int*)*(int*)&dc+1 << endl; cout << "dev_class 的 第三个函数首地址:" << (int*)*(int*)&dc+2 << endl; cout << "dev_class 的 第四个函数首地址:" << (int*)*(int*)&dc+3 << endl; cout << "dev_class 的 第五个函数首地址:" << (int*)*(int*)&dc+4 << endl; cout << "dev_class 的虚函数表结束标志: " << *((int*)*(int*)&dc+5) << endl; // 通过函数指针调用函数,验证正确性 typedef void(*func_pointer)(void); func_pointer fp = NULL; for (int i=0; i<5; i++) { fp = (func_pointer)*((int*)*(int*)&dc+i); fp(); } return 0; }

输出结果:

dev_class 的虚函数表首地址为:0x22ff0c
dev_class 的 第一个函数首地址:0x472d50
dev_class 的 第二个函数首地址:0x472d54 dev_class 的 第三个函数首地址:0x472d58 dev_class 的 第四个函数首地址:0x472d5c dev_class 的 第五个函数首地址:0x472d60 dev_class 的虚函数表结束标志: 0 This is dev_class's v_func1() This is dev_class's v_func2() This is base_class's v_func3() This is dev_class's v_func4() This is dev_class's v_func5()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

可以看出:当派生类中 dev_class 中重写了父类 base_class 的前两个虚函数(v_func1,v_func2)之后,使用派生类的虚函数指针代替了父类的虚函数。未重写的父类虚函数位置没有发生变化。

不知道看到这里,你心里有没有一个小问题?至少我是有的。看下面的代码:

virtual void v_func1()
{base_class::v_func1();cout << "This is dev_class's v_func1()" << endl; }

既然派生类的虚函数表中用 dev_class::v_func1 指针代替了 base_class::v_func1,假如我显示的调用

base_class::v_func1,会不会有错呢?答案是没错的,可以正确的调用!不是覆盖了吗?dev_class 已经不知道 base_class::v_func1 的指针了,怎么调用的呢?

如果你想知道原因,请关注这两个帖子:

  • http://stackoverflow.com/questions/11426970/why-can-a-derived-class-virtual-function-call-a-base-class-virtual-fuction-how
  • http://topic.csdn.net/u/20120711/14/fa9cfba2-8814-4119-8290-99e6af2c21f4.html?seed=742904136&r=79093804#r_79093804

四、多重继承下的虚函数表

4.1子类没有重写父类的虚函数

#include <iostream>
using namespace std;class base_class1
{
public: virtual void bc1_func1() { cout << "This is bc1_func1's v_func1()" << endl; } }; class base_class2 { public: virtual void bc2_func1() { cout << "This is bc2_func1's v_func1()" << endl; } }; class dev_class : public base_class1, public base_class2 { public: virtual void dc_func1() { cout << "This is dc_func1's dc_func1()" << endl; } }; int main() { dev_class dc; cout << "dc 的虚函数表 bc1_vt 地址:" << (int*)&dc << endl; cout << "dc 的虚函数表 bc1_vt 第一个虚函数地址:" << (int*)*(int*)&dc+0 << endl; cout << "dc 的虚函数表 bc1_vt 第二个虚函数地址:" << (int*)*(int*)&dc+1 << endl; cout << "dc 的虚函数表 bc1_vt 结束标志:" << *((int*)*(int*)&dc+2) << endl; cout << "dc 的虚函数表 bc2_vt 地址:" << (int*)&dc+1 << endl; cout << "dc 的虚函数表 bc2_vt 第一个虚函数首地址::" << (int*)*((int*)&dc+1)+0 << endl; cout << "dc 的虚函数表 bc2_vt 结束标志:" << *((int*)*((int*)&dc+1)+1) << endl; // 通过函数指针调用函数,验证正确性 typedef void(*func_pointer)(void); func_pointer fp = NULL; // bc1_vt fp = (func_pointer)*((int*)*(int*)&dc+0); fp(); fp = (func_pointer)*((int*)*(int*)&dc+1); fp(); // bc2_vt fp = (func_pointer)*(((int*)*((int*)&dc+1)+0)); fp(); return 0; }

输出结果:

dc 的虚函数表 bc1_vt 地址:0x22ff08
dc 的虚函数表 bc1_vt 第一个虚函数地址:0x472d38
dc 的虚函数表 bc1_vt 第二个虚函数地址:0x472d3c dc 的虚函数表 bc1_vt 结束标志:-4 dc 的虚函数表 bc2_vt 地址:0x22ff0c dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472d48 dc 的虚函数表 bc2_vt 结束标志:0 This is bc1_func1's v_func1() This is dc_func1's dc_func1() This is bc2_func1's v_func1()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

可以看出:多重继承的情况,会为每一个基类建一个虚函数表。派生类的虚函数放到第一个虚函数表的后面。

陈皓在他的文章中有这么一句话:“这个结束标志(虚函数表)的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在 Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。”我在 Windows 7 + Code::blocks 10.05 下尝试,这个值是如果是 -4,表示还有下一个虚函数表,如果是0,表示是最后一个虚函数表。我在 Windows 7 + vs2010 下尝试,两个值都是 0 。

4.2子类重写了父类的虚函数

#include <iostream>
using namespace std;class base_class1
{
public: virtual void bc1_func1() { cout << "This is base_class1's bc1_func1()" << endl; } virtual void bc1_func2() { cout << "This is base_class1's bc1_func2()" << endl; } }; class base_class2 { public: virtual void bc2_func1() { cout << "This is base_class2's bc2_func1()" << endl; } virtual void bc2_func2() { cout << "This is base_class2's bc2_func2()" << endl; } }; class dev_class : public base_class1, public base_class2 { public: virtual void bc1_func1() { cout << "This is dev_class's bc1_func1()" << endl; } virtual void bc2_func1() { cout << "This is dev_class's bc2_func1()" << endl; } virtual void dc_func1() { cout << "This is dev_class's dc_func1()" << endl; } }; int main() { dev_class dc; cout << "dc 的虚函数表 bc1_vt 地址:" << (int*)&dc << endl; cout << "dc 的虚函数表 bc1_vt 第一个虚函数地址:" << (int*)*(int*)&dc+0 << endl; cout << "dc 的虚函数表 bc1_vt 第二个虚函数地址:" << (int*)*(int*)&dc+1 << endl; cout << "dc 的虚函数表 bc1_vt 第三个虚函数地址:" << (int*)*(int*)&dc+2 << endl; cout << "dc 的虚函数表 bc1_vt 第四个虚函数地址:" << (int*)*(int*)&dc+3 << endl; cout << "dc 的虚函数表 bc1_vt 结束标志:" << *((int*)*(int*)&dc+4) << endl; cout << "dc 的虚函数表 bc2_vt 地址:" << (int*)&dc+1 << endl; cout << "dc 的虚函数表 bc2_vt 第一个虚函数首地址::" << (int*)*((int*)&dc+1)+0 << endl; cout << "dc 的虚函数表 bc2_vt 第二个虚函数首地址::" << (int*)*((int*)&dc+1)+1 << endl; cout << "dc 的虚函数表 bc2_vt 结束标志:" << *((int*)*((int*)&dc+1)+2) << endl; // 通过函数指针调用函数,验证正确性 typedef void(*func_pointer)(void); func_pointer fp = NULL; // bc1_vt fp = (func_pointer)*((int*)*(int*)&dc+0); fp(); fp = (func_pointer)*((int*)*(int*)&dc+1); fp(); fp = (func_pointer)*((int*)*(int*)&dc+2); fp(); fp = (func_pointer)*((int*)*(int*)&dc+3); fp(); // bc2_vt fp = (func_pointer)*(((int*)*((int*)&dc+1)+0)); fp(); fp = (func_pointer)*(((int*)*((int*)&dc+1)+1)); fp(); return 0; }

输出结果:

dc 的虚函数表 bc1_vt 地址:0x22ff08
dc 的虚函数表 bc1_vt 第一个虚函数地址:0x472e28
dc 的虚函数表 bc1_vt 第二个虚函数地址:0x472e2c dc 的虚函数表 bc1_vt 第三个虚函数地址:0x472e30 dc 的虚函数表 bc1_vt 第四个虚函数地址:0x472e34 dc 的虚函数表 bc1_vt 结束标志:-4 dc 的虚函数表 bc2_vt 地址:0x22ff0c dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472e40 dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472e44 dc 的虚函数表 bc2_vt 结束标志:0 This is dev_class's bc1_func1() This is base_class1's bc1_func2() This is dev_class's bc2_func1() This is dev_class's dc_func1() This is dev_class's bc2_func1() This is base_class2's bc2_func2()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

是不是感觉很乱?其实一点都不乱!就是两个单继承而已。把多余的部分(派生类的虚函数)增加到第一个虚函数表的最后,CB(Code::Blocks)是这样实现的。我试了一下,vs2010不是这样实现的,读者可以自己尝试一下。本文只针对 CB 来探讨。

有人觉得多重继承不好理解。我想如果你明白了它的虚函数表是怎么样的,也就没什么不好理解了吧。

也许还有人会说,不同的编译器实现方式是不一样的,我去研究某一种编译器的实现有什么意义呢?我个人理解是这样的:

  • 实现方式是不一样的,但是它们的实现结果是一样的(多态)。
  • 无论你了解虚函数表或者不了解虚函数表,我相信你都很少会用到它。但是当你了解了它的实现机制之后,你再去看多态,再去写虚函数的时候[作为你一个coder],相信你的感觉是不一样的。你会感觉很透彻,不会有丝毫的犹豫。
  • 学习编译器这种处理问题的方式(思想),这才是最重要的。[好像扯远了,^-^]。

如果你了解了虚函数表之后,可以通过虚函数表直接访问类的方法,这种访问是不受成员的访问权限限制的(private,protected)。这样做是很危险的,但是确实是可以这样做的。这也是C++为什么很危险的语言的一个原因……

写到这里,文章也就基本结束了。作为读者的你,看完之后,你不是产生了许多其他的问题呢?作为笔者的我,有了新几个问题[我这人问题特别多。^-^]比如:

  • 访问权限是怎么实现的?编译器怎么知道哪些函数是public,哪些是protected?
  • 虚函数调用是通过虚函数表实现的,那么非虚成员函数存放在哪里?是怎么实现的呢?
  • 类的成员存放在什么位置?怎么继承的呢?[这是对象布局问题,=.=]

你知道的越多,你感觉你知道的越少。推荐大家一本书吧,《深度探索C++对象模型》(英文名字是《Inside to C++ Object Model》),看完你会明白很多。


感谢阅读,下面列出参考资料[顺便给大家推荐一下陈皓的博客吧:http://coolshell.cn/,经常去逛逛,会学到很多,至少我是这样觉得的。^-^]:

  • http://blog.csdn.net/haoel/article/details/1948051/
  • http://baike.baidu.com/view/3750123.htm
  • http://www.cnblogs.com/wirelesser/archive/2008/03/09/1097463.html

转载于:https://www.cnblogs.com/alantu2018/p/8446836.html

C++ 虚函数表浅析相关推荐

  1. C++对象的内存布局1---基础篇----C++ 虚函数表解析

    [-] 前言 虚函数表 一般继承(无虚函数覆盖) 一般继承(有虚函数覆盖) 多重继承(无虚函数覆盖) 多重继承(有虚函数覆盖) 安全性 结束语 附录一:VC中查看虚函数表 附录 二:例程 前言 C++ ...

  2. C++中的虚函数表介绍

            在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定.因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义.通常情况下,如果我们不使 ...

  3. 虚函数表剖析,网上转的,呵呵

    http://www.cppblog.com/xczhang/archive/2008/01/20/41508.html C++虚函数表解析(转) C++中的虚函数的作用主要是实现了多态的机制.关于多 ...

  4. C++ 虚函数表解析

    转载自 https://blog.csdn.net/zhou191954/article/details/44919479 C++ 虚函数表解析 前言 C++中的虚函数的作用主要是实现了多态的机制.关 ...

  5. C++对象内存布局--①测试虚函数表属于类

    C++对象内存布局--①测试虚函数表属于类 测试1:同一个类的多个对象共享同一张虚函数表.   //虚函数表.cpp //2010年8月18日 //测试虚函数表,说明虚函数表属于类所有.同一个类的多个 ...

  6. C++迟后联编和虚函数表

    先看一个题目: class Base { public:virtual void Show(int x){cout << "In Base class, int x = &quo ...

  7. 虚函数表 vtable

    如果一个类包含了虚函数,那么在创建对象时会额外增加一张表,表中的每一项都是虚函数的入口地址.这张表就是虚函数表,也称为 vtable. 可以认为虚函数表是一个数组. 为了把对象和虚函数表关联起来,编译 ...

  8. 图解C++虚函数 虚函数表

    图解C++虚函数 2016年07月02日 17:47:17 海枫 阅读数:5181 标签: 虚函数c++g++对象模型C++虚函数更多 个人分类: C/C++/linux 版权声明:本文为博主原创文章 ...

  9. C/C++杂记:虚函数的实现的基本原理 虚函数表

    Malecrab 博客园 首页 新随笔 联系 订阅 管理 C/C++杂记:虚函数的实现的基本原理 1. 概述 简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函 ...

最新文章

  1. static关键字的作用?
  2. mysql 虚表_mysql虚拟表
  3. Redis在持久化时产生的延迟
  4. Django models模型
  5. Oracle--plsql异常处理
  6. java并发编程之thread.join()方法详解
  7. javascript编辑器
  8. java 正则regex_Java中的正则表达式– Java Regex示例
  9. myeclipse 8.5安装freemarker插件方法
  10. 公路堵车概率模型:Nagel-Schreckenberg 模型模拟
  11. 自抗扰控制的入门学习(一)—— 前言
  12. Qt Designer界面简介
  13. 话剧《燃烧的梵高》:梵高的世界并非理所当然
  14. 3D俯视角色割草游戏模板+视频教程,免费发布 | 一周精品推荐
  15. 基于深度学习的依存句法分析进展
  16. 为何国外的人都爱用电子邮箱?注册电子邮箱有哪些好处呢?
  17. 教你怎么打印出实际大小的身份证
  18. JS报错-TypeError: xxx is not a function
  19. 有关Web常用字体的研究?
  20. Fast Adaptive Similarity Search through Variance-Aware Quantization (ICDE 2022)

热门文章

  1. python 成员运算符_Python的“ in”和“ not in”成员资格运算符
  2. java开发课程表_Java开发人员课程包,折扣高达86%
  3. java java se_Java SE 9:不可变列表的工厂方法
  4. 云和物联网(IoT)是不可分割的,因为物联网需要云来运行和执行
  5. tomcat部署项目启动采坑之UnknownHostException
  6. vscode中vue-cli项目es-lint的配置
  7. ## normalize.css 中文版
  8. 第三堂:Java程序流程控制
  9. ios官方菜单项目重点剖析附项目源码
  10. oracle linux下数据迁移到不同服务器