当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。
重载输出运算符<<
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为无法直接复制一个ostream对象。第二个形参一般来说是一个常量的引用,这样可以避免复制实参。
输入输出运算符必须是非成员函数。因此,以下两段代码仅仅展示了函数重载为成员函数和重载为普通函数的示例。
运算符作为类成员:
#include <iostream>
#include <string>
class Cisbn
{
public:
Cisbn() = default;
Cisbn(std::string str1, int price) :m_name(str1), m_price(price){ ; }
~Cisbn() = default;
std::ostream & operator<<(std::ostream & os); // 将<<重载为成员函数(实际禁止这种用法)
std::string getName() { return this->m_name; }
int getPrice() { return this->m_price; }
Cisbn & operator+(const Cisbn &isbn) {
this->m_name += " " + isbn.m_name;
this->m_price += isbn.m_price;
return *this;
}
private:
std::string m_name;
int m_price;
};
int main() {
Cisbn aisbn("Primer", 89);
Cisbn bisbn("C++", 11);
aisbn.operator+(bisbn);
/* 下面两句调用时等价的,从第二种调用方式可看出为啥要禁止<<重载为成员函数了 */
aisbn.operator<<(std::cout);
// aisbn<<(std::cout);
system("pause");
}
std::ostream & Cisbn::operator<<(std::ostream & os)
{
os << this->getName() << " : " << this->getPrice() << std::endl;
return os;
}
运算符作为普通函数:
#include <iostream>
#include <string>
class Cisbn
{
public:
Cisbn() = default;
Cisbn(std::string str1, int price) :m_name(str1), m_price(price) { ; }
~Cisbn() = default;
Cisbn & operator+(const Cisbn &isbn) {
this->m_name += " " + isbn.m_name;
this->m_price += isbn.m_price;
return *this;
}
std::string getName() const { return this->m_name; }
int getPrice() const { return this->m_price; }
private:
std::string m_name;
int m_price;
};
/* 重载输出运算符<<作为普通函数 */
std::ostream & operator<<(std::ostream & os, const Cisbn & acisbn)
{
os << acisbn.getName() << " : " << acisbn.getPrice() << std::endl;
return os;
}
int main() {
Cisbn aisbn("Primer", 89);
Cisbn bisbn("C++", 11);
aisbn.operator+(bisbn);
operator<<(std::cout, aisbn);
system("pause");
}
上面两段代码运行的结果都是:Primer C++ : 100
。
重载输入运算符>>
通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
#include <iostream>
#include <sstream> // istringstream
class Cisbn
{
public:
Cisbn(){m_name = "";m_price = 0;}
Cisbn(std::string astr, int price) : m_name(astr), m_price(price) {}
~Cisbn() = default;
std::string getName() { return m_name; }
int getPrice() { return m_price; }
private:
std::string m_name;
int m_price;
friend std::istream & operator >> (std::istream & in, Cisbn & acisbn);
};
std::istream & operator >> (std::istream & in, Cisbn & acisbn) {
in >> acisbn.m_name >> acisbn.m_price;
// 检查输入是否成功,如果输入失败则对象被赋予默认的状态
if (!in)
{
acisbn.m_name = "";
acisbn.m_price = 0;
}
// 此处可以有一些使用acisbn数据成员的操作
return in;
/* 在该函数中我们没有逐个检查每个读取操作,
而是等读取了所有数据后赶在使用acisbn的数据之前一次性检查。 */
}
int main() {
Cisbn abook("Primer", 89);
std::cout << abook.getName() << " : " << abook.getPrice() << std::endl;
std::istringstream newbook("C++ 11");
operator >> (newbook, abook);
std::cout << abook.getName() << " : " << abook.getPrice() << std::endl;
system("pause");
}
程序运行的结果是:
Primer : 89
C++ : 11
重载前(后)置递增、递减运算符>>
定义递增和递减运算符的类应该同时定义前置和后置版本。这些运算符通常应该被定义成类的成员。
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用。
为了解决前后置递增递减使用同一个符号在调用时无法区分的的问题,后置版本接受一个额外的(不被使用的)(必须是)int
类型的形参。当我们使用后置运算符时,如果是显式地调用后置运算符则为形参提供一个值为整数(例如0)的实参即可,如果是隐式调用,则编译器会自动地为这个形参提供一个值为0的实参。
#include <iostream>
class OverLoad
{
public:
OverLoad(int i) :m_a(i) {};
~OverLoad()=default;
/* 前置递增、递减 */
OverLoad & operator++() {
m_a++;
return *this;
}
OverLoad & operator--() {
m_a--;
return *this;
}
/* 后置递增、递减 */
OverLoad operator++(int) {
OverLoad ret = *this;
++(*this);
return ret;
}
OverLoad operator--(int) {
OverLoad ret = *this;
--(*this);
return ret;
}
int getValue() {
return m_a;
}
private:
int m_a;
};
int main() {
OverLoad aol(4);
std::cout << "原始值:" << aol.getValue() << std::endl; //4
/* 下面两句等价,调用前置运算符的时候不要加参数。第一个是显示调用,第二个是隐式调用 */
std::cout << "前置递增的返回值:" << (aol.operator++()).getValue() << std::endl; // 5
//std::cout << "前置递增的返回值:" << (++aol).getValue() << std::endl; // 5
std::cout << "前置递增后的值:" << aol.getValue() << std::endl; // 5
/* 下面两句等价,注意,在使用函数法调用后置递增、递减运算符的时候必须加参数,
否则调用的是前置运算符。第一个是显示调用,第二个是隐式调用 */
std::cout << "后置递增的返回值:" << aol.operator++(0).getValue() << std::endl; // 5
//std::cout << "后置递增的返回值:" << aol++.getValue() << std::endl; // 5
std::cout << "后置递增的值:" << aol.getValue() << std::endl; // 6
std::cout << "后置递减的返回值:" << aol--.getValue() << std::endl; // 6
std::cout << "后置递减的值:" << aol.getValue() << std::endl; // 5
std::cout << "前置递减的返回值:" << (--aol).getValue() << std::endl; // 4
std::cout << "前置递减后的值:" << aol.getValue() << std::endl; // 4
}
成员访问运算符
在迭代器及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)。
我们可以使用operator*
完成任何完成任何我们指定的操作(虽然有些用法不太好)。但是,箭头运算符则不能这样,箭头运算符永远不能丢掉成员访问这个最基本的含义。当我们重载箭头运算符时,可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变。
示例:
#include <iostream>
#include <memory>
#include <vector>
#include <initializer_list>
#include <exception> // std::out_of_range
#include <string>
class StrBlobPtr;
class StrBlob
{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob():m_data(std::make_shared<std::vector<std::string>>()){}
StrBlob(std::initializer_list<std::string>il) :
m_data(std::make_shared<std::vector<std::string>>(il)) {}
~StrBlob()=default;
size_type size() const{
return m_data->size();
}
bool empty() const {
return m_data->empty();
}
std::string & front() {
check(0, "front on empty StrBlob");
return m_data->front();
}
std::string &back() {
check(0, "back on empty StrBlob");
return m_data->back();
}
void push_back(const std::string &str) {
m_data->push_back(str);
}
void pop_back() {
check(0, "pop_back on empty StrBlob");
return m_data->pop_back();
}
private:
std::shared_ptr<std::vector<std::string>> m_data;
void check(size_type i, const std::string &msg) const {
if (i >=m_data->size()) {
throw std::out_of_range(msg);
}
}
friend StrBlobPtr;
};
class StrBlobPtr
{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlobPtr() :m_curr(0) {}
StrBlobPtr(StrBlob &asb,size_type sz=0):m_wptr(asb.m_data),m_curr(sz){}
~StrBlobPtr()=default;
/* 解引用运算符 */
std::string &operator*() const {
auto it = check(m_curr, "dereference past end");
return (*it)[m_curr]; // *it是对象所指的vector
}
/* 箭头运算符 */
std::string * operator->() const {
return &(this->operator*()); // 将实际工作委托给解引用运算符
}
private:
std::weak_ptr<std::vector<std::string>> m_wptr;
size_type m_curr;
std::shared_ptr<std::vector<std::string>> check(size_type i, const std::string &msg) const{
auto ret = m_wptr.lock();
if (!ret) {
throw std::runtime_error("unbound StrBlobPtr");
}
if (i >= ret->size()) {
throw std::out_of_range(msg);
}
return ret;
}
};
int main() {
StrBlob asb({ "Hisi","OmniVision","Ambarella" });//
StrBlobPtr asbp(asb, 0);
*asbp = "IntelliVision"; //*asbp返回asb第一个元素的引用,并将其重新赋值
std::cout << asbp->size() << std::endl; // asbp->返回asb首元素地址
// 输出结果为13,即"IntelliVision"的长度
}
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。此时,该类的对象称作函数对象(function object)。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。
#include <iostream>
#include <string>
class Cisbn
{
public:
Cisbn(std::string str, float p):m_name(str),m_price(p){}
~Cisbn()=default;
void operator()() {
std::cout << m_name << " : " << m_price << std::endl;
}
private:
std::string m_name;
float m_price;
};
int main() {
Cisbn acisbn("Primer C++", 104.6);
acisbn(); // 运行结果,输出:Primer C++ : 104.6
}
标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符合逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。这些类都被定义成模板的形式,可以为其指定具体的应用类型,这里的类型即调用运算符的形参类型。例如,std::plus<std::string>
是对std::string
类型的对象进行加法运算;std::plus<Sales_data>
对Sales_data对象执行加法运算,等等。
示例:
#include <iostream>
#include <functional>
int main() {
std::plus<int> intAdd;
std::negate<int> intNegate;
auto ret = intAdd(1, intNegate(2));
std::cout << ret << std::endl; // 输出:-1
}
可调用对象、标准库function类型
C++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象、重载函数调用运算符的类。
不同类型的可调用对象有可能共享同一种调用形式
(call signature)。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:int (int,int)
是一个函数类型,它接受两个int、返回一个int。
我们可以构建一个函数表
(function table)来存储这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数。
示例:
#include <iostream>
#include <functional>
#include <map>
#include <string>
/* 重载函数调用运算符 */
class Cadd
{
public:
Cadd()=default;
~Cadd()=default;
/* 加法 */
int operator()(int a, int b) {
return a + b;
}
};
/* lambda表达式,减法 */
auto lamMinus = [](int a, int b) {return a - b; };
/* 函数,乘法 */
int multipy(int a, int b) {
return a * b;
}
int main() {
std::map<std::string, std::function<int(int, int)>> mapfunc;
mapfunc.insert({ "*",multipy }); // 函数指针
mapfunc.insert({ "/",std::divides<int>() }); // 标准库函数对象
mapfunc.insert({ "+",Cadd() }); // 用户自定义函数对象
mapfunc.insert({ "-",lamMinus }); // 命名了的lambda对象
mapfunc.insert({ "mod",[](int a,int b) {return a % b; } }); // 未命名的lambda
int a = 9;
int b = 5;
std::cout << "mapfunc[\"+\"](a, b): " << mapfunc["+"](a, b) << std::endl; // 14
std::cout << "mapfunc[\"-\"](a, b): " << mapfunc["-"](a, b) << std::endl; // 4
std::cout << "mapfunc[\"*\"](a, b): " << mapfunc["*"](a, b) << std::endl; // 45
std::cout << "mapfunc[\"mod\"](a, b): " << mapfunc["mod"](a, b) << std::endl; // 4
std::cout << "mapfunc[\"/\"](a, b): " << mapfunc["/"](a, b) << std::endl; // 1
}
类型转换运算符
类型转换运算符(conversion operator)是类的一种特殊成员函数,负责将一个类类型的值转换成其它类型。类型转换函数的一般形式如下:operator type() const
,其中,type
表示要将类类型转换成的类型。类型转换运算符既没有显式的返回类型,没有形参,而且必须定义为类的成员函数。类型转换运算符通常不应该改变转换对象的内容,因此,类型转换运算符一般被定义为const成员。
类似为了防止将其它类型隐式转换为类类型,在构造函数前面加上explicit
关键字一样,为了防止将类类型隐式转换为其它类型,也可以在类型转换符前面加explicit
关键字,使程序员在使用时知道自己在干什么。
#include <iostream>
#include <string>
class Coper
{
public:
/* 禁止其它类型隐式转换为类类型Coper */
explicit Coper(std::string astr):m_str(astr) {}
~Coper()=default;
std::string getValue() {
return m_str;
}
/* 重载类型转换运算符,将Coper类型转换为std::string类型,必须显示转换 */
explicit operator std::string() const {
return m_str;
}
private:
std::string m_str;
};
int main() {
Coper acoper("Seagate");
std::cout << acoper.getValue() << std::endl; // Seagate
acoper = static_cast<Coper>("WesternDigital");
std::cout << acoper.getValue() << std::endl; // WesternDigital
std::cout << static_cast<std::string>(acoper) + " & OmniVision" << std::endl; // WesternDigital & OmniVision
}
隐式地调用显式的类型转换运算符: