- 赋值操作符
类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,但也可以是类类型或对类类型的非 const 引用。如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。可以为一个类定义许多附加的赋值操作符,这些赋值操作符会因右操作符类型不同而不同。例如,标准库的类 string 定义了 3 个赋值操作符:除了接受 const string& 作为右操作数的类赋值操作符之外,类还定义了接受 C 风格字符串或 char 作为右操作数的赋值操作符,这些操作符可以这样使用:
string car ("Volks"); car = "Studebaker"; // string = const char* string model; model = 'T'; // string = char
为了支持这些操作符,string 类包含如下成员:
// illustration of assignment operators for class string class string { public: string& operator=(const string &); // s1 = s2; string& operator=(const char *); // s1 = "str"; string& operator=(char); // s1 = 'c'; // .... };
赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。
- 赋值必须返回对*this的引用
string 赋值操作符返回 string 引用,这与内置类型的赋值一致。而且,因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用,例如,这是 Sales_item 复合赋值操作符的定义:
// assumes that both objects refer to the same isbn Sales_item& Sales_item::operator+=(const Sales_item& rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
一般而言,赋值操作符与复合赋值操作符应返回操作符的引用。
- 下标操作符
可以从容器中检索单个元素的容器类一般会定义下标操作符,即 operator[]。标准库的类 string 和 vector 均是定义了下标操作符的类的例子。下标操作符必须定义为类成员函数。
提供读写访问。定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
- 原型下标操作符
下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector<int>: 中:
class Foo { public: int &operator[] (const size_t); const int &operator[] (const size_t) const; // other interface members private: vector<int> data; // other member data and private utility functions };
下标操作符本身可能看起来像这样:
int& Foo::operator[] (const size_t index) { return data[index]; // no range checking on index } const int& Foo::operator[] (const size_t index) const { return data[index]; // no range checking on index }
- 成员访问操作符
为了支持指针型类,例如迭代器,C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。
构建更安全的指针:解引用操作符和箭头操作符常用在实现智能指针的类中。作为例子,假定想要定义一个类类型表示指向Screen 类型对象的指针,将该类命名为 ScreenPtr。
ScreenPtr 类将类似于我们的第二个 HasPtr 类。ScreenPtr 的用户将会传递一个指针,该指针指向动态分配的 Screen,ScreenPtr 类将拥有该指针,并安排在指向基础对象的最后一个 ScreenPtr 消失时删除基础对象。另外,不用为 ScreenPtr 类定义默认构造函数。因此,我们知道一个 ScreenPtr 对象将总是指向一个 Screen 对象,不会有未绑定的 ScreenPtr,这一点与内置指针不同。应用程序可以使用 ScreenPtr 对象而无须首先测试它是否指向一个 Screen 对象。
像 HasPtr 类一样,ScreenPtr 类将对其指针进行使用计数。我们将定义一个伙伴类保存指针及其相关使用计数:
// private class for use by ScreenPtr only class ScrPtr {//智能指针 friend class ScreenPtr; Screen *sp; size_t use; ScrPtr(Screen *p): sp(p), use(1) { } ~ScrPtr() { delete sp; } };
这个类看来很像 U_Ptr 类并且作用同样。ScrPtr 保存指针及其相关使用计数。将 ScreenPtr 设为友元,以便 ScreenPtr 可以访问使用计数。ScreenPtr 类将管理使用计数:
/* * smart pointer: Users pass to a pointer to a dynamically allocated Screen, which * is automatically destroyed when the last ScreenPtr goes away */ class ScreenPtr { public: // no default constructor: ScreenPtrs must be bound to an object ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { } // copy members and increment the use count ScreenPtr(const ScreenPtr &orig): ptr(orig.ptr) { ++ptr->use; } ScreenPtr& operator=(const ScreenPtr&); // if use count goes to zero, delete the ScrPtr object ~ScreenPtr() { if (--ptr->use == 0) delete ptr; } private: ScrPtr *ptr; // points to use-counted ScrPtr class };
因为没有默认构造函数,所以 ScreenPtr 类型的每个对象必须提供一个初始化函数,初始化函数必须是另一个 ScreenPtr 对象或指向动态分配的 Screen 的指针。构造函数分配一个新的 ScrPtr 对象以保存那个指针及相关的使用计数。试图定义一个不带初始化式的 ScreenPtr 对象是错误的:
ScreenPtr p1; // error: ScreenPtr has no default constructor ScreenPtr ps(new Screen(4,4)); // ok: ps points to a copy of myScreen
- 支持指针操作
指针支持的基本操作有解引用操作和箭头操作。我们的类可以这样定义这些操作:
class ScreenPtr { public: // constructor and copy control members as before Screen &operator*() { return *ptr->sp; } Screen *operator->() { return ptr->sp; } const Screen &operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // points to use-counted ScrPtr class };
解引用操作符是个一元操作符。在这个类中,解引用操作符定义为成员,因此没有显式形参,该操作符返回对 ScreenPtr 所指向的 Screen 的引用。像下标操作符一样,我们需要解引用操作符的 const 和非 const 版本。它们的区别在于返回类型:const 成员返回 const 引用以防止用户改变基础对象。
箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。这里没有第二个形参,因为 -> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。
当这样编写时:
point->action();
由于优先级规则,它实际等价于编写:
(point->action)();
换句话说,我们想要调用的是对 point->action 求值的结果。编译器这样对该代码进行求值:如果 point 是一个指针,指向具有名为 action 的成员的类对象,则编译器将代码编译为调用该对象的 action 成员;否则,如果 action 是定义了 operator-> 操作符的类的一个对象,则 point->action 与 point.operator->()->action 相同。即,执行 point 的 operator->(),然后使用该结果重复这三步;否则,代码出错。
- 使用重载箭头
ScreenPtr p(&myScreen); // copies the underlying Screen p->display(cout);
因为 p 是一个 ScreenPtr 对象,p->display 的含义与对 (p.operator->())->display 求值相同//p是对象,不是指针,这里就需要对操作符“->”进行重载,先调用p.operator->(),返回一个p的指针,然后再调用displat()。对 p.operator->() 求值将调用 ScreenPtr 类的 operator->,它返回指向 Screen 对象的指针,该指针用于获取并运行 ScreenPtr 所指对象的 display 成员。
对重载箭头的返回值的约束:重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。如果返回类型是指针,则内置箭头操作符可用于该指针,编译器对该指针解引用并从结果对象获取指定成员。如果被指向的类型没有定义那个成员,则编译器产生一个错误。如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。
- 自增操作符和自减操作符
自增(++)和自减(--)操作符经常由诸如迭代器这样的类实现,这样的类提供类似于指针的行为来访问序列中的元素。例如,可以定义一个类,该类指向一个数组并为该数组中的元素提供访问检查。理想情况下,带访问检查的指针类可用于任意类型的数组,这一点的实现我们将在介绍类模板时学习。现在,我们的类将处理 int 数组:
/* * smart pointer: Checks access to elements throws an out_of_range * exception if attempt to access a nonexistent element * users allocate and free the array */ class CheckedPtr { public: // no default constructor; CheckedPtrs must be bound to an object CheckedPtr(int *b, int *e): beg(b), end(e), curr(b) { } // dereference and increment operations private: int* beg; // pointer to beginning of the array int* end; // one past the end of the array int* curr; // current position within the array };
- 定义自增自减操作符
C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。在为类定义重载的自增操作符和自减操作符之前,还必须考虑另一件事情。对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。毫不奇怪,也可以为我们自己的类定义自增操作符和自减操作符的前缀和后缀实例。我们首先介绍前缀形式,然后实现后缀形式。
- 定义前自增前自减操作符
前缀式操作符的声明看起来像这样:
class CheckedPtr { public: CheckedPtr& operator++(); // prefix operators CheckedPtr& operator--(); // other members as before };
为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用。
这个自增操作符根据 end 检查 curr,从而确保用户不能将 curr 增量到超过数组的末端。如果 curr 增量到超过 end,就抛出一个 out_of_range 异常;否则,将 curr 加 1 并返回对象引用:
// prefix: return reference to incremented/decremented object CheckedPtr& CheckedPtr::operator++() { if (curr == end) throw out_of_range ("increment past the end of CheckedPtr"); ++curr; // advance current state return *this; }
除了将 curr 减 1 并检查是否会减到 beg,自减操作符的行为与自增操作符类似:
CheckedPtr& CheckedPtr::operator--() { if (curr == beg) throw out_of_range ("decrement past the beginning of CheckedPtr"); --curr; // move current state back one element return *this; }
- 区别操作符前缀和后缀形式
同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符,编译器提供 0 作为这个形参的实参。//C++自身规定的,后缀操作符后边有一个默认的int类型的参数。尽管我们的前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区别开来。
- 定义后缀式操作符
class CheckedPtr { public: // increment and decrement CheckedPtr operator++(int); // postfix operators CheckedPtr operator--(int); // other members as before };
为了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用。后缀式操作符可以这样实现:
// postfix: increment/decrement object but return unchanged value CheckedPtr CheckedPtr::operator++(int) { // no check needed here, the call to prefix increment will do the check CheckedPtr ret(*this); // save current value ++*this; // advance one element, checking the increment return ret; // return saved state } CheckedPtr CheckedPtr::operator--(int) { // no check needed here, the call to prefix decrement will do the check CheckedPtr ret(*this); // save current value --*this; // move backward one element and check return ret; // return saved state }
操作符的后缀式比前缀式复杂一点,必须记住对象在加 1/减 1 之前的当前状态。这些操作符定义了一个局部 CheckedPtr 对象,将它初始化为 *this 的副本,即 ret 是这个对象当前状态的副本。保存了当前状态的副本后,操作符调用自己的前缀式操作符分别进行加 1 或减 1:
++*this
调用这个对象的 CheckedPtr 前缀自增操作符,该操作符检查自增是否安全并将 curr 加 1 或抛出一个异常。假定不抛出异常,前自增操作符函数以返回存储在 ret 的副本而结束。因此,返回之后,对象本身加了 1,但返回的是尚未自增的原值。因为通过调用前缀式版本实现这些操作符,不需要检查 curr 是否在范围之内,那个检查以及必要的 throw,在相应的前缀式操作符中完成。因为不使用 int 形参,所以没有对其命名。
- 显示调用前缀式操作符
可以显式调用重载操作符而不是将它作为操作符用在表达式中。如果想要使用函数调用来调用后缀式操作符,必须给出一个整型实参值:
CheckedPtr parr(ia, ia + size); // iapoints to an array of ints parr.operator++(0); // call postfix operator++ parr.operator++(); // call prefix operator++
所传递的值通常被忽略,但该值是必要的,用于通知编译器需要的是后缀式版本。一般而言,最好前缀式和后缀式都定义。只定义前缀式或只定义后缀式的类,将会让习惯于使用两种形式的用户感到奇怪。
- 调用操作符和函数对象
可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载调用操作符。例如,可以定义名为 absInt 的结构,该结构封装将 int 类型的值转换为绝对值的操作:
struct absInt { int operator() (int val) { return val < 0 ? -val : val; } };
这个类很简单,它定义了一个操作:函数调用操作符,该操作符有一个形参并返回形参的绝对值。通过为类类型的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:
int i = -42; absInt absObj; // object that defines function call operator unsigned int ui = absObj(i); // calls absInt::operator(int)
尽管 absObj 是一个对象而不是函数,我们仍然可以“调用”该对象,效果是运行由 absObj 对象定义的重载调用操作符,该操作符接受一个 int 并值并返回它的绝对值。函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。
- 将函数对象用于标准库算法
函数对象经常用作通用算法的实参。程序分析一组故事中的单词,计算有多少个单词长度在 6 字符以上。该解决方案的一个部分包括定义一个函数以确定给定 string 的长度是否大于 6 字符:
// determine whether a length of a given word is 6 or more
bool GT6(const string &s) { return s.size() >= 6; }
使用 GT6 作为传给 count_if 算法的实参,以计算使 GT6 返回 true 的单词的数目:
vector<string>::size_type wc =
count_if(words.begin(), words.end(), GT6);//判断words这个vector中的单词中,长度大于6的个数
函数对象可以比函数更灵活。我们的实现有个严重问题:它将 6 这个数字固化在 GT6 函数的定义中。count_if 算法运行只用一个形参且返回 bool 的函数。理想情况下,应传递 string 和我们想要的长度进行测试。通过该方式,可以使用同一代码对不同长度的字符串进行计数。通过将 GT6 定义为带函数调用成员类,可以获得所需的灵活性。将这个类命名为 GT_cls 以区别于函数:
// determine whether a length of a given word is longer than a stored bound
class GT_cls {
public: GT_cls(size_t val = 0): bound(val) { } bool operator()(const string &s)//利用对“()”的重载,类对象可以当做一个函数一样去使用 { return s.size() >= bound; } private: std::string::size_type bound; };
这个类有一个构造函数,该构造函数接受一个整型值并用名为 bound 的成员记住那个值。如果没有提供值,构造函数将 bound 置 0。该类也定义了调用操作符,接受一个 string 参数并返回一个 bool。调用操作符将 string 实参的长度与数据成员 bound 中存储的值相比较。
可以像前面一样进行计数,但这一次使用 GT_cls 类型的对象而不是 GT6 函数:
cout << count_if(words.begin(), words.end(), GT_cls(6))
<< " words 6 characters or longer" << endl;
这个 count_if 调用传递一个 GT_cls 类型的临时对象而不再是名为 GT6 的函数。用整型值 6 来初始化那个临时对象,构造函数将这个值存储在 bound 成员中。现在,count_if 每次调用它的函数形参时,它都使用 GT_cls 的调用操作符,该调用操作符根据 bound 的值测试其 string 实参的长度。使用函数对象,容易修改程序以根据其他值进行测试,只需为传给 count_if 的对象改变构造函数实参即可。例如,这样修改程序,就可以计算长度在 5 个字符以上的单词数:
cout << count_if(words.begin(), words.end(), GT_cls(5))
<< " words 5 characters or longer" << endl;
更为有用的是,还可以计算长度在 1 到 10 个字符的单词数:
for (size_t i = 0; i != 11; ++i)
cout << count_if(words.begin(), words.end(), GT(i))
<< " words " << i << " characters or longer" << endl;
如果使用函数代替函数对象来编写这个程序,可能需要编写 10 个不同的函数,每个函数测试一个不同的值。
- 标准库定义的函数对象
标准库定义了一组算术、关系与逻辑函数对象类,标准库还定义了一组函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。这些标准库函数对象类型是在 functional 头文件中定义的。
- 每个类表示一个给定操作符
每个标准库函数对象类表示一个操作符,即,每个类都定义了应用命名操作的调用操作符。例如,plus 是表示加法操作符的模板类型。plus 模板中的调用操作符对一对操作数应用 + 运算。不同的函数对象定义了执行不同操作的调用操作符。正如 plus 定义了执行 + 操作符的调用操作符,modulus 类定义了应用二元操作符 % 的调用操作符,equal_to 类应用 ==,等等。有两个一元函数对象类:一元减(negate<Type>))和逻辑非(logical_not<Type>))。其余的标准库函数对象都是表示二元操作符的二元函数对象类。为二元操作符定义的调用操作符需要两个给定类型的形参,而一元函数对象类型定义了接受一个实参的调用操作符。
- 表示操作数类型的模板类型
每个函数对象类都是一个类模板,我们需要为该模板提供一个类型。正如从诸如 vector 的顺序容器所了解的,类模板是可以用于不同类型的类。函数对象类的模板类型指定调用操作符的形参类型。例如,plus<string> 将 string 加法操作符应用于 string 对象,对于 plus<int>,操作数是 int 值,plus<Sales_item> 将 + 应用于 Sales_items; 对象,依次类推:
plus<int> intAdd; // function object that can add two int values negate<int> intNegate; // function object that can negate an int value // uses intAdd::operator(int, int) to add 10 and 20 int sum = intAdd(10, 20); // sum = 30 // uses intNegate::operator(int) to generate -10 as second parameter // to intAdd::operator(int, int) sum = intAdd(10, intNegate(10)); // sum = 0
函数对象常用于覆盖算法使用的默认操作符。例如,sort 默认使用 operator< 按升序对容器进行排序。为了按降序对容器进行排序,可以传递函数对象 greater。该类将产生一个调用操作符,调用基础对象的大于操作符。如果 svec 是一个 vector<string> 对象,以下代码
// passes temporary function object that applies > operator to two strings sort(svec.begin(), svec.end(), greater<string>());
将按降序对 vector 进行排序。像通常那样,传递一对迭代器以指明被排序序列。第三个实参用于传递比较元素的谓词函数。该实参 greater<string> 类型的临时对象,是一个将 > 操作符应用于两个 string 操作符的函数对象。
- 函数对象的函数适配器
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象;求反器,是一种函数适配器,它将谓词函数对象的真值求反。
标准库定义了两个绑定器适配器:bind1st 和 bind2nd。每个绑定器接受一个函数对象和一个值。正如你可能想到的,bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参。例如,为了计算一个容器中所有小于或等于 10 的元素的个数,可以这样给 count_if 传递值:
count_if(vec.begin(), vec.end(), bind2nd(less_equal<int>(), 10));
传给 count_if 的第三个实参使用 bind2nd 函数适配器,该适配器返回一个函数对象,该对象用 10 作右操作数应用 <= 操作符。这个 count_if 调用计算输入范围中小于或等于 10 的元素的个数。
标准库还定义了两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。为了对 less_equal 函数对象的绑定求反,可以编写这样的代码:
count_if(vec.begin(), vec.end(), not1(bind2nd(less_equal<int>(), 10)));
这里,首先将 less_equal 对象的第二个操作数绑定到 10,实际上是将该二元操作转换为一元操作。再用 not1 对操作的返回值求反,效果是测试每个元素是否 <=。然后,对结果真值求反。这个 count_if 调用的效果是对不 <= 10 的那些元素进行计数。