构造函数、析构函数、复制构造函数、赋值运算操作符

    在定义一个类时,每个类型定义了创建该类型的对象时会发生什么——构造函数定义了该类类型对象的初始化。类型还能控制复制、赋值或撤销该类型的对象时会发生什么——通过特殊的成员函数:复制构造函数赋值操作符析构函数来控制这些行为。

1、构造函数

    构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数,构造函数的工作是保证每个对象的的数据成员具有合适的初值。构造函数的名字与类的名字相同,并且不能指定返回类型。构造函数可以被重载,即可以定义多个同名但是形参表不同的构造函数(注:不能仅仅基于不同的返回类型而实现函数重载),在定义对象时,用于初始化对象的实参类型决定使用哪个构造函数,只要创建给类型的一个对象,编译器就运行一个构造函数。构造函数不能为const,因为const成员函数不能修改调用该函数的对象。构造函数一般为public如果将构造函数定义为private的,则不能定义该类的对象,这样的话,这类就没什么用了。

class Sales_item{
public:
    Sales_item();
    Sales_item(const std::string&);
    Sales_item(std::istream&);
    ...
private:
    std::string isbn;
    unsigned units_sold;
    double revenue;
    ...
};

(1)构造函数初始化式

    和其它函数一样,构造函数具有名字、形参表和函数体。与其它函数不同的是,构造函数也可以包含一个构造函数初始化表,比如:

Sales_item::Sales_item(const string &book):isbn(book),units_sold(0),revenue(0.0){}

   构造函数初始化式只在构造函数的定义中,而不是声明中指定。构造函数初始化式也叫显式初始化。从概念上讲,可以认为构造函数分为两个阶段执行:1)初始化阶段;2)普通的计算阶段。计算阶段由构造函数体中的所有语句组成。不管是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化,初始化阶段发生在计算阶段开始之前。在构造函数初始化表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的初始化默认构造函数来初始化类类型的数据成员。内置或者复合类型的成员的初始值依赖于对象的作用域:局部作用域中这些成员不被初始化,全局作用域中它们被初始化为0。比如: 

Sales_item::Sales_item(const string &book)
{
    isbn = book;
    units_sold = 0;
    revenue = 0.0;
}

  这个构造函数没有显示初始化,但不管是否有显式的初始化式,在执行构造函数之前,要初始化isbn这个成员。这个构造函数隐式使用默认的string构造函数来初始化isbn,在执行构造函数时,isbn已经有值了,该值被构造函数中的赋值所覆盖。但无论如何上述两个构造函数的效果是相同的,不同之处在于:使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数在构造函数体中对数据成员赋值。 

    有时我们必须要使用构造函数初始化列表进行初始化,因为对于这样的成员,咱构造函数体中对他们赋值不起作用:1)没有默认构造函数的类类型的成员;2)const类型的成员;3)引用类型的成员。这三种情形必须在构造函数初始化表中进行初始化。

    注意:构造函数中成员被初始化的次序就是定义成员的次序,而不是初始化表中的顺序!!!!

    注意:构造函数中成员被初始化的次序就是定义成员的次序,而不是初始化表中的顺序!!!!

    注意:构造函数中成员被初始化的次序就是定义成员的次序,而不是初始化表中的顺序!!!!

(2)默认构造函数

    为所有形参提供默认实参的构造函数定义了默认构造函数。只要定义了一个对象时没有提供初始化式,就调用默认构造函数。一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。当一个类没有定义构造函数时,编译器才会生成一个默认构造函数。由编译器创建的构造函数通常称为合成的默认构造函数。它将使用与初始化变量相同的规则来进行初始化。对于具有类类型的成员,则会调用该类型的初始化默认构造函数来初始化类类型的数据成员。内置或者复合类型的成员的初始值依赖于对象的作用域:局部作用域中这些成员不被初始化,全局作用域中它们被初始化为0。合成的构造函数一般适用于仅包含类类型成员的类,而对于含有内置类型或复合类型成员的类,则通常该定义他们自己的默认构造函数初始化这些成员。

(3)友元

    友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字friend开始,它只能出现在类定义的内部,友元声明可以出现在类中的任何地方(private或者public),因为友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。

(4)隐式类类型转换

    结论:可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。

