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新特性模板的编写,有时间也会通过博客记录下面。其实写这几篇也是因为现在的模板太难看懂了,重新奠定加强基础。