本文主要是介绍【C++从入门到入土】第四篇:运算符重载,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
系列文章目录
【C++从入门到入土】第一篇:从C到C++.
【C++从入门到入土】第二篇:类和对象基础.
【C++从入门到入土】第三篇:类和对象提高.
文章目录
- 系列文章目录
- 前言
- 运算符重载
- 1、定义和相关规则
- 运算符重载的形式
- 2、赋值运算符的重载
- 浅拷贝和深拷贝
- 对 operator = 返回值类型的讨论
- 3、运算符重载为友元函数
- 4、运算符重载实例:可变长整型数组(类似vector)
- 5、流插入运算符和流提取运算符的重载
- 6、类型转换运算符的重载
- 7、自增、自减运算符的重载
- 8、函数调用运算符重载
- 总结
前言
- 在数学上,两个复数可以直接进行+、-等运算。但 在C++中,直接将+或-用于复数对象是不允许的。
- 有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。
- 例如: complex_a和complex_b是两个复数对象; 求两个复数的和, 希望能直接写:
complex_a + complex_b
所以让我们对运算符进行重载,满足自身的需求。
本文较长,请耐心阅读。
运算符重载
1、定义和相关规则
- 运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。
- 运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。
- 同一个运算符,对不同类型的操作数,所发生的行为不同。
- 运算符重载的实质是函数重载
以下是运算符重载的规则:
- 不可以引人新的运算符。除了 . 、.* 、:: 、? :四个运算符,其他的运算符皆可被重载。
- 运算符的操作数(operand)个数不可改变。每个二元运算符都需要两个操作数,每个一元运算符都需要恰好一个操作数。因此,我们无法定义出一个 equality运算符,并令它接受两个以上或两个以下的操作数。
- 运算符的优先级(precedence)不可改变。例如,除法的运算优先级永远高于加法。
- 运算符函数的参数列表中,必须至少有一个参数为 class类型。也就是说,我们无法为诸如指针之类的non-class类型,重新定义其原已存在的运算符,当然更无法为它引进新运算符。
运算符重载的形式
- 运算符重载的实质是函数重载
- 可以重载为普通函数,也可以重载为成员函数
- 把含运算符的表达式转换成对运算符函数的调用。
- 把运算符的操作数转换成运算符函数的参数。
- 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。
定义形式:
返回值类型 operator 运算符(形参表)
{
……
}
重载实例:
class Complex
{
public:double real,imag;Complex( double r = 0.0, double i= 0.0 ):real(r),imag(i) { }Complex operator-(const Complex & c);
};
Complex operator+( const Complex & a, const Complex & b)
{return Complex( a.real+b.real,a.imag+b.imag); // 返回一个临时对象
}
Complex Complex::operator-(const Complex & c)
{return Complex(real - c.real, imag - c.imag); // 返回一个临时对象
}
int main()
{Complex a(4,4),b(1,1),c;c = a + b; // 等价于c=operator+(a,b);cout << c.real << "," << c.imag << endl;cout << (a-b).real << "," << (a-b).imag << endl;//a-b 等价于a.operator-(b)return 0;
}
特点:
重载为成员函数时 , 参数个数为运算符目数减一 。
重载为普通函数时 , 参数个数为运算符目数 。
2、赋值运算符的重载
赋值运算符 ‘=’重载
有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。
- 赋值运算符“=”只能重载为成员函数赋值。
代码如下 :
class String {
private:char * str;
public:String ():str(new char[1]){ str[0] = 0;}const char * c_str() { return str; };String & operator = (const char * s);~String( ) { delete [] str; }
};
String & String::operator = (const char * s)
{ // 重载“=”得 以使得 obj = “hello” 能够成立delete [] str;str = new char[strlen(s)+1];strcpy( str, s);return * this;
}
int main()
{String s;s = "Good Luck," ; //等价于 s.operator=("Good Luck,");cout << s.c_str() << endl;// String s2 = "hello!"; //这条语句要是不注释掉就会出错//因为这里不是使用赋值语句而是初始化,应调用构造函数s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");cout << s.c_str() << endl;return 0;
}
输出:
Good Luck,
Shenzhou 8!
浅拷贝和深拷贝
引用上面的类说明此问题
有如下代码:
String S1, S2;
S1 = "this";
S2 = "that";
s1 = s2;
执行完这些语句,真的能达到我们想要效果吗?
当然没有,甚至很多时候会发生内存泄漏等重大错误。
- 如不定义自己的赋值运算符,那么S1=S2实际上导致 S1.str和 S2.str 指向同一地方。
- 如果S1对象消亡,析构函数将释放 S1.str指向的空间,则S2消亡时还 要释放一次,不妥。
- 另外,如果执行 S1 = “other”;会导致S2.str指向的地方被delete。
因此要在 class String里添加成员函数,定义一个属于String类的赋值运算
String & operator = (const String & s)
{delete [] str;str = new char[strlen( s.str)+1];strcpy( str,s.str);return * this;
}
这么做就够了吗?还有什么需要改进的地方?
考虑下面语句:
String s;
s = "Hello";
s = s;
是否会有问题?
如果这样会开辟两片相同的空间,存放相同的数据,但是却都由s.str指向,明显错误,无法执行。
解决办法:
加一个判断两个变量是否相同
String & operator = (const String & s)
{if( this == & s)return * this;delete [] str;str = new char[strlen(s.str)+1];strcpy( str,s.str);return * this;
}
对 operator = 返回值类型的讨论
void 好不好?
String 好不好?
为什么是 String &
- 对运算符进行重载的时候,好的风格是应该尽量保留运算符原本的特性
考虑: a = b = c;
和 (a=b)=c; //会修改a的值
分别等价于:
a.operator=(b.operator=( c ));
(a.operator=(b)).operator=( c );
上面的String类是否就没有问题了?
为 String类编写拷贝构造函数的时候,会面临和 = 同样的问题(浅深拷贝),用同样的方法处理。
String( String & s)
{str = new char[strlen(s.str)+1];strcpy(str,s.str);
}
3、运算符重载为友元函数
- 一般情况下,将运算符重载为类的成员函数,是较好的选择。
- 但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
例如:
class Complex
{double real,imag;
public:Complex( double r, double i):real(r),imag(i){ };Complex operator+( double r );
};
Complex Complex::operator+( double r )
{ //能解释 c+5return Complex(real + r,imag);
}
经过上述重载后:
Complex c ;
c = c + 5; // 有定义,相当于 c = c.operator +(5);
但是:
c = 5 + c; //编译出错
所以,为了使得上述的表达式能成立,需要将 + 重载为普通函数。
Complex operator+ (double r,const Complex & c)
{ //能解释 5+creturn Complex( c.real + r, c.imag);
}
但是普通函数又不能访问私有成员,所以,需要将运算符 + 重载为友元。
class Complex
{double real,imag;
public:Complex( double r, double i):real(r),imag(i){ };Complex operator+( double r );friend Complex operator + (double r,const Complex & c);
};
4、运算符重载实例:可变长整型数组(类似vector)
首先确定我们要实现的功能
- 可变长 -> 动态开辟内存
- 能用数组初始化 -> 重写拷贝构造函数
- 有时需要用到赋值运算 -> 重载赋值运算符
- 需要输出类似a[i]的值 -> 重载下标运算符 ’ [ ] ’
代码如下:
int main()
{ //要编写可变长整型数组类,使之能如下使用:CArray a; //开始里的数组是空的for( int i = 0;i < 5;++i)a.push_back(i);CArray a2,a3;a2 = a; //重载“=”for( int i = 0; i < a.length(); ++i )cout << a2[i] << " " ;a2 = a3; //a2是空的for( int i = 0; i < a2.length(); ++i ) //a2.length()返回0cout << a2[i] << " ";cout << endl;a[3] = 100;CArray a4(a);//要自己写拷贝构造函数for( int i = 0; i < a4.length(); ++i )cout << a4[i] << " ";return 0;
}
接下来开始设置类
class CArray {
int size; //数组元素的个数
int *ptr; //指向动态分配的数组
public:CArray(int s = 0); //s代表数组元素的个数CArray(CArray & a);~CArray();void push_back(int v); //用于在数组尾部添加一个元素vCArray & operator=( const CArray & a);//用于数组对象间的赋值int length() { return size; } //返回数组元素个数_____ CArray::operator[](int i) //返回值是什么类型?{ //用以支持根据下标访问数组元素,// 如n = a[i] 和a[i] = 4; 这样的语句return ptr[i];}
};
上述重载下标运算符 [] 需要什么返回值呢?
因为我们需要支持如n = a[i] 和a[i] = 4; 这样的语句
所以需要引用,它能当左值。
具体函数实现:
CArray::CArray(int s):size(s)
{if( s == 0)ptr = NULL;elseptr = new int[s];
}
CArray::CArray(CArray & a)
{if( !a.ptr) {ptr = NULL;size = 0;return;}ptr = new int[a.size];memcpy( ptr, a.ptr, sizeof(int ) * a.size);size = a.size;
}
CArray::~CArray()
{if( ptr) delete [] ptr;
}
CArray & CArray::operator=( const CArray & a)
{ // 赋值号的作用是使“=” 左边对象里存放的数组,大小和内容都和右边的对象一样if( ptr == a.ptr) // 防止a=a 这样的赋值导致出错return * this;if( a.ptr == NULL) { // 如果a 里面的数组是空的//判断this->ptr是否为空if( ptr ) delete [] ptr;ptr = NULL;size = 0;return * this;}if( size < a.size) { // 如果原有空间够大,就不用分配新的空间if(ptr)delete [] ptr;ptr = new int[a.size];}memcpy( ptr,a.ptr,sizeof(int)*a.size);size = a.size;return * this;
}
void CArray::push_back(int v)
{ // 在数组尾部添加一个元素if( ptr) {int * tmpPtr = new int[size+1]; // 重新分配空间memcpy(tmpPtr,ptr,sizeof(int)*size); // 拷贝原数组内容delete [] ptr;ptr = tmpPtr;}else // 数组本来是空的ptr = new int[1];ptr[size++] = v; // 加入新的数组元素
}
5、流插入运算符和流提取运算符的重载
- cout 是在 iostream 中定义的,ostream 类 的对象。
- “<<” 能用在cout 上是因为,在iostream里对 “<<” 进行了重载。 考虑,怎么重载才能使得cout << 5; 和cout << “this”都能成立 ?
因为语句cout<<5<<“this”;能成立,即<<能持续作用,既可作为右值也可作为左值,所以<<的返回值应该为ostream类的引用。
猜测代码如下:
ostream & ostream::operator<<(int n)
{…… // 输出n 的代码return * this;
}
ostream & ostream::operator<<(const char * s )
{…… // 输出s 的代码return * this;
}
cout << 5 << “this”;
本质上的函数调用的形式是什么?
cout.operator<<(5).operator<<(“this”);
当我们对一个类输出时,可对" << "重载:
ostream & operator<<( ostream & o,const String & s)
{o << s.ptr ;return o;
}
对" >> "也同样如此重载,需要访问类的私有成员时可重载为友元函数。
6、类型转换运算符的重载
需要类和内置类型互相转换时可用
#include <iostream>
using namespace std;
class Complex
{double real,imag;
public:Complex(double r=0,double i=0):real(r),imag(i) { };operator double () { return real; }//重载强制类型转换运算符 double
};
int main()
{Complex c(1.2,3.4);cout << (double)c << endl; //输出 1.2double n = 2 + c; //等价于 double n=2+c.operator double()cout << n; //输出 3.2
}
7、自增、自减运算符的重载
自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前
置运算符还是后置运算符,C++规定:
前置运算符作为一元运算符重载
重载为成员函数:
T & operator++();
T & operator–();
重载为全局函数:
T1 & operator++(T2);
T1 & operator—(T2);
后置运算符作为二元运算符重载,多写一个没用的参数:
重载为成员函数:
T operator++(int);
T operator–(int);
重载为全局函数:
T1 operator++(T2,int );
T1 operator—( T2,int);
接下来为函数实现:
class CDemo
{private :int n;public:CDemo(int i=0):n(i) { }CDemo & operator++(); // 用于前置形式CDemo operator++( int ); // 用于后置形式operator int ( ) { return n; }friend CDemo & operator--(CDemo & );friend CDemo operator--(CDemo & ,int);
}
CDemo & CDemo::operator++()
{ //前置 ++n ++;return * this;
} // ++s即为: s.operator++();CDemo CDemo::operator++( int k )
{ //后置 ++CDemo tmp(*this); // 记录修改前的对象n ++;return tmp; // 返回修改前的对象
}
CDemo & operator--(CDemo & d)
{ // 前置--d.n--;return d;
} //--s即为: operator--(s);
CDemo operator--(CDemo & d,int)
{ // 后置--CDemo tmp(d);d.n --;return tmp;
} //s--即为: operator--(s, 0);
类型转换函数重载
operator int ( ) { return n; }
这里int 作为一个类型强制转换运算符被重载, 此后
Demo s;
(int) s ; //等效于 s.int();
- 类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型
8、函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyPrint
{
public:void operator()(string text){cout << text << endl;}
};
void test01()
{//重载的()操作符 也称为仿函数MyPrint myFunc;myFunc("hello world");
}
class MyAdd
{
public:int operator()(int v1, int v2){return v1 + v2;}
};
void test02()
{MyAdd add;int ret = add(10, 10);cout << "ret = " << ret << endl;//匿名对象调用 cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
int main() {test01();test02();system("pause");return 0;
}
使用“类名()”时会产生一个该类匿名对象,起作用后便消亡。
后面会着重学习仿函数。
总结
以上就是今天要讲的内容,介绍了基本的运算符重载,此部分内容需要动手实践才能发现一些隐藏的容易错误的点。
欢迎点赞评论,如有错误请指正。
这篇关于【C++从入门到入土】第四篇:运算符重载的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!