在C ++中,我们可以使操作符(operator)为用户定义的类在调用层可以像一般运算表达式一样参与运算。 这意味着C ++能够为操作符提供数据类型的特殊含义,这种能力称为操作符重载(operator overloading)。例如,我们可以在string之类的类中重载操作符“ +”,以便仅使用+即可连接两个字符串。
重载“+”操作符
下面我们用一个类似超市的结算小程序来作为本小节的示例
Good类接口
#ifndef GOOD_HH
#define GOOD_HH
#include <string>
class Good
{
std::string d_name;
double d_price;
public:
Good();
Good(std::string, double);
double price() const;
void set_price(double price);
std::string name() const;
bool operator<(const Good &obj) const;
//两个Good实例 1+Good实例2
Good operator+(Good const &good);
};
#endif
Customer接口
#ifndef CUSTOMER_HH
#define CUSTOMER_HH
#include "../header/good.hh"
#include <iostream>
#include <map>
#include <string>
class Customer
{
std::string d_fullname; //用户名
double d_pay; //应付金额
double d_bonus; //奖励积分
double d_count; //购买数量
double d_discnt; //让利总计
std::map<Good, double> cart;
public:
Customer(std::string, double);
//购买
void buy(const Good, double);
//支付
void payment();
//结帐信息
void display_info();
private:
//折扣
void discount(const Good &, double);
};
#endif
类实现
下面的重点就是 Good Good::operator+(Good const &obj)的实现,“+”操作符的重载函数内部封装了两个Good类中的d_name字符串的拼接操作,以及两个Good对象的d_price的加法赋值。
Good Good::operator+(Good const &obj)
{
this->d_price += obj.d_price * 0.95;
this->d_name = this->d_name + "和" + obj.d_name;
}
调用代码
int main(int argc, char const *argv[])
{
Good a{"香蕉", 5.7};
Good b{"奇异果", 13.4};
Good c{"榴莲", 13.5};
Good d{"苹果", 5.7};
Good e{"奶蕉", 7.7};
Good r = a + e;
Customer p1{"Lisa", 800};
p1.buy(a, 23);
p1.buy(c, 12);
p1.buy(d, 32);
p1.buy(r, 45);
p1.payment();
p1.display_info();
return 0;
}
程序输出
因此,我们知道重载操作符实质上就是重载函数,函数内部封装了运算对象(即:对象内部各种数据成员)的对应运算操作。
重载operator[]
除非你是你自己实现类似动态数组的顺序存储结构,才需要重载索引操作符,使用标准库的的顺序容器,重载索引操作符显得有些画蛇添足。
以下是有关[]重载的一些特殊情况
- 当我们自己实现的顺序存储结构,需要检查索引越界问题,[]的重载可能很有用。
- 我们必须在函数中通过引用返回,因为像“ arr [i]”之类的表达式可以用作左值。
关于重载下标运算符的具体示例可以看我这篇文章,里面说的很详细
《C++ 数据结构--动态顺序表的实现》
重载operator ++
重载增量运算符(operator)和减量运算符(operator--)会带来一个小问题:每个运算符都有两个版本,因为它们可以用作后缀运算符(例如x)或用作前缀运算符(例如x)。
用作后缀运算符时,该值的对象作为右值,临时const对象返回,且后递增变量本身从视图中消失。 用作前缀运算符时,变量会递增,其值将作为左值返回,并且可以通过修改前缀运算符的返回值来再次更改。 尽管在运算符重载时不需要这些特性,但强烈建议在任何重载的增量或减量运算符中实现这些特性。
假设我们围绕size_t值类型定义一个包装器类。 这样的类可以提供以下(部分显示)接口:
class Customer
{
size_t d_bonus; //奖励积分
public:
Customer(std::string, size_t);
Customer &operator++();
}
类的最后一个成员声明前缀重载的增量运算符。 返回的左值是Customer&。 该成员很容易实现:
Customer &Customer::operator++()
{
++d_bonus;
return *this;
}
要定义后缀运算符,需要定义该运算符的重载版本,并期望使用(虚拟)int参数。 这可能被认为是错误的,或者是函数重载的可接受的应用程序。 无论您对此有何看法,都可以得出以下结论:
- 没有参数的重载增量和减量运算符是前缀运算符,应返回对当前对象的引用。
- 具有int参数的重载增量和减量运算符是后缀运算符,并且应在使用后缀运算符的位置返回一个值,该值是对象的副本。
后缀增量运算符在Customer类的接口中声明如下:
//后缀增量操作符重载
Customer operator++(int);
实现如下,请注意,运算符的参数并未使用。 在实现和声明中消除前缀和后缀运算符的歧义只是实现的一部分。
Customer Customer::operator++(int)
{
Customer tmp{*this};
++d_bonus;
return tmp;
}
在上面的示例中,增加当前对象的语句提供了空位保证,因为它仅涉及对原始类型的操作。 如果初始副本构造抛出异常,则原始对象不会被修改,如果return语句抛出异常,则对象已被安全地修改。 但是增加一个对象本身可能会引发异常。 在这种情况下,如何实现增量运算符? 再次,swap是我们最好的选择。 当数据成员增量执行增量操作可能抛出时,以下是前缀和后缀运算符提供了有力的保证:
重载new操作符和delete操作符
当运算符new重载时,它必须定义一个void *返回类型,并且其第一个参数的类型必须为size_t。 默认运算符new仅定义一个参数,但是重载版本可以定义多个参数。 第一个没有明确指定,但是从重载了new运算符的类的对象的大小推导得出。 在本节中,将讨论重载运算符new。
new和delete操作符的作用域
- 如果使用某个类的成员函数来重载这些运算符,则意味着这些运算符仅针对该特定类才被重载
- 如果重载是在类外完成的(即它不是类的成员函数),则只要您使用这些运算符(在类内或类外),都将调用重载的“ new”和“ delete”。 这是全局超载。
new运算符的函数原型
void *operator new(size_t n);
重载的new运算符接收的大小为size_t类型,该大小指定要分配的内存字节数。 重载的new的返回类型必须为void *。重载的函数返回一个指向分配的内存块开头的指针。
delete运算符的函数原型
void operator delete(void*);
该函数接收一个必须删除的void *类型的参数。 函数不应该返回任何东西。
注意:默认情况下,重载的new和delete运算符函数都是静态成员。 因此,他们无权访问此指针。
类接口
class Good
{
std::string d_name;
double d_price;
public:
Good(std::string, double);
//重载new操作符原型
void *operator new(size_t n);
//重载delete操作符
void operator delete(void *);
void display();
};
类实现
void *Good::operator new(size_t n)
{
std::cout << "重载new操作符" << std::endl;
void *p = ::new Good();
return p;
}
void Good::operator delete(void *p)
{
std::cout << "重载delete操作符" << std::endl;
free(p);
p = NULL;
}
void Good::display()
{
std::cout << "品名:" << d_name << std::endl;
std::cout << "价格:" << d_price << "RMB" << std::endl;
}
调用代码
#include "source/good.cpp"
#include <iostream>
int main(int argc, char const *argv[])
{
Good *g = new Good("香焦", 10);
g->display();
delete g;
return 0;
}
在上面的新重载函数中,是通过new运算符分配了动态内存,但是它是全局的new运算符,否则它内部将递归调用因为new的内容将一次又一次地重载,就像下面的代码
void *p=new Good(); //错误的示例
正确的示例,通过::作用域操作符,从全局调用new操作符
void *p=::new Good();
在全局作用域重载new和delete操作符
void *operator new(size_t n){
std::cout<<"全局作用域重载new操作符"<<std::endl;
void *p=malloc(n);
return p;
}
void operator delete(void *p){
std::cout<<"全局作用域重载delete操作符"<<std::endl;
free(p);
p=NULL;
}
int main(){
int n=5;
int *p=new int[5];
for(int i=0;i<n;i++){
p[i]=i;
}
for(size_t i=0;i<n;i++){
std::cout<<p[i]<<std::endl;
}
delete p;
}
在上面的代码中,在new操作符的重载函数中,我们无法使用::new int [5]分配内存,因为它将以递归方式进行。 我们只需要使用malloc分配内存。
输出
全局作用域重载new操作符
0 1 2 3 4
全局作用域重载delete操作符
重载new/delete操作符的原因
- 重载的new运算符函数可以接受参数; 因此,一个类可以具有多个重载的new运算符的能力。 这使程序员在自定义对象的内存分配方面具有更大的灵活性。 例如:
void *operator new(size_t n,
- 重载的new或delete运算符还为类的对象提供了垃圾回收。
- 可以在重载的新运算符函数中添加异常处理例程。
- 提供了重载版本的new和delete操作符能够做一些编译器默认版本的new和delete不能做的自定义操作。 例如,您可能会编写一个自定义运算符delete,以用0覆盖释放的内存,以提高应用程序数据的安全性。
- 可以在新函数中使用realloc函数动态重新分配内存。
- 重载的新运算符还使程序员能够从程序中榨取一些额外的性能。 例如,在一个类中,为了加快新节点的分配速度,维护了一个已删除节点的列表,以便在分配新节点时可以重用它们的内存。在这种情况下,重载的delete运算符会将节点添加到列表中 删除的节点和重载的new运算符将从列表中分配内存,而不是从堆中分配内存以加速内存分配。 当删除的节点列表为空时,可以使用堆中的内存。
operator函数和普通函数有什么区别?
- operator函数与普通函数相同
- 唯一的区别是,操作符的名称始终是操作符operator关键字,后跟operator关键字的符号,并且在使用相应的操作符时会调用operator函数。
- 几乎所有的操作符可以重载,但以下C++内置的操作符号是无法重载的。
重载操作符的注意事项
- 为了使操作符重载起作用,至少一个操作数必须是用户定义的类对象。
- 赋值操作符:编译器会为每个类自动创建一个默认的赋值操作符。 默认的赋值操作符确实将右侧的所有成员分配到左侧,并且在大多数情况下都可以正常工作(此行为与复制构造函数相同)。 有关更多详细信息,请参见此内容。
- 转换操作符:我们还可以编写可用于将一种类型转换为另一种类型的转换操作符。
操作员重载规则
- 仅内置操作符可以重载。 无法创建新的操作符。
- 操作符的优先级和关联性不能更改。
- 重载的操作符不能具有默认参数,但函数调用operator() 可以具有默认参数。
- 不能仅对内置类型重载操作符。 必须至少使用一个操作数定义类型
- 必须将赋值(=),下标([]),函数调用(“()”)和成员选择(->)操作符定义为成员函数