在第3章介绍过类的构造函数,当使用new关键字创建对象时,类的构造函数被自动调用,如果没有定义专门的构造函数,一个默认的无参数构造函数被调用。
在继承条件下,因为父类和子类都可以有自己的构造函数,情况就变得比较复杂了。
1.子类、父类构造函数的调用次序
请看以下代码(示例项目Constructors):
class Parent
{
public Parent()
{
System.Console.WriteLine("Parent的默认构造函数被调用");
}
}
class Child : Parent
{
public Child()
{
System.Console.WriteLine("Child的默认构造函数被调用");
}
}
调用代码如下:
Child c = new Child();
输出结果如下:
Parent的默认构造函数被调用
Child的默认构造函数被调用
可以看到,在创建子类对象时,首先调用父类构造函数,再调用子类构造函数。如果父类还有一个“祖父类”存在,则会先上溯到“祖父类”,调用“祖父类”构造函数,接着是父类构造函数,最后才是子类构造函数[①]。
继承条件下类构造函数的这种调用次序可以称之为“尊重长辈”原则。
向读者提个问题:
为什么子类的构造函数在运行之前,会调用父类的构造函数,反过来行不行?
给个线索:构造函数的主要作用是什么?可以从这个方面去想。
空几行,请读者不要“偷看答案”。
……
……
……
答案揭晓:
类的构造函数主要用于初始化类的数据成员,而父类中的数据成员可能会被子类构造函数所访问,因此,显然应该先初始化最顶层的父类数据成员,再依派生顺序初始化其子类的数据成员。
当对象析构时,会首先调用子类的析构函数,再调用父类的析构函数,刚好与对象创建时构造函数的调用顺序相反。
由于C#和Visual Basic.NET中对象的回收由CLR的垃圾收集机制负责,因此本节使用C++来展示对象的析构过程。
2.非托管代码对象的创建与回收问题
请参看以下代码(示例项目ObjectCreateAndDestoryForCPP):
class Parent
{
public:
Parent()
{
cout<<"Parent的默认构造函数被调用"<<endl;
}
~Parent()
{
cout<<"Parent的默认析构函数被调用"<<endl;
}
};
class Child: public Parent
{
public:
Child()
{
cout<<"Child的默认构造函数被调用"<<endl;
}
~Child()
{
cout<<"Child的默认析构函数被调用"<<endl;
}
};
程序中定义了两个有继承关系的类Parent和Child,并分别定义了一个构造函数与析构函数。
调用代码如下:
Child *pC=new Child(); //创建对象
delete pC; //销毁对象
运行结果:
Parent的默认构造函数被调用
Child的默认构造函数被调用
Child的默认析构函数被调用
Parent的默认析构函数被调用
程序的运行结果验证了我们的结论:
对象销毁时,先调用子类的析构函数,再调用父类的析构函数。
使用C++编写非托管的应用程序时,必须高度注意构造函数与析构函数的调用顺序问题。尤其是析构函数,一定要保证析构函数中不能有代码访问已被销毁的对象,否则将会引发内存存取冲突。
修改ObjectCreateAndDestoryForCPP示例的代码,以展示C++如何在对象构造与析构之时分配和释放资源。
首先,增加一个类A,其中有一个公有字段i。
class A
{
public:
int i;
};
接着,在Parent类中增加一个保护类型的成员pA,在构造函数中设置pA所指对象的i字段初值为100。
class Parent
{
protected:
A *pA;
public:
Parent()
{
pA->i=100;
cout<<"Parent的默认构造函数被调用"<<endl;
}
~Parent()
{
cout<<"Parent的默认析构函数被调用"<<endl;
}
};
在Child类的构造函数中创建A对象,然后,在析构函数中销毁对象A。
class Child: public Parent
{
public:
Child()
{
pA=new A(); //创建对象A
cout<<"Child的默认构造函数被调用"<<endl;
}
~Child()
{
delete pA; //删除对象A
cout<<"Child的默认析构函数被调用"<<endl;
}
};
调用代码不变:
Child *pC=new Child(); //创建对象
delete pC; //销毁对象
当编译程序并运行时,Visual Studio 2005报告发生内存访问冲突(见图4-6)。
图4-6 访问未创建的对象时引发内存访问冲突
引发以上错误的原因在于:父类Parent构造函数先于子类Child构造函数运行,而pA所指对象是在子类Child构造函数中创建的,因此,在Parent类构造函数运行时,pA所指对象还未创建,因而犯了“访问未创建对象”的错误。
如果在Parent类构造函数中删除“pA->i=100;”这句,并将其移到子类的构造函数中,则一切正常。
class Child: public Parent
{
public:
Child()
{
pA=new A(); //创建对象A
pA->i=100; //访问pA所指对象的字段i
cout<<"Child的默认构造函数被调用"<<endl;
}
//……
};
然而,要注意这时在Parent类的析构函数中不能有任何代码访问pA对象,为了检验这点,我们在Parent类的析构函数中加一句代码访问pA->i。
class Parent
{
public:
~Parent()
{
cout<<pA->i<<endl; //访问pA所指对象的字段i
cout<<"Parent的默认析构函数被调用"<<endl;
}
//……
};
运行结果为:
Parent的默认构造函数被调用
Child的默认构造函数被调用
Child的默认析构函数被调用
-17891602
Parent的默认析构函数被调用
可以看到,有一个很奇怪的数字“-17891602”出现,不是预想中的100。
根本原因在于子类Child析构函数最先运行,它销毁了pA所指的对象,这样,当父类Parent的析构函数运行时,就犯了“访问已销毁对象”的错误。由于在销毁pA时,未及时地设置其为NULL指针,所指向的内存单元已不再有效(可能被其他进程所使用)。所以,输出的是内存中此单元的当时内容,因而出现了这样奇怪的数字。
要更正很简单,请遵循一个基本编程原则:
每个类都负责创建与销毁归自己管的资源。
由于pA是类Parent的数据成员,因此,由Parent类负责其创建与回收工作,其子类只管使用就行了。正确的代码如下:
class Parent
{
protected:
A *pA;
public:
Parent()
{
pA=new A(); //创建对象A
pA->i=100; //访问pA所指对象的字段i
cout<<"Parent的默认构造函数被调用"<<endl;
}
~Parent()
{
cout<<pA->i<<endl; //访问pA所指对象的字段i
delete pA; //删除对象A
pA=NULL;//删除完对象之后,记住要及时将指针置为NULL
cout<<"Parent的默认析构函数被调用"<<endl;
}
};
现在,类Child只管使用pA,不再理会pA对象的创建与销毁问题。
可以看到,使用C++编程,对象的创建与销毁是比较复杂的,只要稍有不慎,就会引发错误。而在.NET下编程,不管是C#还是Visual Basic.NET,程序员都可以不用理会对象的销毁工作,麻烦事全让CLR代劳了,从而使程序员可以摆脱各种技术细节的纠缠,更高效地编程,应该是一件值得欢迎的事。
3.子类与父类构造函数的重载
父类中可以有多个构造函数,子类可以根据需要有目的地选择一个调用。请看以下代码(示例项目Constructors):
class Parent
{
public Parent()
{
System.Console.WriteLine("Parent的默认构造函数被调用");
}
public Parent(String info)
{
System.Console.WriteLine("Parent.Parent(String)被调用:"+info);
}
}
上述代码中类Parent提供了两个构造函数。类Child可选择调用父类任意一个构造函数。
class Child : Parent
{
public Child() //调用父类默认构造函数
{
System.Console.WriteLine("Child的默认构造函数被调用");
}
public Child(String info):base(info)//调用父类重载的构造函数
{
System.Console.WriteLine("Child.Child(String)被调用:"+info);
}
}
特别注意C#使用base关键字调用父类重载的构造函数,并且这一调用声明紧跟在子类构造函数声明的后面,用冒号隔开。
调用代码如下:
Child c = new Child("Hello");
运行结果:
Parent.Parent(String)被调用:Hello
Child.Child(String)被调用:Hello
试一试:Visual Basic.NET子类使用关键字MyBase调用父类的构造函数,详情请查询Visual Studio 2005文档。读者可将上述C#代码转换为Visual Basic.NET作为练习。