文档章节

C++对象模型学习——构造、析够、拷贝语意学

thanatos_y
 thanatos_y
发布于 2016/07/11 20:49
字数 11464
阅读 51
收藏 1

   考虑下面这个abstract base class 声明:

class Abstract_base
{
  public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char*
      mumble() const { return _mumble; }
  
  proteceted:
    char *_mumble;
};

    虽然这个class被设计为一个抽象的base class(其中有pure virtual function,使得

Abstract_base不可能拥有实例),但它仍然需要一个显式的构造函数以初始化其data member

_mumble。如果没有这个初始化操作,其局部性对象_mumble将无法决定初值,例如:

class Concrete_derived : public Abstract_base
{
  public:
    Concrete_derived();
    // ...
};

void foo()
{
  // Abstract_base::_mumble 未被初始化
  Concrete_derived trouble;
  // ...
}

     如果Abstract_base的设计者意图让其每一个derived class提供_mumble的初值。然而如果是

这样,derived class的唯一要求就是Abstract_base必须提供一个带有唯一参数protected

constructor:

Abstract_base::
Abstract_base( char *mumble_value = 0 )
  : _mumble( mumble_value )
  { }

       一般而言,class的data member应该被初始化,并且只在constructor中或是在class的其他

member functions中指定初值。其他任何操作都将被破坏封装性质,使class的维护和修改更加

困难。   

       1、纯虚函数的存在(Presence of a Pure Virtual Function)

        pure virtual function可以被定义和调用(invoke),不过它只能被静静地调用(invoked

statically),不能经由虚拟机制调用。例如:

// ok:定义pure virtual function
// 但只可能被静态地调用(invoked statically)

inline void 
Abstract_base::interface() const  // 先前声明这是一个pure virtual const function
{
  // ...
}

inline void
Concrete_derived::interface() const 
{
  // 静态调用(static invocation)
  Abstract_base::interface(); // 我们竟然能够调用一个pure virtual function
  
  // ...
}

        要不要这样做,全由class设计者决定。唯一例外是pure virtual destructor:class设计者一

定得定义它。因为每一个derived class destructor会被编译器加以扩张,以静态调用的方式调用

其“每一个virtual base class”以及“上一层base class”的destructor。因此,只要缺乏任何一个

base class destructors的定义,就会导致链接失败。

        这样设计是以C++语言的一个保证为前提:继承体系中每一个class object的destructor都会

被调用。所以编译器不能压抑这一调用。而编译器也没有足够的知识合成一个pure virtual

destructor的函数定义。

#include <iostream>

class Abstract_base
{
  public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }
  
  protected:
    char *_mumble;
};

class Concrete_derived : public Abstract_base
{
  public:
    Concrete_derived() { }
    ~Concrete_derived() { }
    void interface() const;

};

inline void 
Abstract_base::interface() const 
{
  std::cout << "mimiasd调用了Abstract_base::interface()" << std::endl;
}

Abstract_base:: ~Abstract_base() 
{
  std::cout << "mimiasd调用了Abstract_base::~Abstract_base()" << std::endl;
}

inline void
Concrete_derived::interface() const 
{
  Abstract_base::interface();
}

int main()
{
  Concrete_derived concrete;
  
  concrete.interface();
}

       

   一个好的代替方案是,不要把virtual destructor声明为pure。

一、“无继承”情况下的对象构造

     考虑下面这个程序片段:

(1)  Point global;
(2) 
(3)  Point foobar()
(4)  {
(5)    Point local;
(6)    Point *heap = new Point;
(7)    *heap = local;
(8)    // ... stuff ...
(9)    delete heap;
(10)   return local;   
(11) }

     L1、L5、L6表现出三种不同的对象产生方式:global内存配置、local内存配置和heap内存配

置。L7把一个class object指定给另一个,L10设定返回值,L9则显式地以delete运算符删除

heap object。

     一个object的生命,是该object的一个执行期属性。local object的生命从L5的定义开始,到

L10为止。global object的生命和整个程序的生命相同。heap object的生命从它被new运算符配

置出来开始,到它被delete运算符摧毁为止。

     下面是Point的第一次声明,可以写成C程序。C++ Standard说这是一种所谓的Plain OI‘ Data

声明形式:

typedef struct
{
  float x, y, z;
} Point;

     如果以C++来编译这段代码。观念上,编译器会为Point声明一个trivial default constructor、

一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment

operator。但实际上,编译器会分析这个声明,并为它贴上Plain OI’ Data标签。

      当编译器遇到这样的定义:

(1)   Point global;

       观念上Point的trivial constructor和destructor都会被产生并调用,constructor在程序起始

(startup)处被调用而destructor在程序的exit()处被调用。然而,事实上那些trivial members要

不是没被定义,就是没被调用,程序的行为一如它在C中的表现一样。

       只有一个小小的例外。在C中,global被视为一个“临时性的定义”,因为它没有显示的初始

化操作。一个“临时性的定义”可以在程序中发生多次。那些实例会被链接器折叠起来,只留下单

独一个实例,被放在程序data segment中一个“特别保留给未初始化之global objects使用”的空

间。由于历史原因,这块空间被称为BSS,这是Block Started by Symbol的缩写。

       C++并不支持“临时性的定义”,这是因为class构造行为的隐式应用之故。虽然公认这个语言

可以判断一个class objects或是一个Plain OI' Data,但似乎没有必要搞得那么复杂。因

此,global在C++中被视为完全定义(它会阻止第二或更多个定义)。C和C++的一个差异就在

于,BSS data segment在C++中相对地不重要。C++的所有全局对象都被以“初始化过的数据”来

对待。

      foobar()函数中的L5,有一个Point object local,同样也是既没有被构造也没有被析够。当

然,Point object local如果没有先经过初始化,可能会成一个潜在的程序“臭虫”——万一第一次

使用它就需要其初值的话(像L7)。至于heap object在L6的初始化操作:

(6)   Point *heap = new Point;

      会被转换为new运算符(由library提供)的调用:

Point *heap = _new( sizeof( Point ) );

      并没有default constructor施于new运算符传回的Point object身上。L7对此object有个指派

(赋值,assign)操作,如果local曾被适当地初始化过,一切就没有问题:

