c/c++开发,无可避免的操作符operator(篇一),操作符重载

                                                              操作符重载的面面观

目录

一、操作符号重载

        1.1 语句表达式

        1.2 重载操作符-operator

        1.3 重载操作符注意事项

        1.4 重载操作符使用

二、操作符重载设计设计习惯

        2.1 赋值操作符重载设计

        2.2 算术操作符重载设计

        2.3 关系操作符重载设计

        2.4 自增自减操作符重载设计

        2.5 输入输出操作符重载设计

        2.6 逻辑操作符重载尽量避免

        2.7 成员访问操作符设计

三、特殊操作符重载设计习惯

        3.1 调用操作符及函数对象

        3.2 布尔运算符

        3.3 内存分配操作符-operator new/delete

        3.4 转换操作

四、本博文演示源码


一、操作符号重载

        虽然c/c++内置了大量各类操作符,开发者可以很方便将其应用数学运算、成员访问、类型转换、内存分配等执行语句中,但很多时候,也需要根据项目应用需要,通过操作符重载,能够针对类类型的操作数定义不同的操作符版本,尤其是针对于自定义类型对象进行操作时。

        1.1 语句表达式

        c/c++是运算符和它们的操作数的序列排列组合成的一项计算。C++ 定义了许多内置类型间的操作符和自动转换。使用这些操作及转换能够编写丰富的混合类型表达式。表达式分为:

        1)求值表达式,可以产生一个结果(比如 2+2 的求值产生结果 4),也可能产生副作用(比如对 std::printf("%d",4) 的求值在标准输出上打印字符 '4')。

        2)不求值表达式,运算符 typeid、sizeof、noexcept 和 decltype (C++11 起) 的操作数是不求值表达式(除非运算符为 typeid 而操作数是多态泛左值),因为这些运算符仅查询其操作数的编译期性质。

        3)弃值表达式,用来实施操作数其副作用的表达式。从这种表达式计算的值被舍弃。这样的表达式包括任何表达式语句的完整表达式,内建逗号运算符的左边的实参,以及转型到类型 void 的转型表达式的实参。弃值表达式的计算结果永远不进行数组到指针和函数到指针转换。当且仅当该表达式是 volatile 限定的泛左值,并是标识、数组下标、成员访问、成员指针、条件、逗号等表达式时,进行左值到右值转换。       

        C++ 允许我们重定义操作符,用于类类型对象时表达,通常c/c++提供了如下常见运算符:

赋值:
a=b  a+=b  a-=b  a*=b  a/=b  a%=b  a&=b  a|=b  a^=b  a<<=b  a>>=b
自增自减: 
++a --a  a++  a--
算术: 
+a -a  a+b  a-b  a*b  a/b  a%b  ~a  a&b  a|b  a^b  a<<b  a>>b
逻辑: 
!a  a&&b  a||b 
比较: 
a==b  a!=b  a<b  a>b  a<=b  a>=b  a<=>b
成员访问: 
a[b] *a &a  a->b  a.b  a->*b  a.*b
其他:
a(...)  a,b  ?: 

        其中,部分内置操作符不能进行重载:

:: .* . ?:

        其他特殊操作符:

类型转换:
static_cast dynamic_cast const_cast reinterpret_cast C风格强制转型 
内存分配:
new new[] delete delete[]
常量:
sizeof typeid 
sizeof... noexcept alignof //C++11 起

        1.2 重载操作符-operator

        重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表。如果需要,可以像内置转换那样使用类类型转换,将一个类型的对象隐式转换到另一类型。尤其是自定义类型时,需要配套创建相关操作符号。

//test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_

#include <ostream>

class Obj
{
public:
    Obj(int val_);
    ~Obj();
    Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=
    Obj& operator=(const int& rhs); //一元操作符,隐式赋值运算=
    friend Obj operator+(const Obj&, const Obj&);//二元操作符+
    friend std::ostream& operator<<(std::ostream&, const Obj&);//io运算操作符<<
private:
    int val;
};

#endif //_TEST_1_H_
//test1.cpp
#include "test1.h"

Obj::Obj(int val_) : val(val_)
{

};

Obj::~Obj()
{

};

Obj& Obj::operator=(const Obj& rhs)
{
    if(this==&rhs)// 防止自赋值
        return *this;
    val = rhs.val;
    return *this;
};

Obj& Obj::operator=(const int& rhs)
{
    val = rhs;
    return *this;
};

Obj operator+(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);
    obj.val+=rhs.val;
    return obj;
};

