再上一篇:第十章 构造函数和析构函数
上一篇:10.2 析构函数
主页
下一篇:10.4 构造函数与对象成员
再下一篇:第十一章 继承和派生类
文章列表

10.3 实现类型转换和拷贝的构造函数

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

本节介绍实现强制类型转换的构造函数和实现对象成员拷贝的构造函数,并说明只有一个参数的构造函数的特殊性质。

10.3.1 实现类型转换的构造函数

我们通过举例,来说明何时需要显式地使用实现类型转换的构造函数,何时使用隐含的实现类型转换的构造函数。

例10.11 单个参数的构造函数。

#include

class Ex1{

int x;

public:

Ex1(int a)

{

x=a;

cout<<"x="<

}

~Ex1()

{

cout<<"调用了析构函数!\n";

}

};

void main(void)

{

Ex1 x1(50); //A

Ex1 x2=100; //B

x2 = 200; //C

cout<<"标志:\n";

}

执行程序后输出以下7行:

50 调用了构造函数!

100 调用了构造函数!

200 调用了构造函数!

调用了析构函数!

标志:

调用了析构函数!

调用了析构函数!

程序中只定义了二个对象,为何会出现调用三次构造函数,调用三次析构函数呢?显然,执行A行时,要调用一次构造函数,输出第一行。执行B行时,也要调用一次构造函数,输出第二行。当构造函数只有一个数值参数时,则可以用语句:

Ex1 x2=100;

代替语句:

Ex1 x2(100);

注意,在这种情况下的等号是传递单个数值到构造函数的另一种方法,这是初始化对象而不是赋值。编译器将B行解释为:

Ex1 x2(100);

当执行C行语句:

x2 = 200;

时,x2应该接受一个Ex1类型的对象(同类型的对象之间可以赋值),这时编译器要调用构造函数将200转换为Ex1类型的对象,即产生一个临时的对象,并将这个对象赋给x2。为此,第三次调用构造函数,输出第三行。一旦完成了这种赋值,立即撤消这个临时对象,即调用析构函数,输出第四行。后面三行的输出是显而易见的,请读者自行分析。

当构造函数只有一个参数时,才能像C行那样使用。当构造函数有多个参数时,必须通过构造函数进行强制类型转换。

例10.12 用构造函数进行强制类型转换。

#include

class Ex2{

int x,y;

public:

Ex2(int a,int b)

{

x=a; y=b;

cout<<"x="<

}

~Ex2()

{

cout<<"调用了析构函数!\n";

}

};

void main(void)

{

Ex2 x1(50,100);

x1 = Ex2(300,600); //A

}

执行程序后输出以下4行:

50 100 调用了构造函数!

300 600 调用了构造函数!

调用了析构函数!

调用了析构函数!

A行中的表达式:

Ex2(300,600)

的作用是调用类Ex2的构造函数产生一个临时的对象,完成赋值后,立即撤消这个临时的对象。用构造函数进行类型转换的一般格式为:

(a1,a2, ......,an)

其作用是调用对应的带有 n 个参数的构造函数,产生一个临时的对象,用参数a1,a2, ......,an初始化这个临时的对象后,将这个对象作为操作数参加运算。运算结束后,系统自动地撤消这个临时的对象。

10.3.2 完成拷贝功能的构造函数

完成拷贝功能的构造函数的一般格式为:

ClassName :: ClassName(ClassName &c)

{

......

//函数体完成对应数据成员的赋值

}

构造函数的参数是该类类型的引用。显然,用这种构造函数来创建一个对象时,必须用一个已产生的同类型对象作为实参。

例10.13 使用完成拷贝功能的构造函数。

#include

class Test{

int x,y;

public:

Test(int a,int b)

{

x=a;y=b;

cout<<"调用了构造函数!\n";

}

Test(Test &t) //A

{

x=t.x; y=t.y;

cout<<"调用了完成拷贝的构造函数!\n";

}

void Show()

{

cout << "x="<

}

};

void main(void)

{

Test t1(10,10);

Test t2=t1; //B

Test t3(t1); //C

t1.Show();

t2.Show();

t3.Show();

}

执行程序后输出以下6行:

调用了构造函数!

调用了完成拷贝的构造函数!

调用了完成拷贝的构造函数!

x=10 y=10

x=10 y=10

x=10 y=10

执行B行和C行时,分别输出对应的第二和第三行。编译器自动将B行转换为:

Test t2(t1);

因此,B行和C行都调用了完成拷贝的构造函数,初始化新产生的对象。

例10.14 使用隐含的完成拷贝功能的构造函数。

#include

class Test{

int x,y;

public:

Test(int a,int b)

{

x=a;y=b;

cout<<"调用了构造函数!\n";

}

void Show()

{

cout << "x="<

}

};

void main(void)

{

Test t1(10,10);

Test t2=t1; //B

Test t3(t1); //C

t1.Show();

t2.Show();

t3.Show();

}

执行程序后输出以下4行:

调用了构造函数!

x=10 y=10

x=10 y=10

x=10 y=10

上例中,在类 Test中并没有定义一个复制数据成员的构造函数,而是由编译器自动地生成一个隐含的完成拷贝功能的构造函数:

Test::Test(Test &t)

{

x=t.x; y=t.y;

}

由编译器为每个类产生的这种隐含的完成拷贝功能的构造函数,依次完成类中对应数据成员的拷贝。但是,在产生对象时,仅只要拷贝同类型对象的部分数据成员时,或者类中的数据成员中使用 new运算符,动态地申请存储空间进行赋初值时,必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确实现数据成员的复制。

例10.15 定义完成拷贝功能的构造函数。

//Ex10_15.h

#include

#include

class String{

char *s;

public:

String( char *p=0)

{

if( p) {

s=new char [strlen(p)+1];

strcpy(s,p);

}

else s=0;

}

String(String &p) //A

{

if(p.s ){

s=new char [strlen(p.s)+1];

strcpy(s,p.s);

}

else s=0;

}

~String( )

{ if(s) delete [ ] s; }

void Show()

{

cout << "s="<

}

};

//Ex10_15.cpp

void main(void)

{

String s1("教师");

String s2(s1); //B

s1.Show();

s2.Show();

}

若把上面程序中A行开始的构造函数String(String &p)删除后,再执行程序时,在执行程序期间要产生一个错误。当没有定义完成拷贝功能的构造函数时,编译器产生如下形式的构造函数:

String(String &p)

{ s=p.s ; }

在执行B行时,把对象s1中的数据成员s赋给s2中的s后,使用使得s1和s2中的成员s都指向同一个用于存放字符串"教师"的动态存储区。假定先撤消对象s1,则调用s1的析构函数,释放了对象s1中的s所指向的动态存储区。而在撤消对象s2时,调用s2的析构函数来释放s2中s所指向的动态存储区时(该存储区已被释放),必然要产生一个运行错误。类似于这种应用时,必须在类中定义一个完成拷贝功能的构造函数。

例10.16 同类型对象之间赋值出错的例子。

//Ex10_16.cpp

#include "Ex10_15.h"

void main(void)

{

String s1("学生");

String s2("教师");

s1.Show();

s2.Show();

s2=s1; //A

}

在A行中进行同类型对象之间的赋值,从语法上来说,这种赋值是允许的。但执行程序时产生运行错误。同样地,在对象s1和s2的生存期结束时,要调用这二个对象的析构函数,把存放字符串“学生”的存储空间释放二次,这是一种严重的错误。类似于这种类型的对象之间的赋值,程序设计者必须定义自己的实现对象之间的赋值运算,即要重载运算符“=”。实现方法在运算符重载这一章中介绍。