0%

C++ Operator Overload(赋值运算符重载)

C++ Operator Overload

转换函数 (Opetartor ())

operator用于类型转换函数

类型转换函数的一般形式为 :
operator 类型名() (const)
{实现转换的语句}
在函数名前面不能指定函数类型,函数没有参数.

类型转换函数的特征

  1. 型转换函数定义在源类中;
  2. 须由 operator 修饰,函数名称是目标类型名或目标类名
  3. 函数没有参数,没有返回值,但是有return 语句。
    在return语句中返回目标类型数据或调用目标类的构造函数。
1
#include <iostream>
2
using namespace std;
3
class my_class
4
{
5
public:
6
    operator int()//定义了一个将类转化为int的转换函数
7
    {
8
        cout << "convert_to_int" << endl;
9
        return 1;
10
    }
11
};
12
13
int main()
14
{
15
    my_class a;
16
    int i_a = (int)a;//第一次显式的转换
17
    cout << a << endl;//第二次隐式的转换
18
19
    return 0;
20
}

可以转化为任意其他的类型,只需要改变转换函数即可。
但是在定义转化函数时,要避免出现二义性,比如不要同时提供多个符合条件的转换函数:

1
#include <iostream>
2
using namespace std;
3
4
class my_class
5
{
6
public:
7
    operator double()//定义了一个将类转化为double的转换函数
8
    {
9
        cout << "convert_to_double" << endl;
10
        return 1.1;
11
    }
12
    operator int()//定义了一个将类转化为int的转换函数
13
    {
14
        cout << "convert_to_int" << endl;
15
        return 1;
16
    }
17
};
18
19
int main()
20
{
21
    my_class a;
22
    cout << a << endl;//报错
23
    return 0;
24
}

输出报错信息:

1
有多个运算符 "<<" 与这些操作数匹配

当然,若同时需要这两个转换函数,这个错误也可以通过很多方法来解决,比如显式的指定要输出的是哪种类型,而不是让编译器自己去选择:

1
cout << (double)a << endl;

或者指定其中一个转换函数只能显式的转换,而不能隐式的转换

1
explicit operator double()
2
{
3
    cout << "convert_to_double" << endl;
4
    return 1.1;
5
}

都将得到正确的结果。

C++ 允许程序员重新定义标准运算符在与类对象一起使用时的工作方式。

赋值运算符重载 (Operator =)

sample

1
#include<iostream>
2
#include<cstring>
3
using namespace std;
4
5
class MyStr
6
{
7
private:
8
    char *name;
9
    int id;
10
11
public:
12
    MyStr() {}
13
14
    MyStr(int _id, char *_name)   //constructor
15
    {
16
        cout << "constructor" << endl;
17
        id = _id;
18
        name = new char[strlen(_name) + 1];
19
        strcpy_s(name, strlen(_name) + 1, _name);
20
    }
21
22
    MyStr(const MyStr& str)
23
    {
24
        cout << "copy constructor" << endl;
25
        id = str.id;
26
        if (name != NULL)
27
            delete[] name;
28
        name = new char[strlen(str.name) + 1];
29
        strcpy_s(name, strlen(str.name) + 1, str.name);
30
    }
31
32
    MyStr& operator =(const MyStr& str)//赋值运算符
33
    {
34
        cout << "operator =" << endl;
35
        if (this != &str)
36
        {
37
            if (name != NULL)
38
                delete[] name;
39
            this->id = str.id;
40
            int len = strlen(str.name);
41
            name = new char[len + 1];
42
            strcpy_s(name, strlen(str.name) + 1, str.name);
43
        }
44
        return *this;
45
    }
46
47
    ~MyStr()
48
    {
49
        delete[] name;
50
    }
51
};
52
53
int main()
54
{
55
    MyStr str1(1, "hhxx");
56
    cout << "====================" << endl;
57
    MyStr str2;
58
    str2 = str1;
59
    cout << "====================" << endl;
60
    MyStr str3 = str2;
61
    return 0;
62
}

输出结果:

1
constructor
2
====================
3
operator =
4
======================
5
copy constructor

参数说明

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用,加const 引用是因为

  1. 不希望在这个函数中对用来进行赋值的“原版”做任何修改
  2. 加上const,对于const的和非const的实参,函数就能接受;
    如果不加,就只能接受非const的实参。
  3. 使用引用避免在函数调用时对实参的一次拷贝,提高了效率

上面的规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象

返回值说明