std::ostream& operator<<(std::ostream& os, const Obj& obj)
{
    os << obj.val;
    return os;
};
//main.cpp
#include "test1.h"

#include <iostream>

int main(int argc, char* argv[])
{
    Obj a(10);
    std::cout << "a = " << a << "\n";//io运算符调用
    Obj b = 11;//隐式转换
    std::cout << "b = " << b << "\n";
    Obj c = a;//赋值运算
    std::cout << "c = " << c << "\n";
    Obj d = b+c;//+运算
    std::cout << "d = " << d << "\n";
    std::cout << "a+b+c = " << a+b+c << "\n";
    return 0;
};
//out log
a = 10
b = 11
c = 10
d = 21
a+b+c = 31

        上述示例中,为Obj类型创建了赋值运算符、int转Obj转换赋值运算、IO移位操作符及加号运算操作符,使Obj类型的使用像内置类型一样直观,就是如同内置类型一样自如运用表达式。

        为类型创建重载操作符,必须选择是将操作符设置为类成员还是普通非成员函数。在某些情况下,开发者没有选择,操作符必须是成员;在另一些情况下,可以依据业务需要决定将操作符设置为类成员还是普通非成员函数:

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
  • 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
  • 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
  • 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。

        1.3 重载操作符注意事项

        重载操作符必须具有至少一个类类型或枚举类型的操作数。即重载操作符不能重新定义用于内置类型对象的操作符的含义,内置类型的操作符,其含义不能改变,例如开发者不能在自定义整型类型的加号操作符:

int operator+(int, int);                //error 非成员运算符要求类类型或枚举类型的参数
int operator+(const int, const int);    //error 非成员运算符要求类类型或枚举类型的参数
int operator+(const int&, const int&);  //error 非成员运算符要求类类型或枚举类型的参数
int& operator+(const int&, const int&); //error 非成员运算符要求类类型或枚举类型的参数
int* operator+(const int&, const int&); //error 非成员运算符要求类类型或枚举类型的参数

        不要重载具有内置含义的操作符,例如:

  • 赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下这些操作符。
  • 合成赋值操作符进行逐个成员赋值:使用成员自己的赋值操作依次对每个成员进行赋值,即尽量采用类类型的成员变量内置操作符完成合成赋值操作符重载。
  • 默认情况下,取地址操作符(&)和逗号操作符(.)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
  • 内置逻辑与(&&)和逻辑或(||)操作符使用短路求值。如果重新定义该操作符,将失去操作符的短路求值特征。

        通过为给定类类型的操作数重定义操作符,可以改变这些操作符的含义。重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。

        定义操作符为类的成员,左操作数将只能是该类类型的对象。作为类成员的重载函数,this 作为隐藏的左操作数(有一个隐含的 this 形参,限定为第一个操作数),其形参看起来比操作数数目少 1,大多是一元操作符。重载一元操作符,如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。类似的二次操作符时,形参+1。通常将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员:

Obj& operator+=(const Obj& rhs);//成员函数,一元操作符+=
friend Obj operator+(const Obj&, const Obj&);//普通函数,二元算术操作符+

Obj& Obj::operator+=(const Obj& rhs)
{
    val += rhs.val;
    return *this;
}

Obj operator+(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);
    obj.val+=rhs.val;
    return obj;
};

        通常在操作符定义为非成员函数时,操作符通常需要访问类的私有部分,通常将它们设置为所操作类的友元

friend Obj operator+(const Obj&, const Obj&);//二元操作符+

Obj operator+(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);
    obj.val+=rhs.val;    //直接操作私有变量
    return obj;
};

        如果害怕友元会破环类型数据的封装性,可以采用类似功能的成员操作符替代来达成目的,例如对于+号操作符,可以用public 成员 operator+= 实现:

Obj& operator+=(const Obj& rhs);//一元操作符+=
Obj& Obj::operator+=(const Obj& rhs)
{
    val += rhs.val;
    return *this;
};

        即使重载了相关的操作符,也不会改变这些操作符的优先级、结合性或操作数目,不管操作
数的类型和操作符的功能定义如何。

//两者的优先级、结合次序并没有不同
//总是将两实参绑定到 operator+,并且将结果用作 operator= 右操作数。
int x=0; y=1;
int k = x+y;//两者的优先级、结合次序并没有不同,

