1. 运算符重载

1.1 基本概念

函数重载(函数多态)是指用户能够定义多个名称相同但参数列表不同的函数,旨在使用户能够用同名的函数来完成相同的基本操作,即使这种操作被用于不同的数据类型。

运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多种含义。

实际上,很多C++(也包括C语言)运算符已经被重载。例如,将*运算符用于地址,将得到存储在这个地址中的值;但用于两个数字时,得到的是它们的乘积。

C++允许运算符重载扩展到用户自定义的类型,例如,允许使用+将两个对象相加。编译器将根据操作数的数目和类型决定使用哪种假发定义。重载运算符可使代码看起来更自然。例如,将两个数组相加通常使用for循环实现:

for(int i=0; i< 10; i++){
    A[i] = B[i] + C[i];
}

但在C++中,可以定义一个表示数组的类,并重载+运算符。于是便有如下语句:

A = B + C;

1.2 运算符重载函数格式

要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:

operatorOP(param1, param2, ...)

例如,operator+()表示重载+运算符,operator*()表示重载*运算符。OP必须是有效的C++运算符,不能虚构一个新的符号,例如不能有operator@()这样的函数,因为C++中没有@运算符。

1.3 案例:重载运算符+

time.h

#ifndef TIME_H
#define TIME_H

class Time{
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0); // 注意:此处不能h和m都设置默认值,否则将和Time()冲突
	void addMin(int m);
	void addHour(int h);
	void resetTime(int h = 0, int m = 0);
	Time operator+(const Time &t) const; // 运算符重载
	void show() const;
};
#endif

如果Time(int h = 0, int m = 0);均设置默认值,使用时创建对象Time t;编译器将不知道是该调用默认构造函数还是带参构造函数(因为带参构造两个参数均有默认值,均可以省略)。

头文件管理:

在同一个文件中只能将同一个头文件包含一次。记住这个规则很容易,但很可能在不知情的情况下将头文件包含多次。例如,可能使用包含了另外一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即if not defined)的。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称TIME_H时,才处理#ifndef#endif之间的语句:

#ifndef TIME_H
...
#endif

编译器是首次遇到该文件时,名称TIME_H没有定义(我们一般根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称)。

time.cpp

#include <iostream>
#include "time.h"

Time::Time(){
	hours = minutes = 0;
}

Time::Time(int h, int m){
	hours = h;
	minutes = m;
}

void Time::addMin(int m){
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}

void Time::addHour(int h){
	hours += h;
}

void Time::resetTime(int h, int m){
	hours = h;
	minutes = m;
}

Time Time::operator+(const Time &t) const{
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes/60;
	sum.minutes %= 60;
	return sum;
}

void Time::show() const{
	std::cout << hours << "小时 " << minutes << "分钟" << std::endl;
}

usetime.ccpp

#include<iostream>
#include"time.h"

int main(){
	using std::cout;
	using std::endl;

	Time time1(2, 45);
	time1.show();
	Time time2(1, 56);
	time2.show();

	Time time3 = time1 + time2;
	time3.show();

	Time time4 = time1.operator+(time2);
	time4.show();

	Time time5 = time1 + time2 + time3;
	time5.show();

	return 0;
}

输出:

2小时 45分钟
1小时 56分钟
4小时 41分钟
4小时 41分钟
9小时 22分钟

Time time5 = time1 + time2 + time3;是如何被转换为函数调用的:

由于+是从左向右结合,因此上述语句首先被转换为:

Time time5 = time1.operator+(time2 + time3);

然后,函数参数本身被转换为一个函数调用,结果为:

Time time5 = time1.opearator+(time2.operator+(time3));

2. 运算符重载注意事项

  1. 重载后的运算符必须至少有一个操作数是用户自定义的类型,这将防止用户为标准类型重载运算符。

  2. 使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成使用一个操作数。同样,不能修改运算符的优先级。因此,如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级。

  3. 不能创建新的运算符。例如,不能定义operator**()函数来表示求幂。

  4. 不能重载下面的运算符:

    • sizeof:sizeof运算
    • .:成员运算符
    • .*:成员指针运算符
    • :: :作用域解析运算符
    • ?::条件运算符
    • typeid:一个RTTI运算符
    • const_cast:强制类型转换运算符
    • dynamic_cast:强制类型转换运算符
    • reinterpret_cast:强制类型转换运算符
    • static_cast:强制类型转换运算符
  5. 下表中的大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符值能通过成员函数进行重载

    • =:赋值运算符
    • ():函数调用运算符
    • []:下标运算符
    • ->:通过指针访问类成员的运算符

    image

