程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载

笔记按照中国大学MOOC上北京大学郭炜老师主讲的程序设计与算法(三)C++面向对象程序设计所作,B站上也有资源。原课程链接如下:

程序设计与算法(三)C++面向对象程序设计

其他各章节链接如下:

程序设计与算法(三)C++面向对象程序设计笔记 第一周 从C到C++

程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础

程序设计与算法(三)C++面向对象程序设计笔记 第三周 类和对象提高

程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载

程序设计与算法(三)C++面向对象程序设计笔记 第五周 继承

程序设计与算法(三)C++面向对象程序设计笔记 第六周 多态

程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板

程序设计与算法(三)C++面向对象程序设计笔记 第八周 标准模板库STL(一)

程序设计与算法(三)C++面向对象程序设计笔记 第九周 标准模板库STL(二)

程序设计与算法(三)C++面向对象程序设计笔记 第十周 C++11新特性和C++高级主题

其他各科笔记汇总

运算符重载

运算符重载基本概念

运算符重载的需求

C++ 预定义的运算符,比如+、-、/、%、^、&、~、!、|、=、<<、>>、!=、…只能用于基本数据类型的运算:整型、实型、字符型、逻辑型 …,不能用于对象之间的运算

在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的

有时会希望,让对象也能通过运算符进行运算,这样代码更简洁,容易理解

例如:complex_a 和 complex_b 是两个复数对象;求两个复数的和,希望能直接写:complex_a + complex_b

运算符重载

运算符重载,就是对已有的运算符( C++ 中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为

运算符重载的目的是:扩展 C++ 中提供的运算符的适用范围,使之能作用于对象

同一个运算符,对不同类型的操作数,所发生的行为不同

  • complex_a + complex_b 生成新的复数对象
  • 5 + 4 = 9
运算符重载的形式

运算符重载的实质是函数重载

可以重载为普通函数,也可以重载为成员函数

把含运算符的表达式转换成对运算符函数的调用

把运算符的操作数转换成运算符函数的参数

运算符被多次重载时,根据实参的类型决定调用哪个运算符函数

返回值类型 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;
}

输出:
5,5
3,3

赋值运算符的重载

赋值运算符 “=” 重载

有时候希望赋值运算符两边的类型可以不匹配,比如,把一个 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;  
    //返回值是String的引用,返回 * this,即对象自身的引用
}

int main()
{
    String s;
    s = "Good Luck," ; //等价于 s.operator=("Good Luck,");
    cout << s.c_str() << endl;
// String s2 = "hello!";    // 这条语句是初始化语句,不是赋值语句,这里的“=”并不是赋值号,初始化语句要求 s2 用构造函数初始化。不注释掉就会出错
    s = "Shenzhou 8!"; // 等价于 s.operator=("Shenzhou 8!");
    cout << s.c_str() << endl;
    return 0;
}

输出:
Good Luck,
Shenzhou 8!

浅拷贝和深拷贝
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){
                     delete [] str;
                     str = new char[strlen(s)+1];
                     strcpy(str, s);
                     return * this;
              };
              ~String( ) { delete [] str; }
};
String S1, S2;
S1 =this;
S2 = “that”;
S1 = S2;

写 S1 = S2 希望达到的目的是 S1 里面放着的字符串和 S2 里面放着的字符串一样

如果不重载这个等号,S1 = S2 原本也能够编译通过,因为等号两边的类型完全相同,都是 String 对象。但原生的等号做的是对象复制的工作,会将 S1 里面的每一个字节都变成跟 S2 里面的每一个字节一样,简单地说就是使得 S1 里面的每一个成员变量都变成跟 S2 里面的成员变量一模一样,实际上会导致 S1.str 和 S2.str 指向同一地方

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6s7cB2Sb-1666237342940)(C++ 面向对象程序设计.assets/image-20221011204101958.png)]

S1.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 操作,这就很可能会出错。如果等号左右两边的对象是同一个, 应该什么都不做

 

解决方法:

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;
}
对 operator = 返回值类型的讨论

为什么是 String & 而不是 void 和 String ?

对运算符进行重载的时候,好的风格是应该尽量保留运算符原本的特性

 

考虑:

a = b = c;
//执行顺序从右到左,先让 b 等于 c,再让 a 等于 b = c 的返回值
//b = c 的返回值即 b 的值

