构造函数 复制构造函数

时间:2022-08-25 00:00:42

本文是学习 C++中的最基本的内容,因此学习 C++就应全部掌握本文的内容.在知道了怎样声明一个类之后,就会遇到关于构造函数的问题,本文是关于构造函数的专题,集中介绍了默认构造函数,初始化列表,重点介绍了复制构造函数, 直接初始化,复制初始化,赋值,临时对象之间的关系.


构造函数和析构函数

一:基础

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);

   hyong::hyong(int i):a(i),b(1)

   {

      cstAqs…tHuu//初始化数组成员在大括号中初始化

int main()

{

    hyong j(2);

    cout<<j.a<<j.b<<"\n"<<j.cs~~BAqs~~"\n";

}

yvwxz{?<GAx|}~~"@{x|<<"\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>ADtAx|}~~"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#tOO"\n"; //输出1,语句m=1会处理成m(1)的形式,即隐式被转换为掉用带有一个参数的构造函数. f(2);

}

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