2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > [读书笔记]《Hands on Design Patterns with C++》——类 继承 多态 模板

[读书笔记]《Hands on Design Patterns with C++》——类 继承 多态 模板

时间:2020-11-03 16:19:24

相关推荐

[读书笔记]《Hands on Design Patterns with C++》——类 继承 多态 模板

前言

《Hands on Design Patterns with C++》首先这本书不是跟之前的书籍一样只是重点在经典的 23 种“设计模式” 上,这些经典的设计模式当然可以使用 C++ 语言来实现,但是 C++ 强大的地方在于其泛型编程能力。而且设计模式一般是在软件设计中比较有挑战性的场景下, 提出的一些解决方案。一个是问题场景,一个是对应提出的解决方案。 随着时间的推移,一些场景中,会有更好地解决方案被提出,同时呢,也会遇到新的挑战。 本书显式介绍了 C++ 的一些特性,并且结合这些特性以及它们可以解决的问题,对设计加以说明,并且会提到一些利用泛型编程对这些模式的改进。

个人读下来前言,之前大部分的设计模式书籍,都是围绕经典的 GoF 23 种设计模式来的,以此来介绍这些设计模式的思想,并且示意它们在软件结构上的特征。然后这里的要素就是,它们都是在特定场景下,提出的被认可的解决方案。 软件越来越复杂,场景也会越来越复杂,并且语言特性也在不断的发展,结合泛型编程,一些设计模式可以加以改进。更多地是如何将一些设计模式,或者说类似的思想用泛型编程来实现。 所以书中也会介绍一些 c++11 ~ 17 的新特性。

首先是介绍 C++ 中比较一些比较经典的设计模式。 以及一些泛型编程下的新变体。同时兼顾使用一些新的语言特性来实现这些内容。

主要是介绍 how(如何使用) 和 why(为什么有用)。 个人也是阅读的过程中学习作者对 why 的描述,能对背后的原因了解更多一点。 思考的角度受益匪浅。

第一章 继承和多态

What are classes and what is their role in C++?

类主要是包含了数据以及操作这些数据的方法,体现它们之间的关系。类属于用户自定义的数据类型。类的封装是 C++ 的重点。其允许我们限定数据及相关方法的访问权限(public,private)等。

这里对数据和成员函数访问权限的控制,决定了在类外,用户可以“看到” 这个类的哪些内容。(这里当然大家都能理解,但是类的设计也是体现了代码者的设计思想在里面的,我希望暴露什么,隐藏什么。)

public 更像是一种 contract,一旦报错不会随意修改。而 private 的数据和函数则更多地是实现的一部分,只要 public 接口不变,这些内容可以更改的。

What are class hierarchies and how does C++ use inheritance?

类能够继承主要有两个目的,一个是允许我们表达对象之间的关系,另一方面可以让我们用简单的类型组合出更复杂的类型。 子类是在父类基础上的扩展。

类之间有 is a 和 has a 的关系。

继承主要方式有公开继承 Public 和私有继承 private。 公开继承继承的是基类的 public 接口,相当于基类有的 Public 接口,承诺有的功能, public 继承的子类也会自动被这样要求,所以在任何可以使用基类的地方,都应该可以用一个子类来替换。 这里就是表达了 is - a 的概念。一个子类的实例也是一个基类的实例。 当然,有的时候在 C++ 中表达 is a 的实例并不是那么的直观, 例如经典的概念上正方形是一个矩形,但是实现上,更应该用矩形的类类继承正方形的类。 如果一个名为 Bird 的基类有 fly 这样的接口(表示会飞的鸟类),那么就不能让 penguin 企鹅子类继承这个基类,需要一个更加抽象的基类,然后继承两个子类,分别代表会飞的子类,不会飞的子类。这样才更合适。

所以上面的描述,主要是想体现,在 C++ 中,是否可以与概念上的类关系一致,要看我们如何设计基类,更确切的说,如何设计类的 public 接口。

因此子类与父类的实例之间有转换关系。

class Base{};class Derived : public Base {};Base* b = new Derived;Derived* d = b; // wrongDerived* d = static_cast<Derived*>(b); // OK

私有继承则不会继承基类的任何接口,只继承了它的实现,一般都是使用基类的实现来完成子类自己的算法,这种模式是 has - a 的概念,基本与 组合 一致。

What is runtime polymorphism and how is it used in C++?

多态支持同一个接口,根据需求实现不同的功能。通过虚函数来实现多态。另一种表述就是可以通过基类指针访问到子类的内容。

虚函数的 override 是通过函数有相同的参数和返回类型。这里可以注意,当返回类型是引用或者指针的时候,override 可以返回子类的指针。 即下面的代码是可以正常运行的。

class Base {public:virtual Base* fly() {return new Base;}};class Derived : public Base {public:Derived* fly() override{// 这里直接返回 Base* 也是可以的return new Derived;}};Base* b = new Derived;Derived* d = static_cast<Derived*>(b);d->fly();

虚函数的特殊形式就是纯虚函数。一般包含纯虚函数的基类也叫做抽象基类,继承它的子类必须都要实现纯虚函数。不能创建抽象基类的对象,但是可以创建抽象基类的指针,还要通过这个指针实现多态呢。

为了防止继承覆写时,人为因素拼写错误,导致函数覆写失败,加上 c++ override 关键字则编译器会帮忙检查子类虚函数的声明是否与基类一致。

另外基类与子类之间的转换,前面强制转换使用的是 static_cast, 但是这里要求你需要知道正确的子类类型,否则即使代码正常运行,最后得到的结果也可能不是我们想要的。对于有虚函数的基类,可以使用 dynamic_cast,来帮助我们确认基类是否可以转换成我们想要的子类类型,如果不行指针它会返回 Nullptr,引用则会直接报错。

上面介绍的都是子类只继承一个基类,还可以同时继承多个基类,即多重继承,这里仍然以 public 继承做说明。

当多重继承是,基类需要同时满足所有基类的 contract。 当两个基类定义了同名的成员函数,并且子类都没有重新实现这个同名函数,则会编译错误,如果是虚函数并且子类覆写了它,则就是正常的多态行为。

当多重继承时,不同类型的基类也可以通过子类和 dynamic_cast 进行 cross-cast。 但是现在一般 不太建议使用多重继承,主要是很难通过多重继承来设计出一个清晰的关系。

class Base1{};class Base2{};class Derived: public Base1, public Base2{};Base1* b1 = new Derived;Base2* b2 = dynamic_cast<Base2*>(b1); // OK

本章思考题:

What is the importance of objects in C++?What relation is expressed by public inheritance?What relation is expressed by private inheritance?What is a polymorphic object?

第二章 类和函数模板

Templates in C++

C++ 语言的一大优势就是泛型编程。其实现方式就是采用模板来实现了。

Class and function templates

首先模板函数,除了虚函数之外,无论是普通函数还是类的成员函数都可以是模板函数。模板类型 T 不仅可以用来声明函数的参数,还可以在函数体中使用。

template <typename T>T add(T x) {T from = x;return from+1;}

类模板中,模板参数 T 一般是用来声明它的成员变量,也可以声明函数和函数中的局部变量。

template <typename T>class ArrayOf2 {public:T& operator[](size_t i) {return a_[i];}const T& operator[](size_t i) const {return a_[i];}T sum() const {return a_[0] + a_[1];}private:T a_[2];};ArrayOf2<int> i;i[0] = 1; i[1] = -4;auto s = i.sum(); // s == -3

注意这里模板只有我们用到的时候才会实例化相关代码。还是以前面的 ArrayOf2 模板类为例,当 T 为指针时,调用 sum() 就会有问题,但是只要我们不调用,代码就还是安全的。

ArrayOf2<char *> i;char s[] = "Hello";i[0] = s;i[1] = s + 2;// 知道这里代码都是可以正常编译的auto x = i.sum() // 编译错误

前面介绍的模板类型都是表示 数据类型。 C++ 也允许非数据类型的模板参数。

非数据类型可以是整数或者是枚举类型值。这时用来初始化非数据类型的模板参数一定得是编译期常量,或者是 constexpr 常量表达式。例如下面:

template <typename T, size_t N>class IArray{public:T &operator[](size_t i) {if (i > N)throw std::out_of_range("Bad insex");return data_[i];}private:T data_[N];};IArray<int, 5> arr; // OKcin >> arr[0];IArray<int, arr[0]> arr1; // Wrong

非数据类型也可以是模板参数。又叫 template template parameter.

template <typename T>using Deq = std::deque<T, std::allocator<T> >;template <typename T>using Vec = std::vector<T, std::allocator<T> >;template <template <typename> class Out_container,template <typename> class In_container,typename T>Out_container<T> resequence(const In_container<T> &in_container){Out_container<T> out_container;for (auto x : in_container){out_container.push_back(x);}return out_container;}std::vector<int> v{1, 223, 33, 4, 5};auto d = resequence<Deq, Vec>(v); // 注意这里不能直接传 deque 和 vector

参考:/zvideo/1282044919448715264

参考:/wangdamingll/article/details/54019506

Template instantiations 模板实例化

实例化模板函数主要是让模板根据输入的实际数据来推导类型,当推导的类型发生冲突时该如何处理的。

template <typename T>T cmp(T& x, T& y) {return x > y ? x : y;}auto re = cmp(2l, 3); // 冲突,会编译出错

实例化模板类的话,会直接实例化所有的成员变量,但是只有当模板成员函数使用到的时候才会实例化。

Template specializations 模板特化

这里以类模板为例, 假设有这样一个类模板:

template <typename U, typename V>class Ratio {public:Ratio() : num_(), denom_() {}Ratio(const U& num, const V& denom) : num_(num), denom_(denom) {}explicit operator double() const {// 类型转换函数return double(num_) / double(denom_);}private:U num_;V denom_;};

全特化

全特化是把所有的模板参数用特定的类型来替代,这里创建了一个同名的函数模板或者类模板的实例,但是可以覆写(override)里面的实现,所以实现可以完全不一样。 例如下面,对模板类 Ratio 进行了全特化,并且改写了其内部实现:

template<>class Ratio<double, double> {public:Ratio():value_() {}template <typename U, typename V>Ratio(const U& num, const V& denom) : value_(double(num) / double(denom)) {}explicit operator double() const {return value_;}private:double value_;};

粒度可以自己把控,如果大部分代码都是可以复用,只是想重新实现一个成员函数时也可以:

template<>Ratio<double, double>::operator double() const {return demon_/num_; }

偏特化

即只确定模板参数的一部分,保留一部分泛型,并且同样的可以在内部覆写前面的实现。注意这里会存在一个可能会实例化失败的情况,还是以 Ratio 模板类为例,其中可以偏特化 U

template <typename V>class Ratio<double, V> {// 偏特化 Upublic:Ratio() : value_() {}Ratio(const double& num, const V& denom) : value_(num / double(denom)) {}explicit operator double() {return value_;}private:double value_;};

Ratio<double, double> r; 这时会优先匹配上面的偏特化版本,因为只需要推导一个模板参数。同时我们还可以同时再写一个偏特化 V 的版本,

template <typename U>class Ratio<U, double> {// 偏特化 Vpublic:Ratio() : value_() {}Ratio(const U& num, const double& denom) : value_(double(num) / denom) {}explicit operator double() {return value_;}private:double value_;};

当我们想实例化一个 Ratio<double, double> 的时候,两个偏特化都可以推导成需要的形式,代码是编译不过的,因为编译器也不知道该选择哪个实现。唯一解决的方法是我们再明确给出一个 Ratio<double, double> 的特例化版本。

Overloading of template functions

普通函数的也有重载机制,有时会存在多个可能的结果,一般隐式变化的成本 增加 const 或者移除引用 > 内置类型转换 > 子类转成父类。 如果是多参数的函数:

void test(long a, long b, int c) {}void test(int a, int b, double c) {}double c = 1.0;test(2l,3l, c); // 编译报错

虽然匹配第一个定义只需要隐式转换 int 到 double, 而匹配第二个定义需要将前两个 double 转换到 int, 看似匹配第一个更优,但是编译器还是会报错。

在函数中引入模板参数,则让模板函数的重载机制更加复杂了。不过基本准则是:

如果有一个非模板函数近乎完美的匹配上了,则优先匹配这个非模板函数如果没有,则模板函数会尝试实例化一个能近乎完美匹配的函数

Variadic templates

C++11 之后,还介绍了可变参数模板,我们可以用可变参数模板来声明一个能接受任意数量参数的函数了:

template <typename ... T>auto sum(const T& ... x);

下面可以实现求和功能:

template <typename T1>auto sum(const T1 &t1){return t1;}template <typename T1, typename... T>auto sum(const T1 &t, const T &...args){return t + sum(args...); }

函数实际调用中的…运算符,它表示参数包扩展,此时会对args解包,展开各个参数,并用逗号分隔, 这样就又会调用 auto sum(const T1 &, const T &…), 知道解到最后一个,直接返回,完成求和操作。

同理也可以使用可变模板来创建一个可变模板类,同样需要一个特例化的单参数类。

Lambda expressions

正常在 C++ 中带有函数功能的一般都是可调用的,正常函数或者是仿函数。一般情况下在一些局部地方定义一些可调用实体,紧挨着能使用到它的地方。但是 C++ 中不允许一个函数中定义另一个函数。如果中间间隔太远那么就不是很方便。

一个绕过的方式是在函数中声明一个可调用的类。

void do_work(){vector<int> v;...struct compare{bool operator() (int x, int y) {return x < y;}};sort(v.begin(), v.end(), compare());}

这样结构足够紧凑可行,但是太冗长了,一些变量需要重复书写。 我们并不真正需要给这个类一个名字,我们只想这样一个类的对象。 lambda 表达式就是这样一个东西:

void do_work(){vector<int> v;...auto compare = [](int x, int y){return x < y;}sort(v.begin(), v.end(), compare);}

返回类型一般由编译器去推导,所以需要配合 auto 一起使用。 lambda 表达式是一个对象,所以它们可以有数据成员,当然一个可调用的局部类也可以有数据成员:

// 不使用 lambda 表达式void do_work(){vector<int> v;...struct compare_with_tolerance{const double tolerance;explicit compare_with_tolerance(double tol) : tolerance(tol){}bool operator() (double x, double y) {return x < y && std::abs(x - y) > tolerance;}};double tolerance = 0.01;sort(v.begin(), v.end(), compare_with_tolerance(tolerance));}

// 使用 lambda 表达式,简洁很多void do_work(){vector<int> v;...double tolerance = 0.01;auto compare_with_tolerance = [=](auto x, auto y) {return x < y && std::abs(x - y) > tolerance;}};sort(v.begin(), v.end(), compare_with_tolerance);}

具体关于 lambda 表达式 cature 局部变量的方式属于用法的内容。

lambda 表达式是对象,不是函数,所以就没有一个函数的重要性质—— 重载。但是对象是一个类的示例,类可以继承,而多重继承,如果多个基类中有同名的函数,那么会有重载的效果,如果完全一致就会编译报错。所以这里使用可变参数类模板来实现 lambda 表达式的重载形式,然后使用可变参数函数来调用可变参数类模板实现。具体比较麻烦,这里先不展开说明了。

本章思考题:

What is the difference between a type and a template?What kind of templates does C++ have?What kinds of template parameters do C++ templates have?What is the difference between a template specialization and a template

instantiation?How can you access the parameter pack of the variadic template?What are lambda expressions used for?

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。