( a = b ) = c; //会修改 a 的值
//a = b 的返回值实际上是 a 的引用,在 C++ 中赋值运算符的返回值是等号左边变量的引用

分别等价于:

a.operator=( b.operator=(c) );
( a.operator=(b) ).operator=(c);

 

 

上面的 String 类是否就没有问题了?

如果 String 类不写复制构造函数,用编译器生成的缺省的复制构造函数,会面临和 “=” 同样的问题。这个复制构造函数会做复制工作,把被初始化对象里面的 str 变成和参数对象的 str 一模一样,指向同一片存储空间

用同样的方法处理

String(String & s)
{
       str = new char[strlen(s.str)+1];
       strcpy(str,s.str);
}

这里就不需要判断当前对象是否和 s 一样,因为当前对象刚刚生成,不可能和现有的对象一样

运算符重载为友元函数

一般情况下,将运算符重载为类的成员函数,是较好的选择

但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元

class Complex
{
       double real,imag;
       public:
           Complex(double r, double i):real(r),imag(i){ };
           Complex operator+( double r );
};

Complex Complex::operator+( double r )
{ //能解释 c+5
      return Complex(real + r,imag);
}

 

经过上述重载后:

Complex c ;
c = c + 5; //有定义,相当于 c = c.operator +(5);

但是:

c = 5 + c; //编译出错

 

所以,为了使得上述的表达式能成立,需要将 + 重载为普通函数

但是普通函数又不能访问私有成员,所以,需要将运算符 + 重载为友元

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);
};

Complex operator+ (double r,const Complex & c)
{ //能解释 5+c
      return Complex(c.real + r, c.imag);
}

运算符重载实例:可变长整型数组

要编写可变长整型数组类,使之能如下使用:

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 1 100 4

 

要做哪些事情?

写一个类一般都要写构造函数、析构函数

需要 push_back()、length() 成员函数

要用动态分配的内存来存放数组元素,需要一个指针成员变量,分配的内存在 CArray 类的析构函数里面释放

参考前面浅拷贝和深拷贝的例子,实现赋值语句 a2 = a 要重载 “=”

a2 是一个对象名,不是数组名,要让 a2[i] 成立要重载 “[]”,“[]”是双目运算符,一个操作数在“[]”外面,一个操作数在“[]”里面

CArray a4(a) 用到了 CArray 的复制构造函数,参考前面浅拷贝和深拷贝的例子,不能用缺省的,要自己写复制构造函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghV0Hzsx-1666237342943)(C++ 面向对象程序设计.assets/image-20221011235620112.png)]

 

 

class CArray {
   int size; //数组元素的个数
   int *ptr; //指向动态分配的数组
   public:
       CArray(int s = 0); //s代表数组元素的个数
       CArray(CArray & a);
       ~CArray();
       void push_back(int v); //用于在数组尾部添加一个元素v
       CArray & operator=(const CArray & a);
       //用于数组对象间的赋值
       int length() { return size; } //返回数组元素个数
       int & CArray::operator[](int i) //返回值为 int 不行!不支持 a[i] = 4,非引用的函数返回值不可以作为左值使用
       {//用以支持根据下标访问数组元素,
        //如n = a[i] 和 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) //防止a=a这样的赋值导致出错
      return * this;
   if( a.ptr == NULL) { //如果a里面的数组是空的
      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;
} // CArray & CArray::operator=(const CArray & a)    

void CArray::push_back(int v)
{  //在数组尾部添加一个元素
   if(ptr) {
         int * tmpPtr = new int[size+1]; //重新分配空间
         memcpy(tmpPtr,ptr,sizeof(int)*size); //拷贝原数组内容
         delete [] ptr;
         ptr = tmpPtr;
   }
   else //数组本来是空的
      ptr = new int[1];
   ptr[size++] = v; //加入新的数组元素
}

这个 push_back() 是比较低效的,比较好的做法是预先分配多一点的空间,C++ STL 里面 vector 的 push_back() 成员函数就采用这种实现方法

流插入运算符和流提取运算符的重载

问题:

out << 5 << "this";

为什么能够成立?

cout是什么?“<<” 为什么能用在 cout 上?

流插入运算符的重载

cout 是在 iostream 头文件中定义的 ostream 类的对象

<<” 能用在 cout 上是因为在 iostream 里对 “<<” 进行了重载

实际上在 C++ 的语法里,“<<” 和 “>>” 是左移和右移运算符,只不过重载后经常用于输入输出,所以又叫流插入和流提取运算符

 

考虑,怎么重载才能使得

cout << 5; cout << "this"

都能成立?

 

 

有可能按以下方式重载成 ostream 类的成员函数:

void ostream::operator<<(int n)
{
         ......//输出n的代码
         return;
}

 

cout << 5; cout << "this"

cout.operator<<(5); cout.operator<<("this");

 

 

 

怎么重载才能使得

cout << 5 << "this";

成立?

 

修改返回值类型为 ostream &

ostream & ostream::operator<<(int n)
{
            ......//输出n的代码
            return * this;
}

ostream & ostream::operator<<(const char * s )
{
            ......//输出s的代码
            return * this;
}

 

cout << 5 <<this;

本质上的函数调用的形式是

cout.operator<<(5).operator<<(this);

 

 

 

例子:

假定下面程序输出为 5hello,该补写些什么?

class CStudent{
    public: int nAge;
};

int main(){
    CStudent s ;
    s.nAge = 5;
    cout << s <<"hello";
    return 0;
}

cout 是 ostream 类的对象,ostream 类早就在 iostream 头文件里写好了,不可能再为它添加成员函数,所以在这里重载 “<<” 时只能重载成一个全局函数

 

ostream & operator<<(ostream & o,const CStudent & s){
      o << s.nAge ;
      return o;
}

这里如果不写 ostream & o 写 ostream o 编译不过

例题

假定 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;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W1MbELAm-1666237342945)(C++ 面向对象程序设计.assets/image-20221012213912966.png)]

 