(7) *heap = local;

      观念上,这样的指定操作会触发trivial copy assignment operator做拷贝搬运操作。然而实际

上该object是一个Plain Ol‘ Data,所以赋值操作(assignment)将只是像C那样的纯粹位搬移操

作。L9执行一个delete操作。

(9)   delete heap;

      会被转换为对delete运算符(由library提供)的调用:

_delete( heap );

      观念上,这样的操作会触发Point的trival destructor。但一如我们所见,destructor要不是没

有被产生就是没有被调用。最后,函数以传值(by value)的方式将local当做返回值传回,这

在观念上会触发trivial copy constructor,不过实际上return操作只是一个简单的位拷贝操作,因

为对象是一个Plain Ol’ Data。

      1、抽象数据类型(Abstract Data Type)

       以下是Point的第二次声明,在Public接口之下多了private数据,提供完整的封装性,但没

有提供任何virtual function:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0, float z = 0.0 )
      : _x( x ), _y( y ), _z( z ) { }
    // mo copy constructor,copy operator
    // or destructor defined ...
    
    // ...
  private:
    float _x, _y, _z;
};

      这个经过封装的Point class,其大小并没有改变,还是三个连续的float。不论private或

public存取层,或是member function的声明,都不会占用额外的对象空间。

      并没有为Point定义一个copy constructor或copy operator,因为默认的位语意(default

bitwise semantics)已经足够了。我们也不需要提供一个destructor,因为程序默认的内存管理

方法也足够了。

     对于global实例:

Point global; // 实施Point::Point( 0.0, 0.0, 0.0 );

     现在有了default constructor作用于其上。由于global被定义在全局范畴中,其初始化操作将

延迟到程序启动(startup)时才开始。

     如果要将class中的所有成员都设定常量初值,那么给予一个explicit initialization list会比较有

效率些(比起意义相同的constructor的inline expansion而言)。甚至在local scope中也是如

此。例如:

void mumble()
{
  Point local1 = { 1.0, 1.0, 1.0 }; 
  Point local2;
  
  // 相当于一个inline expansion
  // explicit initialization会稍微快一些
  // local2._x = 1.0
  // local2._y = 1.0
  // local2._z = 1.0
}

        local1的初始化操作会比local2的有效率些。这是因为当函数的activation record被放进程序

堆栈时,上述initialization list中的常量就可以被放进去local1内存中了。

        Explicit initialization list带来三项缺点:

       1)只有当class members都是public,此法才奏效;   

       2)只能指定常数,因为它们在编译时期就可以被评估求值。

       3)由于编译器并没有自动施行之,所以初始化行为的失败可能性会高一些。

       那么,explicit initialization list所带来的效率优点,一般而言不能够弥补其软件工程上的缺

点,然而在某些特殊情况下又不一样。例如,或许你以手工打造了一些巨大的数据结构如调色

盘(color palette),或是你正要把一堆常量数据倾倒给程序,那么explicit initialization list 的

效率会比inline constructor好得多,特别是对全局对象(global object)而言。

        在编译器层面,会有一个优化机制用来识别inline constructors,后者简单地提供一个

member-by-member的常量指定操作。然后编译器会抽取出那些值,并且对待它们就好像是

explicit initialization list所供应的一样,而不会把constructor扩展成为一系列的assignment指

令。

       于是,local Point object的定义:

{
  Point local;
  // ...
}

       现在被附加上default Point constructor的inline expansion:

{
  // inline expansion of default constructor
  Point local; 
  local._x = 0.0; local._y = 0.0; local._z = 0.0;
  // ...
}

        L6配置出一个heap Point object:

(6)   Point *heap = new Point;

        现在则被附加一个“对default Point constructor的有条件调用操作”:

// C++伪码
Point *heap = _new( sizeof( Point ) );
if( heap != 0 )
  heap->Point::Point();

        然后才被编译器进行inline expansion操作。至于把heap指针指向local object:

(7)  *heap = local;

        则保持着简单的位拷贝操作。以传值方式传回local object,情况也是一样:

(10)   return local;

        L9删除heap所指的对象:

(9)   delete heap;

        该操作不会导致destructor被调用,因为我们并没有显式提供一个destructor函数实例。

        观念上,我们的Point class有一个相关的default copy constructor、copy operator、和

destructor。然而它们都是无用的(trivial),而且编译器实际上根本没有产生它们。

      2、为继承做准备

      第三个Point声明,将为“继承性质”以及某些操作的动态决议(dynamic resoluton)做准备。

目前我们限制对z成员做存取操作:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0, float z = 0.0 )
      : _x( x ), _y( y ), _z( z ) { }
    // mo copy constructor,copy operator
    // or destructor defined ...
    
    virtual float z();
    // ...
  private:
    float _x, _y, _z;
};

      并没有定一个copy constructor、copy operator、destructor。我们的所有members都以数值

来存储,因此在程序层面的默认语意之下,行为良好。可能virtual functions的导入应该总是附

带着一个virtual destuctor的声明。但这样做在这个例子中对我们并无好处。

      virtual functions的导入促使每一个Point object拥有一个virtual table pointer。这个指针给我

们提供virtual接口的弹性,其成本是:每一个object需要额外的一个word空间。具体影响视情况

而定,这可能有意义,也可能没有意义,必须视它对多态(ploymorphism)设计所带来的实际

效益的比例而定。只有在实际完成之后,才能评估要不要避免之。

      除了每一个class object多负担一个vptr之外,virtual function的导入也引发编译器对于我们

的Point class产生膨胀作用:

     1)我们所定义的constructor被附加了一些代码,以便将ptr初始化。这些代码必须被附加在

任何base class constructors的调用之后,但必须在任何由使用者供应的代码之前。例如,下面

就是可能的附加结果:

// C++伪码:内部膨胀
Point* Point::Point( Point *this, float x, float y )
         : _x( x ), _y( y )
{
  // 设定object的virtual table pointer(vptr)
  this->_vptr_Point = _vtbl_Point;

  // 扩展member initialization list
  this->_x = x;
  this->_y = y;

  // 传回this对象
  return this;
}

     2)合成一个copy constructor和一个copy assignment operator,而且其操作不再是

trivaial(但implicit destuctor仍然是trivial)。如果一个Point object被初始化或以一个derived

