重载运算与类型转换

文章目录
  1. 1. 基本概念
    1. 1.1. 直接调用一个重载的运算符函数
    2. 1.2. 某些运算符不能被重载
    3. 1.3. 使用内置类型一致的含义
    4. 1.4. 赋值和符合赋值运算符
    5. 1.5. 选择作为成员或者非成员
  2. 2. 输入和输出运算符
  3. 3. 算术和关系运算符
    1. 3.1. 相等运算符
    2. 3.2. 关系运算符
  4. 4. 赋值运算符
    1. 4.1. 复合赋值运算符
  5. 5. 下标运算符
  6. 6. 递增和递减运算符
  7. 7. 成员访问运算符
  8. 8. 函数调用运算符
    1. 8.1. lambda是函数对象
      1. 8.1.1. 表示lambda及相应捕获行为的类
    2. 8.2. 标准库定义的函数对象
      1. 8.2.1. 在算法中使用标准函数对象
    3. 8.3. 可调用对象和function
      1. 8.3.1. 不同类型可能具有相同的调用形式
      2. 8.3.2. 标准库function类型
  9. 9. 重载、类型转换、运算符
    1. 9.1. 类型转换运算符
      1. 9.1.1. 类型转换运算符可能产生意外结果
      2. 9.1.2. 显示类型转换
      3. 9.1.3. 转化为bool
    2. 9.2. 避免有二义性的类型转换
    3. 9.3. 函数匹配和重载运算符
  1. 基本概念
  2. 输入输出运算符
  3. 算术和关系运算符
  4. 赋值运算符
  5. 下标运算符
  6. 递增和递减运算符
  7. 成员访问运算符
  8. 函数调用运算符
  9. 重载、类型转换、运算符

当运算符被用于类类型时,C++语言允许为其指定新的含义。同时我们能自定义类类型之间的转化规则。

基本概念

重载运算符是具有特殊名字的函数:他们的名字由关键字operator和其后要定义的运算符共同组成。和其他函数一样,重载运算符也包含返回类型、参数列表、以及函数体。除了重载的函数调用运算符operator()之外,其他的重载运算符不能还有默认实参。

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

对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。

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

1
2
3
4
5
data1+data2;
operator+(data1,data2); ///非成员函数

data1+=data2;
data1.operator+=(data2) ///成员运算符函数的直接调用

某些运算符不能被重载

某些运算符定义了求值顺序,因为重载的运算符本质上是函数调用,所以不能应用求值顺序的规则,所以这些运算符不能被重载。例如逻辑与,逻辑或的短路求值顺序。 逗号表达式和取地址运算符也不能被重载,因为已经定义了作用域类类型的含义。

使用内置类型一致的含义

只有当操作的含义对用户来说,清晰明了了时才使用运算符。

赋值和符合赋值运算符

有算法运算符或者位运算符,则最后提供赋值运算符

选择作为成员或者非成员

  1. 赋值、下标、调用、成员访问必须是成员函数
  2. 改变对象状态的运算符或者密切相关的运算符,如递增、递减、解引用,通常定义为成员
  3. 具有对称的运算符可能转化为任意一端的运算符对象,如算术、相等、关系和位运算符 通常应该定义为非成员函数。

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

输入和输出运算符

通常情况下,输出运算符的第一个形参是一个非常量ostream对象引用,之所以ostream是非非常,因为向流写入内容会改变流的状态。而形参是引用是因为我们无法直接复制一个一个ostream对象。

第二个形参一般是一个常量引用,引用避免复制,常量表示不修改对象

1
2
3
4
ostream &operator<<(ostream &os, const Sales_data &item)
{
return os;
}

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

重载输入运算符

1
2
3
4
istream & operator>>(istream& is, Salses_data)
{
return is;
}

算术和关系运算符

通常情况下,我们把算术和关系运算符定义为非成员函数以允许左侧和右侧的运算对象进行转换。因为这些运算对象一般不需要改变运算对象的状态,所以形参都是常量引用

如果类同时定义了算法运算符和相关的复合赋值运算符,则,通常情况下应该使用复合赋值来实现算术运算符。

相等运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
///加法运算符
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum+= rhs;
return sum;
}

///相等运算符
bool operater==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.units_sold == rhs.units.sold;
}

相等运算符和不等运算符中的一个应该把工作委托给另外一个。

关系运算符

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

赋值运算符

  1. 拷贝赋值
  2. 移动赋值
  3. 初始值列表赋值
1
2
3
4
5
6
7
8
9
10
v = {"a","an","the"};

StrVec & operator=(initializer_list<string> il)
{
auto data = alloc_n_copy(il.begin(),il.end());
free();
elemests = data.first;
fisrs_free = cap = data.second;
return *this;
}

复合赋值运算符

赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这样做,这两类运算符都应该返回左侧运算对象的引用。

下标运算符

下标运算符必须定义成成员函数,下标运算符通常以访问元素的引用作为返回值,这样的好处是下标运算符可以出现在赋值运算符的任意一端。