Obj a=0;  b=1;
Obj c = a+b; //两者的优先级、结合次序并没有不同,

        开发者自行重载操作符约束限制:

  • 不能重载运算符 ::(作用域解析)、.(成员访问)、.*(通过成员指针的成员访问)及 ?:(三元条件)。
  • 不能创建新运算符,例如 **、<> 或 &|。
  • 不可能更改运算符的优先级、结合方向或操作数的数量。
  • 重载的运算符->必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符->的对象。
  • 运算符 && 与 || 的重载失去短路求值。

        重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR和逗号操作符的操作数求值。在 && 和 ||的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载 &&、|| 或逗号操作符不是一种好的做法。

        1.4 重载操作符使用

        重载操作符和内置操作符一样,都支持显式调用和隐式调用:

Obj a = 10; Obj b = a;//隐式
std::cout << "a+b = " << a+b << "\n";//隐式
std::cout << "a+b = " << operator+(a,b) << "\n";//显式,向使用函数一样

        调用成员操作符函数与调用任意其他函数是一样的:指定运行函数的对象,然后使用点或箭头操作符获取希望调用的函数,同时传递所需数目和类型的实参。

Obj a = 10; b = 11; //隐式
a += b;             //隐式
a.operator+=(b);    //显式,成员函数调用方式 

二、操作符重载设计设计习惯

        有时我们需要定义自己的操作符时,表达逻辑最好和内置操作类型行为保持一致,例如不要重载operator+操作符,却做了减法的工作。如果行为不能和内置类型存在差异,最好采用成员函数或普通函数来实现,给这些函数其一个知其意的名字,比重载操作符更好。

        重载的赋值运算应在赋值的内置含义基础上进行定制,而不是完全绕开,例如类成员变量是vector,那么重载操作符内部操作就直接使用vector操作,而非另起灶炉,除非成员变量没有提供支持。

        大多数操作符对类对象没有意义,为类重载操作符时,除了提供几个常规的操作符(如,赋值=、相等==)外,其余的根据开发进度过程涉及到需要在按需添加。另外操作符号通常是按归类一起提供的:

        2.1 赋值操作符重载设计

        赋值操作符应该是每个自定义类型必备的,赋值必须返回对 *this 的引用。即使开发者不显示定义,编译器也会自动创建同类赋值操作符,开发者也可以为一个类定义许多附加的赋值操作符,这些赋值操作符会因右操作符类型不同而不同。当然+=、-=操作符也是赋值操作符范畴,它也是返回自身,但是通常它们会与+、-操作符配套一起。同样,先a*=b  a/=b  a%=b  a&=b  a|=b  a^=b  a<<=b  a>>=b运算操作符号都类似=操作符一样,this作为左操作数,并返回对 *this 的引用。

//test1.h
class Obj
{
public:
    //...other
    Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=,不主动定义,一般编译器会默认定义这个
    Obj& operator=(const int& rhs); //一元操作符,隐式赋值运算=
    Obj& operator=(const char& rhs); //一元操作符,隐式赋值运算=
    Obj& operator+=(const Obj& rhs);//一元操作符+=
    Obj& operator-=(const Obj& rhs);//一元操作符-=
private:
    int val;
};
//test1.cpp
Obj& Obj::operator=(const Obj& rhs)
{
    if(this==&rhs)
        return *this;
    val = rhs.val;
    return *this;
};

Obj& Obj::operator=(const int& rhs)
{
    val = rhs;
    return *this;
};

Obj& Obj::operator=(const char& rhs)
{
    val = rhs;
    return *this;
};

        2.2 算术操作符重载设计

        如果一个类有算术操作符或位操作符,一般定义为非成员函数(除以自身为左操作数的),通常,也提供相应的复合赋值操作符一般是个好的做法。例如,Obj类定义了 +操作符,逻辑上,它也应该定义 +=。使其操作行为与内置操作符一样:复合赋值的行为应与 + 之后接着 = 类似。算术操作符+a -a  a+b  a-b  a*b  a/b  a%b  ~a  a&b  a|b  a^b  a<<b  a>>b等都是类似的实现原理。

//test1.h
class Obj
{
public:
    //...other
    Obj& operator+=(const Obj& rhs);//一元操作符+=
    Obj& operator-=(const Obj& rhs);//一元操作符-=
    friend Obj operator+(const Obj&, const Obj&);//二元操作符+
    friend Obj operator-(const Obj&, const Obj&);//二元操作符-
private:
    int val;
};