3. 运算符重载与友元:重载运算符<<

3.1 <<的第一种重载版本

要使Time类知道使用cout,必须使用友元函数。这是因为下面这样的语句使用两个对象,其中一个时ostream类对象(cout):

cout << time;

如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数,这就意味着必须这样使用<<:

time << cout;

而这样会令人迷惑,但通过友元函数,可以像下面这样重载运算符:

void operator<<(ostream &os, const Time &t){
    os << t.hours << t.minutes;
}

time.h

#ifndef TIME_H
#define TIME_H
#include<iostream>

class Time{
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void addMin(int m);
	void addHour(int h);
	void resetTime(int h = 0, int m = 0);
	Time operator+(const Time &t) const;
	// void show() const; 去除show方法,转而由<<重载函数替代
	friend void operator<<(std::ostream &os, const Time &t); // 友元函数对<<进行重载
};
#endif

time.cpp

#include <iostream>
#include "time.h"

Time::Time(){
	hours = minutes = 0;
}

Time::Time(int h, int m){
	hours = h;
	minutes = m;
}

void Time::addMin(int m){
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}

void Time::addHour(int h){
	hours += h;
}

void Time::resetTime(int h, int m){
	hours = h;
	minutes = m;
}

Time Time::operator+(const Time &t) const{
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes/60;
	sum.minutes %= 60;
	return sum;
}

void operator<<(std::ostream &os, const Time &t){
	os << t.hours << "小时 " << t.minutes << "分钟";
}

usetime.cpp

#include<iostream>
#include"time.h"

int main(){
	using std::cout;
	using std::endl;

	Time time1(2, 45);
	cout << time1;

	return 0;
}

输出:

1676722710371

3.2 <<的第二种重载版本

<<的第一种重载版本虽然成功使得cout << time;正常工作,但是不支持连续输出:

cout << “时间为:” << time << endl;

而这是由于在iostream定义中,<<运算符要求左边必须是一个ostream对象,由于cout是ostream对象,所以重载后cout << time;满足这种要求。但是重载之后,cout << x << y;中cout<<x的返回为空,所以<<y的左边并不满足ostream对象这一条件。

因此,第二种重载版本便是将重载函数operator<<()的返回值设置成ostream对象的引用即可:

ostream& operator<<(ostream &os, const Time &t){
    os << t.hours << t.minutes;
    return os;
}

注意:返回类型是ostream&,而不是ostream。这是因为若返回类型是ostream,将调用ostream类的拷贝构造函数,但是ostream的拷贝构造函数是private权限。而返回类型是ostream&,意味着函数的返回值就是传递给该函数的对象。

time.h

#ifndef TIME_H
#define TIME_H
#include<iostream>

class Time{
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void addMin(int m);
	void addHour(int h);
	void resetTime(int h = 0, int m = 0);
	Time operator+(const Time &t) const;
	friend std::ostream& operator<<(std::ostream &os, const Time &t); // <<的第二种重载版本
};
#endif

time.cpp

#include <iostream>
#include "time.h"

Time::Time(){
	hours = minutes = 0;
}

Time::Time(int h, int m){
	hours = h;
	minutes = m;
}

void Time::addMin(int m){
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}

void Time::addHour(int h){
	hours += h;
}

void Time::resetTime(int h, int m){
	hours = h;
	minutes = m;
}

Time Time::operator+(const Time &t) const{
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes/60;
	sum.minutes %= 60;
	return sum;
}

std::ostream& operator<<(std::ostream &os, const Time &t){
	os << t.hours << "小时 " << t.minutes << "分钟";
	return os;
}

usetime.cpp

#include<iostream>
#include"time.h"

int main(){
	using std::cout;
	using std::endl;

	Time time1(2, 45);
	cout << "时间为:" << time1 << endl;

	return 0;
}

输出:

时间为:2小时 45分钟

4. 重载运算符=

4.1 编译器默认给类提供了一个默认的赋值运算符重载函数

默认的赋值运算符重载函数进行了简单的赋值操作。

demo

