再上一篇:9.4 成员函数的重载
上一篇:9.5 this指针
主页
下一篇:10.2 析构函数
再下一篇:10.3 实现类型转换和拷贝的构造函数
文章列表

第十章 构造函数和析构函数

《VC++程序设计基础》,讲述C++语言的语法和标准库,以及Visual C++ 函数库和MFC类库的使用,并附相关代码示例。

本章讨论类的几个特殊的成员函数,它们涉及到对象的创建、对象的初始化、对象的拷贝初始化和对象的撤消。

10.1构造函数

在产生对象时,对对象的数据成员进行初始化的方法可有三种:一种是使用初始化数据列表的方法,第二种是通过构造函数实现初始化;第三种方法是通过对象的拷贝初始化函数来实现。

例10.1 使用初始化数据列表的方法对新产生的对象初始化。

#include

class C{

public:

int i;

char *name;

float num[2];

};

C c1={25,"张 三",{77.8,99.56}}; //A

void main(void )

{

cout<

cout<

}

执行程序时的输出为:

25 张 三 77.8 99.56

在A行产生对象c1时,完成对象数据成员的初始化。这种方法只能对类的公有数据成员初始化,而不能对私有的或保护的数据成员进行初始化。通常,产生对象时,对其数据成员进行初始化,是通过构造函数来实现的,而不是使用初始化列表的方法来实现。构造函数是类的成员函数,系统约定构造函数名必须与类名相同。构造函数提供了初始化对象的一种简单的方法。

10.1.1 定义构造函数

在定义一个类时,可根据需要定义一个或多个构造函数(重载构造函数)。构造函数与类的成员函数一样,可以在类中定义函数体,也可在类外定义函数体。在类中定义构造函数的一般格式为:

ClassName ( <形参表 >)

{

......

//函数体

}

在类外定义构造函数的一般格式为:

ClassName:: ClassName (<形参表 >)

{

......

//函数体

}

例10.2 使用构造函数对新产生的对象初始化。

#include

class C{

int i;

public:

char *name;

protected:

float num[2];

public:

C(int a, char *s,float x,float y)

{

i=a;

name=s;

num[0] = x; num[1]=y;

}

void Print(void)

{

cout<

cout<

}

};

void main(void )

{

C c1(25,"张 三",77.8,99.56); //B

c1.Print();

}

执行程序时的输出为:

25 张 三 77.8 99.56

从这程序中可以看出,在程序中并没有显式地调用构造函数C( ),而在执行B行产生对象c1时,由系统自动完成调用类的构造函数C( )。

对构造成函数,说明以下几点:

1、构造函数的函数名必须与类名相同。只有约定了构造函数名,系统在生成类的对象时,才能自动地调用类的构造函数。构造函数的主要作用是完成初始化对象的数据成员以及其它的初始化工作。

2、构造函数与其它的成员函数不一样,在定义构造函数时,不能指定函数返回值的类型,也不能指定为void类型。因构造函数是由系统自动调用的,所以不能指定构造函数的类型。

3、构造函数可以不带参数,也可以带若干个参数,也可以指定参数的缺省值。一个类可以定义一个构造函数,也可以定义若干个构造函数。当定义多个构造函数时,必须满足函数重载的原则,即所带的参数个数或参数的类型是不同的。这种情况属于构造函数的重载。4、若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其它类时,则可将构造函数定义为保护的成员函数。

例10.3 定义一个矩形类的构造函数。

class Rectangle{

private:

int Left,Right,Top,Bottom;

public:

Rectangle(int L,int R,int T,int B)

{

Left =L; Right =R;

Top=T;

Bottom = B;

}

Rectangle( )

{

Left =0; Right =0;

Top=0; Bottom = 0;

}

...... // 其它成员函数的定义

};

上例中定义了二个构造函数,一个不带参数,另一个带有四个参数。

10.1.2 构造函数与对象的初始化

当定义了类的构造函数后,在产生该类的一个对象时,系统根据定义对象时给出的参数,自动调用对应的构造函数,完成对象的数据成员的初始化工作。由于构造函数属于类的成员函数,它对私有数据成员、保护的数据成员和公有的数据成员均能进行初始化。

例10.4 自动调用构造函数来初始化对象。

//Ex10_4.h

#include

class Rectangle{

private:

int Left,Right,Top,Bottom;

public:

Rectangle(int L,int R,int T,int B)

{

Left =L; Right =R;

Top=T;

Bottom = B;

cout<<"调用带参的构造函数!\n";

}

Rectangle( )

{

Left =0; Right =0;

Top=0; Bottom = 0;

cout<<"调用不带参的构造函数!\n";

}

void Print(void)

{

cout<

<<'\t'<

}

};

//Ex10_4.cpp

#include “Ex10_4.h”

void main(void )

{

Rectangle r1(100,200,300,400); //A

r1.Print();

Rectangle r2, r3( ); //B

r2.Print();

// r3.Print(); //C

}

执行程序后的输出为:

调用带参的构造函数!

100 200 300 400

调用不带参的构造函数!

0 0 0 0

根据程序的输出,可以看出,在执行A行产生对象r1时,系统自动地调用了带参的构造函数r1.Rectangle(100,200,300,400),把对象名后括号中的参数作为调用构造函数的参数,完成对象r1的数据成员的初始化。执行B行产生对象r2时,系统自动地调用了不带参的构造函数r2.Rectangle( )。注意,B行中在对象名r2后并没有给出一对括号。在定义对象时,在对象名后加上一对括号后,并不表示定义对象,更不表示要调用不带参数的构造函数。如B行中r3( ),它表示r3是一个不带参数的函数,它的返回值为类Rectangle 的对象。只有进行这种约定后,系统才能区分是对不带参数的函数的原型说明,还是定义对象。

