3DS MAX SDK开发学习记录

3DS MAX SDK开发学习记录 - 程序逻辑

 目 录
  1.1 基本思想
  1.2 系统管理插件的接口部分
  1.3 用户启用插件生成物体时的接口部分

1.1、基本思想
  3DS MAX SDK是一组库及相应的头文件,其中包含了3DS MAX的大部分核心代码,利用库中的函数就可与系统内核通信。3DS
MAX的插件(Plugins)就是一种动态链接库,每个插件都包含有若干个类及对象和公用函数。有一组公用函数及几个类的对象是提供给系统挂接和管理插件用的,另外一些类则实现插件本身的功能(如造型或修改)。

  插件中的每种活动都是由相应如的类对象来完成的,如系统获取插件中定义的类的描述、插件对象创建时的鼠标交互响应、用户对插件对象参数的修改等都有相应的类,系统会调用指定的公用函数取得这些对象的地址,并在适当的时候激活它们。这就是3DS
MAX的回调机制。

1.2、系统管理插件的接口部分

  

  // 定义插件对象的32位类ID。
  #define SPHERE_C_CLASSID1 8239283498
  #define SPHERE_C_CLASSID2 8239283498

  // 插件描述类对象
  SphereClassDesc sphereDesc;
  ClassDesc* GetSphereDesc() { return
&sphereDesc;}

  // 系统管理所需方法
  // 这几个方法都是dllexport型的
  LibVersion() {return VERSION_3DSMAX;}
  LibNumberClasses() {return 1;}
  LibClassDesc() {return GetSphereDesc();}
  LibDescription() {return _T("Sphere Object. Call
1-800-plig-in");}

  // 插件描述类,成员为供系统管理用的方法
  class SphereClassDesc: public ClassDesc
  {
  public:
    int      IsPublic()   {return 1;} // true可由用户选取,
                         // false由其它插件调用不与用户打交道。
    void *     Create(BOOL loading=FALSE)  {return new
SphereObject;}
    const TCHAR * ClassName()  { return _T("Sphere_c");}
    SClass_ID   SuperClassID(){ return GEOMOBJECT_CLASS_ID;}
    Class_ID    ClassID()   { return
CLASS_ID(SPHERE_C_CLASSID1,SPHERE_C_CLASS_ID2); }
    const TCHAR*  Category()   { return _T("How To");}
  }

1.3、用户启用插件生成物体时的接口部分

  

  // 创建时期的用户鼠标交互接口
  // 由一个CreateMouseCallBack衍生类的对象定义
  class SphereObjCreateCallBack: public CreateMouseCallBack
  {
    IPoint2 sp0;
    SphereObject *ob;
    Point3 p0;
  public:
  // 开发者定义的创建时期的用户交互方法
    int proc(ViewExp *vpt, int msg, int point, int flags, IPoints2
m, Matrix3 &mat);
    void SetObj(SphereObject *obj) { ob=obj;}
  }

  // 创建时对用户鼠标交互活动的响应代码
  int SphereObjCreateCallBack::proc(ViewExp *vpt,
      int msg, int point, int flags, IPoints2 m, Matrix3
&mat)
  {
    float r;
    Point3 p1,center;
    if(msg==MOUSE_POINT || msg==MOUSE_MOVE)
    {
      switch (point)
      {
        case 0:... break;
        case 1:... break;
      }
    else if(msg==MOUSE_ABORT) { return CREATE_ABORT; }
    return TRUE;
  }

  // 定义创物体创建时的回调类对象。
  static SphereObjCreateCallBack sphereCreateCB;

  // 在插件所创建的物体的类中定义获取创建回调函数地址的方法。
  CreateMouseCallBack* SphereObject::GetCreateMouseCallBack()
  {
    sphereCreateCB.SetObj(this);
    return &sphereCreateCB;
  }

  // 创建时期对用户修改物体参数的响应
  // 在用户编辑实体参数(创建时或修改时)系统将调用BeginEditParams(),该方法负责为面添加并注册滚转页
  // 在编辑完成时系统将会调用EndEditParams()。

  //BeginEditParams用来在用户进入参数编辑框时
  void SphereObject::BeginEditParams(IObjParam*ip,ULONG
flags,Animatable *prev)
  {
    SimpleObject::BeginEditParams(ip,flags,prev);
    this->ip=ip;
    if(pmapCreate&&pmapParam)
     {
      pmapCreate->SetParamBlock(this);
      pmapTypeIn->SetParamBlock(this);
      pmapParam->SetParamBlock(pblock);
    }
    else
    {
      if (flags&GEGIN_EDIT_CREATE)
      {
        pmapCreate=CreateCPParamMap(
            descCreate,
            CREATEDESC_LENGTH,
            this,
            ip,
            hInstance,
            MAKEINTRESOURCE(IDD_SPHEREPARAM1),
            _T("Creation Method"),
            0 );
        pmapTypeIn=CreateCPParamMap(
            descTypeIn,
            TYPEINDESC_LENGTH,
            this,
            ip,
            hInstace,
            MAKEINTRESOURCE(IDD_SPHEREPARAM3),
            _T("Keyboard Entry"),
            APPENDROOL_CLOSED );
      }
      pmapParam=CreateCPParamMap(
         descParam,
         PARAMDESC_LENGTH,
         pblock,
         ip,
         hInstace,
         MAKEINTRESOURCE(IDD_SPHEREPARAM2),
         _T("arameters"),
         0 );
    }
    if(pmapTypeIn)
    {
      pmapTypeIn->SetUserDlgProc(new
SphereTypeInDlgProc(this));
    }
  }

  void SphereObject::EndEditParams(IObjParam *ip,ULONG
flags,Animatable *next)
  {
    SimpleObject::EndEditParams(ip,flags,next);
    this->ip=NULL;
    if(flags&END_EDIT_REMOVEUI)
    {
      if(pmapCreate) DestroyCPParamMap(pmapCreate);
      if(pmapTypeIn)DestroyCPParamMap(pmapTypeIn);
      DestroyCPParamMap(pmapParam);
      pmapParam=NULL;
      pmapTypeIn=NULL;
      pmapCreate=NULL;
    }
    pblock->GetValue(PB_SEGS,ip->GetTime(),dlgSegments,FOREVER);

    pblock->GetValue(PB_SMOOTH,ip->GetTime(),dlgSmooth,FOREVER);

  }

  class SphereTypeInDlgProc: public ParamMapUserDlgProc
  {
  public:
    SphereObject *so;
    SphereTypeInDlgProc(SphereObject *s){so=s;}
    BOOL DlgProc(TimeValue t,IParamMap*map,HWIND hWnd,UINT
msg,WPARAM wParam,LPARAM lParam);
    void DeleteThis(){delete this;}
  };

  BOOL SphereTypeInDlgProc:lgProc(TimeValue,IParamMap *map,HWIND
hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
  {
    switch(msg)
    {
      case WM_COMMAND:
        switch(LOWORD(wParam))
        {
          case IDC_TI_CREATE:
            {
              if(so->crtRadius==0.0) return
TRUE;
              if(so->TestAFlag(A_OBJ_CREATING))

              {
                so->pblock->SetValue(PB_RADIUS,0,so->crtRadius);

              }
              Matrix3 tm(l);
              tm.SetTrans(so->crtPos);
              so->ip->NonMouseCreate(tm);

              return TRUE;
            }
            break;
        }
        return FALUSE;
    }

  目 录  2.1 基本的介绍  2.2 插件必须的函数  2.3 类描述器方法成员   3DS MAX
SDK插件是以DLL形式存在的。通常我们用Microsoft Visual C++来开发。建立一个新的工程的描述见“Creating
A New Plugin Project”开发者可以将这些DLL插件存放在任何地方,但是要想法子让3DS
MAX知道到哪里去找这些文件。这部分是在“Plug-In Directory Search
Mechanism”里讨论的。插件开发者可以为应用加上在线帮助,并使用望可在Max Help菜单里访问。细节见“Plug-In
Help
System”。有一个标准的位置供开发者保存插件所需的任何配置文件。这些可能是.ini文件,二进制配置文件或任何需要的文件。详见“Plug-In
Configuration System”。2.1、基本的介绍  2.1.1
标准DLL函数  所有的插件DLL都必须实现一套标准的函数:  DLLMain()  LibDescription()  LibNumberClasses()  LibClassDesc()  LibVersion()  这些允许3DS
MAX访问、维护在DLL内在插件并与之协同工作。这些函数的详情见“DLL, LIbrary Functions, and Class
Descriptors”。  2.1.2 重入与线程安全的插件  3DS
MAX插件必须是可重入与线程安全的。详在高级主题的“Thread Safe Plug-Ins”章节里。2.2、插件必须的函数  3DS
MAX进行DLL装入、分类、管理插件。包括DLL例程和类描述类。“Class
Descriptors”提供插件类的信息,用以实现LibClassDesc()函数。  2.2.1 DLL
functions  DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID
lpvReserved)
  当DLL被装入时由windows调用。该函数也会在时间关键性操作期间被多次调用,如渲染。所以开发者在该函数内要小心谨慎。注意以下的示意代码中,在DLL第一次调用以后只有很少的语句被执行。该函数应该返回TRUE。  int
controlsInit = FALSE;  BOOL WINAPI DllMain(HINSTANCE hinstDLL,ULONG
fdwReason,LPVOID lpvReserved)  {    // Hang on to this DLL's
instance handle.    hInstance = hinstDLL;    if (!
controlsInit) {      controlsInit = TRUE;      // Initialize MAX's
custom controls      InitCustomControls(hInstance);      //
Initialize Win95
controls      InitCommonControls();    }    return(TRUE);  }  2.2.2
LibNumberClasses()  当3DS
MAX启动之后,它找到并装入这些DLLs。然后,它需要有个方法判断DLL中的插件类数目。开发者应当在本函数中提供,例如:  __declspec(dllexport)
int LibNumberClasses() { return 1; }    返回值即插件类的个数。  2.2.3
LibClassDesc(i)  插件必须向系统提供一个方法以获取插件定义的类的描述器(Class
Descriptors)。类描述器向系统提供DLL中的插件类的信息。本函数使系统可以访问类描述器,返回值应该是指向第i个类描述器的指针。(一个DLL中可以有许多个类描述器)。例如:  __declspec(dllexport)
ClassDesc *LibClassDesc(int i)  {     switch(i) {      case 0:
return &MeltCD;      case 1: return
&CrumpleCD;      default: return
0;    }  }  这里是有关必须被LibClassDesc(i)返回的类描述器的资料。类描述器(Class
Descriptors)向系统提供DLL中插件类的信息。类描述的一个方法负责分配插件类的新实例(Create)。开发者通过从ClassDesc衍生一个子类并实现若干方法成员来建立类描述器。下面是个简单的类描述器及其静态实例的例子。  class
MeltClassDesc : public ClassDesc  {  public:    int      IsPublic()
{ return TRUE; }    void *    Create(BOOL loading=FALSE) { return
new MeltMod(); }    const TCHAR * ClassName() { return _T("Melt");
}    SClass_ID   SuperClassID() { return OSM_CLASS_ID;
}    Class_ID   ClassID() { return Class_ID(0xA1C8E1D1,
0xE7AA2BE5); }    const TCHAR* Category() { return _T("");
}  };  static MeltClassDesc MeltCD;  2.2.4
LibDescription()  当包含入口(过程物体、修改器或控制器等)的MAX文件被装入且系统还没有访问它时(如DLL无效),一个消息被发给用户。当DLL不可用时,系统要求每个DLL返回一个字符串向用户说明情况。例如,假定用户有一个融化修改器,他将此融化修改器应用到场景中的某个节点上,并保存此文件。当他将这个文件给一个没有这个融化DLL的朋友,这个朋友打开这个文件时,系统将发出一条消息说明文件中的一个入口所依赖的DLL找不到,这个消息可能是“融化修改器。想要请打电话025-1234PLUG-INS”。  DLL必须实现LibDescription()才能向系统提供这个字符串。该函数返回值即为当找不到该DLL时要显示的文字。该字符串也将显示在Summary
Info/Plug-In
Info...对话框中。一旦DLL中的一个插件已经在场景中使用过,系统就会把这个字符串保存到max文件里(以便在DLL丢失时显示)。  注意:即使DLL缺席时场景仍然会被打开。3DS
MAX保留任何DLL丢失的节点(entities),这样如果文件被修改并保存然后再在有DLL的系统打开修改后的文件,这些节点仍然存在并链接进场景。不能访问其DLL的入口称为封闭入口(orphaned
entities)。  封闭入口将作为其超类(SupperClass)的通用代表导入。该代表将在场景中显示最少的信息。举个实例:如果入口是个修改器,它将在修改器清单中显示自己的名字,但不会显示任何参数。如果没有对象类型信息,那么将在场景中显示为虚物体(dummy)。它们可以被移动、旋转、缩放、链接、组合、删除……任何与节点相关的操作。丢失的控制器只提供不变的默认值,这些值是不可调的。  参考“Read
Only
Plug-Ins”部分。通过允许插件工作在只读模式,用户可以自由分发DLL,其他人除非通过了基于硬件锁ID的认证可以运行它,否则使用将受到限制直到购买自己的拷贝。以下是该函数实现的一个例子:  __declspec(
dllexport ) const TCHAR *LibDescription()  {    return _T("Melt
Modifier. Call 1-800-PLUG-INS to obtain a copy");  }  2.2.5
LibVersion()  开发者必须实现一个函数以便系统处理不同版本的3DS
MAX插件DLL。因为MAX体系与插件的关系如此之紧密,系统有时候需要阻止插件的老版本被调用。要使MAX能够完成它,DLL必须实现一个名为LibVersion()的函数。这个函数只简单的返回一个预定义的常量,这个常量表明在插件编译时系统的版本。未来版本的MAX可能更新该常量,而老的DLL总是返回以前的值。该函数使得系统可以检查任意一个DLL是否已经被装入,如果是这样则显示一条消息。  __declspec(
dllexport ) ULONG LibVersion() { return VERSION_3DSMAX;
}  注意:开发者可以用下面的全局函数获取该值。  DWORD
Get3DSMAXVersion();  返回正在运行的MAX版本被编译时“\MAXSDK\INCLUDE\PLUGAPI.H”文件中包含的VERSION_3DSMAX宏定义的状态。  总结  插件必须实现这五个函数:DLLMain()、LibNumberClasses()、LibClassDesc(i)、LibDescription()、LibVersion()。这些函数允许系统取得DLL中插件的信息。2.3、六个类描述器方法成员  下面我们来讲讲六个类描述器方法成员:IsPublic()、Create()、ClassName()、SuperClassID()、ClassID()及Category()。  2.3.1
IsPublic()  该方法返回一个布尔值。如果插件可以被用户选取和指派,正是常见的情况,返回TRUE。某些插件可能是同一DLL的实现的其它插件私有专用的,并不出现在清单供用户选择。这些插件将返回FALSE。  2.3.2
Create(BOOL loading =
FALSE)  MAX在需要得到一个指向插件类的新实例的时候调用该方法。例如,如果MAX从磁盘打开一个包含前边用过的插件的文件,它将调用插件的Create()方法。插件负责分配一个插件类的新实例。在上边的例子的是用一个“new”操作简单实现的。  Create()的可选参数是一个标识表明要创建的类是否将从一个磁盘文件中装入。如果该标识为TRUE,插件可以不必做任何初始化工作,因为装入进程将会处理它。见“Loading
and
Saving”章节。  当系统需要删除一个插件类的实例时,它会调用Animatable的DeleteThis()方法。插件开发者必须实现该方法。因为开发使用new操作分配内存,它也应该用delete操作释放之。如开发者可以如下实现DeleteThis():  void
DeleteThis() { delete this; }  进一步的细节参考“Memory
Allocation”章节。  2.3.3
ClassName()  该方法返回类的名字。这个名字将出现在MAX用户界面的插件按钮上。该方法也在调试时显示类的名字。  2.3.4
SuperClassID()  该方法返回系统预定义的常量,该常量表示插件类是从哪个类衍生来的。例如,弯曲修改器返回OSM_CLASS_ID。这个超类ID被所有对象空间修改器使用。其它的超类ID例子有:CAMERA_CLASS_ID、LIGHT_CLASS_ID、SHAPE_CLASS_ID、HELPER_CLASS_ID、SYSTEM_CLASS_ID。完整的清单见“List
of Super Class IDs”。  2.3.5 ClassID()  该方法必须为对象返回一个唯一性ID。3DS MAX
SDK中包含一个生成这种ClassID的程序。使用该程序为你的插件创建ClassID是非常重要的。如果如果你使用任何一个例子程序的源代码来建立自己的插件,必须改变已经存在Class_ID。如果不这样,将会出现冲突。如果两个ClassID冲突,系统将加载它找到的第一个(并将在试图加载第二个时显示存在Class_ID冲突)。  一个Class_ID包含两个无符号的32位整数。建构函数为每一个赋值,如Class_ID(0xA1C864D1,
0xE7AA2BE5)。参见“Class Class
ID”。  注意在MAX使用的插件样本代码将类ID第二个32位整数设为0,只有与MAX一起发售的内建插件才可以这样做。所有的插件开发者都应该同时使用两个32位整数。还有,确保你使用SDK提供程序建立类ID,这可确保两个插件类之间不会冲突。要生成一个随机的Class_ID并可选的将其拷贝至剪帖板中,单击DLL
Function and Class部分的“Generate a Class_ID”。  2.3.6
Category()  在建立面板底部下拉选单选择类别。如果设成已经存在的类别(i.e. "Standard Primitives",
"article Systems",
etc),插件将会出现在那个类别里。开发者不应该加到MAX提供的类别里(见下边的注释)。如果类别还不存在,则将被创建。如果插件不需要出现在清单中,它可以简单的返回一个null字符串_T("")。Category()也被按钮设置对话框中用于插件分类。  重要说明:MAX体系在Create分支面板里有每类12个插件的限制。为预防每个类别有太多插件的问题,开发者应该总是为自己的插件建立一个新的类别而不是使用一个MAX标准插件已经使用的类别。注意早于1.2版本的MAX在每个类别有多于12个按钮时会崩溃。
  管理过程物体创建过程并编辑其参数  这部分讨论以下方法:  GetCreateMouseCallBack()  proc()  BeginEditParams()  EndEditParams()  GetValue()  SetValue  NumSubs()  SubAnim()  SubAnimName()  当MAX用户将要建立一个新的过程物体时,系统会调用插件的一个方法接管创建阶段的用户输入活动。插件可以任意实现其用户界面,但必须向MAX提供一个途径与用户交互过程联系。插件要实现GetCreateMouseCallBack()函数以向MAX提供该途径。这个函数返回一个指向CreateMouseCallBack衍生类实例的指针。这个类有一个proc()方法,程序员在这个函数里定义物体创建阶段的用户交互活动。系统实际上需要一个函数指针,这个函数是由插件实现可以被系统调用。参考sphere_c.cpp源代码。  插件必须实现一些方法以处理用在命令面板中的输入。插件要负责实现从Animatable类继承来的两个方法,这两个方法用来处理用户在命令面板中的输入:BeginEditParams()和EndEditParams()。Begin在用户可以编辑实体参数时由系统调用(物体创建或修改已经存在的实体时都可能调用),它负责向面板里添加卷展栏并将其注册到系统中。加入卷展栏函数带有一个Dialog
Proc参数。这个对话处理过程控制用户与对话框控件的交互活动。过程球体的例子使用了参数映射机制来简化开发者与管理用户界面控制相关工作任务。参数映射被用于管理UI交互活动。见参数映射。  End方法则在用户结束编辑一个物体的参数时被调用。系统将传递一个标识给EndEditParams()以指示卷展栏是否应该删除。如果为TURE,插件必须注销卷展栏,并将其从面板中删掉。在某些情况,物体的卷展栏应该保留在命令面板里。例如如果用户已经完成一个过程球体的建立,它的EndEditParams()方法被调用。然而用户可能希望建立另一个球,这样开发者不可以立刻移走卷展栏。这样用户界面不会因为删除后立刻加回来而闪烁。  参数映射  这部分对于理解例子及简化开发相当有用,所以列入基本内容。参数映射用于最小化插件管理用户界面参数所需的编程工作。一个简单插件,如过程球体,拥有由类似微调控件(to
be continued...)。

高质量C++教程 — 第9章 类的构造函数、析构函数与赋

高质量C++教程 -- 第9章 类的构造函数、析构函数与赋值函数
来源:www.vcworld.net 

构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。

每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如

   
A(void);                   
// 缺省的无参数构造函数

    A(const A
&a);               
// 缺省的拷贝构造函数

   
~A(void);                   
// 缺省的析构函数

    A
& operate =(const A
&a);   
// 缺省的赋值函数

 

这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?

原因如下:

(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。

(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

      

对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。

本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下:

    class
String

    {

     
public:

       
String(const char *str =
NULL);    //
普通构造函数

       
String(const String
&other);   
// 拷贝构造函数

       
~
String(void);                   
// 析构函数

       
String & operate =(const String
&other);   
// 赋值函数

     
private:

       
char     
*m_data;               
// 用于保存字符串

    };

9.1 构造函数与析构函数的起源

作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。

根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。Stroustrup在设计C++语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。

构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀‘~’以示区别。

除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(以上典故参考了文献[Eekel,
p55-p56])

9.2 构造函数的初始化表

构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {}
之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。

构造函数初始化表的使用规则:

如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。

例如

    class
A

    {…

       
A(int
x);       
// A的构造函数

}; 

    class B :
public A

    {…

       
B(int x, int y);// B的构造函数

    };

    B::B(int
x, int y)

    
:
A(x)            
// 在初始化表里调用A的构造函数

    {

     

}  

类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化(参见5.4节)。

类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如

    class
A

{…

   
A(void);               
// 无参数构造函数

    A(const A
&other);       
// 拷贝构造函数

    A
& operate =( const A
&other);   
// 赋值函数

};

 

    class
B

    {

     
public:

       
B(const A
&a);   
// B的构造函数

     
private:   

       

m_a;           
// 成员对象

};

 

示例9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。

示例9-2
(b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。

B::B(const A &a)

: m_a(a)

{

}

B::B(const A &a)

{

m_a = a;

}

示例9-2(a) 成员对象在初始化表中被初始化 示例9-2(b)
成员对象在函数体内被初始化

 

对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。若类F的声明如下:

class F

{

public:

F(int x, int y); // 构造函数

private:

int m_x, m_y;

int m_i, m_j;

}

示例9-2(c)中F的构造函数采用了第一种初始化方式,示例9-2(d)中F的构造函数采用了第二种初始化方式。

F::F(int x, int y)

: m_x(x), m_y(y)

{

m_i = 0;

m_j = 0;

}

F::F(int x, int y)

{

m_x = x;

m_y = y;

m_i = 0;

m_j = 0;

}

示例9-2(c) 数据成员在初始化表中被初始化 示例9-2(d)
数据成员在函数体内被初始化

9.3 构造和析构的次序

构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。

一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。[Eckel,
p260-261]

9.4
示例:类String的构造函数与析构函数

// String的普通构造函数

String::String(const char *str)

{

if(str==NULL)

{

m_data = new char[1];

*m_data = ‘’;

}

else

{

int length = strlen(str);

m_data = new char[length+1];

strcpy(m_data, str);

}

}

 

// String的析构函数

String::~String(void)

{

delete [] m_data;

// 由于m_data是内部数据类型,也可以写成 delete m_data;

}

9.5 不要轻视拷贝构造函数与赋值函数

由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:

本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。

现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data =
a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。

 

拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?

String a(“hello”);

String b(“world”);

String c = a; // 调用了拷贝构造函数,最好写成 c(a);

c = b; // 调用了赋值函数

本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。

9.6
示例:类String的拷贝构造函数与赋值函数

// 拷贝构造函数

String::String(const String &other)

{

// 允许操作other的私有成员m_data

int length = strlen(other.m_data);

m_data = new char[length+1];

strcpy(m_data, other.m_data);

}

 

// 赋值函数

String & String::operate =(const String
&other)

{

// (1) 检查自赋值

if(this == &other)

return *this;

 

// (2) 释放原有的内存资源

delete [] m_data;

 

// (3)分配新的内存资源,并复制内容

int length = strlen(other.m_data);

m_data = new char[length+1];

strcpy(m_data, other.m_data);

 

// (4)返回本对象的引用

return *this;

}

 

类String拷贝构造函数与普通构造函数(参见9.4节)的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。

类String的赋值函数比构造函数复杂得多,分四步实现:

(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a
这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如

// 内容自赋值

b = a;

c = b;

a = c;

// 地址自赋值

b = &a;

a = *b;

也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”

他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if语句

if(this == &other)

错写成为

if( *this == other)

(2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。

(3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘’。函数strcpy则连‘’一起复制。

(4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this
错写成 return this 。那么能否写成return other 呢?效果不是一样吗?

不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return
other返回的将是垃圾。

9.7
偷懒的办法处理拷贝构造函数与赋值函数

如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?

偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。

例如:

class A

{ …

private:

A(const A &a); // 私有的拷贝构造函数

A & operate =(const A &a); //
私有的赋值函数

};

 

如果有人试图编写如下程序:

A b(a); // 调用了私有的拷贝构造函数

b = a; // 调用了私有的赋值函数

编译器将指出错误,因为外界不可以操作A的私有函数。

9.8 如何在派生类中实现类的基本函数

基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

u 派生类的构造函数应在其初始化表里调用基类的构造函数。

u 基类与派生类的析构函数应该为虚(即加virtual关键字)。例如

#include

class Base

{

public:

virtual ~Base() { cout<< "~Base"
<< endl ; }

};

 

class Derived : public Base

{

public:

virtual ~Derived() { cout<<
"~Derived" << endl ; }

};

 

void main(void)

{

Base * pB = new Derived; // upcast

delete pB;

}

 

输出结果为:

~Derived

~Base

如果析构函数不为虚,那么输出结果为

~Base

 

u 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:

class Base

{

public:

Base & operate =(const Base
&other); // 类Base的赋值函数

private:

int m_i, m_j, m_k;

};

 

class Derived : public Base

{

public:

Derived & operate =(const Derived
&other); // 类Derived的赋值函数

private:

int m_x, m_y, m_z;

};

 

Derived & Derived::operate =(const Derived
&other)

{

//(1)检查自赋值

if(this == &other)

return *this;

 

//(2)对基类的数据成员重新赋值

Base::operate =(other); // 因为不能直接操作私有数据成员

 

//(3)对派生类的数据成员赋值

m_x = other.m_x;

m_y = other.m_y;

m_z = other.m_z;

 

//(4)返回本对象的引用

return *this;

}

 

9.9 一些心得体会

有些C++程序设计书籍称构造函数、析构函数和赋值函数是类的“Big-Three”,它们的确是任何类最重要的函数,不容轻视。

也许你认为本章的内容已经够多了,学会了就能平安无事,我不能作这个保证。如果你希望吃透“Big-Three”,请好好阅读参考文献[Cline]
[Meyers] [Murry]。

高质量C++教程 — 第6章 函数设计

高质量C++教程 -- 第6章 函数设计
来源:www.vcworld.net 

函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实现的一些规则。

函数接口的两个要素是参数和返回值。C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by
value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by
reference)。由于引用传递的性质象指针传递,而使用方式却象值传递,初学者常常迷惑不解,容易引起混乱,请先阅读6.6节“引用与指针的比较”。

 

6.1 参数的规则

●【规则6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。

例如:

void SetValue(int width, int height); // 良好的风格

void SetValue(int, int); // 不良的风格

float GetValue(void); // 良好的风格

float GetValue(); // 不良的风格

●【规则6-1-2】参数命名要恰当,顺序要合理。

例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1和str2,例如

void StringCopy(char *str1, char *str2);

那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。

可以把参数名字起得更有意义,如叫strSource和strDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。

还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。

如果将函数声明为:

void StringCopy(char *strSource, char *strDestination);

别人在使用时可能会不假思索地写成如下形式:

char str[20];

StringCopy(str, “Hello World”); // 参数顺序颠倒

●【规则6-1-3】如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。

例如:

void StringCopy(char *strDestination,const char *strSource);

●【规则6-1-4】如果输入参数以值传递的方式传递对象,则宜改用“const
&”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

●【建议6-1-1】避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。

●【建议6-1-2】尽量不要使用类型和数目不确定的参数。

C标准库函数printf是采用不确定参数的典型代表,其原型为:

int printf(const chat *format[, argument]…);

这种风格的函数在编译时丧失了严格的类型安全检查。

 

6.2 返回值的规则

●【规则6-2-1】不要省略返回值的类型。

C语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void类型。

C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/
C函数都必须有类型。如果函数没有返回值,那么应声明为void类型。

●【规则6-2-2】函数名字与返回值类型在语义上不可冲突。

违反这条规则的典型代表是C标准库函数getchar。

例如:

char c;

c = getchar();

if (c == EOF)

按照getchar名字的意思,将变量c声明为char类型是很自然的事情。但不幸的是getchar的确不是char类型,而是int类型,其原型如下:

int getchar(void);

由于c是char类型,取值范围是[-128,127],如果宏EOF的值在char的取值范围之外,那么if语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数getchar误导了使用者。

●【规则6-2-3】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。

回顾上例,C标准库函数的设计者为什么要将getchar声明为令人迷糊的int类型呢?他会那么傻吗?

在正常情况下,getchar的确返回单个字符。但如果getchar碰到文件结束标志或发生读错误,它必须返回一个标志EOF。为了区别于正常的字符,只好将EOF定义为负数(通常为负1)。因此函数getchar就成了int类型。

我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return语句返回。

函数getchar可以改写成 BOOL GetChar(char *c);

虽然gechar比GetChar灵活,例如 putchar(getchar());
但是如果getchar用错了,它的灵活性又有什么用呢?

●【建议6-2-1】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。

例如字符串拷贝函数strcpy的原型:

char *strcpy(char *strDest,const char *strSrc);

strcpy函数将strSrc拷贝至输出参数strDest中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性:

char str[20];

int length = strlen( strcpy(str, “Hello World”) );

●【建议6-2-2】如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。

例如:

class String

{…

// 赋值函数

String & operate=(const String
&other);

// 相加函数,如果没有friend修饰则只许有一个右侧参数

friend String operate+( const String &s1, const
String &s2);

private:

char *m_data;

}

String的赋值函数operate = 的实现如下:

String & String::operate=(const String
&other)

{

if (this == &other)

return *this;

delete m_data;

m_data = new char[strlen(other.data)+1];

strcpy(m_data, other.data);

return *this; // 返回的是 *this的引用,无需拷贝过程

}

对于赋值函数,应当用“引用传递”的方式返回String对象。如果用“值传递”的方式,虽然功能仍然正确,但由于return语句要把
*this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。例如:

String a,b,c;

a = b; // 如果用“值传递”,将产生一次 *this 拷贝

a = b = c; // 如果用“值传递”,将产生两次 *this 拷贝

String的相加函数operate + 的实现如下:

String operate+(const String &s1, const String
&s2)

{

String temp;

delete temp.data; // temp.data是仅含‘’的字符串

temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];

strcpy(temp.data, s1.data);

strcat(temp.data, s2.data);

return temp;

}

对于相加函数,应当用“值传递”的方式返回String对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象temp的“引用”。由于temp在函数结束时被自动销毁,将导致返回的“引用”无效。例如:

c = a + b;

此时 a + b 并不返回期望值,c什么也得不到,流下了隐患。

 

6.3 函数内部实现的规则

不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。

●【规则6-3-1】在函数体的“入口处”,对参数的有效性进行检查。

很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。详见6.5节“使用断言”。

●【规则6-3-2】在函数体的“出口处”,对return语句的正确性和效率进行检查。

如果函数有返回值,那么函数的“出口处”是return语句。我们不要轻视return语句。如果return语句写得不好,函数要么出错,要么效率低下。

注意事项如下:

(1)return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。例如

char * Func(void)

{

char str[] = “hello world”; // str的内存位于栈上

return str; // 将导致错误

}

(2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。

(3)如果函数返回值是一个对象,要考虑return语句的效率。例如

return String(s1 + s2);

这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建一个局部对象temp并返回它的结果”是等价的,如

String temp(s1 + s2);

return temp;

实质不然,上述代码将发生三件事。首先,temp对象被创建,同时完成初始化;然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中;最后,temp在函数结束时被销毁(调用析构函数)。然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。

类似地,我们不要将

return int(x + y); // 创建一个临时变量并返回它

写成

int temp = x + y;

return temp;

由于内部数据类型如int,float,double的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。

 

6.4 其它建议

●【建议6-4-1】函数的功能要单一,不要设计多用途的函数。

●【建议6-4-2】函数体的规模要小,尽量控制在50行代码之内。

●【建议6-4-3】尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。

带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。

●【建议6-4-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。

●【建议6-4-5】用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。

 

6.5 使用断言

程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。

断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。示例6-5是一个内存复制函数。在运行过程中,如果assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。

 

void *memcpy(void *pvTo, const void *pvFrom, size_t size)

{

assert((pvTo != NULL) && (pvFrom
!= NULL)); // 使用断言

byte *pbTo = (byte *) pvTo; // 防止改变pvTo的地址

byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址

while(size -- > 0 )

*pbTo ++ = *pbFrom ++ ;

return pvTo;

}

示例6-5 复制不重叠的内存块

 

assert不是一个仓促拼凑起来的宏。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。

很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你化了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序员偶尔还会设计出有错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽?除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。[Maguire,
p8-p30]

 

●【规则6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

●【规则6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。

●【建议6-5-1】在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。

●【建议6-5-2】一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

 

6.6 引用与指针的比较

引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,n是m的一个引用(reference),m是被引用物(referent)。

int m;

int &n = m;

n相当于m的别名(绰号),对n的任何操作就是对m的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以n既不是m的拷贝,也不是指向m的指针,其实n就是m它自己。

引用的一些规则如下:

(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。

(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

以下示例程序中,k被初始化为i的引用。语句k =
j并不能将k修改成为j的引用,只是把k的值改变成为6。由于k是i的引用,所以i的值也变成了6。

int i = 5;

int j = 6;

int &k = i;

k = j; // k和i的值都变成了6;

上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。

以下是“值传递”的示例程序。由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n,
所以n的值仍然是0。

void Func1(int x)

{

x = x + 10;

}

int n = 0;

Func1(n);

cout << “n = ”
<< n <<
endl; // n = 0

 

以下是“指针传递”的示例程序。由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。

void Func2(int *x)

{

(* x) = (* x) + 10;

}

int n = 0;

Func2(&n);

cout << “n = ”
<< n <<
endl; // n = 10

 

以下是“引用传递”的示例程序。由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。

void Func3(int &x)

{

x = x + 10;

}

int n = 0;

Func3(n);

cout << “n = ”
<< n <<
endl; // n = 10

 

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?

答案是“用适当的工具做恰如其分的工作”。

指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?

如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。