class object赋值,那么以位为基础(bitwise)的操作可能对vptr带来非法设定。

// C++伪码
// copy constructor的内部合成
inline Point*
Point::Point( Point *this, const Point &rhs )
{
  // 设定object的virtual table pointer( vptr )
  this->_vptr_Point = _vtbl_Point;
  
  // 将rhs坐标中的连续位拷贝到this对象,
  // 或是经由member assignment提供一个member ...
  
  return this;  
}

     编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会实现一个精

确地“以成员为基础(memberwise)”的赋值操作。C++ Standard要求编译器尽量延迟nontrivial

members的实际合成操作,直到真正遇到其使用场合为止。

      L1的global初始化操作、L6的heap初始化操作以及L9的heap删除操作,都还是和稍早的

Point版本相同,然而L7的memberwise赋值操作:

*heap = local;

      很有可能触发copy assignment operator的合成,及其调用操作的一个inline expansion(行

内扩张):以this取代heap,而以rhs取代local。

      最戏剧性的冲击发生在以传值方式传回local的那一行(L10)。由于copy constructor的出

现,foobar()很有可能被转化为下面这样:

// C++伪码:foobar()的转化,
// 用以支持copy constructor

Point foobar( Point &_result )
{
  Point local;
  local,Point::Point( 0.0, 0.0 );
  // heap的部分与前面相同...
  
  // copy constructor的应用
  _result.Point::Point( local );

  // local 对象的destructor将在这里执行。
  // 调用Point定义的destructor:
  // local.Point::~Point();

  return;
}

     如果支持named return value(NRV)优化,这个函数进一步被转化为:

// C++伪码:foobar()的转化,
// 以支持named return value(NRV)优化
Point foobar( Point&_result )
{
  _result.Point::Point( 0.0, 0.0 );

  // heap的部分与前相同......
  
  return;
}

     一般而言,如果你的设计之中,有许多函数都需要以传值方式(by value)传回一个local

class object,例如:

T operator+( const T&, const T& )
{
  T result;
  // ...真正的工作在此
  return result;
}

      那么提供一个copyconstructor就比较合理——甚至即使default memberwise语意已经足够。

它的出现会触发NRV优化。然而,NRV优化后不再需要调用copy constructor,因为运算结果已

经被直接计算于“将被传回的object”体内了。

二、继承体系下的对象构造

      当我们定义一个object如下:

T object;

       如果T有一个constructor(不论是由用户提供或是由编译器合成的),它会被调用。这很明

显,比较不明显的是,constructor的调用真正伴随了什么?

       Constructor可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视

class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:

       1)记录在member initialization list中的data members初始化操作会被放进constructor的函

数本体,并以members的声明顺序为顺序。

       2)如果有一个member并没有出现在member initialization list之中,但它有一个default

constructor,那么该default constructor必须被调用。

       3)在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初值,指向适当

的virtual table(s)。

       4)在那之前,所有上一层的base class constructors必须被调用,以base class的声明顺序

为顺序(与member initialization list中的顺序没关联):

        如果base class被列于member initialization list中,那么任何显示指定的参数都应该传递过

去。

        如果base class没有被列于member initialization list中,而它有default constructor(或

default memberwise copy constructor),那么就调用之。

        如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。

       5)在那之前,所有virtual base class constructors必须被调用,从左到右,从最深到最浅:

         如果class被列于member initialization list中,那么如果有任何显式指定的参数,都应该传

递过去。若没有列于list之中,而class有一个default constructor,亦应该调用之。

         此外,class中的每一个virtual base class subobject的偏移位置(offset)必须在执行期可

被存取。

         如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以

支持这一行为的机制必须被放进来。

         下面要从“C++语言对classes所保证的语意“这个角度,探讨constructor扩充的必要性。再

次以Point为例,并为它增加一个copy constructor、一个copy operator、一个virtual

destructor,如下所示:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );            // copy constructor
    Point& operator=( const Point& ); // copy assignment operator
  
    virtual ~Point();                 // virtual destructor
    virtual float z() { return 0; }
    // ...
  
  protected:
    float _x, _y;
};

       在开始介绍并一步步走过以Point为根源的继承体系之前,先很快地看看Line class的声明和

扩充结果,它由_begin和_end两个点构成:

class Line
{
  Point _begin, _end;
  
  public:
    Line( float = 0.0, float = 0.0, float = 0.0, float = 0.0 );
    Line( const Point&, const Point& );

    draw();
    // ...
};

       每一个explicit constructor都会被扩充以调用其两个member class objects的constructor。如果我们定义constructor如下:

Line::Line( const Point &begin, const Point &end )
  : _end( end ), _begin( begin )
{ }

       它被编译器扩充并转换为:

// C++伪码:Line constructor的扩充
Line* Line::Line( Line *this, const Point &begin, const Point &end )
{
  this->_begin.Point::Point( begin );
  this->_end.Point::Point( end );
  return this;
}

      由于Point声明了一个copy constructor、一个copy operator、以及一个destructor(本例为

virtual),所以Line class的implicit copy constructor、copy operator和destructor都将具有具体

效用(nontrivial)。

      当程序员写下:

Line a;

      时,implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的

destructor将会是virtual。然而由于Line只是内含Point objects而非继承自Point,所以被合成出

来的destructor只是nontrivial而已)。其中,它的member class objects的destructor会被调用

(以其相反顺序):

// C++伪码:合成出来的Line destrutor
inline void
Line::~Line( Line *this )
{
  this->_end.Point::~Point();
  this->_begin.Point::~Point();
}

     当然,如果Point destructor是inline函数,则每一个调用操作会在调用地点被扩展开来。虽然

Point destructor是virtual,但其调用操作(在containing class destructor之中)会被静态地决议

出来。

      类似的道理,当写下:

Line b = a;

       时,implicit Line copy constructor会被合成出来,成为一个inline public member。

       最后,当写下:

a = b;

       时,implicit copy assignment operator会被合成出来,成为一个inline public member。

      关于在产生copy operator的时候,要加入如下的条件过滤:

if( this == &rhs ) return *this;

      防止自我指派(赋值),例如自我赋值如下的失败:

// 使用者提供的copy assignment operator
// 忘记提供一个自我拷贝时的过滤

