C++运算符重载

运算符重载

当运算符作用域类类型对象时,可以通过运算符重载重新定义该运算符的含义,明智使用运算符重载能令我们的代码更易于编写和阅读。

重载的运算符是具有特殊名字的函数,它们的名字由关键字operator和其后要定义的运算符号共同组成,重载运算符函数的参数数量与该运算符的运算对象数量易于多,一元运算符有一个参数,二元运算符有两个参数,除了重载运算符operator()外,其它重载运算符不能有默认实参。

如果一个运算符函数是成员函数,则它的第一个运算对象绑定到隐式的this指针上,因为成员运算符函数的显式参数数量比运算符的运算对象少一个。

运算符函数至少含有一个类类型的参数,不能重定义内置类型的运算符。

//错误,不能重定义int类型的加法运算符
int operator+(int value1, int value2);

哪些运算符可以重载

大部分运算符都能被重载,有些运算符则不行,例如::,.*,.,? :

逻辑运算符&&,||会先求左侧运算对象的值,再求右侧对象的值,仅当左侧运算对象无法确定表达式的结果时才会再求右侧运算对象的值,这种策略称为短路求值

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值,它会首先对左侧的表达式求值,然后将求值结果丢弃,逗号表达式的真正的结果是右侧表达式的值。

重载逻辑运算符&&,||和逗号运算符会破坏求值顺序或短路求值特性,所以不建议重载它们。

对于类类型对象,取地址运算符有内置的含义,一般也不应该被重载。

使用与内置类型一致的含义

  • 如果类进行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
  • 如果类的某个操作时检查想等性,则定义operator==,如果有了operator==则通常也应该有operator!=。
  • 如果类包含有单序比较操作,则定义operator<,如果有了operator<,则通常也应该有其它关系操作。
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容,例如逻辑运算符&&应该返回bool,算符运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符应该返回左侧运算对象的一个引用。
  • 如果类重载了算术运算符或者位运算符,最好也提供对应的复合赋值运算符,例如+=,~=。
  • 明智使用运算符重载,不要滥用这一特性造成误解。

使用成员函数还是非成员函数

  • 赋值=,下标[],调用(),成员访问箭头->运算符必须是成员函数。
  • 复合赋值运算符通常应该是成员函数。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增,递减,解引用运算符,通常应该是成员函数。
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术,想等性,关系,位运算等,它们通常应该是普通的非成员函数。

输入和输出运算符

与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是成员函数,否则它们的左侧运算对象将是我们类的一个对象。

通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,因为向流写入内容会改变其状态,引用是因为我们无法直接复制一个ostream对象。第二个形参一般是一个常量的引用,引用是因为我们希望避免复制实参,之所以是常量是因为输出对象不会改变对象内容。返回值一般要返回它的ostream形参。

与输出运算符不同,输入运算符的第二个参数必须是非常量的,因为输入运算符会改变对象。

class Person
{
public:
    int height = 180;
};

std::ostream& operator<<(std::ostream& os, const Person& person)
{
    os << "person.height=" << person.height;
    return os;
}

std::istream& operator>>(std::istream& is, Person& person) 
{
    is >> person.height;
    if (!is) {
        person.height = 0;
    }
    return is;
}

int main(void)
{
    Person person;
    std::cin >> person;
    std::cout << person;
    system("pause");
    return 0;
}

算术运算符

算术运算符通常定义为非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参一般是常量的引用。算术运算符通常会计算它的两个运算对象并得到一个新值,所以算符运算符返回值通常是新值的副本。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

Money operator+(const Money& money1, const Money& money2)
{
    return Money(money1.value+money2.value);
}


int main(void)
{
    Money money1{10};
    Money money2{ 50 };
    Money money3 = money1 + money2;
    std::cout << money3.value;
    system("pause");
    return 0;
}

相等运算符

相等运算符与算术运算符类似,通常也是非成员函数,形参为常量的引用,但是它的返回值是bool。

如果一个类含有判断两个对象是否相等的操作,最好把函数函数定义为operator==而不是一个普通的命名函数,这样更容易理解。

相等运算符应该有传递性,即a==b和b==c都为真,则a==c也为真。

如果类定义了operator==,则也应该定义operator!=,相等运算符和不相当运算符中的一个应该把工作委托给另外一个,这样可以复用代码。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

bool operator==(const Money& money1, const Money& money2)
{
    return money1.value == money2.value;
}

bool operator!=(const Money& money1, const Money& money2)
{
    return !(money1==money2);
}

int main(void)
{
    Money money1{ 10 };
    Money money2{ 50 };
    Money money3{ 10 };
    std::cout << ((money1 == money2) ? "true" : "false") << std::endl;//false
    std::cout << ((money1 == money3) ? "true" : "false") << std::endl;//true
    std::cout << ((money1 != money2) ? "true" : "false") << std::endl;//true
    system("pause");
    return 0;
}

关系运算符(大于和小于)

当需要对某个类的对象进行排序时可以定义关系运算符,如果类同时包含相等运算符,则关系运算符的定义和相等运算符的结果应该是一致的。

如果类中有多个数据成员可以用来比较,例如Person类中有height,age等,这种类不定义关系运算符比较好,不然容易造成歧义。

对某个类定义了大于,小于,等于运算符后,不会自动定义小于等于和大于等于运算符。

class Money
{
public:
    int value = 0;
    Money(int value) 
    {
        this->value = value;
    };
};

bool operator==(const Money& money1, const Money& money2)
{
    return money1.value == money2.value;
}

bool operator!=(const Money& money1, const Money& money2)
{
    return !(money1==money2);
}

bool operator<(const Money& money1, const Money& money2)
{
    return money1.value < money2.value;
}

bool operator<=(const Money& money1, const Money& money2)
{
    return (money1 < money2 || money1 == money2);
}

bool operator>(const Money& money1, const Money& money2)
{
    return money1.value > money2.value;
}

bool operator>=(const Money& money1, const Money& money2)
{
    return (money1 > money2 || money1 == money2);
}

int main(void)
{
    Money money1{ 10 };
    Money money2{ 50 };
    Money money3{ 10 };
    std::cout << ((money1 > money2) ? "true" : "false") << std::endl;//false
    std::cout << ((money1 < money2) ? "true" : "false") << std::endl;//true
    std::cout << ((money1 <= money3) ? "true" : "false") << std::endl;//true
    std::cout << ((money2 >= money3) ? "true" : "false") << std::endl;//true
    system("pause");
    return 0;
}

赋值运算符

之前已经介绍过拷贝赋值和移动赋值运算符,它们可以把类的一个对象赋值给该类的另一个对象。除此之外,类还可以定义其他赋值运算符以使用别的类型作为右侧使运算对象。

下标运算符

下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标运算符可以出现在赋值运算符的任意一端,另外我们最好同时定义下标运算符的常量版本和非常量版本。

class Http
{
private:
    std::string headers_[3] = {"hello ","world","!"};
public:
    std::string& operator[](std::size_t n) 
    {
        return headers_[n];
    }
    const std::string& operator[](std::size_t n) const
    {
        return headers_[n];
    }
};


int main(void)
{
    Http http;
    std::cout << http[0]<<http[1]<<http[2] << std::endl;
    system("pause");
    return 0;
}

递增和递减运算符

由于递增和递减运算符会改变操作对象的状态,所以最好定义为成员函数。定义递增和递减运算符的类应该同时定义前置版本和后置版本。

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用,后置运算符应该返回对象的原值(递增和递减之前),返回的形式是值而非引用。

由于前置版本和后置版本的递增或递减运算符使用的是同一个符号,为了区分它们,后置版本接受一个额外的不被使用的int类型的形参,这个形参的唯一作用就是区分前置版本和后置版本。

class Money
{
private:
    int value_ = 0;
public:
    int getValue() { return value_;}
    Money& operator++()
    {
        ++value_;
        return *this;
    }

    Money& operator--()
    {
        --value_;
        return *this;
    }

    Money operator++(int)
    {
        Money oldMoney = *this;
        ++*this;
        return oldMoney;
    }

    Money operator--(int)
    {
        Money oldMoney = *this;
        --*this;
        return oldMoney;
    }
    
};


int main(void)
{
    Money money;
    std::cout << money.getValue() << std::endl;//0
    std::cout << (++money).getValue()  << std::endl;//1
    std::cout << (--money).getValue() << std::endl;//0
    std::cout << (money++).getValue() << std::endl;//0
    std::cout << (money--).getValue() << std::endl;//1
    std::cout << money.getValue() << std::endl;//0
    system("pause");
    return 0;
}

成员访问运算符

成员访问运算符包括解引用运算符和箭头运算符,箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此。

我们可以重载解引用运算符完成任何想要的操作,但是箭头运算符永远不能丢掉成员访问这个基本的含义,重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

对于形如object->member的表达式来说
1.如果object是一个对象的指针,则会执行(*object).member。
2.如果object是一个重载了箭头运算符的对象,则会调用重载的箭头运算符计算结果,如果结果依然是一个指针,则执行第一步,如果结果本身也重载了箭头运算符,则重复调用当前步骤。

class Money
{
private:
    int value_ = 0;
    std::string hello_ = "hello,world!";
public:
    std::string& operator*()
    {
        return hello_;
    }
    
    std::string* operator->() 
    {
        return &this->operator*();
    }
    
};


int main(void)
{
    Money money;
    std::cout << (*money).c_str() << std::endl;
    std::cout << money->c_str() << std::endl;
    system("pause");
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容

  • C++运算符重载-下篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 1,440评论 0 49
  • C++运算符重载-上篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 2,296评论 0 51
  • C++语言的一个很有意思的特性就是除了支持函数重载外还支持运算符重载,原因就是在C++看来运算符也算是一种函数。比...
    欧阳大哥2013阅读 2,706评论 0 8
  • C++运算符重载的实质:运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的C++多态。目的在于让人能够...
    Nancy_Shi阅读 1,046评论 1 3
  • 运算符重载规则 1.被重载的运算符必须是已经存在的C++运算符,不能重载自己创建的运算符; 2.运算符被重载之后,...
    随波逐流007阅读 725评论 0 1