C++ 对象的内存布局

来源:http://blog.csdn.net/haoel/article/details/3081328


前言

07年12月,我写了一篇《C++虚函数表解析》的文章,引起了大家的兴趣。有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的。我在这里一并对大家的留言表示感谢。这也是我为什么再写一篇续言的原因。因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单。不想,这篇文章成为了打开C++对象模型内存布局的一个引子,引发了大家对C++对象的更深层次的讨论。当然,我之前的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数表里,至少有以下这些内容没有涉及:

1)有成员变量的情况。

2)有重复继承的情况。

3)有虚拟继承的情况。

4)有钻石型虚拟继承的情况。

这些都是我本篇文章需要向大家说明的东西。所以,这篇文章将会是《C++虚函数表解析》的一个续篇,也是一篇高级进阶的文章。我希望大家在读这篇文章之前对C++有一定的基础和了解,并能先读我的上一篇文章。因为这篇文章的深度可能会比较深,而且会比较杂乱,我希望你在读本篇文章时不会有大脑思维紊乱导致大脑死机的情况。;-)

对象的影响因素

简而言之,我们一个类可能会有如下的影响因素:

1)成员变量

2)虚函数(产生虚函数表)

3)单一继承(只继承于一个类)

4)多重继承(继承多个类)

5)重复继承(继承的多个父类中其父类有相同的超类)

6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论C++语言上的影响。

本篇文章着重讨论下述几个情况下的C++对象的内存布局情况。

1)单一的一般继承(带成员变量、虚函数、虚函数覆盖)

2)单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)

3)多重继承(带成员变量、虚函数、虚函数覆盖)

4)重复多重继承(带成员变量、虚函数、虚函数覆盖)

5)钻石型的虚拟多重继承(带成员变量、虚函数、虚函数覆盖)

我们的目标就是,让事情越来越复杂。

知识复习

我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:

typedef void(*Fun)(void);Base b;Fun pFun = NULL;cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;// Invoke the first virtual function 
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。

本篇文章中的例程或内存布局主要使用如下编译器和系统:

1)Windows XP 和 VC++ 2003

2)Cygwin 和 G++ 3.4.4

单一的一般继承

下面,我们假设有如下所示的一个继承关系:

注意:在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而子类覆盖了父类的f()方法,孙子类覆盖子类的g_child()及其超类的f()。

我们的源程序如下所示:

class Parent {
public:int iparent;Parent ():iparent (10) {}virtual void f() { cout << " Parent::f()" << endl; }virtual void g() { cout << " Parent::g()" << endl; }virtual void h() { cout << " Parent::h()" << endl; }};class Child : public Parent {
public:int ichild;Child():ichild(100) {}virtual void f() { cout << "Child::f()" << endl; }virtual void g_child() { cout << "Child::g_child()" << endl; }virtual void h_child() { cout << "Child::h_child()" << endl; }
};class GrandChild : public Child{
public:int igrandchild;GrandChild():igrandchild(1000) {}virtual void f() { cout << "GrandChild::f()" << endl; }virtual void g_child() { cout << "GrandChild::g_child()" << endl; }virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
我们使用以下程序作为测试程序:(下面程序中,我使用了一个 int** pVtab  来作为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感到奇怪,)
typedef void(*Fun)(void);GrandChild gc;int** pVtab = (int**)&gc;cout << "[0] GrandChild::_vptr->" << endl;
for (int i=0; (Fun)pVtab[0][i]!=NULL; i++){pFun = (Fun)pVtab[0][i];cout << "    ["<<i<<"] ";pFun();
}
cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;
cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;
cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;
其运行结果如下所示:(在 VC++ 2003 G++ 3.4.4 下)

[0] GrandChild::_vptr->

    [0] GrandChild::f()

    [1] Parent::g()

    [2] Parent::h()

    [3] GrandChild::g_child()

    [4] Child::h1()

    [5] GrandChild::h_grandchild()

[1] Parent.iparent = 10

[2] Child.ichild = 100

[3] GrandChild.igrandchild = 1000


使用图片表示如下:


可见以下几个方面:

1)虚函数表在最前面的位置。

2)成员变量根据其继承和声明顺序依次放在后面。

3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

多重继承

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:


我们的类继承的源代码如下所示:父类的成员初始为102030,子类的为100

class Base1 {
public:int ibase1;Base1():ibase1(10) {}virtual void f() { cout << "Base1::f()" << endl; }virtual void g() { cout << "Base1::g()" << endl; }virtual void h() { cout << "Base1::h()" << endl; }};class Base2 {
public:int ibase2;Base2():ibase2(20) {}virtual void f() { cout << "Base2::f()" << endl; }virtual void g() { cout << "Base2::g()" << endl; }virtual void h() { cout << "Base2::h()" << endl; }
};class Base3 {
public:int ibase3;Base3():ibase3(30) {}virtual void f() { cout << "Base3::f()" << endl; }virtual void g() { cout << "Base3::g()" << endl; }virtual void h() { cout << "Base3::h()" << endl; }
};class Derive : public Base1, public Base2, public Base3 {
public:int iderive;Derive():iderive(100) {}virtual void f() { cout << "Derive::f()" << endl; }virtual void g1() { cout << "Derive::g1()" << endl; }
};
查看子类实例的内存布局:下面程序中,注意我使用了一个 s 变量,其中用到了 sizof(Base) 来找下一个类的偏移量。(因为我声明的是 int 成员,所以是 4 个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)

typedef void(*Fun)(void);Derive d;int** pVtab = (int**)&d;cout << "[0] Base1::_vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << "     [0] ";
pFun();pFun = (Fun)pVtab[0][1];
cout << "     [1] ";pFun();pFun = (Fun)pVtab[0][2];
cout << "     [2] ";pFun();pFun = (Fun)pVtab[0][3];
cout << "     [3] "; pFun();pFun = (Fun)pVtab[0][4];
cout << "     [4] "; cout<<pFun<<endl;cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;int s = sizeof(Base1)/4;cout << "[" << s << "] Base2::_vptr->"<<endl;
pFun = (Fun)pVtab[s][0];
cout << "     [0] "; pFun();Fun = (Fun)pVtab[s][1];
cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];
cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];
out << "     [3] ";
cout<<pFun<<endl;cout << "["<< s+1 <<"] Base2.ibase2 = " << (int)pVtab[s+1] << endl;s = s + sizeof(Base2)/4;cout << "[" << s << "] Base3::_vptr->"<<endl;
pFun = (Fun)pVtab[s][0];
cout << "     [0] "; pFun();pFun = (Fun)pVtab[s][1];
cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];
cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];
cout << "     [3] ";
cout<<pFun<<endl;s++;
cout << "["<< s <<"] Base3.ibase3 = " << (int)pVtab[s] << endl;
s++;
cout << "["<< s <<"] Derive.iderive = " << (int)pVtab[s] << endl;
其运行结果如下所示:(在 VC++ 2003 G++ 3.4.4 下)

[0] Base1::_vptr->

     [0] Derive::f()

     [1] Base1::g()

     [2] Base1::h()

     [3] Driver::g1()

     [4] 00000000      ç 注意:在GCC下,这里是1

[1] Base1.ibase1 = 10

[2] Base2::_vptr->

     [0] Derive::f()

     [1] Base2::g()

     [2] Base2::h()

     [3] 00000000      ç 注意:在GCC下,这里是1

[3] Base2.ibase2 = 20

[4] Base3::_vptr->

     [0] Derive::f()

     [1] Base3::g()

     [2] Base3::h()

     [3] 00000000

[5] Base3.ibase3 = 30

[6] Derive.iderive = 100

使用图片表示是下面这个样子:


我们可以看到:

1)  每个父类都有自己的虚表。

2)  子类的成员函数被放到了第一个父类的表中。

3)  内存布局中,其父类布局依次按声明顺序排列。

4)  每个父类的虚表中的f()函数都被overwrite成了子类的f()。解决了不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

重复继承

下面我们再来看看,发生重复继承的情况。所谓重复继承,也就是某个基类被间接地重复继承了多次。

下图是一个继承图,我们重载了父类的f()函数。


其类继承的源代码如下所示。其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数。如子类D中,f()覆盖了超类的函数, f1() f2() 覆盖了其父类的虚函数,Df()为自己的虚函数。

class B
{
public:int ib;char cb;
public:B():ib(0),cb('B') {}virtual void f() { cout << "B::f()" << endl;}virtual void Bf() { cout << "B::Bf()" << endl;}
};
class B1 :  public B
{
public:int ib1;char cb1;
public:B1():ib1(11),cb1('1') {}virtual void f() { cout << "B1::f()" << endl;}virtual void f1() { cout << "B1::f1()" << endl;}virtual void Bf1() { cout << "B1::Bf1()" << endl;}};
class B2:  public B
{
public:int ib2;char cb2;
public:B2():ib2(12),cb2('2') {}virtual void f() { cout << "B2::f()" << endl;}virtual void f2() { cout << "B2::f2()" << endl;}virtual void Bf2() { cout << "B2::Bf2()" << endl;}};class D : public B1, public B2
{
public:int id;char cd;
public:D():id(100),cd('D') {}virtual void f() { cout << "D::f()" << endl;}virtual void f1() { cout << "D::f1()" << endl;}virtual void f2() { cout << "D::f2()" << endl;}virtual void Df() { cout << "D::Df()" << endl;}};
我们用来存取子类内存布局的代码如下所示:(在 VC++ 2003 G++ 3.4.4 下)
typedef void(*Fun)(void);
int** pVtab = NULL;
Fun pFun = NULL;D d;
pVtab = (int**)&d;
cout << "[0] D::B1::_vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << "     [0] ";    pFun();
pFun = (Fun)pVtab[0][1];
cout << "     [1] ";    pFun();
pFun = (Fun)pVtab[0][2];
cout << "     [2] ";    pFun();
pFun = (Fun)pVtab[0][3];
cout << "     [3] ";    pFun();
pFun = (Fun)pVtab[0][4];
cout << "     [4] ";    pFun();
pFun = (Fun)pVtab[0][5];
cout << "     [5] 0x" << pFun << endl;cout << "[1] B::ib = " << (int)pVtab[1] << endl;
cout << "[2] B::cb = " << (char)pVtab[2] << endl;
cout << "[3] B1::ib1 = " << (int)pVtab[3] << endl;
cout << "[4] B1::cb1 = " << (char)pVtab[4] << endl;cout << "[5] D::B2::_vptr->" << endl;
pFun = (Fun)pVtab[5][0];
cout << "     [0] ";    pFun();
pFun = (Fun)pVtab[5][1];
cout << "     [1] ";    pFun();
pFun = (Fun)pVtab[5][2];
cout << "     [2] ";    pFun();
pFun = (Fun)pVtab[5][3];
cout << "     [3] ";    pFun();
pFun = (Fun)pVtab[5][4];
cout << "     [4] 0x" << pFun << endl;cout << "[6] B::ib = " << (int)pVtab[6] << endl;
cout << "[7] B::cb = " << (char)pVtab[7] << endl;   
cout << "[8] B2::ib2 = " << (int)pVtab[8] << endl;
cout << "[9] B2::cb2 = " << (char)pVtab[9] << endl;cout << "[10] D::id = " << (int)pVtab[10] << endl;
cout << "[11] D::cd = " << (char)pVtab[11] << endl;

程序运行结果如下:

GCC 3.4.4

VC++ 2003

[0] D::B1::_vptr->

     [0] D::f()

     [1] B::Bf()

     [2] D::f1()

     [3] B1::Bf1()

     [4] D::f2()

     [5] 0x1

[1] B::ib = 0

[2] B::cb = B

[3] B1::ib1 = 11

[4] B1::cb1 = 1

[5] D::B2::_vptr->

     [0] D::f()

     [1] B::Bf()

     [2] D::f2()

     [3] B2::Bf2()

     [4] 0x0

[6] B::ib = 0

[7] B::cb = B

[8] B2::ib2 = 12

[9] B2::cb2 = 2

[10] D::id = 100

[11] D::cd = D

[0] D::B1::_vptr->

     [0] D::f()

     [1] B::Bf()

     [2] D::f1()

     [3] B1::Bf1()

     [4] D::Df()

     [5] 0x00000000

[1] B::ib = 0

[2] B::cb = B

[3] B1::ib1 = 11

[4] B1::cb1 = 1

[5] D::B2::_vptr->

     [0] D::f()

     [1] B::Bf()

     [2] D::f2()

     [3] B2::Bf2()

     [4] 0x00000000

[6] B::ib = 0

[7] B::cb = B

[8] B2::ib2 = 12

[9] B2::cb2 = 2

[10] D::id = 100

[11] D::cd = D

下面是对于子类实例中的虚函数表的图:


我们可以看见,最顶端的父类B其成员变量存在于B1和B2中,并被D给继承下去了。而在D中,其有B1和B2的实例,于是B的成员在D的实例中存在两份,一份是B1继承而来的,另一份是B2继承而来的。所以,如果我们使用以下语句,则会产生二义性编译错误: 