String& String::operator= ( const String &rhs )
{
  // 这里需要过滤(在释放资源之前)
  delete [] str;
  str = new char[ strlen( rhs.str ) + 1 ];
}

    1、虚拟继承(Virtual Inheritance)

     考虑下面这个虚拟继承:

class Point3d : public virtual Point
{
  public:
    Point3d( float x = 0.0, float y = 0.0 )
      : Point( x, y ), _z( z ) { }
    Point3d( const Point3d& rhs )
      : Point( rhs ), _z( rhs._z ) { }
    ~Point3d();
    Point3d& operator=( const Point3d& );

    virtual float z() { return _z; }
    // ...
  
  protected:
    float _z;
};

   传统的“constructor扩充现象”并没有用,这是因为virtual base class的“共享性”之故:

// C++伪码
// 不合法的constructor扩充内容
Point3d*
Point3d::Point3d( Point3d *this, float x, float y, float z )
{
  this->_vptr_Point3d = _vtbl_Point3d;
  this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
  this->_z = rhs._z;
 
  return this;
}

     试着想以下三种类的派生情况:

class Vertex : virtual public Point{ ... };
class Vertex3d : public Point3d, public Vertex { ... };
class PVertex : public Vertex3d{ ... };

    Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex3d

的subobjects时,它们对Point constructor的调用操作一定不可以发生;取而代之的是,作为一个

底层的class, Vertex3d有责任将Point初始化。而更往后的继承,则由PVertex来负责完成“被共

享之Point subobject”的构造。

     传统策略如果要支持“初始化virtual base class”,会导致constructor中有更多的扩充内容,用

以指示virtual base classconstructors应不应该被调用。constructor的函数本体因而必须条件式地

测出传进来的参数,然后决定调用或不调用相关的virtual base class constructors。下面就是

Point3d的constructor扩充内容:

// C++代码
// 在virtual base class情况下的consrtuctor扩充内容
Point3d*
Point3d::Point3d( Point3d *this, bool _most_derived, float x, float y, float z )
{
  if( _most_derived != false )
    this->Point::Point( x, y );

  this->_vptr_Point3d = _vtbl_Point3d;
  this->_vptr_Point3d_Point = _vtbl_Point3d_Point;
  this->_z = rhs._Z;

  return this;
}

     在更深层的继承情况下,例如Vertex3d,调用Point3d和Vertex的constructor时,总会把

_most_derived参数设为false,于是就压制了两个constructor中对Point constructor的调用操

作。

// C++伪码
// 在virtual base class情况下的constructor扩充内容
Vertex3d*
Vertex3d::Vertex3d( Vertex3d *this, bool _most_derived, 
                    float x, float y, float z )
{
  if( _most_derived != false )
    this->Point::Point( x, y );

  // 调用上一层base classes
  // 设定_most_derived为false

  this->Point3d::Point3d( false, x, y, z );
  this->Vertex::Vertex( false, x, y );

  // 设定vptrs
  // user code
  
  return this;
}

    这样的策略得以保持语意的正确无误。举个例子,当我们定义:

Point3d origin;

    时,Point3d constructor可以正确地调用其Point virtual base class subobject。而当我们定

义:

Vertex3d cv;

    时,Vertex3d constructor正确地调用Point constructor。Point3d和Vertex的constructor会做

每一件该做的事情——对Point的调用操作除外。如果这个行为是正确的,那么什么是错误的呢?

    在一种状态中,“virtual base class constructors的被调用”有着明确的定义:只有当一个完整的

class object被定义出来(例如origin)时,它才会被调用;如果object的subobject,它就不会被调

用。

    以此为杠杆,我们可以产生更有效率的constructors。某些新进的编译器把每一个constructor

分裂为二,一个针对完整的object,另一个针对subobject。“完整object”版无条件地调用virtual

base constructors,设定所有ptrs等。“subobject”版则不调用virtual base constructors,也可能

不设vptrs等。

    2、vptr初始化语意(The Semantics of the vptr Initialization)

    当我们定义一个PVertex object时,constructor的调用顺序是:

Point( x, y );
Point3d( x, y, z );
Vertex( x, y, z );
Vertex3d( x, y, z );
PVertex( x, y, z );

    假设这个继承体系中的每一个class都定义了一个virtual function size(),此函数数负责传回

class的大小。如果我们写: 

PVertex pv;
Point3d p3d;

Point *pt = &pv;

    那么这个调用操作:

pt->size();

    将传回PVertex的大小,而:

pt = &p3d;
pt->size();

    将传回Point3d的大小。

     更进一步,我们假设这个继承体系中的每一个constructor内含一个调用操作,像这样:

Point3d::Point3d( float x, float y, float z )
  : _x( x ), _y( y ), _z( z )
{
  if( spyOn )
    cerr << "Within Point3d::Point3d()"
         << "size: " << size() << endl;
}

    当我们定义PVertx object时,前述的5个constructors会如何会如何?每一次size()调用会被决

议为PVertex::size()吗?或者每次调用会被决议为“目前正在执行之constructor所对应之class”的

size()函数实例?

     C++语言规则告诉我们,在Point3d constructor中调用size()函数,必须被决议为

Point3d::size()而不是PVertex::size)。更一般性地说,在一个class(本例为Point3d)的

constructor(和destructor)中,经由构造中的对象(本例为PVertex对象)来调用一个virtual

function,其函数实例应该是在此class(本例为Point3d)中有作用的那个。由于各个

constructor的调用顺序,上述情况是必需的。Constructors的调用顺序是:由根源而末端

(bottom up)、由内而外(inside out)。当base constructor执行时,derived实例还没有被构

造起来。

     意思是,当每一个PVertex base class constructors被调用时,编译系统必须保证有适当的

size()函数实例被调用。怎样才能办到这一点?

     如果调用操作限制必须在constructor(或destructor)中直接调用,那么答案十分明显:将每

一个调用操作以静态方式决议,千万不要用到虚拟机制。如果是在Point3d constructor中,就显

式调用Point3d::size()。

      然而如果size()之中又调用一个virtual function,会发生什么事情?这种情况下,这个调用也

必须决议为Point3d的函数实例。而在其他情况下,这个调用是纯正的virtual,必须经由虚拟机

