重载与类型转换

来源:互联网 发布:大数据质量管理 编辑:程序博客网 时间:2024/05/22 13:18

重载:定义类与类之间运算符所要完成的具体工作。

类型转换:定义类与类、类与内置类型之间的转化规则。

一、重载运算概述

1、格式

重载运算符本质上是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。

2、形参数目

重载函数的形参数与其作用的运算对象数量一样多。但是若一个运算符函数是成员函数,则其第一个运算对象绑定到this指针上,因此其形参数比运算对象数少一个。

3、调用形式

我们可以像调用普通函数一样直接调用运算符函数,实现与直接使用运算符相同的功能:

data1+data2;//普通的表达式operator+(data1, data2);//等价的函数调用

如果一个运算符是类的成员函数:

data1+=data2;//基于“调用”的表达式data1.operator+=(data2);//对成员运算符函数的等价调用

4、含义

设计重载函数时,应该使用与内置类型一致的含义。

比如operator+一般将其定义成类的加法操作,而不要将其定义成减法。

5、成员还是非成员

在设计重载运算符之间,需要选择其作为类的成员函数还是普通函数。

a)、赋值(=)、下标([  ])、调用((  ))和成员访问箭头(->)运算符必须是成员。

b)、复合赋值运算符(+=,-=)一般来说应该是成员,但不是必须的。

c)、改变对象状态的运算符通常应该是成员,如递增(++)、递减(--)和解引用(*)。

d)、具有对称性质的运算符通常应该是普通的非成员函数,如算术、相等性、关系和位运算符。

二、输入输出运算符

输入输出运算符必须是非成员函数,但由于IO运算符通常需要读写类的私有成员,所以一般声明为友元。

1、输出运算符重载函数operator<<

输出运算符第一个形参是一个非常量ostream对象的引用,第二个形参是一个常量的引用,返回值的是它ostream形参。

Sales_data的输出运算符:

ostream &operator<< (ostream &os, const Sales_data &item){os<< item.isbn() <<  " " <<item.units_sold<< " " <<item.revenue<<  " " << item.avg_price();return os; }

2、输入运算符重载函数operator>>

输入运算符的第一次形参是运算符将要读取的流的引用,第二形参是将要读入到的对象的引用,返回值是某个给定流的引用。

Sales_data的输入运算符:

istream &operator>>(istream &is, Sales_data &item){double price;is >> item.bookNo >> item.units_sold >>price;if (is)//检查输入是否成功item.revenue = item.unites_sold * price;elseitem = Sales_data();//输入失败,对象被赋予默认的状态return is;}

三、算术、关系运算符

算术和关系运算符通常定义为非成员函数,从而可以对左侧或右侧的运算对象进行转换。

算术、关系运算符一般不会改变输入变量的值,因此其形参是常量的引用。

运算结果一般位于一个局部变量之内,操作完成后返回该局部变量的副本作为其结果。

1、相等运算符operator==

通常来说,我们使用operator==来检验两个对象是否相等,我们通过比较对象中的每个元素的值来得到结果。

如果类定义了operator==,则这个类也应该定义operator!=。

2、关系运算符operator<

因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用。

定义的关系运算符必须满足两个条件:

a)、定义顺序关系,令其与关联容器中对关键字的要求一致。

b)、如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。特别是,如果两个对象是!=的,那么一个对象应该<另外一个。

四、赋值运算符operator=

通过重载赋值号=,我们可以用其他类型对象向特定类的对象赋值。

赋值运算符必须为类的成员函数。

与内置类型的赋值运算符一致,新的赋值运算符也将返回其左侧运算对象的引用(*this)。

