您的位置:首页 > 编程语言 > C语言/C++

C++运算符重载

2015-10-03 16:43 218 查看
重载操作符就是让操作符作用域非内置类型时也有自己独特的意义。

对于内置类型,当操作符作用于它们时,编译器会规定操作的意义:两个int型数据相加的结果与数学运算的加法相同。但是对于非内置类型,比如类或者枚举类型,编译器并没有规定操作符作用于它们的意义。有些时候,这样做是合理的,比如对于两个Student类对象(其中的数据成员有姓名、学号),对它们进行加法操作的确没有什么意义;但是有的时候,我们却希望能够像操作普通变量一样用运算符操作它们,比如我们希望可以直接通过cout<<stduent1来实现输出姓名和学号。虽然我们可以通过设计同样功能的函数来实现这样的功能,但是这样远没有cout<<stduent1来的简单方便,尤其是对类的使用者而言,良好地重载操作符可以是他们不用记住许多接口函数。

在前面的章节中,我们接触了赋值操作符,它其实就是重载了“=”操作符,使得它具有让做操作数的数据成员有右操作数的数据成员相等的功能。先看看我们整个篇幅都要使用的类:

[cpp] view
plaincopy

class Student

{

private:

int schoolNumber;

string name;

string address;

static int cnt;

public:

//默认构造函数

Student():schoolNumber(cnt++),name("NoName"),address("xust"){}

//构造函数

Student(string nm,string add):schoolNumber(cnt++),name(nm),address(add){ cout<<"Student构造"<<endl;}

就是为学生定义了学号,姓名,地址,然后定义了一个类静态成员cnt来控制新创建的学生的学号是依次递增的。

那么重载输出操作符的方法如下:

[cpp] view
plaincopy

//重载输出操作符

ostream& operator<<(ostream &os,const Student &sd)

{

os<<sd.schoolNumber<<"\t"<<sd.name<<"\t"<<sd.address<<"\t";

return os;

}

可以看出,重载操作符其实也是一个函数,其中返回值、形参与一般的函数相同,唯一要注意的是函数名,使用关键字operator加上你要重载的符号来组成函数名。当你定义一个Student 的对象std1时,就可以直接使用cout<<sdt1了。

我们不难发现,其实通过定义函数的方法也能完全解决这个问题:

[cpp] view
plaincopy

void Student::display()

{

cout<<schoolNumber<<"\t"<<name<<"\t"<<address<<"\t"<<endl;;

}

使用调用对象的output函数就可以了。

言归正传,并不是所有操作符都能够重载的。它们分别是:作用域操纵符“::”,成员指针解引操作符“.*”,成员选择操作符“.”,以及条件表达式“?:”。我们也没有必要大量的设计重载操作符函数,除非这个重载后的操作符能很好符合人思维的预期:比如对两笔交易相加的结果,应该是总的交易数量相加,总的交易钱数也相加,而平均交易价格并不是相加,而是总钱数除以总数量的结果。

在重载操作符时,还有一些小的细节需要注意:

1.我们只能“重载”操作符,而不是“创建”操作符,C++规定的操作符无非是加减乘除等等那么60来个,有不少是通过几个操作符组合起来的,你不能标新立异的自己“组合”一些操作符来达到特定的操作,比如试图定义operator**来定义平方操作等等。

2.操作符的操作数至少有一个类或者枚举类型,对于内置类型,操作符的意义是不允许改变的。

3.操作符重载后,尽管它们有了自己的意义,但是它们的优先级不会发生变化:x == y + z不论如何重载,都是先算y+z,在算==。

4.重载的操作符不能保证操作数的求值顺序。举个例子,对于“&&”、“||”等操作符,计算时是“短路”的:如果第一个操作数为假,那么“&&”就不会计算第二个操作数;如果第一个操作数为“真”那么“||”不会计算第二个操作数。

5.重载操作符不能改变操作符的操作数。但这里有一点需要特别注意,如果重载操作定义为成员函数,那么他的形参比操作数数目少一,因为this指针已经指向了调用这个操作的对象了;如果重载操作定义为非成员函数(一般设为友元函数),那么操作数与形参个数相等。

其实,重载的目的就是为了使用起来“顺手”,有一种可以“望文生义”的感觉,如果重载的意义不是很明显,或者这个操作符很少使用,把它定义成一个普通的函数也许更好。这一点编译器做的很好,当你自己不“犯贱”的重载时,编译器会自己定义一些操作符:赋值操作符(对类的数据成员逐个赋值),取地址操作符(返回对象的地址),逗号操作符(从左往右计算,返回最右边的值)。

当然,对于有些类在使用时,你不得不重载一些操作符:比如,对于关联容器,必须支持“<”操作,当关联容器里装的是类时,这个类就必须重载“<”。而且对于一般的类,也应该支持“==”操作,因为很多算法库的很多函数都支持==操作。

还有一个问题,就是应该把类设计成成员函数还是友元函数呢,有一些好的指导原则可以供我们做参考:

1.赋值“=”、下标“[]”、调用“()”成员访问箭头“->”必须定义为成员函数,如果定义为非成员会导致编译错误。

2.复合赋值操作符也应定义为成员函数。但是如果不这样做,不会导致编译错误。

3.改变对象状态或与给定类型紧密联系的操作符,比如自增“++”、自减“--”、解引“*”,也应该定义为成员函数。

4.对称的操作符,比如算数操作符,关系操作符等,最好定义为非成员函数。

下面介绍一些常见的操作符的重载。

首先是输入输出操作符。

先看前面提过的输出操作符:

[cpp] view
plaincopy

//重载输出操作符

ostream& operator<<(ostream &os,const Student &sd)

{

os<<sd.schoolNumber<<"\t"<<sd.name<<"\t"<<sd.address<<"\t";

return os;

}

由于是第一个操作符,所以值得细说一下。形参中因为ostream不支持复制,所以必须使用引用;因为输出会改变流的状态,所以不能设为const;输出操作一般不会改变Sales_item对象的内容,所以设为const;设为引用是为了省去实参到形参的复制工作,因为当类很大时,复制也是一笔不小的开销。函数的是调用操作符的ostream对象的引用,这使得我们可以cout<<std1<<sdt2这样连续使用输出操作符。

有一点需要注意,就是对于输出操作,尽量不要使用格式化操作,比如左对齐,换行等等;而应该把这个自由度留给用户,让用户以自己习惯的方式输出。

那么这个操作应该放在内成员还是友元中呢?答案是:必须放在友元中。因为如果放在类成员中,那么第一个操作数必须是类,所以我们就得这样使用<<操作符:sdt1<<cout。这显然不是我们所希望的。

再看输入操作符的重载:

[cpp] view
plaincopy

//重载输入操作符

istream& operator>>(istream& is,Student &sd)

{

is>>sd.schoolNumber>>sd.name>>sd.address;

if(!is)

sd = Student();

return is;

}

二者在函数的声明上差别不大,需要说明的是因为输入操作会改变类的内容,所以没有使用const修饰。二者的主要区别在于输入操作需要判断输入的内容是否合法!这一点是至关重要,要是对于非法的输入,也会得到一个结果,那么这个操作的鲁棒性就太差了。在这里,我们要求依次输入输入学号、姓名、地址,如果输入有误,那么从重新新建一个空的Student对象,并返回流。对于更好的设计,最好能够指明哪里出了错误,正确的输入格式应该是什么等等,但是这里就统统略去了。

下面看看重载关系运算符:

[cpp] view
plaincopy

//相等操作:判断二人学号是否相等

bool operator==(const Student& std1,const Student& std2)

{

return std1.schoolNumber == std2.schoolNumber&&

std1.name == std2.name&&

std1.address == std2.address;

}

//利用相等判断不等

bool operator!=(const Student& std1,const Student& std2)

{

return std1 == std2;

}

只有当两个学生的所有信息都相等时,才判为相等。当定义了相等以后,就可以利用相等定义不等操作了,这就省去了很多事。

[cpp] view
plaincopy

//重载小于操作符:判断学号大小

bool operator<(const Student& std1,const Student& std2)

{

return std1.schoolNumber < std2.schoolNumber;

}

//重载大于操作符:利用小于判断大于

bool operator>(const Student& std1,const Student& std2)

{

return std2<std1;

}

对于关联容器的对象,需要支持<操作,所以我们这里也定义了<操作:通过学号判断大小。一般情况下,如果定义了小于,也要顺便定义大于,这样会是得类的使用者用起来比较顺手。

接下来定义+和+=。

[cpp] view
plaincopy

//重载+=操作符

Student& Student::operator+=(const Student& rhs)

{

schoolNumber += rhs.schoolNumber;

return *this;

}

//利用+=操作符重载+

//加法操作返回的是右值,而不是引用

Student operator+(const Student& std1,const Student& std2)

{

//新建一个Student对象,并用左操作数初始化

Student ret(std1);

ret += std2;

return ret;

}

其中+声明为友元,而+=定义为成员函数。可能很多人都不理解,为什么要先定义+=操作,然后用它定义+操作?首先,肯定是可以那样定义的,但是如果那样定义的话,在+=操作中,就会有类似:std1 = std1 +std2这样的代码,而我们知道,当编译器执行这个代码时,会先创建一个临时的Student对象来存储std1 +std2的结果,然后再把结构赋值给std1,然后再撤销这个临时的对象,效率会比较低。

还有一点需要注意,一般+操作返回的都是一个右值,而不是一个引用;而+=操作返回都是一个引用,这样就不需要创建和撤销临时的副本对象了。

接下来是重载下标操作符:

由于我们的Student的数据成员中并没有使用下标的元素,所以我们定义一个新的类Team:

[cpp] view
plaincopy

class Team

{

public:

//构造函数接受一个参数来创建一个多人的team

Team(size_t nm)

{

for(size_t i = 0;i<nm;++i)

{

Student std;

team.push_back(std);

}

}

//重载下标操作符声明

Student& operator[](const size_t);

private:

vector<Student> team;

};

Team的数据成员是一个vector<Student>,并且接受一个实参来控制每个Team中有多少个Student。我们对重载下标操作符,希望的是它能够返回一个team中的某一个Student成员,想清楚了这个,重载其实并不困难:

[cpp] view
plaincopy

//重载下标操作符

Student& Team::operator[](const size_t index)

{

//return team[index];

return team.at(index);

}

在主函数中,可以使用

[cpp] view
plaincopy

Team tm(3);

tm[1].display();

来显示一个学生的信息。

下面看一个比较难的例子,重载解引操作和箭头操作。

首先我们的类成员中并没有指针,我们本打算定义一个新的类,这个类中的数据成员就是指向Student类对象的指针。但是根据前面提过的“智能指针”以及引用计数原理,我们的程序变成了这样:

[cpp] view
plaincopy

//智能指针

class StdPtr

{

friend class StudentPtr;

//实际的指针

Student *sp;

//引用计数

size_t use;

//构造函数

StdPtr(Student *p):sp(p),use(1){cout<<"StdPtr构造"<<endl;}

//析构函数

~StdPtr(){cout<<"StdPtr析构"<<endl;delete sp;}

};

class StudentPtr

{

public:

//复制操作符

StudentPtr(const StudentPtr& orig):ptr(orig.ptr){++ptr->use;}

//赋值操作符

StudentPtr& operator=(const StudentPtr&);

//构造函数

StudentPtr(Student *p):ptr(new StdPtr(p)){cout<<"StudentPtr构造"<<endl;}

//析构函数

~StudentPtr()

{

cout<<"StudentPtr析构"<<endl;

if(--ptr->use == 0)

delete ptr;

}

//重载解引操作符:

Student &operator*(){return *ptr->sp;}

//重载箭头操作符:

Student *operator->(){return ptr->sp;}

//与之对应的const版本

const Student &operator*()const{return *ptr->sp;}

const Student *operator->()const{return ptr->sp;}

private:

StdPtr *ptr;

};

我们在StudentPtr中重载了解引和箭头操作。因为StudentPtr本意为指向Student的指针,所以对它解引应该返回一个Student对象的引用,对它的箭头操作应该返回Student的成员。想清楚了这个道理,重载程序实际上就简单了。还有一点要注意,解引操作时一员操作,这里定义为成员函数,所以没有形参;而箭头操作看起来像是二元操作符:接受一个对象和一个成员名,而实际上,箭头操作的右操作数并不是一个表达式,而是类成员的标示符,编译器自动帮你处理了将一个标示符传递给函数以获取类成员的工作。

通常,对于指针和箭头都应该定义两个版本,一个是const,另一个是非const,const版本返回const引用以防止用户改变基础对象,这与STL标准库的思想很像,既有普通迭代器,也有const迭代器。

最后我们看看如何重载自增或者自减操作符。

自增自减操作符常用作迭代器或者的类型,按理说,我们应该定义一个类,它是指向任何一种对象的指针,但是这样需要使用时模版。

这里考虑一种简单的情况:这个类能够处理int型的数组:

[cpp] view
plaincopy

//该类是一个指向数组的指针

class CheckedPtr

{

public:

//构造函数:必须绑定一个数组对象

CheckedPtr(int *b,int *e):beg(b),end(e),curr(b){}

//重构前自增操作符

CheckedPtr& operator++();

//重构前自减运算符

CheckedPtr& operator--();

//重载后自增运算符:通过调用参数与前自增运算符区别开来

CheckedPtr& operator++(int);

//重载下标操作符

int& operator[](const size_t);

const int& operator[](const size_t)const;

//重载解引操作符

int& operator*();

const int& operator*() const;

//重载==操作符

friend bool inline operator==(const CheckedPtr&,const CheckedPtr&);

//重载!=操作符

friend bool inline operator!=(const CheckedPtr&,const CheckedPtr&);

//重载+操作

friend CheckedPtr operator+(const CheckedPtr&,const size_t);

//重载-操作

friend CheckedPtr operator-(const CheckedPtr&,const size_t);

private:

int *beg;

int *end;

int *curr;

};

数据成员只有3个,分别指向数组的第一个数,最后一个数,以及当前指向数组的哪一个数。初始化时,将当前指向初始化为数组的第一个数。这里并没有默认构造函数,因为我们希望CheckedPtr类的对象在建立时,就与某一个数组绑定。

自增操作和自减操作意味着移动指向数组中某一元素的指针curr,想明白了这个道理,其实自增或者自减操作的重载并不困难:

[cpp] view
plaincopy

CheckedPtr& CheckedPtr::operator++()

{

if(curr == end)

throw out_of_range("increment past the end of CheckedPtr");

++curr;

return *this;

}

CheckedPtr& CheckedPtr::operator--()

{

if(curr == beg)

throw out_of_range("decrement past the beginning of CheckedPtr");

--curr;

return *this;

}

首先看返回值:定义要与内置类型的操作一致:返回被自增/自减量的引用。其次,要检查指针是否越界。

通过程序,我们能看出来,这里定义的是“前自增”:因为程序中使用的是++curr;那么如何定义后自增呢?问题麻烦再后自增的操作符也是“++”,操作数也是一个,类型也相同。这里,通过接收一个额外的int型参数来区别后自增:

[cpp] view
plaincopy

CheckedPtr& CheckedPtr::operator++(int)

{

//保存当前值

CheckedPtr ret(*this);

//调用前自增来实现后自增

++*this;

//返回当前值

return *this;

}

在使用时,通过ptr.operator++(0)来调用后自增。对于一般的类,如果定义了前自增,也应该定义后自增。这里虽然定义的不好看,但是还是勉强能用。

总而言之,重载操作符的关键是确定你到底想得到一个什么样的效果,这与其说是一门技术,不如说是一门艺术:好的重载能让类的使用者感到很方便,而差劲的重载会让类的使用者摸不着头脑,举一个例子:

还是上面那个类,我们也定义了解引操作符:

[cpp] view
plaincopy

int& CheckedPtr::operator*()

{

if(curr == end)

throw out_of_range("invalid curremt point");

return *curr;

}

在主函数中:

[cpp] view
plaincopy

#define RANGE 10

int main()

{

int arr[RANGE] = {0,1,2,3,4,5,6,7,8,9};

CheckedPtr ptr(arr,arr+10);

//后自增运算返回的是当前对象,但是对象的curr指针已经指向下一个元素了,所以解引的是下一个元素

cout<<*ptr.operator++(0)<<endl;

return 0;

}

我们希望的效果应该是输出0,然后自增,但是由于后自增运算虽然返回的是当前对象,但是对象的curr指针已经指向下一个元素了,所以解引的是下一个元素,所以会输出1.这个隐蔽的“错误”很难发现。

所以不到万不得以而且确保自己的重载万无一失的话,还是老老实实的调用函数会更好一些。

C++中的操作符重载

一、什么是操作符重载
操作符重载可以分为两部分:“操作符”和“重载”。说到重载想必都不陌生了吧,这是一种编译时多态,重载实际上可以分为函数重载和操作符重载。运算符重载和函数重载的不同之处在于操作符重载重载的一定是操作符。我们不妨先直观的看一下所谓的操作符重载:

1 #include <iostream>
2
3 using namespace std;
4
5 int main()
6 {
7     int a = 2 , b = 3;
8     float c = 2.1f , d = 1.2f;
9     cout<<"a + b = "<<a+b<<endl;
10     cout<<"c + d = "<<c+d<<endl;
11     return 0;
12 }


我们看到操作符“+”完成floatint两种类型的加法计算,这就是操作符重载了。这些内置类型的操作符重载已经实现过了,但是如果现在我们自己写过的类也要实现实现类似的加法运算,怎么办呢??比如现在现在有这样一个点类point,要实现两个点的相加,结果是横纵坐标都要相加,这时候就需要我们自己写一个操作符重载函数了。



1 #include <iostream>
2
3 using namespace std;
4
5 class point
6 {
7     double x;
8     double y;
9 public:
10     double get_x()
11     {
12         return x;
13     }
14     double get_y()
15     {
16         return y;
17     }
18     point(double X = 0.0 , double Y = 0.0):x(X),y(Y){};
19     point operator +(point p);
20 };
21 //重载操作符“+”
22 point point::operator +(point p)
23 {
24     double x = this->x + p.x;
25     double y = this->y + p.y;
26     point tmp_p(x,y);
27     return tmp_p;
28 }
29 int main()
30 {
31     point p1(1.2,3.1);
32     point p2(1.1,3.2);
33     point p3 = p1+p2;
34     cout<<p3.get_x()<<" "<<p3.get_y()<<endl;
35     return 0;
36 }


二、实现操作符重载的两种方式
操作符重载的实现方式有两种,即通过“友元函数”或者“类成员函数”。
1.友元函数重载操作符的格式:

1 class 类名
2 {
3     friend 返回类型 operator 操作符(形参表);
4 };
5 //类外定义格式:
6 返回类型 operator操作符(参数表)
7 {
8     //函数体
9 }


2.类成员函数实现操作符重载的格式:

1 class 类名
2 {
3 public:
4     返回类型 operator 操作符(形参表);
5 };
6 //类外定义格式
7 返回类型 类名::operator 操作符(形参表)
8 {
9     //函数体
10 }


这样说吧,还是不足以比较这两种实现方式的区别,我们分别用两种实现方式写point类的”+“和”-“的重载。代码如下:



1 #include <iostream>
2
3 using std::endl;
4 using std::cout;
5
6 class point
7 {
8     double x;
9     double y;
10 public:
11     double get_x()
12     {
13         return x;
14     }
15     double get_y()
16     {
17         return y;
18     }
19     point(double X = 0.0 , double Y = 0.0):x(X),y(Y){};
20     friend point operator -(point p1,point p2);
21     point operator +(point p);
22 };
23 //重载操作符“-”
24 point operator -(point p1,point p2)
25 {
26     double x = p1.get_x() - p2.get_x();
27     double y = p1.get_y() - p2.get_y();
28     point p3(x,y);
29     return p3;
30 }
31 //重载操作符“+”
32 point point::operator +(point p)
33 {
34     double x = this->x + p.x;
35     double y = this->y + p.y;
36     point tmp_p(x,y);
37     return tmp_p;
38 }
39 int main()
40 {
41     point p1(1.2,3.2);
42     point p2(1.1,3.1);
43     point p3 = p1+p2;
44     point p4 = operator-(p1,p2);
45     cout<<p3.get_x()<<" "<<p3.get_y()<<endl;
46     cout<<p4.get_x()<<" "<<p4.get_y()<<endl;
47     return 0;
48 }


这里不知道大家看到没有,利用友元函数重载二元操作符”-“时,形式参数是两个,而利用类成员函数时,形式参数却只有一个。这时因为类成员函数中存在this指针,这相当于一个参数,所以类成员实现操作符重载需要的形式参数比原来少一个,这比如:利用类成员函数实现一元操作符”-“,就不需要参数了。也正是因为这个原因,友元函数实现的操作符重载是有限制的,比如:[] ,(),->和 =不能利用友元函数实现运算符的重载。
在实际开发过程中,单目运算符建议重载为成员函数,而双目运算符建议重载为友元函数。通常下双目运算符重载为友元函数比重载为成员函数更方便,但是有时双目运算符必须重载为成员函数,例如赋值运算符=。还有如果需要修改对象内部的状态,一般可以选择利用类成员函数进行修改。

三、运算符重载的原则
这样一看,运算符重载还是蛮简单的嘛,实际上运算符重载也是要遵循一些原则的:
1.C++中只能对已有的C++运算符进行重载,不允许用户自己定义新的运算符。
2.C++中绝大部分的运算符可重载,除了成员访问运算符.,作用域运算符::,长度运算符sizeof以及条件运算符?:
3.运算符重载后不能改变运算符的操作对象(操作数)的个数。如:"+"是实现两个操作数的运算符,重载后仍然为双目运算符。
4.重载不能改变运算符原有的优先级和原有的结合性。

6.运算符重载不能全部是C++中预定义的基本数据,这样做的目的是为了防止用户修改用于基本类型数据的运算符性质。

四、为什么要进行运算符重载
关于运算符重载要遵循这么多原则,那么为什么还要进行运算符重载呢?为什么我不是写一个add()函数,代替operator +()呢??个人感觉C++中之所以要支持运算符的重载是为了与内置数据类型统一操作,比如:c = a + b 和 c = add(a,b),这看起来哪个更直观一点呢,显然是前者了。同时,我们希望操作我们自己定义的数据类型能像操作int和double这些内置数据类型一样方便。可能举这个加法的例子有点不好,现在加入重载的运算符是[],<<,^,|等呢?这时我们要用什么成员函数代替呢??代替之后又是一种什么效果呢?会一眼就看出来这个函数要干什么吗??
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: