第14章 重载运算符与类型转换
14.1 基本概念
只有当操作的含义对于用户来说清晰明了时才使用运算符。
选择作为成员还是非成员?
赋值、下标、调用和成员访问运算符必须是成员。
复合赋值运算符一般是成员。
改变对象状态或者与给定类型密切相关的,如递增、解引用通常是成员。
具有对称性的运算符可能转换任意一端元素的运算对象,例如算数、相等、关系和位运算等,通常是非成员函数。
14.2 输入输出运算符(略)
14.3 算术与关系运算符(略)
14.4 赋值运算符(略)
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;
}
StrVec svec(10);
svec[0] = “zero”;
14.6 递增和递减运算符
区分前置和后置运算符
后置定义时加上(int)
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
后置运算符应该返回对象的原值。
解释了有哪些是生成右值?
14.7 成员访问运算符(略)
14.8 函数调用运算符
如果类定义了调用运算符,则该类的对象称为函数对象。
实验:函数调用运算符必须是非静态成员函数
函数调用运算符适用于对象的名称,而不是函数的名称。 |
14.8.1 lambda是函数对象
当我们编写一个lambda后,编译器将该表达式翻译成一个未命名的类的未命名对象。该类中包含一个重载的函数调用运算符。
14.8.2 标准库定义的函数对象
plus<Type>
less<Type>
equal_to<Type>
在算法中使用标准库函数对象:
比如
vector<string> svec;
sort(svec.begin(), svec.end(), greater<string>());
14.8.3 可调用对象与function
普通函数
int add(int i, int j) {return i + j;}
lambda
auto mod = [] (int i, int j) {return i%j};
函数对象类
struct divide {
int operater()(int denominator, int divisor) {
return denominator /divisor;
}
};
标准库function类型:function<int(int,int)> 它表示接受两个int、返回一个int的可调用对象。
function<int(int,int)> f1 = add;
function<int(int,int)> f2 = divide();
function<int(int,int)> f3 = [](int j,int i){return j*i;};
5种可调用对象,完善函数指纹的不能完成的
map<string, function<int(int,int)>>binops = {
{“+”, add},
{“-”, std::minus<int>()},
{“/”, divide()},
{“*”, [](int i, int j){return i*j;}},
{“%”, mod}
};
14.9 重载、类型转换与运算符
显示的类型转换运算符
为了防止异常情况的发生,C++11引入了显示的类型转换运算符(explicit conversion operator)
class SmallInt {
public:
SmallInt(int i =0):val (i)
{
}
explicit operator int() const { return val;}
private:
std::size_t val;
};
当表达式如下时,显示的类型转换将被隐式地执行:
if、while、do、for语句的条件表达式、逻辑运算、条件运算符
第15章 面向对象程序设计
15.1 OOP:概述
面向对象程序设计的核心思想:数据抽象、继承和动态绑定。
数据抽象:可以将类的接口和实现分离;
继承:相似性的类、相似性关系建模;
动态绑定:忽略相似性类的区别,以统一的方式使用对象。
虚函数:希望它的派生类各自定义适合自己的版本。
class Quote{
public:
string isbn() const;
virtual double net_price(std::size_t n) const;
};
class Bulk_quote: public Quote{
public:
double net_price(std::size_t n) const override;
};
动态绑定(运行时绑定):当我们使用基类的引用或指针调用一个虚函数时将发送动态绑定。
每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口。
防止继承的发生
class NoDerived final {};
15.2 定义基类和派生类
类型转换:
从派生类向基类的转换只对指针或引用类型有效;
基类向派生类不存在隐式类型转换(不怕出问题的显示强制转换是可以的);
15.3 虚函数
基类希望其派生类进行覆盖的函数。
override和final的作用
15.4 抽象基类
纯虚函数:无需定义。函数声明处最后=0。
class exp{
public:
double net_price(std::size_t) const = 0;
};
含有(或者未经覆盖直接继承)纯虚函数的类称为抽象基类。抽象基类负责定义接口。不能创建抽象基类的对象。
15.5 访问控制和继承
每个类负责控制自己的成员的访问控制。有元关系不能传递,也不能继承。
struct D1:Base{}; // 默认public继承
class D2:Base{}; //默认private继承
struct和class唯一区别:默认成员访问说明符和默认派生访问说明符。
15.6 继承中的类作用域
在编译时进行名字查找。
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义的基类中的名字。
名字查找先于类型检查。如果派生类的成员和基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。所以要么重载所有基类的某类函数,要么一个也不覆盖。
15.7 构造函数和拷贝控制
派生类中的创建、拷贝、移动、赋值、销毁。
15.7.1 虚析构函数
将基类的析构函数设置为虚函数,确保当我们delete基类指针时将运行正确的析构函数。
15.7.2 合成拷贝控制与继承
基类因为定义了析构函数而不能拥有合成的移动操作,因此当我们移动基类时,实际运行的是基类的拷贝操作。基类没有移动,意味着派生类也没有移动操作。
但是可以在基类中显示的定义移动、拷贝操作。
15.7.3 派生类的拷贝控制成员
派生类的析构函数只负责派生类自己分配的资源。派生类的构造和移动需要负责包括基类部分成员部分。
class Base{};
Base &Base::operator=(const Base&){
//...
return *this;
};
class D:public Base{
public:
D(const D& d):Base(d){};
D(&& d):Base(std::move(d)){};
};
D &D::operater=(const D&rhs)
{
Base::operater=(rhs);
//...
return *this;
}
如果想在派生类中拷贝和移动基类部分,则必须在派生类的构造函数初始值列表中显示的使用基类的拷贝构造函数。
15.8 容器与继承
当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。
解决办法:在容器中放置(智能)指针而非对象。
例子:
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>(“123-3344”,2));
basket.push_back(make_shared<Bulk_quote>(“234-344”,50));