本章内容一览:

1、基本概念 和 限制条件

  • 只有重载的函数调用运算符operator()能有默认实参,其他重载运算符不能有默认实参。
  • 一个重载的运算符,至少含有一个类类型的参数。
  • 可被重载的运算符:
  • 一般不重载&& || , &这几个运算符
  • 返回值类型一般与内置版本兼容:
    • 逻辑和关系运算符返回bool
    • 算术运算符返回类类型的值
    • 赋值运算符 和 复合运算符 返回左侧运算对象的引用
  • 若一个运算符是成员函数,则他的左侧运算对象绑定到隐式的this指针上。这就是为什么后面重载<< >>运算符不能是成员函数的原因。

2、定义为 成员 还是 非成员?


什么是对称性呢?举个例子:
计算一个intdouble的和,他们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法就是对称的。如果想计算 含有类对象混合的表达式,则运算符必须定义成非成员函数

当把运算符定义为成员函数时,他的左侧运算对象必须是运算符所属类的对象。

练习题:



说明g:==具有对称性,所以定义为非成员

3、重载输入运算符

3.1 注意处理输入失败

X& operator>>(istream& is, X& x)
{is >> 输入巴拉巴拉.....if (!is)        这个检查是一次性检查,没有逐个检查每个读取操作x = X(); 输入失败,赋予对象默认状态else        某些成员需要由刚刚出入的数据计算出来,则在确保输入正常的情况下进行return is;
}

发生输入错误可能是如下原因:

  • 输入的数据类型与代码要求的不符
  • 读取到达文件末尾,或遇到其他错误

3.2 标示错误

4、算术运算符 和 复合赋值运算符


因为使用复合赋值运算符来实现算数运算符是很方便的。如下所示:

Sale_data operator+(const Sale_data& s1, const Sale_data& s2)
{Sale_data sum = s1;sum += s2;       直接使用复合赋值运算符就行了,而不用逐成员相加return sum;
}

5、相等运算符

  • 定义了==也应该定义!=
  • != ==其中一个定义好之后,直接用他去实现另一个
bool operator==(const Sale_data& s1, const Sale_data& s2)
{return s1.isbn() == s2.isbn() &&s1.units_sold == s2.units_sold &&s1.reevnue == s2.revenue;
}bool operator!=(const Sale_data& s1, const Sale_data& s2)
{return !(s1 == s2);      就像这样,不要傻不拉几再逐个比一遍了
}

6、关系运算符


也就是说要有严密的逻辑自洽。当两个对象经!=运算返回true,那么必有一个是<另一个的。

7、赋值运算符

之前学的拷贝赋值运算符只是赋值运算符重载中的一种。我们还可以用很多其他类型给目标赋值。
如下面的例子:

class StrVec
{public:StrVec& operator=(initializer_list<string> l){auto data = alloc_n_copy(l.begin(), l.end());free();               不管什么样的赋值运算符都要先销毁掉原来的内存空间elements = data.first;first_free = cap = data.second;return *this;}
};int main()
{StrVec s;s = { "hello", "world" };return 0;
}

上例实现了自定义类型StrVec的列表赋值。

8、下标运算符

  • 下标运算符必须是成员函数
  • 类的下标运算符通常定义一对,一个返回普通引用,一个返回常量引用

如下例所示:

class StrVec
{public:string& operator[](size_t n) { return elements[n]; }const string& operator[](size_t n) const { return elements[n]; } const的对象使用下标运算符,就用这个版本
};

9、递增和递减运算符

  • 通常定义为成员函数。
  • 要同时定义前置后置 版本

9.1 前置版本

class X
{public:X& operator++() { x++; return *this; }X& operator--() { x--; return *this; }
private:int x = 0;
};

注意:返回的是对象的引用。内置版本也是这样的。

9.2 后置版本

虽说是后置版本,但是名字完全一样,所以为了重载他们勉强加一个int类型的参数进去。这个int的作用只有区分重载版本:

class X
{public:X operator++(int);     这里只是声明X operator--(int);        函数的定义和前置有所区别
private:int x = 0;
};

注意:返回的是对象的原值(递增减之前的值)。内置版本也是这样的。
在递增减之前要首先记录对象的状态

X operator++(int)  不需要用到int形参,无需为其命名
{X x = *this;  记录原始状态,便于待会返回++*this;  递增,直接用已实现的前置递增更方便,当然,是对于更复杂的类来说的,这个类太简单了return x;       返回对象原始状态的副本(拷贝)
}X operator--(int)  与后置递增同理
{X x = *this;--*this;return x;
}

10、成员访问运算符*->

箭头运算符必须是类的成员,解引用运算符通常也是类的成员,但也可以不是。

重载 ->

  • 箭头运算符的作用只能是获取成员
  • 对于形如point->member的表达式,point只能是指向类对象的指针或者是一个重载了operator->的类的对象,绝无其它。

11、函数调用运算符

必须是成员函数。

11.1 函数对象

函数对象其实是类,这个类定义了函数调用运算符,当使用这个类的对象调用重载的函数调用运算符时,其形式和调用函数一毛一样,所以叫做函数对象。下面我们举个例子:

class X
{public:int operator()(int value){return value > 0 ? value : -value;}
};int main()
{X x;int a = x(-42);           a 的值是42
}

你看,a = x(-42),这多像函数调用,可以称X为函数对象。

函数对象通常作为泛型算法的实参。

就类似lambda表达式那个作用,还是举个例子,让我们把上面的类改一改。

class X
{public:X(ostream& o = cout, char c = ' ') : os(o), sep(c) { }       构造函数void operator()(const int& x){os << (x > 0 ? x : -x) << sep;}
private:ostream& os;char sep;
};int mina()
{X x;vector<int> v{ -1, 3, 5, -9, -54, 9, -1, -3, -2 };for_each( v.begin(), v.end(), X(cout, '\n') );
}

修改后的类X有两个成员;分别表示 输出流 和 间隔符。
他的函数调用运算符,能够向给定的输出流以给定间隔符 输出传入的int的绝对值。

main函数中我们使用for_each对每个v的元素调用X的函数调用运算符,就实现了像标准输出流输出v中所有元素的绝对值,并以换行符为间隔。

这就是上面说的:函数对象通常作为泛型算法的实参

11.2 深入剖析lambda表达式

当我们编写一个lambda表达式,编译器将该表达式翻译为一个未命名类未命名对象,这个对象是一个函数调用运算符
哇,原来如此,惊为天人,原来是这样。我们举个例子:

stable_sort(words.begin(), words.end(), [](const string& a, const string& s2) { return a.size() < b.size(); }

上面实现了根据长度将string排序。其lambda表达式的行为类似于下面的函数对象

class shorter
{bool operator()(const string& s1, const string& s2){return s1.size() < s2.size();}
};

调用这个函数对象:

stable_sort( words.begin(), words.end(), shorter() );

通过这个例子可一目了然地看到,上面的 lambda表达式和下面的函数对象是等价的。

那么 捕获 又是怎么一回事呢?

两种情况

  • lambda通过引用捕获变量时,由程序确保引用的对象确实存在,故,编译器直接使用该引用, 不用在lambda产生的类中将其存储为数据成员
  • lambda通过值捕获方式捕获变量时,变量是被拷贝到lambda的,故,lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数构造函数使用捕获到的值初始化数据成员

焯!连起来的,知识连起来了!太妙了!!!还是举个例子:

auto first_iter = find_if(words.begin(), words.end(), [size](const string& a) { return a.size() >= size; }

上面能够获得指向序列中第一个长度>= size的迭代器。
lambda产生的类形如:

class size_cpomare
{public:size_compare(size_t n) : size(n) { } 这里得构造函数使用捕获到的值初始化 size 成员bool operator(const string& s){return s.size() >= size;}
private:size_t size;    这里有一个名为 size 的成员,准们保存捕获到的 size 的值
};

调用这个函数对象:

auto first_iter = find_if( words.begin(), words.end(), size_compare(size) );

lambda表达式产生的类不含默认构造函数、赋值运算符 及 默认析构函数;是否含有默认的拷贝 / 移动构造函数 则通常要视捕获的数据成员类型而定。

这里有个好问题:

11.3 标准库定义的函数对象

下面是标准库定义的表示算术关系逻辑 运算符的类,每个类都定义了调用运算符,可以执行相应的运算。

标准库定义的这些类都非常好,可以进行我们做不到的操作。例如通过比较指针的地址排序指针序列
这些指针可以是毫无关系的指针,我们直接比较两个指针会产生未定义的行为,使用标准库的less则不会。

vector<string*> v;
sort(v.begin(), v.end(), [](string* a, string* b) { return a < b; } 错误,v里面的指针彼此之间没有关系,<会产生未定义行为。
sort( v.begin(), v.end(), less<string*>() );      正确,标准库定义的 less 是良好的

注意
关联容器默认使用less<key_type>对元素排序,因此可以定义一个指针的set或在map中使用指针作为关键字而无须声明less

精彩练习:

答案:这个办法就很妙!隐式的取模的结果转换成bool类型正好可以用于判断是否能整除

bool divided_by_all(vector<int>& vec, int dividend)
{return count_if( vec.begin(), vec.end(), bind2nd(modulus<int>(), dividend) ) == 0;
}

11.4 可调用对象 和 function类型

目前为止见过的可调用对象有:

  • 函数
  • 函数指针
  • lambda表达式
  • bind创建的对象
  • 重载了函数调用运算符的类
  • 标准库定义的函数对象(其实这个也算上一条里的)

和其它对象一样,可调用对象也有类型。例如,每个lambda表达式都有自己唯一的(未命名)类类型。函数和函数指针也有类型,他们的类型由其返回值类型和实参类型决定

但是,不同类型的可调用对象,可能共享同一种调用形式

调用形式:是可调用对象的 返回值 和 参数列表 的 组合 。

举个例子,现有如下三种可调用对象:

auto add1 = [](int a, int b) { return a + b; }    这是lambda表达式,是 未命名 的类,用编译器打印    typeid(add1).name() ,是下面这串东西class <lambda_1df7cfe0482636e736c1805ed2e94511>int add2(int a, int b) { return a + b; }       这是函数,是 int (int, int) 类,它的类型其实就是调用形式class add3        这是函数对象,是 add 类
{public:int operator()(int a, int b) { return a + b; }
};

嗯,他们都是可调用表达式,类都不相同。但是他们都有 共同的调用方式

那就是:

int(int, int)        返回值 和 参数列表的组合,

根据这一特性,C++推出一个 function,允许我们 用一个对象 存储 具有相同调用方式的 可调用对象

function


function对象允许我们用一个对象,存储一类具有相同调用方式的可调用对象。
就用上面的三个可调用对象举个例子:

             f1, f2, f3, 都具有相同的类型,都是 function<int(int, int)> 类
function<int(int, int)> f1 = add1;           f1 存储了可调用对象 add1
function<int(int, int)> f2 = add2;           f2 存储了可调用对象 add2
function<int(int, int)> f3 = add3();     f3 存储了可调用对象 add3cout << f1(1, 2);     打印3
cout << f2(1, 2);     打印3
cout << f3(1, 3);     打印3

下面使用function做一个简易计算器,阅读代码,体会一下function的用处:

class mul        函数对象,实现乘法
{public:int operator()(int a, int b) { return a * b; }
};int divi(int a, int b)        函数,实现除法
{return a / b;
}int main()
{map<string, function<int(int, int)>> cal;   一个 map,运算符号为键,其实现作为值auto mod = [](int a, int b) { return a % b; };      lambda表达式,实现取模插入元素:cal.insert({ "+", plus<int>() });                       标准库的加法函数对象cal.insert({ "-", [](int a, int b) { return a - b; } });  未命名的 lambda 对象,实现减法cal.insert({ "*", mul() });                               函数指针cal.insert({ "/", divi });                                用户定义的函数对象cal.insert({ "%", mod });                                命名了的 lambda 表达式int a, b;string s;while (cin >> a >> s >> b){cout << a << s << b << " = " << cal[s](a, b) << endl;}
}

上面代码中名为calmap是一种叫函数表的东西:

重载函数与function

我们不能直接重载函数的名字存入function类型的对象中。如下所示:

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }function(int(int, int)> cal = add;     错误,出现二义性,到底要存哪个 add

即便在声明cal前面加上了调用方式仍然不行
这时应该使用函数指针,而非函数的名字。

int (*fun_ptr)(int, int) = add;     指针'fun_ptr'所指的'add'是接受两个 int 的版本
cal = fun_ptr;         这回对了

12、重载、类型转换 与 运算符

我们已经见过很多内置类型之间的类型转换了,下面将学习自定义类的类型转换

这其中包含两个方向的转换,从其他类型转换到本类型,通过转换构造函数来完成。
以及从本类型转换成其他类型,通过类型转换运算符完成。

转换构造函数其实和普通的构造函数没啥区别,只不过他只有一个其它类型的参数,用这个参数来构造一个本类型的对象。

12.1 类型转换运算符

类型转换函数的一般形式:
operator type() const;       type 是要转换成的类型

type的类型要求是能作为函数的返回类型void除外)。因此不允许传换成数组函数类型,但可以是数组指针、函数指针或者引用类型。

注意:

  • 类型转换运算符必须是成员函数
  • 不能声明返回类型
  • 形参列表必须为空
  • 函数应该用const修饰

下面举一个简单的例子:

class X
{public:X(int i = 0) : val(i) { }           转换构造函数, int 类型转 X 类型operator int() { return val; }       类型转换运算符,X 类型转 int 类型
private:size_t val;
};X x;
x = 4;     4 先隐式转换成 X 类型,然后调用合成的赋值运算符
x + 3;     x 隐式的转换成 int 类型,然后执行整数加法x = 3.14;   3.14 转换成 int ,int 再转换成 X
x + 3.14;  x 先转换成 int,int 再转换成 double

哎停停停停停!最后两行转换代码是不是有什么问题?怎么会自动执行两步类型转换呢???
之前学的分明是:编译器只会自动执行一步类型转换。

嗯,确实只会执行一步。不过这里出现了新的特性

类型转换运算符的特性:

用户定义的隐式的类型转换可以置于一个内置类型转换之前或之后,并与之一起使用。 只有这种情况下,允许执行两步类型转换。

上面的最后两条语句都是有自定义的类型转换参与,所以可以进行。

因此,可以把任何算术类型传递给X的构造函数,同理,也能使用类型转换运算符把一个 X对象转换成int,然后把得到的int转换成任何其他算术类型

不能滥用类型转换运算符:

直接上例子,在早期C++版本中,下面的代码是能编译通过的:

int i = 10;
cin << i;     这里 cin 后接左移运算符,显然是错的,但却能通过编译

istream本身确实没定义<<,能通过编译是因为,在早期C++版本的这种情况下,istream能够隐式的转换成bool类型,由于bool是算术类型,所以能执行<<,就是将bool的值左移10位。这显然不是我们想要的结果,他应该报错才对。

现实的类型转换运算符:

为了避免上述情况,C++11引入显式类型转换运算符
在转换运算符前用explicit修饰,就不会自动执行这一类型转换。同时,(一般情况下)也不能用于隐式类型转换。只能显示的使用它。如下所示:

class X
{public:operator int() { return val; }int val;
};X x;
x + 3;                     错误,隐式转化
static_cast<int>(x) + 3; 正确,显示强转转换

上面的规定存在 例外 :

如果表达式被用作条件,则编译器会将explicit修饰的类型转换运算符自动应用于它。在下列位置,显式类型转换将被隐式的执行:

  • ifwhiledo语句的条件部分
  • for语句的条件表达式
  • !||&&这些逻辑运算符
  • ? : 条件运算符的条件表达式

这已经是明示你了,C++就是想让你在自定义的类型转换运算符的时候,使用explicit修饰,同时,要把类类型转换成bool类型,专门用于这些判断是否为真的情况。不过这是我个人的理解,有时候肯定也是有其他应用情况的。

12.2 避免有二义性的类型转换

有四种由类型转换导致的二义性错误。

12.2.1 两个类定义了转换到同一个类型的成员

图示:

class A
{public:A(const B&); 转换构造函数,B 类型转 A 类型A fun(const A&);
};class B
{public:operator A() const;  转换运算符,B 类型转 A 类型
};B b;
A a = fun(b);      fun()里需要一个A类型对象,b需要转换,但是会产生二义性是 fun(B::operator()) 呢? 还是 fun(A::A(const B&)) 呢?

你现在有两种由B转换成A的方法,那到底用哪个???这就产生了二义性。
想要区分两种方法,就必须显示的调用相关成员:

A a1 = f(b.operator A());   用B的类型转换运算符
A a2 = f( A(b) );          用A的构造函数

解决方法:

不要定义出两种转换成同一类型的方法噻。有一种方法不就够了吗

12.2.2 涉及到内置类型的多重类型转换

图示:

直接上例子:

class A
{public:A(int = 0);     int 转换成 AA(double);     double 转换成 Aoperator int() const;       A 转换成 intoperator double() const;   A 转换成 double
};void fun(long double);        函数,需要 long double 做成员A a;        a 到底是先转 int 再转 long double 呢?
fun(a);     还是先转 double 再转 long double 呢?lone lg;    lg 到底是先转 int 再转 A 呢?
A a2(lg);   还是先转 double 再转 A 呢?

解决方法:

少定义点类类型 和 算术类型之间的转换,很多算术类型间本来就能转换,你还定义这么多,不是添乱??

12.2.3 重载 和 转换构造函数

直接上例子:

class A
{public:A(int);      int 转 A
};class B
{public:B(int);      int 转 B
};void f1(const A&);        用 A 重载
void f1(const B&);      用 B 重载
f1(10);     显然,又二义性了f1( A(10) ); 显式的用A,避免二义性

虽说可以通过显式的构造,但是存在这种操作,说明程序设计存在缺陷。

12.2.3 重载 和 转换构造函数(比上一个稍微复杂点)

这个和上一个很想,但要复杂一点:

class A
{public:A(int);      int 转 A
};class B
{public:B(double);       double 转 B
};void f1(const A&);        用 A 重载
void f1(const B&);      用 B 重载
f1(10);     哎!哎哎哎!!10是 int 欸!是不是不会二义性了?不不不!很可惜,还是会二义性。

分析一波:

  • f1(const A&)成立,int可以转A,而且是精确匹配
  • f1(const B&)成立,int可先转double,然后double再转B

啊?这不是有一个可精确匹配吗?虽然但是,不可以!
编译器会报错,这是二义性。

就是因为你有两个类,如果只有一个类的话,里面有intdouble的构造函数就不会有二义性了。

这小节很绕,看看这个练习题:

14.50:
看好了LongDouble并不是内置的long double,不存在精确匹配一说。
14.51:
这一题告诉我们,内置类型转换优先于自定义的转换构造函数。

12.3 函数匹配 与 重载运算符

注意:本节的讨论的核心是重载运算符

一言以蔽之,就是下面这句话:

说详细点就是:
当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。

也就是说,若a是一种类类型,则表达式a sym b的等价可能是:

a.operator(b);       a 的成员函数
operator(a, b);     一个普通的非成员函数

产生二义性的例子:

class X
{friend X operator+(const X&, const X&);
public:X(int = 0);operator int() const { return val; }int val;
};X x1, x2;
X x3 = x1 + x2;       使用重载的operator+成员
int i = s3 + 0;       产生二义性

例题:
这个例题算是您能遇见的最复杂的情况了,把他搞明白,就没问题了。

这是 上图缺少的 LongDoubleSmallInt的定义:

对于第一个式子ld = si + ld;
对于第二个式子ld = ld +si:仍然使用同样的策略

总结:

当两个类类型相加时,优先考虑一方进行类型之间的转换,再相加,如果不行,再考虑双方都转换成内置类型相加。

再来一个,这个题主要想让你知道,出现二义性,要怎么改
SmallInt的定义和上题一样
很明显会产生二义性

所以我们只需 显式的 让他走其中一条路即可。

C++ 操作重载与类型转换 《C++Primer》第14章 读书笔记相关推荐

  1. C primer plus第二章读书笔记3

    进一步使用C 以下是在C中如何使用C计算,运算符的引入使得这个过程变得简单 int main(void) {int feet, fathoms;fathoms = 2;feet = 6 * fatho ...

  2. C++ Primer 第三版 读书笔记

    1.如果一个变量是在全局定义的,系统会保证给它提供初始化值0.如果变量是局部定义的,或是通过new表达式动态分配的,则系统不会向它提供初始值0 2.一般定义指针最好写成:" string * ...

  3. C++ primer 第14章 操作重载与类型转换

    文章目录 基本概念 直接调用一个重载的运算符函数 某些运算符不应该被重载 使用与内置类型一致的含义 选择作为成员或者非成员 输入和输出运算符 重载输出运算符<< 输出运算符尽量减少格式化操 ...

  4. c++ primer 第14章 习题解答

    14.1节 14.1答 不同点: 重载操作符必须具有至少一个class或枚举类型的操作数. 重载操作符不保证操作数的求值顺序,例如对&&和| | 的重载版本不再具有"短路求值 ...

  5. 《C++ Primer Plus 6th》读书笔记 - 第8章 函数探幽

    1. 摘录 默认参数指的是当函数调用中省略了实参时自动使用的一个值. 默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式.使用默认参数,可以减少要定义的析构函数.方法以及方法重载的数量. 试图 ...

  6. python程序操作的核心_python核心编程-第五章-个人笔记

    1.用del删除对对象的引用 >>> a = 123 >>>a123 >>> dela>>>a Traceback (most ...

  7. mysql中用完即删用什么_MySQL使用和操作总结(《MySQL必知必会》读书笔记)

    简介 MySQL是一种DBMS,即它是一种数据库软件.DBMS可分为两类:一类是基于共享文件系统的DBMS,另一类是基于客户机--服务器的DBMS.前者用于桌面用途,通常不用于高端或更关键应用. My ...

  8. C++ Primer第三章 心得笔记

    之前的一章我本末倒置了,我看了一个大佬的此书笔记整理得很详细 很得体.我也想按照他的这种方法 在我学习和敲代码的时候进行记录,但是我发现为了记笔记而记笔记 这种方法使我很累.违背了记录分享交流的初衷. ...

  9. 《C++ Primer》第14章 14.3节习题答案

    <C++ Primer>第14章 操作重载与类型转换 14.3节  算术和关系运算符  习题答案 练习14.13:你认为Sales_data类还应该支持哪些其他算术运算符(参见表4.1,第 ...

最新文章

  1. C++字符串详解(三) 字符串的查找
  2. 2.变量/字符串/if/while/数据类型
  3. C++ 3 基本数据类型
  4. 【数据结构与算法】之连通网络的操作次数的算法
  5. arch linux 安装 arm,给树莓派安装 Arch Linux ARM
  6. Python小白的数学建模课-06.固定费用问题
  7. 开平区教育局资源分布式存储解决方案
  8. java文件与bean所定义的_Spring定义bean的三种方式和自动注入
  9. CCP/XCP和T-BOX知识点
  10. 31岁零基础转行软件测试,现已成功入职月薪14K+
  11. android finish 判断当前_Android开发,源码分析finish()和onBackPressed()的区别
  12. 人生每一件事都是为自己而做
  13. cad工具箱详细讲解_cad学院派工具箱(cad绘图教程配解析)V20160804 最新版
  14. 期货开户手续费是怎么查询?
  15. html怎么读取lrc文件,lrc文件怎么打开?lrc是什么文件?
  16. shell脚本使用教程3
  17. 技术分享 | 服务端接口自动化测试, Requests 库的这些功能你了解吗?
  18. 【FPGA实例】基于FPGA的DDS信号发生器设计
  19. 【Pytorch】带注释的Transformer (各个部件的实现及应用实例)
  20. Python - 100天到大师学习笔记(2)

热门文章

  1. React笔记随笔---kalrry
  2. 8种基本数据类型转换
  3. CV中的Attention机制总结
  4. 关于snp-calling中GATK软件的最佳线程(核心)数
  5. js逆向第5例:猿人学第8题-验证码图文点选
  6. ASP.NET2.0雷霆之怒盗链者的祝福
  7. php setcookie 全局,PHP-setcookie(); 不工作
  8. Vote3Deep: Fast Object Detection in 3D Point Clouds Using Efficient Convolutional Neural Networks
  9. 欧几里得算法和更相减损术证明
  10. 设计模式第十二次作业——观察者模式、状态模式