《程序设计实习》之【运算符重载】

运算符重载的基本概念

运算符

C++预定义表示数据的运算:

+, -, *, /, %, ^, &, ~, !, |, =, <<, >>, != ……
  • 只能用于基本的数据类型
    • 整型, 实型, 字符型, 逻辑型……

自定义数据类型与运算符重载

  • C++提供了数据抽象的手段:用户自己定义数据类型 – 类
    • 调用类的成员函数 -> 操作它的对象
  • 类的成员函数 -> 操作对象时,很不方便
    • 在数学上,两个复数可以直接进行+/-等运算
    • 在C++中,直接将+或-用于复数是不允许的

运算符重载

  • 运算符重载
    • 对已有的运算符赋予多重的含义
    • 对同一运算符作用于不同类型的数据时 -> 不同类型的行为
  • 目的
    • 扩展C++中提供的运算符的适用范围。以用于类所表示的抽象数据类型
  • 同一个运算符,对不同类型的操作数,所发生的行为不同
5, 10i) + (4, 8i) = (9, 18i)
5 + 4 = 9
  • 运算符重载的实质是函数重载
返回值类型 operator 运算符(形参表){
    ……
}

在程序编译时:

  • 把含运算符的表达式-> 对运算符函数的调用
  • 把运算符的操作数 -> 运算符函数的参数
  • 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数
  • 运算符也可以被重载成普通函数
  • 也可以被重载成类的成员函数

运算符被重载为普通函数

重载为普通函数时, 参数个数为运算符目数

class Complex {
public:
    Complex( double r = 0.0, double i= 0.0 ){
        real = r;
        imaginary = i;
    }
    double real;     // real part
    double imaginary; // imaginary part
};

Complex operator+ (const Complex & a, const Complex & b) {
    return Complex( a.real+b.real, a.imaginary+b.imaginary);
} // “类名(参数表)” 就代表一个对象

Complex a(1,2), b(2,3), c;
c = a + b;   // 相当于 c = operator+(a, b);

运算符被重载为成员函数

重载为成员函数时, 参数个数为运算符目数减一

class Complex {
public:
    Complex( double r= 0.0, double m = 0.0 ): real(r), imaginary(m) { }  // constructor
    Complex operator+ ( const Complex & ); // addition
    Complex operator- ( const Complex & ); // subtraction
private:
    double real;   // real part
    double imaginary; // imaginary part
};
// Overloaded addition operator
Complex Complex::operator+(const Complex & operand2) {
    return Complex( real + operand2.real, imaginary + operand2.imaginary );
}
// Overloaded subtraction operator
Complex Complex::operator- (const Complex & operand2){
    return Complex( real - operand2.real, imaginary - operand2.imaginary );
}

int main(){
    Complex x, y(4.3, 8.2), z(3.3, 1.1);
    x = y + z;  // 相当于x = y.operator+(z);
    x = y - z;  // 相当于x = y.operator-(z);
    return 0;
}

赋值运算符的重载

赋值运算符=重载

  • 赋值运算度两边的类型可能不匹配
    • 把一个int型变量赋值给一个complex对象
    • 把一个char *类型的字符串赋值给一个字符串对象
    • 需要重载赋值运算符=
  • 赋值运算符=只能重载为成员函数

编写一个长度可变的字符串类String

  • 包含一个char *类型的成员变量 -> 指向动态分配的存储空间
  • 该存储空间用于存放\0结尾的字符串
class String {
private:
    char * str;
public:
    String () : str(NULL) { }   //构造函数, 初始化str为NULL
    const char * c_str() { return str; }
    char * operator = (const char * s);
    ~String( );
};
//重载 ‘=’  obj = “hello”能够成立

char * String::operator = (const char * s) {
    if(str) delete [] str;
    if(s) {   //s不为NULL才会执行拷贝
        str = new char[strlen(s)+1];
        strcpy(str, s);
    }
    else
        str = NULL;
    return str;
}
String::~String( ) {
    if(str) delete [] str;
};

int main() {
    String s;
    s = “Good Luck,” ;
    cout << s.c_str() << endl;
    // String s2 = “hello!”; //这条语句要是不注释掉就会出错,因为没有这样的构造函数
    s = "Shenzhou 8!";
    cout << s.c_str() << endl;
    return 0;
}
程序输出结果:
Good Luck,
Shenzhou 8!