Obj& Obj::operator+=(const Obj& rhs)
{
    val += rhs.val;  //会改变对象状态
    return *this;    //返回本身
}
Obj& Obj::operator-=(const Obj& rhs)
{
    val -= rhs.val;  //会改变对象状态
    return *this;    //返回本身
}
//加法操作符并不改变操作符的状态,操作符是对 const 对象的引用;
Obj operator+(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);//它产生并返回一个新的 Obj 对象,该对象初始化为 lhs 的副本。
    obj.val+=rhs.val;
    return obj;    //不会改变对象状态,返回拷贝对象
};
Obj operator-(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);//它产生并返回一个新的 Obj 对象,该对象初始化为 lhs 的副本。
    obj.val-=rhs.val;
    return obj;    //不会改变对象状态,返回拷贝对象
};

        2.3 关系操作符重载设计

       比较及关系操作符号,一般定义为非成员函数,例如类定义 < 操作符,最好也能提供<=、>、>= 这些操作符;提供类==操作符号,最好也能提供!=操作符号。

//test1.h
class Obj
{
public:
    //...other
    static int cmp_Obj(const Obj &obj1, const Obj &obj2);
private:
    int val;
};

inline bool operator==(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) == 0; }
inline bool operator!=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) != 0; }
inline bool operator>=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) >= 0; }
inline bool operator<=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) <= 0; }
inline bool operator>(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) > 0; }
inline bool operator<(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) < 0; }
//test1.cpp
int Obj::cmp_Obj(const Obj &obj1, const Obj &obj2)
{
    return obj1.val - obj2.val;
}

      2.4 自增自减操作符重载设计

         C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。那么为我们自己的类定义自增操作符和自减操作符时,同样也可提供前缀和后缀两种实现方式。

//test1.h
class Obj
{
public:
    //...other
    Obj operator++();//一元操作符++,前缀式操作符
    Obj operator--();//一元操作符--,前缀式操作符
    Obj operator++(int rhs);//一元操作符++,后缀式操作符
    Obj operator--(int rhs);//一元操作符--,后缀式操作符
private:
    int val;
};
//test1.cpp
//为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用
Obj Obj::operator++()
{
    ++val;
    return *this;
}
Obj Obj::operator--()
{
    --val;
    return *this;
}
//为了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),
//并且,应作为值返回,而不是返回引用。
Obj Obj::operator++(int rhs)
{
    Obj ret(*this);
    ++*this;
    return ret;
}
Obj Obj::operator--(int rhs)
{
    Obj ret(*this);
    --*this;
    return ret;
}

        操作符的后缀式比前缀式复杂一点,先记住对象在加 1/减 1 之前的当前状态。这些操作符定义了一个局部 Obj对象,将它初始化为 *this 的副本,即 ret 是这个对象当前状态的副本。保存了当前状态的副本后,操作符调用自己的前缀式操作符分别进行加 1 或减1:++*this,调用这个对象的Obj前缀自增操作符,返回之后,对象本身加了 1,但返回的是尚未自增的原值。

        自增(++)和自减(--)操作符经常由诸如迭代器这样的类实现,这样的类提供类似于指针的行为来访问序列中的元素。例如,可以定义一个类,该类指向一个数组并为该数组中的元素提供访问检查。

        2.5 输入输出操作符重载设计

        支持 I/O 操作的类所提供的 I/O 操作接口,一般应该与标准库 iostream为内置类型定义的接口相同,因此,许多类都需要重载输入和输出操作符。输出操作符 << 的重载是较常实现的一个操作符号,为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对ostream 形参的引用。

//test1.h
class Obj
{
public:
    //...other
    friend std::ostream& operator<<(std::ostream&, const Obj&);//io运算操作符<<
private:
    int val;
};
//test1.cpp
std::ostream& operator<<(std::ostream& os, const Obj& obj)
{
    os << obj.val;
    return os;
};

        输出操作符 << 的重载,其左操作数为 ostream 类型,因此只能定义非成员函数。需要建议的是,输出操作符应输出对象的内容,进行最小限度的格式化,例如,换行符等就尽量不要给出,那是操作符调用者该操心的事。

        输入操作符 >> 的重载,这个相对如<<操作符,一般使用到较少,但是作为配对使用原则,类似设计者一般也会同版本给出:

//test1.h
class Obj
{
public:
    //...other
    friend std::istream& operator>>(std::istream& in, Obj&);//io运算操作符>>
private:
    int val;
};
//test1.cpp
std::istream& operator>>(std::istream& in, Obj& obj)
{
    //最好安全检查
    in >> obj.val;  //取出数据写入val
    return in;
}

        2.6 逻辑操作符重载尽量避免

        前面也讲述过,重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR和逗号操作符的操作数求值。因此,重载 &&、|| 或逗号操作符不是一种好的做法。

        2.7 成员访问操作符设计

        为了支持指针型类,例如迭代器,C++ 语言允许重载解引用操作符(*)和箭头操作符(->)。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员。

        解引用操作符和箭头操作符常用在实现智能指针的类中,例如在本专栏的C/C++开发,无可避免的内存管理(篇四)-智能指针备选_py_free-物联智能的博客-CSDN博客。的“2.1 仿auto_ptr 的类模板”知识点就做过类似的成员访问操作符设计实现:

#ifndef _AUTO_PTR_H_
#define _AUTO_PTR_H_
 
#include <iostream>
 
template<typename T>
class AutoPtr
{
public:
    explicit AutoPtr(T* p=0) : m_ptr(p) {
        std::cout << "AutoPtr create!\n";
     };
    ~AutoPtr() { 
        std::cout << "AutoPtr delete!\n";
        delete m_ptr; 
    };
    T& operator*() const { 
        return *m_ptr; 
    };
    T* operator->() const { 
        return m_ptr;
    };
    
private:
    T* m_ptr; // dumb pointer
};
 
void func1(void)
{
    AutoPtr<int> a(new int(100));
    std::cout << "*a = " << *a << "\n";
    std::cout << "a->() = " << a->() << "\n";
}
 
#include "autoptr.cpp"
#endif // _AUTO_PTR_H_

        解引用操作符是个一元操作符。在这个类中,解引用操作符定义为成员,因此没有显式形参,该操作符返回对 AutoPtr所指向的 T的引用。

        箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。

a->(); 本质上是 (a->)();

        这里没有第二个形参,因为 -> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。

        下标操作符,即operator[],可以从容器中检索单个元素的容器类一般会定义下标操作符,下标操作符必须定义为类成员函数。本专栏博文c/c++开发,无可避免的模板编程实践(篇九)-c++11的新顺序容器_py_free-物联智能的博客-CSDN博客

就讲述如何定义一个仿std::array容器类时,就做了典型的operator[]操作符实现:

#include <cstring>
#include <cassert>
 
template<typename T,int SIZE>
struct my_array
{
	my_array() 
	{
		memset(Buf,0,SIZE);
	};
    ~my_array()
	{
	};
    T& operator[]( const int pos )
    {
        assert(pos >= 0 && pos < SIZE);//边界判断
        return Buf[pos];
    }
    T& at(const int pos)
    {
        assert(pos >= 0 && pos < SIZE);
        return Buf[pos];
    }
    //其他实现
	T Buf[SIZE];
};
 
void myarray_test(void)
{
    //调用
    my_array<int,3> a;          //int类型数据,长度大小3
    a[0] = 9;
    a.at(1) = 10;
    std::cout << "a.at(0)=" << a.at(0) << "\n";
    std::cout << "a[1]=" << a[1] << "\n";
};

        定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用,如果只选择定义一个,建议选择后者。

三、特殊操作符重载设计习惯

        3.1 调用操作符及函数对象

        调用操作符,即operator(),该操作符可以有零或多个形参和返回值。当用户定义的类重载了函数调用运算符 operator() 时,它就成为函数对象 (FunctionObject) 类型。这种类型的对象能用于函数调用式的表达式。

//test1.h
class Obj
{
public:
    //...other
    int operator()();
    int operator()(int val_);
private:
    int val;
};
//test1.cpp
int Obj::operator()()
{
    return val;
}
int Obj::operator()(int val_)
{
    return val_<0?-val_:val_;
}
//main.cpp
//
    Obj obj_op(10);
    std::cout << "obj_op() = " << obj_op() << "\n";
    std::cout << "obj_op(-100) = " << obj_op(-100) << "\n";