D d;

d.ib = 0;               //二义性错误

d.B1::ib = 1;           //正确

d.B2::ib = 2;           //正确

注意,上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的编译错误,但B类在D中还是有两个实例,这种继承造成了数据的重复,我们叫这种继承为重复继承。重复的基类数据成员可能并不是我们想要的。所以,C++引入了虚基类的概念。

钻石型多重虚拟继承

虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的。钻石型的结构是其最经典的结构。也是我们在这里要讨论的结构:

上述的“重复继承”只需要把B1和B2继承B的语法中加上virtual 关键,就成了虚拟继承,其继承图如下所示:


上图和前面的“重复继承”中的类的内部数据和接口都是完全一样的,只是我们采用了虚拟继承:其省略后的源码如下所示:

class B {……};
class B1 : virtual public B{……};
class B2: virtual public B{……};
class D : public B1, public B2{ …… };
在查看 D 之前,我们先看一看单一虚拟继承的情况。下面是一段在 VC++2003 下的测试程序:(因为 VC++ GCC 的内存而局上有一些细节上的不同,所以这里只给出 VC++ 的程序, GCC 下的程序大家可以根据我给出的程序自己仿照着写一个去试一试):
int** pVtab = NULL;
Fun pFun = NULL;B1 bb1;pVtab = (int**)&bb1;
cout << "[0] B1::_vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << "     [0] ";
pFun(); //B1::f1();
cout << "     [1] ";
pFun = (Fun)pVtab[0][1];
pFun(); //B1::bf1();
cout << "     [2] ";
cout << pVtab[0][2] << endl;cout << "[1] = 0x";
cout << (int*)*((int*)(&bb1)+1) <<endl; //B1::ib1
cout << "[2] B1::ib1 = ";
cout << (int)*((int*)(&bb1)+2) <<endl; //B1::ib1
cout << "[3] B1::cb1 = ";
cout << (char)*((int*)(&bb1)+3) << endl; //B1::cb1cout << "[4] = 0x";
cout << (int*)*((int*)(&bb1)+4) << endl; //NULLcout << "[5] B::_vptr->" << endl;
pFun = (Fun)pVtab[5][0];
cout << "     [0] ";
pFun(); //B1::f();
pFun = (Fun)pVtab[5][1];
cout << "     [1] ";
pFun(); //B::Bf();
cout << "     [2] ";
cout << "0x" << (Fun)pVtab[5][2] << endl;cout << "[6] B::ib = ";
cout << (int)*((int*)(&bb1)+6) <<endl; //B::ib
cout << "[7] B::cb = ";

其运行结果如下(我结出了GCC的和VC++2003的对比):

GCC 3.4.4

VC++ 2003

[0] B1::_vptr ->

    [0] : B1::f()

    [1] : B1::f1()

    [2] : B1::Bf1()

    [3] : 0

[1] B1::ib1 : 11

[2] B1::cb1 : 1

[3] B::_vptr ->

    [0] : B1::f()

    [1] : B::Bf()

    [2] : 0

[4] B::ib : 0

[5] B::cb : B

[6] NULL : 0

[0] B1::_vptr->

     [0] B1::f1()

     [1] B1::Bf1()

     [2] 0

[1] = 0x00454310 ç该地址取值后是-4

[2] B1::ib1 = 11

[3] B1::cb1 = 1

[4] = 0x00000000

[5] B::_vptr->

     [0] B1::f()

     [1] B::Bf()

     [2] 0x00000000

[6] B::ib = 0

[7] B::cb = B


这里,大家可以自己对比一下。关于细节上,我会在后面一并再说。

下面的测试程序是看子类D的内存布局,同样是VC++ 2003的(因为VC++和GCC的内存布局上有一些细节上的不同,而VC++的相对要清楚很多,所以这里只给出VC++的程序,GCC下的程序大家可以根据我给出的程序自己仿照着写一个去试一试):

D d;pVtab = (int**)&d;
cout << "[0] D::B1::_vptr->" << endl;
pFun = (Fun)pVtab[0][0];
cout << "     [0] ";    pFun(); //D::f1();
pFun = (Fun)pVtab[0][1];
cout << "     [1] ";    pFun(); //B1::Bf1();
pFun = (Fun)pVtab[0][2];
cout << "     [2] ";    pFun(); //D::Df();
pFun = (Fun)pVtab[0][3];
cout << "     [3] ";
cout << pFun << endl;//cout << pVtab[4][2] << endl;
cout << "[1] = 0x";
cout <<  (int*)((&dd)+1) <<endl; //????cout << "[2] B1::ib1 = ";
cout << *((int*)(&dd)+2) <<endl; //B1::ib1
cout << "[3] B1::cb1 = ";
cout << (char)*((int*)(&dd)+3) << endl; //B1::cb1//---------------------
cout << "[4] D::B2::_vptr->" << endl;
pFun = (Fun)pVtab[4][0];
cout << "     [0] ";    pFun(); //D::f2();
pFun = (Fun)pVtab[4][1];
cout << "     [1] ";    pFun(); //B2::Bf2();
pFun = (Fun)pVtab[4][2];
cout << "     [2] ";
cout << pFun << endl;cout << "[5] = 0x";
cout << *((int*)(&dd)+5) << endl; // ???cout << "[6] B2::ib2 = ";
cout << (int)*((int*)(&dd)+6) <<endl; //B2::ib2
cout << "[7] B2::cb2 = ";
cout << (char)*((int*)(&dd)+7) << endl; //B2::cb2cout << "[8] D::id = ";
cout << *((int*)(&dd)+8) << endl; //D::id
cout << "[9] D::cd = ";
cout << (char)*((int*)(&dd)+9) << endl;//D::cdcout << "[10]  = 0x";
cout << (int*)*((int*)(&dd)+10) << endl;
//---------------------
cout << "[11] D::B::_vptr->" << endl;
pFun = (Fun)pVtab[11][0];
cout << "     [0] ";    pFun(); //D::f();
pFun = (Fun)pVtab[11][1];
cout << "     [1] ";    pFun(); //B::Bf();
pFun = (Fun)pVtab[11][2];
cout << "     [2] ";
cout << pFun << endl;cout << "[12] B::ib = ";
cout << *((int*)(&dd)+12) << endl; //B::ib
cout << "[13] B::cb = ";
cout << (char)*((int*)(&dd)+13) <<endl;//B::cb

下面给出运行后的结果(分VC++和GCC两部份)

GCC 3.4.4

VC++ 2003

[0] B1::_vptr ->

    [0] : D::f()

    [1] : D::f1()

    [2] : B1::Bf1()

    [3] : D::f2()

    [4] : D::Df()

    [5] : 1

[1] B1::ib1 : 11

[2] B1::cb1 : 1

[3] B2::_vptr ->

    [0] : D::f()

    [1] : D::f2()

    [2] : B2::Bf2()

    [3] : 0

[4] B2::ib2 : 12

[5] B2::cb2 : 2

[6] D::id : 100

[7] D::cd : D

[8] B::_vptr ->

    [0] : D::f()

    [1] : B::Bf()

    [2] : 0

[9] B::ib : 0

[10] B::cb : B

[11] NULL : 0

[0] D::B1::_vptr->

     [0] D::f1()

     [1] B1::Bf1()

     [2] D::Df()

     [3] 00000000

[1] = 0x0013FDC4  ç 该地址取值后是-4

[2] B1::ib1 = 11

[3] B1::cb1 = 1

[4] D::B2::_vptr->

     [0] D::f2()

     [1] B2::Bf2()

     [2] 00000000

[5] = 0x4539260   ç 该地址取值后是-4

[6] B2::ib2 = 12

[7] B2::cb2 = 2

[8] D::id = 100

[9] D::cd = D

[10]  = 0x00000000

[11] D::B::_vptr->

     [0] D::f()

     [1] B::Bf()

     [2] 00000000

[12] B::ib = 0

[13] B::cb = B

关于虚拟继承的运行结果我就不画图了(前面的作图已经让我产生了很严重的厌倦感,所以就偷个懒了,大家见谅了)

在上面的输出结果中,我用不同的颜色做了一些标明。我们可以看到如下的几点:

1)无论是GCC还是VC++,除了一些细节上的不同,其大体上的对象布局是一样的。也就是说,先是B1(黄色),然后是B2(绿色),接着是D(灰色),而B这个超类(青蓝色)的实例都放在最后的位置。

2)关于虚函数表,尤其是第一个虚表,GCC和VC++有很重大的不一样。但仔细看下来,还是VC++的虚表比较清晰和有逻辑性。

