文章目录
内置类型运算都有基本的运算符来支持,而我们想要实现类类型的一些运算,就需要自己重载运算符。
一、基本概念
重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和后面要定义的运算符号共同组成。和其他函数一样,也有返回类型,参数列表和函数体。
【Note】:
1)当一个重载的运算符是成员函数时,this绑定到左侧的对象,成员运算符函数的参数比运算对象的参数少一个。比如+重载成成员函数,那么它的参数只能有一个,默认类对象this本身是一个参数。
2)不能重载的运算符 : :: .* . ?:
3)不应该被重载的运算符: 逻辑运算符,逗号运算符,取地址运算符。 重载逻辑运算符的时候不支持短路求值和求值顺序的属性,逗号和取地址本身有内置含义。尽量明智地使用运算符重载。
4)我们重载运算符时最好和内置的形成映射最好,也就是类似程度。比如IO运算符,运算符(若定义也该定义!=)等等。
选择作为成员函数还是非成员函数:
(1)成员函数:
=,[ ],( ), ->,
符合赋值+=,-=… ,
++,–,(解引用) 改变对象的状态或与给定类型密切相关的运算符。
(2)非成员函数:
算数+,-,,/…
相等性 ,!=
关系 >,<,>=,<=…
位运算符 ^, |, &
可以看一个运算符左右两侧的对象是否可以互换位置,不能互换位置则一般为成员函数,可以互换位置则一般为非成员函数。
二、输入输出运算符
1、重载输出运算符<<
输出运算符应定义为非成员函数,因为要读写类的非共有数据,所以要定义为友元。且返回值为std::ostream&, 参数为std::ostream&和类对象的引用。
【Note】:
1)通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符我们应减少对输出格式的控制。
2、重载输入运算符>>
输入运算符和输出运算符格式上类似,也是非成员函数,返回输入流引用(流不能拷贝)。当流含有错误类型的数据时读取操作可能失败,读取数据到文件末尾或遇到其他流错误也会失败。
【Note】:
1)参数是输入流引用和类对象和输出运算符不同的是,输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
2)不需要逐个检查,只在末尾检查即可,当读取操作发生错误时,输入运算符应该负责从错误中恢复。
一些输入运算符可能需要更多的数据验证工作。
friend ostream &operator<<(ostream &out, MyString &s);
friend istream &operator>>(istream &in, MyString &s);
ostream &operator<<(ostream &out, MyString &s)
{
out << s.m_p;
return out;
}
istream &operator>>(istream &in, MyString &s)
{
in >> s.m_p;
return in;
}
三、算数和关系运算符
我们把算数和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换,一般不需要改变参数状态,所以都是常量引用。
如果定义了算数运算符,则他一般也会定义一个对应的复合赋值运算符,最有效的方式是使用复合赋值来定义算数运算符。
1、相等运算符
依次比较每个成员是否相等。
【Note】:
1)如果类定义了operator==,那么类也应该定义operator!=。
2)相等运算符和不相等运算符中的一个应该把工作委托给另外一个。
3)如果某个类逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使得用户更加容易的使用标准库算法(部分标准库算法必须要有==支持)。
4)shared_ptr 和 unique_ptr 都用get( )返回的指针来比较是否指向同一个对象
5)weak_ptr 要先使用lock( )获取shared_ptr 然后在用get( )来比较地址从而判定是否指向同一个对象。
2、关系运算符
定义了相等运算符的类通常也应该定义关系运算符,因为关联容器和一些算法要用到小于运算符。所以operator<会比较有用。
【Note】:
1)如果存在唯一一种逻辑可靠的<定义,则应该为这个类定义<运算符,如果类同时还包含==,则当且仅当<的定义和产生的结果一致时才定义<运算符。
不要轻易定义<运算符,如果<和比较的逻辑相同(也就是比较的成员相同)才定义<运算符。一些情况我们必须定义<运算符,比如类对象需要存在map或set中的时候等等。
bool operator==(const char *p) const;
bool operator==(const MyString &s) const;
int operator<(const char *p);
int operator>(const MyString &s);
bool MyString::operator==(const char *p) const
{
if(p == NULL)
{
return m_len ==0 ? true : false;
}
else
{
return m_len == strlen(p) ? !strcmp(m_p,p) : false;
}
}
bool MyString::operator==(const MyString &s) const
{
return *this == s ? true : false;
}
//重载大于、小于。
int MyString::operator<(const char *p)
{
return strcmp(m_p,p);
}
int MyString::operator>(const MyString &s)
{
return strcmp(m_p,s.m_p);
}
四、赋值运算符
类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。和拷贝赋值运算符及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,不同之处是无需检查自赋值。销毁原资源,更新资源。
复合赋值运算符不非得是类的成员,不过我们还是倾向于把包括复合赋值在内的所有赋值运算符都定义在类的内部。
为了与内置类型的复合赋值保持一直,类中的复合赋值运算符也要返回其左侧运算对象的引用。
【Note】:
1)我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
2)赋值运算符必须定义成类的成员,复合赋值运算符通常也应该这样做,这两类运算符都应该返回对象的引用。
MyString &operator=(const char *p);
MyString &operator=(const MyString &s);
MyString &operator+=(const char *p);
MyString &operator+=(const MyString &s);
//重载赋值操作符。
MyString &MyString::operator=(const char *p)
{
if(m_p != NULL)
{
delete [] m_p;
m_len = 0;
}
if(p == NULL)
{
m_len = 0;
m_p = new char[1];
strcpy(m_p,"");
}
else
{
m_len = strlen(p);
m_p = new char[m_len+1];
strcpy(m_p,p);
}
return *this;//返回引用。
}
//重载赋值操作符。
MyString &MyString::operator=(const MyString &s)
{
if(m_p != NULL)
{
delete [] m_p;
m_len = 0;
}
else
{
m_len = s.m_len;
m_p = new char[m_len+1];
strcpy(m_p,s.m_p);
}
}
//重载加号运算符。
MyString &MyString::operator+=(const char *p)
{
strcat(m_p,p);
return *this;
}
MyString &MyString::operator+=(const MyString &s)
{
strcat(m_p,s.m_p);
return *this;
}
五、下标运算符
表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[ ]。
【Note】:
1)下标运算符必须是成员函数。
2)如果一个类包含下标运算符,则它通常会有两个版本:一个返回普通引用,另一个是类的常量成员并返回常量引用。
下标运算符通常以所访问元素的引用作为返回值。可以作为左值或右值。
我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会修改返回值。
char &operator[](int index) const;
//重载下标操作符。
char &MyString::operator[](int index) const
{
return m_p[index];
}
六、递增递减运算符
【Note】:
1)定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符应该被定义为类的成员。
2)为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
3)为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非一个引用。
区分前置运算符和后置运算符:后置版本提供一个额外的不被使用的int类型的参数,使用后置运算符时,编译器为这个形参提供一个值为0的实参。这个形参的唯一作用就是区分前置版本和后置版本。对于后置版本来说,在递增或递减之前首先需要记录对象的状态。
后置版本里可以调用前置版本。前置版本在递增之前要判断是否到达末尾,前置版本递减要在递减之后判断是否出界。如果我们要通过函数调用的方式调用后置版本,则必须为他整型参数传递一个值,尽管我们不使用这个值。
七 、成员访问运算符
解引用运算符检查是否在范围内,然后返回所指元素的一个引用,箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果的地址。
【Note】:
1)箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此。
2)重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
八、函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象,因为这个类同时也能存储状态,所以与普通函数相比更加具有灵活性。
#include <iostream>
struct absInt
{
int operator()(int val)const
{
return val < 0 ? -val : val;
}
};
int main()
{
int val = -42;
absInt t;
std::cout << t(val) << std::endl; //t是一个对象而非函数
}
【Note】:
1)函数调用运算符必须是成函数。一个类可以员定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类被称为函数对象,因为可以调用这种对象,所以我们说这些对象的行为像函数一样。
函数对象类除了operator()之外也可以包含其他成员,通常函数对象类包含一些数据成员,这些成员用来定制调用运算符中的操作。
#include <iostream>
#include <string>
class PrintString
{
public:
PrintString(std::ostream &o = std::cout, char t = ' '):
os(o), c(t) { }
void operator()(const std::string &s)const //借用辅助工具来完成函数的操作
{
os << s << c;
}
private: //private成员可以用来保存“辅助”工具
std::ostream &os;
char c;
};
int main()
{
PrintString ps;
std::string s = "abc";
ps(s);
}
函数对象比一般函数灵活就是它可以让另完成函数所需要的辅助成员成为自己的类成员。和lambda类似,函数对象常常作为泛型算法的实参。
1、lambda表达式的原理
当定义一个lambda时,编译器生成一个与lambda对应的新的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象。
其实当我们编写一个lambda后,编译器将该表达式翻译成一个匿名的类,并且重载了函数调用运算符即括号运算符。
[](const string &lhs, const string &rhs)
{ return lhs.size() < rhs.size(); }
//上述等价于:
class shrotstring
{
public:
bool operator()(const string &lhs, const string &rhs)const
{ return lhs.size() < rhs.size(); }
};
对于捕获变量的lambda表达式来说,编译器在创建类的时候,通过成员函数的形式保存了需要捕获的变量。
lambda产生的类不含默认构造函数,赋值运算符及默认析构函数:它是否含有默认的拷贝/移动构造函数则通常视捕获的数据成员类型而定。
2、标准库定义的函数对象
头文件#include <functional>
,标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命令操作的调用运算符。
//算术:
plus<Type>、minus<Type>、multiplies<Type>、divides<Type>
modulus<Type>、negate<Type>
//关系:
equal_to<Type>、not_equal_to<Type>、greater<Type>
greater_equal<Type>、less<Type>、less_equal<Type>
//逻辑运算符:
logical_and<<#class _Tp#>>、logical_not<<#class _Tp#>>、logical_or<<#class _Tp#>>
//我们也可以在算法中使用标准库函数对象。
sort(svec.begin(), svec.end(), std::greater<std::string>());
// 而且标准库规定其函数对象对于指针同样适用。排序的同样是string。
sort(svec.begin(), svec.end(), std::less<std::string *>());
3、可调用对象与function
c++中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象,以及函数对象(重载了函数调用运算符的类)。
和其他对象一样,可调用的对象也有类型。然而两个不同的可调用对象确有可能享用同一种调用形式。
int add(int a, int b) { return a+b; }
auto t = [](int a, int b) { return a+b; }
class A
{
int operator()(int a, int b)
{
return a+b;
}
}
....
类型都是 int(int, int)
但实际操作比如我们想vector<int(*)(int, int)>
来保存他们是不行的,只能保存第一个,因为他们毕竟是不同的对象!
4、标准库function类型:
我们可以使用一个名为function的新的标准库类型来解决上面的问题。头文件#include <functional>
#include <bits/stdc++.h>
using namespace std;
//普通函数。
int add(int i, int j){ return i + j; }
int main(int argc, char const *argv[])
{
/*********************
*标准库定义的函数对象。
**********************/
vector<string> svec{"abc","bcd"};
sort(begin(svec),end(svec),greater<string>());
for (const auto s : svec)
{
cout << s << endl;
}
/***************
*标准库function。
****************/
//lambda对象类。
auto mod = [] (int i, int j){ return i % j; };
//函数对象类。
struct divide
{
int operator()(int i, int j){ return i / j; }
};
map<string, function<int(int ,int)>> binops =
{
{"+",add},
{"-",minus<int>()},
{"*",[] (int i, int j){ return i*j; }},
{"/",divide()},
{"%",mod}
};
vector<int> ivec
{
binops["+"](4,2),
binops["-"](4,2),
binops["*"](4,2),
binops["/"](4,2),
binops["%"](4,2)
};
for (const auto i : ivec)
{
cout << i << endl;
}
system("pause");
return 0;
}
【Note】:
1)当我们有函数重载对象时,不能直接将相同的函数名字放入function,必须通过函数指针或者lambda来消除二义性。
九、重载,类型转换与运算符
我们能定义类类型之间的转换,转换构造函数和类型转换运算符共同定义了类类型转换。
1、类型转换运算符
类的一种特殊的成员函数,负责将一个类类型的值转换成其他类型。
operator type( )const;
class SmallInt
{
friend ostream &operator<<(ostream &out, SmallInt &s);
public:
SmallInt(int i = 0):val(i)
{
if(i < 0 || i > 255)
throw out_of_range("bad value");
}
SmallInt &operator=(size_t p);
explicit operator int() const { return val; }
~SmallInt(){}
private:
size_t val;
};
type表示某种类型。但是该类型必须能作为返回类型。类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数,类型转换通常不应该改变待转换的对象。
【Note】:
1)一个类型转换函数必须是类的成员函数,它不能声明返回类型,也没有形参,类型转换函数通常为const。
2)向bool的类型转换通常用在条件部分,因此operator bool 一般定义为explicit 的。
3)应该避免过度的使用类型转换函数。
(1)类型转换函数可能产生意外结果
类通常很少定义类型转换运算符,但是定义像bool类型转换还是比较常见。
(2)c++11 显示类型转换运算符
explicit operator type( ) const { };
static_cast<type>(name);
当类型转换运算符是显式的我们才能只能类型转换。不过必须通过显式的强制类型转换才行。但是存在一个例外:既当如果表达式被用作 条件,则编译器会将显示的类型转换自动应用于它。包括while, if, do, for语句头条件表达式,(!, ||, &&)的运算对象, (? :)条件表达式。 流对象转换bool也是因为标准库定义了流向bool显式类型转化。
2、避免有二义性的类型转换
如果一个类中包含一个或多个类型转换,则必须确保在类类型和目标类型转换之间只存在唯一一种转换方式。否则我们的代码可能会存在二义性。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算数类型的转换。我们无法用强制类型转换来解决二义性,因为强制类型转换也面临着二义性。最好不要创建两个转换源都是算数类型的转换。
operator int( )const
operator double( )const
当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。
【Note】:
1)不要让两个类执行相同的类型转换,比如A转换B的同时B也转换为A。
2)避免转换目标是内置算数类型的类型转换。
3)如果我们对一个类既提供了转换目标是算数类型的类型转换,也提供了重载的运算符,则会遇到重载运算符与内置运算符二义性的问题。
#include <bits/stdc++.h>
using namespace std;
class SmallInt
{
friend ostream &operator<<(ostream &out, SmallInt &s);
public:
SmallInt(int i = 0):val(i)
{
if(i < 0 || i > 255)
throw out_of_range("bad value");
}
SmallInt &operator=(size_t p);
explicit operator int() const { return val; }
~SmallInt(){}
private:
size_t val;
};
SmallInt &SmallInt::operator=(size_t p)
{
val = p;
return *this;
}
ostream &operator<<(ostream &out, SmallInt &s)
{
out << s.val;
return out;
}
int main(int argc, char const *argv[])
{
SmallInt si = 3;
cout << si << endl;
int k = static_cast<int>(si) + 3;
cout << k;
system("pause");
return 0;
}
十、重载二元运算符
为了满足某些运算符的可交换性,重载二元操作符时应该将其声明为友元函数。
#include <bits/stdc++.h>
using namespace std;
class Integer
{
friend Integer operator+(int value, Integer integer);
public:
Integer();
Integer(int value);
Integer operator+(int value);
void operator=(int value);
operator int() const;
private:
int m_value;
};
Integer::Integer() {
m_value = 0;
}
Integer::Integer(int value) {
m_value = value;
}
Integer Integer::operator+(int value) {
int tmpValue = m_value + value;
return Integer(tmpValue);
}
void Integer::operator=(int value) {
m_value = value;
}
Integer::operator int() const {
return m_value;
}
Integer operator+(int value, Integer integer) {
int tmpValue = integer.m_value + value;
return Integer(tmpValue);
}
int main(int argc, char const *argv[])
{
Integer integer = Integer(10);
Integer tmpInteger = 100; //重载=运算符。
tmpInteger = integer + 1; //重载Integer成员函数+运算符。
tmpInteger = 1 + tmpInteger;//重载友元函数+运算符。
return 0;
}
十一、String类的实现:
#include <bits/stdc++.h>
using namespace std;
class MyString
{
friend ostream &operator<<(ostream &out, MyString &s);
friend istream &operator>>(istream &in, MyString &s);
public:
MyString() = default;
MyString(const char *p);
MyString(const MyString &s);
~MyString();
MyString &operator=(const char *p);
MyString &operator=(const MyString &s);
bool operator==(const char *p) const;
bool operator==(const MyString &s) const;
int operator<(const char *p);
int operator>(const MyString &s);
MyString &operator+=(const char *p);
MyString &operator+=(const MyString &s);
// MyString &operator*();
// MyString *operator->();
char &operator[](int index) const;
inline int Size() const{ return m_len; }
private:
int m_len;
char *m_p;
};
class CheckString
{
public:
CheckString() = default;
~CheckString() = default;
bool operator()(const MyString &s1, const MyString &s2) const
{
return s1.Size() < s2.Size();
}
};
//默认构造函数。
MyString::MyString()
{
m_len = 0;
m_p = new char[1];
strcpy(m_p,"");
}
//构造函数。
MyString::MyString(const char *p)
{
if(p == NULL)
{
m_len = 0;
m_p = new char[1];
strcpy(m_p,"");
}
else
{
m_len = strlen(p);
m_p = new char[m_len+1];
strcpy(m_p,p);
}
}
//拷贝构造函数。
MyString::MyString(const MyString &s)
{
m_len = s.m_len;
m_p = new char[m_len+1];
strcpy(m_p,s.m_p);
}
//析构函数。
MyString::~MyString()
{
if(m_p != NULL)
{
delete [] m_p;
m_p = NULL;//防止出现野指针。
m_len = 0;
}
}
//重载赋值操作符。
MyString &MyString::operator=(const char *p)
{
if(m_p != NULL)
{
delete [] m_p;
m_len = 0;
}
if(p == NULL)
{
m_len = 0;
m_p = new char[1];
strcpy(m_p,"");
}
else
{
m_len = strlen(p);
m_p = new char[m_len+1];
strcpy(m_p,p);
}
return *this;//返回引用。
}
//重载赋值操作符。
MyString &MyString::operator=(const MyString &s)
{
if(m_p != NULL)
{
delete [] m_p;
m_len = 0;
}
else
{
m_len = s.m_len;
m_p = new char[m_len+1];
strcpy(m_p,s.m_p);
}
}
//重载下标操作符。
char &MyString::operator[](int index) const
{
return m_p[index];
}
//重载输出操作符。
ostream &operator<<(ostream &out, MyString &s)
{
out << s.m_p;
return out;
}
istream &operator>>(istream &in, MyString &s)
{
in >> s.m_p;
return in;
}
//重载等号操作符。
bool MyString::operator==(const char *p) const
{
if(p == NULL)
{
return m_len ==0 ? true : false;
}
else
{
return m_len == strlen(p) ? !strcmp(m_p,p) : false;
}
}
bool MyString::operator==(const MyString &s) const
{
return *this == s ? true : false;
}
//重载大于小于。
int MyString::operator<(const char *p)
{
return strcmp(m_p,p);
}
int MyString::operator>(const MyString &s)
{
return strcmp(m_p,s.m_p);
}
//重载加号运算符。
MyString &MyString::operator+=(const char *p)
{
strcat(m_p,p);
return *this;
}
MyString &MyString::operator+=(const MyString &s)
{
strcat(m_p,s.m_p);
return *this;
}
// MyString &operator*()
// {
// return *this;
// }
// MyString *operator->()
// {
// return &this->operator*();
// }
int main(int argc, char const *argv[])
{
MyString s1 = "s11";
MyString s2("s22");
MyString s3 = s2;
s3 = "s33";
cout << s2[0] << endl;
cout << s3 << endl;
if(s3 == "s33")
{
cout << "equal" <<endl;
}
if(s2 > s3)
{
cout << "s2>s3" << endl;
}
else
{
cout << "s2<s3" << endl;
}
s3+=s2;
cout << s3 << endl;
vector<MyString> vec{s1,s2,s3};
stable_sort(begin(vec),end(vec),CheckString());
cout << vec[1];
// for (const auto s : vec)
// {
// cout << s << endl;
// }
system("pause");
return 0;
}