如果一个类包含下标运算符符,则通常会定义两个版本,一个返回普通引用,另一个是类的常量成员,并返回常量引用。

1
2
3
4
5
6
7
8
9
std::string&& operator[](std::size_t n)
{
return elements[n];
}

const std::string&& operator[](std::size_t n) const
{
return elements[n];
}

递增和递减运算符

递增和递减运算符改变所操作对象的状态,所以建议将其定义为成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 前置递增版本
StrBolbPtr & StrBlobPtr::operator++()
{
check(curr,"increment past end of StrBlobPtr");
++cur;
return *this;
}

///后置版本
StrBolbPtr & StrBlobPtr::operator++(int)
{
StrBolbPtr ret = *this;
++*this;
return ret;
}

为了区分前置和后置版本,后置运算符接受一个额外的(不被使用的)int类型参数。后置版本调用了前置版本运算符。

成员访问运算符

  1. 解引用运算符 *
  2. 箭头运算符 ->
1
2
3
4
5
6
7
8
9
10
std::string& operator*() const
{
auto p = check(curr,"");
return (*p)[curr];
}

std::string* operator*() const
{
return &this->operator*();
}

箭头运算符必须是类的成员。解引用通常也是类的成员。

函数调用运算符

如果一个类重载了函数调用运算符,则我们可以像使用函数一样使用该类对象。因为这样的类能存储状态,所以比普通函数相比,更加灵活。

函数调用运算符必须是成员函数。一个类可以定义多个版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

如果类定义了调用运算符,则该类对象称为函数对象(function object)

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

lambda是函数对象

当我们编写一个lambda后,编译器将表达式翻译成一个未命名的对象。

1
2
3
4
5
6
7
8
9
10
11
12
stable_sort(words.begin(), words.end(),[](const string &a, const string &b)){return a.size()<b.size();});

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

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

默认情况下,lambda不能改变捕获的变量。因此默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。 如果lambda是可变的,则调用运算符就不是const的了

表示lambda及相应捕获行为的类

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引用的对象确实存在。通过值捕获的变量,必须为类建立对应的数据成员。同时创建构造函数。

lambda表达式产生的类不含有默认构造函数、赋值运算符、默认析构函数。

标准库定义的函数对象

  1. plus类定义了一个函数调用运算符用于对运算对象执行+操作
  2. modules类定义了一个调用运算符执行二元的%操作
  3. equal_to类执行==
算术
plus<Type>
minus<Type>
multiplies<Type>
divides<Type>
modulus<Type>
negate<Type>
关系
qual_to<Type>
no_equal_to<Type>
greater<Type>
greater_equal<Type>
less<Type>
less_qual<Type>
逻辑
logic_and<Type>
logic_or<Type>
logic_not<Type>

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

表示运算符的函数对象通常用来替换算法中的默认运算符。

可调用对象和function

可调用对象:

  1. 函数
  2. 函数指针
  3. lambda表达式
  4. bind创建的对象
  5. 重载了调用运算符符的类

不同类型可能具有相同的调用形式

调用形式指明了调用返回的类型以及传递给调用的类型参数。

lambda表达式有他自己的类型,虽然调用形式一样,但是类型不同。

标准库function类型

function的操作 说明
function f f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同 T是returntype(args)
function f(nullptr) 显示的构造一个空function
function(obj) f中存储可调用对象obj的副本
f 将f作为条件,当f包含一个可调用对象是为真,否则为假
f(args) 调用f中的对象,参数是args
定义为function的成员类型
result_type
argument_type
first_argument_type
second_argument_type

重载、类型转换、运算符

由一个实参调用的非显式构造函数定义了一种隐式类型转换,这种构造函数将实参类型的对象转换为类类型。我们同样能定义对于类类型的类型转换,通过定义类型转换运算符可以做到这一点。转换构造函数和类型转换运算符共同定义了类型转换。

类型转换运算符

1
2
3
4
operator type() const
{
return val;
}

一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为空,类型转换函数通常应该const。

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

对于类来说,定义向bool的类型转换还是比较普遍的现象。

显示类型转换

1
2
3
4
5
explicit operator int() const 
{
return val;
}
static_cast<int>(si) + 3;

当表达式出现在下列位置时,显示的类型转化将被隐式的执行:

  1. if、for、do 语句的条件
  2. for语句的条件表达式
  3. 逻辑非、逻辑或、逻辑与的运算对象
  4. 条件运算符

转化为bool

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

避免有二义性的类型转换

如果类中有一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式,否则的话,我们编写的代码将很可能具有二义性。

  1. 例如A接受B的构造函数
  2. B定义了一个转换目标是A的转化运算符

对于某个给定的类来说,最好只定义最多一个算术类型相关的转化规则。

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

在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型时才有用,如果所需的用户类型不止一个,则该调用具有二义性。

函数匹配和重载运算符

在表达式运算符的候选函数既应该包括成员函数,也应该包括成员函数。

普通的函数就不需要这样,因为他们的调用形式不同。