3)VC++和GCC都把B这个超类放到了最后,而VC++有一个NULL分隔符把B和B1和B2的布局分开。GCC则没有。

4)VC++中的内存布局有两个地址我有些不是很明白,在其中我用红色标出了。取其内容是-4。接道理来说,这个指针应该是指向B类实例的内存地址(这个做法就是为了保证重复的父类只有一个实例的技术)。但取值后却不是。这点我目前还并不太清楚,还向大家请教。

5)GCC的内存布局中在B1和B2中则没有指向B的指针。这点可以理解,编译器可以通过计算B1和B2的size而得出B的偏移量。




本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/496779.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VB.NET 中的 As New 以及型別指定

常有人以為 VB.NET 程式的執行效能不如 C#&#xff0c;但根據 msdn 的說法&#xff0c;VB.NET 和 C# 都是編譯成 MSIL 中繼語言&#xff0c;因此基本上以二者所寫出來的應用程式也具有相同的效能。最常導致二者在 web 應用程式中&#xff0c;執行效能差異的罪魁禍首&#xff0c…

普华永道:2018 AI预测报告 将回答关于数据的重大问题

来源&#xff1a;网络大数据近日 &#xff0c;普华永道发布关于2018年人工智能趋势预测的报告&#xff0c;同时介绍人工智能对商业、政府和社会的等方面的影响。人工智能非常复杂&#xff0c;且发展迅速。AI 在一些领域做了很多&#xff0c;在另一些领域做得较少&#xff0c;这…

过渡效果_剪映教程:剪映怎么添加视频之间的过渡转场效果?

今天是国庆假期的第三天&#xff0c;直播apk小编今天确实郁闷的无比&#xff0c;因为今天小编所在的老家正在下雨&#xff0c;而且从上午一直下到现在下了整整一天!OMG&#xff0c;浪费了一天的假期。不过还好有你们&#xff0c;小编还可以静下心来给大家写教程&#xff0c;好了…

C++ 多继承和虚继承的内存布局

来源&#xff1a;http://www.oschina.net/translate/cpp-virtual-inheritance 来源&#xff1a;http://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html C中的虚拟继承的一些总结 1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念。虚拟基类是为解决…

C++学习之路 | PTA乙级—— 1076 Wifi密码 (15 分)(精简)

1076 Wifi密码 (15 分) 下面是微博上流传的一张照片&#xff1a;“各位亲爱的同学们&#xff0c;鉴于大家有时需要使用 wifi&#xff0c;又怕耽误亲们的学习&#xff0c;现将 wifi 密码设置为下列数学题答案&#xff1a;A-1&#xff1b;B-2&#xff1b;C-3&#xff1b;D-4&…

怎么导出链接_如何导出CocosCreator项目供cocos2dx加载

1、新建CocosCreator项目。2、安装creator_to_cocos2dx 插件拷贝到 Creator 项目的 packages 目录3、在cocoscreator项目中&#xff0c;项目(Project) -> LuaCPP Support -> Setup Target Project&#xff0c;出现的对话框中 Project Path 选择目标 Cocos2d-x 工程的路径…

C++对象模型

From&#xff1a;http://blog.csdn.net/wxc1987821/article/details/5958325 图说C对象模型&#xff1a;对象内存布局详解&#xff1a;http://blog.csdn.net/alexqiweek/article/details/52624641 1 前言 了解你所使用的编程语言究竟是如何实现的&#xff0c;对于C程序员可能…

北斗产业发展现状与前景预测研究

来源&#xff1a;北斗时空作者&#xff1a;曹冲北斗产业&#xff0c;泛指中国卫星导航产业&#xff0c;2012年底我国北斗系统投入区域服务以后&#xff0c;中国的卫星导航产业就以北斗为轴心主线加以推动发展&#xff0c;从此称其为北斗产业。顺理成章由其延续我国整个导航定位…

自已动手制作寸照

毕业在即&#xff0c;学校动不动就给张单子&#xff0c;说&#xff0c;添了他&#xff0c;贴照片别忘。苦于我对镜头不感冒&#xff0c;照出来的相片很难看&#xff0c;真的没有本人好看。而且在照证明相的时候都不会笑了&#xff0c;脸很僵硬&#xff0c;不像平时的照片那么活…

Java并发编程实战~CountDownLatch