class Sales_item{
public:
    Sales_item(const std::string &book=""):isbn(book),units_sold(0),revenue(0){}
    Sales_item(std::istream &is);
    bool same_isbn(const Sales_item &rhs) const{ return isbn = rhs.isbn;}
private:
    ...

};

    这里的每个构造函数都定义了一个隐式转换,因此在期待一个Sales_item类型对象的地方,可以使用一个string或一个istream:

string null_book = "9-999-99999-9"
item.same_isbn(null_book);           
//Sales_item items = null_book;
//Sales_item items = new Salse_item(null_book);
//Sales_item items(null_book);

     这段程序使用一个string类型对象作为实参传给Sales_item的same_isbn函数。该函数期待一个Sales_item对象作为实参。编译器使用接受一个string的Sales_item构造函数从null_book生成一个新的Sales_item对象,新生成(临时的)的Sales_item对象被传递给same_isbn。这种行为的正确与否取决于业务需要。假如你只是想测试一下item的isbn是否为null_book,这么做也许是方便的。但是假如在same_isbn函数中还涉及到未初始化的其它属性,那么这么做可能就是错误的。所以要限制这种隐式类型转换。可以将构造函数声明为explicit,将构造函数声明为显式的,来防止在需要隐式转换的上下文中使用构造函数:

class Sales_item{
public:
    explicit Sales_item(const std::string &book=""):isbn(book),units_sold(0),revenue(0){}
    explicit Sales_item(std::istream &is);
    bool same_isbn(const Sales_item &rhs) const{ return isbn = rhs.isbn;}
private:
    ...

};

    explicit关键字只能用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复它,现在两个构造函数都不能用于隐式创建对象。 Google约定所有单参数的构造函数都必须是显式的,只有极少数情况下拷贝构造函数可以不声明称explicit。例如作为其他类的透明包装器的类。

(5)static类成员

    通常非static数据成员存在于类类型的每个对象中。不像普通的数据成员,static数据成员独立于该类的任何对象而存在,每个static数据成员是与类关联的对象,并不与该类的对象相关联。类可以定义static数据成员和static成员函数,static成员函数没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。使用static成员而不是全局对象有三个优点:1)static成员的名字是在类的作用域中,因此可以避免与其它类的成员或全局对象名冲突;2)可以是实施封装。static可以是私有的成员,而全局对象不可以;3)通过阅读程序可以看出static成员是与特定类关联的。

  1)static成员函数:

    static成员函数是类的组成部分但不是任何对象的组成部分,因此static成员函数没有this指针。因为static成员不是任何对象的组成部分,所以static成员函数不能声明为const,毕竟声明成员函数为const就是承诺不会修改该函数所属的对象。最后static成员函数不能声明为虚函数。可以通过作用域操作符直接调用static成员,或者通过对象、引用或指向该类类型对象的指针间接调用。static成员函数一般声明为公有的,但是也可以是私有的,此时只允许类的成员函数或者友元调用。

  2)static数据成员:

   static数据成员必须在类定义体的外部定义。static关键字只能用于类定义体内部的声明,定义时不能标为static。不像普通的数据成员,static成员不是通过类构造函数进行初始化,而是应该在类定义时进行初始化。唯一的一个例外是,如果static成员是const类型的,那么就可以咱类的定义体中进行初始化

class Account
{
public:
    static double rate(){ return interestRate ; }
    static void rate(double);
private:
    static const int period = 30;
    double daily_tbl[period];

};

    const static 数据成员在类的定义体中初始化时,该数据成员仍必须在定义体之外进行定义:

const int Account::period;

2、复制构造函数

    只有单个形参,而且该形参是对本类类型的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可以用于:

1)根据另一个同类型的对象显式或隐式初始化一个对象;
2)复制一个对象,将它作为实参传递给一个函数;
3)从函数返回时复制一个对象;
4)初始化顺序容器中的元素;
5)根据元素初始式列表初始化数组元素;

    C++支持两种初始化形式:直接初始化和复制初始化。复制初始化使用=符号,直接初始化将初始化式放在圆括号中。直接初始化调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。

    如果我们不定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数不同,即使我们定义了其它构造函数,也会合成复制构造函数。合成复制构造函数的行为是执行逐个成员初始化,即编译器将现有对象的每个非static成员,一次复制到正创建的对象。每个成员的类型决定了复制该成员的含义,合成复制构造函数直接复制内置类型的成员的值,类类型的成员使用该类的复制构造函数进行复制。 我们也可以自己定义复制构造函数:

class Foo
{
public:
    Foo();                    //默认构造函数
    Foo(const Foo&);          //复制构造函数
}

     注意:复制构造函数的形参一定是类类型的引用,如果不是引用,就会在复制构造函数内调用复制构造函数,就会形成无休止的递归调用,从而导致栈溢出,在GCC或VS中都将导致编译出错。有些类需要完全禁止复制,如果想要禁止复制,似乎可以省略复制构造函数,然而如果不定义复制构造函数,编译器将合成一个。为了防止复制,类必须显示声明其复制构造函数为private。此时类的友元和成员任然可以进行复制,如果想要连友元和成员的复制也禁止,就可以定义一个private复制否早函数到那时不对其定义。

3、赋值运算操作符=

    与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。重载操作符是一些函数,其名字为operator后跟着所定义的操作符的符号。因此,通过定义名为operator=的函数,我们可以对赋值进行定义。操作符函数有一个返回值和一个形参列表。形参表必须与具有该操作符操作数数目相同的形参(如果操作符是一个成员,则包括隐式this形参)。赋值是二元运算,所以操作符函数有两个形参:第一个对应左操作数,第二个对应右操作数。大多数操作符可以定义为成员函数或非成员函数,当操作符函数为成员函数时,它的第一个操作数隐式绑定到this指针。因此,赋值操作接受单个形参,且该形参是同一类类型的对象,右操作数一般为const引用传递。赋值操作符函数返回类型应该与内置类型赋值运算返回的类型相同,即返回右操作数的引用,因此赋值操作符也返回同一类类型的引用。如下:

class Sales_item
{
public:
    Sales_item& operator=(const Sales_item &);
    ...
};

Sale_item& Sales_item::operator=(const Sales_item &rhs)
{
    isbn = rhs.isbn;
    units_sold = rhs.units_sold;
    revenue = rhs.revenue;
    return *this;
}

4、析构函数

(1)何时调用析构函数:

    撤销类对象时会自动调用析构函数:

Sales_item *p = new Sales_item;
{
    Sales_item item(*p);
    delete p;
}

    变量item在超出作用域时自动撤销。因此当遇到右花括号时,运行item的析构函数。动态分类的对象*p只有在指向该对象的指针被删除时才撤销,如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏。 

(2)何时编写析构函数:

    许多具有构造函数的类不一定需要定义自己的析构函数,仅仅在有些工作需要析构函数完成时,才需要析构函数。析构函数通常用于释放在构造函数或在对象生命周期获取的资源。析构函数体自身并不直接销毁成员,是在函数体之后隐含的析构阶段中被销毁的。如果类需要析构函数,那么它也需要复制构造函数和赋值操作符函数。

(3)合成析构函数:

    与复制构造函数或赋值操作符不同,编译器总会为我们合成一个析构函数。合成的析构函数按对象创建时的逆序撤销每个非static成员,因此,它按照成员在类中声明次序的逆序撤销成员。对于类类型的成员,合成析构函数调用该成员的析构函数来撤销对象。

(4)如何编写析构函数:

    析构函数没有返回值,也没有形参,它的名字是在类名前加上~。因为不能指定任何形参,所以不能重载析构函数。析构函数与复制构造函数或赋值操作符函数的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数在我们编写的析构函数运行之后仍然运行。

(5)析构函数可以设为私有的吗?(参考)(参考):

    结论:将析构函数设为私有,类对象就无法建立在栈上了,而是在堆中建立。

    在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
    当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。如下类中将析构函数定义为私有的:

class alloc
{
public:
    alloc():
private:
   ~alloc();
};

    上例中,由于析构函数是私有的,如果在栈上分配空间,类在离开作用域时会调用析构函数释放空间,此时无法调用私有的析构函数。因此只能在堆上分配空间,但是堆上分配的空间只有在delete时才会调用析构函数。 可以添加一个destroy()函数来释放,从而解决不能在析构函数中添加delete的问题。

class alloc
{
public:
    alloc():
   destroy(){ delete this;}  
private:
   ~alloc();
};

注意:对于复制构造函数、赋值运算操作符、析构函数等,当你确定用不着这些函数时,可以把这些函数做private声明而不去实现它,这就防止了会有人去调用它们,也防止了编译器去生成它们。 

参考:《C++ Primer》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值