构造函数和析构函数

构造函数和析构函数
一:基础
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";}

~hyong(){cout<<"xigou"<<"\n";}    

 hyong(const hyong &obj){a=b=c=i;cout<<"gouchao2"<<"\n";}  //复制构造函数。

};


void h(hyong k){cout<<"haoshu"<<k.a<<k.b<<"\n";}  //按值传递对象
hyong f(){hyong m3(5); return 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 m6; m6=m;//把m的值赋给m&,注意这里不会调用复制构造函数,也不会生成临时对象,因为这里会把m当成是默赋值操作符的一个参数,调用的是默认赋值操作符。

}

例:直接调用类中的构造函数 

class A

{

public : int a ;

 A(){a=0;}

A(int i){a=i;}

 ~A(){cout<<"xi"<<"\n";}

A f(){return A (3);}\\在类中调用类的构造函数,当该函数被对象调用时反回由构造函数`构造的一个临时对象。

A g(){return A (5);}\\类外的函数调用类的构造函数的方法,注意,这里是直接使用函数名的。

}

int main()

{

 A m(1);

 A n(2); 

n=m.f(); 

cout<<n.a<<"\n";  //输出3,调用类中的f函数,f函数用构造函数反回一个临时对象。

n=g(); cout<<n.a<<"\n";     //输出5,调用类外的函数g
n=A(6);cout<<n.a;//在main函数中直接调用构造函数创造一个临时对象,然后把这个临时对象的值赋给对象n。
n.A(7);}//错误,不能用类的对象来调用构造函数。
六带有一个参数的构造函数的隐式类型转换和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(constA & j){}。
3. 如果不需要这种隐式的类型转换则在构造函数前使用关见字explicit,这个关见字只能用于构造函数前。如果在构造函数前使用explicit关见字,这时语句A m=1和f(2)都将出错。

例:带一个参数的构造函数的隐式类型转换情况

class A

public:int b; 

A(){b=0;}

  ~A(){cout<<"xi"<<"\n";}

}//定义带有一个参数的构造函数,此构造函数存在类类型间的隐式转换问题

//以下是几种正确定义的f函数,这些函数不会在f(2)这样的调用时出错

//形式:void f(A j){} 形式:void f(const A&j){} 形式:void f(const A j){}

void f(const A &j){}//错误当出现f(2)这样的调用时会出错,但f(m)其中m是¯的对象不会出错。具体原因还不清楚,系统提示不能将int类型转换为A&类型的错误。

int main()

{

A m=1;

cout<<m.b<<“\n”;

f(2);

}//调用f函数输出f和2,这里很奇怪,明明f函数是接受一个A类型的对象,但这里用整数也能正确调用,原因就在于A类中定义了一个带有一个参数的构造函数,当调用这个函数时f函数的形参A j会被自动转换为A j=2的形式,即调用类A中带有一个参数的构造函数来构造一个对象j。


1. MFC类库中,CObject类的重要性不言自明。在CObject的定义中,我们看到一个有趣的现象,即CObject的析构函数是虚拟的。为什么MFC的编写者认为虚拟构造函数是必要的?
答:首先构造一个类
class CBase
{
   public:
     ~CBase()  {...........}
   .....
};
 
class CChild:public CBase
{
   ~CChild()   {...........}
  ......
};
 
main()
{
   CChild c;
   .......
   return 0;
}
上段代码在运行时,由于在生成CChild对象c时,实际上在调用CChild类的构造函数之前必须调用基本CBase的构造函数,所以在撤销对象c时,也会在调用CChild类析构函数之后,在调用CBase类的析构函数(析构函数的调用顺序与构造函数相反)。也就是说,无论析构是不是虚函数,派生类对象被撤销时,肯定会依次上调基类的析构函数。而之所以CObject类要搞一个虚的析构函数是因为多态性的存在。仍以上面的代码为列子,如果main中有如下代码:
CBase *pBase;
CChild c;
pBase = &c;
那么在pBase指针被撤销时,调用的是CBase的析构函数还是CChild的呢?显然是CBase的(静态联编)析构函数。但如果把CBase类的析构函数改成为virtual型,当pBase指针被撤销时,就会先调用CChild类的析构函数,在调用CBase类的析构函数。
在这个例子中,所有对象都存在于栈框中,当离开其所处的作用域时,该对象会被自动撤销,似乎看不出什么大问题。但是如果在CChild类的构造函数中分配了内存,而其析构函数又不是virtual型的,那么撤销pBase时,将不会调用CChild的析构函数,从而不会释放内存,造成内存泄露。
将CObject的析构函数设为virtual型,则所有CObject类的派生类的析构函数都将自动变成virtual型,这保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄露。这才是MFC将CObject类的析构函数设为virtual的真正原因。
 
2. 析构函数可以为virtual型,构造函数则不能,那为什么构造函数不能为虚的呢?
答:虚函数采用一种虚调用的办法。虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚的。
 
3. 如果虚函数是非常有效的,我们是否可以把每个函数都声明为虚函数?
答:不行,这是因为虚函数是有代价的,由于每个虚函数的对象都必须维护一个v表,因此使用虚函数的时候都会产生一个系统开销。如果仅仅是一个很小的类,且不想派生其他类,那么根本没有必要使用虚函数。
 
4. 析构函数可以是内联函数吗?
答:析构函数可以是内联函数。

  请注意,这一节内容是c++的重点,要特别注意! 

  我们先说一下什么是构造函数。 

  上一个教程我们简单说了关于类的一些基本内容,对于类对象成员的初始化我们始终是建立成员函数然后手工调用该函数对成员进行赋值的,那么在c++中对于类来说有没有更方便的方式能够在对象创建的时候就自动初始化成员变量呢,这一点对操作保护成员是至关重要的,答案是肯定的。关于c++类成员的初始化,有专门的构造函数来进行自动操作而无需要手工调用,在正式讲解之前先看看c++对构造函数的一个基本定义。   1.C++规定,每个类必须有默认的构造函数,没有构造函数就不能创建对象。 

  2.若没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数,它仅仅负责创建对象而不做任何赋值操作。 

  3.只要类中提供了任意一个构造函数,那么c++就不在自动提供默认构造函数。 

  4.类对象的定义和变量的定义类似,使用默认构造函数创建对象的时候,如果创建的是静态或者是全局对象,则对象的位模式全部为0,否则将会是随即的。 

  我们来看下面的代码:

#include <iostream>  
using namespace std;    
class Student  
{  
    public:  
    Student()//无参数构造函数  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
  
};  
  
void Student::show()  
{  
    cout<<number<<endl<<score<<endl;  
}  
  
void main()  
{  
    Student a;  
    a.show();  
    cin.get();  
}

  在类中的定义的和类名相同,并且没有任何返回类型的Student()就是构造函数,这是一个无参数的构造函数,他在对象创建的时候自动调用,如果去掉Student()函数体内的代码那么它和c++的默认提供的构造函数等价的。

  构造函数可以带任意多个的形式参数,这一点和普通函数的特性是一样的!



  下面我们来看一个带参数的构造函数是如何进行对象的始化操作的。 

  代码如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher(char *input_name)//有参数的构造函数  
    {  
        name=new char[10];  
        //name=input_name;//这样赋值是错误的  
        strcpy(name,input_name);  
    }  
    void show();  
  
    protected:  
    char *name;  
  
};  
  
void Teacher::show()  
{  
    cout<<name<<endl;  
}  
  
void main()  
{  
         //Teacher a;//这里是错误的,因为没有无参数的构造函数  
    Teacher a("test");  
    a.show();  
    cin.get();  
}

  我们创建了一个带有字符指针的带有形参的Teacher(char *input_name)的构造函数,调用它创建对象的使用类名加对象名称加扩号和扩号内参数的方式调用,这和调用函数有点类似,但意义也有所不同,因为构造函数是为创建对象而设立的,这里的意义不单纯是调用函数,而是创建一个类对象。 

  一旦类中有了一个带参数的构造函数而又没无参数构造函数的时候系统将无法创建不带参数的对象,所以上面的代码

Teacher a;

  就是错误的!!! 

  这里还有一处也要注意

//name=input_name;//这样赋值是错误的

  因为name指是指向内存堆区的,如果使用name=input_name;会造成指针指向改变不是指向堆区而是指向栈区,导致在后面调用析构函数delete释放堆空间出错!(析构函数的内容我们后面将要介绍) 

  如果需要调用能够执行就需要再添加一个没有参数的构造函数 

  对上面的代码改造如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher(char *input_name)  
    {  
        name=new char[10];  
        //name=input_name;//这样赋值是错误的  
        strcpy(name,input_name);  
    }  
    Teacher()//无参数构造函数,进行函数重载  
    {  
      
    }  
    void show();  
  
    protected:  
    char *name;  
  
};  
  
void Teacher::show()  
{  
    cout<<name<<endl;  
}  
  
void main()  
{  
    Teacher test;  
    Teacher a("test");  
    a.show();  
    cin.get();  
}

  创建一个无阐述的同名的Teacher()无参数函数,一重载方式区分调用,由于构造函数和普通函数一样具有重载特性所以编写程序的人可以给一个类添加任意多个构造函数,来使用不同的参数来进行初始话对象。

  现在我们来说一下,一个类对象是另外一类的数据成员的情况,如果有点觉得饶人那么可以简单理解成:类成员的定义可以相互嵌套定义,一个类的成员可以用另一个类进行定义声明。

  c++规定如果一个类对象是另外一类的数据成员,那么在创建对象的时候系统将自动调用那个类的构造函数。 

  下面我们看一个例子。 

  代码如下:

#include <iostream>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher()  
    {  
        director = new char[10];  
        strcpy(director,"王大力");  
    }  
    char *show();  
    protected:  
    char *director;  
};  
char *Teacher::show()  
{  
    return director;  
}  
class Student  
{  
    public:  
    Student()  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
    Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的  
  
};  
  
void Student::show()  
{  
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;  
}  
  
void main()  
{  
    Student a;  
    a.show();  
    Student b[5];  
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)  
    {  
        b[i].show();  
    }  
    cin.get();  
}

  上面代码中的Student类成员中teacher成员是的定义是用类Teacher进行定义创建的,那么系统碰到创建代码的时候就会自动调用Teacher类中的Teacher()构造函数对对象进行初始化工作! 

  这个例子说明类的分工很明确,只有碰到自己的对象的创建的时候才自己调用自己的构造函数。

  一个类可能需要在构造函数内动态分配资源,那么这些动态开辟的资源就需要在对象不复存在之前被销毁掉,那么c++类的析构函数就提供了这个方便。 

  析构函数的定义:析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用。 

  析构函数与构造函数最主要大不同就是在于调用期不同,构造函数可以有参数可以重载! 

  我们前面例子中的Teacher类中就使用new操作符进行了动态堆内存的开辟,由于上面的代码缺少析构函数,所以在程序结束后,动态开辟的内存空间并没有随着程序的结束而小时,如果没有析构函数在程序结束的时候逐一清除被占用的动态堆空间那么就会造成内存泄露,使系统内存不断减少系统效率将大大降低!

 

  那么我们将如何编写类的析构函数呢? 

  析构函数可以的特性是在程序结束的时候逐一调用,那么正好与构造函数的情况是相反,属于互逆特性,所以定义析构函数因使用"~"符号(逻辑非运算符),表示它为腻构造函数,加上类名称来定义。 

  看如下代码:

#include <iostream>  
#include <string>  
using namespace std;    
class Teacher  
{  
    public:  
    Teacher()  
    {  
        director = new char[10];  
        strcpy(director,"王大力");  
        //director = new string;  
        // *director="王大力";//string情况赋值  
    }  
    ~Teacher()  
    {  
        cout<<"释放堆区director内存空间1次";  
        delete[] director;  
        cin.get();  
    }  
    char *show();  
    protected:  
    char *director;  
    //string *director;  
};  
char *Teacher::show()  
{  
    return director;  
}  
class Student  
{  
    public:  
    Student()  
    {  
        number = 1;  
        score = 100;  
    }  
    void show();  
  
    protected:  
    int number;  
    int score;  
    Teacher teacher;  
  
};  
  
void Student::show()  
{  
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;  
}  
void main()  
{  
    Student a;  
    a.show();  
    Student b[5];  
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)  
    {  
        b[i].show();  
    }  
    cin.get();  
}

  上面的代码中我们为Teacher类添加了一个名为~Teacher()的析构函数用于清空堆内存。 

  建议大家编译运行代码观察调用情况,程序将在结束前也就是对象生命周期结束的时候自动调用~Teacher() 

  ~Teache()中的delete[] director;就是清除堆内存的代码,这与我们前面一开始提到的。 

name=input_name;//这样赋值是错误的 

  有直接的关系,因为delete操作符只能清空堆空间而不能清楚桟空间,如果强行清除栈空间内存的话将导致程序崩溃!

  前面我们已经简单的说了类的构造函数和析构函数,我们知道一个类的成员可以是另外一个类的对象,构造函数允许带参数,那么我们可能会想到上面的程序我们可以在类中把Student类中的teacher成员用带参数的形式调用Student类的构造函数,不必要再在Teacher类中进行操作,由于这一点构想我们把程序修改成如下形式:

#include <iostream>    
#include <string>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);  
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();    
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student()    
    {    
        number = 1;    
        score = 100;    
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher("王大力");//错误,一个类的成员如果是另外一个类的对象的话,不能在类中使用带参数的构造函数进行初始化  
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;    
}    
void main()    
{    
    Student a;    
    a.show();    
    Student b[5];    
    for(int i=0; i<sizeof(b)/sizeof(Student); i++)    
    {    
        b[i].show();    
    }    
    cin.get();    
}

  可是很遗憾,程序不能够被编译成功,为什么呢? 

  因为:类是一个抽象的概念,并不是一个实体,并不能包含属性值(这里来说也就是构造函数的参数了),只有对象才占有一定的内存空间,含有明确的属性值! 

  这一个问题是类成员初始化比较尴尬的一个问题,是不是就没有办法解决了呢?呵呵。。。。。。 

  c++为了解决此问题,有一个很独特的方法,下一小节我们将介绍。

  对于上面的那个"尴尬"问题,我们可以在构造函数头的后面加上:号并指定调用哪那个类成员的构造函数来解决! 

  教程写到这里的时候对比了很多书籍,发现几乎所有的书都把这一章节叫做构造类成员,笔者在此觉得有所不妥,因为从读音上容易混淆概念,所以把这一小节的名称改为构造类的成员比较合适!



  代码如下:

#include <iostream>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);    
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();    
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student(char *temp):teacher(temp)  
    {    
        number = 1;    
        score = 100;    
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher;    
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl;    
}    
void main()    
{    
    Student a("王大力");    
    a.show();    
    //Student b[5]("王大力");  //这里这么用是不对的,数组不能够使用带参数的构造函数,以后我们将详细介绍vector类型  
    // for(int i=0; i<sizeof(b)/sizeof(Student); i++)    
    //{    
    //    b[i].show();    
    //}    
    cin.get();    
}

  大家可以发现最明显的改变在这里 

Student(char *temp):teacher(temp) 

  冒号后的teacher就是告诉调用Student类的构造函数的时候把参数传递给成员teacher的Teacher类的构造函数,这样一来我们就成功的在类体外对teacher成员进行了初始化,既方便也高效,这种冒号后指定调用某成员构造函数的方式,可以同时制定多个成员,这一特性使用逗号方式,例如: 

Student(char *temp):teacher(temp),abc(temp),def(temp) 

  由冒号后可指定调用哪那个类成员的构造函数的特性,使得我们可以给类的常量和引用成员进行初始化成为可能。 

  我们修改上面的程序,得到如下代码:

#include <iostream>    
#include <string>    
using namespace std;      
class Teacher    
{    
    public:    
    Teacher(char *temp)    
    {    
        director = new char[10];    
        strcpy(director,temp);    
    }  
    ~Teacher()    
    {    
        cout<<"释放堆区director内存空间1次";    
        delete[] director;    
        cin.get();  
    }    
    char *show();    
    protected:    
    char *director;    
};    
char *Teacher::show()    
{    
    return director;    
}    
class Student    
{    
    public:    
    Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10)  
    {    
        number = 1;    
        score = 100;  
    }    
    void show();    
    
    protected:    
    int number;    
    int score;    
    Teacher teacher;  
    int &pk;  
    const int ps;  
    
};    
    
void Student::show()    
{    
    cout<<teacher.show()<<endl<<number<<endl<<score<<endl<<pk<<endl<<ps<<endl;    
}    
void main()    
{    
    char *t_name="王大力";  
    int b=99;  
    Student a(t_name,b);  
    a.show();  
    cin.get();  
}

  改变之处最重要的在这里Student(char *temp,int &pk):teacher(temp),pk(pk),ps(10) 

  调用的时候我们使用 

Student a(t_name,b); 

  我们将b的地址传递给了int &pk这个引用,使得Student类的引用成员pk和常量成员ps进行了成功的初始化。



  但是细心的人会发现,我们在这里使用的初始化方式并不是在构造函数内进行的,而是在外部进行初始化的,的确,在冒号后和在构造函数括号内的效果是一样的,但和teacher(temp)所不同的是,pk(pk)的括号不是调用函数的意思,而是赋值的意思,我想有些读者可能不清楚新标准的c++对变量的初始化是允许使用括号方式的,int a=10和int a(10)的等价的,但冒号后是不允许使用=方式只允许()括号方式,所以这里只能使用pk(pk)而不能是pk=pk了。

  这一小节的内容是说对象构造的顺序的,对象构造的顺序直接关系程序的运行结果,有时候我们写的程序不错,但运行出来的结果却超乎我们的想象,了解c++对对象的构造顺序有助于解决这些问题。 

  c++规定,所有的全局对象和全局变量一样都在主函数main()之前被构造,函数体内的静态对象则只构造一次,也就是说只在首次进入这个函数的时候进行构造! 

  代码如下:

#include <iostream>    
#include <string>    
using namespace std;      
  
class Test  
{  
public:  
    Test(int a)  
    {  
        kk=a;  
        cout<<"构造参数a:"<<a<<endl;  
    }  
public:  
    int kk;  
};  
  
void fun_t(int n)  
{  
    static Test a(n);  
    //static Test a=n;//这么写也是对的  
    cout<<"函数传入参数n:"<<n<<endl;  
    cout<<"对象a的属性kk的值:"<<a.kk<<endl;  
}  
Test m(100);  
void main()  
{  
    fun_t(20);  
    fun_t(30);  
    cin.get();  
}

  下面我们来看一下,类成员的构造顺序的问题。 

  先看下面的代码:

#include <iostream>    
using namespace std;      
  
class Test  
{  
public:  
    Test(int j):pb(j),pa(pb+5)  
    {  
          
    }  
public:  
    int pa;  
    int pb;  
};  
void main()  
{  
    Test a(10);  
    cout<<a.pa<<endl;  
    cout<<a.pb<<endl;  
    cin.get();  
}

  上面的程序在代码上是没有任何问题的,但运行结果可能并不如人意。 

  pa并没有得到我们所希望的15而是一个随机的任意地址的值。 

  这又是为什么呢? 

  类成员的构造是按照在类中定义的顺序进行的,而不是按照构造函数说明后的冒号顺序进行构造的,这一点需要记住!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值