08模板学习之自己写一个模板数组MyArray案例(等号重载为什么要返回引用)

08模板学习之自己写一个模板数组MyArray案例(等号重载为什么要返回引用)

1 案例代码
注意,因为是模板类的编写,所以直接写在同一文件,不必注意.h和.cpp分离时重载运算符的声明或者友元函数的声明。
文件命名为:MyArray.hpp。这里为了方便,直接把它塞进main.cpp了,如果想要使用的话分离开即可,但是大可不必,因为有Vector容器可以使用。

下面注释已经很清楚了,特别是PushBack函数中调用了等号操作符,它很清楚的让我们知道Vector这些容器PushBack时,容器的值都是拷贝份。

这里还需要注意的一点是,在重写等号重载时,必须返回引用,具体看下面的例子分析。
下面代码等号重载,加法函数,+=重载已经全部加上引用,自己要测试的话需要将相应的引用去掉。并且去掉count成员数据为完全正确的模板数组。

#include<iostream>
using namespace std;

template<class T>
class MyArray {
public:
	//有参构造
	MyArray<T>(int capacity) {
		this->mCapacity = capacity;
		this->mSize = 0;
		//申请内存
		this->pAddr = new T[this->mCapacity];

		this->count = 0;
	}
	//拷贝构造
	MyArray<T>(const MyArray<T>& arr) {
		this->mSize = arr.mSize;
		this->mCapacity = arr.mCapacity;

		//申请内存空间
		this->pAddr = new T[this->mCapacity];
		//数据拷贝
		for (int i = 0; i < this->mSize; i++) {
			this->pAddr[i] = arr.pAddr[i];
		}

		this->count = arr.count;
	}

	//等号重载,必须加引用
	MyArray<T>& operator=(const MyArray<T>& arr) {

		//1 判断是否自己给自己赋值,若是则直接返回
		if (this == &arr) {
			return *this;
		}

		//2 若之前不为空,必须先释放之前的内容
		if (this->pAddr != NULL) {
			delete[] this->pAddr;
		}

		// 3 赋值
		this->mSize = arr.mSize;
		this->mCapacity = arr.mCapacity;
		//申请内存空间
		this->pAddr = new T[this->mCapacity];
		//数据拷贝
		for (int i = 0; i < this->mSize; i++) {
			this->pAddr[i] = arr.pAddr[i];
		}
		this->count = arr.count;

		//4 返回引用自己
		return *this;
	}

	// 重载下标运算符(类内实现不需要声明template<calss T>)
	T& operator[](int index) {
		return this->pAddr[index];
	}

	//只能解决左值进数组,例如int a=10,marray.PushBack(a);右值时marray.PushBack(100);会报错
	void PushBack(T& data) {
		//判断容器中是否有位置
		if (this->mSize >= this->mCapacity) {
			return;
		}

		//调用拷贝构造,=号操作符
		//1. 对象元素必须能够被拷贝
		//2. 容器都是值寓意,而非引用寓意 向容器中放入元素,都是放入的元素的拷贝份
		//3  如果元素的成员有指针,注意深拷贝和浅拷贝问题

		//即上面的意思是:
		//实际上这里并不会调用拷贝构造,但是因为很多人在写等号重载时会直接将第3步变成调用拷贝函数,
		//所以把拷贝函数也考虑进来。
		//1 在下面的赋值语句中,PushBack是会调用等号操作符,所以等号操作符必须重写
		//2 在调用等号操作符时,是重新拷贝了一份新的数据放进数组(容器),所以数组中的元素都是拷贝份
		//3 如果拷贝的元素有指针,在写拷贝构造和重写等号时需要注意深拷贝和浅拷贝问题。
		this->pAddr[this->mSize] = data;
		this->mSize++;

	}

	//解决右值不能push进数组的函数接口,例如marray.PushBack(100);
	void PushBack(T&& data) {
		//判断容器中是否有位置
		if (this->mSize >= this->mCapacity) {
			return;
		}

		//这里同理。
		this->pAddr[this->mSize] = data;
		this->mSize++;

	}

	//用于测试连加时不加引用返回
	MyArray<T>& TestAdd(const MyArray<T>& arr) {

		this->count = this->count + arr.count;

		return *this;
	}

	//重载+=运算符,用于测试连加时不加引用返回
	MyArray<T>& operator+=(const MyArray<T>& arr) {

		this->count = this->count + arr.count;

		return *this;
	}

