首页 > 代码库 > C++11(13):重载运算与类型转换
C++11(13):重载运算与类型转换
除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
当以个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数比运算对象的数量少一个。
当运算符作用于内置类型的运算对象时,我们无法改变运算的含义。
:: .* . ?: 这四个运算符不能被重载。
我们只能重载已有的运算符,不能发明新的。优先级和结合律不变。
data1 + data2;
operator+(data1,data2);
上述两个是等价调用。
因为重载运算符其实是函数调用,逻辑与、逻辑或、和逗号的运算对象求值顺序规则无法保留下来,因此,不应该被重载。
还有一个原因是的我们不重载逗号和取址运算符:c++语言已经定义了这两种运算符用于类类型对象时的特殊含义,所以一般不重载,否则它们的行为将异于常态,从而导致类用户无法适应。
赋值运算,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。
成员函数还是非成员函数:
赋值(=)、下标([ ])、调用( ( ) )和成员访问箭头(->)运算符必须是成员函数。
复合运算符一般来说应该是成员,但非必须,只一点与赋值运算略有不同。
改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是 成员。
具有对称性的运算符可能转换任意一端的运算对象,例如算术、想等性、关系和位运算符等,因此他们通常应该是普通非成员函数。
如果我们想提供含有类对象的混合型表达式,则运算符必须定义成非成员函数。如int和double相加。
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
输出运算符的第一个参数是一个非常量ostream对象的引用,因为写入内容会改变其状态,是引用因为我们无法直接复制一个ostream对象。
第二个参数一般是一个常量的引用,该常量使我们想要打印的类类型。引用是因为我们希望避免复制实参。常量是不会改变内容。
为了与其他保持一致一般返回ostream形参
ostream &operator<<(ostream &os , const Sales_data &item)
{
os<<item.isbn() <<" "<<item.units_sold<< " " <<item.revenue <<" "<<item.avg_price();
return os;
}
输出运算符应该尽量减少格式操作,可以使用户来控制输出细节。不应该打印换行符
与iostream标准库兼容的输入输出运算符必须是普通的分成员函数
输入运算符的第一个参数是运算符将要读取的流额引用,第二个参数是将要读入到的( 非常量 )对象的引用,通常会返回某个给定流的引用。
istream &operator>>(istream &is , Sales_data &item)
{
double price;
is >> item.bookNo >>item.units_sold >>price;
if(is)
item.revenue = item.units_sold * price;
else
item = Sales_data( );
return is;
}
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
在执行输入运算时可能发生以下错误:
当流含有错误类型的数据时读取操作可能失败。
当读取操作到达文件末尾或者遇到输入流的其他错误是也会失败。
当读取操作发生错误时,输入运算符应该负责从错误中恢复。
虽然我们可以使用failbit,eofbit,badbit等错误标示,但最好是让标准库来标示错误。
算术和关系运算符定义成非成员函数,以允许左右的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量引用。
如果定义了算术运算符,则一般也会定义一个对应的复合运算符。 符合允许符一般来说应该是成员,但非必须,只一点与赋值运算略有不同。
如果类同时定义了算术运算符合相关的复合赋值运算符,则通常情况下应该使用复合赋值运算符来实现算术运算符。
Sales_data operator+(const Sales_data &lhs , const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}
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 != (cosnt Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
如果某个类在逻辑上有相等的含义,则应该定义operator == 。
特别因为关联容器和一些算法要用到小于运算符,所欲定义operator<会比较有用。
如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类同时还包含 == ,这当且仅当<的定义和==产生的救国一致是才定义<运算符。
除了拷贝赋值和移动赋值,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。例如vector,定义了第三中赋值运算符,元素列表作为参数。
vector<sring> v;
v = {"a" , "an" , "the" };
为StrVec添加这个运算符。
class StrVec{
public:
StrVec &operator = (std::initializer_list<std::string>);
};
StrVec& StrVec::operator = (std::initializer_list<std::string>)
{
auto data = http://www.mamicode.com/alloc_n_copy(il.begin() , il.end() );
free( );
elements = data.first;
first_free = cap =data.second;
return *this;
}
赋值运算符必须是成员函数。
复合赋值运算符不非得是类的成员, 不过我们还是倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部。与标准库一致,返回其左侧运算对象的引用
Sales_data& Sales_data::operator +=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
下标运算符必须是成员函数。
为了与原始定义兼容。下标返回的是访问元素的引用。这样做的好处是下标可以出现在赋值运算符的任意一端。进一步,我们最好定义下标运算的常量版本和非常量版本,当作用一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。
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];}
private:
std::string *elements;
};
在迭代器类中通常有递增和递减运算符,不必须是成员函数,但因为它们改变的正好是所操作对象的状态,所以建议使用成员函数。
为了与内置版本一致,前置返回的是递增后的对象的引用。后值饭会的是对象的原值,返回的形式是一个值而非引用。
//前置版本
class StrBlobPtr{
public:
StrBlobPtr& operator++ ( );
StrBlobPtr& operator-- ( );
};
StrBlobPtr& StrBlobPtr::operator++()
{
//如果curr已经指向容器尾后位置,则无法递增它
check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}
}
StrBlobPtr& StrBlobPtr::operator--()
{
//如果curr是0,则继续递减它将产生一个无效下标
--curr; //如果curr已经是0,那么我们传递个check的值将是一个表示无效下标的非常大的正整数
check(curr , "decrement past begin of StrBlobPtr");
return *this;
}
为了区分前置和后置版本,后置版本接受一个额外的int类型的形参。只是为了区分前置和后置的函数
class StrBlobPtr{
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
};
StrBlobPtr StrBlobPtr::operator++()
{
// 此处无效检查有效性,调用前置递增运算时才需要检查。
StrBlobPtr ret = *this;
++*this;
return ret;
}
StrBlobPtr StrBlobPtr::operator--(int)
{
StrBlobPtr ret = *this;
--*this;
return ret;
}
我们的后置版本是通过前置版本完成工作的。因为我们不会用到int形参,所以无需为其命名。
显示调用:
StrBlobPtr p(al);
p.operator++(0); ///后置版本
p.operator++();//前置版本
在迭代器类和智能指针类中常常用到解引用运算符(*)和箭头运算符(->)
class StrBlobPtr{
public:
std::string& operator* ( )const
{
auto p = check(curr,"dereference past end");
return (*p)[curr];
}
std::string* operator->( ) const
{
return & this->operator* ( );
}
};
箭头运算符必须是类的成员。解引用运算符通常也是类的成员函数,尽管非必须。
返回值分别是非常量string的引用或指针,因为一个StrBlobPtr智能绑定非常量的StrBlob对象(构造函数接受非const)
和大多数其他运算符一样,我们可以令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;
char sep;
};
函数对象常常用作泛型算法的实参。
for_each(vs.begin() , vs.end() , PrintString(cerr, ‘\n‘));
for_each 的第三个参数是类型PrintString的一个临时对象,其中我们用cerr和换行符初始化该对象。当程序调用for_each时,将会把vs中的没个元素打印到cerr中,元素之间以换行符分隔。
当我们编写了一个lambda表达式后,编译器将该表达式翻译成以个未命名的对象。lambda表达式中含有一个重载的函数调运运算符。
默认情况下,lambda表达式不能改变他捕获的变量,因此默认情况,由lambda产生的类中的函数调用运算符是以个const成员函数。如果被声明成可变的,则调用运算符就不是const了
捕获的变量被拷贝到lambda表达式中,因此,这种lambda表达式产生的类必须为每个值捕获变量建立对应的数据成员,同时建立构造函数。令其使用捕获的变量的值类初始化数据成员。如:
auto wc = find_if(words.begin(),words.end(),[sz](const string &a){ return a.size( ) >=sz;});
该lambda表达式产生的类是:
class SizeComp{
SizeComp(size_t n):sz(n) { }
bool operator( ) (const string &s) const
{ return s.size() >= sz; }
private:
size_t sz;
};
auto wc = find_if(words.begin( ) ,words.end( ) , SizeComp(sz));
lambda表达式产生的类不含有默认构造函数、赋值运算符以及默认析构函数;它是否含有默认拷贝/移动构造函数则通常要是捕获的数据类型而定。
标准库定义的函数对象:都在functional头文件中。都是模板,应该是plus<Type>,下面只给出名字
算术:plus,minus ,multiplies, divides, modulus, negate
关系:equal_to, not_equal_to, greater, greater_equal, less, less_equal
逻辑:logical_and,logical_or,logicla_not
在算法中使用标准库的函数对象:
vector<string * > nameTable;
sort(nameTable.begin(), nameTable.end(), [ ](string *a , string *b) { return a< b;}); //错误,指针之间么有关系,将产生未定义。
sort(nameTable.begin(),nameTable.end() , less<string*>());//正确
关联容器使用less<key_type>对元素排序, 因此我们可以定义一个指针的set或者在map中使用指针作为关键值而无需直接声明less
c++的可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
和其他对象一样,可调用对象也有类型。
不同的类型可能具有相同的调用形式。
对于几个可调用对象共享同一个调用形式的情况,有时我们会希望把他们看成具有相同的类型。
我们可以使用function来解决这个问题,在functional头文件中。
function的操作:
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只有一个实参,则agreement_type是该类型的同义词;如果有两个实参,则
first_argument_type 和 second_argument_type 分别代表两个实参的类型。
function是一个模板,需要提供额外的信息,function类型能够表示的对象的调用形式。
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 ; };
map<string , function<int(int , int)>> binops = {
{"+",add},
{"-",std::minus<int>()},
{"/",divide()},
{"*",[ ] (int i, int j){ return i* j ; }},
{"%",mod}
};
binops["+"](10,5);
我们不能(直接)将重载函数名字存入function类型的对象中,解决二义性,一种途径是使用函数指针,一种是用lambda表达式
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} ); //错误:哪个add?
int (*fp)(int,int) = add;
binops.inset( {"+" , fp});
binops.insert({"+" , [ ](int a , int b){ return add(a,b);} });
转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时候也被称作用户定义的类类型转换。
负责将一个类类型装换成其他类型。
operator type( ) const;
类型转换运算符可以面向任意类型(void除外)进行定义,只要该类型能作为函数的返回类型。因此,我们不允许转换成数组或者函数,但允许转换成指针(包括数组指针和函数指针)或引用。
类型转换运算符既没有显式的放回类型,也没有形参,而且必须是成员函数。而且一般是const成员。
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;
};
编译器一次只能执行一个用户定义的类型转换。但是隐式的用户定义的类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。类型转换运算符是隐式执行的。
编译器一次只能执行一个用户定义的类型转换。但是隐式的用户定义的类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。类型转换运算符是隐式执行的。
通常,类很少提供类型转换运算符。在大多数情况下,如果自动转换,可能会让用户感到很意外。然而有个例外:对于类来说,定义向bool的类型转换还是比较普遍的现象。
在早期版本中如果定义一个bool的类型转换。则它常常遇到一个问题:
int i = 42;
cin<<i;
这段代码试图将输出运算符作用于输入流。因为istream本身没有定义<< 所以本来代码会产生错误。然而,改代码能使用istream的bool类型装换运算符将cin转换成bool,而这个bool接着会被提升成int并用作内置的左移运算符的左侧运算对象。
显式的类类型转换运算符:
class SmallInt{
public:
explicit operator int( ) const{ return val;}
};
SmallInt si = 3;
si + 3; //错误
static_cost<int>(si) + 3; //正确
当类型转换运算符是显式的,我们也能执行类型转换,不过必须通过显式的强制类型转换才可以。但是该规则存在一个例外,当表达式被用作条件,则编译器会将显式的类型转换自动应用于它:
if、while及do语句,for语句头的条件表达式,逻辑运算符的运算对象,条件运算符的条件表达式。
在早起版本,IO定义了想void*的转换规则,以求避免转换过度。但在新标准,IO这定义的是explicit的转换
向bool的类型转换通常用在条件部分,因此 operator bool 一般定义成explicit
如果类中包含一个或多个类型转换,则必须确保在类类型和目的类型之间只存在唯一一种转换方式。否则将很有可能产生二义性。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标的算术类型的转换。
在一个例子中,我们定义了两中将B转换成A的方法:以中使用B的类型转换运算符、另一种使用A的以B为参数的构造函数
另外如果定义了一组类型转换,他们的转换源(或转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题。最简单也是最困扰我们的例子就是类中定义了多个参数都是算术类型,的构造函数,或目标是。
struct A{
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
void f(long double );
A a;
f(a)//;二义性
上面的转换之所以有二义性,根本原因是他们所需的标准类型转换级别一致。当我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程
short
//把short提升成int优先于把short转换成double
a a3(s); //使用 A::A(int);
除了显示地向bool类型的转换之外,我们应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显示构造函数、
如果在调用重载函数时我们需要使用构造函数或者强制类型转换类改变实参的类型,则这通常意味着程序的设计存在不足。
在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
重载函数必须使用的是同一个类型转换。而不是去考虑转换的优先级。
重载运算符也是重载函数。因此,通用的函数匹配规则同样使用。不过对于重载运算符我们无法区分成员还是非成员还是内置版本。除此之外,如果左侧运算对象时类类型,则定义在该类中的运算符的重载版本也包含在候选函数内。
如果我们队同一个类其提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算与内置运算符的二义性问题。
如使用 + 是用重载版本 还是将一边进行转换。
C++11(13):重载运算与类型转换
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。