//out log
obj_op() = 10
obj_op(-100) = 100

        尽管 Obj 是一个对象而不是函数,我们仍然可以“调用”该对象,效果是运行由 Obj 对象定义的重载调用操作符。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型、返回约束加以区别。

        定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象,在标准库中经常用作通用算法的实参关于函数对象,在本专栏的博文:C/C++开发,无可避免的多线程(篇四).线程与函数的奇妙碰撞_py_free-物联智能的博客-CSDN博客有更多的描述,大家可移步参考,这里就不展开论述。

        3.2 布尔运算符

        布尔运算符主要是基于类类型 bool安全问题引入的,在 C++11 引入显式转换函数之前,设计一个能用于布尔语境的类(比如,if(obj) { ... })会出现问题:给定一个用户定义转换函数,如T::operator bool() const;,则隐式转换序列允许再多一步标准转换序列,也就是 bool 结果会转换成 int,允许诸如 obj << 1; 或 int i = obj; 这样的代码。

        一个早期的解决方案可参见 std::basic_ios,它定义 operator! 和 operator void*(C++11 前),使得如 if(std::cin) {...} 的代码能编译,因为 void* 能转换为 bool,但int n = std::cout; 不能,因为 void* 不可转换至 int。这仍然允许无意义代码能编译,如 delete std::cout;。许多 C++11 前的第三方库设计带有更为复杂的解决方案,称作安全 Bool 手法。
        显式 bool 转换也能用于解决安全 bool 问题

explicit operator bool() const { ... } // (C++11 起)

        用户定义类在有意用于布尔语境时,可以定义bool重载运算符 operator bool,而 operator! (布尔取反运算符)的受期待行为是返回 operator bool 的取反,由于内建运算符 ! 进行按语境到 bool 的转换,用户定义类可以只提供 operator bool 而无需重载 operator!。

//test1.h
class Obj
{
public:
    //...other
    explicit operator bool() const;
private:
    int val;
};
//test1.cpp
Obj::operator bool() const
{
    return 0!=val;
}
//main.cpp
//
    Obj obj_b(0);
    if(!obj_b)
        std::cout << "obj_b() = " << obj_b() << "\n";
    obj_b = 1;
    if(obj_b)
        std::cout << "obj_b() = " << obj_b() << "\n";
//out log
obj_b() = 0
obj_b() = 1

        3.3 内存分配操作符-operator new/delete

        在自己的类模板中,通过定义自己的名为 operator new 和 operator delete 的成员,可以屏蔽标准库operator new 和 operator delete 函数的调用,类就可以管理用于自身类型的内存。编译器看到类类型的 new 或 delete 表达式的时候,它查看该类是否有operator new 或 operator delete 成员,如果类定义(或继承)了自己的成员new 和 delete 函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。关于这方面的详细说明请参考本专栏的博文:C/C++开发,无可避免的内存管理(篇三)-规划好内存_py_free-物联智能的博客-CSDN博客的“二、new和delete表达式的秘密”去深入了解。

//BaseObj.h
#ifndef _BASE_OBJ_H_
#define _BASE_OBJ_H_
 
#include <memory>
 
template <typename T> 
class BaseObj 
{
public:
    void *operator new(std::size_t);
    void operator delete(void *, std::size_t);
    virtual ~BaseObj() { }
protected:
    T *next;
private:
    static void add_to_freelist(T*);
    static std::allocator<T> alloc_mem;
    static T *freeStore;
    static const std::size_t chunk;
};
 
#include "BaseObj.cpp"
#endif //_BASE_OBJ_H_
//BaseObj.cpp
#include "BaseObj.h"
 
#include <iostream>
#ifdef __linux__
#include <stdio.h>
#include <exception>
#endif
//
template <typename T> 
std::allocator<T> BaseObj<T>::alloc_mem;
 
template <typename T> 
T *BaseObj<T>::freeStore = 0;
 
template <typename T> 
const std::size_t BaseObj<T>::chunk = 24;
 
//operator new 使用这个函数将新分配的对象放到自由列表,删除对象
template <typename T>
void *BaseObj<T>::operator new(size_t sz)
{
    std::cout << "BaseObj operator new func is call!\n";
    // new should only be asked to build a T, not an object
    // derived from T; check that right size is requested
    if (sz != sizeof(T))
    {
        #ifdef WIN32
        throw std::runtime_error("BaseObj: wrong size object in operator new");
        #else
        std::cout << "BaseObj: wrong size object in operator new\n";
        #endif
    }
    if (!freeStore) 
    {
        // the list is empty: grab a new chunk of memory
        // allocate allocates chunk number of objects of type T
        T * array = alloc_mem.allocate(chunk);
        // now set the next pointers in each object in the allocated memory
        for (size_t i = 0; i != chunk; ++i)
        {
            add_to_freelist(&array[i]);
        }
    }
    T *p = freeStore;
    freeStore = freeStore->BaseObj<T>::next;
    return p; // constructor of T will construct the T part of the object
}
//operator delete 也使用该函数将对象放回自由列表
template <typename T>
void BaseObj<T>::operator delete(void *p, size_t)
{
    std::cout << "BaseObj operator delete func is call!\n";
    if (p != 0){
        // put the "deleted" object back at head of freelist
        add_to_freelist(static_cast<T*>(p));
    }
}
 
