目录
前言
学习面向对象程序设计,当构造大量的类时,为了使运算的方便,C++允许定义多个同名函数来处理类似却不同的形式的操作。
一、运算符重的概念与意义
1.运算符重载
(1)函数重载
- C++中允许定义多个同名函数来实现类似操作
- 重载函数的参数一般不同(参数个数、类型或顺序不同)
- 构造函数重载
(2)运算符重载
- +、-、*、/运算符的重载
- cout、cin运算符的重载
2.运算符重载的意义
(1)例子引入
考虑下面关于复数类 complex 的定义:
class Complex {
private:
double real; double image; // 实部,虚部
public:
Complex();
Complex(double, double);
virtual ~Complex();
Complex add(const Complex &) const; //复数加
Complex sub(const Complex &) const; //复数减
Complex mul(const Complex &) const; //复数乘
Complex div(const Complex &) const; //复数除
void input(); //复数输入
void print(); //复数输出
};
实现和调用 :
Complex Complex::add (const Complex & c) const {
Complex d(this->real + c.real, this->image+c.image);
return d;
};
int main() {
Complex c1(3,5);
Complex c2(2,6);
Complex c = c1.add(c2);
return 0;
};
观察下面的数学运算表达式
- 自然数加法:3+5
- 有理数加法:3.5+5/2
- 实数加法:e + 2.7
- 复数加法:(2+4i) + (5+6i)
从数学表达式到程序表示
- 运算符“+”可应用于 double 类型及其子类型。
- 能否将“+、-、*、/”等运算符用于自定义的数据类型(如 Complex)中?
(2)意义
使用运算符重载
class Complex {
double real; double image; // 实部,虚部
public:
Complex();
Complex(double, double);
~Complex();
Complex operator+(const Complex &) const; //复数加
Complex operator-(const Complex &) const; //复数减
Complex operator*(const Complex &) const; //复数乘
Complex operator/(const Complex &) const; //复数除,等会不写了
friend ostream& operator<< (ostream&, Complex &);
friend istream& operator>> (istream&, Complex &);
};
双目运算符重载
Complex Complex::operator+ (const Complex & c) {
Complex d(this->real + c.real, this->image+c.image);
return d;
};
Complex Complex::operator- (const Complex & c) {
Complex d(this->real - c.real, this->image-c.image);
return d;
};
Complex Complex::operator* (const Complex & c) {
Complex d(this->real * c.real - this->image*c.image,
this->real * c.image + this->image * c.real);
return d;
};
流运算符重载
ostream& operator<< (ostream& os, Complex & c) {
os << c.real<<“+i*”<<c.image;
return os;
}
istream& operator>> (istream& is, Complex & c) {
is >> c.real>>c.image;
return is;
}
调用程序
int main()
{
Complex x, y, z;
cin >> y >> z;
x = y + z;
cout << x << endl;
cout << y – z << endl;
return 0;
}
意义
- 通过运算符能实现的功能通过函数一样能够实现,运算符重载不会在程序功能上带来好处
- 使用运算符能使程序变得更加清晰、简单、符合人们的习惯
- 运算符重载使C++具有更好的可扩充性,这也是C++最具吸引力的特点之一
(3)运算符重载的限制
C++ 中可被重载的运算符
- 算术运算:+、-、*、/、%、++、–
- 位运算:^、&、| 、~、>>、<<
- 逻辑/关系运算:!、&&、||、<、>、>=、<=、==、!=
- 赋值运算:=、+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^=
- 其他:->、,、[]、()、new、delete
不允许被重载的运算符
- . 、::、?:、sizeof
对运算符重载的限制
- 运算符重载不能改变运算符的优先级
- 运算符重载不能改变运算符的结合性
- 运算符重载不能使用默认参数
- 运算符重载不能改变操作数的个数
- 不能建立新的运算符
- 运算符重载只能作用于自定义数据类型
- 重载运算符 () 、[]、->、=时,必须使用成员函数进行重载
- 所有运算符必须显式重载
二、运算符成员函数/非成员函数
1.两种运算符重载方式
- 通过成员函数进行重载
- 通过非成员(友元)函数进行重载
2.二者差别
- 通过成员函数重载的运算符,在调用时,其第一操作数必须是该类的一个对象
- 使用非成员函数重载的运算符无此限制,只需要调用时的参数类型与定义时依次相同即可
3. 如何选择?
- 一般情况下,尽量使用成员函数进行重载
- 某些特殊情况只能使用非成员函数,比如流操作和需要保留某些运算符的可操作性
4.单双目运算符重载
单目运算符重载(以“ !”为例)
-
使用成员函数/方法进行重载:
-
使用非成员函数进行重载:
双目运算符重载(以“ + ”为例)
-
使用成员函数进行重载:
-
使用非成员函数进行重载:
三、单目运算符重载
1.单目运算符重载特点
- 采用成员方法/函数重载单目运算符时,参数数目为 0
- 重载单目运算符时,其唯一的操作数只能是(自定义)对象或者其引用
2.示例
定义字符串类型 String,并以成员函数方式重载运算符 !,当字符串 s 长度为 0 时 !s 返回值为 true,否则 !s 返回值为 false。
class String {
public:
String (const char* m = NULL);
~String();
bool operator! ();
private:
char* str;
};
String::String(const char* m) {
if (m==NULL)
str=NULL;
else {
str = new char[strlen(m)+1];
strcpy(str,m);
}
}
String::~String() {
if (str!=NULL) delete [] str;
}
bool String::operator! () {
if (str==NULL || strlen(str)==0)
return true;
else
return false;
}
int main (int argc, char* argv[]) {
String s1, s2(“Something”);
if (!s1) cout<<“s1 is empty!”<<endl;
if (!s2) cout<<“s2 is empty!”<<endl;
//这里,!s1 和 !s2 本质上是 s1.operator!() 和 s2.operator!() 的简写。
return 0;
}
还可以以友元函数的方式进行重载
class String {
friend bool operator! (const String &);
public:
String (const char* m = NULL);
~String();
private:
char* str;
};
bool operator! (const String & str) {
if (str.str==NULL || strlen(str.str)==0)
return true;
else return false;
}
3.重载单目运算符时需要注意的地方
自增运算符(++)和自减运算符(–)有两种使用方式,即前置方式与后置方式。C++ 规定,重载后置方式的自增/自减运算时,需要增加一个额外 int 型参数
四、重载流运算符
1.流运算符回顾
- 流插入运算符 “<<”和流提取运算符“>>”,其第一操作数分别是一个 ostream 对象(典型的如 std::cout)和一个 istream 对象(如 std::cin)
- cin 和 cout 对象支持任意基本数据类型的输入/输出操作。这是因为每个基本类型均在 istream / ostream 中有相应的重载(overload)
若希望自定义类型支持标准输入/输出,应如何实现?如:
Complex c(3,5); // c 是一个复数对象
cout << c << endl; // 通过标准输出打印 c
2.重载流运算符
- 这时,应该重载 << 运算符;显然不应该修改系统标准库中 ostream 的实现
- 如果通过 Complex 类的成员函数重载 << 运算符,那么在使用该运算符时,必须是某个Complex 类的对象作为第一操作数,这显然不行
- 因此,必须用非成员函数对其进行重载
ostream& operator<< (ostream& os, Complex & c) {
os << c.real<<“+i*”<<c.image;
return os;
};
istream& operator>> (istream& is, Complex & c) {
is >> c.real>>c.image;
return is;
};
五、双目运算符重载
1.双目运算符重载
-
回顾
:前面讨论了两个特殊的双目运算符——流插入和流提取运算符的重载。 -
分析
:重载双目运算时需要考虑两个因素:一是被重载的运算符对左右操作数是否有要求;二是是否要保留运算符的可交换性。 -
总结
:双目运算被重载为带有一个参数的成员函数或者两个参数的非成员函数,且重载为非成员函数时必须有一个参数为类对象或者类对象的引用。
2.示例,重载 Complex 类的 +、-、*、/ 运算
class Complex {
double real; double image; // 实部,虚部
public:
Complex(); Complex(double, double);
virtual ~Complex();
double getReal(); double getImage();
void setReal(double); void setImage(double);
Complex operator+(const Complex &); //复数加
Complex operator-(const Complex &); //复数减
Complex operator*(const Complex &); //复数乘
Complex operator/(const Complex &); //复数除
friend ostream& operator<< (ostream&, Complex &);
friend istream& operator>> (istream&, Complex &);
};
Complex Complex::operator+ (const Complex & c) {
Complex d(this->real + c.real, this->image+c.image);
return d;
};
Complex Complex::operator- (const Complex & c) {
Complex d(this->real - c.real, this->image-c.image);
return d;
};
Complex Complex::operator* (const Complex & c) {
Complex d(this->real * c.real - this->image*c.image,
this->real * c.image + this->image * c.real);
return d;
};
3.补充
练习
- 重载 List 类的 [] 运算符,用以实现下标功能
-
思考,函数的原型应该是什么?
问题
:
何时应该返回引用?何时返回值?
——当整个表达式“具有左值属性时”,换句话说,能够被赋值时,应该返回引用。
Node& List::operator[] (int index)
{
for(Node* pnt=head; pnt != NULL && index>0; )
{
pnt = pnt->next;
index--;
} // 定位到链表的第 index 个节点
if (pnt == NULL)
throw 0; // 抛出异常
return (*pnt); // 获得 pnt 节点处的 value 值
}
六、赋值运算符重载
1.赋值运算符的缺省语义
事实上,赋值运算符不需显式重载便可直接使用在自定义类对象之间;其缺省操作是逐个复制所有的成员(浅拷贝)。比如
的执行结果是使 c1.real 和 c1.image 的值分别赋为 3 和5。但是,当对象成员中含有指针字段时,这样的操作会带来一定的副作用。
2.缺省复制操作的副作用(举例说明)
Stack s1(15);
Stack s2;
s2=s1;
上方程序会造成什么副作用?
该赋值发生后:
- s1.data 和 s2.data 将指向相同的内存空间
- s2 原来指向的内存空间丢失
-
对 s1的操作也会影响到 s2,反之亦然
因此,在这种情况下,应该重载赋值运算符:
Stack& Stack::operator= (const Stack & st){
if (&st!=this){ // important!!!
delete[] data;
data = new int [st.size];
top = st.top;
size=st.size;
for(int i=0; i<top; i++) // 数据复制
data[i] = st.data[i];
}
return * this;
}
总结
重载函数/运算符的操作实则有很多细节和限制,本篇博客总结了课堂的PPT以及补充内容,其中的真正知识运用还得实践操作才能融会贯通!