现在的位置: 首页 > 综合 > 正文

学习心得——构造函数

2018年05月05日 ⁄ 综合 ⁄ 共 5773字 ⁄ 字号 评论关闭

        类中一个很重要的概念就是构造函数(constructor)。

一、定义

        (1)构造函数,顾名思义,就是起到构造作用的函数。当我们创建类的一个对象时,会自动调用构造函数。构造函数是对象调用的第一个函数,它不需要用户显式调用,也不能由用户显式调用,只会在对象创建时执行。

        (2)构造函数的作用:构造函数用来确保每个对象的所有数据成员都有一个明显的初始值(sensible initial value)。

        (3)不存在常构造函数。构造函数用于初始化对象,不管对象是否为常对象(const object)。

        (4)构造函数定义方式:  类名(构造函数参数)[:初始化列表]{}         []表示这部分可省略。

             构造函数没有返回值,函数名和类名一样,可以不接收参数,也可以接收单个或多个参数。

        (5)用户可以显式的定义构造函数,但不可显式调用构造函数。如果用户没有显式的定义构造函数,那么如有必要,编译器会自动生成一个构造函数,这个构造函数是默认构造函数。

        上面这些在任何一本C++书中都会介绍,没什么可讲的。但是为了本文的完整性,还是把它写了出来。

 

二、构造函数初始化(The Constructor Initializer)

        构造函数是一个函数,由函数名、参数表和函数体构成。与其它函数不同的是,构造函数可以包含构造函数初始化列表(consturctor initializer list)。

        构造函数初始化列表跟在函数参数表后面,以一个冒号开始,冒号后是类的一些数据成员,每个数据成员后有一个括号,括号内为其初始值,数据成员间以逗号隔开,如下所示:

//类名为Test,我们可以在类中这样定义
Test(int x, int y):fir(x),sec(y),thr("hello")
{
}

         参数化列表的作用在于初始化对象的数据成员。

         很多时候初始化列表被省略,我们在类中使用赋值语句("=")来为对象的数据成员赋值。很多人误以为构造函数初始化的过程在函数体内完成,其实不然。构造函数的初始化过程是在构造函数的初始化列表中完成的,函数体内不是初始化过程。

         我们可以认为构造函数执行的过程是这样的:

         (1)根据创建对象的语句,将对应的实参传递给构造函数的形参。

         (2)执行构造函数初始化列表。使用初始化列表中的初始值来初始化其对应的数据成员。

              如果数据成员没有出现在初始化列表中,或者构造函数没有定义初始化列表,那么根据对象的数据成员的类型,采取对应的初始化方法:类型为类的数据成员,调用类的构造函数初始化它;类型为string的数据成员(其实这个也可视为类),调用string的构造函数初始化;内置类型(built-in type)或者混合类型(compound)的数据成员,如果它在的对象是局部的,那么不初始化它,如果是全局的,初始化为0。(这个初始化规则和定义变量后初始化变量的规则一样)

         (3)执行构造函数体。通常是对对象的一些数据成员赋值。

         以下程序可以验证这一点:

class Test
{
public:
    Test()
    {
          cout<<fir<<" "<<sec<<" "<<"thr:"<<thr<<endl;
          fir=sec=10;
          thr="hello";
           cout<<fir<<" "<<sec<<" "<<"thr:"<<thr<<endl;
    }
private:
    int fir;
    int sec;
    string thr;
};

int main()
{
    Test app;
    return 0;
}

        我们定义了一个无参构造函数,它没有初始化列表。首先它会输出对象的成员变量,然后我们执行赋值语句,再输入这些成员变量。在主程序中我们定义了类的一个对象,当编译器执行到这一句时,会调用构造函数。该程序输出两行,第一行fir和sec的值不确定,因编译器的不同而不同,thr的值为空,第二行的值为10 10 thr:hello。

        这个程序说明在构造函数中的赋值语句执行之前,对象的成员变量已被初始化了。fir和sec是内置整型变量,仅分配了内存(至于是不是在此时分配内存,根据语法,猜测是这样,但是无法肯定),未进行初始化。thr是string类型,调用string的默认构造函数,初始化为空。    
        根据上述说明,不难理解下面这句话:

        有些成员必须使用构造函数初始化列表初始化。例如没有默认构造函数的类类型的成员必须使用构造函数初始化列表初始化,由const修饰的常成员,引用成员,不管它们是什么类型,也必须使用构造函数初始化列表初始化。

 

三、对象成员的初始化顺序

        这是一个小知识点。对象成员的初始化顺序与其类中定义的顺序是保持一致的,通常在构造函数初始化列表中,我们无需关心这个顺序。但是,如果我们使用对象的某个成员来作为另一成员的初始值,就要注意初始化列表中成员的顺序。

        比如我们这些写初始化列表:Test(int x,int y):sec(x),fir(sec),thr("hello"){},那么就会出现错误。因为虽然sec写在了fir前面,但是执行时仍然先初始化fir,再初始化sec,这样用未初始化的sec去初始化fir,是不正确的。

        当我们写构造函数初始化列表时,最好按顺序书写。最好尽可能的避免使用某些成员变量去初始化其它的成员变量。

 

四、构造函数的分类

        构造函数的参数个数不定,这使得构造函数可以重载。我们可以在一个类中定义多个构造函数。有几种构造函数比较特别,它们有专门的名字,下面一一道来。

        (1)带默认参数的构造函数(constructor with default argument)

             在构造函数的参数表中指出某些参数的值(即默认值、缺省值default value)。构造函数的参数通常是用于在构造函数体中对对象的数据成员进行赋值。(请注意区分:构造函数的参数表是指构造函数名后的括号中的参数,构造函数的初始化列表指的是构造函数参数表后面冒号后的那些参数)。有时候我们有必要提供一个默认值。

Test(int x,int y=10)
{ 
       fir=x; 
       sec=y; 
       thr="hello"; 
} //我们这样定义构造函数       

Test app(100,100);//对象app中fir=100,sec=100
Test app2(100);//对象app2中fir=100,sec=10

        对象app2虽然只有一个参数,但是第二个参数缺省为10,所以仍然有结果。

        (2)默认构造函数(default constrcutor)

        默认构造函数就是默认情况下使用的构造函数。什么叫默认情况呢?所谓默认情况,就是我们在定义一个对象时,没有为对象提供初始化值的情况,这时,就会调用默认的构造函数。例如我们这样定义Test类的对象app:Test app; 我们没有为对象提供初始值,这个时候调用的构造函数就是默认构造函数。

        默认构造函数有两种,这两种可以说是两个极端情况。

        一是构造函数参数表为空的构造函数。

        例如这样的构造函数Test(){.....},构造函数参数表为空,构造函数的函数体可以不空。

        这种没有参数的构造函数可以被用户显式的定义,也可以由编译器隐式的生成,编译器隐式生成的默认构造函数的函数体是空的。

        需要注意到一点,如非必要,编译器不会隐式的生成默认构造函数。当我们在类中提供了构造函数,那么编译器不会生成默认构造函数,即使我们没有提供构造函数,在一些特殊的情况下编译器也不会生成默认构造函数。(这些特殊情况出现的概率较小,即使出现了,我们也不关心它是否有构造函数)

        有时候我们需要默认构造函数。因为没有这种构造函数会给我们带来极大的不便。例如我们定义了多个构造函数,那么编译器不会为我们生成默认构造函数。这时这个类就是没有默认构造函数的类。如果我们想像Test app那样定义对象,就会编译错误。我们想让这种类的一个对象成为另外一个类的数据成员,就必须在那个类的构造函数初始化列表对此类对象初始化。还有很多情况就不一一列举了。所以,当类中已定义了构造函数,我们最好也定义一个默认构造函数,即使这个函数什么也不做

        二是构造函数参数表中参数值全为默认值的构造函数。

        这种情况不难理解,当我们为参数表中所有的参数都提供了缺省值,那么我们可以不必为对象提供实参值,对象会自动将其参数设为缺省值(这里要注意是构造函数的所有参数都是缺省值,而不是对象的所有数据成员,它们是两个无关的概念)。例如构造函数Test(int x=100,int y=100){...},我们可以使用Test app;定义对象app,也可以使用Test app(100);定义,还可以使用Test app(100,100);定义,这三种定义方式都定义了一个对象app,它的两个参数均为100。 

        这种构造函数并不是很好用,因为它带来了一些限制。当我们将构造函数参数表中的所有参数值全部设为默认后,我们无法再定义类似于Test(int x){}或者Test(int x,int y){}这样的构造函数,因为编译时无法识别到底该调用哪一个构造函数。

       

          (3) 复制构造函数(copy constructor)

        复制构造函数是指只有一个参数,且参数为类对象的引用的构造函数。形为Test(Test& ap){}。

        复制构造函数主要作用是复制对象。所谓复制对象就是将对象的所有(非静态)数据成员的值去初始化另外一个对象。

        在一个类里面,总是会有复制构造函数,如果我们不显式定义,那么编译器就会自动生成。请注意到这点,它和默认构造函数不同,当我们在类中定义了构造函数,没有定义默认构造函数时,编译器是不会为我们生成的,而没有定义复制构造函数时,编译器会为我们生成。

        编译器自动生成的复制构造函数仅仅是用对象的非静态数据成员的值去初始化新创建的对象,使新创建的对象成为当前对象的副本。对于对象中的内置类型成员变量,直接复制,对于对象去中类类型的成员变量,调用它所属类的复制构造函数。

        我们可以这样定义对象:假设我们定义了默认构造函数,则可使用Test app定义对象app。我们可以使用Test app2(app);来定义对象app2,它是对象app的一个副本。当然,我们也可以使用Test app2=app;来定义app2,编译器会自动重载赋值运算符,如果我们没有显式的重载它。重载赋值运算符和复制构造函数的目的都是为了完成对象的复制。

        类的复制构造函数十分有用,所以类中总是有复制构造函数(我们不讨论(2)中提到的特殊情况)。当我们将类的一个对象作为实参传递,那么就会复制这个对象,此时调用的就是复制构造函数。当我们定义一个函数,返回类型为类类型,那么最终返回的是这个类的一个对象,也需要调用复制构造函数。

        有些类的对象需要防止被复制,例如iostream类的对象。我们通过显式的声明复制构造函数为private来实现。我们在类的private部分声明一个复制构造函数,而不定义它。这样任何试图复制对象的行为都将会失败

        如果显式定义了复制构造函数,我们一定要显式定义默认构造函数,这样可以避免一些不必要的问题出现。

 