template <typename T>
void BaseObj<T>::add_to_freelist(T *p)
{
    p->BaseObj<T>::next = freeStore;
    freeStore = p;
}

        operator new /delete 操作符重载主要难点在与内存分配及回收上,是通过内置类型allocator内存分配器来实现的,allocator 类的 construct 及destroy 成员函数可以支持到内存分配及释放。

        3.4 转换操作

       在布尔操作符时,大家可注意到是用一个 explicit 的布尔操作符函数定义一个显式转换,若无explicit 标识,则认为是隐式转换,例如前面见到的operator =。当提供了实参类型的对象而需要一个类类型的对象时,编译器将使用该转换。这种构造函数定义了到类类型的转换。除了定义到类类型的转换之外,我们还可以定义从类类型的转换。即,我们可以定义转换操作符,给定类类型的对象,该操作符将产生其他类型的对象。像其他转换一样,编译器将自动应用这个转换。

        转换操作符是一种特殊的类成员函数,通用形式是operator type(),type 表示内置类型名、类类型名或由类型别名定义的名字。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型。

//test1.h
class Obj
{
public:
    //...other
    Obj(int val_);
    operator int() const;
private:
    int val;
};
//test1.cpp
Obj::Obj(int val_) : val(val_)
{

};
//各种以内置类型或自定义类型的函数名都可以
Obj::operator int() const
{
    return val;
}
//main.cpp
    Obj obj_i = 10;
    int i_ = obj_b;

        对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。
        转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员。

        虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operator int 返回一个 int 值;如果定义 operator Obj,它将返回一个 Obj对象,诸如此类通过类型转换操作符,可以很方便支持混合类型表达式,同时转换减少所需操作符的数目,便于代码书写及理解。

        转换函数只能应用一个类类型转换,如果存在多个,会产生歧义,则编译报错,自定义的和内置算术类型的转换起了冲突:

//接前面例子代码
if(i_==obj_i)   //需要注释inline bool operator==(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) == 0; }
        std::cout << "obj_b() = " << obj_b() << "\n";

        与使用重载操作符一样,转换操作符的适当使用可以大大简化类设计的工作并使得类的使用更简单。但是,有两个潜在的缺陷:定义太多转换操作符可能导致二义性代码,一些转换可能利大于弊。避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型。最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。例如如果类 Foo 具有接受类 Bar 的对象的构造函数,不要再为类 Bar 定义到类型 Foo 的转换操作。

        当然c++11标准库还提供了标准转换,从一个类型到另一类型的隐式转换。

//隐式转换
static_cast 转换一个类型为另一相关类型
dynamic_cast 在继承层级中转换
const_cast 添加或移除 cv 限定符
reinterpret_cast 转换类型到无关类型
//显示转型转换
使用 C 风格写法和函数式写法
(type)val

四、本博文演示源码

        通过定义内置操作符的重载版本,我们可以为自己的类型(即,类类型或枚举类型)的对象定义同样丰富的表达式集合。重载操作符必须具有至少一个类类型或枚举类型的操作符。应用于内置类型时,重载操作符与对应操作符具有同样数目的操作数、同样的结合性和优先级。
        大多数重载操作符可以定义为类成员或普通非成员函数,赋值操作符、下标操作符、调用操作符和箭头操作符必须为类成员。操作符定义为成员时,它是普通成员函数。具体而言,成员操作符有一个隐式 this 指针,该指针一定是第一个操作数,即,一元操作符唯一的操作数,二元操作符的左操作数。

        重载了 operator()的类的对象,称为“函数对象,常用语定义与算法产生很好的结合使用的谓词函数。类可以定义转换,可以很好帮助优化代码及设计,但是,必须注意避免避免定义一个类型与另一类型之间的多个转换,产生二义性。

        本博文撰写的代码如下,部分在其他博文中可以详细了解,文中以及给出跳转,这里就不给出那些代码。编译指令g++ main.cpp test1.cpp -o test.exe -std=c++11。

        test1.h

#ifndef _TEST_1_H_
#define _TEST_1_H_

#include <istream>
#include <ostream>