重载赋值运算符的意义- 浅复制和深复制

  • S1 - S2
  • 浅复制/浅拷贝
  • 执行逐个字节的复制工作
MyString S1, S2;
S1 = “this”;
S2 = “that”;
S1 = S2;


- 深复制/ 深拷贝
- 讲一个对象指针变量指向的内容复制到另一个对象中指针成员对象指向的地方

MyString S1, S2;
S1 = “this”;
S2 = “that”;
S1 = S2;

class MyString中添加成员函数:

String & operator = (const String & s) {
    if(str) delete [] str;
    str = new char[strlen(s.str)+1];
    strcpy(str, s.str);
    return * this;
}

思考

考虑下面的语句,是否会有问题?

MyString s;
s = “Hello”;
s = s;

正确写法:

String & String::operator = (const String & s){
    if(str == s.str) return * this;
    if(str) delete [] str;
    if(s.str) {     //s.str不为NULL才会执行拷贝
        str = new char[strlen(s.str)+1];
        strcpy( str,s.str);
    }  
    else
        str = NULL;
    return * this;
}

operator =返回值类型的讨论

  • void好不好?
    • 考虑a = b = c;,等价于a.operator=(b.operator=(c));
  • String好不好?
    • 考虑(a=b)=c;,等价于(a.operator=(b)).operatpr=(c);
  • String &

复制构造函数

为String类编写复制构造函数时,会面临和=同样的深拷贝/浅拷贝的问题,用同样的方法处理

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

运算符重载为友元函数

通常,将运算符重载为类的成员函数

重载为友元函数的情况:
- 成员函数不能满足使用要求
- 普通函数又不能访问类的私有成员

运算符重载为友元

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; //编译出错

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

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

但是普通函数不能访问私有成员 -> 将运算符+重载为友元函数

class Complex {
    double real, imag;
public:
    Complex( double r, double i):real(r),imag(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;
}
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; //加入新的数组元素
}

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

问题

  • cout << 5 << "this";为什么能够成立
  • cout是什么?<<为什么能用在cout上?

流插入运算符的重载

  • cout是在iostream中定义的,ostream类的对象
  • <<能用在cout上是因为,在iostream中对<<进行了重载
  • 考虑,怎么重载才能使得cout << 5;cout << "this";都能成成立
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;
}
ostream & operator<<( ostream & o, const CStudent & s) {
    o << s.nAge ;
    return 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;
}

程序运行结果可以如下:
13.2+133i 87
13.2+133i, 87
#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 就是cout
    os << c.real << "+" << c.imag << "i"; // 以"a+bi"的形式输出
    return os;
}
istream & operator>>( istream & is,Complex & c) {  // is 就是cin
    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;
}

自增/自减运算符的重载

  • 自加++/自减--运算符有前置/后置之分
  • 前置运算符作为一元运算符重载
    • 重载为成员函数:
      • T& operator++();
      • T& operator--();
    • 重载为全局函数:
      • T& operator++(T &);
      • T& operator--(T &);
    • ++obj, obj.operator++(), operator++(obj) 都调用上述函数
      -后置运算符作为二元运算符重载
    • 多写一个参数,具体无意义
    • 重载为成员函数:
      • T operator++(int);
      • T operator--(int);
    • 重载为全局函数:
      • T operator++(T &, int);
      • T operator--(T &, int);
    • obj++, obj.operator++(0), operator++(obj, 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

如何编写 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;
}
CDemo CDemo::operator++(int k) {  // 后置 ++
    CDemo tmp(*this); //记录修改前的对象
    n++;
    return tmp;   // 返回修改前的对象
}
CDemo & operator--(CDemo & d) {   // 前置--
    d.n--;
    return d;
}
CDemo operator--(CDemo & d, int) {  // 后置--
    CDemo tmp(d);
    d.n--;
    return tmp;
}
operator int ( ) { return n; }
  • int 作为一个类型强制转换运算符被重载
Demo s;
(int) s ; // 等效于 s.int();
  • 类型强制转换运算符重载时,
    • 不能写返回值类型
    • 实际上其返回值类型 – 类型强制转换运算符代表的类型

运算符重载的注意事项

  • C++不允许定义新的运算符
  • 重载后运算符的含义应该符合日常习惯
    • complex_a + complex_b
    • word_a > word_b
    • date_a = date_a + n
  • 运算符重载不改变运算符的优先级
  • 以下运算符不能被重载:. * :: ?: sizeof
  • 重载运算符() [] ->或者赋值运算符=时,重载函数必须声明为类的成员函数
