1. 运算符重载简介
所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。同样运算符重载(Operator Overloading)可以让同一个运算符可以有不同的功能。
- 可以对 int、float、string 等不同类型数据进行操作
<< 既是位移运算符,又可以配合 cout 向控制台输出数据
也可以自定义运算符重载:
class Complex
{
public:
Complex();
Complex(double real, double imag);
Complex operator+(const Complex &a) const;
void display() const;
private:
double m_real;
double m_imag;
};
// ...
// 实现运算符重载
Complex Complex::operator+(const Complex &A) const{
Complex B;
B.m_real = this->m_real + A.m_real;
B.m_imag = this -> m_imag + A.m_imag;
return B;
// return Complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}
int main(){
Complex c1(4.3, 5.8);
Complex c2(2.7, 3.7);
Complex c3;
c3 = c1 + c2; // 运算符重载
c3.display();
return 0;
}
运算结果
7 + 9.5i
运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数,它本质上是函数重载。
c3 = c1 + c2;
实际上通过调用成员函数 operator+(),会转换为下面的形式:
c3 = c1.operator+(c2);
全局范围内重载运算符
运算符重载函数不仅可以作为类的成员函数,还可以作为全局函数。
复数加法运算通过全局范围内重载 + 实现:
class Complex{
// ...
friend complex operator+(const complex &A, const complex &B);
};
// 全局函数
Complex operator+(const Complex &A, const Complex &B)
{
return Complex(A.m_real + B.m_real, A.m_imag + B.m_imag); // 访问了Complex 的 private 成员变量,需要声明为友元函数
}
2. 运算符重载时要遵循的规则
-
能够重载的运算符
+ - * / % ^ & | ~ ! = < > += = = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , -> - > () [] new new[] delete delete[]
长度运算符 sizeof、条件运算符: ?、成员选择符. 和域解析运算符::不能被重载。
-
重载不能改变运算符的优先级和结合律
-
重载不会改变运算符的用法,即操作数个数、位置都不会改变
-
运算符重载函数不能有默认的参数,因为这改变了运算符操作数个数
-
运算符重载函数既可作为类成员函数,也可为全局函数,注意全局函数如何要访问类对象的私有成员,需要声明为类的友元
-
箭头运算符->、下标运算符[]、函数调用运算符()、赋值运算符 =,只能以成员函数的形式重载。
3. 重载运算符实现形式的选择
重载运算符可以通过成员函数和全局函数(友元)来实现
转换构造函数
Complex c1(25, 25);
Complex c2 = c1 + 15.6;
Complex c3 = 28.23 + c1; // 要以全局函数实现重载
这几行代码都可以顺利运行,说明 Complex 对象可以和 double 类型对象相加。其实,编译器在检测到 Complex 和 double(小数默认为 double 类型)相加时,会先尝试将 double 转换为 Complex,或者反过来将 Complex 转换为 double(只有类型相同的数据才能进行 + 运算),如果都转换失
败,或者都转换成功(产生了二义性),才报错。实际上,上述两行加法代码会转换为:
Complex c2 = operator+(c1,Complex(15.6));
Complex c3 = operator+(Complex(28.23),c1);
Complex(double real)
在作为普通构造函数的同时,还能将 double 类型转换为 Complex 类型,集合了“构造函数”和“类型转换”的功能,所以被称为「转换构造函数」。换句话说,转换构造函数用来将其它类型(可以是 bool、int、double等基本类型,也可以是数组、指针、结构体、类等构造类型)转换为当前类类型。
以全局函数形式重载
以全局函数的形式重载了 +、-、*、/、==、!=,这样做是为了保证这些运算符能够被对称的处理。
如果将 operator+定义为成员函数,根据“+ 运算符具有左结合性”这条原则,Complex c2 = c1 + 15.6;
会被转换为下面的形式:
Complex c2 = c1.operator+(Complex(15.6));
但是对于 Complex c3 = 28.23 + c1
,编译器会尝试转换为不同形式:
Complex c3 = (28.23).operator+(c1);
显然这是错误的。
以成员函数的形式重载
以成员函数的形式重载了 +=、-=、 *=、/=。
运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的, 是首选的。不过类的成员函数不能对称处理数据,运算符的第一个运算对象不会出现类型转换(要类的对象才能调用类的成员函数)。
C++ 规定,箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。
4. 重载 >> 和 <<(输入输出运算符)详解
C++ 标准库已对 左移运算符 << 和 >> 右移运算符进行了承载,如果我们定义新的类型需要输入输出运算符去处理,需要再进行重载。
重载运算符 >>
以全局函数的形式重载>>,使它能够读入两个 double 类型的数据,并分别赋值给复数的实部和虚部:
istream & operator>>(istream &in, Complex &A) // 友元
{
in >> A.m_real >> A.m_image;
return in;
}
之所以返回 istream 类 对象的引用,是为了能够连续读取复数,让代码书写更加漂亮,例如:
Complex c1,c2;
cin >> c1 >> c2;
如果不返回引用,需要一个一个读取:
cin >> c1;
cin >> c2;
实际上,上述 >> 运算符会被转换成如下形式:
operator<<(cin, c);
重载运算符 <<
ostream & operator<<(ostream &out, complex &A){
out << A.m_real <<" + "<< A.m_imag <<" i ";
}
5. 重载 [] 下标运算符
下标运算符 [] 必须以成员函数的形式进行重载。
int& Array::operator[](int i){
return m_p[i];
}
const int & Array::operator[](int i) const{
return m_p[i];
}
当 Array 是 const 对象时,如果没有提供 const 版本的 operator[],会报错。
6. 重载 ++ 和 -- 自增自减运算符
class Stopwatch{
// ... 秒表类
public:
Stopwatch operator++(); // ++i,前置形式
Stopwatch operator++(int); // i++,后置形式
Stopwatch run(); // 运行
private:
int m_min; // 分钟
int m_sec; // 秒钟
};
Stopwatch Stopwatch::run(){
++m_sec;
if(m_sec == 60){
m_min++;
m_sec = 0;
}
return *this;
}
Stopwatch Stopwatch::operator++(){
return run();
}
Stopwatch Stopwatch::operator++(int n){
Stopwatch s = *this; // i++, 先返回对象,再对象自增,所以需要将对象保存
run();
return s;
}
函数中参数 n 是没有任何意义的,它的存在只是为了区分是前置形式还是后置形式。
7. 重载 new 和 delete 运算符
内存管理运算符 new、new[]、delete 和 delete[] 也可以进行重载,其重载形式既可以是类的成员函数,也可以是全局函数。一般情况下,内建的内存管理运算符就够用了,只有在需要自己管理内存时才会重载。
// 成员函数
void * className::operator new( size_t size ){
//TODO:
}
void className::operator delete( void *ptr){
//TODO:
}
// 全局函数
void * operator new( size_t size ){
//TODO:
}
void operator delete( void *ptr){
//TODO:
}
在重载 new 或 new[] 时,无论是作为成员函数还是作为全局函数,它的第一个参数必须是 size_t 类型。size_t 表示的是要分配空间的大小,对于 new[] 的重载函数而言,size_t 则表示所需要分配的所有空间的总和。
size_t 在头文件
当然,重载函数也可以有其他参数,但都必须有默认值,并且第一个参数的类型必须是 size_t。
如果类中没有定义 new 和 delete 的重载函数,那么会自动调用内建的 new 和 delete 运算符。
8. 重载()(强制类型转换运算符)
类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当 重载后,(类型名)对象这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名(),即变成对运算符函数的调用。
class Complex{
public:
// ...
operator double()
{
return real;
}
private:
double m_real;
double m_imag;
};
int main()
{
Complex c(1.2,3.4);
cout << (double)c << endl; // 1.2
double n = 2 + c; // 等价于 double n = 2 + c.operator double()
}
9. 运算符重载总结
注意事项:
- 重载后运算符的含义应该符合原有用法习惯。例如重载 + 运算符,完成的功能就应该类似于做加法,在重载的+ 运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
- C++ 规定,运算符重载不改变运算符的优先级。
- 以下运算符不能被重载:.、.*、::、? :、sizeof。
- 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
-
运算符的实质是将运算符重载为一个函数,使用运算符的表达式就会被解释为对重载函数的调用。
-
运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。(友元)
-
运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。
-
必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。
-
<<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。
-
类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
-
自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。
-
运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。