一般地,返回值是被赋值者的引用,即__*this__

  • 这样在函数返回时避免一次拷贝,提高了效率。
  • 更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。

这也不是强制的,我们可以将函数返回值声明为void,然后什么也不返回,只不过这样就不能够连续赋值了。

调用机制

当为一个类对象赋值(注意:可以用本类对象为其赋值(如sample),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例子)时,会由该对象调用该类的赋值运算符重载函数。

如上边代码中

1
str2 = str1;

一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。

需要注意的是

1
MyStr str2;
2
str2 = str1;

1
MyStr str3 = str2;

在调用函数上是有区别的。正如我们在上面结果中看到的那样。

前者MyStr str2是str2的声明加定义,调用无参构造函数,所以str2 = str1是在str2已经存在的情况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;

而后者,是用str2来初始化str3,调用的是拷贝构造函数。

提供默认赋值运算符重载函数的时机

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。

注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说__只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本__。可见,所谓默认,就是“以本类或本类的引用为参数”的意思

1
#include<iostream>
2
#include<string>
3
using namespace std;
4
5
class Data
6
{
7
private:
8
    int data;
9
public:
10
    Data() {};
11
    Data(int _data)
12
        :data(_data)
13
    {
14
        cout << "constructor" << endl;
15
    }
16
    Data& operator=(const int _data)
17
    {
18
        cout << "operator=(int _data)" << endl;
19
        data = _data;
20
        return *this;
21
    }
22
};
23
24
int main()
25
{
26
    Data data1(1);
27
    Data data2,data3;
28
    cout << "=====================" << endl;
29
    data2 = 1;
30
    cout << "=====================" << endl;
31
    data3 = data2;
32
    return 0;
33
}

输出结果:

1
constructor
2
=====================
3
operator=(int _data)
4
=====================

上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1 一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2 一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。

构造函数还是赋值运算符重载函数

如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。

1
class Data
2
{
3
private:
4
    int data;
5
public:
6
    Data() {};
7
    Data(int _data)
8
        :data(_data)
9
    {
10
        cout << "constructor" << endl;
11
    }
12
13
//    Data& operator=(const Data& dd)
14
//    {
15
//        cout << "woshi shei" << endl;
16
//        return *this;
17
//    }
18
//
19
//    Data& operator=(const int _data)
20
//    {
21
//        cout << "operator=(int _data)" << endl;
22
//        data = _data;
23
//        return *this;
24
//    }
25
};
26
27
int main()
28
{
29
    Data data1(1);
30
    Data data2,data3;
31
    cout << "=====================" << endl;
32
    data2 = 1;
33
    cout << "=====================" << endl;
34
    data3 = data2;
35
    return 0;
36
}

输出结果:

1
constructor
2
=====================
3
constructor
4
=====================

可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时

  • 如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。
  • 如果只有匹配的构造函数存在,就会调用这个构造函数。

