第14章 重载操作符与转换
- 不能重载的操作符包括:
::
、*
(取值)和三元运算符?:
。 - 不能通过连接其他合法操作符来创建任何新操作符
- 重载操作符必须具有至少一个类类型或枚举类型的操作数,因此内置类型的操作符含义不能改变,也不能为任何内置类型定义额外的新的操作符。
- 操作符的优先级、结合性或操作数数目不能改变。除了函数调用操作符
()
外,重载操作符时使用默认实参是非法的。 - 重载操作符并不保证操作数的求值顺序(尤其是
&&
、||
和逗号操作符)。在&&
和||
的重载版本中,两个操作数都要进行求值。 - 一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。
- 也可以像调用普通函数那样调用重载操作符如:
- 非类成员:
operator+(item1, item2);
- 类成员:
item1.operator+=(item2);
- 非类成员:
- 不应重载具有内置含义的操作符:
=
、&
、,
、&&
、||
。 - 赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)必须定义为成员函数。 - 箭头操作符虽然表现得像二元操作符,但重载时不接受显式形参:
Type operator->() {...}
。 当这样编写时:pointer->action();
,由于优先级规则,它等价于(pointer->action)();
。求值过程为:- 若
pointer
是一个指针,指向具有名为action
的成员的类对象,则编译器将代码编译为调用该对象的action
成员; - 否则,若
pointer
是定义了operator->
操作符的一个对象,则pointer->action
与pointer.operator->()->action
相同,即执行pointer
的operator->()
,然后调用该结果重复这两步; - 否则,代码出错。
因此,重载箭头操作符必须返回指向类类型的指针,或返回定义了自己箭头操作符的类类型对象。
- 若
- 两种自增操作符的重载:
- 前自增:
Type &operator++() {...}
- 后缀式自增:Type operator++(int) {…}`
为解决前/后缀形式的形参数目和类型相同的问题,后缀式操作符接受一个额外的(无用的)
int
型形参。使用时编译器提供0
作为其实参。该形参不用命名,因为不应被使用到。显式调用的方法为:obj.operator++(0); // postfix obj.operator++(); // prefix
- 前自增:
- 一般为表示操作的类重载调用操作符,其对象称为函数对象。
-
转换操作符(conversion operator)是一种特殊的类成员函数,它定义将类类型值转变为其他类型值的转换;它在类定义体内声明,在保留字
operator
之后跟着转换的目标类型:operator Type2() const {...} // 无返回类型,形参表为空
对任何可作为函数返回类型的类型(
void
除外),都可以定义转换函数(故数组和函数类型不行)。虽然不能指定返回类型,但必须显式返回一个指定类型的值。 - 只要定义了转换,编译器将在可以使用内置转换的地方自动调用它,包括显式类型转换
static_cast
。 - 使用转换函数时,被转换的类型不必与所需类型完全匹配,必要时可在类类型转换之后跟上标准转换(如
int
->double
这种)以获得想要的类型,但不能再跟另一个类类型转换! - 标准转换可放在类类型转换之前,例如使用构造函数执行的隐式转换:
double
->int
->Type
,其中Type
类型有一个带单int
参数的构造函数。 - 如果两个转换操作符都可用于同一个调用中,而且转换之后存在标准转换,则根据该标准转换的类别使用最佳匹配
- 当两个构造函数定义的转换都可以使用时,如果存在构造函数实参所需的标准转换,就用该标准转换的类别选择最佳匹配。
-
当两个类定义了转换时的二义性:
/*I. */ class A { public: A(B); }; // B->A /*II.*/ class B { public: operator A() const; }; // B->A void func(A); func(B()); // ambiguous
但若将I改为
A(const B&);
则不再存在二义性,这是因为使用I
将需要一个引用绑定到B
的对象,而使用II则避免了这个额外步骤。 - 转换与函数重载确定的关系:如果重载集里面的两个函数可以用同一转换函数匹配,则使用在转换之后或之前的标准转换序列的等级来确定最佳匹配;否则,如果可以使用不同的转换操作,则认为这两个转换是一样好的匹配,不管可能需要或不需要的标准转换等级如何。
- 操作符的重载确定遵循常见的三步过程,而操作符的候选函数可能包括成员和非成员函数。
第15章 面向对象编程
- 派生类对其基类类型的对象的
protected
成员没有特殊访问权限。 - 派生类的虚函数的声明必须与基类完全匹配,但有个例外:派生类中的虚函数可以返回基类函数所返回类型(必须是引用或指针)的派生类的引用(或指针)
- 一旦函数在基类中声明为虚函数,它就一直是虚函数,不管派生类中用不用
virtual
- 用作基类的类必须是已定义的,只声明不行(否则就可以从自身派生出一个类了)
- 可以使用作用域操作符覆盖虚函数机制并强制函数调用使用虚函数的特定版本
- 虚函数的默认实参不受动态绑定影响:通过类
A
的引用或指针调用虚函数时,默认实参是在A
中定义的值,不管该引用或指针绑定的是A
的对象还是其派生类对象。另外,基类中虚函数如果指定了默认实参,派生类可以指定也可以不指定;反之亦然。 - 派生类不能访问基类的
private
成员 - 使用
using
,派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松(但g++ 4.6.1上却可以任意指定新的访问级别) - 友元关系不能继承:
- 基类的友元对派生类没有特殊访问权限
- 如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系给基类的类
- 若基类定义了
static
成员,则整个继承层次中只有一个这样的成员,每个static
成员只有一个实例。 - 派生类可以将基类的非纯虚的虚函数变为纯虚函数
- 派生类到基类转换的可访问性:
- 如果是
public
继承,则用户代码和后代类都可以访问(使用)派生类到基类的转换 - 如果是
protected
或private
继承,则用户代码不能将派生类型对象转换为基类对象 - 如果是
private
继承,则从private
继承类派生出来的类(即孙子层)不能转换为基类(不管是用户代码还是在下层派生类中) - 如果是
protected
继承,则后续派生类的成员可以转换为基类类型 - 派生类本身的成员和友元总是可以访问派生类到基类的转换(其实只要能访问到基类的
public
成员即可,不管派生了多少层)
- 如果是
- 合成的默认ctor、copy ctor和
=
以及析构函数将对对象的基类部分连同派生部分的成员一起进行初始化、复制、赋值和撤销;自定义的copy ctor和=
需要显式调用基类的copy ctor和=
或自己另行实现才能复制、赋值基类部分 - 如果在构造函数或析构函数中调用虚函数(不论直接还是间接),则运行的是为构造函数或析构函数自身类型定义的版本。
- 由于派生类的作用域嵌套在基类作用域中,在基类和派生类中使用同一名字的成员函数时,就像一般的在局部声明的函数和在全局声明的函数一样:在派生类作用域中派生类成员将屏蔽基类成员,即使函数原型不同。
- 局部作用域中声明的函数不会重载外围作用域中定义的函数,而是会屏蔽!因此,若派生类定义了重载成员,则通过派生类(的对象、指针或引用)只能访问派生类中重定义的那些成员,不管基类中的成员是否为
virtual
的!(可以通过using
声明解决这个问题) -
通过基类可以调用被屏蔽的虚函数:
struct Base { virtual int fcn(); } struct D1 : Base { int fcn(int); /* 屏蔽了Base的fcn */ } struct D2 : D1 { int fcn(int); /* 屏蔽了D1的fcn */ int fcn();} Base b; D1 d1; D2 d2; Base *bp1 = &b, *bp2 = &d1, *bp3 = &d2; bp1->fcn(); // Base::fcn bp2->fcn(); // Base::fcn,注意不能用D1的对象、引用或指针调用fcn(),因为它被fcn(int)屏蔽了! bp3->fcn(); // D2::fcn
- (名字查找与继承的关系)确定函数调用遵循以下四个步骤
- 首先确定进行函数调用的对象、引用或指针的静态类型
- 在该类中查找函数(名字),若找不到则从直接基类开始往上找,若最终找不到则出错
- 一旦找到了该名字,就进行常规类型检查,看调用是否合法
- 若合法,编译器就生成代码。若函数为虚且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。