C++Primer_Chap14_操作重载和类型转换_笔记

基本概念

  重载运算符函数的参数数量和该运算符作用的运算对象数量一样多。对于二院运算符,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

  如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上。因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个

  对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。(即,不能对内置的运算符重定义)

运算符
可以被重载的运算符
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete[]
不能重载的运算符
  :: .* . ?:  

直接调用一个重载的运算符函数

  一个非成员运算符函数的等价调用:

data1 + data2;
operator+(data1, data2);

  成员函数运算符函数的登记调用:

data1 += data2;
data1.operator+=(data2);

某些运算符不应该被重载

  某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以关于运算对象求值顺序的规则无法应用到重载的运算符上。特别是:逻辑与运算符(&)、逻辑或运算符(|)和逗号运算符(,)的运算对象求值顺序规则无法保留下来。除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总会被求值。

通常,不应该重载逗号(,)、取地址(&)、逻辑与(&)和逻辑或(|)运算符。

使用与内置类型一致的含义

  如果某些操作在逻辑上与运算符相关,则他们适合定义成重载的运算符:

  • 如果类执行IO操作,则定义移位运算符(<<和>>)使其与内置类型的IO保持一致
  • 如果类的某个操作具有检查相等性,则定义operator==;如果类有了operator==,通常也应该有operator!=
  • 如果类包含一个内在的单序比较操作,则定义operator<;如果类有了operator<,通常也应该有operator>
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和符合赋值运算符应该返回左侧运算符对象的一个引用 。

选择作为成员或者非成员

  • 赋值( = )、下标( [] )、调用( () )和成员访问箭头( -> )运算符必须是成员
  • 复合赋值运算符一般来说应该是成员,但并非必须。这一点与赋值运算符略有不同。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减、或解引用运算符,通常应该是成员
  • 具有对称性的运算符可以转换任任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

  在含有混合类型的表达式中使用对称性运算符(例如:对int和double使用对称性运算符+)。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义为非成员函数。

  当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。

string s = "world";
string t = s + "!";    //正确
string u = "hi" + s;    //如果+是string的成员,产生错误

输入和输出运算符

重载输出运算符<<

  通常,输出运算符的第一个形参是一个非常量ostream对象的引用,第二个形参一般来说是一个常量的引用(避免复制),为了与其他输出运算符保持一致,operator<<一般返回它的ostream形参。

  输出运算符尽量减少格式。 

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

输入输出运算符必须是非成员函数

    与iostream标准库兼容的输入输入运算符必须是普通的非成员函数,而不能是类的成员函数。否则,他们的左侧运算对象将是我们的类的一个对象

Sales_data data;
data << cout;    //如果operator<<是Sales_data的成员

重载输入运算符>>

std::istream& operator>>( std::istream &is, const Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    if(is)
	    item.revenue = price * item.units_sold;
    else
        item = Sales_data();
    return is;
}

  输入运算符必须处理输入可能失败的情况,而输出运算符不需要。在执行输入运算符时可能发生下列错误:、

  • 当流含错误类型的数据时读取操作可能失败。例如在读取完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);
}

关系运算符

  • 定义顺序关系,令其与关联容器中对关键字的要求一致
  • 如果类同时也含有==运算符,则定义一种关系令其与==一致。特别是,如果两个对象是!=的,那个一个对象应该<另一个

  如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。

赋值运算符

StrVec &StrVec::operator=( initializer_list i1)
{
	auto data = alloc_n_copy(i1.begin(), i1.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;
}

下标运算符

  表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]。下标运算符都必须定义为成员函数。如果一个类包含下标运算符,则它通常会定义两个版本:

  1. 返回普通引用
  2. 是类的常量成员函数,返回常量引用
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++()
{
    check( curr, "increment past end of StrBlobPtr");
    ++curr;
    return *this;
} 

StrBlobPtr& StrBlobPtr::operator--()
{
    --curr;
    check( curr, "decrement past begin of StrBlobPtr");
    return *this;
}

区分前置和后置运算符

  为了解决同时定义前置和后置运算符时,普通的重载形式无法区分的情况,后置版本接受一个额外的(不被使用)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。该形参唯一作用就是区分前置版本和后置版本的函数。

class StrBlobPtr
{
public:
    StrBlobPtr& operator++(int);
    StrBlobPtr&	operator--(int);

}; 

StrBlobPtr& StrBlobPtr::operator++(int)
{
    StrBlobPtr ret = *this;
    ++*this;
    return *this;
} 

