OOP Lec10: Overloaded Operator

Overloaded Operator

人生得意需尽欢——在有电的地方赶紧把电子设备都充满。—— Weng Kai

重载的实现

C++ 实现了运算符重载,允许用户对自定义的类型使用和 C 一样的运算符。

经典重载: std::cin >> x 重载了用于位移的运算符。

可以重载的运算符

+ - * / % & | ^ ~ << >>
= += -= *= /= %= &= |= <<= >>=
< > == != <= >=
! && || ++ --
, ->* ->() []
new delete new[] delete[]

不能被重载的运算符

. .* :: ?:
sizeof typeid
static_cast dynamic_cast const_cast
reinterpret_cast

重载条件

  • 不在 C 语言的运算符之内的符号不能用于重载,例如 **

  • 必须在类或枚举类(enumeration)上定义

  • 算子(operand)的数量保持一致,例如 a / b 必须两个参数。

    • 在 IntegerI 领域中,算子指在矩阵中的一个操作(operator),需要区分开来。
  • 重载后运算符的优先级不变。

语法

用关键字 operator 来声明运算符重载。既可以是类的成员函数,也可以是自由函数。

类内重载:

// inside class
Integer operator+(const Integer& b){
    return Integer(this->val + b.val);
}

自由函数:

// globally
Integer operator+(const Integer& a, const Integer& b){
    return Integer(a.val + b.val);
}
class Integer{
public:
    Integer(int ii): i(ii){}
    void print(){ std::cout << ii << std::endl;}
    Integer operator+(const Integer& b){
        std::cout << "operator + " << std::endl;
        return Integer(this->val + b.val);
    }
    Integer operator-(){
        return Integer(-i);
    }
    friend Integer operator*(const Integer& a, const B& b);
private:
    int i;
};

Integer operator*(const Integer& a, const B& b){
    return Integer(a.i + b.i);
}

signed main(){
    Integer a1(1), a2(2);
    Integer a3 = a1 + a2;// a1.operator+(a2);
    Integer a4 = a1 + 3;
    Integer a5 = -a1;// a5.operator=(a1.operator-());
}

注意事项:

  • 在接收端不会进行类型转换
    • a1 + 33 转换为 Integer(3),再相加
    • 3 + a1 不能通过,因为 a1 不能转换为 int
    • 所以操作符左端的类型决定了使用的操作符的类型。
  • 赋值运算符有默认函数
  • 声明为自由函数时,操作数全部都要显式声明
    • 在类中提供接口 .geti() 来使用值
    • 或者声明为 friend 来使用
    • 此时没有接收端,两侧都可能进行类型转换(3 + a1 可以通过编译)

成员函数 VS 自由函数

  • 单目运算符应该声明为成员
  • =, (), [], ->, ->* 必须是成员
  • 其他的赋值运算符 += -= <<= 应该为成员
  • 二目运算符建议为自由函数

参数和返回类型

参数类型:考虑有没有赋值,没有赋值的操作,参数 const _Tp&,声明为 const 函数。

返回类型:考虑返回类型能不能作左值,能就返回一个可写的类型,否则返回不可写的类型。

const Integer operator+(const Integer& a, const Integer& b);

重载实现

函数原型

  • + - * / % ^ & | ~
    • const _Tp operator X(const _Tp& l, const _Tp& r);
  • == != < > <= >=
    • bool operator X(const _Tp& l, const _Tp& r);
  • []
    • _Tp& operator X(int index);
  • = += -= *= /= <<= >>=
    • _Tp& operator X(_Tp& l, const _Tp& r);

特殊问题:++--

class Integer{
public:
    const Integer& operator++();// prefix++
    const Integer  operator++(int);// postfix++
    const Integer& operator--();// prefix--
    const Integer  operator--(int);// postfix--
    ...
private:
    ...
};
const Integer& Integer::operator++(){
    *this += 1;
    return *this;
}
const Integer Integer::operator++(int){// just leave the parameter unnamed
    Integer old(*this);
    ++(*this);
    return old;
}

重载比较运算符

bool Integer::operator==(const Integer& rhs)const{
    return this->i == rhs.i;
}
bool Integer::operator!=(const Integer& rhs)const{
    return !(*this == rhs);
}

bool Integer::operator<(const Integer& rhs)const{
    return this->i < rhs.i;
}
bool Integer::operator>(const Integer& rhs)const{
    return rhs < *this;
}
bool Integer::operator<=(const Integer& rhs)const{
    return !(rhs < *this);
}
bool Integer::operator>=(const Integer& rhs)const{
    return !(*this < rhs);
}

对于满足比较运算符性质的运算(对称性、自反性),可以只修改 ==< 的内容,剩下的转化成 ==<! 的组合。

寻址运算符

// inside class Vector
// suppose int* buf, int size
int& operator[](int index){
    return buf[index];
}

流输入输出

istream& operator>>(istream& is, _Tp& obj){
    // read obj from is
    return is;
}
ostream& operator<<(ostream& os, const _Tp& obj){
    // output obj in os
    return os;
}
ostream& tab(ostream& os){// manipulator
    return os << '\t';
}

这样实现才能允许连续输入(is >> a >> b >> c 等同于 ((is >> a) >> b) >> c)。

赋值运算符

默认的赋值运算符 = 和默认拷贝构造函数一样,member-wise。

需要自定义的情况也和拷贝构造函数一样,有非完全包含的资源等。

_Tp& _Tp::operator=(const _Tp& rhs){
    if(this != &rhs){// explain[1]
        size = rhs.size;
        delete[] p;
        p = new int[size];
        for(int i = 0 ; i < size; ++i)
            p[i] = rhs.p[i];
    }
    return *this;
}

[1]:如果出现了 a = a;,在分配内存的时候很可能先释放了 a 的外部资源的内存(*p),重新申请再构造,这个时候就会把有效数据释放。

和拷贝构造类似,如果你的类不需要赋值运算,可以将 = 声明成 private,同时也不需要实现。这样在调用 operator= 的时候就会报错。

private:
    _Tp& _Tp::operator=(const _Tp& rhs);

使用建议

谨慎使用重载运算符。仅在运算符能切合语义、简化代码和提升可读性的情况下使用。

不要重载 &&||,

类型转换

隐式类型转换

编译器会发生隐式的类型转换,例如前文:

signed main(){
    Integer a1(1);
    Integer a2 = a1 + 3;
}

隐式地将 3 转换为 Integer(3),再相加。

这种转换,在自定义类有对应的单参数构造函数时就可以触发。

class PathName{
public:
    PathName(const string& pn): pth(pn){}
    ~PathName(){}
private:
    string pth;
}
int main(){
    string str("xyz");
    PathName path("abc");
    path = str;// Ok, path = PathName(str);
}

在构造函数前,可以使用 explicit 关键字:

explicit PathName(const string& pn): pth(pn){}

这样 PathName(const string&) 就不能用于隐式地转换了。

自定义类型转换

在类中,类型转换是特殊的成员函数,声明方式如下:

operator _Tp() const;

_Tp 可以是基本类型,或者是另一个类。

class Rational{
public:
    Rational(long long _p, long long _q): numerator(_p), denominator(_q){}
    ~Rational(){}
    operator double()const;
private:
    long long numerator, denominator;
}
Rational::operator double()const{
    return numerator / double(denominator);
}
int main(){
    Rational r(1, 3);
    double c = 3.14 * r; // Ok, 3.14 * 0.333
}

内置类型转换

对于基本类型的转换:

char => short => int => float => double
char => short => int => long

对于自定义类型的转换:

T => T&  T& => T T => (const T)
T[] => T*  T* => T[] T* => void*

使用建议

尽量不要使用类型转换,因为在意想不到的地方可能会被调用。

如果确实需要这样的转换,可以用单独的 toTp() 函数,而不是自定义类型转换。

double Rational::toDouble()const{
    return numerator / double(denominator);
}

转换的匹配

如果 A 的对象需要被当作 B 的对象看待(例如传参、运算)时,C++ 会去寻找最优匹配来构造参数。

  1. 精确的类型匹配(A 就是 B),这是最简单、最好的
  2. 内置类型转换
  3. 自定义的类型转换
    1. 如果有 B(A) 的构造函数,那么优先使用它
    2. 如果没有 B(A) 的构造函数,或者它被声明为 explicit,同时有 AB 的自定义类型转换,那么优先使用
    3. C++11 以后的关键字 static_cast:如果使用 static_cast<B>,那么明确使用构造函数。
  4. 对于模板函数的参数,如果上述条件均不满足,则编译器会考虑使用其他版本的函数。
#include <algorithm>
#include <iostream>
using namespace std;

class A;
class B;

class A{
public:
    A();
    operator B()const;
};
class B{
public:
    B();
    explicit B(A);
};

A::A(){cout << "constructor of A\n";}
A::operator B()const{
    cout << "transformer from A to B\n";
    return B();
}

B::B(){cout << "constructor of B\n";}
B::B(A v){cout << "constructor of B by A\n";}

void functionTakingB(B thing){
    cout << "OK" << std::endl;
}

signed main(int argc, char **argv){
    A a;
    B b;
    cout << "Round 1" << std::endl;
    functionTakingB(b);
    cout << "Round 2" << std::endl;
    functionTakingB(a);
    cout << "Round 3" << std::endl;
    functionTakingB(static_cast<B>(a));
    return 0;
}

可以本地编译一下看看输出,顺便观察编译器有没有优化拷贝构造。

如果不声明 explicit,则编译器会警告。

转换运算符

C++ 中有四个转换运算符 Cast Operator:

  • static_cast:基本类型
  • dynamic_cast:down-cast,安全
  • const_cast:修改 const 属性
  • reinterpret_cast:低级别

static_cast

用于在相关类型之间转换。在编译时刻完成。

  • 基本类型的转换
  • 子类指针/引用向父类指针/引用的转换
  • void 指针和其他类型指针的转换

dynamic_cast

用于多态类型的转换,在运行时刻检查类型安全。(down-cast)

  • 父类指针/引用向子类指针/引用的转换:如果这个指针实际上不是子类及其派生类的指针,会在转换时抛出异常

const_cast

用于修改类型的 constvolatile 属性。

  • 去除 const 属性,使变量可以修改(传给另一个指针来修改)
  • volatile 属性指的是变量不能被优化在寄存器中,每次修改必须访问内存

reinterpret_cast

低级别的、无类型检查的转换。可以在几乎任何类型间转换,但是非常危险。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