C++核心编程 day05 运算符的重载
01. 数组类的封装
经过day04的内容,接下来需要强化锻炼一个关于类的封装的能力。现在需要定义一个数组的类,实现以下的一些功能:
- 数组的构造方法,实现无参构造(默认数组容量为100),有参构造(参数为数组的容量)、拷贝构造函数。
- 在数组的末尾插入一个元素。
- 根据位置下标设置数组某个位置上的数据。
- 获取数组中指定下标存储的数据。
- 获取数组的容量。
- 获取数组的当前大小。
根据上面的内容,我们将数组的定义和实现进行了分开,下面是实现代码:
Array.h
#pragma once
class Array
{
public:
Array(); // 默认构造,给100容量
Array(int capacity); // 有参构造
Array(const Array & array); // 拷贝构造
// 尾插法
void pushBack(int val);
// 根据位置设置数据
void setData(int pos, int val);
// 根据位置获取数据
int getData(int pos);
// 获取数组容量
int getCapacity();
// 获取数组大小
int getSize();
// 析构函数
~Array();
// 重载运算符[]
int &operator[](int index);
private:
int capacity; // 数组容量
int size; // 数组大小
int *pArray; // 堆区的数组指针
};
Array.cpp
#include "Array.h"
#include <iostream>
#include <cstdlib>
using namespace std;
// 默认构造,给100容量
Array::Array()
{
cout << "默认构造函数调用" << endl;
this->capacity = 100;
this->size = 0;
this->pArray = new int[this->capacity];
}
// 有参构造
Array::Array(int capacity)
{
cout << "有参构造函数调用" << endl;
this->capacity = capacity;
this->size = 0;
this->pArray = new int[this->capacity];
}
// 拷贝构造
Array::Array(const Array & array)
{
cout << "拷贝构造函数调用" << endl;
this->capacity = array.capacity;
this->size = array.size;
this->pArray = new int[this->capacity];
memcpy(this->pArray, array.pArray, sizeof(int)* this->size);
}
// 尾插法
void Array::pushBack(int val)
{
if (this->size == this->capacity)
{
return;
}
this->pArray[this->size ++] = val;
}
// 根据位置设置数据
void Array::setData(int pos, int val)
{
if (pos >= this->size)
{
return;
}
this->pArray[pos] = val;
}
// 根据位置获取数据
int Array::getData(int pos)
{
if (pos >= this->size)
{
return -1;
}
return this->pArray[pos];
}
// 获取数组容量
int Array::getCapacity()
{
return this->capacity;
}
// 获取数组大小
int Array::getSize()
{
return this->size;
}
// 析构函数
Array::~Array()
{
if (this->pArray != NULL)
{
cout << "析构函数调用" << endl;
delete[] this->pArray;
this->pArray = NULL;
}
}
// 重载运算符[]
int &Array::operator[](int index)
{
return this->pArray[index];
}
接下来对数组进行测试,测试代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "Array.h"
int main()
{
Array arr;
for (int i = 0; i < 10; i++)
{
arr.pushBack(i);
}
for (int i = 0; i < 10; i++)
{
cout << arr.getData(i) << " ";
}
cout << endl;
Array arr2(arr);
for (int i = 0; i < 10; i++)
{
cout << arr2.getData(i) << " ";
}
cout << endl;
arr.setData(3, 100);
cout << arr.getData(3) << endl;
cout << "数组容量为: " << arr.getCapacity() << endl;
cout << "数组大小为: " << arr.getSize() << endl;
for (int i = 0; i < 10; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
运行结果符合预期的要求。
02. 运算符重载
运算符重载就是对已有的运算符进行重新定义并且赋予另一种功能。比如在C语言中&
的作用是取地址符,而在C++中赋予了一种新的功能为引用。再比如<<
在C语言中只能表示二进制左移,而在C++中在cout
之后表示拼接。运算符的重载和定义一个普通函数的形式差不多,只不过函数名变成了operator + 运算符
。在这个函数中,函数的形式参数取决于两个因数,该运算符是一元运算符还是二元运算符?该函数是重载在类中作为成员函数还是全局函数。如果该运算符被定义在类中作为成员函数,则该类的成员默认算作是一个运算数,作为左操作数。
在运算符的重载之中,我们可以写全局函数,也可以写成为成员函数。但是这里有个问题的时候当我们写成全局函数的时候就无法去访问类中的私有权限的变量,因此我们一般将运算符重载的全局函数形式与友元进行使用。让全局函数作为某一个类的好朋友。例如我们会对<<
进行操作。一般来说cout
是位于变量的左边的,而我们使用成员函数来进行运算符的重载之后,由于第一个操作数是该类本身的对象,所以会导致我们的变量在cout
的左边,不适合我们平时的写法。因此我们需要使用全局函数去进行重载,这样第一个参数为ostream
类型的引用,第二个参数为需要传入的类的对象,这样就可以保证cout
位于对象的左边。但是这样会导致函数无法去访问某些私有变量,假设我们需要输出某些属性,这样我们就需要将该全局函数作为类的友元函数就可以访问私有属性了。
在C语言中的所有运算符,我们基本上都可以进行重载,但是当运算符的使用受到限制的时候,我们最好不能去重载。比如&&
或者||
之类的 ,由于这两个运算符会涉及到短路运算,而这种短路的效果是我们重载无法实现的,因此我们不去重载这两个运算符。重载的时候如果会改变十字的运算顺序,改变运算符的优先级,我哦们也是不可以进行重载的。下面给出C++中允许重载和不允许重载的运算符。如下:
01. 加号运算符重载
首先最简单的就是重载加法的运算符。例如这里有两个Person
的实例,里面有两个成员变量,而我们加起来可以得到一个Person
类的实例,其中的成员变量分别是另外两个成员变量之和。在这里我们可以选择是用全局函数重载加法运算符还是使用成员函数重载加法运算符。我们可以写出以下的两个重载的定义:
Person operator+(const Person &); // 作为成员函数重载,该类的示例作为左操作数,形式参数作为右操作数。
Person operator+(const Person &, const Person &); // 作为全局函数重载加号运算符,第一个形式参数作为第一个操作数,第二个形式参数作为第二个操作数。
当然这是两个Person
类的对象相加,如果我们需要使一个Person
类的对象加上一个int
类型的数据,使得结果是将Person
对象中的成员变量值增加相应的量,。此时我们可以写成以下的重载定义:
Person operator+(const int &); // 作为成员函数重载加号运算符,左操作数为该类的对象,右操作数是形式参数。
Person operator+(const Person &, const int &); // 作为全局函数重载加号运算符,其中左操作数为第一个形式参数,右操作数为第二个形式参数。
这就是加号运算符的重载,接下来我们给一个示例代码,代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
this->m_a = 0;
this->m_b = 0;
}
Person(int a, int b) :m_a(a), m_b(b) {}
利用成员函数实现加号运算符重载
//Person operator+(Person &p)
//{
// Person tmp;
// tmp.m_a = this->m_a + p.m_a;
// tmp.m_b = this->m_b + p.m_b;
// return tmp;
//}
int m_a;
int m_b;
};
// 利用全局函数实现加号运算符重载
Person operator+(Person &p1, Person &p2)
{
Person tmp;
tmp.m_a = p1.m_a + p2.m_a;
tmp.m_b = p1.m_b + p2.m_b;
return tmp;
}
Person operator+(Person &p, const int &num)
{
Person tmp;
tmp.m_a = p.m_a + num;
tmp.m_b = p.m_b + num;
return tmp;
}
int main()
{
Person p1;
Person p2(1, 3), p3(4, 3);
cout << "p2.m_a = " << p2.m_a << " p2.m_b = " << p2.m_b << endl;
cout << "p3.m_a = " << p3.m_a << " p3.m_b = " << p3.m_b << endl;
p1 = p2 + p3;
//p1 = p2.operator+(p3); // 成员函数本质
//p1 = operator+(p2, p3); // 全局函数本质
cout << "p1.m_a = " << p1.m_a << " p1.m_b = " << p1.m_b << endl;
p1 = p1 + 10;
cout << "p1.m_a = " << p1.m_a << " p1.m_b = " << p1.m_b << endl;
int a = 30;
p1 = p1 + a;
cout << "p1.m_a = " << p1.m_a << " p1.m_b = " << p1.m_b << endl;
system("pause");
return 0;
}
02. 左移运算符重载
这里我们才重载一下左移运算符,实现某个类能够使用cout
去进行输出。关于这里的注意事项我们在最前面已经提到过,这里就直接上代码和注释。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
friend ostream &operator<<(ostream &, Person &);
public:
Person(int a, int b):m_a(a), m_b(b) {}
利用成员函数重载左移运算符
//ostream& operator<<(ostream &cout)
//{
// // 输出会变成 p << cout; 而我们习惯于把cout放左边,因此采取全局函数重载
// cout << "m_a = " << this->m_a << ", m_b = " << this->m_b << endl;
// return cout;
//}
private:
int m_a;
int m_b;
};
// 利用全局函数实现左移运算符重载
ostream &operator<<(ostream &cout, Person &p)
{
cout << "m_a = " << p.m_a << ", m_b = " << p.m_b;
return cout;
}
int main()
{
Person p1(12, 43);
//p1.operator << (cout);
//p1 << cout;
cout << p1 << endl;
system("pause");
return 0;
}
03. 递增运算符重载
递增运算符为++
,递减运算符和递增运算符的重载是类似的,因此此处只说递增运算符。递增运算符我们分为前置运算符和后置运算符,那么我们怎么去区分究竟是重载了前置自增运算符还是后置自增运算符。这里的解决方法其实就是我们前面说的一个占位参数。假如有个自定义的MyInter
类,我们来看看这两个重载的声明:
// 前置递增
MyInter& operator++();
// 后置递增
MyInter operator++(int);
这里就是一个关于占位参数的使用。或许你会问第一个为什么是引用,而后置递增中我们使用的却是一个普通的变量作为返回值。我们来回忆一下C语言的一些语法。在C语言中我们执行++ ++a
是可以的,而我们执行a ++ ++
之后得到的a
或许就不是你想要的结果,假如a
的初始值为0
,那么前置自增运算符两次得到的结果就是2
,而两次后置自增运算符得到的结果就是1
了。其实无论后置递增中你写多少个,原来的变量都只会执行一次递增,因为后面的已经不是变量自身了,也就是我写成a ++ ++ ++ ++ ++ ++ ++
得到的最终的a
结果还是1
。因此我们才会前置写变量的引用,后置返回值为一个变量。接下来我们来看一个示例代码。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class MyInter
{
friend ostream &operator<<(ostream &, MyInter &);
public:
MyInter()
{
this->m_number = 0;
}
MyInter(int a) :m_number(a){}
// 重载前置++
MyInter& operator++()
{
this->m_number++;
return *this;
}
// 重载后置++
MyInter operator++(int)
{
// 记录初始状态
MyInter tmp = *this;
this->m_number++;
return tmp;
}
private:
int m_number;
};
ostream &operator<<(ostream &cout, MyInter &myInter)
{
cout << myInter.m_number;
return cout;
}
int main()
{
MyInter a = 10;
cout << a << endl; // 10
cout << a++ << endl; // 10
cout << a << endl; // 11
cout << ++a << endl; // 12
cout << a << endl; // 12
cout << ++ ++a << endl; // 14
cout << a << endl; //14
cout << a++ ++<< endl; // 14
cout << a << endl; // 15
system("pause");
return 0;
}
04. 指针运算符重载
指针运算符主要包括*
和->
,重载的作用主要在于智能指针的使用。也就是对象的释放全部交由智能指针来完成,用户不需要管理堆区内存的释放。智能指针的示例如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age) :m_age(age)
{
cout << "Person的有参构造函数调用" << endl;
}
void showAge()
{
cout << "年龄为: " << this->m_age << endl;
}
~Person()
{
cout << "Person的析构函数调用" << endl;
}
private:
int m_age;
};
class SmartPoint
{
public:
SmartPoint(Person *pPerson) :m_pPerson(pPerson)
{
cout << "SmartPoint的有参构造函数调用" << endl;
}
// 重载->运算符
Person *operator->()
{
return this->m_pPerson;
}
// 重载*运算符
Person& operator*()
{
return *this->m_pPerson;
}
~SmartPoint()
{
cout << "SmartPoint的析构函数调用" << endl;
if (this->m_pPerson != NULL)
{
delete this->m_pPerson;
this->m_pPerson = NULL;
}
}
private:
Person *m_pPerson;
};
int main()
{
// 利用智能指针,管理new出 来的Person的释放操作
SmartPoint sp(new Person(18));
// sp->m_pPerson->m_age
sp->showAge(); // 本质为 sp->->showAge(); 编译器简化为sp->showAge();
(*sp).showAge();
system("pause");
return 0;
}
05. 赋值运算符重载
在类的定义中,编译器会默认提供四个函数,分别是无参构造函数、析构函数、拷贝构造函数(值拷贝)、operator=
函数(值拷贝)。而这里的赋值运算符重载就是去重载=
。因为我们大部分时候创建的对象都是放在堆区上的。如果使用默认的赋值运算是简单的值拷贝。当我们要释放某一个变量的内存,则会出现重复释放同一块内存的情况,就会导致程序出错崩溃。赋值运算符重载的示例代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
// 编译器默认给一个类提供4个函数 默认构造、析构、拷贝构造(值拷贝)、operator=(值拷贝)
class Person
{
public:
Person(char *name, int age)
{
this->name = new char[strlen(name) + 1];
strcpy(this->name, name);
this->age = age;
}
// 重载 =
Person &operator=(const Person &p)
{
// 判断原来堆区是否有内容,有则释放
if (this->name != NULL)
{
delete[] this->name;
this->name = NULL;
}
this->name = new char[strlen(p.name) + 1];
strcpy(this->name, p.name);
this->age = p.age;
return *this;
}
// 拷贝构造
Person(const Person &p)
{
this->name = new char[strlen(p.name) + 1];
strcpy(this->name, p.name);
this->age = p.age;
}
// 析构函数
~Person()
{
if (this->name != NULL)
{
delete[] this->name;
this->name = NULL;
}
}
char *name;
int age;
};
ostream& operator<<(ostream &cout, Person &p)
{
cout << "姓名: " << p.name << " ,年龄: " << p.age;
return cout;
}
int main()
{
Person p1("迪杰斯特拉", 46);
Person p2("蘑古力", 43);
Person p3("", 0);
p3 = p1;
cout << "p1的信息=>" << p1 << endl;
cout << "p2的信息=>" << p2 << endl;
cout << "p3的信息=>" << p3 << endl;
p1 = p3 = p2;
cout << "p1的信息=>" << p1 << endl;
cout << "p2的信息=>" << p2 << endl;
cout << "p3的信息=>" << p3 << endl;
system("pause");
return 0;
}