版权声明:本文为beashaper_原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/beashaper_/article/details/80627451

智能推荐

20145107 《Java程序设计》第五次实验报告

实验简述: 在本周,我们进行了Java的第五次试验,本次实验的主要内容是结对编程。本次实验的大体过程是: 1.先进行Java的客户端与服务端的代码编写。结对是两个人,一人负责客户端,一人负责服务端。 2.利用加解密代码包,编译运行代码,客户端加密,服务器解密。 3.客户端加密明文后将密文通过TCP发送。 4.在本次的代码编写上,要求代码可以实现两者之间的数据传输,在代码传输的基础上加上一定的加密过...

更改springboot启动拼成的字母

1.更改springboot启动拼成的字母 其实很好改,只需要在resources下新建一个txt文件就可以,命名为banner.txt,那这种字符该怎么拼出来呢,下面推荐一个网址,有这种工具 传送门 2.集成...

Node.js安装配置

好久都没更新博客了,今天心血来潮,决定是时候更新一篇了,首先我们来认识一下node.js。 什么是node.js? 简单的说 Node.js 就是运行在服务端的 JavaScript。 Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。 Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的...

RocketMQ之双Master集群搭建笔记记录

一:RocketMQ双master集群部署 服务器环境(我采用的虚拟机,centos6 .5【特别注意:安装的虚拟机centos系统一定得是64位的,32位的会启动不起来。即便起来了也会有很多问题,深坑勿踩】)  ip       用户名    密码        角色     模式 192.168.197.101   root        nameServer1,brokerServer1  ...

蓝桥杯试题集-基础练习题-数列特征(Java)

//做题笔记,仅自己看得懂 题目: 正确姿势:...

猜你喜欢

多线程爬取4k超高清美图壁纸

多线程爬取4k美图壁纸 前言:看完此篇文章你可以更加深入的了解多线程的使用,并且最重要的你能够下载你自己想要的超高清4k壁纸 爬取结果: 1. 分析网站 要爬取的url :http://pic.netbian.com/ a) 判断网页是动态加载还是静态加载页面。右击查看网页源代码,按Ctrl + f在源代码中搜索网站的详情页地址,从而判断整个网页是静态加载的 b) 明确爬取的目标。我们要爬取的目标...

elementUI-添加自定义图标

elementui的小图标有限,跟UI给的不一样,这个时候咋办呢?百度走起。。。。参考了两篇博主分享的 自定义elementui中的图标 和 建立图标库,这里主要用到第一种 实际中: elementUI导航栏 具体代码: 汉字转换Unicode编码: 直接打开控制台: 汉字.chatCodeAt().toString(16); 然后回车; 至于三角形的图标,我直接把箭头的 unicode 值改成了...

[Linux]——文件缓冲区

文件缓冲区 提到文件缓冲区这个概念我们好像并不陌生,但是我们对于这个概念好像又是模糊的存在脑海中,之间我们在介绍c语言文件操作已经简单的提过这个概念,今天我们不妨深入理解什么是文件缓冲区。 为什么需要文件缓冲区 当我们在程序中写下一条printf语句时,我们希望将这条语句的内容打印到屏幕上。但是如果你将语句放在循环中,难道你执行一次循环那么操作系统就要打印一次这条数据么?答案当然不是 我们对于程序...

基于FPGA的IIC协议详解——EEPROM控制器(1)

IIC协议举例 常用IIC协议使用地方 常见IIC协议的注意点 24LC64芯片读写命令的时序图 eeprom控制器的系统框图 时序图设计 代码设计 EEPROM控制器测试模块的代码 结束语 常用IIC协议使用地方 熟悉一个协议一定要知道这个协议应该用到什么地方,IIC协议作为飞利浦公司定义的一个慢速传输协议,常用于: 1、芯片寄存器的配置; 2、eeprom的读写; 本次实验我们将使用eepro...

ssm 工程简易搭建(idea 工具下)

现在都流行springboot了,大部分都给你配合了了,可以快速开发。但是今天我们回顾一下ssm的搭建,其实也挺简单的,比刚入门那会觉得简单多了。 这个先讲几点: 1.开始搭建前,idea 自己要配置maven 和tomcat和jdk ,这里就不讲了。 2.为了简化配置,我们按照“约定优于配置”的原则(这种原则在sprigboot项目里面特别明显),启动项目时,项目有个文件...