无线程池实现 while(存在未对账订单){// 查询未对账订单Thread T1 new Thread(()->{pos getPOrders();});T1.start();// 查询派送单Thread T2 new Thread(()->{dos getDOrders();});T2.start();// 等待 T1、T2 结束T1.join();T2.join();// 执行对账操作diff check(…

linuxoracle查看用户权限_权限管理系统设计过程

今天和大家一起探讨权限管理方面的设计心得。权限管理&#xff0c;是B端后台系统一个重要的组成部分&#xff0c;属于底层的支撑功能&#xff0c;系统内所有的功能&#xff0c;甚至字段的增减都涉及到权限的分配和管理。因此怎样配置后台的权限系统&#xff0c;以适应多变业务需…

MIT:大脑如何跟踪运动中的物体?

来源&#xff1a;脑健康联盟抓住一个弹起的球&#xff0c;或者用球拍击球&#xff0c;这两种行动都需要合理地估算触球时间。一直以来&#xff0c;神经科学家都相信&#xff0c;大脑是通过计算物体的运动速度来完成这些动作的。然而&#xff0c;麻省理工学院的一项新研究表明&a…

物联网测试完整解决方案 | 为你的芯片、模块、终端保驾护航

来源&#xff1a;物联网智库2017年被视为物联网商用元年。窄带物联网标准正式冻结&#xff0c;国内运营商重点布局以及设备制造商的强力推动……自此&#xff0c;物联网规模化商用迈入了快车道。然而&#xff0c;与传统的智能手机类似&#xff0c;基于NB-IoT技术的物联网设备在…

Java并发编程实战~CyclicBarrier

用 CyclicBarrier 实现线程同步 线程 T1 和 T2 要做到步调一致&#xff0c;都完成后通知到线程 T3 创建了一个计数器初始值为 2 的 CyclicBarrier&#xff0c;你需要注意的是创建 CyclicBarrier 的时候&#xff0c;我们还传入了一个回调函数&#xff0c;当计数器减到 0 的时候…

梦中的婚礼

让女孩听了陶醉的曲子梦中的婚礼 :http://player.youku.com/player.php/sid/XMjA1ODc3MTI/v.swf 电脑键盘弹奏 梦中的婚礼 :http://player.youku.com/player.php/sid/XMzMzNzQyMDA/v.swf 转载于:https://www.cnblogs.com/cube/archive/2008/07/19/1246736.html

DeepMind提出图形的「深度生成式模型」,可实现「任意」图形的生成

原文来源&#xff1a;arXiv作者&#xff1a;Yujia Li、Oriol Vinyals、Chris Dyer、Razvan Pascanu、Peter Battaglia 「雷克世界」编译&#xff1a;嗯~阿童木呀、KABUDA一般来说&#xff0c;图形是基本的数据结构&#xff0c;它在诸如知识图、物理和社会交互、语言和化学等许多…

eviews曲线图怎么做_【干货速递】Eviews:你不可不知的经典问答!

更多精彩内容请关注211统计课堂计量经济学是分析啥的&#xff1f;01计量经济学的主要用途或目的主要有两个方面&#xff1a;1.理论检验。这是计量经济学用途最为主要的和可靠的方面。这也是计量经济学本身的一个主要内容。2.预测应用。从理论研究和方法的最终目的看&#xff0c…

感恩的心

不气歌&#xff08;一&#xff09; 他人气我我不气&#xff0c;我本无心他来气。 倘若生气中他计&#xff0c;气出病来无人替。 请来医生把病治&#xff0c;反说气病治非易。 茶不思来饭无味&#xff0c;通宵达旦不入睡。 倘你伸腿离我去&#xff0c;撇我一人活受罪。 奉…

Java并发编程实战~并发容器

在容器领域一个容易被忽视的“坑”是用迭代器遍历容器&#xff0c;例如在下面的代码中&#xff0c;通过迭代器遍历容器 list&#xff0c;对每个元素调用 foo() 方法&#xff0c;这就存在并发问题&#xff0c;这些组合的操作不具备原子性。 List list Collections.synchronize…

工信部:筹建全国首个区块链和分布式记账标准化技术委员会

作者&#xff1a;李秀琴在3.15即将来临之时&#xff0c;我国工信部给区块链行业又带来了一大利好消息。3月12日&#xff0c;工业和信息化部&#xff08;以下简称工信部&#xff09;在官网发布公告称&#xff0c;其正在就筹建全国区块链和分布式记账技术标准化技术委员会事宜开展…