目录
14.1 基本概念
- 重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和后面要定义的运算符号共同组成。和其他函数一样,也有返回类型,参数列表和函数体。
- 重载运算符函数的参数数量与该运算符作用的运算对象一样多。一元运算符有一个参数,对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。
- 当一个重载的运算符是成员函数时,this绑定到左侧的对象,成员运算符函数的参数比运算对象的参数少一个。比如+重载成成员函数,那么它的参数只能有一个,默认类对象this本身是一个参数。
- 对于一个运算符函数来说,他或者是类的成员,或者至少含有一个类类型的参数。
- 不能重载的运算符 : :: .* . ?:
- 不应该被重载的运算符:逻辑运算符,逗号运算符,取地址运算符或运算符。
- 有的运算符必须作为成员;另一些情况下,运算符作为普通函数比作为成员更好,方便记忆:可以看一个运算符左右两侧的对象是否可以互换位置,不能互换位置则一般为成员函数,可以互换位置则一般为非成员函数
- 成员函数:
(1)=,[ ],( ), ->
(2)复合赋值+=,-=… ,
(3)++,–,*(解引用) 改变对象的状态或与给定类型密切相关的运算符 - 非成员函数
(1)算术运算符:+,-,*,/…(混合类型表达式的计算)
(2)相等性:== ,!=
(3)关系运算符:>,<,>=,<=…
(4)位运算符: ^, |, &
14.2 输入和输出运算符
- 输出运算符应定义为非成员函数,因为要读写类的非共有数据,所以要定义为友元。
- 且返回值为std::ostream&, 参数为std::ostream&和类对象的常量引用。
- 注意:通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符,我们应减少对输出格式的控制。
- 输入运算符和输出运算符格式上类似,也是非成员函数,返回输入流引用(流不能拷贝),参数是输入流引用和类对象,和输出运算符不同的是,输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
- 当流含有错误类型的数据时读取操作可能失败,读取数据到文件末尾或遇到其他流错误也会失败。不需要逐个检查,只在末尾检查即可,当读取操作发生错误时,输入运算符应该负责从错误中恢复。一些输入运算符可能需要更多的数据验证工作。
14.3 算术和关系运算符
- 我们把算数和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换,一般不需要改变参数状态,所以都是常量引用。
- 如果定义了算数运算符,则他一般也会定义一个对应的复合赋值运算符,最有效的方式是使用复合赋值来定义算数运算符。
- 如果类同时定义了算数运算符和相关的赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
13.3.1 相等运算符
- 如果类定义了operator==,那么类也应该定义operator!=。
- 智能指针的比较问题
- shared_ptr 和 unique_ptr 都用get( )返回的指针来比较是否指向同一个对象
- weak_ptr 要先使用lock( )获取shared_ptr 然后在用get( )来比较地址从而判定是否指向同一个对象。
14.3.2 关系运算符
- 定义了相等运算符的类通常也应该定义关系运算符,因为关联容器和一些算法要用到小于运算符。所以operator<会比较有用。
- 意味这不要轻易定义<运算符,如果<和==比较的逻辑相同(也就是比较的成员相同)才定义<运算符。
14.4 赋值运算符
- 和拷贝赋值运算符及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前内存空间,不同之处是无需检查自赋值。
- 类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。
class StrVec {
StrVec &operator=(std::initializer_list<tsd::string>);
// 其他成员一致
}
StrVec &StrVec::operator=(std::initializer_list<tsd::string> il){
// allloc_n_copy分配内存空间并从给定范围内拷贝数据
auto data = alloc_n_copy(il.begin(), il.end());
free(); // 这里记得释放内存
elements = data.first();
first_free = cap = data.second;
return *this;
}
14.5 下标运算符
- 下标运算符通常以所访问元素的引用作为返回值。可以作为左值或右值
- 我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会修改返回值。
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; // 指向数组首元素的指针
}
14.6 递增和递减运算符
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符应该被定义为类的成员。
- 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。区分前置运算符和后置运算符:后置版本提供一个额外的不被使用的int类型的参数,使用后置运算符时,编译器为这个形参提供一个值为0的实参。这个形参的唯一作用就是区分前置版本和后置版本。
- 为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非一个引用。对于后置版本来说,在递增或递减之前首先需要记录对象的状态。
- 后置版本里可以调用前置版本。
- 前置版本在递增之前要判断是否到达末尾,前置版本递减要在递减之后判断是否出界。
- 如果我们要通过函数调用的方式调用后置版本,则必须为他整型参数传递一个值,尽管我们不使用这个值。
14.7 成员访问运算符
- 解引用运算符检查是否在范围内,然后返回所指元素的一个引用,箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果的地址。
- 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
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类(注意模版)
#ifndef _STRING_H_
#define _STRING_H_
#include <memory>
#include <iostream>
#include <cstring>
class String
{
friend std::ostream& operator<<(std::ostream &os, const String &s);
friend bool operator<(const String &lhs, const String &rhs);
friend bool operator==(const String &lhs, const String &rhs);
public:
String():st(nullptr) { }
String(const char *s);
String(const String &s);
String& operator=(const String &s);
char& operator[](std::size_t n);
const char& operator[](std::size_t n)const;
~String();
private:
char* alloc_n_copy(const char *s);
void free();
static std::allocator<char> alloc; // 声明一个静态数据成员用于分配字符空间
char *st;
};
#endif
#include "string.h"
char* String::alloc_n_copy(const char *s) // 这里用到了动态分配内存的方法(有什么好处?)
{
st = alloc.allocate(strlen(s)+1); // 为st分配内存空间
char *p = st;
int i = 0;
while(s[i] != '\0')
{
alloc.construct(p++, s[i++]);
}
alloc.construct(p, s[i]);
return st;
}
void String::free()
{
char *p = st;
size_t num = strlen(p)+1;
while(*p != '\0')
{
alloc.destroy(p++); // 将内存空间中的对象都销毁
}
alloc.destroy(p);
alloc.deallocate(st, num); // 将内存空间销毁
}
String::String(const char *s)
{
st = alloc_n_copy(s);
}
String::String(const String &s) // 拷贝构造函数
{
st = alloc_n_copy(s.st);
}
String& String::operator=(const String &s) // 拷贝赋值运算符
{
if(st != s.st) // 注意判断是否相等
{
if(st)
{
free(); // 先释放内存
}
alloc_n_copy(s.st);
}
return *this;
}
String::~String()
{
if(st)
{
free();
}
}
std::ostream& operator<<(std::ostream &os, const String &s)
{
os << s.st;
return os;
}
bool operator<(const String &lhs, const String &rhs)
{
std::cout << __func__ << std::endl; // 只是所在函数
if(strcmp(lhs.st, rhs.st) < 0)
return true;
else
return false;
}
bool operator==(const String &lhs, const String &rhs)
{
std::cout << __func__ << std::endl;
if(strcmp(lhs.st, rhs.st) == 0)
return true;
else
return false;
}
char& String::operator[](std::size_t n)
{
return st[n];
}
const char& String::operator[](std::size_t n)const
{
return st[n];
}
#include "string.h"
#include <set>
std::allocator<char> String::alloc;
int main()
{
String s1("123");
String s2("456");
String s3("789");
String s4("123");
std::set<String>st;
st.insert(s2);
st.insert(s1);
st.insert(s3);
st.insert(s4);
for(const String &s : st)
{
std::cout << s << std::endl;
}
std::cout << s1[1] << std::endl;
}
14.8 函数调用运算符
- 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象,因为这个类同时也能存储状态,所以与普通函数相比更加具有灵活性。
#include <iostream>
struct absInt
{
int operator()(int val)const
{
return val < 0 ? -val : val;
}
};
int main()
{
int val = -42;
absInt t;
std::cout << t(val) << std::endl; //t是一个对象而非函数
}
- 函数调用运算符必须是成员函数。一个类可以员定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
- 函数对象类除了operator()之外也可以包含其他成员,通常函数对象类包含一些数据成员,这些成员用来定制调用运算符中的操作。
#include <iostream>
#include <string>
class PrintString
{
public:
PrintString(std::ostream &o = std::cout, char t = ' '):
os(o), c(t) { }
void operator()(const std::string &s)const //借用辅助工具来完成函数的操作
{
os << s << c;
}
private: //private成员可以用来保存“辅助”工具
std::ostream &os;
char c;
};
int main()
{
PrintString ps;
std::string s = "abc";
ps(s);
}
- 函数对象比一般函数灵活就是它可以让另外完成函数所需要的辅助成员成为自己的类成员。和lambda类似
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'));
#include <iostream>
#include <vector>
#include <algorithm>
// 编写一个类令其检查两个值是否相等,令其替换某序列中给定值的所有实例
class cmp
{
public:
cmp(int i = 0):
t(i) { }
bool operator()(int a)
{
return a == t;
}
private:
int t;
};
int main()
{
std::vector<int>ivec = {1,2,1,2,1,2,1,2,1,2};
cmp cp(2);
//最好不要在for_each里面使用类成员函数!
std::for_each(ivec.begin(), ivec.end(), [&](int &i){ if(cp(i)) i = 100; });
for(const int i : ivec)
std::cout << i << std::endl;
}
14.8.1 lambda是函数对象
- 当定义一个lambda时,编译器生成一个与lambda对应的新的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象、其实当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符。
[](const string &lhs, const string &rhs)
{ return lhs.size() < rhs.size(); }
// 等价于
class shorterString
{
public:
bool operator()(const string &lhs, const string &rhs)const
{ return lhs.size() < rhs.size(); }
};
// 根据单词的长度对其进行排序,对于单词长度相同的单词按照字典序排序
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size(); });
// 等价于
stable_sort(words,begin(), words.end(), shorterString());
- 当一个lambda通过引用捕获变量时,将由程序负责确保lambda执行时引用所引用的对象的确存在,因此,编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员。
- 当一个lambad通过值将变量拷贝到lambda时,lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量来初始化数据成员。
- lambda产生的类不含默认构造函数,赋值运算符及默认析构函数:它是否含有默认的拷贝/移动构造函数则通常视捕获的数据成员类型而定。
14.8.2 标准库定义的函数对象
- 因为有些谓词函数规定是一个参数,而我们有时需要传递两个或多个参数这时就可以使用bind或lambda。
- 定义在头文件functional中
#include <functional>
#include <iostream>
int main()
{
//算数
std::cout << "算数" << std::endl;
int a = 10, b = 20;
int ret;
std::plus<int>add; //加法
ret = add(a, b);
std::cout << ret << std::endl;
std::minus<int>min; //减法
ret = min(a, b);
std::cout << ret << std::endl;
std::multiplies<int>mp; //乘法
ret = mp(a, b);
std::cout << ret << std::endl;
std::divides<int>divi; //除法
ret = divi(a, b);
std::cout << ret << std::endl;
std::modulus<int>mod; //求余
ret = mod(a, b);
std::cout << ret << std::endl;
std::negate<int>neg; //相反数
ret = neg(a);
std::cout << ret << std::endl;
//关系
std::cout << "关系" << std::endl;
int a2 = 10, b2 = 20;
int ret2;
std::equal_to<int>et; //是否相等
ret2 = et(a2, b2);
std::cout << ret2 << std::endl;
std::not_equal_to<int>net; //不等
ret2 = net(a2, b2);
std::cout << ret2 << std::endl;
std::greater<int>gt; //大于
ret2 = gt(a2, b2);
std::cout << ret2 << std::endl;
std::greater_equal<int>gte; //大于等于
ret2 = gte(a2, b2);
std::cout << ret2 << std::endl;
std::less<int>ls; //小于
ret2 = ls(a2, b2);
std::cout << ret2 << std::endl;
std::less_equal<int>lel; //小于等于
ret2 = lel(a2, b2);
std::cout << ret2 << std::endl;
//逻辑
int a3 = 10, b3 = 20;
int ret3;
std::cout << "逻辑" << std::endl;
std::logical_and<int>la; //and
ret3 = la(a3, b3);
std::cout << ret3 << std::endl;
std::logical_or<int>lo; //or
ret3 = lo(a3, b3);
std::cout << ret3 << std::endl;
std::logical_not<int>ln; //not
ret3 = ln(a3);
std::cout << ret3 << std::endl;
}
- 在算法中使用标准库函数对象
sort(svec.begin(), svec.end(), std::greater<std::string>());
sort(svec.begin(), svec.end(), std::less<std::string *>());
#include <vector>
#include <iostream>
#include <functional>
#include <algorithm>
// 统计大于10的值有多少个
int main()
{
std::vector<int>ivec = {1,22,3,44,5,66,7,88,9,100};
sort(ivec.begin(), ivec.end(), std::greater<int>()); //排序
for(int i : ivec)
std::cout << i << " ";
std::cout << std::endl;
//auto p = find_if(ivec.begin(), ivec.end(), [](int &a) { return a < 10; }); //查找第一个满足条件的下标
//std::cout << "result:" << p - ivec.begin() << std::endl;
int i = 10;
//auto num = std::count_if(ivec.begin(), ivec.end(), bind2nd(std::greater<int>(), i)); //通过bind1st和bind2nd处理
//auto num = std::count_if(ivec.begin(), ivec.end(), bind1st(std::greater<int>(), i));
//auto num = std::count_if(ivec.begin(), ivec.end(), bind(std::greater<int>(), std::placeholders::_1, i)); //通过bind来处理
//不管绑定第一个参数还是第二个参数都是绑定一个,所以是_1
auto num = std::count_if(ivec.begin(), ivec.end(), bind(std::greater<int>(), i, std::placeholders::_1)); // 只传入一个参数然后和i进行比较
std::cout << num << std::endl;
}
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
#include <string>
// 找到第一个不等于pooh的字符串
int main()
{
std::string s = "pooh";
std::vector<std::string>svec = {"pooh","pooh","pooh","p","pooh"};
//不管绑定第一个还是第二个参数,s的位置都在那
//auto p = std::find_if(svec.begin(), svec.end(), std::bind1st(s,std::not_equal_to<std::string>()));//error s在后面位置,1st只是表名绑定的是第一个参数
auto p = std::find_if(svec.begin(), svec.end(), std::bind2nd(std::not_equal_to<std::string>(), s)); //绑定了第二个参数
std::cout << *p << std::endl;
}
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
// 将所有值乘2
using namespace std::placeholders;
int main()
{
std::vector<int>ivec = {1,2,3,4,5,6,7,8,9,0};
std::vector<int>ivec2(ivec.size());
std::transform(ivec.begin(), ivec.end(), ivec2.begin(), bind(std::multiplies<int>(), _1, 2)); //transform 谓词函数处理后放到新的位置
for(const int i : ivec2)
std::cout << i << std::endl;
}
14.8.3 可调用对象与function
- c++中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象,以及函数对象(重载了函数调用运算符的类)。
- 和其他对象一样,可调用的对象也有类型。然而两个不同的可调用对象确有可能享用同一种调用形式。但实际操作比如我们向vector<int(*)(int, int)>来保存他们是不行的,只能保存第一个,因为他们毕竟是不同的对象!
int add(int a, int b) { return a+b; }
auto t = [](int a, int b) { return a+b; }
class A
{
int operator()(int a, int b)
{
return a+b;
}
}
// 类型都是 int(int, int)
map<string, int(*)(int, int)> binops;
binops.insert({"+", add}); // 正确,add是一个指向函数的指针
binops.insert({"%", mod}); // 错位,mod不是一个函数指针
- 我们可以使用一个名为function的新的标准库类型来解决上面的问题。头文件 #include <functional>
#include <iostream>
#include <functional>
#include <vector>
#include <map>
#include <string>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
class multip
{
public:
int operator()(int a, int b)
{
return a * b;
}
};
int fun(int a, int b, int c)
{
return a + b + c;
}
int main()
{
auto div = [](int a, int b) { return a / b; };
int (*p)(int, int) = sub;
//function<T>f;
//function<T>f(nullptr);
//function<T>f(obj);
//f 判断f里是否含有一个条件
//f(args) 调用
std::function<int(int, int)> func1 = add; //函数
std::function<int(int, int)> func2 = p; //函数指针
std::function<int(int, int)> func3 = multip(); //函数对象
std::function<int(int, int)> func4 = div; //lambda
std::cout << div(1,2) << std::endl;
std::map<std::string, std::function<int(int, int)>>cal;
cal.insert({"+", func1});
cal.insert({"-", func2});
cal.insert({"*", func3});
cal.insert({"/", func4});
std::string s;
std::cin >> s;
std::cout << cal[s](10,5) << std::endl;
std::function<int(int, int, int)> func6 = fun;
- 当我们有函数重载对象时,不能直接将相同的函数名字放入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
14.9 重载、类型转换与运算符
- 我们能定义类类型之间的转换,转换构造函数和类型转换运算符共同定义了类类型转换。
14.9.1 类型转换运算符
- 是类的一种特殊的成员函数,负责将一个类类型的值转换成其他类型。不允许转换成数组或者函数类型,但允许转换成指针(数组指针和函数指针)或者引用类型。
- 一个类型转换函数必须是类的成员函数,它不能声明返回类型,也没有形参,类型转换函数通常为const
- 注意:应该避免过度的使用类型转换函数。类型转换函数可能产生意外结果。
operator type( )const;
SmallInt si;
si = 4; // 首先将4隐式的转换成SmallInt,然后调用SmallInt::operator=
si + 3; // 首先将si隐式的转换成int,然后执行整数加法
SmallInt si = 3.14; // 调用SmallInt(int)构造函数
si + 3.14; // 内置类型转换将得到的int继续转换成double
- 显示类型的数据转换
explicit operator type( ) const { };
static_cast<type>(name);
- 但是存在一个例外:既当如果表达式被用作条件,则编译器会将显示的类型转换自动应用于它。包括while, if, do, for语句头条件表达式,(!, ||, &&)的运算对象, (? :)条件表达式
- 向bool的类型转换通常用在条件部分,因此operator bool 一般定义为explicit 的。
14.9.2 避免二义性的类型转换
- 如果一个类中包含一个或多个类型转换,则必须确保在类类型和目标类型转换之间只存在唯一一种转换方式。否则我们的代码可能会存在二义性。
- 我们无法用强制类型转换来解决二义性,因为强制类型转换也面临着二义性。通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上的转换源或转换目标是算术类型的转换。
operator int( )const // 不要创建两个转换对象都是算术类型的类型转换
operator double( )const
A(int = 0); // 不要创建两个转换源都是算术类型的类型转换(转换构造函数)
A(double);
- 不要让两个类执行相同的类型转换,比如A转换B的同时B也转换为A。
- 避免转换目标是内置算数类型的类型转换。
- 如果我们对一个类既提供了转换目标是算数类型的类型转换,也提供了重载的运算符,则会遇到重载运算符与内置运算符二义性的问题。
14.9.3 函数匹配与重载运算符
- 如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载运算符,则将会遇到重载运算符与内置运算符的二义性问题。