软件工程 4:C++编程范式

C++编程范式

    • 结构化编程
    • 泛型编程
      • C 的泛型
      • C++ 的泛型
      • 类型系统
      • 泛型本质
    • 函数式编程
    • 面向对象
    • 超级范式
      • 元编程

 


在学数据结构的时候,您可能看过一个公式:

  • 程序 = 算法 + 数据结构

这个公式蕴含着编程的真谛,但其实在暗处还有一个我们没看见的东西:【编程范式】。

  • 程序 = 算法 + 数据结构 + 编程范式

算法是人的灵魂,数据结构是人的骨架,而编程范式就是人的皮肤(形式)。

同样是人体器官,很少有人每天保养心、肝、肺,可到了皮肤,女人会花很多钱,很多时间…

之所以皮肤这么金贵,是因为它在人体最外面,您的健康状况、活力指数,甚至还有性吸引力,这些重要的社交信息,通通都被它暴露了,所以人就忍不住要修饰它,尤其是脸上的皮肤。
 


结构化编程

绝大多数编程语言都是【命令范式】的子集,即我们常说的【面向过程】。

过程式编程(面向过程的前身)是【命令范式】的一个子集,【命令范式】 是冯诺伊曼通用计算机运行机制的抽象 — 依照顺序,从内存中获取指令和数据,而后去执行。

  • 战略:程序 = 若干行动指令组成的有序列表
  • 战术:用变量存储数据,用语句来执行指令

【命令范式】,强调怎么做,完成一件事是先做这,再做那。

过程式编程(面向过程的前身)是引入了几个限制条件(过程、函数、子程序)的【命令范式】。

在过程式编程发展过程中,诞生了一种叫结构化编程的东西,也就是我们说的【面向过程】。

软件工程 4:C++编程范式

这种类型的计算首先需要设定一个计算的目标,通过定义一个可以量化的终止条件,然后从一个猜测的初始值开始,通过检验初始值是否满足计算目标所期望的条件,从而开始这个过程。

如果满足条件,那这个过程就结束了;否则,需要更新猜测。整个计算过程中,更新的好坏和计算终止条件的标准的高低,决定了程序是否能够在足够短的时间内获得足够好的结果。

流程图主要是为计算机实现做准备的,学过程序设计应该熟悉这种表示方法。

这个流程图的结构是一个循环结构,根据结构化定理[^Ravi Sethi. Programming Languages: Concepts & Constructs, 2nd Ed… Reading, MA: Addison Wesley, 1996. 59-77],任何程序都可以用顺序、选择、循环这 3 种基本控制结构来表示。

结构化编程就是在这三种基础结构上的嵌套组合,又因为顺序、选择、循环都是单入口、单出口,所以程序逻辑通常是井然有序的,不会乱七八糟。

结构化编程:

  • 在微观上,循规蹈矩
  • 在宏观上,分而治之

如果想写什么就写什么,那程序往往如杂草松散紊乱,而采用结构化设计,每个模块大小适中、模块之间的关系脉络清晰,单从视觉上就会给人一种美感。

【扩展】结构化设计 –> 请猛击:《软件工程 3》。
 


泛型编程

泛型编程以算法为导向,以算法为起点、中心点,逐渐将其所涉及的概念内涵(数据类型、数据结构、类)模糊化,外延扩大化,将其所涉及的运算(函数、接口、方法)抽象化、一般化,打破数据类型的壁垒,大大减少了代码量,不过用法复杂了一些,可读性也差一些。

思想:将算法和其作用的数据结构相分离,并将数据结构尽可能的泛化,最大限度复用算法。

这种泛化是基于【模板的参数多态】,相比 O O P OOP OOP 基于【继承的子类型多态】,普适性更好,效率也更高。

一个良好的泛型编程需要解决如下几个泛型编程的问题:

  • 算法的泛型;
  • 类型的泛型;
  • 数据结构的泛型(容器:抽象化的数组、迭代器:抽象化的指针)。

不同语言实现泛型的机制:

  • C++   ~   采用类型模板
  • Java 采用类型擦除
  • C#    ~~    采用类型具化
     

C 的泛型

最开始学编程的时候,我们会做一点简单的练习。

比如,打印6颗 *。

