运算符重载可以看做是一个有特殊名称的函数,由operator和要重载的运算符构成
return_type class_name::operator op(operand2) {}
return_type & class_name::operator op(operand2) {}
类成员函数运算符重载
重载一元运算符时没有参数,操作的是该对象本身,通过this指针隐式传递;重载二元运算符时,只需显示传递一个参数(作为二元运算符的右操作数),而左操作数是对象本身,用this指针隐式传递
运算符 | 操作符 | |
二元 | 算术运算符 | +,-,*,/,% |
关系运算符 | ==,!=,<,>,<=,>= | |
逻辑运算符 | ||,&&,! | |
位运算符 | |,&,~,……,<<,>>, | |
单目运算符 | +(正),-(负),*(指针),&(取地址)) | |
自增自减运算符 | ++,-- | |
赋值运算符 | =,+=,-=,/=,%=,&=,|=,^=,<<=,>>= | |
其他运算符 | ()(函数调用),->(成员访问),[](下标) | |
空间申请释放 | new,delete,new[],delete[] |
不可以重载的运算符:
- ::(作用域解析)
- .(成员访问)
- .*(通过指针成员的访问)
- ? :(三元运算符)
- 除new,delete外的关键字运算符
- # (预处理符号)
此外还有其他限制:
- 不能创建新运算符
- ->要么返回裸指针,要么按引用或值返回同样重载了运算符->的对象
- 不能更改运算符优先级,结合方向和操作数数量
-
&&与||的重载失去短路求值
&&与||的重载失去短路求值
先回顾之前的知识,对于A||B,我们知道当A为真时编译器就不会再检测B是否为真;同理对于A&&B,A为假时也不会检查B是否为真。而&&与||重载之后便会失去上述特性,不管A是否为真,编译器都会检查B的值。
我们来看一个例子:
class Boolean {
public:
Boolean() {}
Boolean(int val) :val(val) {}
bool operator && (const Boolean& other);
bool operator || (const Boolean& other);
int getVal() { return val; }
private:
int val;
};
bool Boolean::operator && (const Boolean& other){
if(val && other.val){
return true;
}
else{
return false;
}
}
bool Boolean::operator || (const Boolean& other){
if(val || other.val){
return true;
}
else{
return false;
}
}
Boolean booleanTest(Boolean i){
std::cout << "Boolean func(Boolean i) : i.val = " << i.getVal() << std::endl;
return i.getVal();
}
bool boolTest(int i){
std::cout << "bool func(int i) : i = " << i << std::endl;
return i;
}
这里为了方便观察在booleanTest和boolTest函数中加入了输出,然后用以下代码检测:
#include <iostream>
#include "Boolean.hpp"
using namespace std;
int main() {
const char* TRUE = "Result is true";
const char* FALSE = "Result is false";
int x, y;
cin >> x >> y;
Boolean a = Boolean(x);
Boolean b = Boolean(y);
if (boolTest(x) && boolTest(y)) {
cout << TRUE << endl;
}
else {
cout << FALSE << endl;
}
if (boolTest(x) || boolTest(y)) {
cout << TRUE << endl;
}
else {
cout << FALSE << endl;
}
if (booleanTest(a) && booleanTest(b)) {
cout << TRUE << endl;
}
else {
cout << FALSE << endl;
}
if (booleanTest(a) || booleanTest(b)) {
cout << TRUE << endl;
}
else {
cout << FALSE << endl;
}
return 0;
}
当x,y为3和4时,得到这样的输出:
bool func(int i) : i = 3
bool func(int i) : i = 4
Result is true
bool func(int i) : i = 3
Result is true
Boolean func(Boolean i) : i.val = 3
Boolean func(Boolean i) : i.val = 4
Result is true
Boolean func(Boolean i) : i.val = 3
Boolean func(Boolean i) : i.val = 4
Result is true
分析结果我们发现,在执行if (boolTest(x) || boolTest(y))语句的时候只输出了bool func(int i) : i = 3这是因为在boolTest(x) || boolTest(y)中boolTest(x)为真,所以并未执行boolTest(y)。然而,到了运算符重载的时候,我们发现两者都有输出,这就是我们所说的失去短路求值。
使x,y为0和1得到下面的输出:
bool func(int i) : i = 0
Result is false
bool func(int i) : i = 0
bool func(int i) : i = 1
Result is true
Boolean func(Boolean i) : i.val = 0
Boolean func(Boolean i) : i.val = 1
Result is false
Boolean func(Boolean i) : i.val = 0
Boolean func(Boolean i) : i.val = 1
Result is true
双目,单目的实现例子
Integer operator - (const Integer& Int){
return Integer(x - Int.x);
}
Integer operator - (){
return Integer(-x);
}
要注意的是左操作数必须是*this。对于双目运算中加减时,可以注意到我们是重新创建了一个新的类,然后将加减后的结果赋值给这个类,也就是说,双目运算符的加减时是不会改变类本身的值的。
自增运算符
前缀自增运算符()内无东西,后缀自增运算符()内有一个int的虚拟形参,但是这个虚拟形参并不会被使用,只是告诉编译器在后缀模式下被重载
实现例子:
Integer &Integer::operator ++ (){
x++;
return *this;
}//前缀自增
Integer Integer::operator ++ (int){
Integer a(x);
x++;
return a;
}//后缀自增
前缀一般增加后返回自身,所以习惯上在前面添加一个引用,然后返回*this;后缀一般先拷贝一个复制体,然后再自增,最后返回复制体。
自减与自增一样
赋值运算符
当有指针成员时要记得深拷贝赋值
Matrix& Matrix::operator=(const Matrix& other){
rows = other.rows;
cols = other.cols;
int* new_data = new int[rows * cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
int idx = i * cols + j;
new_data[idx] = other.data[idx];
}
}
delete [] data;
data = new_data;
return *this;
}
下标运算符
值得注意的是下标运算符[ ]和指针的成员->不适用于在类外定义
声明格式如下
Integer &operator[](int i);
const Integer &operator[](int i) const;
我们最好把两种形式都写了,这样做是为了适应const对象,因为通过const对象只能调用const成员函数,如果不提供const的形式则无法访问const对象的任何元素
在重载下标运算符时我们有时候可能会遇到类似于二维数组的情况:
int main() {
int m, n;
cin >> m >> n;
Matrix matrix(m, n);
cin >> matrix;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
matrix[i][j] += i;
}
}
cout << matrix;
return 0;
}
class Matrix {
public:
Matrix() :rows(0), cols(0), data(NULL) {}
Matrix(int rows, int cols) :rows(rows), cols(cols) {
data = new int[rows * cols];
}
~Matrix() {
delete[] data;
}
int* operator[](int );
const int* operator[](int ) const;
friend istream& operator>>(istream& is, Matrix& matrix);
friend ostream& operator<<(ostream& os, const Matrix& matrix);
private:
int rows, cols;
int* data;
};
由于下标运算符重载的返回类型为 int* 这时编译器只会对第一个[ ]进行重载,因为在对第一个[ ]重载了之后返回的int*与[ ]结合不会重载。如果返回类型为Matrix的话应该会与第二个[ ]进行重载。所以对于实际情况,在写下标运算符重载的时候我们应该倍乘矩阵的列数
int* Matrix::operator[](int i){
return data + cols * i;
}
const int* Matrix::operator[](int i) const{
return data + i * cols;
}
类外定义运算符重载
使用类外定义运算符重载,用户自定义的对象x也可以cin>>x读入,cout<<x输出;对于第一操作数不是该类的类型的左值对象(Integer c = 4 + a)也可以执行
struct Integer{
int x;
};
Integer operator+(const Integer&a, const Integer&b){
return Integer(a.x + b.x);
}
int main(){
Integer a = 3, b;
b = 4 + a;
}
在主函数中先会构造一个类并将4赋值给x,后进行运算符重载
但是上面的代码有一个问题,对于class来说,x是私有变量,这样外部函数operator+就无法访问x了,为解决这个问题,我们需要了解友元函数
友元函数
友元函数在类的内部声明friend普通函数,然后在类外实现该函数。友元函数不属于这个类,但是可以访问类的全部变量及函数
其他类的成员函数也可以是友元函数,其他类也可以是友元函数
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth(Box box);
friend class BigBox;//友元类
friend bool BigBox::operator()(const Box &a, const Box &b);
void setWidth(double wid);
};
class BigBox
{
public :
bool operator()(const Box &a, const Box &b);
void Print(int width, Box &box)
{
// BigBox是Box的友元类,它可以直接访问Box类的任何成员
box.setWidth(width);
cout << "Width of box : " << box.width << endl;
}
};
// 成员函数定义
void Box::setWidth(double wid)
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数,不要写成Box::printWidth()
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}
int main()
{
Box box;
BigBox big;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth(box);
// 使用友元类中的方法设置宽度
big.Print(20, box);
return 0;
}
当重载函数被声明为友元函数时,=, ->, [], ()由于不可以是全局域中的重载,故也不能重载为友元函数
输入/输出运算符重载
我们需要把运算符重载函数声明为类的友元函数,这样可以避免创建对象并且直接对其私有变量进行输入输出
class Fraction {
friend istream& operator>>(istream& is, Fraction& f);
friend ostream& operator<<(ostream& out, const Fraction& f);
};
istream& operator>>(istream& is, Fraction& f){
is >> f.numerator >> f.denominator;
int gcd = findGCD(f.numerator, f.denominator);
f.numerator /= gcd;
f.denominator /= gcd;
return is;
}
ostream& operator<<(ostream& out, const Fraction& f){
out << f.numerator << "/" << f.denominator;
return out;
}
对于上面的这句out << f.numerator << "/" << f.denominator;编译器会先输出f.numerator的值,后返回一个输出流对象out,这就是为什么我们可以一直输出下去的原因,所以一定要记得返回输出流对象。然后记得从流中提取输入的值和将要输出的值输出到流中
get函数
get()函数是cin输入流对象的成员函数,可以从输入流中提取一个字符并且赋值给括号内的字符
istream& operator>>(istream& is, String& s){
s.clear();
char c;
for( ; is.get(c) && isspace(c) == 0 ; ){
s += c;
}
return is;
}
String StringJoiner::operator()(const String& a, const String& b) const{
char short_time[a.getSize() + b.getSize() + 1];//没有'/0'的空间,内存测试不通过
char short_time[a.getSize() + b.getSize() + 2];//可以通过
strcpy(short_time, a.c_str());
strcat(short_time, ",");
strcat(short_time, b.c_str());
String res(short_time);
return res;
}
最后一点补充:对于字符串一定要留够末尾'/0'的空间,如没有则程序无法检测到字符串的结尾最后会导致内存泄露
由于学习不精,文章有不正确的地方望读者指正。