class Obj
{
public:
    Obj(int val_);
    ~Obj();
    operator int() const;
    int operator()();
    int operator()(int val_);
    explicit operator bool() const;
    Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=
    Obj& operator=(const int& rhs); //一元操作符,隐式赋值运算=
    Obj& operator=(const char& rhs); //一元操作符,隐式赋值运算=
    Obj& operator+=(const Obj& rhs);//一元操作符+=
    Obj& operator-=(const Obj& rhs);//一元操作符-=
    //
    Obj operator++();//一元操作符++,前缀式操作符
    Obj operator--();//一元操作符--,前缀式操作符
    Obj operator++(int rhs);//一元操作符++,后缀式操作符
    Obj operator--(int rhs);//一元操作符--,后缀式操作符
    //
    static int cmp_Obj(const Obj &obj1, const Obj &obj2);
    friend Obj operator+(const Obj&, const Obj&);//二元操作符+
    friend Obj operator-(const Obj&, const Obj&);//二元操作符-
    friend std::ostream& operator<<(std::ostream&, const Obj&);//io运算操作符<<
    friend std::istream& operator>>(std::istream& in, Obj&);//io运算操作符>>
private:
    int val;
};

// inline bool operator==(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) == 0; }
inline bool operator!=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) != 0; }
inline bool operator>=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) >= 0; }
inline bool operator<=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) <= 0; }
inline bool operator>(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) > 0; }
inline bool operator<(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) < 0; }

#endif //_TEST_1_H_

        test1.cpp

#include "test1.h"

Obj::Obj(int val_) : val(val_)
{

};

Obj::~Obj()
{

};

Obj::operator int() const
{
    return val;
}

int Obj::operator()()
{
    return val;
}
int Obj::operator()(int val_)
{
    return val_<0?-val_:val_;
}

Obj::operator bool() const
{
    return 0!=val;
}

Obj& Obj::operator=(const Obj& rhs)
{
    if(this==&rhs)
        return *this;
    val = rhs.val;
    return *this;
};

Obj& Obj::operator=(const int& rhs)
{
    val = rhs;
    return *this;
};

Obj& Obj::operator=(const char& rhs)
{
    val = rhs;
    return *this;
};


Obj& Obj::operator+=(const Obj& rhs)
{
    val += rhs.val;
    return *this;
}

Obj& Obj::operator-=(const Obj& rhs)
{
    val -= rhs.val;
    return *this;
}

Obj Obj::operator++()
{
    ++val;
    return *this;
}
Obj Obj::operator--()
{
    --val;
    return *this;
}
Obj Obj::operator++(int rhs)
{
    Obj ret(*this);
    ++*this;
    return ret;
}
Obj Obj::operator--(int rhs)
{
    Obj ret(*this);
    --*this;
    return ret;
}

int Obj::cmp_Obj(const Obj &obj1, const Obj &obj2)
{
    return obj1.val - obj2.val;
}

Obj operator+(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);
    obj.val+=rhs.val;
    return obj;
};

Obj operator-(const Obj& lhs, const Obj& rhs)
{
    Obj obj(lhs);
    obj.val-=rhs.val;
    return obj;
};

std::ostream& operator<<(std::ostream& os, const Obj& obj)
{
    os << obj.val;
    return os;
};

std::istream& operator>>(std::istream& in, Obj& obj)
{
    //最好安全检查
    in >> obj.val;  //取出数据写入val
    return in;
}

        main.cpp

#include "test1.h"

#include <iostream>

int main(int argc, char* argv[])
{
    Obj a(10);
    std::cout << "a = " << a << "\n";
    Obj b = 11;
    std::cout << "b = " << b << "\n";
    Obj c = a;
    std::cout << "c = " << c << "\n";
    Obj d = b+c;
    std::cout << "d = " << d << "\n";
    std::cout << "a+b+c = " << a+b+c << "\n";
    //
    Obj obj_op(10);
    std::cout << "obj_op() = " << obj_op() << "\n";
    std::cout << "obj_op(-100) = " << obj_op(-100) << "\n";
    //
    Obj obj_b(0);
    if(!obj_b)
        std::cout << "obj_b() = " << obj_b() << "\n";
    obj_b = 1;
    if(obj_b)
        std::cout << "obj_b() = " << obj_b() << "\n";
    //
    Obj obj_i = 10;
    int i_ = obj_b;
    if(i_==obj_i)   //需要注释inline bool operator==(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) == 0; }
        std::cout << "obj_b() = " << obj_b() << "\n";
    return 0;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

py_free-物联智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值