#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"; //以"a+bi"的形式输出
   return os;
}

istream & operator>>( istream & is,Complex & c)
{
   string s;
   is >> s; //将"a+bi"作为字符串读入,“a+bi” 中间不能有空格
   int pos = s.find("+",0);
   string sTmp = s.substr(0,pos); //分离出代表实部的字符串
   c.real = atof(sTmp.c_str()); 
   //atof库函数能将const char*指针指向的内容转换成 float
   sTmp = s.substr(pos+1, s.length()-pos-2);  // 分离出代表虚部的字符串
   c.imag = atof(sTmp.c_str());
   return is;
}

“<<” 和 “>>” 运算符只能重载为全局函数,不可能重载为 ostream类 或 istream 类的成员函数,因为这两个类都已经写好了。由于在 Complex 这个类里 real、imag 这两个成员变量都是私有的,全局函数要被声明为 Complex 类的友元才能访问

类型转换运算符和自增、自减运算符的重载

重载类型转换运算符

任何一个类型的名字本身就是类型转换运算符

#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
}

operator double() 是一个函数,返回值类型是 double。类型转换运算符进行重载时不写返回值类型,它的返回值类型就是这个类型本身

double 是一个单目运算符,(double)c 被解释为 c.operator double()

如果 c 能够被自动转换成一个 double 类型的东西就可以和2相加,编译器也是这么考虑的

 

重载类型转换运算符之后,就可以把 Complex 对象转换成一个 double 类型,这个转换可以是显式的,也可以是自动的,在任何可以出现 double 类型变量的地方,如果出现的是一个 Complex 对象,那这个对象就会被自动转换成 double 类型的东西

自增,自减运算符的重载

自增运算符 ++ 、自减运算符 – 有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,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);
      

但是在没有后置运算符重载而有前置重载的情况下,在 vs 中,obj++ 也调用前置重载,而 dev 则令 obj++ 编译出错

 

 

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

 

如何编写 CDemo?

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);

在 C++ 里面,++a 的返回值是 a 的引用,a++ 返回一个临时变量,临时变量的值就是 a 被修改之前的值

后置的++和–生成了一个局部对象,引发复制构造函数调用,返回值也是一个对象,又会引发构造函数调用。前置的++和–里面并没有生成对象,返回的也是引用,运行速度较快

运算符重载的注意事项

  1. C++ 不允许定义新的运算符
  2. 重载后运算符的含义应该符合日常习惯
    • complex_a + complex_b
    • word_a > word_b
    • date_b = date_a + n
  3. 运算符重载不改变运算符的优先级
  4. 以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof
  5. 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值