1、构造函数的设计查看文章 论 C+构造函数中的不合理设计2009-02-19 12:23在 C+中,构造函数是一个在构件对象的时候调用的特殊的函数,其目的是对对象进行初始化的工作,从而使对象被使用之前可以处于一种合理的状态。但是,构造函数的设计并不完美,甚至有些不合理的特性。比如说,限定构造函数名称与类的名称相同的条件。这些特性在构造 C+编译器的时候是值得引起注意的。还有,在今后 C+的标准修订或者制定其他面向对象的设计语言时候应当避免这些特性。这里也提出了一些解决的方案。C+中,任何类都有一个(至少有一个)构造函数,甚至在没有构造函数被声明的时候亦是如此。在对象被声明的时候,或者被动态生成的
2、时候,这些构造函数就会被调用。构造函数做了许多不可见的工作,即使构造函数中没有任何代码,这些工作包括对对象的内存分配和通过赋值的方式对成员进行初始化。构造函数的名称必须与类的名称相同,但是可以有许多不同的重载版本来提供,通过参数类型来区分构造函数的版本。构造函数可以显式的通过用户代码来调用,或者当代码不存在是通过编译程序来隐式插入。当然,显式地通过代码调用是推荐的方法,因为隐式调用的效果可能不是我们所预料的,特别是在处理动态内存分配方面。代码通过参数来调用唯一的构造函数。构造函数没有返回值,尽管在函数体中可以又返回语句。每个构造函数可以以不同的方式来实例化一个对象,因为每个类都有构造函数,至少
3、也是缺省构造函数,所以每个对象在使用之前都相应的使用构造函数。构造函数的调用如图 1 所示。 图 1. The activities involved in the execution of a constructor因为构造函数是一种函数,所以他的可见性无非是三种public、private、protected。通常,构造函数都被声明为 public型。如果构造函数被声明为 private 或 protected,就限制了对象的实例化。这在阻止类被其他人实例化的方面很有效。构造函数中可以有任何 C+的语句,比如,一条打印语句,可以被加入到构造函数中来表明调用的位置。构造函数的类型C+中构造函
4、数有许多种类型,最常用的式缺省构造函数和拷贝构造函数,也存在一些不常用的构造函数。下面介绍了四种不同的构造函数。1、缺省构造函数缺省构造函数是没有参数的函数。另外,缺省构造函数也可以在参数列表中以参数缺省值的方式声明。缺省构造函数的作用是把对象初始化为缺省的状态。如果在类中没有显式定义构造函数,那么编译器会自动的隐式创建一个,这个隐式创建的构造函数和一个空的构造函数很相像。他除了产生对象的实例以外什么工作都不做。在许多情况下,缺省构造函数都会被自动的调用,例如在一个对象被声明的时候,就会引起缺省构造函数的调用。2、拷贝构造函数拷贝构造函数,经常被称作 X(X / Default constru
5、ctormystring (mystring / Coercion constructormystring ( char scr , size_t len);/ User-Defined constructor;4、强制构造函数C+中,可以声明一个只有一个参数的构造函数来进行类型转换。强制构造函数定一个从参数类型进行的一个类型转换(隐式的或显式的) 。换句话说,编译器可以用任何参数的实例来调用构造函数。这样做的目的是建立一个临时实例来替换一个参数类型的实例。注意标准新近加入 C+的关键字 explicit 是用来禁止隐式的类型转换。然而,这一特性还没能被所有的编译器支持。下面是一个强制构造函数
6、的例子:class A public :A(int ) ;void f(A) void g()A My_Object= 17;A a2 = A(57);A a3(64);My_Object = 67;f(77);像 A My_Object= 17;这种声明意味着 A(int)构造函数被调用来从整型变量生成一个对象。这样的构造函数就是强制构造函数。普遍特性下面是一些 C+构1、构造函数可以为内联,但不要这样做一般来讲,大多数成员函数都可以在前面加入“inline“关键字而成为内联函数,构造函数也不例外,但是别这么做!一个被定义为内联的构造函数如下:class x public : x (int
7、);:;inline x:x(int ).在上面的代码中,函数并不是作为一个单独的实体而是被插入到程序代码中。这对于只有一两条语句的函数来说会提到效率,因为这里没有调用函数的开销。用内联的构造函数的危险性可以在定义一个静态内联构造函数中体现。在这种情况下,静态的构造函数应当是只被调用一次。然而,如果头文件中含有静态内联构造函数,并被其他单元包括的话,函数就会产生多次拷贝。这样,在程序启动时就会调用所有的函数拷贝,而不是程序应当调用的一份拷贝。这其中的根本原因是静态函数是在以函数伪装下的真实对象。 应该牢记的一件事是内联是建议而不是强制,编译器产生内联代码。这意味着内联是与实现有关的编译器的不同
8、可能带来很多差异。另一方面,内联函数中可能包括比代码更多的东西。构造函数被声明为内联,所有包含对象的构造函数和基类的构造函数都需要被调用。这些调用是隐含在构造函数中的。这可能会创建很大的内联函数段,所以,不推荐使用内联的构造函数。2、构造函数没有任何返回类型对一个构造函数指定一个返回类型是一个错误,因为这样会引入构造函数的地址。这意味着将无法处理出错。这样,一个构造函数是否成功的创建一个对象将不可以通过返回之来确定。事实上,尽管 C+的构造函数不可以返回,也有一个方法来确定是否内存分配成功地进行。这种方法是内建在语言内部来处理紧急情况的机制。一个预定好的函数指针 new-handler,它可以
9、被设置为用户定制的对付 new 操作符失败的函数,这个函数可以进行任何的动作,包括设置错误标志、重新申请内存、退出程序或者抛出异常。你可以安心的使用系统内建的 new-handler。最好的使构造函数发出出错信号的方法,就是抛出异常。在构造函数中抛出异常将清除错误之前创建的任何对象及分配的内存。如果构造函数失败而使用异常处理的话,那么,在另一个函数中进行初始化可能是一个更好的主意。这样,程序员就可以安全的构件对象并得到一个合理的指针。然后,初始化函数被调用。如果初始化失败的话,对象直接被清除。3、构造函数不可以被声明为 staticC+中,每一个类的对象都拥有类数据成员的一份拷贝。但是,静态成
10、员则没有这样而是所有的对象共享一个静态成员。静态函数是作用于类的操作,而不是作用在对象上。可以用类名和作用控制操作符来调用一个静态函数。这其中的一个例外就是构造函数,因为它违反了面向对象的概念。关于这些的一个相似的现象是静态对象,静态对象的初始化是在程序的一开始阶段就进行的(在 main()函数之前) 。下面的代码解释了这种情况。MyClass static_object(88, 91);void bar()if (static_object.count( ) 14) .在这个例子中,静态变量在一开始的时候就被初始化。通常这些对象由两部分构成。第一部分是数据段,静态变量被读取到全局的数据段中。
11、第二部分是静态的初始化函数,在 main()函数之前被调用。我们发现,一些编译器没有对初始化的可靠性进行检查。所以你得到的是未经初始化的对象。解决的方案是,写一个封装函数,将所有的静态对象的引用都置于这个函数的调用中,上面的例子应当这样改写。 static MyClass* static_object = 0;MyClass*getStaticObject()if (!static_object)static_object = new MyClass(87, 92);return static_object;void bar()if (getStaticObject()-count( ) 15
12、).4、构造函数不能成为虚函数虚构造函数意味着程序员在运行之前可以在不知道对象的准确类型的情况下创建对象。虚构造函数在 C+中是不可能实现的。最通常遇到这种情况的地方是在对象上实现 I/O 的时候。即使足够的类的内部信息在文件中给出,也必须找到一种方法实例化相应的类。然而,有经验的 C+程序员会有其他的办法来模拟虚构造函数。模拟虚函数需要在创建对象的时候指定调用的构造函数,标准的方法是调用虚的成员函数。很不幸,C+在语法上不支持虚构造函数。为了绕过这个限制,一些现成的方法可以在运行时刻确定构件的对象。这些等同于虚构造函数,但是这是 C+中根本不存在的东西。第一个方法是用 switch 或者 i
13、f-else 选择语句来手动实现选择。在下面的例子中,选择是基于标准库的 type_info 构造,通过打开运行时刻类型信息支持。但是你也可以通过虚函数来实现 RTTIclass Basepublic:virtual const char* get_type_id() const;staticBase* make_object(const char* type_name);const char* Base:get_type_id() constreturn typeid(*this).raw_name();class Child1: public Base;class Child2: publ
14、ic Base;Base* Base:make_object(const char* type_name)if (strcmp(type_name,typeid(Child1).raw_name() = 0)return new Child1;else if (strcmp(type_name,typeid(Child2).raw_name() = 0)return new Child2;elsethrow exception(“unrecognized type name passed“);return 0X00; / represent NULL这一实现是非常直接的,它需要程序员在 mai
15、n_object 中保存一个所有类的表。这就破坏了基类的封装性,因为基类必须知道自己的子类。一个更面向对象的方法类解决虚构造函数叫做标本实例。它的基本思想是程序中生成一些全局的实例。这些实例只再虚构造函数的机制中存在:class Basepublic:staticBase* make_object(const char* typename)if (!exemplars.emp ty()Base* end = *(exemplars.end();list:iterator iter =exemplars.begin();while (*iter != end)Base* e = *iter+;i
16、f (strcmp(typename,e-get_typename() = 0)return e-clone();return 0X00 / Represent NULL;virtual Base() ;virtual const char* get_typename() constreturn typeid(*this).raw_name();virtual Base* clone() const = 0;protected:static list exemplars;list Base:exemplars;/ T must be a concrete class/ derived from
17、 Base, abovetemplateclass exemplar: public Tpublic:exemplar()exemplars.push_back(this);exemplar()exemplars.remove(this);class Child: public Basepublic:Child()Base* clone() constreturn new Child;exemplar Child_exemplar;在这种设计中,程序员要创建一个类的时候要做的是创建一个相应的 exampler类。注意到在这个例子中,标本是自己的标本类的实例。这提供了一种高校得实例化方法。5、创
18、建一个缺省构造函数当继承被使用的时候,却省构造函数就会被调用。更明确地说,当继承层次的最晚层的类被构造的时候,所有基类的构造函数都在派生基类之前被调用,举个例子来说,看下面的代码:#includeclass Baseint x;public :Base() : x(0) / The NULL constructorBase(int a) : x(a) ;class alpha : virtual public Baseint y;public :alpha(int a) : Base(a), y(2) ;class beta : virtual public Baseint z;public
19、:beta(int a) : Base(a), z(3) ;class gamma : public alpha, public betaint w;public :gamma ( int a, int b) : alpha(a), beta(b), w(4) ;main().在这个例子中,我们没有在 gamma 的头文件中提供任何的初始化函数。编译器会为基类使用缺省的构造函数。但是因为你提供了一个构造函数,编译器就不会提供任何缺省构造函数。正如你看到的这段包含缺省构造函数的代码一样,如果删除其中的缺省构造函数,编译就无法通过。如果基类的构造函数中引入一些副效应的话,比如说打开文件或者申请内存
20、,这样程序员就得确保中间基类没有初始化虚基类。也就是,只有虚基类的构造函数可以被调用。 虚基类的却省构造函数完成一些不需要任何依赖于派生类的参数的初始化。你加入一个 init()函数,然后再从虚基类的其他函数中调用它,或在其他类中的构造函数里调用(你的确保它只调用了一次) 。6、不能取得构造函数的地址C+中,不能把构造函数当作函数指针来进行传递,指向构造函数的的指针也不可以直接传递。允许这些就可以通过调用指针来创建对象。一种达到这种目的的方法是借助于一个创建并返回新对象的静态函数。指向这样的函数的指针用于新对象需要的地方。下面是一个例子:class Apublic:A( ); / cannot
21、 take the address of this/ constructor directlystatic A* createA();/ This function creates a new A object/ on the heap and returns a pointer to it./ A pointer to this function can be passed/ in lieu of a pointer to the constructor.;这一方法设计简单,只需要将抽象类置入头文件即可。这给new 留下了一个问题,因为准确的类型必须是可见的。上面的静态函数可以用来包装隐藏子
22、类。7、位拷贝在动态申请内存的类中不可行C+中,如果没有提供一个拷贝构造函数,编译器会自动生成一个。生成的这个拷贝构造函数对对象的实例进行位拷贝。这对没有指针成员的类来说没什么,但是,对用了动态申请的类就不是这样的了。为了澄清这一点,设想一个对象以值传递的方式传入一个函数,或者从函数中返回,对象是以为拷贝的方式复制。这种位拷贝对含有指向其他对象指针的类是没有作用的(见图 2) 。当一个含有指针的类以值传递的方式传入函数的时候,对象被复制,包括指针的地址,还有,新的对象的作用域是这个函数。在函数结束的时候,很不幸,析构函数要破坏这个对象。因此,对象的指针被删除了。这导致原来的对象的指针指向一块空
23、的内存区域-一个错误。在函数返回的时候,也有类似的情况发生。图 2. The automatic copy constructor that makes a bitwise copy of the class.这个问题可以简单的通过在类中定义一个含有内存申请的拷贝构造函数来解决,这种靠叫做深拷贝,是在堆中分配内存给各个对象的。8、编译器可以隐式指定强制构造函数因为编译器可以隐式选择强制构造函数,你就失去了调用函数的选择权。如果需要控制的话,不要声明只有一个参数的构造函数,取而代之,定义 helper 函数来负责转换,如下面的例子:#include #include class Moneypub
24、lic:Money();/ Define conversion functions that can only be/ called explicitly.static Money Convert( char * ch ) return Money( ch ); static Money Convert( double d ) return Money( d ); void Print() printf( “n%f“, _amount ); private:Money( char *ch ) _amount = atof( ch ); Money( double d ) _amount = d
25、; double _amount;void main()/ Perform a conversion from type char */ to type Money.Money Account = Money:Convert( “57.29“ );Account.Print();/ Perform a conversion from type double to type/ Money.Account = Money:Convert( 33.29 );Account.Print();在上面的代码中,强制构造函数定义为 private 而不可以被用来做类型转换。然而,它可以被显式的调用。因为转换函数是静态的,他们可以不用引用任何一个对象来完成调用。总结要澄清一点是,这里提到的都是我们所熟知的 ANSI C+能够接受的。许多编译器都对 ANSI C+进行了自己的语法修订。这些可能根据编译器的不同而不同。很明显,许多编译器不能很好的处理这几点。探索这几点的缘故是引起编译构造的注意,也是在 C+标准化的过程中移除一些瑕疵。