《C++Primer》复制控制 重载操作符与转换

● 复制构造函数

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

1.根据另一个同类型的对象显式或隐式初始化一个对象。

2.复制一个对象,将它作为实参传给一个函数。

3.从函数返回时复制一个对象。

4.初始化顺序容器中的元素。

5.根据元素初始化式列表初始化数组元素。

 

直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。

 

当形参为非引用类型的时候,将复制实参的值。类似的,以非引用类型做返回值,将返回return语句中的值的副本。

 

如果我们没有定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数。合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。

 

为了防止复制,类必须显式声明其复制构造函数为private。

 

一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。

 

合成赋值操作符:

Sales_item&  Sale_item::operator=(const Sales_item &rhs)

{

     isbn = rhs.isbn;

     uints_sold = rhs.uints_sold;

     revence = rhs.revence;

     return *this;

}

 

● 析构函数

当对象的引用或指针超出作用域时,不会运行析构函数。只有当删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。

如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。

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

析构函数与复制构造函数或复制操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。

 

● 智能指针

智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

 

使用计数类:

定义一个单独的具体类用以封装使用计数和相关指针。

实现引用计数有两种经典策略,在这里将使用其中一种,这里所用的方法中,需要定义一个单独的具体类用以封装引用计数和相关指针:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义仅由HasPtr类使用的U_Ptr类,用于封装使用计数和相关指针
// 这个类的所有成员都是private,我们不希望普通用户使用U_Ptr类,所以它没有任何public成员
// 将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员
class U_Ptr
{
    friend class HasPtr;
    int *ip;
    size_t use;
    U_Ptr(int *p) : ip(p) , use(1)
    {
        cout << "U_ptr constructor called !" << endl;
    }
    ~U_Ptr()
    {
        delete ip;
        cout << "U_ptr distructor called !" << endl;
    }
};

 

HasPtr类需要一个析构函数来删除指针。但是,析构函数不能无条件的删除指针。”
   条件就是引用计数。如果该对象被两个指针所指,那么删除其中一个指针,并不会调用该指针的析构函数,因为此时还有另外一个指针指向该对象。看来,智能指针主要是预防不当的析构行为,防止出现悬垂指针。

指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针。

 

image

 

     如上图所示,HasPtr就是智能指针,U_Ptr为计数器;里面有个变量use和指针ip,use记录了*ip对象被多少个HasPtr对象所指。假设现在又两个HasPtr对象p1、p2指向了U_Ptr,那么现在我delete  p1,use变量将自减1,  U_Ptr不会析构,那么U_Ptr指向的对象也不会析构,那么p2仍然指向了原来的对象,而不会变成一个悬空指针。当delete p2的时候,use变量将自减1,为0。此时,U_Ptr对象进行析构,那么U_Ptr指向的对象也进行析构,保证不会出现内存泄露。

 

  HasPtr 智能指针的声明如下,保存一个指向U_Ptr对象的指针,U_Ptr对象指向实际的int基础对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include<iostream>
using namespace std;

// 定义仅由HasPtr类使用的U_Ptr类,用于封装使用计数和相关指针
// 这个类的所有成员都是private,我们不希望普通用户使用U_Ptr类,所以它没有任何public成员
// 将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员
class U_Ptr
{
    friend class HasPtr;
    int *ip;
    size_t use;
    U_Ptr(int *p) : ip(p) , use(1)
    {
        cout << "U_ptr constructor called !" << endl;
    }
    ~U_Ptr()
    {
        delete ip;
        cout << "U_ptr distructor called !" << endl;
    }
};

class HasPtr
{
public:
    // 构造函数:p是指向已经动态创建的int对象指针
    HasPtr(int *p, int i) : ptr(new U_Ptr(p)) , val(i)
    {
        cout << "HasPtr constructor called ! " << "use = " << ptr->use << endl;
    }

    // 复制构造函数:复制成员并将使用计数加1
    HasPtr(const HasPtr& orig) : ptr(orig.ptr) , val(orig.val)
    {
        ++ptr->use;
        cout << "HasPtr copy constructor called ! " << "use = " << ptr->use << endl;
    }

    // 赋值操作符
    HasPtr& operator=(const HasPtr&);

