前言
该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。
本文大致整理了第十四章的知识点,涉及到C++关于运算符重载和类型转换的知识,内容比较多和琐碎,有一定难度!
链接目录
- 第二章:变量与基本类型
- 第三章:字符串、向量和数组
- 第四章:表达式
- 第五章:语句
- 第六章:函数
- 第七章:类
- 第八章:IO库
- 第九章:顺序容器
- 第十章:泛型算法
- 第十一章:关联容器
- 第十二章:动态内存
- 第十三章:拷贝控制
- 第十四章:重载运算与类型转换
- 第十五章:面向对象程序设计
- 第十六章:模板与泛型编程
- 第十七章:标准库特殊设施
- 第十八章:用于大型程序的工具
- 第十九章:特殊工具与技术
基本概念
二元运算符:左侧对象传递给第一个参数,右侧对象传递给第二个参数。
重载运算符不能有默认实参。
如果运算符函数是成员函数,则左侧对象绑定到隐式的this
指针上,所以成员运算符函数参数数量会少一个。
不能重载内置类型的运算符。
例如:int operator+(int, int);//错误,不能重定义内置的运算符
直接调用重载运算符函数:
data1 + data2;
operator+(data1, data2);//等价
data1 += data2;
data1.operator+=(data2);//等价
一般不建议重载逻辑与、或、逗号运算符和取地址运算符,会影响到求值顺序。
重载运算符函数应该与内置类型的含义一致:
- 类执行IO操作,移位运算符应该与IO保持一致
- 定义相等性操作,通常也应该定义
!=
操作 - 定义比较操作,也应该定义其他关系操作
- 重载运算符的返回类型通常应该与内置版本的返回类型兼容(逻辑和关系运算符返回
bool
,算术运算符返回类类型的值,赋值运算符和复合运算符应该返回左侧对象的一个引用)
选择非成员或者成员:
- 赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)必须是成员 - 复合赋值运算符一般来说应该是成员,但并非必须
- 改变对象状态的运算符,递增、递减和解引用一般应该是成员
- 具有对称性的运算符,例如算术、相等性、关系和尾运算符,应该是非成员
//应当定义为非成员,等价于operator(s, "!")或operator("!", s)
string s = "hello";
string t = s + "!";//正确
//等价于s.operator+("!")
string u = "!" + s;//错误,+如果是string的成员,则无法正常执行
//等价于"!".operator+(s)
重载输出运算符
输出运算符的第一个形参是非常量的ostream
对象的引用,第二个形参是一个常量(通常)的引用
Sales_data
的输出运算符:
ostream &operator<<(ostream &os, const Sales_data &item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
输入输出运算符必须是非成员函数:否则左侧运算符无法是iostream
类型,通常也需要对非公数据进行读写,所以IO运算符一般要声明为友元。
重载输入运算符
第一个形参是将要读取的流的引用,第二个形参是将要读入的对象的引用。
Sales_data
的输入运算符:
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;
}
输入时的错误:IO错误状态
- 当流含有错误类型的数据时,读取操作可能失败。例如读取完
bookNo
后,输入运算符假定接下来读入两个数字数据,如果输入的不是,就会失败。 - 当读取操作到达文件末尾或遇到输入流的其他错误时也会失败。
相等运算符
通常将算术和关系运算符定义成非成员函数。
设计准则:
- 通常情况下相等运算符应该具有传递性,如果
a==b
,b==c
,那么应该a==c
- 如果定义了
==
,也应该定义!=
- 相等运算和不等运算中的一个应该把工作委托给另一个,例如编写了相等运算,则不等运算调用相等运算,然后结果取非。
关系运算符
关系运算符应该:
- 定义顺序关系
- 如果类包含
==
运算符,则应该保证如果两个对象是!=
的,应当一个对象<
另外一个
赋值运算符
//列表初始化
class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
};
StrVec &StrVec::operator=(initializer_list<string> il){
auto data = 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;
}
s1 += s2;
下标运算符
为了和原始定义兼容,通常下标返回一个引用,最好也同时定义常量版本和非常量版本(作用于一个常量对象,返回常量引用确保不会改动对象)。
class StrVec{
public:
string& operator[](size_t n){
return elements[n];
}
const string& operator[](size_t n) const{
return elements[n];
}
private:
string *elements;
};
关于const
的重载,当const
对象调用该函数时,调用const
重载版本的函数,虽然const
对象也能调用非const
版本的函数,但const
版本更加匹配,优先级更高。
递增和递减运算符
因为++
和--
通常是一元运算符,只有一个对象,但改变的对象是所操作的对象,所以建议仍然设定为成员函数。
定义前置递增/递减运算符:
class StrBlobPtr{
public:
StrBlobPtr& operator++();
StrBlobPtr& operator--();
};
StrBlobPtr& StrBlobPtr::operator++(){
check(curr, "...");//检查下标合法性
++curr;
return *this;
}
StrBlobPtr& StrBlobPtr::operator--(){
--curr;
check(curr, "...");//检查下标合法性
return *this;
}
区分前置和后置运算符:参数列表的int必不可少,用来区分前置和后置,虽然函数中用不到。
//后置版本返回的是值,而非引用
class StrBlobPtr{
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
};
StrBlobPtr StrBlobPtr::operator++(int){
StrBlobPtr ret = *this;//记录当前值,临时量
++*this;//调用前置版本
return ret;
}
StrBlobPtr StrBlobPtr::operator--(int){
StrBlobPtr ret = *this;
--*this;//调用前置版本
return ret;
}
成员访问运算符
class StrBlobPtr{
public:
string& operator*()const{
auto p = check(curr, "...");//检查下标合法性
return(*p)[curr];
}
string* operator->()const{
return & this->operator*();//返回解引用的结果
}
}
对箭头运算符返回值的限定:
point->mem
,point
必须是指向类的指针或一个重载了->
操作的类的对象,根据类型不同,point->mem
分别等价于:
(*point).mem
,point
是一个内置的指针类型point.operator()->mem
,point
是类的一个对象
箭头运算符执行过程:
- 如果
point
是指针,应用内置的箭头运算符。 - 如果
point
是定义了箭头运算符的类的对象,则箭头运算符返回的是一个指针,然后执行第一步,如果结果本身也重载了箭头运算符,则重复步骤,直到返回所需的内容。
函数调用运算符
函数调用运算符必须是成员函数
struct absInt{
//返回绝对值
int operator()(int val) const{
return val > 0 ? -val : val;
}
};
int i = -42;
absInt absObj;
int ui = absObj(i);//像调用函数一样使用对象
含有状态的函数对象类:函数对象通常作为泛型算法的实参,例如for_each
。
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;
};
PrintString printer;
printer(s);//在cout中打印s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s);//在cerr中打印s,后面跟一个换行符
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
lambda是函数对象
编写lambda
表达式时,编译器会翻译成一个未命名类的未命名对象,含有一个重载的函数调用运算符。
lambda
中的数据成员:lambda
捕获和返回
- 通过引用捕获的对象,无需在类内存储为数据成员
- 通过值捕获的对象,每个值需要建立对应的数据成员,同时创建构造函数,利用值捕获的变量初始化数据成员
标准库定义的函数对象
在算法中使用标准库函数对象:
sort(svec.begin(), svec.end(), greater<string>());
可调用对象与function
可调用对象:
- 函数
- 函数指针
lambda
表达式bind
创建的对象- 重载了函数调用运算符的类
可调用对象也有类型,不同类型的可调用对象可能共享调用形式
//以下三种都共享同一种调用形式:int(int, int)
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<string, int(*)(int, int)> binops;
binops.insert({"+", add});//加入一个pair
//但不能存入mod或divide,二者都有自己的类型,并不是单纯的函数指针
标准库function
类型:定义在functional
头文件中
function<int(int, int)> f1 = add;//函数指针
function<int(int, int)> f2 = divede();//函数对象
function<int(int, int)> f3 = [](int i, int j){
return i * j;
};//lambda
cout << f1(4, 2) << endl;
cout << f2(4, 2) << endl;
cout << f3(4, 2) << endl;
//通过function统一起函数指针和函数对象,可以存入map等数据结构
map<string, function<int(int, int)>> binops = {
{"+", add},
{"-", std::minus<int>()},
{"/", divede()},
{"*", [](int i, int j){return i*j}},
{"%", mod}
};
binops["+"](10, 5);//调用add(10, 5)
重载的函数与function
:不能将重载函数的名字存入function
类型的对象中,否则会产生二义性
int add(int, int);
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add});//无法确定哪一个add
//解决方式
//1.函数指针
int (*fp)(int, int) = add;//显式说明是哪一个版本
binops.insert({"+", fp});
//2.通过lambda表达式告知是哪一个版本
binops.insert({"+", [](int a, int b){return add(a, b);}});
类型转换运算符
operator type() const
:type
表示类型(该类型能作为返回类型),不允许转换数组或函数类型,但可以转换成指针或引用类型。
定义含有类型转换运算符的类:
class SmallInt{
public:
SmallInt(int i = 0) : val(i){
if(i < 0 || i > 255)
throw std::out_of_range("Bad value");
}
//类型转换为int
operator int() const{
return val;
}
private:
std::size_t val;
};
SmallInt si;
si = 4;//将4隐式转换成SmallInt
si + 3;//将si隐式转换成int
SmallInt si = 3.14;//调用SamllInt(int)构造函数
si + 3.14;//si转换成int,再转成double
显式的类型转换运算符:为了防止程序中不经意的隐式类型转换产生难以预测的结果,引入类型转换运算符。
class SmallInt{
public:
explicit operator int() const {return val;}
...
private:
std::size_t val;
};
SmallInt si = 3;
si + 3;//错误,此处存在隐式的类型转换,但运算符要求是显式的
static_cast<int>(si) + 3;//正确
类型转换是显式时,仍有例外会执行隐式的类型转换(表达式用作条件):
if
、while
和do
语句的条件部分for
语句头的条件表达式- 逻辑与(
&&
)、或(||
)、非(!
)的运算对象 - 条件运算符(
?:
)的条件表达式
向bool
的类型转换通常在条件部分,所以operator bool
一般定义成explicit
,这样在一般的表达式中不会隐式转换,在条件表达式中也不需要显式转换
避免有二义性的类型转换
有两种情况可能产生多重转换路径:
A
类接受B
类对象的转换,B
类也接受A
类的转换- 类定义了多个转换规则,这些转换涉及的类型又与其他类型转换联系在一起
通常情况不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。
实参匹配和相同的类型转换:
struct A{
...
A(const B&);
...
};
struct B{
...
operator A() const;
...
};
A f(const A&);
B b;
A a = f(b);//二义性错误,是将b通过类型转换成A还是用A类中的构造函数
//解决二义性,只能显式调用
A a1 = f(b.operator A());
A a2 = f(A(b));
二义性与转换目标为内置类型的多重类型转换:
struct A{
A(int = 0);
A(double);
operator int() const;
operator double() const;
};
void f2(long double);
A a;
f2(a);//二义性错误,是转成int还是double,二者都可转成long double
long lg;
A a2(lg);//二义性错误,是调用int的构造函数还是double的
除了显式地向bool
类型的转换之外,应该尽量避免定义类型转换函数。
重载函数与用户定义的类型转换:
struct C{
C(int);
...
}
struct E{
E(double);
...
};
void f(const C&);
void f(const E&);
f(10);//二义性错误,f(C(10))还是f(E(double(10)))
此时即使存在精确匹配也会产生二义性错误
函数匹配与重载运算符
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;//二义性错误,可以把0转换成SmallInt,也可以把s3转换成int