在这个例子中,静态变量在一开始的时候就被初始化。通常这些对象由两部分构成。第一部分是数据段,静态变量被读取到全局的数据段中。第二部分是静态的初始化函数,在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)
{
...
}
}
4、构造函数不能成为虚函数
虚构造函数意味着程序员在运行之前可以在不知道对象的准确类型的情况下创建对象。虚构造函数在C++中是不可能实现的。最通常遇到这种情况的地方是在对象上实现I/O的时候。即使足够的类的内部信息在文件中给出,也必须找到一种方法实例化相应的类。然而,有经验的C++程序员会有其他的办法来模拟虚构造函数。
模拟虚函数需要在创建对象的时候指定调用的构造函数,标准的方法是调用虚的成员函数。很不幸,C++在语法上不支持虚构造函数。为了绕过这个限制,一些现成的方法可以在运行时刻确定构件的对象。这些等同于虚构造函数,但是这是C++中根本不存在的东西。
第一个方法是用switch或者if-else选择语句来手动实现选择。在下面的例子中,选择是基于标准库的type_info构造,通过打开运行时刻类型信息支持。但是你也可以通过虚函数来实现RTTI
class Base
{
public:
virtual const char* get_type_id() const;
staticBase* make_object
(const char* type_name);
};
const char* Base::get_type_id() const
{
return typeid(*this).raw_name();
}
class Child1: public Base
{
};
class Child2: public 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;
else
{
throw exception
("unrecognized type name passed");
return 0X00; // represent NULL
}
}
这一实现是非常直接的,它需要程序员在main_object中保存一个所有类的表。这就破坏了基类的封装性,因为基类必须知道自己的子类。
一个更面向对象的方法类解决虚构造函数叫做标本实例。它的基本思想是程序中生成一些全局的实例。这些实例只再虚构造函数的机制中存在:
class Base
{
public:
staticBase* make_object(const char* typename)
{
if (!exemplars.empty())
{
Base* end = *(exemplars.end());
list<Base*>::iterator iter =
exemplars.begin();
while (*iter != end)
{
Base* e = *iter++;
if (strcmp(typename,
e->get_typename()) == 0)
return e->clone();
}
}
return 0X00 // Represent NULL;
}
virtual ~Base() { };
virtual const char* get_typename() const
{
return typeid(*this).raw_name();
}
virtual Base* clone() const = 0;
protected:
static list<Base*> exemplars;
};
list<Base*> Base::exemplars;
// T must be a concrete class
// derived from Base, above
template<class T>
class exemplar: public T
{
public:
exemplar()
{
exemplars.push_back(this);
}
~exemplar()
{
exemplars.remove(this);
}
};
class Child: public Base
{
public:
~Child()
{
}
Base* clone() const
{
return new Child;
}
};
exemplar<Child> Child_exemplar;
在这种设计中,程序员要创建一个类的时候要做的是创建一个相应的exampler<T>类。注意到在这个例子中,标本是自己的标本类的实例。这提供了一种高校得实例化方法。
5、创建一个缺省构造函数
当继承被使用的时候,却省构造函数就会被调用。更明确地说,当继承层次的最晚层的类被构造的时候,所有基类的构造函数都在派生基类之前被调用,举个例子来说,看下面的代码:
#include<iostream.h>
class Base
{
int x;
public :
Base() : x(0) { } // The NULL constructor
Base(int a) : x(a) { }
};
class alpha : virtual public Base
{
int y;
public :
alpha(int a) : Base(a), y(2) { }
};
class beta : virtual public Base
{
int z;
public :
beta(int a) : Base(a), z(3) { }
};
class gamma : public alpha, public beta
{
int w;
public :
gamma ( int a, int b) : alpha(a), beta(b), w(4) { }
};
main()
{.....
}
在这个例子中,我们没有在gamma的头文件中提供任何的初始化函数。编译器会为基类使用缺省的构造函数。但是因为你提供了一个构造函数,编译器就不会提供任何缺省构造函数。正如你看到的这段包含缺省构造函数的代码一样,如果删除其中的缺省构造函数,编译就无法通过。
如果基类的构造函数中引入一些副效应的话,比如说打开文件或者申请内存,这样程序员就得确保中间基类没有初始化虚基类。也就是,只有虚基类的构造函数可以被调用。
虚基类的却省构造函数完成一些不需要任何依赖于派生类的参数的初始化。你加入一个init()函数,然后再从虚基类的其他函数中调用它,或在其他类中的构造函数里调用(你的确保它只调用了一次)。
6、不能取得构造函数的地址
C++中,不能把构造函数当作函数指针来进行传递,指向构造函数的的指针也不可以直接传递。允许这些就可以通过调用指针来创建对象。一种达到这种目的的方法是借助于一个创建并返回新对象的静态函数。指向这样的函数的指针用于新对象需要的地方。下面是一个例子:
class A
{
public:
A( ); // cannot take the address of this
// constructor directly
static 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留下了一个问题,因为准确的类型必须是可见的。上面的静态函数可以用来包装隐藏子类。
7、位拷贝在动态申请内存的类中不可行
C++中,如果没有提供一个拷贝构造函数,编译器会自动生成一个。生成的这个拷贝构造函数对对象的实例进行位拷贝。这对没有指针成员的类来说没什么,但是,对用了动态申请的类就不是这样的了。为了澄清这一点,设想一个对象以值传递的方式传入一个函数,或者从函数中返回,对象是以为拷贝的方式复制。这种位拷贝对含有指向其他对象指针的类是没有作用的(见图2)。当一个含有指针的类以值传递的方式传入函数的时候,对象被复制,包括指针的地址,还有,新的对象的作用域是这个函数。在函数结束的时候,很不幸,析构函数要破坏这个对象。因此,对象的指针被删除了。这导致原来的对象的指针指向一块空的内存区域-一个错误。在函数返回的时候,也有类似的情况发生。
图2. The automatic copy constructor that makes a bitwise copy of the class.
这个问题可以简单的通过在类中定义一个含有内存申请的拷贝构造函数来解决,这种靠叫做深拷贝,是在堆中分配内存给各个对象的。
8、编译器可以隐式指定强制构造函数
因为编译器可以隐式选择强制构造函数,你就失去了调用函数的选择权。如果需要控制的话,不要声明只有一个参数的构造函数,取而代之,定义helper函数来负责转换,如下面的例子:
#include <stdio.h>
#include <stdlib.h>
class Money
{
public:
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( " %f", _amount ); }
private:
Money( char *ch ) { _amount = atof( ch ); }
Money( double d ) { _amount = d; }
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++标准化的过程中移除一些瑕疵。