#include <iostream>
#include <string>
using namespace std;

class Person{
friend void Display(Person &p){
        cout << "id:" << p.id << " name:" << p.name << endl;
    }
private:
    int id;
    string name;
public:
    Person(int id, string name){
        this->id = id;
        this->name = name;
    }
};

int main(){
    Person p1(1, "leo");
    Person p2(2, "mike");
    Display(p1);
    Display(p2);
    p1 = p2;
    Display(p1);
    Display(p2);
    return 0;
}

输出:

id:1 name:leo
id:2 name:mike
id:2 name:mike
id:2 name:mike

4.2 当类含有成员指针时,默认的赋值运算符重载函数将出现异常

#include <iostream>
#include <cstring>
using namespace std;

class Person{
friend void Display(Person &p){
        cout << "id:" << p.id << " name:" << p.name << endl;
    }
private:
    int id;
    char *name;
public:
    Person(int id, const char *name){
        this->id = id;
        this->name = new char[strlen(name)+1];
        strcpy(this->name, name);
    }
    ~Person(){
        if(this->name != NULL){
            delete[] this->name;
            this->name = NULL;
        }
    }
};

void test(){
    Person p1(1, "leo");
    Person p2(2, "mike");
    Display(p1);
    Display(p2);
    p1 = p2;
    Display(p1);
    Display(p2);
}

int main(){
    test();
    return 0;
}

输出(报错):

id:1 name:leo
id:2 name:mike
id:2 name:mike
id:2 name:mike
free(): double free detected in tcache 2

注意,由于未给Person重载赋值运算符,将调用编译器默认提供的赋值运算符重载函数。但是默认的赋值运算符重载函数只进行了简单的赋值操作,即浅拷贝。浅拷贝示意图如下。

img

所以,当类含有成员指针时,需要为其重载赋值运算符函数,代码如下:

// 重载赋值运算符操作
Person& operator=(const Person &p){
    if(this->name != NULL){
        delete[] name;
        name = NULL;
    }
    this->name = new char[strlen(p.name) + 1];
    strcpy(this->name, p.name);
    this->id = p.id;
    return *this;
}

Q1:为什么需要先判断this->name是否为空?

由于调用赋值运算符重载函数时,此时当前对象(*this)已经创建完毕,那么就有可能this->name指向堆内存。这个时候如果不对其name指针进行判断而直接进行赋值,会导致this原指向内存得不到及时释放。

Q2:为什么返回引用类型*this?

为了实现连续赋值。如p1 = p2 = p3,意义为p3赋值给p2,然后p2赋值给p1。如果Person的赋值重载函数返回的是Person而不是Person&,那么p2 = p3这个表达式将会产生一个新的匿名对象,然后将这个匿名对象赋予p1。

C++规定为了实现连续赋值,赋值操作符必须返回一个引用指向操作符的左侧实参。这是为class实现赋值操作符必须遵循的协议。这个协议不仅适用于标准的赋值形式,也适用于+=、-=、*=等。

void test(){
    Person p1(1, "leo");
    Person p2(2, "mike");
    p1 = p2;

    cout << &(p1 = p2) << endl;
    cout << &p1 << endl;
}

输出:

0x61fdd0
0x61fdd0

5. 重载运算符++

++属于一元运算符。一般有前置操作和后置操作两种情况(即前置加加++i和后置加加i++)。

而对一元运算符进行重载需实现“编译器能够根据运算符出现在作用对象的前面还是后面来调用不同的函数”。

C++规定:

  • 当编译器看到++a(即前置++)时,它就会调用a对象所属类重载的operator++()方法;
  • 当编译器看到a++(即后置++)时,它就会调用a对象所属类重载的operator++(int)方法。

demo:

#include <iostream>

using namespace std;

class Complex{
// 友元函数:重载<<运算符
friend ostream& operator<<(ostream& os, Complex &co){
    os << "A: " << co.mA << " B: " << co.mB;
    return os;
}
public:
    Complex(){
        this->mA = 0;
        this->mB = 0;
    }
    // 重载前置++
    Complex& operator++(){
        mA++;
        mB++;
        return *this;
    }
    // 重载后置++ 
    const Complex operator++(int){
        Complex tmp(*this);
        ++(*this);
        return tmp;
    }
    // 重载前置--
    Complex& operator--(){
        mA--;
        mB--;
        return *this;
    }
    // 重载后置--
    const Complex operator--(int){
        Complex tmp(*this);
        --(*this);
        return tmp;
    }
    
private:
    int mA;
    int mB;
};