制来决定归向。也就是说,虚拟机制本身必须知道是否这个调用源自于一个constructor中。

       另一个我们可以采取的方法是,在constructor(或destructor)内设立一个标志,用静态方

式来决议。然后我们就可以用标志值作为判断依据,产生条件式的调用操作。

        这的确可行,虽然感觉起来有点不够优雅和有效率。

        这个解法方法感觉起来比较像是我们的第一个设计策略失败后的一个策略,而不是釜底抽

薪的办法。根本的解决之道是,在执行一个constructor时,必须限制一组virtual function候选名

单。

        virtual table是决定一个class的virtual function名单的关键。而Virtual table通过vptr。所以

为了控制一个class中所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即

可。当然,设定vptr是编译器的责任,任何程序员都不必操心此事。

        vptr初始化操作的处理,本质而言,这得视vptr在constructor之中“应该在何时被初始化”而

定。我们有三种选择:

        1)在任何操作之前。

        2)在base class constructors调用操作之后,但是程序员供应的代码或是”member

initialization list中所列的members初始化操作“之前。

        3)在每一件事情发生之后。

        答案是2。另两个选择没有什么价值。策略2解决了”在class中限制一组virtual functions名单

“的问题。如果每一个constructor都一直等待到其base class constructors执行完毕之后才设定其

对象的vptr,那么每次它都能够调用正确的virtual function实例。

          令每一个base class constructor设定其对象的vptr,使它指向相关的virtual table之后,构

造中的对象就可以严格而正确地变成”构造过程中所幻化出来的每一个class“的对象。也就是

说,一个PVertex对象会先形成一个Point对象、一个Point3d对象、一个Vertex对象、一个

Vertex对象。在每一个base class constructor中,对象可以与constructor‘s class的完整对象做

比较。对于对象而言,”个体发生学“概括了”系统发生学“。constructor的执行算法通常如下:

       1)在derived class coinstructor中,”所有virtual base classes“及”上一层base class“的

constructor会被调用。

       2)上述完成之后,对象的vptr(s)被初始化,指向相关的virtual table(s)。

       3)如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设

定之后才做,以免有一个virtual member function被调用。

        4)最后,执行程序员所提供的代码。

        例如,已知下面这个由程序员定义的PVertex constructor:

PVertex::PVertex( float x, float y, float z )
  : _next( 0 ), Vertex3d( x, y, z ), Point( x, y )
{
  if( spyOn )
    cerr << "Within PVertex::PVertex()"
         << "size: " << size() << endl;
}

        它很有可能被扩展为:

// C++伪码:
// PVertex constructor 的扩展结果
PVertex*
PVertex::PVertex( PVertex* this, bool _most_derived,
                  float x, float y, float z )
{
  // 条件式地调用virtual base constructor
  if( _most_derived != false )
    this->Point::Point( x, y );

  // 无条件地调用上一层base
  this->Vertex3d::Vertex3d( x, y, z );

  // 将相关的vptr初始化
  this->_vptr_PVertex = _vtbl_PVertex;
  this->_vptr_Point_PVertex = 
        _vtbl_Point_PVertex;

  // 程序员所写的代码
  if( spyOn )
    cerr << "Within PVertex::PVertex()"
         << "size: "
         // 经由虚拟机制调用
         << ( *this->_vptr_PVertex[ 3 ].faddr )( this )
         << endl;

  // 传回构造的对象
  return this;
  
}

     这真是个完美的解答吗?假设我们的Point constructor定义为:

Point::Point( float x, float y )
  : _x( x ), _y( y ) { }

      我们的Point3d constructor定义为:

Point3d::Point3d( float x, float y, float z )
  : Point( x, y ), _z( z ) { }

       更进一步假设我们的Vertex和Vertex3d constructors有类似的定义。

       下面是vptr必须被设定的两种情况:

        1)当一个完整的对象被构造起来时。如果我们声明一个Point对象,则Point constructor必

须设定其vptr。

         2)当一个subobject constructor调用一个virtual function(不论是其直接调用或间接调用)

时。

       如果我们声明一个PVertex对象,然后由于我们对其base class constructors的最新定义,其

vptr将不再需要在每一个base class constructor中被设定。解决之道是把constructor分裂为一个

完整的object实例和一个subobject实例。在subobject实例中,vptr的设定可以省略(如果可能

的话)。

       知道了这些之后,你应该能够回答下面的问题了:在class的constructor的member

initialization list中调用该class的一个虚拟函数,安全吗?就实际而言,将此函数施于其class’s

data member的初始化行动中,总是安全的。这是因为,正如我们所见,vptr保证能够在

member initialization list被扩展之前,由编译器正确地设定好。但是在语意上这可能是不安全

的,因为函数本身可能还得依赖未被设立初值的members。所以并不推荐这种做法。然而,

从vptr的整体角度来看,这是安全的。

      何时需要供应参数给一个base class constructor?这种情况下在”class的constructor的

member initialization list中“调用该class的虚拟函数,是不安全的。此时,vptr若不是尚未被设

定好,就是被设定指向错误的class。更进一步地说,该函数所存取的任何class‘s data

members一定还没有被初始化。

三、对象复制语意学(Object Copy Semantics)

     当我们设计一个class,并以一个class object指定另一个class object时,我们有三

种选择:

     1)什么都不做,因此得以实施默认行为。

     2)提供一个explicit copy assignment operator。

     3)显式地拒绝把一个class object指定给另一个class object。

     如果选择第3点,不准将一个class object指定给另一个class object,那么只要将copy

assignment operator声明为private,并且不提供其定义即可。把它设为private,我们就不再允

许于任何地点(除了在member functions以及该class的friends之中)做赋值(assign)操作。

不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接时就

会失败。一般认为这和链接器的性质有关(也就是说并不属于语言本身的性质),所以不是很

令人满意。

      这里要验证copy assignment operator的语意,以及它们如何被模塑出来。再次利用Point

class来帮助讨论:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    // ... ( 没有virtual function )
  
  protected:
    float _x, _y;
};

       没有什么理由需要禁止拷贝一个Point object。因此问题就变成了:默认行为是否足够?如

果我们要支持的只是一个简单的拷贝操作,那么默认行为不但足够而且有效率,我们没有理由

