C++(重载)
string类对象为什么可以使用 运算符 操作。
C++中把运算符当作函数处理,string类之所以能使用运算符操作,是因为该类中重载了运算符这种特殊的成员函数。
一、运算符函数
C++中把运算符当作函数处理,在表达式中遇到运算符时,编译器会先寻找相关的运算符函数。
例如a+b,会先寻找 [const]T[&] operator+ ([const] T& a,[const] T& b) 格式的函数,这种函数又被称为全局运算符函数,运算对象都做为参数传递给该函数。
如果运算对象是自定义类型,还可以把运算符函数实现该类型的内部,a+b的运算符函数格式为 [const] T[&] operator+([const]T& that) [const],这种函数又被称为成员运算符函数,一般由左边的运算对象发起调用,右边的运算对象作为参数。
二、运算符重载
在设计结构、联合、类时,为了让它们的对象的操作更简便、易懂,我们会为它们实现部分运算符函数,这种行为被称为运行符重载,我们以自定义string为例进行讲解运算符重载。
1、双目运算符重载
重载为成员运算符函数
1、左边的运算对象如果可能是常量,则需要把运算符函数设置为常函数。
2、如果右边的运算对象可能是常量,则需要给形参设置const属性。
3、一般在运算符函数中不修改运算对象的数据,而是由左右的运算对象的运算结果产生临时对象。
#include <iostream> #include <stdlib.h> using namespace std; class Int { int num; public: Int(int num):num(num) {} // 双目运算符 成员函数 const Int operator+(const Int& that)const { return Int(num+that.num); } const Int operator-(const Int& that)const { return Int(num-that.num); } const Int operator*(const Int& that)const { return Int(num*that.num); } const Int operator/(const Int& that)const { return Int(num/that.num); } const Int operator%(const Int& that)const { return Int(num%that.num); } };
重载为全局运算符函数
给类对象优先重载成员运算符函数时,必须要在类的内部实现,当无法修改对象的源码时,也就无法在结构、联合、类的内部增加成员运算符函数,又想给类对象增加运行符函数,这种情况只能定义全局的运算符函数,把运算对象都作为参数传递给运算符函数。
如何在全局运行符函数内访问私有成员—友元
当一个非成员函数,需要访问类中的私有成员时,可以把函数设置为该类的friend函数(也叫友元函数),这样在函数内就可以访问类的所有成员变量和成员函数。
一般情况下,在类的内部进行声明该函数(头文件内),并在声明的前面添加friend关键字,在类外实现友元函数。
也可以直接在类的内部实现有友函数,即使在类的内部实现也依然不是成员函数。
当重载全局运算符函数基本上都需要使用友元函数,我个人建议把友元函数实现在类的内部。
#include <iostream> #include <stdlib.h> using namespace std; class Int { int num; public: Int(int num):num(num) {} void show(void)const { cout << "num=" << num << endl; } friend const Int operator-(const Int& i1,const Int& i2); friend const Int operator*(const Int& i1,const Int& i2); friend const Int operator/(const Int& i1,const Int& i2); friend const Int operator%(const Int& i1,const Int& i2); }; const Int operator-(const Int& i1,const Int& i2) { return Int(i1.num-i2.num+100); } const Int operator*(const Int& i1,const Int& i2) { return Int(i1.num*i2.num+100); } const Int operator/(const Int& i1,const Int& i2) { return Int(i1.num/i2.num+100); } const Int operator%(const Int& i1,const Int& i2) { return Int(i1.num%i2.num+100); }
3、输入、输出运算符重载
1、cin、cout是标准库中具有输入、输出功能的类对象,它的类名叫istream、ostream,当我们想给某种类对象增加输入、输出的功能时,就需要实现 >>、<<运算符函数对于该类对象的支持,也就是重载输入、输出运算符。
2、由于cin、cout都在>>、<<运算的左边,如果想实现成员运算符函数,就需要在istream、ostream类中实现,而我们无法增加或修改istream、ostream类的代码,所以只能实现全局的 >>、<< 运算符函数。
3、由于输入、输出过程中需要使用cin、cout记录成功、失败等状态,所以输入、输出函数的istream、ostream要使用引用,且不能用const修饰。
4、输入、输出过程中还可能要访问对象的 私有、保护的成员,所以全局的 >>、<< 运算符函数有必要设置成友元函数(右边对象)。
friend ostream& operator<<(ostream& os,const <类名>& obj) { return os << obj.成员1 << obj.成员1 << ...; } friend istream& operator>>(istream& is,<类名>& str) { return is >> obj.成员1 >> obj.成员1 >> ...; }
练习:实现自定义的MyString类,实现它的构造、拷贝构造、赋值、析构,并重载以下运算符。
== > < >= <= != + += >> <<
4、单目运算符的重载
单目运算符的重载与双目运算符重载的规则几乎相同,只是运算对象数量不同而已
重载为成员运算符函数
唯一的运算对象就是函数的调用者,参数列表为空,[cosnt] T[&] operator<单目运算符>(void)[const]。
#include <iostream> #include <stdlib.h> using namespace std; class Int { int num; public: Int(int num):num(num) {} const Int operator-(void)const { return Int(0-num); } bool operator!(void)const { return !num; } const Int operator~(void)const { return Int(~num); } Int* operator&(void) { return (Int*)# } const Int* operator&(void)const { return (const Int*)# } void show(void)const { cout << "num=" << num << endl; } };
重载为全局运算符函数
与双目运算符一样,只有无法修改运算对象的代码时,才会重载全局运算符函数,参数列只有一个,[cosnt] T[&] operator<单目运算符>([const] T&)[const]。
#include <iostream> #include <stdlib.h> using namespace std; class Int { int num; public: Int(int num):num(num) {} void show(void)const { cout << "num=" << num << endl; } friend const Int operator-(const Int& that) { return Int(0-that.num); } friend bool operator!(const Int& that) { return !that.num; } friend const Int operator~(const Int& that) { return ~that.num; } friend Int* operator&(Int& that) { return (Int*)&that.num; } friend const Int* operator&(const Int& that) { return (const Int*)&that.num; } };
注意:尽量重载为成员运算符函数。
5、自变运算符重载
前自变运算符重载 ++i/--i
前自变运算符与普通的单目运算符重载方法相同。
后自变运算符重载 ++i/--i
后自变运算符的运算对象也只有一个,为了区别前自变,我们需要在参数列增加一个什么都不做的int关键字,我们把这种用法叫哑元。
#include <iostream> #include <stdlib.h> using namespace std; class Int { int num; public: Int(int num):num(num) {} void show(void)const { cout << "num=" << num << endl; } Int& operator++(void) { num++; cout << "前++" << endl; return *this; } Int operator++(int) // 哑元 { cout << "后++" << endl; return Int(num++); } /* friend Int& operator++(Int& that) { cout << "前++" << endl; that.num++; return that; } friend Int operator++(Int& that,int) // 哑元 { cout << "后++" << endl; return Int(that.num++); } */ };
6、new/delete运算符的重载
在C++中是把new/delete关键字当作运算符,也就是运算符函数,所以如果有特殊需要可以对它们进行重载,重载格式参考单目运算符。
new运算符的重载:
new 运算符有两中用法,也就有两个格式重载函数。
格式1:new TYPE
该格式既可以重载为全局函数,也可以重载为成员函数。
格式2:new(ptr) TYPE
该格式只能重载为成员函数,因为C++标准库中已经为该格式的函数。
new/delete运算符有什么用:
1、记录下申请、释放的内存块的地址,方便程序员检测是否有内存泄漏。
2、防止用户大量分配、释放小块内存,产生内存碎片。
3、默认的new只能给类对象分配内存,重载后可以给成员指针一起分配内存,该方法适合重载为类的成员函数。
void* operator new(size_t size) { void* ptr = malloc((size/4+1)*4); cout << ptr << "自定义的new"<< endl; return ptr; } void operator delete(void* ptr) { cout << ptr << "自定义的delete" << endl; free(ptr); }
#include <iostream> #include <stdlib.h> using namespace std; class Student { char sex; short age; float score; public: char* name; void* operator new(size_t size) { cout << "自定义的new运算符成员函数" << endl; Student* stup = (Student*)malloc(size); stup->name = (char*)malloc(20); return stup; } void operator delete(void* ptr) { cout << "自定义的delete运算符成员函数" << endl; Student* stup = (Student*)ptr; free(stup->name); free(stup); } }; int main(int argc,const char* argv[]) { Student* stup = new Student; delete stup; return 0; }
三、运算符重载的局限性
1、无法重载的运算符
. 成员访问运算符 :: 作用域运算符 sizeof 长度运算符 ?: 条件运算符 # 预处理符号
2、只能重载为成员函数的运算符
在C++中,有几个函数只能被重载为成员函数,它们分别是:
-
赋值运算符重载
=
,也叫赋值函数,要进行深拷贝时就需要重载。 -
索引运算符重载
[]
,可以把对象伪装成数组使用。 -
函数调用运算符重载
()
,可以把对象伪装成函数使用。 -
成员访问运算符重载
->
,可以把对象伪装成指针使用。需要注意的是,这些函数只能被定义为成员函数,而不能被定义为全局函数或友元函数。除了上述函数,其他函数可以被定义为全局函数或友元函数。
3、优先重载为成员函数
只有无法修改该对象的代码时,才建议重载为全局函数,但重载全局函数需要在运算对象的类内设置友元,所以语法上支持,但实际情况是个死局。
4、重载运算符时要注意的问题
1、重载运算符的目标是为了提高代码可读性、实用性,如果达不该效果建议定义为函数。
2、重载运算符函数时,要符合情况情理,不要反人性,不要成为你炫技的工具。
总结:
1、理解string类为什么可以使用运算符操作。
2、理解什么是友元、哑元。
3、熟练掌握输入、输出运算符的重载。
4、复习减少内存碎片、内存泄漏的方法。