第17章 运算符重载 一、运算符重载的概念 二、禁止重载的运算符 三、运算符重载的规则 四、单目运算符函数 五、双目运算符函数
一、运算符重载的概念 系统的运算符主要分两大种类:一是单目运算符,另一 是双目运算符。用不属于字符集的@代表各种许可的运算 符,运算符构成的表达式抽象的表现格式为: 1. @x 2. x@y x和y是运算符关联的操作数,其原先可以出现的数据 类型是算术类型以及相关的指针类型如char*,CType*等。仅 当存在运算符函数,操作数x或y才可以是对象 。 能转换为运算符函数的普通函数或单参数的函数,此时 称单目运算符函数;或双参数的函数,此时称双目运算符函数.
原来普通的双参数(非静态的成员函数隐含this参数)函数: int CType::Add (int k) {return n+k; } int Sub (CType a,int k) { return a.n-k;} 可以相应地改写为运算符函数(其中n是CType类的int 型数据成员): int CType::operator+ (int k) { return n+k; } int operator- (CType a,int k) { return a.n-k; } 关键字operator是实现普通函数转换为运算符函数的语 法中介,operator@是运算符函数名。 当@分别对应+-*/时加分别得到四个运算符函数,依次 是operator+加号运算符函数、operator-减号运算符函数、 operator*乘号运算符函数和operator/除号运算符函数。
运算符重载是简化对象运算的函数调用现象,通过定义 函数名为operator@的运算符函数,有关对象的函数调用可 以简化为x@y或 @x隐含调用的替代形式。 运算符函数operator@可以存在多个版本,只要编译器 根据名称细分的结果在函数调用点能够进行唯一的匹配。 例如:iostream类中operator<<左移运算符函数就存在 多个版本。 名称为operator@的运算符函数可以是成员函数也可以 是全局函数。基于解除私有封装的考虑,全局函数声明为类 的友员函数。 如果类只有公共成员,则无需声明为友员函数。作为非 虚的成员运算符函数和全局运算符函数的重载在编译阶段完 成函数调用的确定,virtual关键字可以修饰作为成员的运算 符函数。
[例]普通函数和相应的运算符函数 #include<stdio.h> struct CType { int n; CType(int r=1) { n=r; } int operator+ (int k) { return n+k; } int Add(int k) { return n+k; } }; int operator- (int k,CType a) { return k-a.n; } int Sub (int k,CType a) { return k-a.n; } void main () { CType a (0), b (-7); printf ("%d,%d,%d;", a+1, a.operator+ (1), a.Add (1)); printf ("%d,%d,%d\n",1-b, operator- (1,b), Sub (1,b)); } //输出:1,1,1;8,8,8
含对象的表达式a+1等价于显式调用a.operator+(1), a+1是CType::operator+(int)的隐含调用,隐含调用方便了 对象的操作。 类似地隐含调用1-b等价于operator-(1,b)的显式调用。 1+a不同于a+1,1+a要求匹配operator+(int,CType)型 的全局运算符函数,而b-1要求匹配operator-(CType,int)型 的全局函数或CType::operator-(int)型的成员函数。 由于上例题未提供相应的函数,1+a和b-1此情形下是错 误的表达式。
二、禁止重载的运算符 们是: . .* :: sizeof ?: 只有区区五个运算符不可以赋予运算符的函数实现,它 们是: . .* :: sizeof ?: 圆点访问成员运算符”.”, 对象访问成员指针运算符”.*”, 这两个运算符分别是箭头访问成员运算符->和对象指针访问 成员指针运算符->*的翻版为对象访问成员保留一条安全的 入口。 作用域分别符::和sizeof运算符都是编译阶段发挥作用。 sizeof运算符的入口参数本身可以是各种类型名。 另一个是三目条件运算符?:,这个运算符本身是if~else 嵌套结构的合理重载。
三、运算符重载的规则 存在两种运算符函数的调用格式,一种是将运算符函数 当作函数名为operator@的显式调用格式,另一种是隐含调 用格式,最终编译器在内部转换为显式调用格式。 操作数x,y是算术或指针类型,表达式x@y, @x是常规 的算术或指针运算,不涉及运算符函数。x或y是对象,x@y 隐含调用相应的双目运算符函数;x是对象,@x 隐含调用 相应的单目运算符函数;若不存在相应的运算符函数,隐含 调用导致错误。 对于一个特定的类,系统提供等号运算符函数 operator= 和取地址运算符函数operator&供程序调用,即 对于该类的对象x,y可以进行运算:x=y,&x。
对于对象表达式的前台隐含操作, 必须存在一个无歧义 的运算符函数作为背景支持。如果无相应的运算符函数或类 型转换函数,则有关对象的隐含调用导致错误。 例如: x+=y是毫无根基的运算,除非存在相关的 operator+= 运算符函数。语句[CType* p;]定义的指针p不 是对象而是一个常规的指针,对于变量指针合适的运算符也 可作用于对象之指针。 运算符始终遵循内部类型所规定的优先原则、结合性。 即对于出现在表达式中的运算符函数隐含调用如x@y, @x,优先级高的运算符函数优先被编译器隐含调用,同等 级别的运算符函数根据结合性进行分解处理。
非静态的成员运算符函数的第一个参量对应隐含this的 当前类的类型。 类型转换运算符、 函数调用运算符()、数组下标索引运 算符 [ ]、箭头运算符-> 函数和等号运算符函数只作为非静 态的成员函数,其余的运算符可以和operator紧贴在一起构 成全局函数。 等号运算符函数operator = 不为派生类继承。 不能凭空捏造C++语言中子虚乌有的运算符如 FORTRAN语句的乘幂运算符**。 运算符函数不允许用户提交缺省的默认值,函数调用运 算符operator()例外。
双目或单目全局运算符函数形参类型至少存在一个用户 声明的类型,枚举类只能有全局运算符函数。 例如对于类声明[struct CType {};],CType或CType& 是用户声明的类型。 CType* 类型为常规的指针类型,不用 于构成运算符函数的对象类型。 void operator*(CType&,int){} 是正确的函数定义,而 void operator-(CType*,int){}与void operator+(char,int){} 是错误的。 可以在声明的类上对运算符函数提交任意的语义实现。 但出于与内置类型表达式的接口考虑,最好按照运算符固有 含义进行运算符函数定义。
四、单目运算符函数 单目运算符中存在格外的运算符,这就是成为 C++由来的后置运算符。这个运算符从后面作用于 操作数,其运算符重载有其特殊的格式。 对于语句[CType obj;]定义的对象obj,单目运 算符函数隐含调用的格式为@obj。
1. 单目成员运算符函数声明时不带参量,编译器隐含补充 this参量。声明格式为: ret_type operator@(); 返回类型 单目运算符函数名(); 在类外定义的格式为: ret_type CType::operator@ () {语句序列;} 隐含调用@obj 转换为显式调用obj.operator@()。 非静态的单目成员运算符函数隐含的形参this在调用点 由&obj赋予具体的值。
2. 单目全局运算符函数仅带一个用户声明的类型,声明 格式为: ret_type operator@ (CType& r); 在实现文件的定义格式为: ret_type operator@ ( CType& r) {显含形参r的语句序列;} 隐含调用@obj 转换为显式调用operator@(obj)。单目 全局运算符函数形参r在调用点由obj赋予具体的值,若需要 访问CType类的私有成员则声明为该类的友元函数。 简洁的@obj形式既可调用obj.operator@()成员版本也 可调用operator@(obj)全局版本。为避免重载的歧义性,只 提交一种版本,或者是成员版本,或者是全局版本。
int CType::operator! ( ){ printf ("%d!,",m++); return !n;} [例] 重载逻辑非运算符!负号运算符-和前置运算符++ static int m=1; #include<stdio.h> class CType { int n; friend CType& operator++(CType& q); friend void main(); friend CType operator-(const CType& r); public: CType(int r) { n=r; } int operator!(); }; int CType::operator! ( ){ printf ("%d!,",m++); return !n;}
CType operator- (const CType& r) { printf ("%d-,", m++); return CType (-r.n); } CType& operator++ (CType& q) { printf ("%d++,", m++); ++q.n; return q; } void main() { CType a (2),b(-1); a=!-++b; printf ("a=%d\n", a.n); a= (operator- (operator++ (b))).operator! ( ); printf ("a=%d", a.n); } //a=1先调用构造函数CType (int)即 a=1相当于a=CType(1) ++b等价于operator++(b), -++b等价于operator-(operator++(b))等。
双目运算符函数如果是非静态的成员函数则声明的时候 五、双目运算符函数 双目运算符函数如果是非静态的成员函数则声明的时候 仅带一个参量,编译器隐含的补充一个 this参量。 如果声明为全局函数,则双目运算符函数带两个参量, 其中一个参量的类型必须是一个用户声明的类型。
1. 双目成员运算符函数 作为成员的双目运算符函数的声明格式为: ret_type operator@ (type); 返回类型 双目运算符函数名 (数据类型); 在类外定义的格式为: ret_type CType::operator@( type arg) { 语句序列; } 对于定义语句[CType obj;]设定的对象obj和type型表 达式var,隐含调用的格式为: obj@var 隐含调用obj@var转换为 obj.operator@(var)的显式 调用形式。 非静态的双目成员运算符函数隐含的形参this在 调用点由&obj赋予具体的值,实参var负责形参arg 的初始 化。
2. 双目全局运算符函数 双目全局运算符函数由于接口的需要花样增多一倍,相 应的声明格式为: ret_type operator@ (CType& r,type arg); //此格式可以从成员版本变换而来 返回类型 双目运算符函数名 (当前类名,其它类名); ret_type operator@ ( type arg, CType& r); 返回类型 双目运算符函数名 (其它类名, 当前类名); 相应的隐含调用的格式为: obj@var var @obj
obj@var转换为显式调用operator@(obj,var) 即启动 operator@ ( CType& r,type arg)函数,var@obj转换为显 式调用 operator@ (var,obj) 即启动 operator@( type arg, CType& r) 函数,形参r由对象名obj初始化,实参var赋予形参 arg 具 体的初值。 obj@var可以调用成员版本obj.operator@ (var)也可 调用全局版本operator@ (obj, var)。 两者本质上应是等价的,因此酌情提交一个版本,以免 导致歧义。
[例] 重载加减乘除运算符实现结构变量的四则运算 #include<stdio.h> struct CA { CA operator+ ( CA b); CA (int r=1){ n=r; }; int n; }; static int m=1; CA CA::operator+( CA a) {printf ("%d+, ",m++); return CA (n+a.n); } CA operator- (CA a, const CA& r) { printf ("%d-,",m++); return CA (a.n-r.n); } CA operator*(CA a, CA b) {printf ("%d*,",m++); return CA (a.n*b.n); } CA operator/ (const CA& r, const CA& q) { printf ("%d/,",m++); return CA (r.n/q.n); }
void main () { CA a,b(2),c(6); a=(a+b)*(c/b)-b; printf ("a=%d; ",a.n); a=operator- ((operator*(a.operator+ (b), operator/ (c, b))),b); } //输出:1/,2+,3*,4-, a=7; 5/,6+,7*,8-,a=25;
对于对象的引用返回,引用是已经建立的对象的别名, 返回的是不独立的对象,此时编译器不需要额外建立临时对 象。 对于对象的数值返回,返回一个局部或临时的独立对 象,输送给主控程序。 主控程序会尽早释放临时对象占有的内存。 算术和指针类型的数值返回其函数调用为右值表达式, 对象类型的数值返回其函数调用可以为左值,但由于返回的 临时对象的生存期不由程序员控制,因此返回数值对象的函 数调用不宜作为左值。 返回引用的函数调用则可稳健地作为左值参入运算。
例如:对象表达式:a=(a+b)*(c/b)-b; 在临时对象q,t中。接着求q*t的值,结果存放在临时对象s 中,再求s-b的值并把结果存入对象a中。 独立的对象操作自身内存单元,附属的引用操作相关对 象的内存单元。 例如:对于前面单目运算例题,隐含调用++b等价于 operator++(b),该operator++函数是引用形参r,引用r操 作b对象的内存单元。 而- ++b等价于operator-(operator++(b)),这个结果 是临时对象。 指针或算术表达式的结果有两种: 一种结果为右值,对于操作出右值结果的运算符如+-*/ 运算符,在转换为运算符函数时建议返回数值对象。
另一种结果为左值,如赋值运算符和复合赋值运算符 +=,-=,*=,/=等。对于这些运算符函数建议处理为对象的引 用返回。 入口形参优先采用引用类型而不是对象的数值类型,内 置类型的形参可以是数值形参或引用形参。 数值形参具有信息的单向安全性,对于对象的引用形参 常加上const修饰以模拟数值形参的单向作用,返回时也可 加const限制。 对于运算符的返回类型最好根据运算符的结果进行返 回,即取地址运算符&返回地址表达式,逻辑非运算符!返回 bool值或int数,前置运算符++返回入口类型的引用等。
请打开“第17章(2).ppt”