再自己提供一个copy assignment operator。

       只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment

operator(memberwise copy及其潜在陷阱)。默认的memberwise copy行为对于我们的Point

object不安全吗?不正确吗?不,由于坐标都内含数值,所以不会发生”别名化(aliasing)“或”

内存泄漏(memory leak)“。如果我们自己提供一个copy assignment operator,程序反倒会执

行得比较慢。

       如果我们不对Point供应一个copy assignment operator,而关是依赖默认的memberwise

copy,编译器会产生出一个实例吗?这个答案和copy constructor的情况一样:实际上不会!由

于此class已经有了bitwise copy语意,所以implicit copy assignment operator被视为毫无用处,

也根本不会被合成出来。

        一个class对于默认的copy assignment operator,在以下情况,不会变现出bitwise copy语

意:

      1)当class内含一个member object,而其class有一个copy assignment operator时。

      2)当一个class的base class有一个copy assignment operator时。

      3)当一个class声明了任何virtual functions(我们一定不要拷贝右端class object的vptr地

址,因为它可能是一个derived class object)时。

       4)当class继承自一个virtual base class(不论此base class 有没有copy operator)时。

       C++ Standard上说copy assignment operators并不表示bitwise copy semantics是

nontrivial。实际上,只有nontrivial instances才会被合成出来。

       于是,对于我们的Point class,这样的赋值(assign)操作:

Point a, b;
...
a = b;

        由bitwise copy完成,把Point b拷贝到Point a,其间并没有copy assignment operator被调

用。从语意上或从效率上考虑,这都是我们所需要的。注意,我们还是可能提供一个copy

constructor,为的是把name return value(NRV)优化打开。copy constructor的出现不应该让

我们以为也一定要提供一个copy assignment operator。

         现在要导入一个copy assignment operator,用以说明该operator在继承之下的行为:

inline
Point&
Point::operator=( const Point &p )
{
  _x = p._x;
  _y = p._y;
  
  return *this;
}

       现在派生一个Point3d class(虚拟继承):

class Point3d : virtual public Point
{
  public:
    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 );
    // ...
 
  protected:
    float _z;
};

      如果我们没有为Point3d定义一个copy assignment operator,编译器就必须合成一个(因为

前述的第二项和第四项理由)。合成而得的东西可能看起来像这样:

// C++伪码:被合成copy assignment operator
inline Point3d&
Point3d::operator=( Point3d* const this, const Point3d &p )
{
  // 调用base class的函数实例
  this->Point::operator=( p );
  
  // memberwise copy the derived class members
  _z = p._z;
  
  return *this;
}

       copy assignmentoperator有一个非正交性情况(nonorthogonal aspect,意指不够理想、不

够严谨的情况),就是它缺乏一个member assignment list(也就是平行于member initialization

list的东西)。因此我们不能够写:

// C++伪码,以下性质并不支持
inline Point3d&
Point3d::operator=( const Point3d &p3d )
  : Point( p3d ), z( p3d._z )
{ }

        我们必须写成以下两种形式,才能调用base class的copy assignment operator:

Point::operator=( p3d );

        或

( *( Point* )this ) - p3d;

        缺少copy assignment list,看起来或许只是一件小事,但如果没有它,编译器一般而言就

没有办法压抑上一层base class的copy operator被调用。例如,下面是个Vertex copy

operator,其中Vertex也是虚拟继承自Point:

// class Vertex : Virtual public Point
inline Vertex&
Vertex::operator=( const Vertex &v )
{
  this->Point::operator=( v );
  _next = v._next;
  
  return *this;
}

       现在让我们从Point3d和Vertex中派生出Vertex3d。下面是Vertex3d的copy assignment

operator:

inline Vertex3d&
Vertex3d::operator=( const Vertex3d &v )
{
  this->Point::operator=( v );
  this->Point3d::operator=( v );
  this->Vertex::operator=( v );
  ...
}

       编译器如何能够在Point3d和Vertex的copy assignment operators中压抑Point 的copy

assignment operators呢?编译器不能够重复传统的constructor解决方案(附加额外的参数)。

这是因为,和constructor以及destructor不同的是,”取copy assignment operator地址“的操作是

合法的。因此下面这个例子是毫无瑕疵的合法程序代码(虽然它也毫无瑕疵地推到了我们希望

把copy assignment operator做得更灵巧的企图):

typedef Point3d& ( Point3d::*pmfPoint3d )( const Point3d& );

pmfPoint3d pmf = &Point3d::operator=;
( x.*pmf )( x );

       然而我们无法支持它,我们仍然需要根据其独特的继承体系,安插任何可能个数的参数给

copy assignment operator。这一点在我们支持由class objects(内含virtual base classes)所

组成的数组的配置操作时,也被证明是非常有问题的。

       另一个方法是,编译器可能为copy assignment operator产生分化函数(splict functions),

以支持这个class成为most-derived class或成为中间的base class。如果copy assinment

operator被编译器产生的话,那么”split function解决方案“可说是定义明确。但如果它是被class

设计者所完成的,那就不能算是定义明确。例如,一个人如何分化像下面这样的函数呢(特别

当init_bases()是virtual时):

inline Vertex3d&
Vertex3d::operator=( const Vertex3d &v )
{
  init_bases( v );
  ...
}

       事实上,copy assignment operator在虚拟继承情况下行为不佳,需要小心地设计和说明。

许多编译器甚至并不尝试取得正确的语意,它们在每一个中间(调停用)的copy assignment

operator中调用每一个base class instance,于是造成virtual base class copy assignment

operator的多个实例被调用。而C++ Standard的说法是:

        我们并没有规定那些代表virtual base class的subobjects是否应该被”隐式定义(implicitly

defined)的copy assignment operator“指派(赋值,assign)内容一次以上。

       如果使用一个语言为基础的解决办法,那么应该为copy assignment operator提供一个附加

的”member copy list“。简单地说,任何解决方案如果是以程序操作为基础,就将导致较高的复

杂度和较大的错误倾向。一般公认,这是语言的一个弱点,也是一个人应该总是小心检验其程

序代码的地方(当他使用virtual base classes时)。

        有一种方法可以保证most-derived class 会引发(完成)virtual base class subobject的

copy行为,那就是在derived class的copy assignment operator函数实例的最后,显式调用那个

operator,像这样:

inline Vertex3d&
Vertex3d:operator=( const Vertex3d &v )
{
  this->Point3d::operator=( v );
  this->Vertex::operator=( v );
  // must place this last if your compiler does
  // not suppress intermediate class invocations
  this->Point::operator=( v );
  ...
}

      这并不能够省略subobjects的多重拷贝,但却可以保证语意正确。另一个解决方案要求把

virtual subobject拷贝到一个分离的函数中,并根据call path,条件化地调用它。

      建议尽可能不要允许一个virtual base class 的拷贝操作。甚至:不要在任何virtual base

class中声明数据。

四、对象的效能(Object Efficiency)

     在以下的效率测试中,对象构造和拷贝所需的成本是以Point3d class声明为基准的,从简单

形式逐渐到复杂形式,包括Plain Ol' Data、抽象数据类型(Abstract Data Type,ADT)、单一

继承、多重继承、虚拟继承。以下函数是测试的主角:

Point3d lots_of_copies( Point3d a, Point3d b )
{
  Point3d pC = a;
  
  pC = b;  // (1)
  b = a;   // (2)
  
  return pC;
}

       它内含4个memberwise初始化操作,包括两个参数、一个传回值以及一个局部对象pC。它

也内含两个memberwise拷贝操作,分别是标示为(1)和(2)那两行的pC和b。main()函数如

下:

main()
{
  Point3d pA( 1.725, 0.875, 0.478 );
  Point3d pB( 0.315, 0.317, 0.838 );
  Point3d pC;

  for( int iters = 0; iters < 10000000; iters++ )
    pC = lots_of_copies( pA, pB );

  return 0;
}

       第一个程序中数据类型是一个struct:

struct Point3d { float x, y, z; };

       第二个程序是拥有public数据的class:

class Point3d { public: float x, y, z; };

       第三个测试,唯一改变的是数据的封装以及inline函数的使用,以及一个inline constructor,

用以初始化每一个object。class仍然展现出bitwise copy语意,所以常识告诉我们,执行期的效

率应该相同。

class Point3d 
{
  public:
    inline Point3d( float _x, float _y, float _z )
        : x( _x ), y( _y ), z( _z ) { }
  
    inline Point3d lots_of_copies( Point3d b )
    {
      Point3d pC = *this;
  
      pC = b;  // (1)
      b = *this;   // (2)
  
      return pC;
    }

  private:
   float x, y, z;
};

       现在修改main()函数的初始化过程:

  Point3d pA;
  pA.x = 1.725; pA.y = 0.875; pA.z = 0.478;
  Point3d pB; 
  pB.x = 0.315; pB.y = 0.317; pB.z = 0.838;

      封装和未封装过的两种Point3d声明之间,另一个差异是关于下一行的语音:

Point3d pC;

       如果使用ADT表示法,pC会以其default constructor的inline expansion自动进行初始化——

甚至虽然在此例而言,没有初始化也很安全。从某一个角度来说,虽然这些差异实在小,但它

们扮演警告角色,警告说“封装加上inline支持,完全相当于C程序中的直接数据存取”。从另一

个角度来说,这些差异并不具有什么意义,因此也就没有理由放弃“封装”特性在软件工程上的利

益。它们是一些你得记在心中以备特殊情况下能够派上用场的东西。

        下一个测试,把Point3d的表现法切割为三个层次的单一继承:

class Point1d{}; // x
class Point2d : public Point1d{}; // y
class Point3d : public Point2d{}; // z

       

       下面的多重继承,一般认为是比较高明的设计。由于其member的分布,它完成了任务:

class Point1d{}; // x
class Point2d{}; // y
class Point3d : public Point1d, public Point2d{}; // z

       

         由于Point3d class仍然显现出bitwise copy语意,所以额外的多重继承关系不应该在

memberwise的对象初始化操作或拷贝操作上增加成本。

         下面是单层的虚拟继承:

class Point1d{}; // x
class Point2d : public virtual Point1d{}; // y
class Point3d : public Point2d{}; // z

         不再允许class拥有bitwise copy语意(第一层虚拟继承不允许之,第二层继承则更加复

杂)。合成型的inline copy constructor和copy assignment operator于是被产生出来,并派上用

场,这导致效率成本上的一个重大增加。

五、析够语意学(Semantics of Destruction)

       如果class没有定义destructor,那么只有在class内含的member object(抑或class自己的

base class)拥有destructor的情况下,编译器才会自动合成出一个来。否则,destructor被视为

不需要,也就不需要被合成(当然更不需要被调用)。例如,我们的Point,默认情况下并没有

被编译器合成出来一个destructor——甚至虽然它拥有一个virtual function:

class Point
{
  public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );

    virtual float z();
    // ...
  private:
    float _x, _y;
};

       类似的道理,如果我们把两个Point对象组合成一个Line class:

class Line
{
  public:
    Line( const Point&, const Point& );
    // ...

    virtual draw();
    // ...
  
  protected:
    Point _begin, _end;
};

      LIne也不会拥有一个合成出来的destructor,因为Point并没有destructor。

      当我们从Point派生出Point3d(即使是一种虚拟派生关系)时,如果我们没有声明一个

destructor,编译器就没有必要合成一个destructor。

      不论Point还是Point3d,都不需要destructor,为它们提供一个destructor反而是低效率的。

应该拒绝那种被称为“对称策略”的奇怪想法:“你已经定义了一个constructor,所以你以为提供

一个destructor也是天经地义的事”。事实上,应该因为“需要”而非“感觉”来提供destructor,更不

要因为不确定是否需要一个destructor,于是就提供它。

       为了决定class是否需要一个程序层面的destructor(或是constructor),请想想一个class

object的生命在哪里结束(或开始)?需要什么操作才能保证对象的完整?这是写程序时比较需

要了解的(或是你的class使用者比较需要了解的)。这也是constructor和destructor什么时候其

作用的关键。例如:

{
  Point pt;
  Point *p = new Point3d;
  foo( &pt, p );
  ...
  delete p;
}

       我们看到,pt和p在作为foo()函数参数之前,都必须初始化为某些坐标值。这时候需要一个

constructor,否则使用者必须显示提供坐标值。一般而言,class的使用者没有办法检验一个