    // 析构函数:如果计数为0,则删除U_Ptr对象
    ~HasPtr()
    {
        cout << "HasPtr distructor called ! " << "use = " << ptr->use << endl;
        if (--ptr->use == 0)
            delete ptr;
    }

    // 获取数据成员
    int *get_ptr() const
    {
        return ptr->ip;
    }
    int get_int() const
    {
        return val;
    }

    // 修改数据成员
    void set_ptr(int *p) const
    {
        ptr->ip = p;
    }
    void set_int(int i)
    {
        val = i;
    }

    // 返回或修改基础int对象
    int get_ptr_val() const
    {
        return *ptr->ip;
    }
    void set_ptr_val(int i)
    {
        *ptr->ip = i;
    }
private:
    U_Ptr *ptr;   //指向使用计数类U_Ptr
    int val;
};
HasPtr& HasPtr::operator = (const HasPtr &rhs)  //注意,这里赋值操作符在减少做操作数的使用计数之前使rhs的使用技术加1,从而防止自我赋值
{
    // 增加右操作数中的使用计数
    ++rhs.ptr->use;
    // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
    if (--ptr->use == 0)
        delete ptr;
    ptr = rhs.ptr;   // 复制U_Ptr指针
    val = rhs.val;   // 复制int成员
    return *this;
}

int main(void)
{
    int *pi = new int(42);
    HasPtr *hpa = new HasPtr(pi, 100);    // 构造函数
    HasPtr *hpb = new HasPtr(*hpa);     // 拷贝构造函数
    HasPtr *hpc = new HasPtr(*hpb);     // 拷贝构造函数
    HasPtr hpd = *hpa;     // 拷贝构造函数

    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
    hpc->set_ptr_val(10000);
    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
    hpd.set_ptr_val(10);
    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;
    delete hpa;
    delete hpb;
    delete hpc;
    cout << hpd.get_ptr_val() << endl;
    return 0;
}

 

 

● 操作符重载

一、什么是操作符重载?
一看到重载,很容易就让人联想到成员函数重载,函数重载可以使名称相同的函数具有不同的实际功能,只要赋给这些同名函数不同的参数就可以了,操作符重载也是基于这一机制的。系统为我们提供了许多操作符,比如“+”,“[ ]”等,这些操作符都有一些默认的功能,而操作符重载机制允许我们给这些操作符赋予不同的功能,并能够按照普通操作符的使用格式来使用自己定义功能的操作符(即重载的操作符)。
定义之后,我们就可以按照平常使用操作符的格式来使用我们自己的重载操作符了。
操作符重载一般在类内部定义,就像成员函数一样定义,这叫做类成员重载操作符。当然也可以在类外定义,即非类成员操作符重载。

重载一元操作符如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。

重载二元操作符定义为成员时有一个形参,定义为非成员函数时有两个形参。

二、如何声明操作符重载?
同普通函数类似,只不过它的名字包括关键字operator,以及紧随其后的一个预定义操作符。例如:
String& operator+=(const String&);
String& operator+=(const char*);

三、怎样使用操作符重载?
两种操作符重载:类成员操作符重载和非类成员操作符重载。
1、类成员操作符重载
已知类String中声明了两个“==”操作符重载,分别是:
bool operator==(const char*) const;
bool operator==(const String&) const;
其中第一个重载的操作符允许我们比较一个String类对象是否等于一个C风格字符串,第二个允许我们比较两个String类对象是否相等。

示例代码:
#include<String.h>
int main()
{
       String flower;
       If(flower==”lily”) //正确:调用bool operator==(const char*) const;
       ……
       else
              if(“tulip”==flower) //错误
              …….
}
关键看一下,为什么第二个重载操作符的使用是错误的?
因为:只有在左操作数是该类类型的对象时,才会考虑使用作为类成员的重载操作符。
因为这里的”tulip”不是String类型对象,所以编译器试图找到一个内置操作符,它可以有一个C风格字符串的左操作数,然而事实上并不存在这样的操作符,所以编译时产生错误。
疑问:我们可以使用String类的构造函数将一个C风格字符串,转换成一个String对象,为什么编译器不能做以上转换呢?即
      if(String(“tulip”)==flower);//这样就是正确的
答:为了效率和正确性。

 