int main()
{
    Complex cp;
    cout << cp << endl; // A: 0 B: 0
    Complex re = cp++;
    cout << re << endl; // A: 0 B: 0
    cout << cp << endl; // A: 1 B: 1
    ++cp;
    cout << cp << endl; // A: 2 B: 2
    return 0;
}

输出:

A: 0 B: 0
A: 0 B: 0
A: 1 B: 1
A: 2 B: 2

Q1: a++的返回类型为什么要是const对象呢?

有两个原因:

  1. 如果不是const对象,a(++)++这样的表达式就可以通过编译。但是,其效果却违反了我们的直觉 。a其实只增加了1,因为第二次自增作用在一个临时对象上。
  2. 另外,对于内置类型,(i++)++这样的表达式是不能通过编译的。自定义类型的操作符重载,应该与内置类型保持行为一致 。

a++的返回类型如果改成非const对象,肯定能通过编译,但是我们最好不要这样做。

Q2: ++a的返回类型为什么是引用呢?

这样做的原因应该就是:与内置类型的行为保持一致。前置++返回的总是被自增的对象本身。因此,++(++a)的效果就是a被自增两次。

6. 智能指针类&重载指针运算符

6.1 引子

#include<iostream>
#include<string>
using namespace std;

class Person{
private:
	string name;
public:
	Person(){ name="no name"; }
	Person(string name){ this->name=name; }
	void show(){ cout << "name=" << name << endl;}
};


int main(){
	Person *p = new Person("Mike");
	p->show();
	delete p; // 如果忘记释放,那么就会造成内存泄漏
	p = NULL;

	return 0;
}

输出:

name=Mike

对象在堆区开辟内存,不会自动调用析构函数,若不利用delete进行手动释放,将会造成内存泄漏。

6.2 智能指针类:托管new出来的对象的释放

智能指针类:

class SmartPoint{
private:
	Person *p;
public:
	SmartPoint(){ p=NULL; }
	SmartPoint(Person *p){
		cout << "SmartPoint带参构造" << endl;
		this->p=p;
	}
	~SmartPoint(){
		cout << "SmartPoint析构" << endl;
		if(p!=NULL){
			delete p;
			p = NULL;
		}
	}
};

使用demo:

void test_func(){
	Person *p = new Person("Mike");
	p->show();
	SmartPoint sm(p);
}

int main(){
	test_func();
	return 0;
}

输出:

name=Mike
SmartPoint带参构造
SmartPoint析构

6.3 重载指针运算符(*和->)

class SmartPoint{
private:
	Person *p;
public:
	SmartPoint(){ p=NULL; }
	SmartPoint(Person *p){
		cout << "SmartPoint带参构造" << endl;
		this->p=p;
	}
	~SmartPoint(){
		cout << "SmartPoint析构" << endl;
		if(p!=NULL){
			delete p;
			p = NULL;
		}
	}
	Person* operator->(){ return this->p; } // 重载->运算符号
	Person& operator*(){ return *(this->p); }  // 重载*运算符号
};

使用:

void test_func(){
	Person *p = new Person("Mike");
	SmartPoint sm(p);
	sm->show(); // 本质sm->->show(); 编译器简化为sm->show()
	(*sm).show(); // *sm相当于-----sm.operator*(),返回值是(*person)
}

int main(){
	test_func();
	return 0;
}

输出:

SmartPoint带参构造
name=Mike
name=Mike
SmartPoint析构
作者:|就良同学|,原文链接: https://www.cnblogs.com/lijiuliang/p/17405801.html

文章推荐

Kafka关键原理

Java的Atomic原子类

【Linux】(小白向)详解VirtualBox网络配置-配置Linux网络

Java中synchronized的优化

深入理解 python 虚拟机:破解核心魔法——反序列化 pyc 文件

【Redis】常用命令介绍

线上诊断神器-arthas基本应用

golang 必会之 pprof 监控系列(5) —— cpu 占用率 统计原理

第一部分:介绍 Spdlog 日志库

《操作系统导论》读书笔记1——CPU虚拟化,进程

前端安全问题——暴破登录

Java 生成二维码实战