local变量或heap变量以知道它们是否被初始化。把constructor想象为程序的一个额外负担是错

误的,因为它们的工作有其必要性。如果没有它们,抽象化(abstraction)的使用就会有错误

的倾向。

       当我们显示地delete掉p,会如何?有任何程序上必须处理的吗?是否需要在delete之前这

么做:

p->x( 0 ); p->y( 0 );

        当然不需要。没有任何理由说在delete一个对象之前先得将其内容清除干净。也不需要归还

任何资源。在结束pt和p的生命之前。没有任何“class使用者层面”的程序操作是绝对必要的,因

此,也就不一定需要一个destructor。 

        然而请考虑我们的Vertex class,它维护了一个由紧邻的“顶点”所形成的链表,并且当一个

顶点的生命结束时,在链表上来回移动以完成删除操作。如果这(或其他语意)正是程序员所

需要的,那么这就是Vertex destructor的工作。

        当我们从Point3d和Vertex派生出Vertex3d时,如果我们不供应一个explicit Vertex3d

destructor,那么我们还是希望Vertex destructor被调用,以结束一个Vertex3d object。因此编译

器必须合成一个Vertex3d destructor,其唯一任务就是调用Vertex destructor。如果我们提供一

个Vertex3d destructor,编译器会扩展它,使它调用Vertex destructor(在我们所供应的程序代

码之后)。一个由程序员定义的destructor被扩展的方式类似constructor被扩展的方式,但顺序

相反:

        1)destructor的函数本体现在被执行,也就是说vptr会在程序员的代码执行前被重设

(reset)。

        2)如果class拥有member class objects,而后者拥有destructors。那么它们会以其声明数

怒的相反顺序被调用。

        3)如果object内含一个vptr,那么首先重设(reset)相关的virtual table。

        4)如果有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会议其声明

顺序的相反顺序被调用。

        5)如果有任何virtual base classes拥有destructor,而目前讨论的这个class是最尾端

(most-derived)的class,那么它们会以其原来的构造顺序的相反顺序被调用。

        就像constructor一样,目前对于destructor的一种最佳实现策略就是维护两份destructor实

例:

        1)一个complete object实例,总是设定好vptr(s),并调用virtual base class destructor。

        2)一个base class subobject实例;除非在destructor函数中调用一个vritual function,否则

它绝不会调用virtual base class destructors并设定vptr。

       一个object的生命结束于其destructor开始执行之时。由于每一个base class destructor都轮

番被调用,所以derived object实际上变成了一个完整的object。例如一个PVertex对象归还其内

存空间之前,会依次变成一个Vertex3d对象、一个Vertex对象,一个Point3d对象,最后称为一

个Point对象。当我们在destructor中调用member functions时,对象的蜕变会因为vptr的重新设

定(在每一个destructor中,在程序员所供应的代码执行之前)而受到影响。

 

© 著作权归作者所有

thanatos_y
粉丝 8
博文 112
码字总数 315059
作品 0
成都
程序员
私信 提问
深度探索C++对象模型-纲要

深度探索C++对象模型-纲要 这个寒假第一个flag就要立起来啦,emmm,我觉得还是有必要坚持的,毕竟自己真的不小了。(2018/1/31) 对于这本书来说 第一章主要是讲关于对象,提供以对象为基础的观...

googler_offer
2018/01/31
0
0
C++基础教程之构造函数与析构函数

构造函数 当我们需要在对象创建时初始化一些数据的时候,我们不可能提供一个普通的成员方法供程序猿在对象创建后调用。因为如果程序猿故意或者无意间忘记了调用该方法,就可能导致程序出现偏...

这个人很懒什么都没留下
2018/09/08
0
0
绕开“陷阱“,阿里专家带你深入理解C++对象模型的特殊之处

摘要:本文介绍了C++对象模型的特殊之处,包括与C兼容的朴素模型,以及能支持多态的虚表模型,同时还带大家了解了构造函数与析构函数相关的一些特性与陷阱。这些内容能够帮助大家更好地学习和...

nirvanalucky
2018/04/25
0
0
python初体验(2)——面向对象篇

在学习python的面向对象时,我始终参照的是c++的面向对象机制。相对而言,python的面向对象确实更简单一些。 基础: 整数也被作为对象。前面就看到过,说所有的东西都是对象,包括函数。 属于...

晨曦之光
2012/06/06
93
0
最好的朋友:C++11 移动语义和 Pimpl 手法

当编译器可以用廉价的挪动操作替换昂贵的复制操作时,也就是当它可以用一个指向一个大对象的指针的浅层复制来替换对这个大对象的深层复制的时候,挪动语义要比复制语义更快速。因此,在类中利...

乌合之众
2016/06/08
3K
5

没有更多内容

加载失败,请刷新页面

加载更多

GatewayWorker 报错:stream_socket_server(): unable to connect to tcp://0.0.0.0:1238

GatewayWorker 报错:stream_socket_server(): unable to connect to tcp://0.0.0.0:1238 (Address already in use) 官方文档虽然有相同的问题,但是对我的问题没起作用…… 后面发现自己手贱...

wenzhizhong
昨天
0
0
REST接口

文章来源 https://zhuanlan.zhihu.com/p/28674721?group_id=886181549958119424 http://www.ruanyifeng.com/blog/2014/05/restful_api.html REST 对请求的约定 REST 用来规范应用如何在 HTTP......

Airship
昨天
3
0
Spring Cloud Config 统一配置中心

Spring Cloud Config 统一配置中心 一、统一配置中心 统一管理配置 通常,我们会使用配置文件来管理应用的配置。如一个 Spring Boot 的应用,可以将配置信息放在 application.yml 文件中,如...

非摩尔根
昨天
2
0
android ------ AAPT2 error: check logs for details解决方法

AAPT 是全称是 Android Asset Packaging Tool,它是构建 App,甚至是构建 Android 系统都必不可少的一个工具。它的作用是将所有资源文件压缩打包到Android APK 当中。我们在 Android SDK 目录...

切切歆语
昨天
2
0
今天的学习

今天学到了<select></select>标签: <label for="unittype">Select unit type: </label><select id="unittype" name="unittype" autofocus > <option value="1"> Miner </option> ......

墨冥
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部