笔记会持续更新,有错误的地方欢迎指正,谢谢!
这一章其实很简单,目的只有一个:
让我们使用自定义的类像使用内置类型一样,运算符的操作可通过重载自定义。
基本概念
规定
- 重载运算符:函数名由关键字operator和其要定义的运算符号共同组成。
- 重载运算符函数的参数数量与该运算符作用的运算对象数量一样,例如对于+,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。
- 除了 operator()(重载的函数调用运算符) 之外,其他重载运算符不能有默认实参,主要还是要保持跟内置类型一致。
- 重载运算符的前提:至少含有一个类类型参数。
- 可以重载大部分已有的运算符,而且优先级也应和原来的运算符一样,讲道理还是为了跟内置类型保持一致。
上面规矩挺多,其实是为了让类类型与内置类型保持一致。
调用重载的运算符函数的方式
有两种,一种是普通的函数调用,一种是运算符调用。
例如我重载了+,就是说我有一个operator+函数,那我有两种方式调用:
data1 + data2; //推荐这种
operator+(data1, data2);
某些运算符不应该被重载
有些运算对象无法保留求值顺序,所以不要重载它们,有&&和||。
不重载逗号和取地址运算符:因为C++已经定义了这两种运算符用于类类型的含义,相当于C++已经帮我们重载好了,我们就不要去添乱了。
选择作为成员函数还是非成员函数
大部分最好作为类的成员函数,那么它的左侧运算对象必须是运算符所属类的一个对象。另外,具有对称性的通常设计为普通非成员函数。
string s = "hey";///
string a = s + "a"; //对
string b = "a" + s; //也对,因为string把+定义为非成员函数,至少一个是string类型也满足。
string c = "x" + "y"; //错,两个对象都不是string类型,"x"和"y"都为char数组。
重载各个运算符
重载输入输出运算符
重载输出运算符
为老朋友Sales_data写个输出运算符:
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " ;
return os;
}
该函数返回一个ostream的引用。比较疑惑的可能是第一个参数,它是一个非const的引用,非const是因为向流写入内容会改变其状态;引用是因为我们无法拷贝ostream对象。
输入输出运算符必为非成员函数
为什么这么强行规定呢,我们来看看在实际调用的时候我们是怎么用的:
cout << a ; //a是一个Sales_data对象
我们假设<<是成员函数,它是二元运算符,所以它的左侧对象必须是类类型,就是说cout得是Sales_data类,显然不对!所以,只能把<<声明为非成员函数。
重载输入运算符
我们还是帮Sales_data写:
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> item.units_sold >> price;
if(is) //检查是否输入成功
{
item.revenue = item.units_sold * price;
}
else
{
item = Sales_data(); //输入失败,调用默认构造函数初始化
}
return is;
}
重载算术和关系运算符
通常,我们把算术和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换;我们还会把形参定义为常量引用,因为这些运算符不会改变运算对象的状态:
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //因为我不去修改lhs和rhs,所以要搞个sum变量出来
sum += rhs; //这里调用的+=也是重载的运算符,后面会定义它的
return sum;
}
相等运算符
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold() == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
关系运算符
通常关联容器和一些算法(例如排序)会用到小于运算符,所以定义operator<会比较有用,如果类同时定义了==运算符,则要与它保持一致,例如两个对象是!=的,那么一个对象应该<另一个。
赋值运算符
我们已经学了拷贝赋值和移动赋值了,它们是把类的一个对象赋值给该类的另一个对象,其实,我们还可以让等号右边不是该类对象,比如:
vector<string> v;
v = {"a", "b"};
我们就来重载这个,把这种赋值方式带到我们的StrVec类中:
class StrVec
{
public:
StrVec &operator=(std::initializer_list<string> il)
{
auto data = lloc_n_copy(il.begin(), il.end()); ///分配刚好的内存并拷贝
free(); //释放this原有的空间
elements = data.first; //头指针
first_free = cap = data.second; //尾指针
return *this;
}
};
复合赋值运算符
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
下标运算符[]
表示容器的类通常可以通过元素在容器中的位置来访问元素,这些类都会定义下标运算符operator[]
我们最好同时定义下标运算符的常量版本和非常量版本,这样的话,当作用于一个常量对象时,下标运算符返回常量引用,以保证我们无法修改它:
class StrVec
{
public:
string& operator[](size_t n)
{
return elements[n];
}
const string& operator[](size_t n) const
//第二个const的作用:任何不会修改数据成员的函数都应该声明为const类型。
{
return elements[n];
}
private:
string *elements; //指向首元素的指针
};
递增和递减运算符
迭代器中我们经常通过这两个运算符来移动迭代器,因为它们很可能改变所操作对象的状态,所以我们一般将其设定为成员函数。
递增递减有前置和后置版本,我们先介绍前置
定义前置
我们用以前的一个类StrBlobStr:
class StrBlobPtr
{
public:
StrBlobPtr& operator++(); //前置
StrBlobPtr& operator--();
};
//check函数的第一个参数小于vector大小,则正常返回;到达尾部就抛出异常
StrBlobPtr& StrBlobPtr::operator++()
{
check(curr, "已经到尾了"); //如果已经到了尾后位置,就无法递增
++curr; //后移
return *this;
}
StrBlobPtr& StrBlobPtr::operator--()
{
--curr;
check(curr, "首元素无法递减");
//curr是0的话,--会产生一个很大的值size_t,肯定大于vector
return *this;
}
区分前后置运算符的方法
我们为了实现重载,在后置函数中强行加上一个没用的形参:
class StrBlobPtr
{
public:
StrBlobPtr operator++(int); //后置
StrBlobPtr operator--(int);
};
定义后置
对于后置版本,我们要记录对象的状态,因为它还有用,所以我们返回的是原值,而不是递增/递减后的值(为了与内置版本一致,我们返回的是值而不是引用):
StrBlobPtr StrBlobPtr::operator++(int)
{
StrBlobPtr ret = *this; //记录原值
++*this; //调用了前置版本,已经包含了错误检测
return ret;
}
StrBlobPtr StrBlobPtr::operator--(int)
{
StrBlobPtr ret = *this; //记录原值
--*this;
return ret;
}
调用前置和后置运算符
StrBlobPtr a1 = {"a", "b"};
StrBlobPtr p(a1);
p.operator++(0); //调用后置
p.operator++(); //调用前置
补充:++i和i++的区别
请见我的另一篇博客:
++i和i++的区别(C++)
http://blog.csdn.net/billcyj/article/details/78883278
成员访问运算符
在迭代器类和智能指针类常用到解引用*和箭头->,我们来为StrBlobPtr类添加:
class StrBlob
{
public:
string operator*() const //与乘法是重载关系,参数不同
{
auto p = check(curr, "超范围了");
return (*p)[curr];
}
string* operator->() const
{
return & this->opertaor*(); //调用解引用*来实现
}
};
我们来使用下,就会发现毫无违和感了:
StrBlob a1 = {"a", "b", "c"};
StrBlob p(a1);
*p = "xyz";
cout << p->size() << endl; //3