不对呀,题目是打印六颗 *,而 puts() 函数里面有 7 颗。

所以,您得擦掉一颗…这样写还真的有点麻烦,还得数一下 * 的个数。

于是,老师介绍了一种新方法:循环。

其实老师介绍的这种方法,不只有循环的写法,还有一种【参数化编程】的东西(变量n)。

参数化编程使得我们的代码行为始终保持一致。

这就是一种泛型编程,核心是抽象和隔离。

C要想实现泛型的写法较麻烦,

  • 通过 宏定义 和 void *、GUN C的 typeof 只能实现类型的泛型
  • 通过函数指针只能实现函数的泛型
  • 但数据结构的泛型,做不做的到呢/li>

如果我们想交换两个字符串数组,类型是:char*,那swap()函数的x和y参数是不是要用void**了/p>

说明 void* 并没有实现泛型,于是我们采用宏定义的方法。

宏定义最大的问题是不安全,就是有可能会有重复执行的问题。

如 swap(i++, j++), i, j 会自增俩次,所以要改成函数形式的宏定义。

typeof 是 GNU C 的内置关键字,Linux上是支持的,系统的内核源码也是这么写的,但不是所有平台、所有系统都支持的。

  • void * :接口都变得复杂了——加入了size,因为如果不加入size的话,那么我们的函数内部就需要自己检查size;char 等数据类型还好,但如果是 char* 等地址类型,那就不能实现泛型。
  • 宏定义 :不安全,有重复执行的问题。
  • typeof : GNU C 独有,不能跨平台。

而且实现的都只是类型的抽象,如果要把它变成泛型,我们需要变更并复杂化函数接口。

  • 如果是交换值,需要加一个 size;
  • 如果是比较值,需要加一个 size,以及一个回调函数泛型比较方式。

功能越多的话,那函数接口就越复杂。

如,实现search函数,再传一个int数组,然后想搜索target,搜到返回数组下标,搜不到返回-1。

这个函数是类型 int 版的。如果我们要把这个函数变成泛型的应该怎么变呢/p>

就像上面swap()函数那样,如果要把它变成泛型,我们需要变更并复杂化函数接口。

  • 在函数接口上加一个 element size,也就是数组里面每个元素的 size。遍历数组的时候,可以通过这个 size 正确地移动指针到下一个数组元素。

  • 加个cmpFn。因为要去比较数组里的每个元素和target是否相等。因为不同数据类型的比较的实现不一样,比如,整型比较用 == 就好了。但是如果是一个字符串数组,那么比较就需要用 strcmp 这类的函数。而如果传一个结构体数组,那比较两个数据对象是否一样就比较复杂了。所以,必须要自定义一个比较函数。

就算是这样的泛型,search函数只能用于数组这样的顺序型的数据结构。

如果这个search函数要能支持一些非顺序型的数据结构,比如:堆、栈、树、图。

对于像search()这样的算法来说,数据类型的自适应问题就已经把事情搞得很复杂了;然而,数据结构的自适应就会把这个事的复杂度搞上几个数量级。

在代码组织和功能编程上,C 语言就不那么美妙了。

C 语言是一门过程式编程语言,优点是底层灵活而且高效,特别适合开发运行较快且对系统资源利用率要求较高的程序,但上面抛出的问题它在后来也没有试图去解决,编程范式的选择基本已经决定了C的“命运”…

如果 C 能借鉴 C++ 的 命名空间、重载、异常处理、STL 等非 OOP 特性,那会更流行的。

不同的语言在设计上都会做相应的取舍。C 语言偏向于让程序员可以控制更多的底层细节,而 Java 和 Python 则让程序员更多地关注业务功能的实现,而 C++ 则是两者都想要,导致语言在设计上非常复杂。
 


C++ 的泛型

C++ 是支持编程范式最多的一门语言,它虽然解决了很多 C 语言的问题,但我个人觉得它最大的意义是解决了 C 语言泛型编程的问题。

  • template : 可以写出类型无关的数据结构
  • 操作符重载 : 操作上的泛型
  • 函数对象 : 用户自定义的数据类型和内建的那些数据类型就很一致
  • STL : 解决了数据结构和算法的N多问题,数据结构的泛型
  • iterator : 抽象掉了不同类型的数据结构的访问方式
  • 通用算法 : 算法的泛型,符合通用算法,对数据类型的最小需求

