面向对象编程基础¶
本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org
ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book
第14章¶
重载运算与类型转换¶
¶
当运算符被用于类类型的对象时,C++语言允许我们为其指定新的含义;
同时,我们也能自定义类类型之间的转换规则。
- 类类型转换隐式地将一种类型的对象转换成另一种我们所需类型的对象。
当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。
- 明智地使用运算符重载能令我们的程序更易于编写和阅读。
cout << item1 + item2; // print the sum of two Sales_items
//没有重载运算符
print(cout, add(data1, data2)); // print the sum of two Sales_datas
基本概念¶
重载运算符是具有特殊名字的函数:
- 由关键字operator和其后要定义的运算符号共同组成。
- 和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。
当一个重载的运算符是成员函数时,this绑定到左侧运算对象。
- 成员运算符函数的(显式)参数数量比运算对象的数量少一个。
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:
// error: cannot redefine the built-in operator for ints
int operator+(int, int);
可以重载大多数(但不是全部)运算符。
只能重载已有的运算符,而无权发明新的运算符号
对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。
x == y + z;
x == (y + z);
运算符¶
可以被重载的运算符
+ - * / % ^
& ~ ! , =
< > <= >= ++ --
<< >> == != &&
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete[]
不可以被重载的运算符
:: .* . ? :
直接调用一个重载的运算符函数¶
//这两次调用是等价的,它们都调用了非成员函数operator+,
//传入data1作为第一个实参、传入data2作为第二个实参。
// equivalent calls to a nonmember operator function
data1 + data2; // normal expression
operator+(data1, data2); // equivalent function call
显式地调用成员运算符函数
data1 += data2; // expression-based ''call''
data1.operator+=(data2); // equivalent call to a member operator function
//这两条语句都调用了成员函数operator+=,
//将this绑定到data1的地址、将data2作为实参传入了函数。
某些运算符不应该被重载¶
某些运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们
- 某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,
所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上
- 逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。
&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,
两个运算对象总是会被求值。
- 一般不重载逗号运算符和取地址运算符
- C++语言已经定义了这两种运算符用于类类型对象时的特殊含义,
这一点与大多数运算符都不相同
这两种运算符已经有了内置的含义,所以一般来说它们不应该被重载,
否则它们的行为将异于常态,从而导致类的用户无法适应。
通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
使用与内置类型一致的含义¶
如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符
- 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。·
- 如果类的某个操作是检查相等性,则定义operator==;如果类有了operator==,意
味着它通常也应该有operator!=。·
- 如果类包含一个内在的单序比较操作,则定义operator<;
如果类有了operator<,则它也应该含有其他关系操作。·
- 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:
- 逻辑运算符和关系运算符应该返回bool,
- 算术运算符应该返回一个类类型的值,
- 赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。
提示:尽量明智地使用运算符重载¶
每个运算符在用于内置类型时都有比较明确的含义。
- 以二元+运算符为例,它明显执行的是加法操作。
因此,把二元+运算符映射到类类型的一个类似操作上可以极大地简化记忆。
string + string
当在内置的运算符和我们自己的操作之间存在逻辑映射关系时,运算符重载的效果最好。
- 此时,使用重载的运算符显然比另起一个名字更自然也更直观。
- 不过,过分滥用运算符重载也会使我们的类变得难以理解。
在实际编程过程中,一般没有特别明显的滥用运算符重载的情况。
- 例如,一般来说没有哪个程序员会定义operator+并让它执行减法操作。
- 经常发生的是,程序员可能会强行扭曲了运算符的“常规”含义使得其适应某种给定的类型,
这显然是我们不希望发生的。因此我们的建议是:只有当操作的含义对于用户来说
清晰明了时才使用运算符。
如果用户对运算符可能有几种不同的理解,则使用这样的运算符将产生二义性。
赋值和复合赋值运算符¶
赋值运算符的行为与复合版本的类似:
- 赋值之后,左侧运算对象和右侧运算对象的值相等,
并且运算符应该返回它左侧运算对象的一个引用。
- 重载的赋值运算应该继承而非违背其内置版本的含义。
如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。
+=运算符的行为显然应该与其内置版本一致,即先执行+,再执行=。
选择作为成员或者非成员¶
当我们定义重载的运算符时,必须首先决定是
- 将其声明为类的成员函数还是声明为一个普通的非成员函数。
- 在某些时候我们别无选择,因为有的运算符必须作为成员;
另一些情况下,运算符作为普通函数比作为成员更好。
准则
- 赋值(=)、下标([ ])、调用(( ))和成员访问箭头(->)运算符必须是成员。·
- 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。·
- 改变对象状态的运算符或者与给定类型密切相关的运算符,
如递增、递减和解引用运算符,通常应该是成员。·
- 具有对称性的运算符可能转换任意一端的运算对象,
例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
希望能在含有混合类型的表达式中使用对称性运算符
- 求一个int和一个double的和
- 加法是对称的。
如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。
运算符定义成成员函数时¶
左侧运算对象必须是运算符所属类的一个对象
string s = "world";
string t = s + "!"; // ok: we can add a const char* to a string
string u = "hi" + s; // would be an error if + were a member of string
//如果operator+是string类的成员,则上面的第一个加法等价于s.operator+("!")。
//同样的,"hi"+s等价于"hi".operator+(s)。
//显然"hi"的类型是const char*,这是一种内置类型,根本就没有成员函数。
//因为string将+定义成了普通的非成员函数,所以"hi"+s等价于operator+("hi",s)
//和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是至少有
//一个运算对象是类类型,并且两个运算对象都能准确无误地转换成string。
练习¶
在什么情况下重载的运算符与内置运算符有所区别?
在什么情况下重载的运算符又与内置运算符一样?
我们可以直接调用重载运算符函数。
重置运算符与内置运算符有一样的优先级与结合性。
练习¶
为 Sales_data 编写重载的输入、输出、加法和复合赋值运算符。
头文件:
#include <string>
#include <iostream>
class Sales_data {
friend std::istream& operator>>(std::istream&, Sales_data&); // input
friend std::ostream& operator<<(std::ostream&, const Sales_data&); // output
friend Sales_data operator+(const Sales_data&, const Sales_data&); // addition
public:
Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data() : Sales_data("", 0, 0.0f){ }
Sales_data(const std::string &s) : Sales_data(s, 0, 0.0f){ }
Sales_data(std::istream &is);
Sales_data& operator+=(const Sales_data&); // compound-assignment
std::string isbn() const { return bookNo; }
练习¶
private:
inline double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
std::istream& operator>>(std::istream&, Sales_data&);
std::ostream& operator<<(std::ostream&, const Sales_data&);
Sales_data operator+(const Sales_data&, const Sales_data&);
inline double Sales_data::avg_price() const
{
return units_sold ? revenue/units_sold : 0;
}
练习¶
主函数:
#include "ex_14_02.h"
Sales_data::Sales_data(std::istream &is) : Sales_data(){
is >> *this;
}
Sales_data& Sales_data::operator+=(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
std::istream& operator>>(std::istream &is, Sales_data &item){
double price = 0.0;
is >> item.bookNo >> item.units_sold >> price;
if (is)
item.revenue = price * item.units_sold;
else
item = Sales_data();
return is;
}
练习¶
std::ostream& operator<<(std::ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
return os;
}
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}
练习¶
string 和 vector 都定义了重载的==以比较各自的对象,
假设 svec1 和 svec2 是存放 string 的 vector,
确定在下面的表达式中分别使用了哪个版本的==?
(a) "cobble" == "stone"
(b) svec1[0] == svec2[0]
(c) svec1 == svec2
(d) svec1[0] == "stone"
(a) 内置版本的==,比较两个指针。
(b) string 版本的==
(c) vector 版本的==
(d) string 版本的==,字符串字面常量转换成string
练习¶
如何确定下列运算符是否应该是类的成员?
(a) %
(b) %=
(c) ++
(d) ->
(e) <<
(f) &&
(g) ==
(h) ()
(a) 通常定义成非成员。
(b) 是成员。会改变对象状态
(c) 是成员。会改变对象状态
(d) 必须是成员。
(e) 不需要是成员。
(f) 不需要是成员。
(g) 不需要是成员。
(h) 必须是成员。
练习¶
在7.5.1节中的练习7.40中,编写了下列类中某一个的框架,
请问在这个类中应该定义重载的运算符吗?如果是,请写出来。
(a) Book
(b) Date
(c) Employee
(d) Vehicle
(e) Object
(f) Tree
Book,应该重载。
头文件:
#include <iostream>
#include <string>
练习¶
class Book{
friend std::istream& operator>>(std::istream&, Book&);
friend std::ostream& operator<<(std::ostream&, const Book&);
friend bool operator==(const Book&, const Book&);
friend bool operator!=(const Book&, const Book&);
public:
Book() = default;
Book(unsigned no, std::string name, std::string author, std::string pubdate)
:no_(no), name_(name), author_(author), pubdate_(pubdate) {}
Book(std::istream &in) { in >> *this; }
private:
unsigned no_;
std::string name_;
std::string author_;
std::string pubdate_;
};
std::istream& operator>>(std::istream&, Book&);
std::ostream& operator<<(std::ostream&, const Book&);
bool operator==(const Book&, const Book&);
bool operator!=(const Book&, const Book&);
练习¶
实现:
#include "ex_14_5.h"
std::istream& operator>>(std::istream &in, Book &book){
in >> book.no_ >> book.name_ >> book.author_ >> book.pubdate_;
if (!in)
book = Book();
return in;
}
std::ostream& operator<<(std::ostream &out, const Book &book){
out << book.no_ << " " << book.name_ << " " << book.author_ << " " << book.pubdate_;
return out;
}
bool operator==(const Book &lhs, const Book &rhs){
return lhs.no_ == rhs.no_;
}
bool operator!=(const Book &lhs, const Book &rhs){
return !(lhs == rhs);
}
练习¶
测试:
#include "ex_14_5.h"
int main()
{
Book book1(66666, "CPP Primer", "someone", "2022");
Book book2(66666, "CPP Primer", "someone", "2022");
if (book1 == book2)
std::cout << book1 << std::endl;
}
输入和输出运算符¶
IO标准库分别使用>>和<<执行输入和输出操作。
对于这两个运算符来说,IO库定义了用其读写内置类型的版本,
而类则需要自定义适合其对象的新版本以支持IO操作。
重载输出运算符<<¶
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。
- 之所以ostream是非常量是因为向流写入内容会改变其状态;
- 而该形参是引用是因为我们无法直接复制一个ostream对象。
第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。
- 第二个形参是引用的原因是我们希望避免复制实参;
而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。
为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。
Sales_data的输出运算符¶
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
//完成输出后,运算符返回刚刚使用的ostream的引用。
输出运算符尽量减少格式化操作¶
用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符,
用户希望类的输出运算符也像如此行事。
如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印一些描述性的文本了。
相反,令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节。
通常输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
输入输出运算符必须是非成员函数¶
而不能是类的成员函数。否则,它们的左侧运算对象将是我们的类的一个对象:
Sales_data data;
data << cout; // if operator<< is a member of Sales_data
//假设输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员。
//然而,这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。
如果我们希望为类自定义IO运算符,则必须将其定义成非成员函数。
IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元
练习¶
为你的 Sales_data 类定义输出运算符。
参考之前。
练习¶
你在13.5节的练习中曾经编写了一个String类,为它定义一个输出运算符。
头文件:
#include <memory>
#include <iostream>
class String
{
friend std::ostream& operator<<(std::ostream&, const String&);
public:
String() : String("") {}
String(const char *);
String(const String&);
String& operator=(const String&);
~String();
const char *c_str() const { return elements; }
size_t size() const { return end - elements; }
size_t length() const { return end - elements - 1; }
练习¶
private:
std::pair<char*, char*> alloc_n_copy(const char*, const char*);
void range_initializer(const char*, const char*);
void free();
private:
char *elements;
char *end;
std::allocator<char> alloc;
};
std::ostream& operator<<(std::ostream&, const String&);
练习¶
实现:
#include "ex_14_7.h"
#include <algorithm>
#include <iostream>
std::pair<char*, char*>
String::alloc_n_copy(const char *b, const char *e)
{
auto str = alloc.allocate(e - b);
return{ str, std::uninitialized_copy(b, e, str) };
}
void String::range_initializer(const char *first, const char *last)
{
auto newstr = alloc_n_copy(first, last);
elements = newstr.first;
end = newstr.second;
}
练习¶
String::String(const char *s){
char *sl = const_cast<char*>(s);
while (*sl)
++sl;
range_initializer(s, ++sl);
}
String::String(const String& rhs){
range_initializer(rhs.elements, rhs.end);
std::cout << "copy constructor" << std::endl;
}
void String::free(){
if (elements)
{
std::for_each(elements, end,
[this](char &c) { alloc.destroy(&c); });
alloc.deallocate(elements, end - elements);
}
}
练习¶
String::~String(){
free();
}
String& String::operator = (const String &rhs){
auto newstr = alloc_n_copy(rhs.elements, rhs.end);
free();
elements = newstr.first;
end = newstr.second;
std::cout << "copy-assignment" << std::endl;
return *this;
}
std::ostream& operator<<(std::ostream &os, const String &s){
char *c = const_cast<char*>(s.c_str());
while (*c)
os << *c++;
return os;
}
练习¶
测试:
#include "ex_14_7.h"
int main()
{
String str("Hello World");
std::cout << str << std::endl;
}
练习¶
你在7.5.1节中的练习中曾经选择并编写了一个类,为它定义一个输出运算符。
参考之前
重载输入运算符>>¶
通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,
第二个形参是将要读入到的(非常量)对象的引用。
该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是
因为输入运算符本身的目的就是将数据读入到这个对象中。
Sales_data的输入运算符¶
istream &operator>>(istream &is, Sales_data &item)
{
double price; // no need to initialize; we'll read into price before we use it
is >> item.bookNo >> item.units_sold >> price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
输入时的错误¶
在执行输入运算符时可能发生下列错误:
当流含有错误类型的数据时读取操作可能失败。
- 例如在读取完bookNo后,输入运算符假定接下来读入的是两个数字数据,
一旦输入的不是数字数据,则读取操作及后续对流的其他使用都将失败。·
当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。
//在程序中我们没有逐个检查每个读取操作,
//而是等读取了所有数据后赶在使用这些数据前一次性检查:
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
当读取操作发生错误时,输入运算符应该负责从错误中恢复。
标示错误¶
一些输入运算符需要做更多数据验证的工作。
- 例如,我们的输入运算符可能需要检查bookNo是否符合规范的格式。
在这样的例子中,即使从技术上来看IO是成功的,
输入运算符也应该设置流的条件状态以标示出失败信息。
- 通常情况下,输入运算符只设置failbit。
除此之外,设置eofbit表示文件耗尽,而设置badbit表示流被破坏。
最好的方式是由IO标准库自己来标示这些错误。
练习¶
为你的 Sales_data 类定义输入运算符。
class Sales_data{
friend istream &operator>>(istream &is, Sales_data &item);
//其它成员
}
istream &operator>>(istream &is, Sales_data &item)
{
double price; // no need to initialize; we'll read into price before we use it
is >> item.bookNo >> item.units_sold >> price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
练习¶
对于 Sales_data 的输入运算符来说如果给定了下面的输入将发生什么情况?
(a) 0-201-99999-9 10 24.95
(b) 10 24.95 0-210-99999-9
(a) 格式正确。
(b) 不合法的输入。因为程序试图将 0-210-99999-9 转换为 floatl。
程序里会给Sales_data对象一个默认值
练习¶
下面的 Sales_data 输入运算符存在错误吗?
如果有,请指出来。
对于这个输入运算符如果仍然给定上个练习的输入将会发生什么情况?
istream& operator>>(istream& in, Sales_data& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
s.revence = s.units_sold * price;
return in;
}
没有输入检查
对于a,输入合法,程序正确执行
对于b,输入错误,程序和预期不一致
练习¶
你在7.5.1节的练习中曾经选择并编写了一个类,
为它定义一个输入运算符并确保该运算符可以处理输入错误。
参考之前。
算术和关系运算符¶
通常情况下,我们把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算
对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
算术运算符通常会计算它的两个运算对象并得到一个新值,
- 这个值有别于任意一个运算对象,常常位于一个局部变量之内,
操作完成后返回该局部变量的副本作为其结果。
如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。
此时,最有效的方式是使用复合赋值来定义算术运算符:
// assumes that both objects refer to the same book
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // copy data members from lhs into sum
sum += rhs; // add rhs into sum
return sum;
}
如果类同时定义了算术运算符和相关的复合赋值运算符,
则通常情况下应该使用复合赋值来实现算术运算符。
练习¶
你认为 Sales_data 类还应该支持哪些其他算术运算符?
如果有的话,请给出它们的定义。
没有其他了。
练习¶
你觉得为什么调用 operator+= 来定义operator+ 比其他方法更有效?
因为用 operator+= 会避免使用一个临时对象,而使得更有效。
而且代码复用,可读性好。
练习¶
你在7.5.1节的练习7.40中曾经选择并编写了一个类,
你认为它应该含有其他算术运算符吗?如果是,请实现它们;如果不是,解释原因。
头文件:
#include <iostream>
#include <string>
class Book{
friend std::istream& operator>>(std::istream&, Book&);
friend std::ostream& operator<<(std::ostream&, const Book&);
friend bool operator==(const Book&, const Book&);
friend bool operator!=(const Book&, const Book&);
friend bool operator<(const Book&, const Book&);
friend bool operator>(const Book&, const Book&);
friend Book operator+(const Book&, const Book&);
public:
Book() = default;
Book(unsigned no, std::string name, std::string author,
std::string pubdate, unsigned number) :no_(no), name_(name),
author_(author), pubdate_(pubdate), number_(number) {}
Book(std::istream &in) { in >> *this; }
Book& operator+=(const Book&);
练习¶
private:
unsigned no_;
std::string name_;
std::string author_;
std::string pubdate_;
unsigned number_;
};
std::istream& operator>>(std::istream&, Book&);
std::ostream& operator<<(std::ostream&, const Book&);
bool operator==(const Book&, const Book&);
bool operator!=(const Book&, const Book&);
bool operator<(const Book&, const Book&);
bool operator>(const Book&, const Book&);
Book operator+(const Book&, const Book&);
练习¶
实现:
#include "ex_14_15.h"
std::istream& operator>>(std::istream &in, Book &book)
{
in >> book.no_ >> book.name_ >> book.author_ >> book.pubdate_ >> book.number_;
return in;
}
std::ostream& operator<<(std::ostream &out, const Book &book)
{
out << book.no_ << " " << book.name_ << " " << book.author_ << " " << book.pubdate_ << " " << book.number_ << std::endl;
return out;
}
bool operator==(const Book &lhs, const Book &rhs)
{
return lhs.no_ == rhs.no_;
}
bool operator!=(const Book &lhs, const Book &rhs)
{
return !(lhs == rhs);
}
练习¶
bool operator<(const Book &lhs, const Book &rhs){
return lhs.no_ < rhs.no_;
}
bool operator>(const Book &lhs, const Book &rhs){
return rhs < lhs;
}
Book& Book::operator+=(const Book &rhs){
if (rhs == *this)
this->number_ += rhs.number_;
return *this;
}
Book operator+(const Book &lhs, const Book &rhs){
Book book = lhs;
book += rhs;
return book;
}
练习¶
测试:
#include "ex_14_15.h"
int main()
{
Book book1(66666, "CPP Primer", "someone", "2022",1);
Book book2(66666, "CPP Primer", "someone", "2022",2);
std::cout << book1 + book2 << std::endl;
return 0;
}
相等运算符¶
通常情况下,C++中的类通过定义相等运算符来检验两个对象是否相等。
- 它们会比较对象的每一个数据成员,
- 只有当所有对应的成员都相等时才认为两个对象相等。
- 依据这一思想,我们的Sales_data类的相等运算符不但应该比较bookNo
还应该比较具体的销售数据:
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
相等运算符¶
设计准则
- 如果一个类含有判断两个对象是否相等的操作,
则它显然应该把函数定义成operator==而非一个普通的命名函数:
因为用户肯定希望能使用==比较对象,所以提供了==就意味着用户
无须再费时费力地学习并记忆一个全新的函数名字。
此外,类定义了==运算符之后也更容易使用标准库容器和算法。·
- 如果类定义了operator==,
则该运算符应该能判断一组给定的对象中是否含有重复数据。·
- 通常情况下,相等运算符应该具有传递性,
换句话说,如果a==b和b==c都为真,则a==c也应该为真。·
- 如果类定义了operator==,则这个类也应该定义operator!=。
对于用户来说,当他们能使用==时肯定也希望能使用!=,反之亦然。·
- 相等运算符和不相等运算符中的一个应该把工作委托给另外一个,
这意味着其中一个运算符应该负责实际比较对象的工作,
而另一个运算符则只是调用那个真正工作的运算符。
如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,
这样做可以使得用户更容易使用标准库算法来处理这个类。
练习¶
为你的 StrBlob 类、StrBlobPtr 类、StrVec 类和 String 类
分别定义相等运算符和不相等运算符。
class StrBlob{
friend bool operator==(const StrBlob &lhs, const StrBlob &rhs);
friend bool operator!=(const StrBlob &lhs, const StrBlob &rhs);
//其它成员
};
bool operator==(const StrBlob &lhs, const StrBlob &rhs)
{
return lhs.data == rhs.data;//指向vector相同
}
bool operator!=(const StrBlob &lhs, const StrBlob &rhs)
{
return !(lhs == rhs);
}
练习¶
class StrBlobPtr{
friend bool operator==(const StrBlobPtr &lhs, const StrBlobPtr &rhs);
friend bool operator!=(const StrBlobPtr &lhs, const StrBlobPtr &rhs);
//其它成员
};
bool operator==(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
{
//wptr 是 weak_ptr
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
if(l == r){
return !r|| lhs.curr == rhs.curr;
}
else return false;
}
bool operator!=(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
{
return !(lhs == rhs);
}
练习¶
class StrVec{
friend bool operator==(const StrVec &lhs, const StrVec &rhs);
friend bool operator!=(const StrVec &lhs, const StrVec &rhs);
//其它成员
};
bool operator==(const StrVec &lhs, const StrVec &rhs){
if(lhs.size()!=rhs.size())
return false;
for(auto itr1=lhs.begin(),itr2=rhs.begin();
itr1!=lhs.end()&&itr2!=rhs.end();itr1++,itr2++ ){
if(*itr1!=*itr2) return false;
}
return true;
}
bool operator!=(const StrVec &lhs, const StrVec &rhs){
return !(lhs == rhs);
}
练习¶
class String{
friend bool operator==(const String &lhs, const String &rhs);
friend bool operator!=(const String &lhs, const String &rhs);
//其它成员
};
bool operator==(const String &lhs, const String &rhs)
{
return strcmp(lhs.c_str(),rhs.c_str());
}
bool operator!=(const String &lhs, const String &rhs)
{
return !(lhs == rhs);
}
练习¶
你在7.5.1节中的练习7.40中曾经选择并编写了一个类,
你认为它应该含有相等运算符吗?如果是,请实现它;如果不是,解释原因。
参考14.15。
关系运算符¶
定义了相等运算符的类也常常(但不总是)包含关系运算符。
特别是,因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用。
通常情况下关系运算符应该
1.定义顺序关系,令其与关联容器中对关键字的要求一致;并且
2.如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。
特别是,如果两个对象是!=的,那么一个对象应该<另外一个。
- 对于Sales_data类来说,不存在一种逻辑可靠的<定义,这个类不定义<运算符也许更好。
如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。
如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。
练习¶
为你的 StrBlob 类、StrBlobPtr 类、StrVec 类和 String 类分别定义关系运算符。
这里定义<,其余类似
class String{
friend bool operator < (const String &s1,const String &s2);
//其它成员
};
bool operator < (const String &s1,const &s2){
return strcmp(s1.c_str(),s2.c_str());
}
class StrBlob{
friend bool operator < (const StrBlob &s1, const StrBlob s2);
//其它成员
};
bool operator < (const StrBlob &s1, const StrBlob s2){
return *s1.data<*s2.data;
}
练习¶
class StrBlobPtr{
friend bool operator < (const StrBlobPtr &s1, const StrBlobPtr s2);
//其它成员
};
bool operator < (const StrBlobPtr &s1, const StrBlobPtr s2){
auto l=s1.wptr.lock(),r=s2.wptr.lock();
if(l==r){
if(!r)
return false;//空指针
else
return lhs.curr<rhs.curr;//比较位置
}
else
return false;
}
练习¶
class StrVec{
friend bool operator < (const StrVec &s1, const StrVec s2);
//其它成员
};
bool operator < (const StrVec &s1, const StrVec s2){
auto p1=s1.begin(),p2=s2.begin();
for(;p1!=s1.end()&&p2!=s2.end();p1++,p2++){
if(*p1<*p2)
return true;
else if(*p1>*p2)
return false;
}
if(p1==s1.end()&&p2!=s2.end())
return true;
return false;
}
¶
你在7.5.1节的练习7.40中曾经选择并编写了一个类,你认为它应该含有关系运算符吗?
如果是,请实现它;如果不是,解释原因。
参考14.15。
赋值运算符¶
拷贝赋值和移动赋值运算符,可以把类的一个对象赋值给该类的另一个对象。
此外,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。
- 举个例子,在拷贝赋值和移动赋值运算符之外,标准库vector类还定义了
第三种赋值运算符,该运算符接受花括号内的元素列表作为参数。
vector<string> v;
v = {"a", "an", "the"};
同样,也可以把这个运算符添加到StrVec类中
class StrVec {
public:
StrVec &operator=(std::initializer_list<std::string>);
// other members as in § 13.5
};
赋值运算符¶
//这个新的赋值运算符将返回其左侧运算对象的引用:
StrVec &StrVec::operator=(initializer_list<string> il){
// alloc_n_copy allocates space and copies elements from the given range
auto data = alloc_n_copy(il.begin(), il.end());
free(); // destroy the elements in this object and free the space
elements = data.first; // update data members to point to the new space
first_free = cap = data.second;
return *this;
}
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
复合赋值运算符¶
倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部
为了与内置类型的复合赋值保持一致,也要返回其左侧运算对象的引用
// member binary operator: left-hand operand is bound to the implicit this pointer
// assumes that both objects refer to the same book
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这样做。
这两类运算符都应该返回左侧运算对象的引用。
练习¶
为你的 Sales_data 类定义加法和复合赋值运算符。
参考之前
练习¶
编写 Sales_data 类的+ 和+= 运算符,使得 + 执行实际的加法操作而 += 调用+。
相比14.3节和14.4节对这两个运算符的定义,本题的定义有何缺点?试讨论之。
缺点:使用了一个 Sales_data 的临时对象,但它并不是必须的。
练习¶
定义赋值运算符的一个新版本,
使得我们能把一个表示 ISBN 的 string 赋给一个 Sales_data 对象。
头文件:
#include <string>
#include <iostream>
class Sales_data
{
friend std::istream& operator>>(std::istream&, Sales_data&);
friend std::ostream& operator<<(std::ostream&, const Sales_data&);
friend Sales_data operator+(const Sales_data&, const Sales_data&);
public:
Sales_data(const std::string &s, unsigned n, double p)
:bookNo(s), units_sold(n), revenue(n*p) {}
Sales_data() : Sales_data("", 0, 0.0f) {}
Sales_data(const std::string &s) : Sales_data(s, 0, 0.0f) {}
Sales_data(std::istream &is);
Sales_data& operator=(const std::string&);
Sales_data& operator+=(const Sales_data&);
std::string isbn() const { return bookNo; }
练习¶
private:
inline double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
std::istream& operator>>(std::istream&, Sales_data&);
std::ostream& operator<<(std::ostream&, const Sales_data&);
Sales_data operator+(const Sales_data&, const Sales_data&);
inline double Sales_data::avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
练习¶
实现:
#include "ex_14_22.h"
Sales_data::Sales_data(std::istream &is) : Sales_data(){
is >> *this;
}
Sales_data& Sales_data::operator+=(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
std::istream& operator>>(std::istream &is, Sales_data &item){
double price = 0.0;
is >> item.bookNo >> item.units_sold >> price;
if (is)
item.revenue = price * item.units_sold;
else
item = Sales_data();
return is;
}
练习¶
std::ostream& operator<<(std::ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}
Sales_data& Sales_data::operator=(const std::string &isbn)
{
*this = Sales_data(isbn);
return *this;
}
练习¶
测试:
#include "ex_4_22.h"
int main()
{
std::string strCp5("C++ Primer 5th");
Sales_data cp5 = strCp5;
std::cout << cp5 << std::endl;
}
练习¶
为你的StrVec 类定义一个 initializer_list 赋值运算符。
头文件:
#include <memory>
#include <string>
#include <initializer_list>
#ifndef _MSC_VER
#define NOEXCEPT noexcept
#else
#define NOEXCEPT
#endif
class StrVec
{
friend bool operator==(const StrVec&, const StrVec&);
friend bool operator!=(const StrVec&, const StrVec&);
friend bool operator< (const StrVec&, const StrVec&);
friend bool operator> (const StrVec&, const StrVec&);
friend bool operator<=(const StrVec&, const StrVec&);
friend bool operator>=(const StrVec&, const StrVec&);
练习¶
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(std::initializer_list<std::string>);
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
StrVec(StrVec&&) NOEXCEPT;
StrVec& operator=(StrVec&&)NOEXCEPT;
~StrVec();
StrVec& operator=(std::initializer_list<std::string>);
void push_back(const std::string&);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
std::string& at(size_t pos) { return *(elements + pos); }
const std::string& at(size_t pos) const { return *(elements + pos); }
void reserve(size_t new_cap);
void resize(size_t count);
void resize(size_t count, const std::string&);
练习¶
private:
std::pair<std::string*, std::string*> alloc_n_copy(
const std::string*, const std::string*);
void free();
void chk_n_alloc() { if (size() == capacity()) reallocate(); }
void reallocate();
void alloc_n_move(size_t new_cap);
void range_initialize(const std::string*, const std::string*);
private:
std::string *elements;
std::string *first_free;
std::string *cap;
std::allocator<std::string> alloc;
};
练习¶
bool operator==(const StrVec&, const StrVec&);
bool operator!=(const StrVec&, const StrVec&);
bool operator< (const StrVec&, const StrVec&);
bool operator> (const StrVec&, const StrVec&);
bool operator<=(const StrVec&, const StrVec&);
bool operator>=(const StrVec&, const StrVec&);
练习¶
实现:
#include "ex_14_23.h"
#include <algorithm>
void StrVec::push_back(const std::string &s)
{
chk_n_alloc();
alloc.construct(first_free++, s);
}
std::pair<std::string*, std::string*>
StrVec::alloc_n_copy(const std::string *b, const std::string *e)
{
auto data = alloc.allocate(e - b);
return{ data, std::uninitialized_copy(b, e, data) };
}
练习¶
void StrVec::free()
{
if (elements)
{
for_each(elements, first_free,
[this](std::string &rhs) { alloc.destroy(&rhs); });
alloc.deallocate(elements, cap - elements);
}
}
void StrVec::range_initialize(const std::string *first, const std::string *last)
{
auto newdata = alloc_n_copy(first, last);
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::StrVec(const StrVec &rhs)
{
range_initialize(rhs.begin(), rhs.end());
}
练习¶
StrVec::StrVec(std::initializer_list<std::string> il)
{
range_initialize(il.begin(), il.end());
}
StrVec::~StrVec()
{
free();
}
StrVec& StrVec::operator = (const StrVec &rhs)
{
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
练习¶
void StrVec::alloc_n_move(size_t new_cap){
auto newdata = alloc.allocate(new_cap);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + new_cap;
}
void StrVec::reallocate(){
auto newcapacity = size() ? 2 * size() : 1;
alloc_n_move(newcapacity);
}
void StrVec::reserve(size_t new_cap){
if (new_cap <= capacity()) return;
alloc_n_move(new_cap);
}
练习¶
void StrVec::resize(size_t count)
{
resize(count, std::string());
}
void StrVec::resize(size_t count, const std::string &s)
{
if (count > size())
{
if (count > capacity()) reserve(count * 2);
for (size_t i = size(); i != count; ++i)
alloc.construct(first_free++, s);
}
else if (count < size())
{
while (first_free != elements + count)
alloc.destroy(--first_free);
}
}
练习¶
StrVec::StrVec(StrVec &&s) NOEXCEPT :
elements(s.elements), first_free(s.first_free), cap(s.cap)
{
// leave s in a state in which it is safe to run the destructor.
s.elements = s.first_free = s.cap = nullptr;
}
StrVec& StrVec::operator = (StrVec &&rhs) NOEXCEPT
{
if (this != &rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
练习¶
bool operator==(const StrVec &lhs, const StrVec &rhs)
{
return (lhs.size() == rhs.size() &&
std::equal(lhs.begin(), lhs.end(), rhs.begin()));
}
bool operator!=(const StrVec &lhs, const StrVec &rhs)
{
return !(lhs == rhs);
}
bool operator<(const StrVec &lhs, const StrVec &rhs)
{
return std::lexicographical_compare(
lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
bool operator>(const StrVec &lhs, const StrVec &rhs)
{
return rhs < lhs;
}
练习¶
bool operator<=(const StrVec &lhs, const StrVec &rhs)
{
return !(rhs < lhs);
}
bool operator>=(const StrVec &lhs, const StrVec &rhs)
{
return !(lhs < rhs);
}
StrVec& StrVec::operator=(std::initializer_list<std::string> il)
{
*this = StrVec(il);
return *this;
}
练习¶
测试:
#include "ex_14_23.h"
#include <vector>
#include <iostream>
int main()
{
StrVec vec;
vec.reserve(6);
std::cout << "capacity(reserve to 6): " << vec.capacity() << std::endl;
vec.reserve(4);
std::cout << "capacity(reserve to 4): " << vec.capacity() << std::endl;
vec.push_back("hello");
vec.push_back("world");
vec.resize(4);
练习¶
for (auto i = vec.begin(); i != vec.end(); ++i)
std::cout << *i << std::endl;
std::cout << "-EOF-" << std::endl;
vec.resize(1);
for (auto i = vec.begin(); i != vec.end(); ++i)
std::cout << *i << std::endl;
std::cout << "-EOF-" << std::endl;
StrVec vec_list{ "hello", "world", "pezy" };
for (auto i = vec_list.begin(); i != vec_list.end(); ++i)
std::cout << *i << " ";
std::cout << std::endl;
练习¶
// Test operator==
const StrVec const_vec_list = { "hello", "world", "pezy" };
if (vec_list == const_vec_list)
for (const auto &str : const_vec_list)
std::cout << str << " ";
std::cout << std::endl;
// Test operator<
const StrVec const_vec_list_small = { "hello", "pezy", "ok" };
std::cout << (const_vec_list_small < const_vec_list) << std::endl;
}
练习¶
你在7.5.1节的练习7.40中曾经选择并编写了一个类,
你认为它应该含有拷贝赋值和移动赋值运算符吗?如果是,请实现它们。
头文件:
#ifndef DATE_H
#define DATE_H
#ifndef _MSC_VER
#define NOEXCEPT noexcept
#else
#define NOEXCEPT
#endif
#include <iostream>
#include <vector>
练习¶
class Date
{
friend bool operator ==(const Date& lhs, const Date& rhs);
friend bool operator < (const Date &lhs, const Date &rhs);
friend bool check(const Date &d);
friend std::ostream& operator <<(std::ostream& os, const Date& d);
public:
typedef std::size_t Size;
// default constructor
Date() = default;
// constructor taking Size as days
explicit Date(Size days);
// constructor taking three Size
Date(Size d, Size m, Size y) : day(d), month(m), year(y) {}
// constructor taking iostream
Date(std::istream &is, std::ostream &os);
练习¶
// copy constructor
Date(const Date& d);
// move constructor
Date(Date&& d) NOEXCEPT;
// copy operator=
Date& operator= (const Date& d);
// move operator=
Date& operator= (Date&& rhs) NOEXCEPT;
// destructor -- in this case, user-defined destructor is not nessary.
~Date() { std::cout << "destroying\n"; }
// members
Size toDays() const; //not implemented yet.
Date& operator +=(Size offset);
Date& operator -=(Size offset);
练习¶
private:
Size day = 1;
Size month = 1;
Size year = 0;
};
static const Date::Size YtoD_400 = 146097; //365*400 + 400/4 -3 == 146097
static const Date::Size YtoD_100 = 36524; //365*100 + 100/4 -1 == 36524
static const Date::Size YtoD_4 = 1461; //365*4 + 1 == 1461
static const Date::Size YtoD_1 = 365; //365
练习¶
// normal year
static const std::vector<Date::Size> monthsVec_n =
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// leap year
static const std::vector<Date::Size> monthsVec_l =
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// non-member operators: << >> - == != < <= > >=
std::ostream& operator <<(std::ostream& os, const Date& d);
std::istream& operator >>(std::istream& is, Date& d);
int operator - (const Date& lhs, const Date& rhs);
bool operator ==(const Date& lhs, const Date& rhs);
bool operator !=(const Date& lhs, const Date& rhs);
bool operator < (const Date& lhs, const Date& rhs);
bool operator <=(const Date& lhs, const Date& rhs);
bool operator >(const Date& lhs, const Date& rhs);
bool operator >=(const Date& lhs, const Date& rhs);
Date operator - (const Date& lhs, Date::Size rhs);
Date operator +(const Date& lhs, Date::Size rhs);
练习¶
// utillities:
bool check(const Date &d);
inline bool isLeapYear(Date::Size y);
// check if the date object passed in is valid
inline bool check(const Date &d)
{
if (d.month == 0 || d.month >12)
return false;
else
{
// month == 1 3 5 7 8 10 12
if (d.month == 1 || d.month == 3 || d.month == 5 || d.month == 7 ||
d.month == 8 || d.month == 10 || d.month == 12)
{
if (d.day == 0 || d.day > 31) return false;
else
return true;
}
练习¶
else
{ // month == 4 6 9 11
if (d.month == 4 || d.month == 6 ||d.month==9||d.month==11){
if (d.day == 0 || d.day > 30) return false;
else
return true;
}
else{ // month == 2
if (isLeapYear(d.year)){
if (d.day == 0 || d.day >29) return false;
else return true;
}
else{
if (d.day == 0 || d.day >28) return false;
else return true;
}
}
}
}
}
练习¶
inline bool isLeapYear(Date::Size y)
{
if (!(y % 400))
{
return true;
}
else
{
if (!(y % 100))
{
return false;
}
else
return !(y % 4);
}
}
#endif // DATE_H
练习¶
实现:
#include "ex_14_24.h"
#include <algorithm>
// constructor taking Size as days
// the argument must be within (0, 2^32)
Date::Date(Size days)
{
// calculate the year
Size y400 = days / YtoD_400;
Size y100 = (days - y400*YtoD_400) / YtoD_100;
Size y4 = (days - y400*YtoD_400 - y100*YtoD_100) / YtoD_4;
Size y = (days - y400*YtoD_400 - y100*YtoD_100 - y4*YtoD_4) / 365;
Size d = days - y400*YtoD_400 - y100*YtoD_100 - y4*YtoD_4 - y * 365;
this->year = y400 * 400 + y100 * 100 + y4 * 4 + y;
// check if leap and choose the months vector accordingly
std::vector<Size>currYear
= isLeapYear(this->year) ? monthsVec_l : monthsVec_n;
练习¶
// calculate day and month using find_if + lambda
Size D_accumu = 0, M_accumu = 0;
std::find_if(currYear.cbegin(), currYear.cend(), [&](Size m)
{
D_accumu += m;
M_accumu++;
if (d < D_accumu)
{
this->month = M_accumu;
this->day = d + m - D_accumu;
return true;
}
else
return false;
});
}
练习¶
// construcotr taking iostream
Date::Date(std::istream &is, std::ostream &os)
{
is >> day >> month >> year;
if (is)
{
if (check(*this)) return;
else
{
os << "Invalid input! Object is default initialized.";
*this = Date();
}
}
else
{
os << "Invalid input! Object is default initialized.";
*this = Date();
}
}
练习¶
// copy constructor
Date::Date(const Date &d) :
day(d.day), month(d.month), year(d.year) {}
// move constructor
Date::Date(Date&& d) NOEXCEPT :
day(d.day), month(d.month), year(d.year)
{ std::cout << "copy moving"; }
// copy operator=
Date &Date::operator= (const Date &d)
{
this->day = d.day;
this->month = d.month;
this->year = d.year;
return *this;
}
练习¶
// move operator=
Date &Date::operator =(Date&& rhs) NOEXCEPT
{
if (this != &rhs)
{
this->day = rhs.day;
this->month = rhs.month;
this->year = rhs.year;
}
std::cout << "moving =";
return *this;
}
练习¶
// conver to days
Date::Size Date::toDays() const
{
Size result = this->day;
// check if leap and choose the months vector accordingly
std::vector<Size>currYear
= isLeapYear(this->year) ? monthsVec_l : monthsVec_n;
// calculate result + days by months
for (auto it = currYear.cbegin();
it != currYear.cbegin() + this->month - 1; ++it)
result += *it;
// calculate result + days by years
result += (this->year / 400) * YtoD_400;
result += (this->year % 400 / 100) * YtoD_100;
result += (this->year % 100 / 4) * YtoD_4;
result += (this->year % 4) * YtoD_1;
return result;
}
练习¶
// member operators: += -=
Date &Date::operator +=(Date::Size offset)
{
*this = Date(this->toDays() + offset);
return *this;
}
Date &Date::operator -=(Date::Size offset)
{
if (this->toDays() > offset)
*this = Date(this->toDays() - offset);
else
*this = Date();
return *this;
}
练习¶
// non-member operators: << >> - == != < <= > >=
std::ostream&
operator <<(std::ostream& os, const Date& d)
{
os << d.day << " " << d.month << " " << d.year;
return os;
}
std::istream& operator >>(std::istream& is, Date& d)
{
if (is)
{
Date input = Date(is, std::cout);
if (check(input)) d = input;
}
return is;
}
练习¶
int operator -(const Date &lhs, const Date &rhs)
{
return lhs.toDays() - rhs.toDays();
}
bool operator ==(const Date &lhs, const Date &rhs)
{
return (lhs.day == rhs.day) &&
(lhs.month == rhs.month) &&
(lhs.year == rhs.year);
}
bool operator !=(const Date &lhs, const Date &rhs)
{
return !(lhs == rhs);
}
练习¶
bool operator < (const Date &lhs, const Date &rhs)
{
return lhs.toDays() < rhs.toDays();
}
bool operator <=(const Date &lhs, const Date &rhs)
{
return (lhs < rhs) || (lhs == rhs);
}
bool operator >(const Date &lhs, const Date &rhs)
{
return !(lhs <= rhs);
}
bool operator >=(const Date &lhs, const Date &rhs)
{
return !(lhs < rhs);
}
练习¶
Date operator - (const Date &lhs, Date::Size rhs)
{ // ^^^ rhs must not be larger than 2^32-1
// copy lhs
Date result(lhs);
result -= rhs;
return result;
}
Date operator + (const Date &lhs, Date::Size rhs)
{ // ^^^ rhs must not be larger than 2^32-1
// copy lhs
Date result(lhs);
result += rhs;
return result;
}
练习¶
测试:
#include "ex_14_24.h"
#include <iostream>
int main()
{
Date lhs(9999999), rhs(1);
std::cout << (lhs -= 12000) << "\n";
return 0;
}
练习¶
上题的这个类还需要定义其他赋值运算符吗
?如果是,请实现它们;同时说明运算对象应该是什么类型并解释原因。
是。参考之前。
下标运算符¶
表示容器的类通常可以通过元素在容器中的位置访问元素,
这些类一般会定义下标运算符operator[]。下标运算符必须是成员函数。
为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,
- 这样做的好处是下标可以出现在赋值运算符的任意一端。
- 进一步,我们最好同时定义下标运算符的常量版本和非常量版本,
当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。
如果一个类包含下标运算符,则它通常会定义两个版本:
- 一个返回普通引用,
- 另一个是类的常量成员并且返回常量引用。
下标运算符¶
//定义StrVec的下标运算符:
class StrVec {
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
const std::string& operator[](std::size_t n) const
{ return elements[n]; }
// other members as in § 13.5
private:
std::string *elements; // pointer to the first element in the array
};
//当StrVec是非常量时,我们可以给元素赋值;
//而当我们对常量对象取下标时,不能为其赋值:
// assume svec is a StrVec
const StrVec cvec = svec; // copy elements from svec into cvec
// if svec has any elements, run the string empty function on the first one
if (svec.size() && svec[0].empty()) {
svec[0] = "zero"; // ok: subscript returns a reference to a string
cvec[0] = "Zip"; // error: subscripting cvec returns a reference to const
}
练习¶
为你的 StrBlob 类、StrBlobPtr 类、StrVec 类和 String 类定义下标运算符。
class StrBlob{
public:
std::string& operator[](std::size_t n){return data[n];}
const std::string& operator[](std::size_t n) const {return data[n];}
//其它成员
};
class StrBlobPtr{
public:
std::string& operator[](std::size_t n){return (*wptr.lock())[n];}
const std::string& operator[](std::size_t n) const {
return (*wptr.lock())[n];}
//其它成员
};
练习¶
class StrVec{
public:
std::string& operator[](std::size_t n){return elements[n];}
const std::string& operator[](std::size_t n) const {return elements[n];}
//其它成员
};
class String{
public:
std::string& operator[](std::size_t n){
char* str= c_str();return str[n];}
const std::string& operator[](std::size_t n) const {
char* str= c_str();return str[n];}
//其它成员
};
递增和递减运算符¶
定义递增和递减运算符的类应该同时定义前置版本和后置版本。
- 这些运算符通常应该被定义成类的成员。
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
//定义前置递增/递减运算符
class StrBlobPtr {
public:
// increment and decrement
StrBlobPtr& operator++(); // prefix operators
StrBlobPtr& operator--();
// other members as before
};
递增和递减运算符¶
//在递增运算符的例子中,我们把curr的当前值传递给check函数。
//如果这个值小于vector的大小,则check正常返回;
//否则,如果curr已经到达了vector的末尾,check将抛出异常:
// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator++(){
// if curr already points past the end of the container, can't increment it
check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
//递减运算符先递减curr,然后调用check函数。
//此时,如果curr(一个无符号数)已经是0了,
//那么我们传递给check的值将是一个表示无效下标的非常大的正数值。
StrBlobPtr& StrBlobPtr::operator--(){
// if curr is zero, decrementing it will yield an invalid subscript
--curr; // move the current state back one element
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
区分前置和后置运算符¶
后置版本接受一个额外的(不被使用)int类型的形参。
当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。
尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。
因为我们不会用到int形参,所以无须为其命名。
//为StrBlobPtr添加后置运算符:
class StrBlobPtr {
public:
// increment and decrement
StrBlobPtr operator++(int); // postfix operators
StrBlobPtr operator--(int);
// other members as before
};
为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),
返回的形式是一个值而非引用。
区分前置和后置运算符¶
//对于后置版本来说,在递增对象之前需要首先记录对象的状态:
StrBlobPtr StrBlobPtr::operator++(int){
// no check needed here; the call to prefix increment will do the check
StrBlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
StrBlobPtr StrBlobPtr::operator--(int){
// no check needed here; the call to prefix decrement will do the check
StrBlobPtr ret = *this; // save the current value
--*this; // move backward one element; prefix -- checks the decrement
return ret; // return the saved state
}
//我们的后置运算符调用各自的前置版本来完成实际的工作。
//例如后置递增运算符执行++*this
//该表达式调用前置递增运算符,前置递增运算符首先检查递增操作是否安全,
//根据检查的结果抛出一个异常或者执行递增curr的操作。
//假定通过了检查,则后置函数返回事先存好的ret的副本。
//因此最终的效果是,对象本身向前移动了一个元素,
//而返回的结果仍然反映对象在未递增之前原始的值。
显式地调用后置运算符¶
如果我们想通过函数调用的方式调用后置版本,则必须为它的整型参数传递一个值:
StrBlobPtr p(a1); // p points to the vector inside a1
p.operator++(0); // call postfix operator++
p.operator++(); // call prefix operator++
尽管传入的值通常会被运算符函数忽略,但却必不可少,
因为编译器只有通过它才能知道应该使用后置版本。
练习¶
为你的 StrBlobPtr 类添加递增和递减运算符。
只显示添加的代码:
class StrBlobPtr {
public:
string& deref() const;
StrBlobPtr& operator++();
StrBlobPtr& operator--();
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
StrBlobPtr& operator+=(size_t);
StrBlobPtr& operator-=(size_t);
StrBlobPtr operator+(size_t) const;
StrBlobPtr operator-(size_t) const;
};
inline StrBlobPtr& StrBlobPtr::operator++(){
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
练习¶
inline StrBlobPtr& StrBlobPtr::operator--()
{
check(--curr, "decrement past begin of StrBlobPtr");
return *this;
}
inline StrBlobPtr StrBlobPtr::operator++(int)
{
StrBlobPtr ret = *this;
++*this;
return ret;
}
inline StrBlobPtr StrBlobPtr::operator--(int)
{
StrBlobPtr ret = *this;
--*this;
return ret;
}
练习¶
inline StrBlobPtr& StrBlobPtr::operator+=(size_t n)
{
curr += n;
check(curr, "increment past end of StrBlobPtr");
return *this;
}
inline StrBlobPtr& StrBlobPtr::operator-=(size_t n)
{
curr -= n;
check(curr, "increment past end of StrBlobPtr");
return *this;
}
inline StrBlobPtr StrBlobPtr::operator+(size_t n) const
{
StrBlobPtr ret = *this;
ret += n;
return ret;
}
练习¶
inline StrBlobPtr StrBlobPtr::operator-(size_t n) const
{
StrBlobPtr ret = *this;
ret -= n;
return ret;
}
练习¶
为你的 StrBlobPtr 类添加加法和减法运算符,使其可以实现指针的算术运算。
参考之前
练习¶
为什么不定义const 版本的递增和递减运算符?
因为递增和递减会改变对象本身,所以定义 const 版本的毫无意义。
成员访问运算符¶
常常用到解引用运算符(*)和箭头运算符(->)
箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
class StrBlobPtr {
public:
std::string& operator*() const{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
std::string* operator->() const{
// delegate the real work to the dereference operator
return & this->operator*();
}
// other members as before
};
//解引用运算符首先检查curr是否仍在作用范围内,是则返回curr所指元素的一个引用。
//箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果元素的地址。
//值得注意的是,我们将这两个运算符定义成了const成员,
//这是因为与递增和递减运算符不一样,获取一个元素并不会改变StrBlobPtr对象的状态。
//同时,它们的返回值分别是非常量string的引用或指针,
//因为一个StrBlobPtr只能绑定到非常量的StrBlob对象
成员访问运算符¶
这两个运算符的用法与指针或者vector迭代器的对应操作完全一致:
StrBlob a1 = {"hi", "bye", "now"};
StrBlobPtr p(a1); // p points to the vector inside a1
*p = "okay"; // assigns to the first element in a1
cout << p->size() << endl; // prints 4, the size of the first element in a1
cout << (*p).size() << endl; // equivalent to p->size()
对箭头运算符返回值的限定¶
我们能令operator*完成任何我们指定的操作
箭头运算符则不是这样,它永远不能丢掉成员访问这个最基本的含义。
- 对于形如point->mem的表达式来说,
point必须是指向类对象的指针或者是一个重载了operator->的类的对象。
根据point类型的不同,point->mem分别等价于
(*point).mem; // point is a built-in pointer type
point.operator()->mem; // point is an object of class type
除此之外,代码都将发生错误。
point->mem的执行过程如下所示:
1.如果point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem。
首先解引用该指针,然后从所得的对象中获取指定的成员。
如果point所指的类型没有名为mem的成员,程序会发生错误。
2.如果point是定义了operator->的类的一个对象,
则我们使用point.operator->()的结果来获取mem。
其中,如果该结果是一个指针,则执行第1步;
如果该结果本身含有重载的operator->(),则重复调用当前步骤。
最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。
重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
练习¶
为你的 StrBlobPtr 类和在12.1.6节练习12.22中定义的 ConstStrBlobPtr 的类
分别添加解引用运算符和箭头运算符。
注意:因为 ConstStrBlobPtr 的数据成员指向const vector,
所以ConstStrBlobPtr 中的运算符必须返回常量引用。
class StrBlobPtr{
public:
std::string& operator*() const{
auto p=check(cuur,"dereference past end");
return (*p)[curr];
}
std::string* operator->() const{
return &(this->operator*());
}
//其它成员
};
练习¶
class ConstStrBlobPtr{
public:
const std::string& operator*() const{
auto p = check(curr,"dereference past end");
return (*p)[curr];
}
const std::string* operator->() const{
return &(this->operator*());
}
//其它成员
}
练习¶
我们的 StrBlobPtr 类没有定义拷贝构造函数、赋值运算符以及析构函数,为什么?
因为使用合成的足够了。
StrBlobPtr 的数据成员是智能指针和size_t类型,智能指针定义了自己的拷贝构造函数,
赋值运算符和析构函数,size_t 用默认的即可。
练习¶
定义一个类令其含有指向 StrBlobPtr 对象的指针,为这个类定义重载的箭头运算符。
头文件:
class StrBlobPtr;
class StrBlobPtr_pointer
{
public:
StrBlobPtr_pointer() = default;
StrBlobPtr_pointer(StrBlobPtr* p) : pointer(p) { }
StrBlobPtr& operator *() const;
StrBlobPtr* operator->() const;
private:
StrBlobPtr* pointer = nullptr;
};
练习¶
实现:
#include "ex_14_32.h"
#include "ex_14_30_StrBlob.h"
#include <iostream>
StrBlobPtr&
StrBlobPtr_pointer::operator *() const
{
return *pointer;
}
StrBlobPtr*
StrBlobPtr_pointer::operator ->() const
{
return pointer;
}
函数调用运算符¶
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。
因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
//这个类只定义了一种操作:函数调用运算符,
//它负责接受一个int类型的实参,然后返回该实参的绝对值。
//我们使用调用运算符的方式是令一个absInt对象作用于一个实参列表,
//这一过程看起来非常像调用函数的过程:
int i = -42;
absInt absObj; // object that has a function-call operator
int ui = absObj(i); // passes i to absObj.operator()
//即使absObj只是一个对象而非函数,我们也能“调用”该对象。
//调用对象实际上是在运行重载的调用运算符。
函数调用运算符必须是成员函数。
一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类的对象称作函数对象(function object)。
因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。
含有状态的函数对象类¶
函数对象类除了operator()之外也可以包含其他成员。
函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' '):
os(o), sep(c) { }
void operator()(const string &s) const { os << s << sep;
}
private:
ostream &os; // stream on which to write
char sep; // character to print after each output
};
//当定义PrintString的对象时,分隔符及输出流既可以使用默认值也可以提供我们自己的值:
PrintString printer; // uses the defaults; prints to cout
printer(s); // prints s followed by a space on cout
PrintString errors(cerr, '\n');
errors(s); // prints s followed by a newline on cerr
含有状态的函数对象类¶
函数对象常常作为泛型算法的实参。
- 例如,可以使用标准库for_each算法和我们自己的PrintString类来打印容器的内容:
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
for_each的第三个实参是类型PrintString的一个临时对象,
//其中我们用cerr和换行符初始化了该对象。
//当程序调用for_each时,将会把vs中的每个元素依次打印到cerr中,
//元素之间以换行符分隔。
练习¶
一个重载的函数调用运算符应该接受几个运算对象?
一个重载的函数调用运算符接受的运算对象应该和该运算符拥有的操作数一样多。
练习¶
定义一个函数对象类,令其执行if-then-else 的操作:
该类的调用运算符接受三个形参,它首先检查第一个形参,
如果成功返回第二个形参值;如果不成功返回第三个形参的值。
struct Test
{
int operator()(bool b, int iA, int iB)
{
return b ? iA : iB;
}
};
练习¶
编写一个类似于 PrintString 的类,令其从 istream 中读取一行输入,
然后返回一个表示我们所读内容的string。如果读取失败,返回空string。
#include <iostream>
#include <string>
class GetInput{
public:
GetInput(std::istream &i = std::cin) : is(i) {}
std::string operator()() const {
std::string str;
std::getline(is, str);
return is ? str : std::string();
}
private:
std::istream &is;
};
int main(){
GetInput getInput;
std::cout << getInput() << std::endl;
return 0;
}
练习¶
使用前一个练习定义的类读取标准输入,将每一行保存为 vector 的一个元素。
#include <iostream>
#include <string>
#include <vector>
class GetInput{
public:
GetInput(std::istream &i = std::cin) : is(i) {}
std::string operator()() const {
std::string str;
std::getline(is, str);
return is ? str : std::string(); }
private:
std::istream &is;};
int main(){
GetInput getInput;
std::vector<std::string> vec;
for (std::string tmp;!(tmp = getInput()).empty();) vec.push_back(tmp);
for (const auto &str : vec) std::cout << str << " ";
std::cout << std::endl;
}
练习¶
编写一个类令其检查两个值是否相等。使用该对象及标准库算法编写程序,
令其替换某个序列中具有给定值的所有实例。
#include <iostream>
#include <algorithm>
#include <vector>
class IsEqual{
int value;
public:
IsEqual(int v) : value(v) {}
bool operator()(int elem) {
return elem == value;
}
};
int main(){
std::vector<int> vec = { 3, 2, 1, 4, 3, 7, 8, 6 };
std::replace_if(vec.begin(), vec.end(), IsEqual(3), 5);
for (int i : vec) std::cout << i << " ";
std::cout << std::endl;
}
lambda是函数对象¶
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象
在lambda表达式产生的类中含有一个重载的函数调用运算符
//例如,对于我们传递给stable_sort作为其最后一个实参的lambda表达式
// sort words by size, but maintain alphabetical order for words of the same size
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size();});
//其行为类似于下面这个类的一个未命名对象
class ShorterString {
public:
bool operator()(const string &s1,const string &s2)const{
return s1.size() < s2.size(); }
};
//产生的类只有一个函数调用运算符成员,它负责接受两个string并比较它们的长度,
//它的形参列表和函数体与lambda表达式完全一样。
//默认情况下lambda不能改变它捕获的变量。
//因此在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。
//如果lambda被声明为可变的,则调用运算符就不是const的了。
lambda是函数对象¶
//用这个类替代lambda表达式后,我们可以重写并重新调用stable_sort:
stable_sort(words.begin(), words.end(), ShorterString());
//第三个实参是新构建的ShorterString对象
//当stable_sort内部的代码每次比较两个string时就会“调用”这一对象,
//此时该对象将调用运算符的函数体,判断第一个string的大小小于第二个时返回true。
表示lambda及相应捕获行为的类¶
当一个lambda表达式通过引用捕获变量时,
- 将由程序负责确保lambda执行时引用所引的对象确实存在。
- 因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。
通过值捕获的变量被拷贝到lambda中。
- 这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,
- 同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
//lambda的作用是找到第一个长度不小于给定值的string对象:
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a) { return s.size() >= sz; });
该lambda表达式产生的类将形如:
class SizeComp {
SizeComp(size_t n): sz(n) { } // parameter for each captured variable
// call operator with the same return type, parameters, and body as the lambda
bool operator()(const string &s) const
{ return s.size() >= sz; }
private:
size_t sz; // a data member for each variable captured by value
};
表示lambda及相应捕获行为的类¶
上面这个类含有一个数据成员以及一个用于初始化该成员的构造函数。
这个合成的类不含有默认构造函数,因此要想使用这个类必须提供一个实参:
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;
它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定
练习¶
编写一个类令其检查某个给定的 string 对象的长度是否与一个阈值相等。
使用该对象编写程序,统计并报告在输入的文件中长度为1的单词有多少个,
长度为2的单词有多少个、......、长度为10的单词有多少个。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class StrLenIs{
public:
StrLenIs(int len1):len(len1){}
bool operator()(const string &str){return str.length()==len;}
private:
int len;
};
void readStr(istream &is,vector<string> &vec){
string word;
while(is>>word){
vec.push_back(word);
}
}
练习¶
int main(){
vector<string> vec;
readStr(cin,vec);
const int minLen=1;
const int maxLen=10;
for(int i=minLen;i<maxLen;i++){
StrLenIs slenIs(i);
cout<<"len:"<<i<<",cnt:"<<count_if(vec.begin(),vec.end(),
slenIs)<<endl;
}
return 0;
}
练习¶
修改上一题的程序令其报告长度在1到9之间的单词有多少个、长度在10以上的单词有多少个。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class StrLenBetween{
public:
StrLenBetween(int min,int max):minLen(min),maxLen(max){}
bool operator()(const string &str){
return str.length()>=minLen&&str.length()<=maxLen;}
private:
int minLen,maxLen;
};
class StrNotShorterThan{
public:
StrNotShorterThan(int len):minLen(len){}
bool operator()(const string & str){return str.length()>=minLen;}
private:
int minLen;
};
练习¶
void readStr(istream &is,vector<string> &vec){
string word;
while(is>>word){
vec.push_back(word);
}
}
int main(){
vector<string> vec;
readStr(cin,vec);
StrLenBetween sLenBetween(1,9);
StrNotShorterThan sNotShorterThan(10);
cout<<"len 1-9:"<<count_if(vec.begin(),vec.end(),sLenBetween)<<endl;
cout<<"len >=10:"<<count_if(vec.begin(),vec.end(),sNotShorterThan)<<endl;
return 0;
}
练习¶
重新编写10.3.2节的biggies 函数,使用函数对象替换其中的 lambda 表达式。
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
class ShorterString{
public:
bool operator()(string const& s1, string const& s2) const {
return s1.size() < s2.size(); }
};
class BiggerEqual
{
size_t sz_;
public:
BiggerEqual(size_t sz) : sz_(sz) {}
bool operator()(string const& s) { return s.size() >= sz_; }
};
练习¶
class Print
{
public:
void operator()(string const& s) { cout << s << " "; }
};
string make_plural(size_t ctr, string const& word, string const& ending)
{
return (ctr > 1) ? word + ending : word;
}
void elimDups(vector<string> &words)
{
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
练习¶
void biggies(vector<string> &words, vector<string>::size_type sz)
{
elimDups(words);
stable_sort(words.begin(), words.end(), ShorterString());
auto wc = find_if(words.begin(), words.end(), BiggerEqual(sz));
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
for_each(wc, words.end(), Print());
cout << endl;
}
int main()
{
vector<string> vec{ "fox", "jumps", "over", "quick", "red", "red",
"slow", "the", "turtle" };
biggies(vec, 4);
}
练习¶
你认为 C++ 11 标准为什么要增加 lambda?对于你自己来说,
什么情况下会使用 lambda,什么情况下会使用类?
lambda起到匿名函数的作用。可看做函数对象在使用方式上的简化。
若这个函数需要多次使用,并且需要保存某些状态,使用函数对象。
标准库定义的函数对象¶
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,
每个类分别定义了一个执行命名操作的调用运算符。
- 例如,plus类定义了一个函数调用运算符用于对一对运算对象执行+的操作;
- modulus类定义了一个调用运算符执行二元的%操作;
- equal_to类执行==,等等。
这些类都被定义成模板的形式,我们可以为其指定具体的调用运算符的形参类型。
- 例如,plus<string>令string加法运算符作用于string对象;
- plus<int>的运算对象是int;
- plus<Sales_data>对Sales_data对象执行加法运算,以此类推:
plus<int> intAdd; // function object that can add two int values
negate<int> intNegate; // function object that can negate an int value
int sum = intAdd(10, 20); // equivalent to sum = 30
sum = intNegate(intAdd(10, 20)); // equivalent to sum = 30
sum = intAdd(10, intNegate(10)); // sum = 0
标准库函数对象¶
算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> not_equal_to<Type> logical_or<Type>
multiplies<Type> greater<Type> logical_not<Type>
divides<Type> greater_equal<Type>
modulus<Type> less<Type>
negate<Type> less_equal<Type>
所列的类型定义在functional头文件中
在算法中使用标准库函数对象¶
表示运算符的函数对象类常用来替换算法中的默认运算符。
//在默认情况下排序算法使用operator<将序列按照升序排列。
//如果要执行降序排列的话,我们可以传入一个greater类型的对象。
//该类将产生一个调用运算符并负责执行待排序类型的大于运算。
//例如,如果svec是一个vector<string>,
sort(svec.begin(), svec.end(), greater<string>());
//标准库规定其函数对象对于指针同样适用。
vector<string *> nameTable; // vector of pointers
// error: the pointers in nameTable are unrelated, so < is undefined
sort(nameTable.begin(), nameTable.end(),
[](string *a, string *b) { return a < b; });
// ok: library guarantees that less on pointer types is well defined
sort(nameTable.begin(), nameTable.end(), less<string*>());
关联容器使用less<key_type>对元素排序,
- 我们可以定义一个指针的set或者在map中使用指针作为关键值而无须直接声明less。
练习¶
使用标准库函数对象及适配器定义一条表达式,令其
(a) 统计大于1024的值有多少个。
(b) 找到第一个不等于pooh的字符串。
(c) 将所有的值乘以2。
std::count_if(ivec.cbegin(), ivec.cend(),
std::bind(std::greater<int>(), _1, 1024));
std::find_if(svec.cbegin(), svec.cend(),
std::bind(std::not_equal_to<std::string>(), _1, "pooh"));
std::transform(ivec.begin(), ivec.end(), ivec.begin(),
std::bind(std::multiplies<int>(), _1, 2));
练习¶
使用标准库函数对象判断一个给定的int值是否能被 int 容器中的所有元素整除。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
bool dividedByAll(vector<int> & ivec, int dividend){
return count_if(ivec.begin(),ivec.end(),
bind1st(modulus<int>(),dividend))==0;
}
int main(){
vector<int> v={1,7,8};
cout<<dividedByAll(v,56);
return 0;
}
可调用对象与function¶
C++语言中有几种可调用的对象:
- 函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类
- 可调用的对象也有类型。 例如,
- 每个lambda有它自己唯一的(未命名)类类型;
- 函数及函数指针的类型则由其返回值类型和实参类型决定,等等。
然而,两个不同类型的可调用对象却可能共享同一种调用形式(call signature)。
- 调用形式指明了调用返回的类型以及传递给调用的实参类型。
一种调用形式对应一个函数类型,
例如int(int, int)是一个函数类型,它接受两个int、返回一个int。
不同类型可能具有相同的调用形式¶
对于几个可调用对象共享同一种调用形式的情况,
有时我们会希望把它们看成具有相同的类型。
- 例如,考虑下列不同类型的可调用对象:
// ordinary function
int add(int i, int j) { return i + j; }
// lambda, which generates an unnamed function-object class
auto mod = [](int i, int j) { return i % j; };
// function-object class
struct div {
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
};
不同类型可能具有相同的调用形式¶
尽管它们的类型各不相同,但是共享同一种调用形式:int(int, int)
可能希望使用这些可调用对象构建一个简单的桌面计算器
- 为了实现这一目的,需要定义一个函数表(functiontable)用于存储指向这些可调
用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。
在C++语言中,函数表很容易通过map来实现
- 使用一个表示运算符符号的string对象作为关键字;
- 使用实现运算符的函数作为值。
- 当我们需要求给定运算符的值时,先通过运算符索引map,然后调用找到的那个元素。
不同类型可能具有相同的调用形式¶
假定我们的所有函数都相互独立,并且只处理关于int的二元运算,
则map可以定义成如下的形式:
// maps an operator to a pointer to a function taking two ints and returning an int
map<string, int(*)(int,int)> binops;
我们可以按照下面的形式将add的指针添加到binops中:
// ok: add is a pointer to function of the appropriate type
binops.insert({"+", add}); // {"+", add} is a pair
但是我们不能将mod或者divide存入binops:
binops.insert({"%", mod}); // error: mod is not a pointer to function
问题在于mod是个lambda表达式,而每个lambda有它自己的类类型,
该类型与存储在binops中的值的类型不匹配。
标准库function类型¶
我们可以使用一个名为function的新的标准库类型解决上述问题,
function定义在functional头文件中
function<T> f; f是一个用来存储可调用对象的空function,
这些可调用对象的调用形式应该与类型T相同。
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj) 在f中存储可调用对象obj的副本
f 将f作为条件:当f含有一个可调用对象时为真;否则为假。
f(args) 调用f中的对象,参数是args
定义为function<T>的成员的类型
result_type 该function类型的可调用对象返回的类型
argument_type 当T有一个或两个实参时定义的类型。
如果T只有一个实参则argument_type是该类型的同义词
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型
function是一个模板,和我们使用过的其他模板一样,
当创建一个具体的function类型时我们必须提供额外的信息。
我们在一对尖括号内指定类型:function<int(int, int)>
标准库function类型¶
//在这里我们声明了一个function类型,它可以表示接受两个int、
//返回一个int的可调用对象。因此,我们可以用这个新声明的类型表示
//任意一种桌面计算器用到的类型;
function<int(int, int)> f1 = add; // function pointer
function<int(int, int)> f2 = div(); // object of a function-object class
function<int(int, int)> f3 = [](int i, int j) // lambda
{ return i * j; };
cout << f1(4,2) << endl; // prints 6
cout << f2(4,2) << endl; // prints 2
cout << f3(4,2) << endl; // prints 8
使用这个function类型我们可以重新定义map:
map<string, function<int(int, int)>> binops;
能把所有可调用对象,包括函数指针、lambda或者函数对象,都添加到这个map中:
map<string, function<int(int, int)>> binops = {
{"+", add}, // function pointer
{"-", std::minus<int>()}, // library function object
{"/", div()}, // user-defined function object
{"*", [](int i, int j) { return i * j; }}, // unnamed lambda
{"%", mod} }; // named lambda object
标准库function类型¶
//当我们索引map时将得到关联值的一个引用。
//如果我们索引binops,将得到function对象的引用。
//function类型重载了调用运算符,该运算符接受它自己的实参
//然后将其传递给存好的可调用对象:
binops["+"](10,5);//calls add(10, 5)
binops["-"](10,5);//uses the call operator of the minus<int> object
binops["/"](10,5);//uses the call operator of the div object
binops["*"](10,5);//calls the lambda function object
binops["%"](10,5);//calls the lambda function object
重载的函数与function¶
我们不能(直接)将重载函数的名字存入function类型的对象中:
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert( {"+", add} ); // error: which add?
解决上述二义性问题的一条途径是存储函数指针而非函数的名字:
int (*fp)(int,int) = add; // pointer to the version of add that takes two ints
binops.insert( {"+", fp} ); // ok: fp points to the right version of add
同样,我们也能使用lambda来消除二义性:
// ok: use a lambda to disambiguate which version of add we want to use
binops.insert( {"+", [](int a, int b) {return add(a, b);} });
新版本标准库中的function类与旧版本中的unary_function和binary_function没有关联,
后两个类已经被更通用的bind函数替代了
练习¶
编写一个简单的桌面计算器使其能处理二元运算
#include <iostream>
#include <string>
#include <map>
#include <functional>
int add(int i, int j) { return i + j; }
auto mod = [](int i, int j) { return i % j; };
struct Div { int operator ()(int i, int j) const { return i / j; } };
auto binops = std::map<std::string, std::function<int(int, int)>>
{
{ "+", add }, // function pointer
{ "-", std::minus<int>() }, // library functor
{ "/", Div() }, // user-defined functor
{ "*", [](int i, int j) { return i*j; } }, // unnamed lambda
{ "%", mod } // named lambda object
};
练习¶
int main()
{
while (std::cout << "Pls enter as: num operator num :\n", true)
{
int lhs, rhs; std::string op;
std::cin >> lhs >> op >> rhs;
std::cout << binops[op](lhs, rhs) << std::endl;
}
return 0;
}
重载、类型转换与运算符¶
一个实参调用的非显式构造函数定义了一种隐式的类型转换,
这种构造函数将实参类型的对象转换成类类型。
我们同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到这一点。
转换构造函数和类型转换运算符共同定义了类类型转换(class-typeconversions),
这样的转换有时也被称作用户定义的类型转换(user-defined conversions)。
类型转换运算符¶
类型转换运算符(conversion operator)是类的一种特殊成员函数,
- 它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:
operator type() const;
- 其中type表示某种类型。
类型转换运算符可以面向任意类型(除了void之外)进行定义,
只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,
但允许转换成指针(包括数组指针及函数指针)或者引用类型。
类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。
类型转换运算符通常不应该改变待转换对象的内容,
因此,类型转换运算符一般被定义成const成员。
一个类型转换函数必须是类的成员函数;
它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
定义含有类型转换运算符的类¶
//定义一个比较简单的类,令其表示0到255之间的一个整数:
class SmallInt {
public:
SmallInt(int i = 0): val(i)
{
if (i < 0 || i > 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const { return val; }
private:
std::size_t val;
};
//SmallInt类既定义了向类类型的转换,也定义了从类类型向其他类型的转换。
//其中,构造函数将算术类型的值转换成SmallInt对象,
//而类型转换运算符将SmallInt对象转换成int:
SmallInt si;
si = 4; // implicitly converts 4 to SmallInt then calls SmallInt::operator=
si + 3; // implicitly converts si to int followed by integer addition
定义含有类型转换运算符的类¶
尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换
可以置于一个标准(内置)类型转换之前或之后并与其一起使用。
//因此,我们可以将任何算术类型传递给SmallInt的构造函数。
//类似的,我们也能使用类型转换运算符将一个SmallInt对象转换成int,
//然后再将所得的int转换成任何其他算术类型:
// the double argument is converted to int using the built-in conversion
SmallInt si = 3.14; // calls the SmallInt(int) constructor
// the SmallInt conversion operator converts si to int;
si + 3.14; // that int is converted to double using the built-in conversion
定义含有类型转换运算符的类¶
因为类型转换运算符是隐式执行的,所以无法给这些函数传递实参,
当然也就不能在类型转换运算符的定义中使用任何形参。
同时,尽管类型转换函数不负责指定返回类型,
但实际上每个类型转换函数都会返回一个对应类型的值:
class SmallInt;
operator int(SmallInt&); // error: nonmember
class SmallInt {
public:
int operator int() const; // error: return type
operator int(int = 0) const; // error: parameter list
operator int*() const { return 42; } // error: 42 is not a
pointer
};
提示:避免过度使用类型转换函数¶
明智地使用类型转换运算符也能极大地简化类设计者的工作,
- 同时使得使用类更加容易。
然而,如果在类类型和转换类型之间不存在明显的映射关系,
- 则这样的类型转换可能具有误导性。
例如,假设某个类表示Date,我们也许会为它添加一个从Date到int的转换。
然而,类型转换函数的返回值应该是什么?
- 一种可能的解释是,函数返回一个十进制数,依次表示年、月、日,
例如,July 30,1989可能转换为int值19890730。
- 另外一种合理的解释,即类型转换运算符返回的int表示的是从某个时间
节点(比如January 1,1970)开始经过的天数。
- 显然这两种理解都合情合理
问题在于Date类型的对象和int类型的值之间不存在明确的一对一映射关系。
- 因此在此例中,不定义该类型转换运算符也许会更好。
- 类可以定义一个或多个普通的成员函数以从各种不同形式中提取所需的信息。
类型转换运算符可能产生意外结果¶
在实践中,类很少提供类型转换运算符
对于类来说,定义向bool的类型转换还是比较普遍的现象。
类类型的对象转换成bool后就能被用在任何需要算术类型的上下文中。
在C++标准的早期版本中,这样的类型转换可能引发意想不到的结果:
int i = 42;
cin << i; // this code would be legal if the conversion to bool were not explicit!
//该代码能使用istream的bool类型转换运算符将cin转换成bool
//提升后的bool值(1或0)最终会被左移42个位置。
显式的类型转换运算符¶
为了防止这样的异常情况发生,
C++11新标准引入了显式的类型转换运算符(explicit conversion operator):
class SmallInt {
public:
// the compiler won't automatically apply this conversion
explicit operator int() const { return val; }
// other members as before
};
//和显式的构造函数一样,
//编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换:
SmallInt si = 3; // ok: the SmallInt constructor is not explicit
si + 3; // error: implicit is conversion required, but operator int is explicit
static_cast<int>(si) + 3; // ok: explicitly request the conversion
//当类型转换运算符是显式的时,我们也能执行类型转换,
//不过必须通过显式的强制类型转换才可以。
显式的类型转换运算符¶
该规定存在一个例外,即如果表达式被用作条件,则编译器会将显式的类型转换
自动应用于它。换句话说,
当表达式出现在下列位置时,显式的类型转换将被隐式地执行:·
- if、while及do语句的条件部分·
- for语句头的条件表达式·
- 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象·
- 条件运算符(? :)的条件表达式。
转换为bool¶
在标准库的早期版本中,IO类型定义了向void*的转换规则,以求避免上面提到的问题。
在C++11新标准下,IO标准库通过定义一个向bool的显式类型转换实现同样的目的。
无论我们什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。例如:
while (std::cin >> value)
//while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。
//为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换。
//如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假
向bool的类型转换通常用在条件部分,因此operatorbool一般定义成explicit的。
练习¶
编写类型转换运算符将一个 Sales_data 对象分别转换成 string 和 double,
你认为这些运算符的返回值应该是什么?
头文件:
#include <string>
#include <iostream>
class Sales_data
{
friend std::istream& operator>>(std::istream&, Sales_data&);
friend std::ostream& operator<<(std::ostream&, const Sales_data&);
friend Sales_data operator+(const Sales_data&, const Sales_data&);
public:
Sales_data(const std::string &s, unsigned n, double p)
:bookNo(s), units_sold(n), revenue(n*p) {}
Sales_data() : Sales_data("", 0, 0.0f) {}
Sales_data(const std::string &s) : Sales_data(s, 0, 0.0f) {}
Sales_data(std::istream &is);
练习¶
Sales_data& operator=(const std::string&);
Sales_data& operator+=(const Sales_data&);
explicit operator std::string() const { return bookNo; }
explicit operator double() const { return avg_price(); }
std::string isbn() const { return bookNo; }
private:
inline double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
std::istream& operator>>(std::istream&, Sales_data&);
std::ostream& operator<<(std::ostream&, const Sales_data&);
Sales_data operator+(const Sales_data&, const Sales_data&);
inline double Sales_data::avg_price() const
{
return units_sold ? revenue / units_sold : 0;
}
练习¶
你认为应该为 Sales_data 类定义上面两种类型转换运算符吗?
应该把它们声明成 explicit 的吗?为什么?
上面的两种类型转换有歧义,应该声明成 explicit 的。
练习¶
说明下面这两个类型转换运算符的区别。
struct Integral {
operator const int();
operator int() const;
}
前者将对象转成const int,在接受const int值的地方才能使用
后者将对象转换成int,更通用一些
练习¶
你在7.5.1节的练习7.40中曾经选择并编写了一个类,
你认为它应该含有向 bool 的类型转换运算符吗?
如果是,解释原因并说明该运算符是否应该是 explicit的;如果不是,也请解释原因。
Date 类应该含有向 bool 的类型转换运算符,并且应该声明为 explicit 的。
练习¶
为上一题提到的类定义一个转换目标是 bool 的类型转换运算符,
先不用在意这么做是否应该。
class Date{
public:
explicit operator bool() {
vector<vector<int>> days_per_month={
{31,28,31,30,31,30,31,31,30,31,30,31},
{31,29,31,30,31,30,31,31,30,31,30,31}
};
return 1<=month && month<=12 && 1<=day &&
day<=days_per_month[isLeapYear()?1:0][month-1];
}
bool isLeapYear(){
return (year%4==0&&year%100!=0)||(year%400==0);
}
//其它成员
}
避免有二义性的类型转换¶
如果类中包含一个或多个类型转换,
则必须确保在类类型和目标类型之间只存在唯一一种转换方式。
否则的话,我们编写的代码将很可能会具有二义性。
在两种情况下可能产生多重转换路径。
- 第一种情况是两个类提供相同的类型转换:
例如,当A类定义了一个接受B类对象的转换构造函数,
同时B类定义了一个转换目标是A类的类型转换运算符时,
我们就说它们提供了相同的类型转换。
- 第二种情况是类定义了多个转换规则,
而这些转换涉及的类型本身可以通过其他类型转换联系在一起。
最典型的例子是算术运算符,对某个给定的类来说,
最好只定义最多一个与算术类型有关的转换规则。
通常情况下,不要为类定义相同的类型转换,
也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。
实参匹配和相同的类型转换¶
在下面的例子中,我们定义了两种将B转换成A的方法:
- 一种使用B的类型转换运算符
- 另一种使用A的以B为参数的构造函数:
// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
A() = default;
A(const B&); // converts a B to an A
// other members
};
struct B {
operator A() const; // also converts a B to an A
// other members
};
A f(const A&);
B b;
A a = f(b); // error ambiguous: f(B::operator A())
// or f(A::A(const B&))
因为同时存在两种由B获得A的方法,对f的调用存在二义性。该调用将产生错误。
实参匹配和相同的类型转换¶
如果我们确实想执行上述的调用,就不得不显式地调用类型转换运算符或者转换构造函数:
A a1 = f(b.operator A()); // ok: use B's conversion operator
A a2 = f(A(b)); // ok: use A's constructor
我们无法使用强制类型转换来解决二义性问题,
因为强制类型转换本身也面临二义性。
二义性与转换目标为内置类型的多重类型转换¶
另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以
通过其他类型转换联系在一起,则同样会产生二义性的问题。
- 类当中定义了多个参数都是算术类型的构造函数,
- 或者转换目标都是算术类型的类型转换运算符。
struct A {
A(int = 0); // usually a bad idea to have two
A(double); // conversions from arithmetic types
operator int() const; // usually a bad idea to have two
operator double() const; // conversions to arithmetic types
// other members
};
void f2(long double);
A a;
f2(a); // error ambiguous: f(A::operator int())
// or f(A::operator double())
long lg;
A a2(lg); // error ambiguous: A::A(int) or A::A(double)
//调用将产生二义性。
¶
之所以会产生二义性,根本原因是它们所需的标准类型转换级别一致
当我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,
- 则标准类型转换的级别将决定编译器选择最佳匹配的过程:
short s = 42;
// promoting short to int is better than converting short to double
A a3(s); // uses A::A(int)
//在此例中,把short提升成int的操作要优于把short转换成double的操作,
//因此编译器将使用A::A(int)构造函数构造a3,其中实参是s(提升后)的值。
当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,
则标准类型转换将决定最佳匹配到底是哪个。
提示:类型转换与运算符¶
设计类的重载运算符、转换构造函数及类型转换函数,必须加倍小心。
尤其是当类同时定义了类型转换运算符及重载运算符时特别容易产生二义性。
- 不要令两个类执行相同的类型转换:
- 如果Foo类有一个接受Bar类对象的构造函数,
则不要在Bar类中再定义转换目标是Foo类的类型转换运算符。·
- 避免转换目标是内置算术类型的类型转换。
特别是当你已经定义了一个转换成算术类型的类型转换时,接下来
- 不要再定义接受算术类型的重载运算符。
如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,
然后使用内置的运算符。
- 不要定义转换到多种算术类型的类型转换。
让标准类型转换完成向其他算术类型转换的工作。
除了显式地向bool类型的转换之外,
应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显式构造函数。
重载函数与转换构造函数¶
当我们调用重载的函数时,从多个类型转换中进行选择将变得更加复杂。
如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。
- 举个例子,当几个重载函数的参数分属不同的类类型时,
如果这些类恰好定义了同样的转换构造函数,则二义性问题将进一步提升:
struct C {
C(int);
};
struct D {
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10); // error ambiguous: manip(C(10)) or manip(D(10))
//调用将具有二义性,调用者可以显式地构造正确的类型从而消除二义性:
manip(C(10)); // ok: calls manip(const C&)
如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,
则这通常意味着程序的设计存在不足。在调用重载函数时,如果需要额外的标准类型转换
则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。
如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
重载函数与用户定义的类型转换¶
当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,
则我们认为这些类型转换一样好。在这个过程中,我们不会考虑任何可能出现的标准
类型转换的级别。只有当重载函数能通过同一个类型转换函数得到匹配时,
我们才会考虑其中出现的标准类型转换。
- 例如当我们调用manip时,即使其中一个类定义了需要对实参进行标准类型转换的
构造函数,这次调用仍然会具有二义性:
struct E {
E(double);
// other members
};
void manip2(const C&);
void manip2(const E&);
// error ambiguous: two different user-defined conversions could be used
manip2(10); // manip2(C(10) or manip2(E(double(10)))
因为调用重载函数所请求的用户定义的类型转换不止一个且彼此不同,
所以该调用具有二义性。
即使其中一个调用需要额外的标准类型转换而另一个调用能精确匹配,
编译器也会将该调用标示为错误。
练习¶
在初始化 ex1 和 ex2 的过程中,可能用到哪些类类型的转换序列呢?
说明初始化是否正确并解释原因。
struct LongDouble {
LongDouble(double = 0.0);
operator double();
operator float();
};
LongDouble ldObj;
int ex1 = ldObj;
float ex2 = ldObj;
ex1 转换不合法,没有定义从 LongDouble 到 int 的转换,
从double转换还是float转换存在二义性。
ex2 合法。
练习¶
在调用 calc 的过程中,可能用到哪些类型转换序列呢?
说明最佳可行函数是如何被选出来的。
void calc(int);
void calc(LongDouble);
double dval;
calc(dval); // 调用了哪个calc?
最佳可行函数是 void calc(int)。
转换的优先级如下:
1. 精确匹配
2. const 转换。
3. 类型提升
4. 算术转换
5. 类类型转换
函数匹配与重载运算符¶
如果a是一种类类型,则表达式a sym b可能是
a.operatorsym (b); // a has operatorsym as a member function
operatorsym(a, b); // operatorsym is an ordinary function
和普通函数调用不同,不能通过调用的形式来区分当前调用的是成员函数还是非成员函数
当我们使用重载运算符作用于类类型的运算对象时,
- 候选函数中包含该运算符的普通非成员版本和内置版本。
- 如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内
当我们调用一个命名的函数时,
- 具有该名字的成员函数和非成员函数不会彼此重载,
这是因为我们用来调用命名函数的语法形式对于成员函数和非成员函数来说是不相同的
当我们通过类类型的对象(或者该对象的指针及引用)进行函数调用时,
- 只考虑该类的成员函数。
当我们在表达式中使用重载的运算符时,
- 无法判断正在使用的是成员函数还是非成员函数,
因此二者都应该在考虑的范围内。
表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数。
函数匹配与重载运算符¶
//举个例子,我们为SmallInt类定义一个加法运算符:
class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); // conversion from int
operator int() const { return val; } // conversion to int
private:
std::size_t val;
};
可以使用这个类将两个SmallInt对象相加,
但如果我们试图执行混合模式的算术运算,就将遇到二义性的问题:
SmallInt s1, s2;
SmallInt s3 = s1 + s2; // uses overloaded operator+
int i = s3 + 0; // error: ambiguous
//第二条加法语句具有二义性:我们可以把0转换成SmallInt,然后使用SmallInt的+
//或者把s3转换成int,然后对于两个int执行内置的加法运算。
如果我们对同一个类既提供了转换目标是算术类型的类型转换,
也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
练习¶
在下面的加法表达式中分别选用了哪个operator+?
列出候选函数、可行函数及为每个可行函数的实参执行的类型转换:
struct Longdouble {
//用于演示的成员operator+;在通常情况下是个非成员
LongDouble operator+(const SmallInt&);
//其他成员与14.9.2节一致
};
LongDouble operator+(LongDouble&, double);
SmallInt si;
LongDouble ld;
ld = si + ld;
ld = ld + si;
ld = si + ld; 不合法。
SmallInt->int
LongDouble->float,double
所以operator+(int,float) operator(int,double)都可行,有二义性
ld = ld + si ; 合法
LongDouble 成员operator+优先匹配,而其它路径要类型转换。
练习¶
假设我们已经定义了如第522页所示的SmallInt,判断下面的加法表达式是否合法。如果合法,使用了哪个加法运算符?如果不合法,应该怎样修改代码才能使其合法?
SmallInt si;
double d = si + 3.14;
不合法,存在二义性。
operator+(int,double)可行
3.14->int->SmallInt 因此 SmallInt::operator+可行
改为:
SmallInt s1;
double d = s1 + SmallInt(3.14);
小结¶
一个重载的运算符必须是某个类的成员或者至少拥有一个类类型的运算对象。
重载运算符的运算对象数量、结合律、优先级与对应的用于内置类型的运算符完全一致。
当运算符被定义为类的成员时,类对象的隐式this指针绑定到第一个运算对象。
赋值、下标、函数调用和箭头运算符必须作为类的成员。
如果类重载了函数调用运算符operator(),则该类的对象被称作“函数对象”。
这样的对象常用在标准函数中。lambda表达式是一种简便的定义函数对象类的方式。
在类中可以定义转换源或转换目的是该类型本身的类型转换,这样的类型转换将自动执行。
只接受单独一个实参的非显式构造函数定义了从实参类型到类类型的类型转换;
而非显式的类型转换运算符则定义了从类类型到其他类型的转换。
实践课¶
- 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课实验课七 操作符重载 https://developer.aliyun.com/adc/scenario/54f9fd06fd7c4bf7ac2d44ca4a6970e4
- 使用g++编译代码
- 编辑一个 readme.md 文档,键入本次实验心得.
- 使用git进行版本控制 可使用之前的gitee代码仓库
- 云服务器(elastic compute service,简称ecs) - aliyun linux 2是阿里云推出的 linux 发行版 - vim是从vi发展出来的一个文本编辑器. - g++ 是c++编译器
习题1
使用操作符重载,给出完整的复数定义。
包括加法+ 输入>> 输出<< 以及++ += ==
class Complex{
double real,imag;
//其它
}
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN: mkdir -p weekN ; mv 文件名 weekN;)
vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN
git push 到你的git仓库
git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md
提交¶
- 截图或复制文字,提交到群作业.
- 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
- 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo
关于使用tmux¶
sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a,
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0
vim 共分为三种模式¶
- 命令模式
- 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
- i 切换到输入模式,以输入字符.
- x 删除当前光标所在处的字符.
- : 切换到底线命令模式,可输入命令.
- 输入模式
- 命令模式下按下i就进入了输入模式.
- ESC,退出输入模式,切换到命令模式
- 底线命令模式
- 命令模式下按下:(英文冒号)就进入了底线命令模式.
- wq 保存退出
vim 常用按键说明¶
除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下:
- 光标移动
- j下 k上 h左 l右
- w前进一个词 b后退一个词
- Ctrl+d 向下半屏 ctrl+u 向上半屏
- G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
- dd 删一行 ndd 删n行
- yy 复制一行 nyy复制n行
- p将复制的数据粘贴在下一行 P粘贴到上一行
- u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
- /word 向下找word ?word 向上找
- n重复搜索 N反向搜索
- :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
取代为 word2
vim 常用按键说明¶
底线命令模式下:
- :set nu 显示行号
- :set nonu 取消行号
- :set paste 粘贴代码不乱序
【注:把caps lock按键映射为ctrl,能提高编辑效率.】
Markdown 文档语法¶
# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
- 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)
|表格标题1|表格标题2|
|-|-|
|内容1|内容2|