面向过程和面向对象是两种最常见的编程思想,但很多刚入门编程的人,甚至是有一定编程经验的人所搞混。我本人其实也不敢保证自己对这两种编程思想的区别有透彻的理解,这里就只说一下我个人对两者的一些见解。
面向过程是大多数初学者刚入门时接触的概念,其特点是以正在发生的事件作为重点进行编程开发。而面向对象则将整个程序中的元素都抽象成了很多个对象,对象之间存在一定的关系(例如继承关系),编程时也更多地关注对象与对象之间的相互影响。
两种编程思想各有其优缺点。简单来说,以面向过程为导向开发出来的程序如果规模较小,那么会比较短小精悍,且效率较高;相反,以面向对象为导向开发的程序因为多了一层逻辑的抽象,且为了使程序的逻辑关系更加的清晰遵循了一些额外的规则,使得最终得到的程序运行效率相对比较低下。然而随着程序规模的不断增加,以面向过程为导向开发的程序很容易就会变得难以扩展和维护,且因为以面向过程开发的程序较难对之前的代码进行复用,导致在出现功能变更的需求时,经常会出现牵一发而动全身,改一个小细节就不得不从头到尾更改代码的地步。而以面向对象为导向开发的程序,因为其高度的模块化,对象之间的互动主要通过预先设计好的接口进行,通常不用在意一个对象具体的细节,所以有较好的可扩展性和可维护性,也方便了阅读代码的人对代码在更宏观的层面上对其进行理解。最终,使得代码的复用变得比较的方便,大大地节省了新功能实现的时间。
很多地方会把不同的编程语言划分为“面向对象的编程语言”和“面向过程的编程语言”,如 C 语言在很多时候就被划分为“面向过程的编程语言”,C++ 很多时候就被认为是一种“面向对象的编程语言”。但这个观点我本人并不赞同,在我看来,使用什么语言和是否面向对象没有绝对的关系,例如用 C 语言编写的 Linux 内核就在实现的过程中运用了很多面向对象的思想,而刚刚入门 C 语言的人在学习了 C++ 以后写出的代码往往还是基于面向过程。所谓“面向对象语言”其实更像是内建了很多方便运用面向对象思维进行编程开发的语法糖,要有效地开发规模较大的程序,无论用什么语言,基本都是摆脱不了面向对象的思维。
“对象”的特点
在面向对象编程中,程序中被抽象出了很多个不同的对象,每个对象都有描述自己状态的属性,和用于更改自身属性或者与外界以及其他对象进行互动的方法。可以通过一个结构体对对象进行描述,用于描述一类对象的结构体也被称作“类”,“类”的属性通常就为结构体的成员变量,“类”的方法通常就为专门用于操作某个“类”的函数。对于 C 语言这类“面向过程”的编程语言,类方法可能是一个普通的函数或者结构体内的一个函数指针;对于 C++ 这类“面向对象”的编程语言而言,类方法则为类的成员函数。
在面向对象编程中,通过抽象出很多个相对独立且具有一定功能的类,划分出了很多个模块。为了保证整个程序的逻辑清晰,以及方便日后的维护,程序员在编程的时候需要尽量做到每个模块都专注于一个特定的功能,尽量隐藏外界操作该模块时不需要关注的细节,减少不同的模块间不必要的依赖,即尽量做到“高内聚低耦合”。同时,为了减少不必要的重复开发,程序员应该尽可能重用之前开发的模块。由此,面向对象编程就有了封装、继承、多态等几个概念。
封装
封装通过隐藏一个模块的大多数具体细节,只提供较少与外界操作的必要接口,使模块能够相对独立,从而实现高内聚低耦合。
在 C++ 等面向对象的编程语言中,通常一个类里面可以有三种成员函数和变量:
| - | - |
|---|---|
| public(公共) | 公共的成员函数能够在任何外部的函数内被调用,公共的成员变量能在任何地方被访问或修改。具有该属性的成员函数和成员变量是一个类对外提供的接口的主要组成部分。 |
| protected(受保护) | 具有该属性的成员只能由其所属对象自己,或由该对象派生的对象所访问和修改,对外是不可见的。该属性主要用于类的派生。 |
| private(私有) | 私有的成员只有该对象自己能访问和修改,在其他任何地方,包括由该对象派生的对象其都是不可见的。依靠该属性,可以隐藏一个类的具体细节,做到对模块的封装。 |
在 C 语言中是没有上述几种成员变量的,甚至 C 语言的结构体没有“成员函数”这个概念,以至于表示某个类的一个结构体内所有的成员变量实际上都是公共的,但依旧可以通过 static 等关键字,以及对函数和变量的命名方式来实现对对象的封装。
继承
继承是面向对象编程中进行代码复用的一种手段。通过对一个基类的继承得到的派生类,通常拥有基类的所有方法和属性,某种程度上相当于派生类内在包含了其所属的基类。
在逻辑上来说,通过类的继承,可以描述类与类的包含关系。例如人和狗都属于动物,作为一个类的话都继承了“动物”这个基类,它们各有不同,但都具有所有动物都具有的属性,可以做出所有动物都能做出的动作。进一步的,“人”作为一个基类也可以被多个类继承,例如可以被“老师”、“学生”等。
在例如 C++ 等语言里,提供了实现类继承的语法,程序员可以通过这些语法轻松地对其进行实现。而对于 C 语言这种语言而言,虽然其并没有特别用于实现类继承的语法(甚至没有 C++ 里完整的“类”这种东西),但依旧可以通过将代表基类的结构体作为代表派生类的结构体内的一个成员变量在逻辑上实现类的继承。特别地,面向对象地程序中,万物皆对象,所有的类通常都派生于一个名为 object 的类。
多态
在现实世界中经常出现一种情况,那就是多个不同的事物都共同属于某一种事物,且这些事物都能做出一种动作,但不同的事物做出这个动作时又有一些细小的差别。例如对于人、狮子,和山羊而言,都属于动物,都能发出“吃”这个动作,但人通常吃加工过的熟食,狮子会直接吃野外捕捉到的未加工的肉,而山羊则是吃草。再对人进行进一步划分的话,可以看到西方人通常使用刀叉吃东西,且食用的食物通常以肉类为主;而东亚人普遍使用筷子吃东西,且以大米饭为主食。
上面的例子就属于多态,即不同的派生类对其所属的基类中定义的一些属性以及方法,有着不一样的实现和动作。这点在 C++ 等语言中可以通过重载操作来实现,对于 C 语言而言,则需要通过一些链接的技巧,或者函数指针来实现多态。
多任务在单片机上进行裸机开发时,即在无操作系统环境的情况下时,整个程序的结构通常是以一段初始化代码开头,紧接着是一个无限重复的死循环。最开头的这段初始化代码在芯片复位后只会执行一次,而后面的死循环中的代码组成了整个程序的功能主体,会被单片机按其中的代码顺序不断地重复执行。根据对程序具体的需求,可在循环的代码中插入一段用于检测和处理芯片输入的代码。这种方式被称作“轮询”,其代码简单,消耗的资源相对较少,但由于每次对输入的检测都会间隔一个周期的时间,所以对输入的响应速度可能会很慢,甚至最终导致整个程序的输出因为响应不及时而产生错误。
为了克服这个问题,可以将原本存在于循环体中用于检测和处理输入的代码放到一个特殊的函数内,当特定的条件被触发时,就会暂停执行当前的任务,转而调用这个特殊的函数,待函数返回时再继续执行之前暂停的任务。这种方式被称为“中断”,而其中这个特殊的函数则被称作“中断服务函数”。中断明显地提高了程序的响应速度,一定程度上减少了程序为检测是否有输入而花费的时间。
但此时同样还存在一个问题,假如响应一次中断需要花费的时间较长,那么如果芯片在当前中断处理完以前再接受到了一个相同的中断,那后来的这个中断将无法得到响应,最终造成部分中断被忽略。忽略了部分的输入,在一些情况下导致的后果可能是灾难性的。我们为了减少这种致命的错误,从而引入了多任务这个概念,以及对多个任务进行调度和管理的操作系统。
实时性与嵌入式操作系统实时性的意思是从某个事件的触发到程序对该事件的响应之间的时间间隔不过超过某个上界。而根据对事件响应超时的容忍度又可进一步细分为硬实时和软实时,前者不能接受任何的响应超时,后者可以接受偶尔的响应超时。
实时操作系统则是为了满足实时性而设计出来的操作系统,通常被作为嵌入式操作系统。需要注意的是,虽然实时操作系统通常都为嵌入式操作系统,但嵌入式操作系统与实时操作系统并不一定等价。
市面上有很多嵌入式操作系统,而 RT-Thread 就是其中的一个开源的国产实时操作系统。
进程与线程在计算机发展的早期时间,其造价及运行成本是极高的,一台计算机通常被非常多用户所共享。为了满足这种对计算资源的共享需求,引入了“进程”这个概念。进程具有独立的地址空间,一个进程不能直接访问另一个进程的资源,需要借助操作系统提供的特殊的 API 实现进程间的通信,加上不同的进程还通过分时的方式轮流占用 CPU 资源,就造成了在一个进程的视角上看来,自己独占了整台计算机。
线程也被称为轻量级的进程,同属于一个进程的多个线程共享了部分地址空间(PPT 上说共享整个地址空间,这点我并不是很赞同,因为现代的通用操作系统中,线程间至少栈空间是被隔离的),能够直接访问其所属进程的全局变量。
而在 RT-Thread 中,线程是任务调度的基本单位,每个线程都有一个储存其状态信息的线程控制块结构体 struct rt_thread ,和存放线程上下文的栈。通过将上下文在寄存器与线程栈中的转移,实现了线程的切换。
优先级与线程调度线程的状态
线程并不是随时都可以被调度,在 RT-Thread 中每个线程都具有以下四个状态:
| - | - |
|---|---|
| RT_THREAD_INIT/CLOSE (初始化或关闭状态) | 此时线程刚刚被创建或者被结束,处于该状态下的线程不参与调度。 |
| RT_THREAD_SUSPEND (挂起状态) | 线程正在等待得到某个资源,在得到对应资源之前线程都位于相应的等待队列中,且此时该线程不参与调度。 |
| RT_THREAD_READY (就绪状态) | 当前线程已经得到其需要的所有资源,随时可以被调度器调度并运行。 |
| RT_THREAD_RUNNING (运行状态) | 处理器当前正在运行中的线程。 |
而这里线程的调度主要涉及到位于就绪队列中的线程,即就绪态的线程和运行状态的线程。
就绪队列
当线程在等待得到某个资源的时候,其线程控制块会一直存在于被等待资源对应的等待队列中,待线程得到所需资源后,其线程控制块会从等待队列中移除,并插入就绪队列中,从而能够被调度器调度。
RT-Thread 的线程可以指定其所在优先级,以让调度器优先调度更重要的任务。优先级的个数有限,可以在编译时指定优先级的个数;而每个优先级的线程数量没有限制。RT-Thread 给不同的优先级各自指定了一个链表作为就绪队列,用位图来标志某个优先级的队列是否存在就绪态的线程。
调度器
调度器在因为各种原因发起线程调度后被激活,并判断当前线程是否需要被切换,和选择合适的线程进行切换。在发起线程调度后,调度器首先会借助位图找到优先级最高的不为空的就绪队列,并从中挑出一个线程进行上下文切换。
正常情况下,就绪队列中只有优先级更高的线程都让出 CPU 以后优先级较低的线程才会得到调度,而对于位于同一优先级下的线程而言,则遵循的是时间片轮转调度算法。另外,由于 RT-Thread 支持抢占调度,所以无论在任何时候,只要有优先级更高的线程进入就绪状态,当前线程就会被让出 CPU 资源,转而执行优先级更高的那个线程。



