C++_Primer_学习笔记_第十四章(重载运算和类型转换)
第十四章(重载运算和类型转换)
1).自定义运算符号以及类型之间的转换规则。
- 重载运算符号,定义运算符号的含义。
{
// 前后对比。
print(cout,add(item1,item2));
cout << item1 + item2;
}
/1.基本概念
1).几点说明。
- 运算符函数名字由
operator
关键字,和它要定义的运算符号一起组成。由返回类型,函数体,参数列表。 - 参数数量和运算对象的关系。
是否成员函数 | 参数数量和运算对象关系 |
---|---|
是 | 比运算对象少一,this 作为隐式的参数绑定第一个参数。 |
否 | 相等 |
- 传入参数时,需要注意,左侧运算对象依次向右,为第一个,第二个……参数。
- 对于既可以是二元也可以是一元的运算符号,通过传入的参数个数来确定是几元的版本。
- 除了,重载调用运算符的运算符函数外,其他的运算符函数都不允许有默认实参。
- 重载运算符号的作用对象,至少有一个是类类型(可以是成员的函数)。即,我们不能修改内置的运算符号的含义。
{
int operator+(int,int);//错误,不可以为内置类型重新定义运算符号。
}
- 只能定义已经存在的运算符号,不可以自己创造符号。
- 重载的运算符的优先级和结合律是不变的。
2).如何使用重载运算符号。
{ //非成员的运算符函数
data1 + data2; //直接使用,也是间接地调用运算符函数。
operator(data1,data2); //等价的函数调用。
// 成员运算符函数
data1 += data2; //基于调用的表达式
data1.operator+=(data2); //等价的函数调用。this绑定了data1的地址,传入。
}
3).并不是所有的运算符号都可以重载。
::``.*
-
.
,? :
- 虽然有些运算可以重载,但是,这些运算
- 原本有明确的求值顺序,例如
&&
,||
,,
- 或者在类中有明确的含义,
&
就是取地址. - 对于
&&
,||
还有短路原则。
- 所以不建议重载。重载之后这些特性将无法保留。
4).该怎么重载运算符号
- 当类的操作和运算符的逻辑是相关的,才考虑重载运算符。
- 尽可能地保持和内置的版本是一样的。
- 例如逻辑运算符和关系运算符应该返回的是
bool
;算术运算返回一个类类型的值; - 赋值运算和复合赋值运算符应该返回左侧运算对象的一个引用。
- 执行顺序应该一致。例如,
operator+=
,应该先执行+
再执行=
;
- 绑定性原则
- 如果一个类定义了,
operator==
通常地它也应该有operator!=
的定义。 - 如果一个类定义了
operator<
,通常他也应该定义其他的关系操作。 - 如果一个类定义了,算术运算,或者运算,最好也将它的复合运算一起定义。
- **无二义性原则,**重载的运算符号,不应该是令人迷惑的。
5).重载成成员函数还是非成员函数?
- 先明确区别。
- 如果是成员,那么它的实参就会比原来运算符的运算对象数量减一。第一个实参就是
this
。 - 如果不是成员,那么实参要和运算对象数量一致,且顺序也一致。
- 和调用对象相关的,改变对象状态的,一般是成员。例如,
=,[],(),->
,与this
息息相关,必须是成员。复合赋值运算一般是成员。递增,递减,解引用,和this
关系密切,应该是成员。 - 有对称,可以进行类型转换运算的,算术,相等性,关系,位运算一般是非成员函数。
- 下面这个说明,混合表达式时必须是非成员的。
{
string s = "hello,world";
string s1 = s + "nihao";//不论是成员还是非成员都是正确的
string s2 = "nihao" + s;//只有当重载的是非成员才是正确的。
// 因为左侧的ui想必须是运算符所属类的一个对象。
// 因为,以上等价于。
// "nihao".operator+(s);
// 而"nihao"是一i个const char *
// 是一个内置类型,根本就没有成员函数。
// 如果定义成非成员函数,则上述的等价于
// operator+("nihao",s);
// 这样只要求,每一个实参可以转换成形参类型,并且有一个实参是类类型就可以。
}
练习,
- 14.2,是否设计成友元,成员,非成员。
{
class Sales_data {
// 设置成友元,因为成员第一个参数只能是this。
friend istream& operator>>(istream &,Sales_data &);
friend ostream& operator<<(ostream &,Sales_data &);
public:
Sales_data& operator+=(const Sales_data&);
};
Sales_data operator+(const Sales_data &,Sales_data &);
}
- 14.3
{
"couble" == "stone"; //使用的是内置的版本,比较的是两个指针
vec[1] == vec[2]; //使用的是string重载的版本
vec == vec;//使用的是vector重载的版本
vec[1] == "stone";//使用的是string版本的==,其中字符串字面量被转换为string
}
- 14.4,
->,()
必须为成员。否则会报错。 - 14.5
{
class Date {
friend ostream& operator<<(ostream &,const Date &);
private:
int year, month, day;
};
ostream& operator<<(ostream &os,const Data &D) {
const char sep = '\t';
os << D.year << sep << D.month
<< sep << D.day << endl;
return os;
}
}
/2.输入和输出运算符
//1.重载输出运算符<<
1).为什么第一个形参是非常量的ostream
引用。
- 非常量,因为向流写入内容会流的状态。
- 引用,
ostream
无法拷贝。
2).第二个形参一般是const
引用。
-
const
,因为打印,不会改变值 - 引用,避免不必要的拷贝。
3).返回什么?
- 为了和内置类型一致,返回的
iotream
的引用。
4).应用例子。如上练习。
- 注意,输出不应该控制太多细节,例如换行,这也是和内置类型保持一致;冲在输出时输出内容即可,而关于格式由用户自己决定。
5).一般是友元。
- 如果是成员。
{
// 调用时发生
Sales_data data;
data << cout;
// 除非data是ostream的对象。
// 书的解释很迷惑
// 我们无法给标准添加成员。
}
- 是友元,需要访问私有的数据成员。
//2.重载输入运算符>>
1).第一个形参是运算符号要读取流的引用。第二形参是要读入到的非常量的对象的引用。返回的是给定流的引用。
2).应用例子。
- 输入必须进行流状态的判断。
{
istream& operator>>(istream &is,Sales_data &d) {
double price;
cin >> d.bookNo >> d.units_sold >> price;
if (is) //检查是否输入成功
d.revenue = d.units_sold * price;
else
d = Sales_data(); //进行默认构造,使得它是合法的,有效的。
return is;
}
}
3).输入时,可能遇到的错误。
- 流中含有错误的数据类型时,读取操作可能会失败。例如,当读取完
bookNO
时,假设后面要输入的应该是两个数字,当后面的数据不是两个数字时,则读取操作会失败,后续的流的其他使用都将会失败。 - 读取操作到达文件流的末尾或者输入流遇到其他的错误时,读取操作也会失败。
- 程序中没有逐一检查,而是后续一起检查。
- 重载的输入操作,应该确保即使输入错误,也保证对象是处于一种合理的状态,如上例,将对象进行重置。
- 更进一步,输入运算符也应该设置,流的条件状态来标示失败的信息。最好的方式就是使用现成的
failbit,badbit,eofbit
/3.算术和关系运算
1).几点说明。
- 一般是定义为非成员函数。
- 一般不需要改变运算对象的类型,所以我们把形参设置为常量的引用。
- 返回的是比较结果的副本即可。
- 如果定义了算术运算,一般也会定义一个对应的复合赋值运算符号。此时,最有效的是用复合赋值运算符来定义算术运算符号(再内部使用)。
{
Sales_data
operator+(const Sales_data &l;const Sales_data &r) {
Sales_data sum = l;
sum += r;
return sum;
}
}
练习,
- 14.13,
{
class Sales_data {
friend Sales_data operator-(const Sales_data &,const Sales_data &);
public:
Sales_data& operator-=(const Sales_data &);
};
Sales_data operator-(const Sales_data &l,const Sales_data &r) {
Sales_data sub = l;
sub -= r; //使用复合的来实现
return sub;
}
Sales_data& Sales_data::operator-=(cosnt Sales_data &r) {
units_sold -= r.units_sold;
revenue -= r.revenue;
return *this;
}
}
- 14.14,使用复合来实现,减少重复的代码。方便。
//1.相等运算符
1).每一个成员都相等才相等。
{
bool operator==(const Sales_data &l,const Sales_data &r) {
return l.isbn() == r.isbn() &&
l.units == r.units &&
l.revenue == r.revenue;
}
bool operator!=(const...) {
return !(l == r);
}
}
2).重载运算符号的好处。
- 类就像是内置类型一样,可以使用
==
来判断是否相等,没有记忆压力,符合习惯。 - 绑定定义。有了
==
,应该有!=
。 - 我们可以进行“委托”,减少代码量。
!=
的实现实际就是依靠==
实现的。
//2.关系运算符
1).由于关联容器和一些算法经常使用到<
。所以定义operator=
会比较有用。
2).关系运算的要求。
- 定义一个明确的顺序。保证有明确的大小关系,传递性质。
- 如果不是相等,那么一定有一个对象小于另一个对象。特别是当定义了
==
时,需要定义一种关系是与==
一致的。
- 例如,对于
Sales_data
,虽然我们可以只定义isbn
的比较满足了1。可是这与我们的==
含义是不一样的。 - 或者
==
,不满足,但是isbn
是相等的。总而言之就是定义不统一。
3).什么时候定义<
。
- 要满足以上的两个条件。
- 否则还有可能一些一些小的复杂情况。
/4.赋值运算符
1).除了同类对象之间的赋值(移动赋值,和拷贝赋值),我们的类还应该定义其他的赋值运算以使用别的类型的作为右侧对象。
- 例如,
vector
就定义了第三种的赋值运算符。接受{}
里面的元素作为参数。 - 对自己的
StrVec
类进行扩展。
- 和
vector
一样,我们应该返回该对象的引用。 - 并且它应该作为类的成员。
- 赋值运算,不管形参是什么,他都应该是类的成员。
{
v = {"a","b"};
class StrVec {
public:
StrVec& operator=(initializer_list<string>);
};
// 这里不需要检查是否是自身赋值。因为形参和对象不是一个类型的。
StrVec& StrVec::operator=(initialized_list<string> il) {
// 分配内存空间,并进行拷贝。
auto data = alloc_n_copy(il.begin(),il.end());
free(); //销毁原对象
elements = data.first;
first_free = data.second;
return *this;
}
}
2).复合赋值运算符
- 我们一般会定义为成员。
- 同理返回左值引用。
{
Sales_data& Sales_data::operator+=(const Sales_data &r) {
units_sold += r.units_sold;
...
return *this;
}
}
练习,
- 14.21,复合运算就对每一个成员都是用这个复合运算;在加法或者减法中,需要拷贝,再进行复合的运算,这样更加复合规范。
- 14.22,可以进行隐式类型转换的。
{
class Sales_data {
public:
Sales_data& operator=(const string &);
};
// 非同类型对象之间的赋值
// 同类型对象之间的赋值,拷贝赋值
// 同类型之间的初始化,拷贝构造函数
// 非同类型之间的初始化,构造函数。
Sales_data& Sales_data::operator=(const string &isbn) {
bookNo = isbn;
return *this;
}
}
- 14.24,浅拷贝就可以满足需求,因此不需要额外定义拷贝赋值,和移动赋值。即可以使用默认的版本。
- 14.25,按照需求进行构造,例如是否需要只接受一个
string
的赋值运算符函数。
/5.下标运算符
1).几点注意,
- 返回值,访问元素的引用(与内置版本一致),既可以作为左值,也可以作为右值;
- 设置两个版本,一个是
const
一个是非const
。例如,当对一个常量进行下标运算时,返回常量引用保证不会修改内容。另一个返回的是普通的引用。
{
// 非const
string& StrVec::operator[](size_t n) {
return elements[n];
}
// const
const string& StrVec::operator[](size_t n) const {
return elements[n];
}
// 例子
const StrVec cvec = vec;
svec[0] = "zero"; //正确,返回的是非常量引用
cvec[0] = "zip";//错误,返回的是常量的引用。
}
/6.递增递减运算符
1).几点说明。
- 同时设置前置和后置,(绑定定义)
- 是对对象的迭代器进行改变,所以一般是成员函数
- 前置的,返回的是对象的引用。
2).定义前置版本。
{
class StrBlobPtr {
public:
StrBobPtr& operator++();
StrBobPtr& operator--();
};
}
- 先检查对象(指针)的有效性,再检查递增或者递减是否合法
- 如果不是抛出异常,反之返回对象的引用。
{
StrBobPtr& StrBobPtr::operator++() {
// 检查是否已经是尾后迭代器了
check(curr,"increment past end of StrBobPtr;");
++curr;
return *this;
}
// 对于递减。
// 这里需要注意,如果curr是一个无符号的数,并且已经是0,递减它将会得到一个非常大的正数。
--curr;
check(curr,"decrement past the begin of StrBobPtr;");
return *this;
}
3).重载前置和后置。
- 由于函数的名字,参数个数,都一样。怎么重载?
- 方法,后置版本添加一个不被使用的
int
类型的形参。这个形参不命名,编译器会为这个形参提供一个值为0的实参。 - 一般来说,这个额外的形参就是器区别的作用。
- 返回值类型是不重载的。
4).定义后置版本的。
- 返回的是一个值。
- 后置版本借助前置版本实现。
{
class...
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
//定义
Str... StrBlo...::operator++(int) {
// 有效性检查,前置版本为我们做好了
StrBlobPtr ret = *this;
++*this;
return ret;
}
// 后置版本
// 同理。
Str... ret = *this;
--*this;
return ret;
}
5).使用
- 就是一个函数匹配的问题
{
// 显式地使用,就必须传参
// 如果是隐式地使用,跟内置版本是一样的
p.operator++(0);//调用后置版本
p.operator++();//调用前置版本的
}
/7.成员访问运算符
1).定义以及说明。
- 定义为
const
成员,不会改变对象的状态。 - 返回值是否是常量,根据指针所指向的内容决定。
- 构造时候的形参,是否是
const
。
{
class...
string& operator*() const {
// 先检查当前的下标是否合法。
auto p = check(curr,"dereference past end");
return (*p)[curr]; //*p就是vector
}
string* operator->() const {
return &(*this->operator*());//借助解引用来实现
//注意不是
// return &(*this);
// 因为我们定义运算符,是对于类的对象而言的。
}
}
2).应用。
{ //p是对象的一个类。
*p = "hi";//对curr元素赋值
p->size();//p->返回的是string*
//等价于p.operator->size();
(*p).size();//这个*p返回的是string&,这个比上面容易理解
}
3).->
和*
的不一样,
- 理论上,对于
*
的重载我们可以是返回一个固定值,或者打印。 - 而对于
->
,我们永远不要丢失访问成员的基本含义。改变的是从哪一个对象获取成员,但是获取成员的含义是不变的。
{
// 对于以下的式子
point->mem;
// point必须是一个指向类对象的指针
// 或者必须是重载了operator->的类的对象。
(*point).mem;//指针
point.operator->mem;//类的一个对象
}
- 等价过程。
- 指针,
(*point).mem
;先解引用,再从对象中获取成员。 - 对于对象。使用
point.operator()
的结果来获取mem
;如果是一个指针,按照1.方式进行。如果结果本身含有重载的operator->
,重复调用当前的步骤。直到过程结束返回所需内容,或者程序报错。
- 重载了箭头的运算函数必须返回类的指针或者自定义了箭头运算符的某个类的对象。
练习,
- 14.31,对于默认可以完成的拷贝控制,我们不需要特别定义。但是注意三五法则。
- 14.32,
{
class...
string* operator->() {
// 这相当于是函数的调用。
return ptr->operator->();
}
StrBlobPtr *ptr;
}
/8.函数调用运算符
1).优势。
- 既可以像函数一样使用该类的对象,
- 也可以保存状态。
2).简单的例子。
- 返回参数的绝对值。
{
struct absInt {
int operator()(int val) const {
return val < 0? -val : val;
}
};
}
- 使用。
{
int i = -42;
absInt = absObj;
int ui = absObj(i); //等价于是absObj.operator()(i);
}
3).几点说明。
- 函数调用运算符必须是成员函数,可以定义多个调用运算符,只要符合重载的定义即可。
- **函数对象。**定义了
()
的类的对象。对象的行为像函数一样。
4).应用。
{
class PrintString...
// 提供默认的版本。
PrintString(ostream &o = cout,char c = ' ') : os(cout),sep(c) {}
void operator()(const string &s) const {os << s << sep;}
ostream &os;
char sep;
// 使用。
PrintString printer;//使用的是默认的
printer(s); //cout和空格
PrintString errors(cerr,'\n');
errors(s); //使用cerr进行输出,后面是'\n'。
}
- 函数对象使用在,泛型算法的实参。
{
for_each(vs.begin(),vs.end(),PrintString(cerr,'\n'));
// 这里是对于容器中的每一个对象调用PrintString的临时对象。然后就会打印处每一个元素的内容。
}
//1.lambda是函数对象
1).lambda
也曾经在for_each
中使用,编译器将该表达式翻译成一个未命名类的未命名对象。并且在lambda
表达式产生的类中含有一个重载的函数调用运算符。
- 函数体,形参列表完全一致。
- 由于一般情况下,
lambda
是值捕获,不会改变变量的值。所以在它等价的类中的operator=
加了const
。如果是引用捕获,那么就不能是const
。
{
[](const Sales_data &a,const Sales_data &b) {return a.size() > b.size();}
// 等价于以下未命名类的一个未命名对象
class ...
bool operator()(const Sales_data &a,const Sales_data &b) const {return a.size() > b.size();}
}
- 假定上面的类的名字是
ShorterString
。泛型算法的第三个实参是可调用对象。
{
stable_sorted(word.begin(),word.end(),ShorterString());
// 注意这里是构造一个空的没有命名对象,
// 对每一个元素调用这个对象。
// 可调用表达式。
}
2).lambda
的捕获类型和数据成员的关系。
- 引用捕获,则无需存储为数据成员
- 值捕获,是拷贝到
lambda
类中去,因此需要建立数据成员,同时创建构造函数,用捕获到的值进行初始化。
{
[sz](const string &s) {return s.size() >= sz;}
// 等价的类,假定名字为c
class c {
c(size_t n) : sz(n) {} //捕获并进行初始化。
bool operator()(const string &s) {return s.size() >= sz;}
size_t sz;
};
}
- 等价的类没有默认构造函数,因此想要构造对象必须有一个实参。
- 调用时,
find_if(w.begin(),w.end(),c(sz));
注意c(sz)
是一个函数对象。 -
lambda
是否需要移动,拷贝构造视情况而定。而赋值一般不需要。而析构一般是默认的。
练习
- 14.41,
lambda
是函数对象的简化;如果需要多次地使用,并且保存装填,使用函数对象。
//2.标准库定义的函数对象
1).性质
- 称为表示运算符的函数对象。
- 这些类都定义了调用运算符(和名称相互匹配的)。
- 都是模板,我们可以指定具体的类型,也就是指定调用运算符的形参类型,从而实例化一个对象。这些对象就是函数对象。
{
plus<int> intAdd; //可执行加法的函数对象
negate<int> intNegate; //对int取反的函数对象
int sum = intAdd(12,12); //等价于sum = 12 + 12;
sum intNegate(inAdd(10,12));//等价于-22;书本的例子错误
sum = intAdd(10,intNegate(10));//sum = 0;
}
2).以下是标准库函数对象。均定义在头文件functional
中
算术 | 关系 | 逻辑 |
---|---|---|
plus<T> |
equal_to<T> |
logical_and<T> |
minus<T> |
not_equal_to<T> |
logical_or<T> |
multiplies<T> |
greater<T> |
logical_not<T> |
divides<T> |
grater_equal<T> |
|
modulus<T> |
less<> |
|
negate<T> |
less_equal<T> |
3).在泛型算法中使用。
- 例如在排序算法中,默认就是使用
operator<
进行排序。如果要进行升序排序。
{
sort(svec.begin(),svec.end(),greater<string>());
// 构造一个空对象。
}
- 注意对于我们的类,只要类中定义了
<>
,就可以使用相似的。例如在Sales_data
定义了<>
,就可以使用模板类生成对应的对象。 - 直接比较两个无关的指针是未定义的。问题,如何排序
vector
中的指针呢?
{
vector<string *> pvec;
sort(pvec.begin(),pvec.end(),[](const string *a,const string *b) {reurn a < b;}) //错误,指针之间没有关系,直接比较是未定义的。
sort(pvec.begin(),pvec.end(),less<string *>()); //正确,使用标准库的运算符,函数对象。在less中是定义良好的。
}
- **由于关联容器是使用
less<key_type>
对元素进行排序的。**因此我们可以定义一个指针的set
或者map
,而不需要声明是less
。
练习,
bind2nd,bind1st???
- 14.42,
{
count_if(vec.begin(),vec.end(),greater<int>(1024));
find_if(vec.begin(),vec.end(),not_equal_to<string>("pooh"));
transform(vec.begin(),vec.end(),vec.end(),multiplies<int>(2));
}
//3.可调用对象和function
1).几种可调用对象。
- 函数,函数指针
-
lambda
,定义了调用运算符号的类的对象 -
bind
创建的对象
2).可调用对象的类型。
-
lambda
,未命名的类类型 - 函数以及函数指针,由返回值类型以及它的参数类型决定。
3).不同类型的可调用对象可能共享一种调用形式。
- 调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。
int (int,int);
表明了一个接受两个int
返回一个int
的函数类型。
{
int add(int i,int j) {return i+ j;}
auto mod = [](int i,int j) {return i % j;};
struct divide {
int operator()(int i,int j) {return i / j;}
};
// 以上定义了三个不同类型的可调用对象。
// 它们类型不一样。但是共享同一种调用形式
int (int,int)
// 对于几个可调用对象共享同一种调用形式的情况。
// 有时候我们希望把它们看成是具有相同的类型。
}
3).函数表,用于存储可调用对象的指针。
- 构建一个简单的桌面计算器。
- 利用
map
来实现。利用表示运算符符号的string
作为关键字。函数指针作为值。 map<string,int(*)(int,int)> binops;
- 添加,
binops.insert({"+",add});``{"+",add}
是一个pair
。 - 问题,
binops.insert({"%",mod});
//错误,这不是函数指针。mod
是一个类类型,而divide
也是一个类类型。
4).解决。标准库function
类型
- 定义在头文件
functional
中。 - 它是一个模板,创建时需要给出额外的信息。
function<int(int,int)>;
这里声明的是一个function
类型,它可以表示,接受两个int
,返回一个int
的调用形式的,可调用对象。
{
function<int(int,int)> f1 = add;//函数指针
function<int(int,int)> f2 = divide(); //函数对象类的对象
function<int(int,int)> f3 = [](int i,int j) {return i * j;}//lambda
cout << f1(3,2) << f2(1,2) << f3(2,3);
}
5).重新定义。
-
map<string,function<int(int,int)>> binops;
传入的参数是可调用对象即可。而不是之前的只能是函数指针。
{
map<string,function<int(int,int)>> binops = {
{"+",add},
{"-"minus<int>()},
{"/",divide()},
{"*",[](int i,int j){return i * j;}},
{"&",mod}
};
}
6).使用。
- 注意到,我们传入的可调用对象不需要命名。通过关键字就可以返回可调用对象,从而进行调用即可。
- 下标运算返回的是引用。
-
function
也重载了调用运算符号,它接受实参,然后传递给存好的可调用对象。
{
binops["+"](10,5);
binops["-"](10.5);
...
}
7).重载函数和function
- 不能直接将重载的函数名字存入
function
中。
{
int add(int i,int j) {return i + j;}
double add(double i,double j) {return i + j;}
map<string,function<int(int,int)>> binops;
binops.insert({"+",add});//错误。
}
- 解决。1使用存储函数指针而不是函数函数的名字。
{
int (*p)(int,int) = add;//指针指向的就是接受两个int的版本。这里由重载
binops.insert({"+",p});
}
- 解决。2使用
lambda
表达式。(但是很麻烦)
{
binops.insert({"+",[](int a,int b){return add(a,b);}});
}
- 旧版本中的
unary_function,binary-function
和这里的function
没有关联。他们已经被更加通用的bind
代替。
8).function
的操作
操作名称 | 相关描述 |
---|---|
function<t> f |
f是可以存储t类型的可调用对象的空function 。 |
function<t> f(nullptr) |
显式地指出为空 |
function<t> f(obj) |
在f中存储可调用对象obj
|
f |
作为一个条件,如果f含有可调用对象为真,反之为假 |
f(args) |
调用f中的可调用对象,args 为传递的参数 |
定义为function<t>的成员类型 |
|
result_type |
该function 类型的可调用对象的返回值类型 |
argument_type |
当只有一个或者两个实参时定义的类型。一个实参时才有 |
first_argument |
两个实参时才有 |
second_argument |
两个实参时才有 |
/9.重载,类型转换和运算符
1).类类型转换由以下共同定义。(也被称为用户自定义的类型转换)
- 转换构造函数
- 类型转换运算符
//1.类类型转换运算符
1).简介。
- 成员函数,将一个类转换为另一个类。不能声明返回类型;形参列表为空;不应该改变转换对象的内容,是一个
const
成员。
{
class SmallInt {
public:
operator int(int = 0)const;//错误,形参列表不为空
};
}
- 形式,
operator type() const {}
- 可以转换为任意类型(函数指针,数组指针,引用等),除了
void
,还有数组,函数等不能作为函数返回类型的类型。
2).为什么类型转换运算符没有形参,以及返回类型的和转换类型的对应关系。
- 类型转换运算符是隐式执行的,所以无法给他们传递实参,也就不可以定义形参。
-
虽然不指定返回类型,但是实际上每一个类型转换函数都会返回一个对应类型的值
operator int*() const {return 42;}//错误,返回的类型和转换的类型不对应
3).例子。
{
// 只能表示0-255之间的一个整数
class SmallInt {
public:
SmallInt(int i = 0) : val(i) {
if (i < 0 || i > 255) {
throw out_of_range("Bad SmallInt value");
}
}
operator int() const {return val;}
private:
size_t val;
};
}
- 以上的例子,既定义了从算术类型转换到类类型的转换,也定义了从类类型到
int
的转换。
{
SmallInt si;
si = 4; //先对4进行类型转换,再调用拷贝赋值运算符
si + 3;//将si隐式地转换为int,再进行整数的转换。
}
- 编译器一次只能执行一次用户自定义的类型转换。
- 内置的类型转换和用户自定义的隐式类型转换可以一起使用。谁前谁后没有关系。
{
// 内置类型转换将double实参转换为int
// 再调用Small(int)构造函数
SmallInt si = 3.14;
// 类型转换运算符将si转换成int,内置类型转换将int转换成double
si + 3.14;
}
4).定义的转换,不应该由二义性。例如Date
,转换为int
是迷惑的。
- 表示时间的数字,19890712
- 表示从某一个时间点开始的天数。
- 这种情况定义成成员函数更好
5).实际中,很少会定义类的类型转换。
- 类型转换是自动发生的,更多的是意外,而不是方便
- 但是转换为
bool
还是比较普遍的。
6).隐式转换的坏处。
- 由于
bool
是一种算术类型,它就可以应用在任何需要算术类型的上下文中。 - 因此转换成
bool
也会由意想不到的后果。这一点在istream
中表现明显。
{
int i = 32;
cin << i;//如果cin的类型转换不是显式的,那么这代码将会编译器视为合法
// 结果是,由于cin没有定义<<,所以cin转换为bool类型,bool进行提升,变为int,然后对int进行内置的左移运算
// 最终就是int被左移32位。
// 这不是我们预期的结果
}
7).解决,显式的类型转换运算符
- 形式,加上关键字
explicit
。 - 有了声明为显式,编译器就不会自动执行这一个类型转换。
{
SmallInt si = 3;
si + 3;//错误,没有隐式的类型转换
static_cast<int>(si) + 3;//显式地要求,正确。
}
- 例外,如果表达式被用于条件,那么编译器会将显式类型转换自动应用,此时相当于是隐式。
-
if,while,do
的条件 -
for
的条件 -
!,||,&&
的运算对象 -
?:
的条件表达式
- 旧版本的解决是,IO库定义了向
void*
的转换
练习,
-
operator const int() {}
转换为const int
//2.避免有二义性的类型转换
1).确保类类型和目标类型之间只有唯一的一种转换方式。以下容易发生二义性。
- 两个类提供类相同的类型转换。例如,A定义了接受B对象的转换构造函数;B定义了向A转换的类型转换运算符。
{
// A的拷贝构造函数
A(const B&){}
// B的转换运算符
operator A(){}
A f(const A&);
B b;
A a = f(b);//可以是调用拷贝构造A::A(const &)
// 也可以是调用B::operator A(){}
// 连个调用效果相当,没有优劣之分
// 如果想要调用,只能是如下方法
A a1 = f(b.operator A());//调用函数
A a2 = f(A(b));//想当于是使用了定义没有名字的对象。
}
- 类定义了多个转化规则。尤其是定义了多个接受参数是算术类型的构造函数,或者转换目标都是算术类型的转换函数。因为算术运算自身就有很多的转换规则。所以类最好只定义一个和算术类型有关的转换规则。
{
struct A {
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
void f2(long double);
A a;
f2(a);//二义性错误。都不是精确的匹配。
// 既可以是A::operator int()也可以是A::operator double()
// 先自定义的再内置
long lg;
A a2(lg);
// 错误,既可以是A(int),也可以是A(double)(都不是精确的匹配)
// 内置
}
2).根本原因其实就是转换的等级是一致的。
3).注意,一旦定义了算术转换运算符
- 不再定义向其他算术类型转换的运算符
- 不再定义接受算术类型的重载运算符函数。
- 其实,除了显式转为
bool
,尽可能避免使用。因为意想不到。
4).重载函数的参数,含有转换构造函数的问题
{
struct C {
C(int);
};
Struct D {
D(int);
};
void f(const C&);
void f(const D&);
f(12);//二义性错误。而且这个错误很隐蔽。
f(C(12));//这样是没有二义性的。
// 这样很麻烦,设计是不良好的。
// 调用重载函数都要显式构造对象,或者强制类型转换。
}
5).不会考虑标准类型转换的情况。
- 当有两个或者多个用户自定义的类型转换都提供了可行的匹配时,标准类型转换会被忽略(可行函数请求的转换函数不唯一。)。因为重载时,是不同类型直接和形参进行匹配。或者,所有可行函数都请求同一用户自定义的类型转换函数进行的匹配。
{
E...
E(double);
void f(const E&);
f(10);//还是二义性的。
}
练习
- 14.51,注意转换的优先级别。
//3.函数匹配和重载运算符
1).重载运算符也是重载的函数。通过给定的表达式,判定到底是使用内置的还是重载的运算符。
- 当运算符函数出现在表达式中时(
c + c1
),候选函数会比调用运算符 调用函数时更大。例如,a sym b
可能是
-
a.operatorsym(b);
//a时类,且有该成员函数 -
operator(a,b);
//该函数是一个普通函数 - 内置的版本。
- 当我们直接调用时,由于调用的形式不一样,含义是较明确的;
- 使用对象或者对象的指针,引用,那么考虑成员版本
- 使用的是普通的调用。考虑的是非成员的版本。
2).重载运算符,算术类型转换运算符和二义性
- 既提供了,转换目标是算术类型的转换,又提供了重载的运算符。这样会使得,重载的运算符号和内置的运算符号二义性。
{
friend operator+(const SmallInt&,const Small...);
SmallInt(int = 0);
operator int() const {}
SmallInt s1,s2;
SmallInt s3 = s1 + s2;//无二义性
// 既可以0->SmallInt->重载的+
// 也可以是,s3->int->内置类型的+
int i = s3 + 0;//二义性。
}
- 结论,不要随意地定义向除了显式的
bool
转换外的算术类型转换。
练习,
- 14.52,注意,是否意味着,
+
的左侧运算对象可以转换成另一个对象,然后使用转换后对象的成员运算符号? - 14.53,使用显式地转换一个运算对象,可以避免二义性。例如,
SmallInt(3.13);//显式地构造一个对象。
或者,static_cast<int> si;//强转为int类型
/10.小节
- 在类中可以定义,转换为源为自身(拷贝构造/赋值运算符?),或者转换目的为自身的类型转换,这样的类型转换将会自动执行。