五、构造函数的另一个作用——隐式的类类型转换

        构造函数除了可以初始化类的对象外,它还有一个很重要的作用,就是将一种类型转换成类类型。转换过程通常是隐式的调用对应的构造函数完成的。

        例如:我们在Test类中定义了一个函数,它接受Test类的对象的引用为参数,比如bool compare(Test& ap);倘若我们定义了四(1)中程序的构造函数,我们可以这样调用函数,设app为Test类的一个对象,则可以这样写:int a=20; bool c=app.compare(a);这个过程中,整型变量a作为参数传递给函数compare,虽然compare函数需要接受类对象,但是由于存在构造函数,编译器会隐式的调用构造函数,将实参a构造成一个临时对象,进而参与到函数中,当函数执行完成后,该临时对象被删除。

        当我们在构造函数声明时,在构造函数名前加上关键字explicit,这个关键字使得无法隐式的调用构造函数来完成类型转换

        当构造函数声明为explicit后,我们可以显式的进行类型转换。如app.compare(Test(a));,有没有感到很熟悉?这个过程十分类似于强制类型转换,只不过是类类型的强制类型转换。

        通常,单个参数的构造函数会加上explicit关键字,这样可以避免很多错误,防止隐式的类型转换。当需要进行类型转换时,显式的使用类型转换会让代码更加清晰明了。

 

 

总结:类的构造函数十分重要,涉及到的内容也十分的多。

      1、构造函数的初始化列表非常有用,也很重要。这个特性经常被很多人忽略。

      2、类中如果显式定义了构造函数,编译器是不会为我们提供默认构造函数的。所以,当显式定义了一个构造函数后,最好也定义一个默认构造函数,这个默认构造函数最好是无参构造函数。

      3、即使我们显式定义了构造函数,只要没有定义复制构造函数,编译器总会为我们生成一个。通常我们不用自己显式定义,最好由系统自己生成。除非我们不想让类的对象被复制。

      4、隐式的类型转换很难被发现,但是如果我们一直显式的使用强制类型转换,那么可以避免一些不必要的麻烦。

      5、据说,一个类是可以没有构造函数的。这些特殊情况比较复杂,涉及的非常深。

 

The End

抱歉!评论已关闭.