显式提供赋值运算符重载函数的时机

  • 用非类A类型的值为类A的对象赋值时(当然也可以不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。
  • 当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝,必须显式提供赋值运算符重载函数(如例1)。

浅拷贝和深拷贝

拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。

所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。

假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2 语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图
string memory

这样,会有两个致命的错误

  1. 当我们通过str2修改它的name时,str3的name也会被修改!
  2. 当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!

所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,delete[] name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图
string memory

这样,str2.name和str3.name各自独立,避免了上面两个致命错误。
我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。

赋值运算符重载函数只能是类的非静态的成员函数

C++规定赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数
因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。假设C++允许将赋值运算符重载函数定义为友元函数,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,而当前类并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以编译器会自动提供一个。此时,执行类str2=str1这样的代码,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?

为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。

赋值运算符重载函数不能被继承

1
#include<iostream>
2
#include<string>
3
using namespace std;
4
5
class A
6
{
7
public:
8
    int X;
9
    A() {}
10
    A& operator =(const int x)
11
    {
12
        X = x;
13
        return *this;
14
    }
15
};
16
17
class B :public A
18
{
19
public:
20
    B(void) :A() {}
21
};
22
23
int main()
24
{
25
    A a;
26
    B b;
27
    a = 45;
28
    //b = 67;
29
    (A)b = 67;
30
    return 0;
31
}

注释掉的一句无法编译通过。报错提示:没有与这些操作数匹配的”=”运算符。
对于//b = 67 一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,类似于该句的代码可以调用匹配的构造函数)。

代码不能编译通过,说明父类的operator = 函数并没有被子类继承。

为什么赋值运算符重载函数不能被继承呢?

因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?

所以,C++规定,赋值运算符重载函数不能被继承

上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。

赋值运算符重载函数要避免自赋值

对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象。

  • 为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

  • 如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,会出现不可预期的结果。

所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。

运算符重载

类型转换:operator <类型>()
运算符重载:<类型> operator <运算符>(<参数表>)

重载主要有两种形式,成员函数形式与友元函数形式。

1
#include <iostream>
2
using namespace std;
3
4
class MyClass
5
{
6
public:
7
    MyClass() {}
8
    MyClass(int a, double b):a_(a),b_(b){}
9
    ~MyClass(){}
10
11
    int get_a() { return a_; }
12
    double get_b() { return b_; }
13
14
    MyClass operator+(const MyClass &adder) const//以成员函数方式重载+
15
    {
16
        MyClass sum;
17
        sum.a_ = a_ + adder.a_;
18
        sum.b_ = b_ + adder.b_;
19
        return sum;
20
    }
21
22
    friend MyClass operator-(const MyClass &A,const MyClass &B)//以友元方式重载-
23
    {
24
        MyClass diff;
25
        diff.a_ = A.a_ - B.a_;
26
        diff.b_ = A.b_ - B.b_;
27
        return diff;
28
    }
29
30
private:
31
    int a_;
32
    double b_;
33
};
34
35
int main()
36
{
37
    MyClass A(1, 1.1);
38
    MyClass B(2, 2.2);
39
    MyClass sum = A + B;
40
    MyClass diff = A - B;
41
    cout << sum.get_a() << "\t" << sum.get_b() << endl;
42
    cout << diff.get_a() << "\t" << diff.get_b() << endl;
43
44
    return 0;
45
}

operator 重载 ()

1
#include <iostream>
2
using namespace std;
3
class Time
4
{
5
    int hour;
6
    int minute;
7
    int second;
8
9
public:
10
    Time( int h=0, int m=0, int s=0 )
11
    {
12
        operator()( h, m, s );
13
    }
14
    //版本0,返回时间表示的秒数
15
    int operator()()
16
    {
17
        return hour*3600+minute*60+second;
18
    }
19
    //版本1,设置为整点
20
    void operator()( int h )
21
    {
22
        operator()( h, 0, 0 );
23
    }
24
    //版本2,设置整小时和分钟
25
    void operator()( int h, int m )
26
    {
27
        operator()( h, m, 0 );
28
    }
29
    //版本3,设置时分秒
30
    void operator()( int h, int m, int s )
31
    {
32
        hour = h;
33
        minute = m;
34
        second = s;
35
    }
36
37
    friend ostream& operator<<( ostream& os, const Time& ct )
38
    {
39
        os << ct.hour << ';:';;
40
        if( ct.minute<10 )
41
            os << ';0';;
42
        os << ct.minute << ';:';;
43
        if( ct.second<10 )
44
            os << ';0';;
45
        os << ct.second;
46
        return os;
47
    }
48
};
49
50
int main()
51
{
52
    Time t;
53
    cout << t << endl;
54
    t( 9 );//调用版本1
55
    cout << t << endl;
56
    t( 7, 30 );//调用版本2
57
    cout << t << endl;
58
    t( 0, 10, 20 );//调用版本3
59
    cout << t << endl;
60
    cout << t() << endl;//调用版本0
61
    return 0;
62
}

operator 重载 &

1
T& operator*() const
2
{
3
	if(!_ptr)
4
    {
5
 		throwNullHandleException(__FILE__, __LINE__);
6
	}
7
	return *_ptr;
8
}

operator 重载 ->

1
#include<iostream>
2
using namespace std;
3
4
class A{
5
public:
6
	void action(){
7
		cout << "Action in class A!" << endl;
8
	}
9
};
10
11
class B{
12
	A a;
13
public:
14
	A* operator->(){
15
		return &a;
16
	}
17
	void action(){
18
		cout << "Action in class B!" << endl;
19
	}
20
};
21
22
class C{
23
	B b;
24
public:
25
	B operator->(){
26
		return b;
27
	}
28
	void action(){
29
		cout << "Action in class C!" << endl;
30
	}
31
};
32
33
int main(int argc, char *argv[])
34
{
35
	C* pc = new C;
36
	pc->action();
37
	C c;
38
	c->action();
39
	getchar();
40
	return 0;
41
}

operator 重载 []

1
T& opetator[] (T instance)
2
{
3
	....
4
	return ...
5
}