C++课程学习笔记第四周:运算符的重载
前言:本文主要是根据MOOC网北大课程——《程序设计与算法(三):C++面向对象程序设计》内容整理归纳而来,整理的课程大纲详见 https://www.cnblogs.com/inchbyinch/p/12398921.html
本文介绍了运算符重载的概念和用法,包括string对象和可变长数组对象的设计。
1 运算符重载基本概念和形式
1.1 运算符重载基本概念
- 在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的,因为C++中预定义的运算符并未提供这种功能。
- 为了让对象也能通过运算符进行运算(这样代码更简洁,容易理解),C++提供了运算符重载的机制。
- 运算符重载的含义:对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。比如 5 + 4 = 9; complex_a + complex_b 生成新的复数对象。
- 运算符重载的目的:扩展C++中提供的运算符的适用范围,使之能作用于对象。
1.2 运算符重载的形式
返回值类型 operator 运算符(形参表){
……
}
- 运算符重载的实质是函数重载;
- 把含运算符的表达式转换成对运算符函数的调用,把运算符的操作数转换成运算符函数的参数;
- 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。
- 可以重载为普通函数,也可以重载为成员函数;
- 重载为成员函数时,参数个数为运算符目数减一;重载为普通函数时参数个数为运算符目数。
//示例
class Complex{
public:
double real,imag;
Complex( double r = 0.0, double i= 0.0 ):real(r),imag(i) { }
Complex operator-(const Complex & c);
};
Complex operator+( const Complex & a, const Complex & b){
return Complex( a.real+b.real,a.imag+b.imag); // 返回一个临时对象
}
Complex Complex::operator-(const Complex & c){
return Complex(real - c.real, imag - c.imag); // 返回一个临时对象
}
int main(){
Complex a(4,4),b(1,1),c;
c = a + b; // 等价于c=operator+(a,b);
cout << c.real << "," << c.imag << endl;
cout << (a-b).real << "," << (a-b).imag << endl;
//a-b 等价于a.operator-(b)
return 0;
}
2 常见运算符的重载
2.1 赋值运算符 ‘=’ 的重载
- 有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。
- 赋值运算符“=”只能重载为成员函数。
class String {
private:
char * str;
public:
String():str(new char[1]) { str[0] = 0;}
const char * c_str() { return str; };
String & operator=(const char * s);
~String() { delete [] str; }
};
String & String::operator = (const char * s){
// 重载“=”得 以使得 obj = “hello” 能够成立
delete [] str;
str = new char[strlen(s)+1];
strcpy( str, s);
return * this;
}
int main(){
String s;
s = "Good Luck," ; //等价于 s.operator=("Good Luck,");
cout << s.c_str() << endl;
// String s2 = "hello!"; // 这条语句要是不注释掉就会出错
s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
cout << s.c_str() << endl;
return 0;
}
上述代码实现了两个功能:将 char* 类型赋给string对象;返回值为String&类型,故可以作为左值。但还需要解决以下问题:
- 使用对象赋值 s2 = s1 时,造成浅拷贝。因为复制的是指针,故两者指向同一个地方;若一个对象消亡或指向其他地方,则会调用delete,那么另一个对象的指针将无法访问这块内容。
- 对象自身赋给自身:s2 = s2 ,可能会导致将指向的内容删掉。
- 编译器默认加上的复制构造函数也会形成浅拷贝。
因此需要加上两个成员函数:赋值运算符的重载函数和复制构造函数。
//赋值运算符的重载函数
String & operator = (const String & s){
if( this == & s) return * this; //排除自身赋给自身的情况
delete [] str;
str = new char[strlen(s.str)+1];
strcpy( str,s.str);
return * this;
}
//复制构造函数
String( String & s){
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
另外:
- 一般情况下,将运算符重载为类的成员函数,是较好的选择。
- 但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
2.2 运算符重载实例:可变长整型数组
//要编写可变长整型数组类,使之能如下使用:
int main() {
CArray a; //开始里的数组是空的
for( int i = 0;i < 5;++i)
a.push_back(i);
CArray a2,a3;
a2 = a;
for( int i = 0; i < a.length(); ++i )
cout << a2[i] << " " ;
a2 = a3; //a2是空的
for( int i = 0; i < a2.length(); ++i ) //a2.length()返回0
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for( int i = 0; i < a4.length(); ++i )
cout << a4[i] << " ";
return 0;
}
//程序输出结果为
//0 1 2 3 4
//0 1 2 100 4
下面的代码可以满足当前要求:
#include <iostream>
#include <cstring>
using namespace std;
class CArray{
int* p;
int n;
public:
CArray(int s=0);
CArray(CArray& a);
~CArray(){ if(n>0) delete []p;}
CArray& operator=(const CArray& a);
int& operator[](int i){if(i>=0 && i<n) return p[i];}
void push_back(int k);
int length(){return n;}
};
CArray::CArray(int s):n(s){
if(n == 0) p = NULL;
else p = new int[n];
}
CArray::CArray(CArray& a){
n = a.n;
if(n == 0){
p = NULL;
}else{ //n>0
p = new int[n];
memcpy(p, a.p, sizeof(int)*n);
}
}
CArray& CArray::operator=(const CArray& a){
if(this == &a) return *this;
//若原空间不足,则需要重新开辟空间,否则不需要
if(n < a.n){ //n可能为0
if(n > 0) delete []p;
p = new int[a.n];
}
//复制数据
if(a.n == 0){
p = NULL;
}else{ //a.n>0
memcpy(p, a.p, sizeof(int)*a.n);
}
n = a.n;
return *this;
}
void CArray::push_back(int k){
//if(n > 0) delete []p;
int* temp = new int[n+1];
memcpy(temp, p, sizeof(int)*n);
temp[n] = k;
if(n > 0) delete []p;
p = temp;
n++;
}
int main() {
CArray a; //开始里的数组是空的
for( int i = 0;i < 5;++i)
a.push_back(i);
CArray a2,a3;
a2 = a;
for( int i = 0; i < a.length(); ++i )
cout << a2[i] << " " ;
a2 = a3; //a2是空的
for( int i = 0; i < a2.length(); ++i ) //a2.length()返回0
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for( int i = 0; i < a4.length(); ++i )
cout << a4[i] << " ";
return 0;
}
2.3 流插入运算符和流提取运算符的重载
cout 是ostream 类的对象,是在 iostream 中定义的。
“<<” 能用在cout上是因为,在iostream里对 “<<” 进行了重载。
cout << 5 << “this” 的本质就是cout.operator<<(5).operator<<(“this”);
//重载成成员函数
ostream & ostream::operator<<(int n){
…… // 输出n的代码
return * this;
}
//重载成全局函数
ostream & operator<<( ostream & o, const CStudent & s){
o << s.nAge ;
return o;
}
2.4 类型强制转换运算符的重载
类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型。
#include <iostream>
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r=0,double i=0):real(r),imag(i) { };
operator double () { return real; } //重载强制类型转换运算符 double
};
int main(){
Complex c(1.2,3.4);
cout << (double)c << endl; //输出 1.2
double n = 2 + c; //等价于 double n=2+c.operator double()
cout << n; //输出 3.2
}
2.5 自增自减运算符的重载
自增运算符++、自减运算符--有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,C++规定:
- 前置运算符作为一元运算符重载。比如重载为成员函数:T & operator++();重载成全局函数:T1 & operator++(T2)。
- 后置运算符作为二元运算符重载,多写一个没用的参数:比如重载为成员函数:T operator++(int);重载成全局函数:T1 operator++(T2,int)。
此处需注意:
- 通常前置返回引用,后置返回类。且后置涉及较多的开销,故尽量用前置。
- int 作为一个类型强制转换运算符被重载,故(int) s 等效于 s.int()。
class CDemo {
private :
int n;
public:
CDemo(int i=0):n(i) { }
CDemo & operator++(); // 用于前置形式
CDemo operator++( int ); // 用于后置形式
operator int ( ) { return n; } //强制类型转换运算符的重载
friend CDemo & operator--(CDemo & );
friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++(){ //前置 ++
n ++;
return * this;
} // ++s即为: s.operator++();
CDemo CDemo::operator++( int k ){ //后置 ++
CDemo tmp(*this); // 记录修改前的对象
n ++;
return tmp; // 返回修改前的对象
} // s++即为: s.operator++(0);
CDemo & operator--(CDemo & d){// 前置--
d.n--;
return d;
} //--s即为: operator--(s);
CDemo operator--(CDemo & d,int){// 后置--
CDemo tmp(d);
d.n --;
return tmp;
} //s--即为: operator--(s, 0);
int main(){
CDemo d(5);
cout << (d++ ) << ","; //于 等价于 d.operator++(0);
cout << d << ",";
cout << (++d) << ","; //于 等价于 d.operator++();
cout << d << endl;
cout << (d-- ) << ","; //于 等价于 operator--(d,0);
cout << d << ",";
cout << (--d) << ","; //于 等价于 operator--(d);
cout << d << endl;
return 0;
}
//输出结果:
//5,6,7,7
//7,6,5,5
对于运算符的重载,注意事项:
- C++不允许定义新的运算符;
- 重载后运算符的含义应该符合日常习惯;
- 运算符重载不改变运算符的优先级;
- 以下运算符不能被重载:“.”、“ .* ”、“::”、“?:”、sizeof;
- 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数。
3 练习
需要掌握string类和可变长数组类的设计。(注:更为全面的string类可见第五章练习)
//001:MyString http://cxsjsxmooc.openjudge.cn/2019t3fall4/001/
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
char * p;
public:
MyString(const char * s) {
if(s){
p = new char[strlen(s) + 1];
strcpy(p,s);
}else
p = NULL;
}
~MyString() { if(p) delete [] p; }
//your code start here
MyString(const MyString& str1){
p = new char[strlen(str1.p)+1];
strcpy(p, str1.p);
}
MyString& operator=(MyString& str1){
if(this==&str1) return *this;
if(p) delete[]p;
p = new char[strlen(str1.p)+1]; //即便str1.p为空也可以
strcpy(p, str1.p);
return *this;
}
MyString& operator=(const char* s){
if(p) delete []p;
p = new char[strlen(s)+1];
strcpy(p, s);
return *this;
}
void Copy(const char* s){
if(p) delete []p;
p = new char[strlen(s)+1];
strcpy(p, s);
}
friend ostream& operator<<(ostream& o, MyString& str1){
cout << str1.p;
return o;
}
//your code end here
};
int main(){
char w1[200],w2[100];
cin >> w1 >> w2;
MyString s1(w1),s2 = s1;
MyString s3(NULL);
s3.Copy(w1);
cout << s1 << "," << s2 << "," << s3 << endl;
s2 = w2;
s3 = s2;
s1 = s3;
cout << s1 << "," << s2 << "," << s3 << endl;
}