StrVec &StrVec::operator=(initializer_list<string> il){// alloc_n_copy分配内存空间并从给定范围内拷贝元素auto data = alloc_n_copy(il.begin(), il.end());free();//销毁对象中的元素并释放内存空间elements = data.first;//更新数据成员使其指向新空间first_free = cap = data.second;return *this;}

五、下标运算符operator[ ]

与下标的原始定义兼容,下标运算符以所访问元素的引用作为返回值。

下标运算符一般定义一个常量版本和一个非常量版本,其中常量版本的返回值不能修改。

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 *element;//指向数组首元素的指针};

六、递增、递减运算符operator++,operator--

递增、递减运算符改变的是所操作对象的状态,一般将其设定为成员函数。

注意:递增和递减既有前置版本,也有后置版本。

1、前置递增、递减运算符

前置递增、递减运算符返回当前对象的引用。

StrBlobPtr& StrBlobPtr::operator++(){check(curr, "increment past end of StrBlobPtr")++curr;return *this;}

2、后置递增、递减运算符

后置版本的递增、递减运算符接受一个额外的int类型实参。这个形参的唯一作用是区分前置版本和后置版本的函数。

StrBlobPtr StrBlobPtr::operator++(int){StrBlobPtr ret = *this;//记录当前的值++*this;//向前移动一个元素return ret;//返回之前记录的状态}

七、成员访问运算符operator*,operator->

成员访问运算符包括解引用运算符(*)和箭头运算符(->)。

解引用运算符返回所指对象的一个引用。箭头运算符不执行自己的操作,而是调用解引用运算符并返回解引用结果元素的地址。

八、函数调用运算符operator()

1、基本用法

如果一个类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。

struct absInt{int operator() (int val) const{return val <  0 ? -val ; val;}} ;

absInt类定义了函数调用运算符,我们可以将其对象向函数一样调用:

int i = -42;absInt absObj;//含有函数调用运算符的对象int ui = absObj(i);//将i传递给absObj.operator()

2、含有状态的函数对象类

函数对象类除了operator()还可以包含其他成员,这些成员可以自定义函数调用运算符的功能。

class PrintString{public:PrintString (ostream &o cou, char c = ' ' ):os(o), sep(c) { }void operator() {const string &s} const { os<<s<<sep; }//函数调用运算符会用到类的私有成员private:ostream &os;//用于写入的目的流char sep;//用于将不同输出隔开的字符};

当定义PrintString的对象时,对于分隔符及输出流既可以使用默认值,也可以提供我们自己的值:

PrintString printer;//使用默认值,打印到coutprinter(s);//在cout中打印s,后跟一个空格PrintString errors(cerr, '\n');//使用提供的值进行PrintString初始化errors(s);//在cerr(而不是cout)中打印s,后跟一个换行符,而不是空格

函数对象常常作为泛型算法的实参,我们可以在for_each算法中使用PrintString 对象:

for_each(vs.begin(), vs.end(),PrintString(cerr, '\n'));

第三个参数是PrintString的临时变量,调用for_each时,会将vs中的每个元素依次打印到cerr中,并以换行符分隔。

3、lambda是函数对象

a)、普通的lambda表达式与不含状态的函数对象等价

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

其中的lambda表达式的行为类似于下面这个类的一个未命名对象:

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

b)、带捕获参数列表的lambda与含有状态的函数对象类等价

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

该lambda产生的类将形如:

class SizeComp{SizeComp(size_t n): sz(n) {}//构造函数形参对应捕获的变量//该调用运算符的返回类型、形参和函数体都与lambda一致bool operator () (const string &s) const{ return s.size() >= sz; }private:size_t sz;//该数据成员对应通过值捕获的变量}

使用函数对象:

auto wc = find_if (words.begin(), words.end(), SizeComp(sz) );

4、标准库定义的函数对象

头文件functional中定义了一组表示算术运算符、关系运算符和逻辑运算符的类。

我们可以在算法中使用函数对象:

sort(svec.begin(), svec.end(), greater<string>());

将svec按降序排列。其中greater<string>是标准库函数对象,用于在两个string间执行>比较运算。

5、可调用对象与function

C++中共有5种可调用对象:函数、函数指针、lamdba表达式、bind创建的对象以及重载了函数调用运算符的类。

对于不同类型的调用对象可能具有相同的调用形式:

int add(int i, int j) {return i+j;}//普通函数auto mod = [ ](int i, int j) { return i%j;};//lambda表达式struct divide//函数对象类{int operator()(int de, int di){return de/di;}};

以上3个可调用对象都具有int(int,int)的调用形式,C++中,我们可以使用标准function类型来存储可调用对象类:

function<int (int, int)> f1 = add;//函数指针function<int (int, int)> f2 = divide(); //函数对象类的对象function<int (int, int)> f3 = [ ] (int i, int j)//lambda表达式{return i*j;};cout<<f1(4,2)<<endl;//打印6cout<<f2(4,2)<<endl;//打印2cout<<f3(4,2)<<endl;//打印8

九、类型转换运算符

1、概述

类型转换运算符负责将一个类类型转换成其他类型。一般形式:

operator type() const;

其中type表示该对象所要转换成的类型。

类型转换运算符特点:

a)、没有显示的返回类型

b)、没有形参

c)、必须定义成类的成员函数

d)、一般定义成const成员

2、例子

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 si;si = 4;//拷贝赋值,现将4隐式转换为SmallInt类型,然后调用SmallInt::operator=si + 3;//先将si隐式转换成int,然后执行整数的加法

3、抑制隐式类型转换

上述类型转换是隐式完成的,它可能会带来我们所不期望的结果,在C++中,我们可以通过explicit关键字来抑制隐式类型转换:

class SmallInt{public://编译器不会自动执行这一类型转换explicit operator int() const { return val; }//其他与之前版本一致};

与显示构造函数类似,编译器也不会将一个显式的类型转换运算符用于隐式类型转换:

SmallInt si = 3;//正确:SmallInt的构造函数不是显式的si + 3;//错误:此处需要隐式的将si转换成int型,但类的运算符是显式的static_cast<int>(si) + 3;//正确:显式地请求类型转换

4、避免二义性的类型转换

定义类型转换时,要确保在类类型和目标类型之间只存在唯一一种转换方式。

两种情况可以产生多重转换路径:

第一种情况是两个类提供相同的类型转换。

例子:

struct B;struct A{A( ) = default;A(const B&);//A类的构造函数接受B类型对象,把一个B转换成A//其他数据成员};struct B{operator A( ) const;//B类中定义的类型转换,也是把一个B转换成A//其他数据成员};A f(const A&);B b;A a = f(b);//二义性错误:含义是f(B::operator A())//还是f(A::A(const B&))?

这里我们调用f(b)时,要将B类型实参b转换成A类型,但是同时有两种方法可以实现:是调用A类的构造函数,还是调用B类的类型转换函数?

在编译器看来,这两种方法效果相同,不分伯仲,因此会产生二义性错误。

第二种情况是类定义了多个转换规则,而这些类型本身又可以通过其他类型转换联系在一起。

例子:

struct A{A(int = 0);//最好不要创建两个转换源都是算术类型的类型转换A(double);operator int() const;//最好不要创建两个转换对象都是算术类型的类型转换operator double() const;//其他成员};void f2(long double);A a;f2(a);//二义性错误:含义是f(A::operator int())//还是f(A::operator double())?long lg;A a2(lg);//二义性错误:含义是A::A(int)还是A::A(double)?



0 0
原创粉丝点击