	~MyArray() {
		if (this->pAddr != NULL) {
			delete[] this->pAddr;
		}
	}
public:
	//一共可以容下多少个元素
	int mCapacity;
	//当前数组有多少元素
	int mSize;
	//保存数据的首地址
	T* pAddr;

	//用于临时测试连加,对数组并无作用
	int count;
};

void test01() {

	MyArray<int> marray(20);
	int a = 10, b = 20, c = 30, d = 40;

	//调用左值PushBack函数
	marray.PushBack(a);
	marray.PushBack(b);
	marray.PushBack(c);
	marray.PushBack(d);

	//调用右值PushBack函数
	marray.PushBack(100);
	marray.PushBack(200);
	marray.PushBack(300);

	for (int i = 0; i < marray.mSize; i++) {
		cout << marray[i] << " ";
	}
	cout << endl;
}

class Person {};

void test02() {

	Person p1, p2;

	//需要注意在PushBack中的等号重载
	MyArray<Person> arr(10);
	arr.PushBack(p1);
	arr.PushBack(p2);

}

//测试等号重载连等时和等号重载连加时.
void test03() {
	MyArray<int> m1(10);
	m1.PushBack(1);
	MyArray<int> m2(20);
	m2.PushBack(2);
	MyArray<int> m3(30);
	m3.PushBack(3);

	//连等从右往左开始运算
	//1) 连等时若返回值不加引用:编译器会创建新的匿名对象返回,而不是直接返回*this自己.
	//例如下面的m1=m2=m3,首先返回m2和m3运算后的m2匿名对象,然后再返回m1的匿名对象给自己m1.虽然现在并未出现问题,但也会浪费拷贝次数
	//m1 = m2 = m3;
	//输出Punshback进m1,m2,m3的各自第一个元素
	//printf("%d %d %d\n", *(m1.pAddr), *(m2.pAddr), *(m3.pAddr));//1 2 3

	//连加从左往右开始运算
	//2) 连加时若返回值不加引用:编译器会创建新的匿名对象返回,而不是直接返回*this自己.所以
	//导致每次都是匿名对象在调用等号重载连加,而本身的值只做了一次加法.
	//例如下面的过程,首先m1与m2先运算,m1.count=1+2等于3后返回匿名对象进行运算,匿名对象3+3=6而m1仍是3
	m1.count = 1;
	m2.count = 2;
	m3.count = 3;
	m1.TestAdd(m2).TestAdd(m3);

	//m1不加引用本身只会加一次,所以结果为3
	printf("%d\n", m1.count);
}

//测试+=重载运算符(不考虑指针情况下,有指针的情况下需要释放原来的内存并且开辟相加后的内存大小,不过实际应用基本用不到).
//测试目的1:看是否会调用等号重载->不会
//测试目的2:不返回引用时是否本身是否可以连加->可以连加,因为等号是从右往左开始运算,所以每次运算后
//返回匿名对象可以赋值给调用的对象,但是会增加拷贝次数.
//所以无论是等号重载,还是加法函数还是+=重载,都必须加上引用返回是最稳定的操作.
void test04() {

	MyArray<int> m1(10);
	m1.PushBack(1);
	MyArray<int> m2(20);
	m2.PushBack(2);
	MyArray<int> m3(30);
	m3.PushBack(3);
	m1.count = 1;
	m2.count = 2;
	m3.count = 3;

	//等号都是从右往左开始运算
	//首先先运算m2=m2+m3,然后返回m2的匿名对象与m1进行运算,最后将匿名对象每次运算的值赋给了m1,所以也是可以连加.
	//这里看到实际上能否连加与操作符的运算顺序有关(左<->右).
	m1 += m2 += m3;
	printf("%d %d %d\n", m1.count, m2.count, m3.count);//6 5 3
}

int main() {

	//test01();

	//test03();

	test04();

	return 0;
}

2 分析等号重载不加引用时的连等和连加函数操作
1)连等测试代码:看注释即可。

//测试等号重载连等时和等号重载连加时.
void test03() {
	MyArray<int> m1(10);
	m1.PushBack(1);
	MyArray<int> m2(20);
	m2.PushBack(2);
	MyArray<int> m3(30);
	m3.PushBack(3);
	
	//连等从右往左开始运算
	//1) 连等时若返回值不加引用:编译器会创建新的匿名对象返回,而不是直接返回*this自己.
	//例如下面的m1=m2=m3,首先返回m2和m3运算后的m2匿名对象,然后再返回m1的匿名对象给自己m1.虽然现在并未出现问题,但也会浪费拷贝次数
	m1 = m2 = m3;
	//输出Punshback进m1,m2,m3的各自第一个元素
	printf("%d %d %d\n", *(m1.pAddr), *(m2.pAddr), *(m3.pAddr));//3 3 3

}