StrBlobPtr& StrBlobPtr::operator--(int)
{
    StrBlobPtr ret = *this;
    --*this;
    return ret;
}

显式调用后置运算符

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*();
	}
		
}; 

对箭头运算符返回值的限定

  箭头运算符永远不能丢掉成员访问这个基本的含义。当我们重载箭头时,可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一事实永远不变。以下三行代码等价:

point->mem;
(*point).mem;
point.operator->()->mem;

  除此之外,代码都将发生错误。

函数调用运算符

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

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

int i = -42;
absInt absObj;
int ui = absObj(i);

 函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量和类型上有所区别。如果类定义了调用运算符,则该类的对象称作函数对象.

含有状态的函数对象类

  

class PrintString{
public:
    PrintString( ostream &os = cout, char c=' '):
	    os(0), sep(c){}
    void operator()(const string &s) const {os << s << seq;}
private:
    ostream &os;
    char sep;
}; 

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

lambda是函数对象

  当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;是否含有默认的拷贝/移动构造函数通常视捕获的数据成员类型而定。

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

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));

标准库定义的函数对象

  标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。

标准库函数对象
算术 关系 逻辑
plus equal_to

logical_and

minus not_equal_to

logical_or

multiplies greater logical_not
divides greater_equal  
modulus less  
negate less_equal  
plus intAdd;         //可执行int加法的函数对象
negate intNegate;    //可对int值取反的函数对象
//使用intAdd::operator(int, int)求10和20的和
int sum = intAdd(10, 20);
sum = intNegate(intAdd(10,20));

在算法中使用标准库函数对象

  表示运算符的函数对象类常用来提货算法中的默认运算符。默认情况下排序算法使用operator<将序列按照升序排列。

vector nameTable;
//错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
sort( nameTable.begin(), nameTable.end(),
        [](string *a, string *b){ return a < b; });
//正确:标准库规定指针的less是定义良好的
sort( nameTable.begin(), nameTable.end(), less());

可调用对象和function

  C++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重在了函数运算符的类

int add(int i, int j){ return i + j; }
auto mod = [](int i, int j){ return i%j;}
struct divide{
    int operator()(int denominator, int divisor){
        return denominator/ divisor;
    }
};

map

不能将mod和divide存入binops

标准function类型

function的操作
function f; f是一个用来存储可调用对象的function,这些可调用对象的调用形式应该与函数类型T相同(即T是retType(args))
function f(nullptr); 显式的构造一个空function
function f(obj) 在f中存储可调用对象obj的副本

f

将f作为条件:当f含有一个可调用对象时为真,否则为假
f(args) 调用f中的对象,参数是args
定义为function ​​​​​​​的成员的类型
result_type 该function类型的可调用对象返回的类型

argument_type

first_argument_type

second_argument_type

当T有一个或两个实参时定义的类型。

如果T只有一个实参,则argument_type是该类型的同义词

如果T有两个实参,则first_argument_type和second_argument_type分别代表两个实参的类型

  function是一个模板,定义在functional头文件

function< int( int, int) > f1 = add;
function< int( int, int) > f2 = divide();
function< int( int, int) > f3 = [](int i, int j)
                                    {return i*j;};

//列举了可调用对象与二元运算符对于关系的表格
//所有可调用对象都必须接受两个int,返回一个int
//其中的元素可以是函数指针、函数对象或者lambda
map< string, function< int( int, int)>> binops ={
    {"+", add},    //函数指针
    {"-", std::minus()},    //标准库函数对象
    {"/", divide()},    //用户定义的函数对象
    {"*", [](int i, int j){ return i*j;}},    //为命名lambda
    {"%", mod}         //命名了的lambda对象
};

binops["+"](10, 5);
binops["-"](10, 5);
binops["/"](10, 5);
binops["*"](10, 5);
binops["%"](10, 5);

重载函数和function

  不能(直接)将重载函数的名字存入function类型的对象中,因为会存在二义性。解决的一条途径是存储函数指针而非函数名字。同样,也能使用lambda来消除二义性

重载、类型转换和运算符

类型转换运算符

  类型转换运算符时类的一种特殊成员函数,它负责将一个类类型的值转换成其他的类型:

operator type() const;

  其中type表示某种类型。类型转换运算符可以面向任何类型(void除外)进行定义,只要该类型能作为函数的返回类型。因此不允许转换成数组或函数类型,但可以是指针或者引用类型。

  类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数。

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

  尽管编译器一次只执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用。因此,我们可以将任何算术类型传递给SmallInt的构造函数。类似的,我们也能使用 类型转换运算符将一个SmallInt对象转换成int,再将所得的int转换成任何其他的算术类型。

SmallInt si = 3.14;    //double->int->SmallInt::SmallInt(int)
si + 3.14;            //SmallInt->int->double

  因为类型转换运算符时隐式执行的,所以无法给这些函数传递实参,也不能在类型转换运算符的定义中使用任何实参。同时,尽管类型转换函数不负责制定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。

class SmallInt;
operator int(SmallInt&);    //错误:非成员函数
class SmallInt{
public:
    int operator int() const;    //错误:指定了返回类型
    operator int(int = 0) const;    //错误:参数列表不为空
    operator int*() const { return 42;}    //错误:42不是指针
};

类型转换运算符可能产生意外结果

  在实践中,类很少提供类型转换运算符。例外是定义向bool类型转换的类型转换运算符,但由于bool是算术类型,所以类类型的对象转换成bool后就能被用在任何需要算术类型的上下文中,可能导致意外的结果。下面代码,cin将被转换成bool,然后变成int,被左移42位。

int i = 42;
cin << i;

显示的类型转换运算符

    为了避免上述的异常发生,C++11新标准引入了显示的类型转换运算符(explicit conversion operator)

class SmallInt{
public:
    SmallInt( int i = 0) : val(i)
    {
        if( i<0 || i>255)
            throw std::out_of_range("Bad SmallInt value");
    }
    explicit operator int() const { return val;}
private:
    std:size_t val;
}; 

SmallInt si = 3;    //正确
si + 3;            //错误:此处需要隐式的类型转换,但类的运算符时显式的
static_cast(si) + 3;    //正确:显示地请求类型转换

  表达不用作条件时,编译器会要求显式的强制类型转换才可以。做条件的时候,显式的类型转换将被隐式地执行:

  • if、while及do语句的条件部分
  • for语句头条件表达式
  • 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
  • 条件运算符(?:)的条件表达式

向bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的

避免有二义性的类型转换

  如果一个类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。在两种情况下可能产生多重转换路径。

  1. 两个类提供相同的类型转换。例如,A类定义了一个接受B类对象的转换构造函数,同时B类定义了一个转换目标是A类的类型转换运算符使,我们称其提供了相同的类型转换
  2. 类定义了多个转换规则,而这些转换涉及到的类型本身可以通过其他类型转换联系在一起。最典型的的是算术运算符。

  通常,要避免上面两种情况。二义性情况1示例与调用:

struct B;
struct A{
    A() = default;
    A(const B&);
};

struct B{
    operator A() const;
};

A f(const A&);
B b;
A a= f(b);    //二义性错误:f(B::operator A())
              //OR  f(A::A(const B&);


A a1 = f(b.operator A());
A a2 = f(A(b));

二义性与转换目标为内置类型的多重类型转换

  二义性情况2示例:

struct A{
    A(int = 0);
    A(double);
    operator int() const;
    operator double() const;
};

void f2(long double);
A a;
f2(a);    //二义性错误:f2(A::operator int())
          //OR    f2(A::operator double())

long lg;
A a2(lg);    //二义性错误:A::A(int)  OR     A::A(double)?

重载函数和转换构造函数

  如果两个或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。

  如果两个或多个用户定义的类型转换都提供了可行匹配,则这些类型转换一样好。

struct C{
    C(int);
};
struct D{
    D(int);
};
struct E{
    E(double);
};

void manip(const C&);
void manip(const D&);
void manip(const E&);
manip(10);        //二义性错误:manip(C(10))、manip(D(10))、manip(E(10))

函数匹配和重载运算符

  和普通函数调用不同,不能通过调用的形式来区分当前调用的是成员函数还是非成员函数:

a.operatorsym(b);
operatorsym(a, b);

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

  当我们调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载,因为调用命名函数的语法形式对于成员函数和非成员函数来说是不同的。但当我们在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数。

class SmallInt{
    friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
    SmallInt(int = 0);
    operator int() const { return val;}
private:
    std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2;    //使用重载的operator+
int i = s3 + 0;    //错误:二义性

如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,就会遇到重载运算符与内置运算符的二义性问题。

你可能感兴趣的:(C++)