前言
c++对类的操作都可以通过类内成员函数来实现,还有函数重载等功能。那为什么还需要对运算符的重载呢?让我们一步一步对重载运算符进行了解:
一、运算符重载的引入
首先我们先来设计一个类
class person //人类
{
public:
void add(int age) //增加年龄
{
_age += age;
}
private:
int _age = 20; //年龄
string _name = "张三"; //姓名
};
我们可以创建一个人类对象,然后通过成员函数(add)对他的年龄进行增加。但是这样的代码一点都不直观,为了使我们的代码更加易于编写和阅读,我们设计了新的语法---运算符重载。通过新语法,我们可以编写一个新函数。
person operator+(person p, int age) // +运算符重载
{
p._age += age;
return p;
}
int main()
{
person p1;
p1 = p1 + 1;
cout << p1._age << endl; //打印结果 21
p1.add(1);
cout << p1._age << endl; //打印结果 22
//这两种调用效果相同,都是对年龄+1
return 0;
}
通过上述步骤,我们发现调用成员函数add 和 +号运算符重载实现的作用是相同的,并且优先级和结合律都和对应的内置符号保持一致。而且十分方便我们代码的阅读和编写。这就是运算符重载引入的原意,接下来我会对他的语法和使用进行解析。
二、语法解析
1.基础语法
函数的返回值: 我们规定运算符重载函数会按照不同的运算符设置不同的返回值,+ 号就会设置成返回类的对象,目的是内置类型的一样,比如:int a = b + c ; ( b + c 返回一个int)
特殊的函数名:语法规定函数名必须是 : operator + (运算符),比如:operator+,
operator-, operator* 等。
参数列表:
1, 和函数类似,编译器可以通过不同的参数传入来决定该调用哪个运算符重载(函数重载(可以看我以前的博客))。同时也可以显示调用
person operator+(person p, int age) { p._age += age; return p; } //和上面的 + 重载 person operator+(person p1, person p2) { p1._age += p2._age; return p1; } int main() { person p1, p2; //这两个调用是等价的 p1 = operator+(p1, 10); p1 = p1 + p2; return 0; }
2, 参数数量是需要和运算符对应的,+ 是二元运算符就需要两个参数,++是一元就需要一个。operator-, operator*等。
3,参数必须要有一个类类型,不能都为内置类型,不然会造成混乱。毕竟这个语法设计出来本来就是为了服务类类型(class和struct)
2.类内定义
但在实践中我们一般会将 运算符重载写在类内:
class person //人类
{
public:
person operator+(int age)
{
_age += age;
return *this;
}
int _age = 20; //年龄
string _name = "张三"; //姓名
};
但是这里为什么只有一个参数了呢? 其实有一个隐式参数是对象本身(参数列表的最左侧就是,只是看不见罢了,传参的时候要按顺序传), this 指针(很重要)。所以如果将运算符函数写在类内的话要少写一个参数。
int main()
{
person p1, p2;
//这两个调用是等价的
p1.operator+(10); //operator(*this,10) 伪代码
p1 = p1 + p2;
return 0;
}
后面我们都写类内的运算符重载,这是最常用的。但是有些运算符只能定义在类外,下面讲
3.各类运算符
(c++ Primer 第五版)
我会对特殊的运算符进行讲解,普通的比如 + - * / %之类的就不写了
++ 运算符
c++中有前置++和后置++,按照上面的取名方法,那名字就是重复的,无法区分。为了解决这个问题,规定后置++的参数列表里有一个 int 参数,编译器会传 0 来初始化 该参数,但一般没有作用,只是为了进行区分,- - 运算符同理
class person //人类
{
public:
//模仿内置 ++ 返回对应类型
person operator++(int) //后置++ int++
{
person ret = *this; //记录++前状态
_age++;
return ret; //拿到类对象
}
person& operator++()
{
_age++;
return *this;
}
int _age = 20; //年龄
string _name = "张三"; //姓名
};
int main()
{
person p1;
p1++; //调用person& operator++(int)
++p1; //调用person& operator++()
p1.operator++(10); //可以显示调用,后置++,但是10没用
return 0;
}
* -> 运算符
首先,* 和 ->在这个person类中没有实质功能,只是为大家讲解下语法。重点是 ->符号,按照下面我写的函数,他不是应该返回一个指针吗,那为什么我可以调用string类型的成员呢。
c++中规定了 operator->的重载如果返回对象是一个指针就再次进行一次 -> 操作,如果是自定义类型就查看是否有重载->, 有的话继续调用它的->。所以这里其实进行了一次隐式的 -> 操作。代码如下
class person //人类
{
public:
int _age = 20; //年龄
string _name = "张三"; //姓名
string& operator*()
{
return _name;
}
string* operator->()//
{
return &_name;
}
};
int main()
{
person p1;
*p1;
//以下两种调用等价
p1->c_str();
p1.operator->()->c_str();
//调用operator->()后拿到指针,编译器再次隐式调用->
return 0;
}
<< >> 运算符
我们正常使用比如:cout << "hello world" << endl; 所以对应的重载形式是:
operator<<(ostream& os, const operson& p)
但如果我们写在类内,但左边第一个一定是给this的,那我们调用就会成为
peoson << os; 太奇怪了,所以我们规定流符号的重载是放在类外的
//返回值和参数不仅要符合自定义,还必须是引用类型,因为流对象不能拷贝
ostream& operator<<(ostream& os,const person& p)
{ //os不能为const引用,因为流会改变,下面同理
os << p._age;
return os;
}
istream& operator>>(istream& is, person& p)
{
is >> p._age;
if (!is)
{ //如果流提却失败记得置为空,防止读取不全有安全隐患
p = person();
}
return is;
}
int main()
{
person p1;
person p2;
cout << "p1: " << p1 << endl;
cout << "p2: " << p2 << endl;
cin >> p1;
cout << "p1:" << p1 << " p2:" << p2;
return 0;
}
new delete 运算符
这部分太复杂了太长了,大家有兴趣可以自己去找别人的博客,这里就给大家放一段没什么实际作用的代码看一眼,看看语法
class person //人类
{
public:
int _age = 20; //年龄
string _name = "张三"; //姓名
void* operator new(size_t size, int i) = delete //如果不加delete,那重载的new和delete
//都会被调用然后打印东西
{
cout << i << endl;
return malloc(size);
}
void operator delete(void* ptr)
{
cout << "void operator delete(void* ptr)" << endl;
free(ptr);
}
};
int main()
{
person* p1 = new(20) person; //报错,不被通过,可以限制只在栈上创建变量,一种设计思想
delete p1;
return 0;
}
[ ] 运算符
大家看代码就能懂了,没什么特殊的
class person //人类
{
public:
int _age = 20; //年龄
string _name = "hello world"; //这里不能给中文了,因为编码的问题会有访问错误
char& operator[](size_t i)
{
return _name[i];
}
};
int main()
{
person p1;
p1[0]; // 是w
p1.operator[](10);
p1[0] = 'z'; //成功修改
return 0;
}
() 运算符
对()的重载很自由,参数数量,返回值类型,大家都可以随便写。就相当于自己设置了一个新函数(可以多个,构成函数重载就可以)。对这种类的重载,c++给了类专业名字:仿函数。运用地点:比如sort()函数的第三个参数,function模板类
class person //人类
{
public:
int _age = 20; //年龄
string _name = "hello world";
//随意实现功能
int operator()(int i) //随便实现,没有要求
{
return i + _age;//有类的特点,可以随时保存变量
}
};
int main()
{
person p1;
p1(10);//可以随意实现函数,可以拥有函数的特点,也有类的特点
return 0;
}
=运算符
= 运算符是编译器默认生成的八个类内函数之一,但只要自己写了,编译器就不会默认生成。这个运算符重载涉及的只是更多是关于类的其他知识,这里就不多讲解了
class person //人类
{
public:
int _age = 20; //年龄
string _name = "张三"; //姓名
//编译器会默认生成的八个函数之一
person& operator=(const person& p)
{
_age = p._age;
_name = p._name;
return *this;
}
};
int main()
{
person p1;
//person p2 = p1;注意如果这样写调用的是拷贝构造,不是operaotr=重载
person p2;
p2 = p1;
return 0;
}
三,重载技巧
1,如果我们重载了 != 符号,那我们实现 == 的时候可以直接调用 !=,节省代码量。<, > 等同理
class person //人类
{
public:
int _age = 20; //年龄
string _name = "hello world";
bool operator!=(string name)
{
return !(_name == name);
}
bool operator==(string name)
{
bool ret = operator!=(name);//复用!=
return !ret;
}
};
int main()
{
person p1;
person p2;
p1 == "hello world";
p1 != "hello world";
return 0;
}
2,一些运算符不应该被重载,比如 && , || 这类逻辑判断符号。重载了也没意义,要想实现对应的功能可以直接用成员函数
四,代码连接
我把代码都放gitee了:大家有需要可以自己拿: 博客资源: 这里存放博客中写的代码,欢迎大家来拿 - Gitee.com
写太多了可能会有错误,可以提出来,马上修改。有不懂的问题都可以随便提问