C实现的 search()函数,里面的 for(int i=0; i<len; i++) 这样的遍历方式,只能适用于顺序型的数据结构的方式迭代,并不适用于非顺序型的数据结构。

除了 search() 函数的“遍历操作”之外,还有 search 函数的返回值,是一个整型的索引下标。这个整型的下标对于“顺序型的数据结构”是没有问题的,但是对于“非顺序的数据结构”,在语义上都存在问题。

所以,如果找不到一种泛型的数据结构的操作方式(如遍历、增删改查等),那任何的算法或是程序都不可能做到真正意义上的泛型。

为了解决泛型的问题,C++ 设计了许多技术:

  • 使用模板技术来抽象类型,这样可以写出类型无关的数据结构

  • 用一个迭代器来遍历或是操作数据结构内的元素

C 版 search() :

C++ 版 search() :

C++ 的泛型中:

  • 使用 typename T抽象了数据结构中存储数据的类型
  • 使用 typename Iter,这是不同的数据结构需要自己实现的“迭代器”,这样也就抽象掉了不同类型的数据结构
  • 对数据容器的遍历使用了 Iter 中的 ++ 方法,这是数据容器需要重载的操作符,这样通过操作符重载也就泛型掉了遍历
  • 在函数的入参上使用了 pStart 和 pEnd 来表示遍历的起止
  • 使用 *Iter 来取得这个“指针”的内容,也是通过重载 * 取值操作符来达到的泛型。

所谓的Iter,在实际代码中,就是像:

  • vector::iterator
  • map<int, string>::iterator

这样的东西,这是由相应的数据结构(STL数据容器)来实现和提供的。

这就是整个 STL 的泛型方法,其中包括:

  • 泛型的数据结构
  • 泛型数据容器的迭代器
  • 泛型的算法

泛型技术是静态系统所独有的特性,本质上我觉得还是为了兼顾执行效率和编程灵活性,实现零成本抽象这一刀尖上跳舞的巨大挑战。

较新的语言标准还提炼了很多基础设施,比如:

  • 大量使用 SFINAE 并泛化之
  • 提供enable if, constexpr if
  • 乃至自动类型推断 auto 和 concept check

和Java那边的套路是完全不一样,可以说不深入理解泛型技术基本,就不能很好理解现代的C++。
 


类型系统

分门别类 是科学研究的基本方法和途径。

人类在认识客观世界的过程就是采用了分类的方法。

世界分为:

  • 生物
  • 非生物

生物分为 动物、植物、微生物等,或者是 界 门 纲 目 科 属 种。

【分门别类】做有助于人类认识世界和改造世界。

程序设计时,就引入了 分门别类 的思想,即 数据类型。

通过 『分门别类』的数据类型:

  • ① 有助于程序设计的 「简明性和数据的可靠性」,可以识别出一个错误无效的表达式。如:“Hello, World” + 3这样的不同数据类型间操作的问题。强类型语言提供更多的安全性,但是并不能保证绝对的安全。

  • ② 有助于数据的存储管理,指定一个类型是 int ,那么编译就知道,这个类型会以 4 个字节的倍数进行对齐,编译器就可以非常有效地利用更有效率的机器指令。

  • ③ 有助于程序的运行效率

在计算机科学中,类型系统用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,以及如何操作这些类型,还有这些类型如何互相作用。

类型可以确认一个值或者一组值具有特定的意义和目的。

编程语言者会有两种类型,一种是内建类型,如 int、float 和 char 等,一种是抽象类型,如 struct、class 和 function 等。

抽象类型在程序运行中,可能不表示为值。

类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。

无论哪种程序语言,都逃避免不了一个特定的类型系统:

  • 静态类型(如 C、C++、Java)检查是在编译器进行语义分析时进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。

  • 动态类型(如 Python、PHP、JavaScript)检查系统更多的是在运行时期做动态类型标记和相关检查。所以,动态类型的语言必然要给出一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。
     


泛型本质

类型是对内存的一种抽象。不同的类型,会有不同的内存

来源:Debroon

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年3月19日
下一篇 2020年3月19日

相关推荐