C++面向对象程序设计(四)
1.运算符重载的基本概念
运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使统一运算符作用于不同类型的数据时导致不同类型的行为。
运算符重载的目的是:扩展C++中提供的运算符的使用范围,使之能作用于对象。
同一个运算符,对不同类型的操作数,所发生的行为不同。
- 运算符重载的形式
运算符重载的实质是函数重载。
可以重载为普通函数,也可以重载为成员函数。
把含运算符的表达式转换成对运算符函数的调用。
把运算符的操作数转换成运算符函数的参数。
运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。
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;
}
习题:
如果将[ ]运算符重载成一个类的成员函数,则该重载函数有几个参数?
(1个)
2.赋值运算符的重载
有时希望赋值运算符两边的类型可以不匹配,比如:把一个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){
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=("Shen zhou 8!");
cout<<s.c_str()<<endl;
return 0;
}
- 浅拷贝和深拷贝
class String{
private:
cahr *str;
public:
String():str(new char[1]){str[0]=0;}
const char * c_str(){return str;};
String &operator=(const cahr *s){
delete[] str;
str=new char[strlen(s)+1];
strcpy(str,s);
return *this;
};
~String(){delete[]str;}
};
String s1="this";
String s2="that";
s1=s2;
如果不定义自己的赋值运算符,那么s1=s2实际上导致s1.str和s2.str指向同一个地方
如果s1对象消亡,析构函数将释放s1.str指向的空间,则s2消亡时还要释放一次,不妥。
另外,如果执行s1=“other”;会导致s2.str指向的地方被delete
因此要在class String里添加新的成员函数:
String &operator=(const String&s){
delete[] str;
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
以上函数仍需要改进
String s;
s="hello";
s=s;
在=运算时,先delete了s的str成员指向的内存,导致出现了问题
String &operator=(const Sting&s){
if(this==&s)
return *this;
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
对operator=返回值类型讨论
对运算符重载的时候,好的风格是应该尽量保留原运算符原本的特性。
考虑 a=b=c; (a=b)=c;
分别等价于:
a.operator=(b.operator=©);
(a.oprator=(b)).operator=©;
为String类编写复制构造函数的时候,会面临和=同样的问题,用同样的方法处理。
在复制时,调用的默认的复制构造函数,只是将str复制过去,即指向同一个地方,析构时就会产生问题。
String(String & s){
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
}
一定要注意指针变量的复制。
3.运算符重载为友元函数
一般情况下,将运算符重载为类的成员函数,是较好的选择。
但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
class Complex{
double:real,imag;
public:
Complex(double x,double y):real(r),imag(i){};
Complex operator+(double r);
};
Complex Complex::operator+(double r){
return Complex(real+r,img);
}
Complex c;
c=c+5;有定义,相当于c.operator+(5);
c=5+c;会出错
所以,为了使上述表达式成立,需要将+重载为普通函数。
Complex operator+(double r,const Complex &c){
return Complex(c.real+r,c.imag);
}//但是普通函数又不能访问私有成员,所以,需要将运算符+重载为友元。
class Complex{
double real,imag;
public:
Complex(double r,double i):real(r),imag(i){};
Complex operator+(double r);
friend Complex operator+(double r,const Complex &c);
};
4.可变长整型数组
可实现如下的操作
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;
for(int i=0;i<a2.length();++i)
cout<<a2[i]<<" ";
cout<<endl;
a[3]=100;
CArray a4(a);//需要自己完成复制构造函数,实现深拷贝
for(int i=0;i<a4.length();++i)
cout<<a4[i]<<" ";
return 0;
}
class CArray{
int size;
int *ptr;
public:
CArray (int s=0);
CArray(CArray & a);
~CArray();
void push_back(int v);
CArray &operator =(const CArray &a);
int length(){return size;}
int &CArray::operator[](int i){//返回值不能是int,如果只是int,不能实现a[i]=4这种语句,只有引用作为函数的返回值才能放在赋值号的左侧。
return ptr[i];
}
}
CArray::CArray(int s):size(s){
if(s==0)
ptr=NULL;
else
ptr=new int [s];
}
CArray::CArray(CArray &a){
if(!a.ptr){
ptr=NULL;
size=0;
return;
}
ptr=new int [a.size];
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size=a.size;
}
CArray::~CArray(){
if(!ptr) delete [] ptr;
}
CArray & CArray::operator=(const CArray&a){
if(ptr==a.ptr)
return *this;
if(a.ptr==NULL){
if(ptr) delete [] ptr;
ptr=NULL;
size=0;
return *this;
}
if(size<a.size){
if(ptr)
delete [] ptr;
ptr=new int [a.size];
}
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size=a.size;
return *this;
}
void CArray::push_back(int v){
if(ptr){
int *tmpPtr=new int [size+1];
memcpy(tmpPtr,ptr,sizeof(int)*a.size);
delete [] ptr;
ptr=tmpPtr;
}
else
ptr=new int[1];
ptr[size++]=v;
}
习题:
在可变长数组类中,重载了哪些运算符或编写了哪些成员函数(B)
A) = , [] , ++
B) = , [] , 复制构造函数
C) = , [] , ++ , 复制构造函数
D) = , [] , & , 复制构造函数
5.流插入运算符和流提取运算符的重载
cout是在iostream中定义的,ostream类的对象
<<能作用在cout上是因为,在iostream中对<<进行了重载
cout<<5 即 cout.operator<<(5);
cout<<“this” 即 cout.operator<<(“this”);
怎样才能cout<<5<<“this”
ostream & ostream::operator<<(int n){
return *this;
}
ostream & ostream::operator<<(const char * s){
return *this;
}
cout<<5<<“this”
本质上的调用形式如下:
cout.operator<<(5).operator<<(“this”);
class CStudent{
public:
int nAge;
};
int main(){
CStudent s;
s.nAge=5;
cout<<s<<"hello";//重载<<使其能够接收 对象 参数
return 0;
}
ostream & operator(ostream & o,const CStudent &s){
o<<s.nAge;
}
例:
假定c是Complex复数类的对象,现在希望写cout<<c,就能以a+bi的形式输出c的值,写cin>>c,就能从键盘接收a+bi,形式的输入,并且使得c.real=a,c.imag=b。
int main(){
Complex c;
int n;
cin>>c>>n;
cout<<c<<","<<n;
return 0;
}
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r=0,double i=0):real(r),imag(i){};
friend ostream &operator<<(ostream &os,const Complex &c);
friend istream &operator>>(istream &is,Complex &c);
};
ostream & operator<<(ostream &os,const Complex &c){
os<<c.real<<"+"<<c.imag<<"i";
return os;
}
istream & operator>>(istream &is,Complex &c){
string s;
is>>s;
int pos=s.find("+",0);//分离实部和虚部
string sTmp=s.substr(0,pos);
c.real=atof(sTmp.c_str());
sTmp=s.substr(pos+1,s.length()-pos-2);//
c.imag=atof(sTmp.c_str());
return is;
}
习题:
重载“<<”用于自定义的对象通过cout输出时,以下说法哪个是正确的?(C)
A)可以将“<<”重载为ostream类的成员函数,返回值类型是ostream &
B)可以将"<<"重载为全局函数,第一个参数以及返回值,类型都是ostream
C)可以将"<<"重载为全局函数,第一个参数以及返回值都是ostream &(正确)
D)可以将"<<"重载为ostream类的成员函数,返回值类型是ostream
6.重载类型转换运算符
#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;}
//重载强制类型转换运算符
};
int main(){
Complex c(1.2,3.4);
cout<<(double)c<<endl;
double n=2+C;//double n=2+c.operator double()
cout<<n;
return 0;
}
7.自增自减运算符的重载
自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,C++规定:
前置运算符作为一元运算符重载
T & operator++();
T & operator--();
T1 & operator++(T2);
T1 & operator--(T2);
后置运算符作为二元运算符重载,多写一个没用的参数:
T operator++(int);
T operator--(int);
T1 operator++(T2,int);
T1 operator--(T2,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;
}
CDemo CDemo::operator++(){//后置++
CDemo tmp(*this);
n++;
return tmp;
}
CDemo & operator--(CDemo & d){//前置--
d.n--;
returrn d;
}
CDemo operator--(CDemo & d,int){//后置--
CDemo tmp(d);
d.n--;
return tmp;
}
int main(){
CDemo d(5);
cout<<(d++)<<",";//d.operator++(0);
cout<<d<<",";
cout<<(++d)<<",";//d.operator++();
cout<<d<<endl;
cout<<(d--)<<",";//d.operator--(d,0);
cout<<d<<",";
cout<<(--d)<<",";//d.operator--(d);
cout<<d<<endl;
return 0;
}
operator int(){return n;}
这里,int作为一个类型强制转换运算符被重载,伺候
Demo s;
(int) s;//等效于s.int();
类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型。
习题:
如何区分自增运算符重载的前置形式和后置形式?
A)重载时,前置形式的函数名是++operator,后置形式的函数名是operator++(错误)
B)后置形式比前置形式多一个int类型的参数(正确)
C)无法区分,使用时不管前置形式还是后置形式,都调用相同的重载函数(错误)
D)前置形式比后置形式多了一个int类型的参数(错误)
运算符重载的注意事项
C++不允许定义新的运算符
重载后运算符和含义应该符合日常习惯
运算符重载不改变运算符的优先级
以下运算符不能被重载
“.” “.*” “::” “?:” “sizeof”
重载运算符()、[]、->后者赋值运算符=时,运算符重载函数必须声明为类的成员函数
小建议:建议使用前置自增,前置自增相较后置自增更高效。