例10.5 产生全局对象、静态对象和局部对象的例子。

//Ex10_5.h

#include

class Rectangle{

private:

int Left,Right,Top,Bottom;

public:

Rectangle(int L,int R,int T,int B)

{

cout<<"调用带参的构造函数(全局)!\n";

Left =L; Right =R;

Top=T;

Bottom = B;

}

Rectangle(int L,int R,int T)

{

cout<<"调用带参的构造函数(静态)!\n";

Left =L; Right =R;

Top=T;

Bottom = 0;

}

Rectangle(int L,int R)

{

cout<<"调用带参的构造函数(局部)!\n";

Left =L; Right =R;

Top=0;

Bottom = 0;

}

Rectangle( )

{

cout<<"调用不带参的构造函数!\n";

Left =0; Right =0;

Top=0; Bottom = 0;

}

void Print(void)

{

cout<

<<'\t'<

}

};

//Ex10_5.cpp

#include "Ex10_5.h"

Rectangle r4(200,300,400,500); //A

void f1(void)

{

cout<<"进入函数f1()\n";

static Rectangle r5(200,200,500); //B

r5.Print();

Rectangle r6(100,100); //C

r6.Print();

}

void main(void )

{

cout<<"进入主函数main()\n";

r4.Print();

f1();

Rectangle r1(100,200); //D

r1.Print();

Rectangle r2;

r2.Print();

f1();

}

执行程序后的输出为:

调用带参的构造函数(全局)!

进入主函数main()

200 300 400 500

进入函数f1()

调用带参的构造函数(静态)!

200 200 500 0

调用带参的构造函数(局部)!

100 100 0 0

调用带参的构造函数(局部)!

100 200 0 0

调用不带参的构造函数!

0 0 0 0

进入函数f1()

200 200 500 0

调用带参的构造函数(局部)!

100 100 0 0

根据程序的执行情况和输出结果可以看出,在A行定义的全局对象r4的构造函数是在main函数执行之前被调用;在函数f1中,B行定义的静态对象r5,是在首次调用函数f1并产生该对象时,调用构造函数的,第二次调用函数f1时,不再重新产生对象r5;C行中定义的局部对象r6,每次调用函数f1时,都要调用构造函数。

10.1.3 构造函数与new运算符

可以使用new运算符来动态地建立对象。用new运算符建立对象时,同样地也要自动调用构造函数,以便完成初始化对象的数据成员。我们用例子来说明用 new运算符产生对象时调用构造函数的情况。

例10.6 用new运算符建立对象时调用构造函数。

#include

class D {

int x,y;

public:

D(int a, int b)

{

x=a; y=b;

cout << "调用构造函数D(int ,int )!\n";

}

D( )

{

cout << "调用构造函数D( )!\n";

}

void ShowXY()

{

cout <<"x=" << x << '\t' <<"y=" << y <<'\n';

}

};

void main(void)

{

D *pd = new D(5,10); //A

pd->ShowXY(); //B

D *p = new D; //C

p->ShowXY(); //D输出的值是不确定的

delete pd; //E

delete p; //F

}

执行程序后的输出为:

调用构造函数D(int ,int )!

x=5 y=10

调用构造函数D( )!

x=-842150451 y=-842150451

当用new建立一个动态的对象时,new首先为类D的对象分配一个内存空间,然后,自动地调用构造函数来初始化对象的数据成员,最后返回这个动态对象的起始地址。和定义对象时的情况一样,对于A行中的表达式:

new D(5,10)

new调用带参数的构造函数。B行输出的成员x和y具有确定的值。而对于B行的表达式:

new D

new调用不带参数的构造函数,故D行输出的成员x和y的值是不确定的。

注意,用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间。E行和F行分别回收pd和p所指向的动态对象占用的内存空间。

10.1.4 缺省的构造函数

在定义类时,若没有定义类的构造函数,则编译器自动产生一个缺省的构造函数,其格式为:

className::className() { }

从定义格式可以看出,这是一个函数体为空的构造函数,即在产生对象时,尽管也调用这缺省的构造函数,但这函数什么事也不做。所以缺省的构造函数并不对所产生对象的数据成员赋初值;换言之,产生对象时尽管调用了缺省的构造函数,但新产生对象的数据成员的值是不确定的。

关于缺省的构造函数,说明以下几点:

1、在定义类时,若定义了类的构造函数,则编译器就不产生缺省的构造函数。

2、在类中,若定义了没有参数的构造函数,或各参数均有缺省值的构造函数也称为缺省的构造函数,缺省的构造函数只能有一个。

3、要对对象的数据成员进行初始化时,必须定义构造函数。

4、产生对象时,系统必定要调用构造函数。所以任一对象的构造函数必须唯一。如:

class E {

int x,y;

public:

E(int a,int b)

{

x=a;y=b;

}

void P(void)

{ cout<

};

如果有说明:

E e;

则是错误的,因产生对象e时,没有合适的构造函数可供调用。这样定义对象时,必须有一个缺省的构造函数供产生对象e时调用。又如:

class Q {

int x,y;

public:

Q(int a=0,int b=0)

{

x=a;y=b;

}

Q( ) { }

void P(void)

{ cout<

};

如果有说明:

Q q;

也是错误的。在编译时,首先指出有二个缺省的构造函数,并在产生对象q时,不知调用哪一个缺省的构造函数,产生了二义性。又如:

class R {

int x,y;

public:

R(int a,int b)

{

x=a;y=b;

}

R( ) { x=y= 0; }

};

如果有说明:

R r();

则编译器认为这不是定义对象r,而是函数原型说明,即说明函数r( )的返回值为类R类型。所以,当定义的对象要调用缺省的构造函数时,在对象名后不能有括号。