您的位置:首页 > 编程语言 > C语言/C++

C++构造函数,复制构造函数和析构函数专题

2010-06-03 00:48 162 查看
本文作者:黄邦勇帅
本文是学习C++中的最基本的内容,因此学习C++就应全部掌握本文的内容。在知道了怎样声明一个类之后,就会遇到
关于构造函数的问题,本文是关于构造函数的专题,集中介绍了默认构造函数,初始化列表,重点介绍了复制构造函数,
直接初始化,复制初始化,赋值,临时对象之间的关系,本文内容全面,简单易懂。
本文内容完全属于个人见解与参考文现的作者无关,其中难免有误解之处,望指出更正。
声明:禁止抄袭本文,若需要转载本文请注明转载的网址,或者注明转载自“黄邦勇帅”。
主要参考文献:
1、C++.Primer.Plus.第五版.中文版[美]Stephen Prata 著孙建春韦强译人民邮电出版社2005 年5 月
2、C++.Primer.Plus.第四版.中文版Stanley B.Lippman、Barbara E.Moo 著李师贤等译人民邮电出版社2006 年3 月
3、C++.Primer.Plus.第三版.中文版Stanley B.Lippman 等著潘爱民张丽译中国电力出版社2002 年5 月
4、C++入门经典第三版[美]Ivor Horton 著李予敏译清华大学出版社2006 年1 月
5、C++参考大全第四版[美]Herbert Schidt 著周志荣朱德芳于秀山等译电子工业出版社2003 年9 月
6、21 天学通第四版C++ [美]Jesse Liberty 著康博创作室译人民邮电出版社2002 年3 月
构造函数和析构函数
一:基础
1.构造函数是用来保证初始化类中的成员变量的初值的,以保证每个对象的数据成员都有合适的初始值,当类的对象
被创建时就会调用构造函数.
2.构造函数的名字和它的类的名字相同,且没有反回类型.它可以有形参也可以没有形参,可以重载多个构造函数的
版本,注意不能用类的对象来调用构造函数.
3.当一个对象被破坏或结束时会调用析构函数,析构函数与类名相同并在名字前面有一个~运算符,析构函数也没有
返回类型.析构函数不能带有任何参数,也就是说析构函数只能有一个.析构函数必须是public 公有的.
二:调用构造函数(即初始化对象)
1.显示调用构造函数:例如定义了类www,则www x = www(5, 8.8, “hy”);将调用www 类的相匹配的构造函数来初
始化对象x.
2.隐式调用构造函数:例如定义了类www,则www x (5, 8.8, “hy”);将调用与www 类相匹配的构造函数来初始化对
象x,此语句与www x = www (5, 8.8, “hy”)是等价的.
3.用new 动态创建对象时都将调用构造函数,例如www *x = new www(5, 8.8, “hy”),同样将调用与www 类相匹配
的构造函数来初始化对象,这个对象没有名称,并将该对象的地址赋给指针变量x,该对象可以使用指针来访问.
三:默认构造函数
1.没有任何参数的构造函数就是默认构造函数.
2.当创建对象时没有显示的调用其他构造函数时将调用默认构造函数初始化该对象.例如定义了类www,则语句www
x 或www x =www()或www *x=new www()都将调用默认构造函数初始化对象.
3.如果没有提供任何构造函数C++将提供一个默认构造函数,如果用户自已定义了任何一个构造函数C++就不自动生
成默认构造函数,如果这时用户自已也没提供默认构造函数则语句www x 将出错.
4.默认构造函数只能有一个.
5.有两种方法提供默认构造函数,一种是不带参数的构造函数,一种是构造函数使用默认值.
6.C++自动提供的默认构造函数提供的是隐式初始化值,内置和复合类型的成员如指针和数组,只对定义在全局作用
域中的对象才初始化,当对象定义在局部作用域时,内置和复合类型的成员不进行初始化,这些对象处于未定义状
态,以任何方式访问这些对象都将是错误的。例如如果x 是int 型全局变量,则将把x 初始化为0;如果x 是局部变
量将不对其进行初始化
7. 当定义了默认构造函数时不要用语句hyong a();来初始化对象a, 因为这里编译器会把a 理解为是一个反回类型为
hyong 的函数,而不会初始化对象a,正确语句应是hyong a=hyong();
8. 只有一个参数的构造函数如hyong (int a);可以用语句hyong x=6 来调用构函数。
四:初始化列表
1.例:假如定义了类www 则www::www (int i, int j, int k):x (i), y(j), z(k){},定义了一个带i,j,k 三个参数的构造函数,
并将参数i 赋给类www 的成员变量x,参数j 赋给y,参数k 赋给z.注意参数列表形式,它们之间用:隔开.带一
个参数的初始化列表为www(int i):x(i),y(1),z(1){}该语句定义了一个带一个参数i 的构造函数,且把i 赋给类的成员
变量x,把1 赋给成员变量y 和z。
2. 没有在初始化列表中提及的成员变量使用与初始化变量相同的规则来初始化该变量,即全局变量初始化为0,而局
部变量就没有确定的值,即使类中定义有默认构造函数且初始化了初始化表中没提及的变量,只要调用了初始化列
表来初始化类的对象,这时这个未被初始化提及的变量也不会被默认构造函数初始化,而是按初始化变量的规则来
初始化这个变量的。
3. 初始化的次序问题:初始化列表的初始化次序是按成员变量在类中的声明次序执行的,而不是按初始化列表列出的
顺序初始化的,例如在类hyong中依次声明int a,b,c;那么hyong():c(a),b(2),a(3){}语句执行顺序是先把a初始化为3,
再把b初始化为2,最后把a的值赋给变量c,这是正确的,但是hyong():c(1),b(2),a(c){}就会出错,因为这时执行顺
序是先把变量c的值赋给变量a,而不是先把整数1赋给变量c,所以这时变量c还未被初始化,而变量a就会得到
一个错误的值。
4. 必须使用初始化列表的情形:因为不能直接在类定义中直接初始化变量,而const类型的变量和引用类型的变量又必
须在声明时进行初始化,const类型的变量只能初始化不能进行赋值,比如hyong类中定义了const int a变量,而在
hyong::hyong(){a=1;}这时就会发生错误,const变量不能赋值,只能初始化。这时就发生了毛盾,解决的方法就是使
用初始化列表,即const 类型的变量和引用类型的变量必须在初始化列表中初始化,比如初始化hyong 类中的const
变量a时就应这样写hyong::hyong():a(1){}就是正确的表达式。
5. 在类中声明了const类型的变量,所有的构造函数都必须使用初始化列表的形式以便初始化const类型的变量。记住
是所有的构造函数,也就是你每定义一个构造函数都必须初始化const变量。
例:构造函数的使用
class hyong
{public: int a,b,c,d; hyong(){a=b=c=d=0;} hyong(int i){a=b=c=d=i;}
// hyong(int i=0,int j=0,int k=0,int l=0){a=b=c=d=0;} //默认构造函数的另一版本,注意此构造函数会与下面的语句hyong(int i)
发生不能确定调用哪个构造函数的错误,如果有语句hyong m(2)的话。};
int main()
{hyong m; cout<<m.a<<m.b<<m.c<<m.d<<"/n"; //调用默认构造函数,输出4个0。
hyong m1=hyong(); cout<<m1.a<<m1.b<<m1.c<<m1.d<<"/n"; //调用默认构造函数的另一方法输出4个0
//hyong m(); //不会调用构造函数,且语句会出错,因为编译器把m当做是一个具有反回类型为hyong的函数来处理的
hyong m2(2); cout<<m2.a<<m2.b<<m2.c<<m2.d<<"/n"; //调用构造函数hyong(int i);输出4个2
hyong m3=hyong(2); cout<<m3.a<<m3.b<<m3.c<<m3.d<<"/n"; //另一种调用构造函数的方法,输出4个2
//hyong m(3); //错误,不能重新初始化对象m,要重新初始化对象只能用赋值语句,即m=hyong(3);
hyong *m4=new hyong(4); cout<<m4->a<<m4->b<<m4->c<<m4->d<<"/n"; delete m4; //用new运算符初始化对象。输出4个4
m3.a=m3.b=m3.c=m3.d=5; cout<<m3.a<<m3.b<<m3.c<<m3.d<<"/n"; //可以使用点运算符直接对类中的公有成员赋值。输出4个5。
hyong m5=5; } //调用只有一个形参的构造函数的另一种方法。可以用等号调用。
例:初始化列表的使用
class hyong
{public: int a,b,c,d;
//如果在类中定声明了const变量,就必须初始化它,如果不初始化就会出错,所以所有的构造函数都必须使用初始化列表的形式以便初始
化const变量
//const int f=9; //错误,不能在类定义中初始化成员变量。
const int e; //const类型的常量e,必须在声明时初始化,但在类定义中不能初始化成员变量,所以e只能在下面的初始化列表中初始化
//hyong(){a=b=c=d=0;e=9;} //错误,const整型常量e只能初始化,不能赋值,const的常量e只能在初始化列表中初始化。
//hyong(int i){a=b=c=d=i;} //错误,类中定义了const常量,必须要初始化const常量,只能用初始化列表形式的构造函数
hyong(); hyong(int i,int j,int k); hyong(int i,int j);}; //声明初始化列表形式的构造函数。
//初始化列表的初始化次序问题
hyong::hyong():a(0),b(0),c(0),d(0),e(1){} //默认构造函数把所有成员变量初始化为0,把const变量e初始化为1。
//初始化列表的初始化次序是按成员变量在类中的声明次序初始化的,与初始化列表的排列顺序无关。所以初始化列表的顺序最好与成员变
量的初始化顺序相一致,以免引起混乱。
hyong::hyong(int i,int j,int k): d(a),b(j),c(k), e(9) ,a(i){} //正确形式。注意语句d(a)
//声明了一个带有三个参数的初始化列表,且初始化的顺序是把i的值赋给a,j赋给b,k赋给c,最后把a的值赋给d,9赋给整型常量e,注意整
型常量e只能在初始化列表中初始化,初始化的顺序与初始化列表成员的排列顺序无关,只与成员变量的初始化次序有关。
hyong::hyong(int i,int j):d(;<=>?@<=A?B<=C?D<=E?F<GH//错误,初始化的次序是把未初始化的d赋给a,i赋给b,j赋给c,M赋给d。a会得
到一个不确定的值。并不是先把X赋给d,i赋给b,j赋给c,9赋给e,最后才把d赋给e.初始化的次序于初始化列表的排列顺序无关。
int main()
{hyong m; cout<<m.a<<m.b<<m.c<<m.d<<m.e<<"/n"; //输出0001,调用默认构造函数的初始化列表。
hyong m1(1,2,3); cout<<m1.a<<m1.b<<m1.c<<m1.d<<m1.e<<"/n"; //输出12319,调用带三个int型参数的构造函数
hyong m2(1,2); cout<<m2.b<<m2.c<<m2.d<<m2.e<<m2.a<<"/n"; } //输出12gD和一个随机数,在初始化列表中d还未被初始化就赋给了a,
所以a输出随机数。
例:用初始化列表初始化数组成员的方法
class hyong {public: int a; const int b;int cqrstuuuuuvwxng(int i); yvwxz{?<GAx|}~~"@{x|€<<"/n";} };
hyong::hyong(int i):a(i),b(1){c‚sƒ„tAq„sƒ…tHuu//初始化数组成员在大括号中初始化
int main() {hyong j(2); cout<<j.a<<j.b<<"/n"<<j.cŽ‚s~~BAq„s~~"/n";}
五:复制构造函数,直接初始化,复制初始化,赋值,临时对象
复制构造函数应弄清的几个问题:何时调用复制构造函数,复制构造函数有何功能,为什么要定义自
已的复制构造函数。
1.复制构造函数:当用户没有定义自已的复制构造函数时系统将生成一个默认的复制构造函数。当按值传递对象时,就
会创建一个形参的临时对象,然后调用复制构造函数把临时对象的值复制给实参。
2.默认复制构造函数的功能:将一个对象的非静态成员的值逐个复制给另一个对象,注意复制的是成员的值,这种复制
方式也称为浅复制。因为静态成员属于整个类,而不属于某个对象,所以调用复制构造函数时静态成员不会受到影响。
3.何时生成临时对象:情形1:按值传递对象注意是按值传递对象,按值传递意味着会创建一个原始对象的副本,
情形2:函数反回对象时。
情形3:用一个对象初始化另一个对象时即复制初始化,语句hyong x=y和hyong x=hyong(y)这里y是hyong类型的
对象。都将调用复制构造函数,但有可能创建临时对象也有可能不创建临时对象而用复制构造函数直接初始化对象,
这取决于编译器。
4.临时对象是由复制构造函数创建的,当临时对象消失时会调用相应的析构函数。也就是说只要创建了临时对象就会多
调用一次析构函数。
5.何时使用复制构造函数:按值传递对象,函数反回对象,用一个对象初始化另一个对象即复制初始化时,根据元素初
始化列表初始化数组元素。这四种情况都将调用复制构造函数。记住,复制构造函数只能用于初始化,不能用于赋值,
赋值时不会调用复制构造函数,而是使用赋值操作符。
6.直接初始化:直接初始化是把初始化式放在圆括号中的,对于类类型来说,直接初始化总是调用与实参匹配的构造函
数来初始化的,
7.复制初始化与复制构造函数:复制初始化使用=等于符号来初始化,复制初始化也是创建一个新对象,并且其初值来
自于另一个已存在的对象,复制初始化总是调用复制构造函数来初始化的,复制初始化时首先使用指定的构造函数创
建一个临时对象,然后用复制构造函数将临时对象的每个非static 成员依次的复制到新创建的对象。复制构造函数执
行的是逐个成员初始化。注意这里是用一个已存在的对象创建另一个新对象,与用构造函数直接创建一个新对象不一
样,使用构造函数初始化时不会使用另一个对象。比如有类hyong,则语句hyong m(1,2)调用构造函数直接初始化,
而语句hyong n=m则是用已存在的对象m去初始化一个新对象n,属于复制初始化。
8.赋值:赋值是在两个已存在的对象间进行的,也就是用一个已存在的对象去改变另一个已存在对象的值。赋值将调用
赋值操作符对对象进行操作,赋值操作符将在操作符重载中讲解。比如有类hyong,有语句hyong x(1);hyong y(1,2)
则x=y;这就是赋值,因为对象x和y是已经存在的对象,而语句hyong x=y;则是复制初始化,是用一个已存在的对
象y去创建一个新对象x,所以是复制初始化。
9.复制初始化和赋值是在两个对象之间进行的操作,而直接初始化则不是。
10.注意:使用复制构造函数不一定创建临时对象就如语句hyong x=hyong(y),其中y是hyong 类型的对象,就有可能
不创建临时对象,这取决于编译器。这里如果创建了临时对象则当临时对象消亡时将调用一次析构函数,而如果没有
调用而是直接用复制构造函数初始化对象的就不会调用析构函数。
11.复制构造函数的形式:hyong(const hyong & obj);它接受一个指向类对象的常量引用作为参数。定义为const是必须的,
因为复制构造函数只是复制对象,所以没必要改变传递来的对象的值,声明为引用可以节省时间,如果是按值传递的
话就会生成对象的副本,会浪费资源,而引用就不会。
12.为什么需要定义自已的复制构造函数:如果类只包含类类型成员和内置类型的成员,则可以不用显示定义复制构造
函数。但如果类中包含有指针或者有分配其他类型资源时就必须重新定义复制构造函数。因为类中有指针成员,当把
用一个对象初始化另一个对象时,这时两个对象中的指针都指向同一段内存,这时如果其中一个对象被消毁了,这时
对象中指针所指向的内存也同样被消毁,但另一个对象确不知道这种情况,这时就会出现问题。比如hyong类中含有
一个成员指针p,当声明了hyong x=y其中y也是hyong类的对象,这时对象x和y中的指针成员p都指向同一段内
存,而如果y被消毁,但x还没被消毁时就会出问题,这时y中对象的成员指针p已经释放了该内存资源,而x中的
成员指针p还不知道已经释放了该资源,这时就会出问题。因为对象x和y中的成员指针共享同一段内存,所以对y
中的成员指针p的修改就会影响到对象x中的成员指针。所有这些情况都需要重定义复制构造函数来显示的初始化成
员的值,这种初始化方式也被称为深度复制。
13.如果显示定义了复制构造函数则调用显示复制构造函数来直接初始化对象,如果没有显示定义复制构造函数,则调
用默认的复制构造函数直接初始化对象。
14.注意:1.在VC++中语句hyong n=m不生成临时对象,但如果显示定义了复制构造函数则调用显示复制构造函数来直
接初始化对象n,如果没有显示定义复制构造函数,则调用默认的复制构造函数直接初始化对象n。
2.在VC++中语句hyong m1=hyong(m)有可能生成临时对象也有可能不生成临时对象,如果显示定义了复制构造函数
则用复制构造函数直接初始化对象m1,不生成临时对象。如果没有显示定义复制构造函数则复制构造函数将创造
临时对象,初始化对象m1
15.C++自动提供的成员函数,有:默认构造函数,复制构造函数,默认析构函数,赋值操作符,地址操作符即this 指
针,这五种函数如果用户没有定义,则系统会自动创建一个。
16.直接调用类中的构造函数:可以在类中的函数,类外的独立函数,即main()函数中直接调用某一个类的构造函数,
比如在main函数中可以有语句n=A(4);这里n是类A的对象,这里就是直接调用类A的构造函数创建一个类A的临
时对象,然后把该临时对象的值赋给类A 的对象n。在类中的函数和在类外的函数调用类的构造函数的方法和这里
类似。注意语句n.A(4)是错误的语句,不能由对象调用类中的构造函数。
例:复制构造函数的使用
class hyong
{public: int a,b,c; hyong(){a=b=c=0;cout<<"gouchao"<<"/n";} hyong(int i){a=b=c=i;cout<<"gouchao2"<<"/n";}
vwxz{?<GAx|}~~"‘@{x|€<<"/n";} hyong(const hyong ’x>B<GEƒ>ƒAƒDtAx|}~~"fu“@€<<"/n";} //复制构造函数。};
™x@F h(hyong k){cout<<"haoshu"<<k.a<<k.b<<"/n";} //按值传递对象
hyong f(){hyong m3(5); žC}|žz m3;} //反回对象。
//如果显示定义了复制构造函数则调用显示复制构造函数来直接初始化对象,如果没有显示定义复制构造函数,则调用默认的复制构造函数
直接初始化对象。
int main()
{//以下为几种复制初始化的方式。
hyong m(1);
// hyong n=m和hyong m1=hyong(m)是否生成临时对象依编译器而定
hyong n=m; //在¶·¸¸中此语句不生成临时对象,调用显示定义的复制构造函数初始化对象
cout<<m.a<<m.b<<"/n";//输出99
cout<<n.a<<n.b<<"/n";//,输出99,调用显示定义的复制构造函数初始化对象n,而不会生成临时对象。
hyong m1=hyong(m); //此语句要特别注意,因为此语句有可能生成临时对象也有可能不生成临时对象,如果显示定义了复制构造函数则用
复制构造函数直接初始化对象m1,而不会生成临时对象。如果没有显示定义复制构造函数则复制构造函数将生成临时对象,然后对m1进行初
始化。
cout<<m1.a<<m1.b<<"/n"; //输出输出11,调用显示定义的复制构造函数初始化对象m1,不生成临时对象。如果没有定义复制构造函数则输
出,同时会生成临时对象,临时对象撤消时会调用析构函数。
hyong m2(m); cout<<m2.a<<m2.b<<"/n"; //输出11,直接调用复制构造函数,因此不会生成临时对象
hyong *p=new hyong(m); cout<<p->a<<p->b<<"/n"; //不生成临时对象,直接调用复制构造函数初始化。
//按值传递和反回对象的例子。
h(m); cout<<"kkk"<<"/n"; //按值传递对象m,当调用函数h时就会使用复制控制函数生成一个临时对象,然后把这个临时对象复制给
实参,当函数调用完毕时就会撤消临时对象,此时会调用一个析构函数,析构函数在h函数的作用域消失时才调用,也就是说在执行了h的函
数体后才会调用析构函数。
hyong m4=f(); cout<<m4.a<<m4.b<<"/n"; //输出55,用返回的对象初始化对象m4,此语句没有生成临时对象,原因还不清楚,待考证,可
能与语句是复制初始化有关。
hyong m5; m5=f(); //此语句调用函数f,f反回一个对象,在反回时会调用复制构造函数生成一个临时对象,并把这个临时对象作为
默认赋值操作符的一个参数,因此这里不但调用了复制构造函数还调用了赋值操作符。
cout<<m5.a<<m5.b<<"/n";
hyong m !"""# $#!""//把m的值赋给m&,注意这里不会调用复制构造函数,也不会生成临时对象,因为这里会把m当成是默赋值操作符的一
个参数,调用的是默认赋值操作符。}
例:直接调用类中的构造函数
class B""""Cpublic: int a; DEFCG$H!I""""""BEint i){a=i;} JBEFCKLMNOO"PQR<<"/n";}
S"TEFCUVNMUW XEYF!I""I!"ZZ在类中调用类的构造函数,当该函数被对象调用时反回由构造函数`构造的一个临时对象。
b"cEF""CdVNMUW eEfF!I"""ZZ类外的函数调用类的构造函数的方法,注意,这里是直接使用函数名的。
int main()
{ q"#ErF!"B"WEsF!""""W$#tTEF!""""KLMNOOWtG<<"/n"; //输出3,调用类中的f函数,f函数用构造函数反回一个临时对象。
n=g(); cout<<n.a<<"/n"; //输出5,调用类外的函数g
n=wE F!""""KLMNOOWtG!"""""ZZ在main函数中直接调用构造函数创造一个临时对象,然后把这个临时对象的值赋给对象n。
//n.~EF!"I"""""ZZ错误,不能用类的对象来调用构造函数。
六带有一个参数的构造函数的隐式类型转换和explicit 关见字
1.当类中带有一个参数的构造函数时,将执形对象的隐式转换,比如有类A,有一个带有一个参数的构造函数A(int i){}
则当出现语句A m=1;就会调用带有一个参数的构造函数来创建对象m,也就是将语句转换为A m(1)的形式。
2. 如果有一个函数,比如void f(A j){}这个函数需要一个类A的对象作为参数,但是当调用语句为f(2)时,同样能调用
这个函数,这时函数的形参A j被解释为,A j=2即会隐式调用带有一个参数的构造函数来创建一个对象j。但是有
一种形式的函数定义当出现语句f(2)这样的调用时会出错,就是函数f定义的形式为void f(A &j){}定义一个接受一
个实参的引用时会出错,具体原因不清楚。但这几种情况都能正确调用void f(A j){},void f(const A j){};void f(const
A & j){}。
3. 如果不需要这种隐式的类型转换则在构造函数前使用关见字explicit,这个关见字只能用于构造函数前。如果在构造
函数前使用explicit关见字,这时语句A m=1和f(2)都将出错。
例:带一个参数的构造函数的隐式类型转换情况
class €
{public:int b; EFC‚$H!I""""JBEFCKLut<<"ƒQR<<"/n";}
„Eint i){b=i; cout<<"gou"<<"/n";}}; //定义带有一个参数的构造函数,此构造函数存在类类型间的隐式转换问题
//以下是几种正确定义的f函数,这些函数不会在f(2)这样的调用时出错
//形式:ŸLQ "TEB"¡FCI"""形式:£LQ "TEKLW¤N"B"¥¡FCI""形式:§LQ "TEKLW¤N"B"¡FCI
¨LQ f( const ©"¥¡FC"KLMNOO"f"<<"/n"<<j.b;}
// ªLQ "TEB"¥¡FCI //错误当出现f(2)这样的调用时会出错,但f(m)其中m是¯的对象不会出错。具体原因还不清楚,系统提示不能将int类
型转换为¾"¥类型的错误。
int main()
{¿"#$r!KLMNOO#t‚OO"/n"; //输出1,语句m=1会处理成m(1)的形式,即隐式被转换为掉用带有一个参数的构造函数。
f(2); }//调用f函数输出f和2,这里很七怪,明明f函数是接受一个È类型的对象,但这里用整数也能正确调用,原因就在于Î类中定义
了一个带有一个参数的构造函数,当调用这个函数时f函数的形参Ñ"¡会被自动转换为Õ"¡$s的形式,即调用类Ö中带有一个参数的构造函
数来构造一个对象j。
作者:黄邦勇帅
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