结果:
在这里插入图片描述
总结:连等时调用等号重载不会出错,但会造成拷贝次数增加,浪费内存和时间。

2)连加函数的测试代码:这里提醒一下,由于运算顺序是从左往右,所以m1本身只做了一次连加,后面连加的对象是匿名对象,并不会重新赋值给m1,所以m1的值为3。

//测试等号重载连等时和等号重载连加时.
void test03() {
	MyArray<int> m1(10);
	m1.PushBack(1);
	MyArray<int> m2(20);
	m2.PushBack(2);
	MyArray<int> m3(30);
	m3.PushBack(3);

	//连等从右往左开始运算
	//1) 连等时若返回值不加引用:编译器会创建新的匿名对象返回,而不是直接返回*this自己.
	//例如下面的m1=m2=m3,首先返回m2和m3运算后的m2匿名对象,然后再返回m1的匿名对象给自己m1.虽然现在并未出现问题,但也会浪费拷贝次数
	//m1 = m2 = m3;
	//输出Punshback进m1,m2,m3的各自第一个元素
	//printf("%d %d %d\n", *(m1.pAddr), *(m2.pAddr), *(m3.pAddr));//1 2 3

	//连加从左往右开始运算
	//2) 连加时若返回值不加引用:编译器会创建新的匿名对象返回,而不是直接返回*this自己.所以
	//导致每次都是匿名对象在调用等号重载连加,而本身的值只做了一次加法.
	//例如下面的过程,首先m1与m2先运算,m1.count=1+2等于3后返回匿名对象进行运算,匿名对象3+3=6而m1仍是3
	m1.count = 1;
	m2.count = 2;
	m3.count = 3;
	m1.TestAdd(m2).TestAdd(m3);

	//m1不加引用本身只会加一次,所以结果为3
	printf("%d\n", m1.count);
}

结果:
在这里插入图片描述
总结:连加函数时会造成逻辑出错,必须增加引用返回。

3 测试+=重载运算符(不考虑指针情况下,有指针的情况下需要释放原来的内存并且开辟相加后的内存大小,不过实际应用基本用不到)

  • 测试目的1:看是否会调用等号重载->不会(调不调用等号重载在下面测试代码打个断点即可)。
  • 测试目的2:不返回引用时是否本身是否可以连加->可以连加,因为等号是从右往左开始运算,所以每次运算后返回匿名对象可以赋值给调用的对象,但是会增加拷贝次数。
void test04() {

	MyArray<int> m1(10);
	m1.PushBack(1);
	MyArray<int> m2(20);
	m2.PushBack(2);
	MyArray<int> m3(30);
	m3.PushBack(3);
	m1.count = 1;
	m2.count = 2;
	m3.count = 3;

	//等号都是从右往左开始运算
	//首先先运算m2=m2+m3,然后返回m2的匿名对象与m1进行运算,最后将匿名对象每次运算的值赋给了m1,所以也是可以连加.
	//这里看到实际上能否连加与操作符的运算顺序有关(左<->右).
	m1 += m2 += m3;
	printf("%d %d %d\n", m1.count, m2.count, m3.count);//6 5 3
}

结果:
在这里插入图片描述
总结:不会调用等号重载,并且可以连加,但会造成拷贝次数增加,浪费内存和时间。

4 总结上面

  • 1)连等时等号重载不加引用返回不会出错,但是会造成拷贝浪费。

  • 2)连加函数必须加上引用返回,防止增加连加函数时造成逻辑错误。

  • 3)+=重载不加引用返回不会出错,但是会造成拷贝浪费。这里注意一下:这里不重载+=运算符的话,自定义类对象无法使对象与对象作和运算,所以对象与对象只能做连等操作,并且上面连等也说了是从右往左运算,并不会造成逻辑出错。很多同学认为连等会造成逻辑出错是不对的,因为是连加函数才会。

  • 4)所以无论是等号重载,还是加法函数还是+=重载,都必须加上引用返回是最稳定的操作.

  • 注:逻辑出错是指连加后的值会赋给连加的对象。例如连加函数TestAdd,我们想要的结果应为6,却返回3,这就是逻辑出错。

模板的基础学习到此结束。后面将学习新的C++11,14,17新特性模板的编写,有时间也会通过博客记录下面。其实写这几篇也是因为现在的模板太难看懂了,重新奠定加强基础。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值