2、非类成员操作符重载
为了解决上面的问题,我们可以考虑使用非类成员操作符代替类成员操作符,这样做的好处是左操作数不必非要是某个类的类型对象了,对于需要两个操作数的操作符重载,我们就可以定义两个参数了。比如:
bool operator==(const String&,const String&);
bool operator==(const String&,const char*);
可以看到,这两个全局重载操作符比成员操作符多了一个参数。
这样定义之后,还是上面的代码,当调用flower==”lily”时,会调用上面的bool operator==(const String&,const char*);。
然而“tulip”==flower会调用哪个操作符重载呢,我们并没有定义bool operator==(const char*,const String&);,我们是不是必须定义这样一个全局操作符重载呢?答案是否定的,因为当一个重载操作符是一个名字空间函数时,对于操作符的第一个和第二个参数,即等于操作符的左右两个操作数都会考虑转换,就像
int vi=1; double vd=2.0; vi=vi+vd; 会先将vd转换成int型,再做加法一样
这意味着,编译器将解释第二个用法如下:
bool operator==(String(“tulip”),flower)。这样会增加系统转换开销。
因此,如果需要频繁比较C风格字符串和String对象,那么最好定义上面的操作符重载,如果不频繁,我们只需定义下面一个就够了:
bool operator==(const String&,const String&);

 

3.什么时候定义类成员操作符重载,什么时候定义非类成员操作符重载?

(1)如果一个重载操作符是类成员,那么只有当跟它一起使用的左操作数是该类对象时,它才会被调用,如果该操作符的左操作数必须是其他类型,那么重载操作符必须是非类成员操作符重载。
(2)C++要求,赋值(=),下标([ ]),调用(())和成员访问箭头(->)操作符必须被指定为类成员操作符,否则错误。

 

四、友元(friend)

考虑到类成员操作符重载可以访问类中的私有变量,但是非类成员重载操作符却不能很方便的访问类的私有成员,为了方便起见,我们可以通过使用友元(friend)的方式,方便的访问类的私有成员。

class String
{
       friend bool operator==(const String&,const String&);
       friend bool operator==(const String&,const char*);
       public:
              //……..
       private:
              //………
}

注意:friend声明紧跟在类名之后,而不是放在public、private或protected中,因为友元不是授权类的成员,并且该关键字只能出现在类中。

经过上述声明之后,我们的非类成员重载操作符就可以直接方位String类的私有成员了。当然我们也可以不使用友元,而使通过该类的共有成员函数来间接访问该类的私有成员也是可以的,内联函数inline就不错,效率也不低。

 

由此看来,声明友元(friend)主要是为了方便高效的访问类的私有成员变量。
分析:什么时候应该使用友元:
(1)       某个类不提供公有的访问私有成员的函数。
(2)       使用共有成员函数访问私有成员变量效率比较差时。

 

● 输入输出操作符

一、输出操作符<<的重载

为了与IO标准库一致,操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回对ostream形参的引用。

ostream& operator<<(ostream& os, const ClassType &object)

{

        os<< //…

        return os;

}

IO操作符必须为非成员函数,否则左操作数只能是该类类型的对象。

 

二、输入操作符>>的重载

输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入对象的非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。

istream& operator>>(istream& in, ClassType &object)

{

        in>>//…

        return in;

}

输入操作符必须处理错误和文件结束的可能性。

 

三、前缀操作符和后缀操作符

前缀式操作符的声明:

class CheckedPtr{

public:

       CheckedPtr&  operator++();

       CheckedPtr&  operator–();

};

后缀式操作符的声明:

class CheckedPtr{

public:

       CheckedPtr  operator++(int);

       CheckedPtr  operator–(int);

};

 

CheckedPtr  CheckedPtr::operator++(int)

{

         CheckedPtr    ret(*this);

        ++*this;

        return ret;

}

 

三、转换操作符

转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字operator之后跟着转换的目标类型:

class SmallInt{

public:

     SmallInt(int i = 0):val(i)

    {

           if(i<0 || i > 255) throw std::out_of_range(“bad smallint initializer”);

    }

    operator int() const {return val;}

private:

    std::size_t val;

}

 

operator type();

虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。

 

BY:AloneMonkey

本文链接:http://www.alonemonkey.com/cplus-review-eight.html