文章目录

  • 软件工程 3
    • 面向过程:模块化设计
      • 模块设计原则:高内聚、低耦合
      • 函数设计:不知道如何下手怎么办
      • 错误处理:每个函数都有错误处理
      • 模块的编译和链接
    • 面向对象:万事万物皆对象
      • 类:一组相似事物的统称
      • 对象:一个真实存在的类
      • 接口:一组相关的交互功能点定义的集合
      • 抽象类:基于类而抽象出来的,用于继承,不能被实例化为具体的对象
      • 封装:保护隐私、隔离关注点,减低复杂度
      • 继承:生物的遗传
      • 多态:指向父类的指针或引用,可以调用子类对象

软件工程 3

软件工程并不是一种科学,而且其重要性也与计算机本身并无关系。

软件工程革命是有关我们如何去思考的方式,以及我们如何去表达自己的思考的一个革命。

在第一课时,对比了【面向过程】、【面向对象】。

这一课,我们来说说他们的思想,具体的设计细节。

面向过程以【计算机】为核心,符合 CPU 顺序处理数据的流水线思路。

面向过程程序 = 算法 + 数据结构:

  • 面向过程的程序是流水线的产品;
  • 算法是流水线;
  • 数据结构是原料;

面向对象以【人】为核心,不再注重机器,而侧重于人对现实世界的观察。

您可以往周围看看,看到的是什么?

可能是您的同事、桌子、墙、电脑、花盆;或者是电话、窗子、书本……这些都是【对象】,是吗~

除了观察目标聚焦于【对象】外,当我们观察人类世界各种事情的运作的时候,我们也不知不觉的聚焦于【对象】。

  • 家庭:父亲、母亲、大哥、小哥等;
  • 公司:董事长、经理、主管、员工等;
  • 国家:…

人一般是按照面向对象的方式思考的,而且人类世界的运转方式通常也是按照面向对象的方式运作的。面向对象,是我们天然的视角。

面向对象程序 = 对象 + 交互:

  • 对象:具体存在的事物;
  • 交互:事物之间的链接;

在工业系统中,面向过程的流水线工艺以【效率】著称,一环套一环非常高效,可灵活性却不好。

一条流水线从建成开始,接下来几年甚至十几年生产的都是同一产品。

但对于软件系统来说,一款软件可能因为增加新功能、需求的变更,每年都要扩展多次…如果用流水线工艺,由于每次需求的变更,流程里的每一步骤、一环套一环,哪怕只是更改了某一环,整个流水线都得重新调整一下。

一想到是这样的情况,头就大了。这就是一个无底洞呀,小型项目还好,要是大项目,每更改一次,软件出现 bug 的概率就大几分。

所以,所以,就有了【面向对象】来解决这个问题,其实就是比较灵活(术语叫“可扩展性”)。

面向对象,适合用于经常变化的地方。

对于软件开发来说,可变的通常集中于客户需要,不变的是计算机系统的基础。

  • 操作系统、数据库、各种协议,它们相对稳定,用【面向过程】更贴切;
  • 企业应用、互联网、游戏,需求经常变更、功能不断扩展的,用【面向对象】才适合。

虽然可扩展性,只是其中之一。

但足够了,如同法师、射手主伤害,坦克主防御,辅助主增益一样,面向对象主敏捷。

了解面向对象的特点,是应用面向对象的关键。千万不要拿把锤子,看什么都是钉子。

面向过程、面向对象都有它们的局限性,也有在各自很优秀的指标。


面向过程:模块化设计

背景:鲁滨孙漂流记。

环境:无人小岛。

目标:做一碗杂酱面。

某天鲁滨孙心血来潮,想吃一碗杂酱面了。

于是,他花了2年时间做这碗杂酱面:

原来吃一碗杂酱面是这么幸福,回想我们想吃杂酱面,只需要在手机点几下就好啦。

为什么我们想吃一碗杂酱面,就这么容易呢?

因为平等,带来了合作,而整个社会的分工合作带来了群体演化速度加快。

面向过程之所以这么高效,就是因为【分而治之】的思想。

把一个项目,分成不同的模块,不同模块交给了不同的人;大家分工协作就能快速实现这碗杂酱面。

模块化编程的特点:

  • 分工实现,适合多人协作;
  • 便于维护、管理、移植;

对于黑客来说,懂模块化编程,对以后分析系统、底层项目是很有意义的,也是大型项目开发的基石。


模块设计原则:高内聚、低耦合

有一个梦想,定居:【苏州】。

因为我是一个面痴,每天早上能吃一碗汤面,我就很开心啦。

苏州的汤面最考究的是面汤(汤也炒鸡好喝),汤要清而不油,味要鲜而食后口不干。

我最喜欢的做法:汤是汤,面是面,先煮好面,而后把面在配好的清汤里过一下,端上来的时候也不烫。

正如上图的面,汤是汤、面是面、大排是大排(配料),这也是模块的高内聚、低耦合。

  • 高内聚:汤是汤、面是面、大排是大排(配料),相互独立;
  • 低耦合:面与汤之间关联很少,一眼就能清晰的看出面和汤来。

模块化编程的核心思想,即【分而治之】。

所以,模块之间一定要界限分明(内聚:模块各元素的关联程度),自己的功能自己实现,不麻烦其它模块。

那如何实现高内聚呢?

  • 功能尽可能单一,一个函数只实现一个功能,函数的源代码行数一般不超过100行;
  • 一个模块实现一个功能,尽可能使用本模块的函数,不要麻烦其它模块;

耦合:模块间的数据传递、控制高系、调用高系的关联。

耦合度越高,耦合性越强,模块独立性就越菜。

耦合方式分为(实际更多):

  • 非直接耦合: 两个模块之间没直接联系
  • 数据耦合: 通过参数来交换数据
  • 标记耦合: 通过参数传递记录信息
  • 控制耦合: 通过标志、开关、名字等,控制另一个模块
  • 外部耦合: 所有模块访问同一个全局变量

耦合越低,维护就越方便。如果模块之间,我可以调用你,你也可以调用我…

整个程序的关系会较乱,修改、添加一个模块,牵一发动全身,所以你先得要理清这个关系,那是很头疼的。

避免高耦合,常用的设计方法:

  • 少用全局变量,通过接口访问;
  • 禁止相互调用,只能单向调用,上下级关系;
  • 多用接口设计,接口是唯一的调用方式;如我们去餐厅,也只会和服务员(唯一接口)打交道,不会和厨房人员交流的。

高内聚导致低耦合,低耦合就意味着高内聚。

  • 内聚:模块内各元素之间的关联交互

  • 耦合:模块间的依赖关联、交互关系


函数设计:不知道如何下手怎么办

我们实现程序里的算法时,其实就是写一个或者若干个函数。

有时候,我也不知道该如何下手。

没关系,我可以分析一下问题:

  • 应该传什么参数给函数(作为输入参数)
  • 函数处理完后,应该把什么数据作为结果返回(作为输出参数)

返回类型 函数名(函数参数)

一步步走,对一道问题完全没想法时,多半是没有分析好,这时候反复要分析。

分析好以后,知道就知道该用什么知识对答,这时候考验的是编程能力了,分析考验的是算法能力,当写好了主函数模版,接下来就思考实现某个算法的原型是怎样的,反复练习让其成为一种习惯。

加油!

我觉得参数设计是在原型设计里最主要的,设计的好,程序简洁,递归可以避免因多层函数参数入栈而引起的爆栈。

如,二分查找的函数原型:

bool binary_search( ... );

返回是否找到,要么直接返回数的下标,要么就返回一个布尔值。

千万不要在这个子函数里面,输出:找到没找到;这样会提高模块化的耦合性,这样相当于告诉别人,我是新手中的新手。

设计参数:

#define T int
bool binary_search ( T arr[], T key, int len );// arr[]:被查找的数组
// key:   查找的值
// len:   被查找数组的长度

参数虽然正确,不过也并不好。

因为查找是不需要修改目标数据(被查找的数组),没有保护数据,所以加上 const 关键字让数据不能被修改。

key和len,运行时,因为是传值,会创建(占空间)副本给形参,我们可以改成传引用减少不必要的空间。

但如何数据量巨大,采用引用会影响效率因为引用是间接寻址,传值是直接寻址所以速度比引用、指针快。

#define T int
bool binary_search ( const T arr[], const T& key, const int& len );// 算法不需要修改数据时,都应设计为 不可修改类型,这样的设计原则其实非常敏捷
// 传引用避免形参拷贝

还可以简化接口,如把数组长度的参数去掉:

​#define T int
const int len = 10;
bool binary_search ( const T (&arr)[len], const T& key );// &是传引用,(&arr)[len]是传数组的引用,可以检查数组是否溢出,只接收 len 个元素的数组。
// 如果传进来的数组,这个数组的元素个数不等于len(10),编译就不过。从侧面避免数组溢出,但引用是C++支持的,C并不支持这种写法。

错误处理:每个函数都有错误处理

确定好了函数的原型之后,紧接着在完成这个函数的功能一开始的地方,就需要严格判断函数输入参数的合法性(防黑客)。

  • 函数参数中的指针是否为NULL;

if( ptr == NULL || *ptr == '\0‘ )

  • 函数参数中缓存的长度是否在合理范围;

if( len <= 0 )

如果类型是浮点数,因为实数在计算和存储时会有误差,因此本来是零的值,由于误差的原因判断为非零。所以,判定方法要改。

‌// 采用绝对精度判断, 原理:判断ta的绝对精度是否小于一个很小的数,如 0.000 001
double eps = 1e-6
if ( fabs(a) <= eps )  // 等于0
if ( fabs(a) - fabs(b) <= eps ) // 俩个数相等// 当变量 a、b 在 eps 精度附近时,判断失误。还有 相对精度 和 绝对精度+相对精度 的判断方法
  • 甚至还要利用C++中的RTTI对参数的类型进行检查。

二分查找为例,判断边界。

#define T int
bool binary_search ( const T arr[], const T& key, const int& len )
{if ( len <= 0 || arr == NULL || *arr == '\0' )    // 短路表达式优化,一真必真,避免不必要的判断, 所以把最高频的放在最左边,其余同理return false
}

在参数的设计时,如果模版参数是通用类型时,要把所有类型情况都考虑进来。

避免程序在运⾏中出错,避免各种漏洞的产生。如:

  • 曾经的SSL协议中的 心脏流血漏洞,就是因为服务端程序在处理时,没有验证来⾃客户端对应的缓存⻓度有效性,造成了该漏洞的产⽣。

  • 被号称漏洞核弹的 微软CVE-2017-0290漏洞,也只因在扫描引擎MsMpEng的NScript模块中没有对输入的参数类型进⾏检查,默认当做字符串来处理造成的。

如果用户输入的参数在边界判断时,发生了异常或者错误,就必须要处理,否则程序就无法继续执行了。

出错处理的方式方式很多,也很灵活。只要不遗漏对错误的处理就行。

C++、Python 这些语言在解决出错的时候多通过异常处理,不过异常在C语言里有一些问题。

在C语言中,我们只有一个返回值,但异常是一个基于栈的返回系统,返回的东西是不确定的。很多编程语言直接把所有边量都放在了堆。

我最喜欢的出错处理方式就是 goto 了。想起来,就开心。优美,简洁,适合小白。

#include<stdio.h>int fun(void)
{if( finshied )goto err_1;           // 一定是往 return 方向跳if( finshied )goto err_2;    /* goto语句 和 err的标签 之间 不能有 定义变量的操作。如 int a = 9, 但声明可 int a */return 1;err_1:do something;           // 只能调到当前函数, err 一般在函数分界处
err_2:do something;
}

还可以更美好,自制一个宏。

#define check(A, M, ...) if(!(A)) {\log_err(M, ##__VA_ARGS__); errno=0; goto error; }

check宏的使用方式,和库函数assert()一样,都是断言(断言括号里的表达式为真,程序能正常运行)。

否则,【打印错误信息】并跳转到【错误标签】error处,清理。

#define T int
bool binary_search ( const T arr[], const T& key, const int& len )
{check( len > 0, "参数len必须大于0"); // 断言,程序正常运行,那 len 就大于 0;// 如果断言失败,就打印错误信息(参数len必须大于0);// 而后跳转到 error 标签,清理。error:do something;return false;
}

我有许多这样,用于错误处理的宏,十分方便。

我还为这些错误处理宏,加上了颜色,输出错误信息时很美观哒。

这套错误处理宏真的好玩,像苏州面一样,要是每天都能接触到,实在是太开心啦。

这套宏,也分享给远方的你。

错误处理模块:自动化错误处理、实现异常机制

#include<stdio.h>
#include<errno.h> /* 调用 errno       */
#include<string.h>  /* 调用 strerror() */
#define clean_errno() (errno == 0 ? "None" : strerror(errno))
// 看懂这句代码,需要了解 errno、strerror()
// errno 是一个数字,errno = 1 是操作不允许,errno = 2 是没有这样的文件或目录,在 errno.h 文件里一共定义了 124 个常见错误 [1, 124]
// errno 本身只是一个数字,如果想知道这串数字的含义是什么,可以通过 strerror() 函数,比如 errno = 2,strerror(errno) 会返回 【Error: No such file or directory】#define log_err(M, ...) fprintf(stderr,\"[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__,\clean_errno(), ##__VA_ARGS__)
// log_err相当于一个调试器,当程序出现错误时,输出 那个文件名(__FILE__)、错误的行数(__LINE__)、错误的原因(strerror(errno)),以及提示信息(这个是我们调用的时候写的)#define check(A, M, ...) if(!(A)) {\log_err(M, ##__VA_ARGS__); errno=0; goto error; }
// check(同assert),断言check括号里的内容为真,否则打印错误信息并跳转到 error 处清理#define check_mem(A) check((A), "内存不足。")
// check_mem() 可以用于检测内存,如果 A == false,就输出【内存不足】,并进行一次跳转int main( )
{FILE *fp = fopen("file.abc","r");     // 打开一个压根不存在的文件if( fp == NULL )                       // 因为文件不存在,所以打不开,指针为空check(fp, "提示信息:fp == NULL呀~" );// 使用我们的错误处理宏 check(), 语法类似 assert()int *a = (int *)malloc(19979797797984);   // 申请一个编译器给不了的空间if( a == NULL )                           // 因为内存不够,所以没申请到,指针为空check_mem(a);                            // 和 check() 一样,只不过不需要给提示信息了error:                                       // 错误处理标签, do something,如if( fp != NULL )free(fp), fp = NULL;if( a != NULL )                          free(a), a = NULL; // 特别说明:在我们的程序里,某个函数往往申请了不止一个空间// 如果有一个空间申请失败,我们就 return -1 的话,那前面申请的空间就还没释放// 所以,释放空间最好使用 goto 统一处理return -1;  // 如果写 return 占位,那函数就结束了// 特别补充:这种统一错误标签(只有一个error标签)其实不灵活。// 因为错误处理也可以检测异常,如果某个地方有问题,那程序跳过这个地方即可,接着运行下面的代码,比如上面的代码,就因为打不开文件,所以程序越过申请空间这步,直接返回了。// 使用多个错误处理标签就可以实现异常/* 异常应该写成这样* if( fp == NULL ) *     goto err_1;   *                   * err_1:*     do something.* * // fp有问题,没关系,接着运行* * if( a == NULL )         *     goto err_2;** err_2:*       do something* * P.S. 如果代码出错了,可能是 goto err_i语句 和 err_i 的标签 之间有【定义变量】。如 int a = 9, 这是不可以的,但声明可 int a*/fclose(fp);    fp = NULL;free(a);       a = NULL;return 0;
}

check 宏类似 assert,都是断言为真,只不过功能更加丰富,为出错处理而设计的。

goto 只能实现函数内部的跳转,不能实现跨函数跳转。

在一些错误处理中,在一个深度嵌套的函数调用中发生了错误,需要放弃当前任务,从多层函数调用中返回,并且在较高层级的函数中继续执行(或许是在 main() 函数中)。

要做到这一点,可以让每个函数都返回一个状态值,由函数的调用者检查并做相应处理,不过一层层的检查比较麻烦,setjmp 宏和 longjmp 函数 可以实现【非本地局部跳转】,也就是跨函数跳转

无论使用什么样的错误处理方式,都不要忘记发现程序中错误的最好方法其实是执行程序,对代码进行逐条跟踪,这样可以【观察数据在函数中的流动】,同时【检查出类似于上溢和下溢错误】、【数据转换错误】、【NULL 指针错误】、【错误的内存单元】、【用 = 代替 ==】、【运算优先级错误】、【逻辑运算】等错误。


模块的编译和链接

/* 当前源文件:demo.c */
#include <stdio.h>int main(){printf("hello, world!");
}

上面程序的作用是打印 hello, world,在编译器里我们只要点【运行】就能看到结果。

但不同文件里的模块到底是怎么链接在一起的?

我们为什么可以在当前源文件 main() 里,直接调用 printf()。

这就得从源代码生成可执行文件的内部机理聊起啦。

从源代码生成可执行文件可以分为四个步骤:

预处理(Preprocessing):处理那些源文件和头文件中以#开头的命令,比如 #include、#define、#ifdef。

  • 将所有的#define删除,并展开所有的宏定义。
  • 处理所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。
  • 处理#include命令,将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样。
  • 删除所有的注释 //、/* … */。
  • 添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。
  • 保留所有的#pragma命令,因为编译器需要使用它们。

预处理的结果是生成【.i文件】。

【.i文件】也是包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。

当你无法判断宏定义是否正确,或者文件包含是否有效时,可以查看【.i文件】来确定问题。

在 GCC 中,可以通过下面的命令生成.i文件:

gcc -E demo.c -o demo.i // -E表示只进行预编译。

编译(Compilation):将预处理完的文件进行词法分析、语法分析、语义分析以及优化后生成相应的【汇编代码文件】。

在 GCC 中,可以使用下面的命令生成.s文件:

gcc -S demo.i -o demo.s

汇编(Assembly):将汇编代码转换成可以执行的机器指令。

汇编的结果是产生【目标文件】,在 GCC 下的后缀为.o。

链接(Linking):目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,程序不能执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。


面向对象:万事万物皆对象

我是从 C 语言入门的,感觉很 nice !!

而后开始接触数据结构,用面向过程的方式实现数据结构时,发现每种数据结构和它的自带算法天然是一个整体,用面向对象的方式描述是不是更适合呢?

我觉得是,所以我去学了一下 C++。

初次接触C++,我编程还没习惯从面向过程转为面向对象,感觉很别扭…

还有概念也太抽象了,只是用一些词来解释另一些词。

语言本身就有二义性,不类比、举例的话,很难理解到这一视角下的世界观。


类:一组相似事物的统称

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

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

世界分为:

  • 生物和非生物;
  • 生物分为 动物 植物 微生物等等;
  • 或者是 界 门 纲 目 科 属 种。

面向对象的类,与古语上的【物以类聚,人以群分】的类、分门别类的类是相同的。

因此,当我们在定义什么才是“类”的时候,其实完全不需要和面向对象扯上关系。

类是【一组】【相似】事物的【统称】。

  • 一组:在于多个,因为单个事物无法此外类;
  • 相似:人类是一个类,但每个人都是不一样的、独一无二的;
  • 统称:能够概括多个事物,每个人都可以统称为“人”、“男人或者女人”,但名字不是一个统称。

对象:一个真实存在的类

在程序里,类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。

只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。

这是因为类是抽象出来的,不是真实存在的。

比如,我们说人、人类、男人、女人这些类时,它们只是一个抽象的概念,其实有没有这个人呢,是没有的。

但要说,牛顿、爱因斯坦,这些对象是真的存在的。

对象时有特点、有行为的,也就产生了数据,由此只有程序实例化对象时,编译器才会储存数据。

类是一个抽象的概念,而对象是真实存在的(类)。


接口:一组相关的交互功能点定义的集合

接口的定义比较难,名词分析:

接口:一组【相关】的【交互】功能点【定义】的【集合】;

以餐馆的列有各种菜肴的清单为例:

  • 相关:接口中包含的功能点是相关的;

各种菜肴都是为了满足口腹之欲(相关),是吧。

  • 交互:用于不同对象的交互;

客人点菜,其实是和大厨之间的交互;一个人是自嗨,不是交互。

  • 定义:没有实现;

清单里的每道菜是都可以做出来的,但客人不点,大厨是不会做的。

  • 集合:多个;

清单里可不止一个菜。

简单来说,接口就像一份列有各种菜肴的清单。

为什么要有接口呢?

有的时候呀,我们并不知道客人想吃什么,但我们又想客人照我们的要求来活动。

这就是接口的用处所在,当您不知道一个对象所属的具体“类”,只知道这些对象都具备某种功能。

接口:一组相关的交互功能点定义的集合。


抽象类:基于类而抽象出来的,用于继承,不能被实例化为具体的对象

从设计的角度来看,抽象类是更高层次的抽象。

如果说普通类是从现实对象抽象出来的,那么抽象类就是基于类而抽象出来的。

有了类,为什么还要抽象类?

为什么设计一种只能继承,不能实例化的类?

答案就在于:在某些场景下普通类不够用。

例如,“苹果”、“橘子”、“香蕉”都是一个类,它们都是“水果”,这里的“水果”就是一个【抽象类】。

您可以说喜欢吃“水果”,但您真正吃“水果”的时候,要么是“苹果”,要么是“橘子”,要么是“香蕉”……但您绝不可能真正吃到一个叫作“水果”的东西。

从实现的角度来看,抽象类与普通类不同的地方在于:

  • 抽象类有的存在抽象方法(方法只有声明,没有定义),子类必须自己定义这些抽象方法,而不能像普通的方法一样,通过继承就可以获得父类的方法。

从这一点来看,抽象类和接口有点类似。

那抽象类和接口有什么区别?

为什么有了接口,还要有抽象类?

因为,

  • 抽象类本质上还是类,强调一组事物的相似性,包括属性和方法的相似性;
  • 而接口只强调方法的相似性,并且仅仅体现在方法声明上的相似性,而没有方法定义上的相似性。

例如。假设我们设计一个游戏,其中使用“苹果”、“橘子”、“香蕉”来做“补血”, “苹果”、“橘子”、“香蕉”都有“颜色”和“重量”这样的属性,但每种水果的补血方式是不一样的。

在这种情况下,使用抽象类可以很好地表达,我们设计一个抽象类“水果”,将“颜色”、“重量”作为“水果”的属性,“获取颜色”、“获取重量”、“减少重量”等方法作为“水果”的方法,将“补血”作为“水果”的抽象方法。这样设计能够大大减少“苹果”、“橘子”、“香蕉”几个普通类的实现工作量,它们只需要实现“补血”方法,其他的属性和方法都只需继承“水果”类即可(代码复用)。

而如果采用接口的方式实现,则“苹果”、“橘子”、“香蕉”每个类都需要自己增加“颜色”、“重量”属性,增加“获取颜色”、“获取重量”、“减少重量”、“补血”等方法,工作量和代码量大大增加。

如上分析,抽象类是一个介于类和接口之间的概念,同时具备类和接口的部分特性。

抽象类:基于类而抽象出来的,用于继承,不能被实例化为具体的对象


封装:保护隐私、隔离关注点,减低复杂度

封装的好处举个例子就明白了。

我们的房子是类的实例(对象),室内的装饰与摆设只能被它的主人欣赏、使用,如果没有四面围墙的遮挡,室内所有人的活动就暴露无疑。

所以,房子这个对象必须加上封装(四面围墙),嘿嘿,不然晚上活动会影响别人的…

public: 不封装,公开的
protected: 对外不公开,但对家人(子类)公开
private:私有的,每个人都有自己的小秘密是不能告诉任何人的

需要保密的时候,用封装。


继承:生物的遗传

继承本身很好理解,和我们日常生活中的“继承”概念基本一样:子承父业!所以,我们可以看到在面向对象的编程语言里面,有了“父类”、“子类”的概念。

但是我个人认为“继承”这个说法并不确切,一般我们理解“继承”都是继承产业、继承财产。

但在面向对象的领域中,并不是“子类”继承了“父类”的产业,而是继承了“父类”的特点,具体来说,就是继承了“属性“和”方法“。

所以,我认为“继承”更加贴切的说法是类似生物学上的“遗传”,按照中国话的说法就是“龙生龙,凤生凤,老鼠生来会打洞”,面向对象的继承意味“子类”遗传了 “父类”的某些属性和方法。

但就像生物学上的进化论一样,有遗传就有变异,正因为有了变异,才会有新的物种产生;有了变异,面向对象才出现了子类。

学好继承记住三句话(如果子类继承于父类):

  • 子类拥有父类非 private 的属性、功能;
  • 子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;
  • 子类还可以以自己的方式实现父类的功能(方法重写);

继承的优点:

  • 继承使得所有子类公共部分都放在了父类(代码复用),另外,继承可使得修改或者扩展继承而来的实现都较为容易。

继承的缺点:

  • 父类变,子类不得不变;
  • 因为父类和子类有很大联系,也增加了耦合性。

对象之间有继承关系的时候,用继承。


多态:指向父类的指针或引用,可以调用子类对象

  • 如果您没有学过设计模式,我写了对于您的理解也是片面的;
  • 如果您学了设计模式,那就不用看我写的了。

所以,所以,我们跳过吧!!!

软件工程 3:模块化设计相关推荐

  1. 软件工程(总体设计②设计原理)

    设计原理 模块化 模块是由边界元素限定的相邻程序元素的序列,而且有一个总体标识符代表它. 按照模块的定义,过程.函数.子程序和宏,都可作为模块. 面向对象方法学中的对象是模块,对象内的方法也是模块.模 ...

  2. [软件工程] 面向对象设计

    面向对象设计 面向对象设计 一. 面向对象设计的准则 (一) 模块化 (二) 抽象 (三) 信息隐藏 (四) 弱耦合 对象之间的耦合可分为两大类: (1) 交互耦合 (2) 继承耦合 (五)强内聚 ( ...

  3. 【软件工程】软件设计概述

    软件设计是一个过程,通过这个过程,创建一个软件工件的规范,目的是实现目标,使用一组原始组件并受到约束.[1]软件设计可以指"所有涉及概念化.框架化.实现.调试的活动,最终修改复杂系统&quo ...

  4. FPGA程序如何模块化设计?

    综合与可综合的HDL设计 综合的定义 综合就是针对给定的电路实现功能和实现此电路的约束条件,如速度.功耗.成本及电路类型等,通过计算机进行优化处理,获得一个能满足上述要求的电路设计方案.  被综合的文 ...

  5. 模拟物流快递系统程序设计java_路辉物流设备:大件快递自动分拣系统的模块化设计...

    物流企业运营所需的能源.劳动力.土地价格持续上涨,依赖"高投入.低产出"传统物流运作模式难以为继,面临着降低成本.提高效率.可持续发展的转型要求.快递行业对于新兴的大件快递消费需求 ...

  6. 基于ASP.NET Core的模块化设计: 虚拟文件系统

    土牛亲自录制的本文介绍视频 Abp中文网(https://cn.abp.io/)提供翻译字幕 基于ASP.NET Core的模块化设计: 虚拟文件系统 简介 创建模块化的应用程序很困难. 构建模块化的 ...

  7. 单片机sleep函数的头文件_单片机代码模块化设计思想浅谈

    前言:前段时间分享的文章[单片机裸机代码框架设计思路],很多读者给我留言,觉得很不错,对于初学者而言,这是一个进阶的技巧,对于我而言,这是对自己总结和表达能力的一个提升. 本文章我们再谈谈单片机代码的 ...

  8. dll模块化设计与编程_FPGA设计原则经验分享

    一.面积和速度如何折中  面积和速度是芯片设计中一对相互制约.影响成本和性能的指标,贯穿FPGA设计的始终.在FPGA设计中,面积是指一个设计消耗的FPGA内部逻辑资源的数量,可以用消耗的触发器和查 ...

  9. 单片机c语言模块化实例程序设计,单片机C语言模块化设计

    原标题:单片机C语言模块化设计 keil中实现 模块化编程.doc 在使用 KEIL 的时候,我们习惯上在一个.c 的文件中把自己要写的东西按照 自己思路的顺序进行顺序书写.这样是很普遍的写法,当程序 ...

最新文章

  1. 我也来搭android环境
  2. 从人肉到智能,阿里运维体系经历了哪些变迁?
  3. 4.7 CNN 特征可视化-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  4. java中的轮子是什么意思_后端的轮子(一) - java后端开发的个人空间 - OSCHINA - 中文开源技术交流社区...
  5. linux卸载python3.6,当python3.6位于/usr/local/bin/python3.6时,如何在ubuntu上卸载它
  6. MyBatis Plus——启动/关闭控制台LOGO
  7. Android ThreadUtil 线程公共类,判断是否在主线程/ 子线程执行 相关操作
  8. codeforces:812(div2):总结
  9. 中英对照 关于计算机的科技英语,《计算机专业英语》(中英文对照).pdf
  10. mysql tomcat 地址池_MySQL tomcat 数据库连接池配置与使用
  11. [leetcode] 95. 不同的二叉搜索树 II
  12. 20170317起mvc功能学习
  13. selenium实现文件上传方法汇总(AutoIt、win32GUI、sengkeys)---基于python
  14. 浅谈Android测试技术
  15. SqlServer2012下载+安装+启动(资源+密钥)
  16. 不伤虫蚁,使虫蚁远离的方法
  17. 外卖也智能!美团骑手智能助手的技术与实践
  18. 写在觉醒时 埋葬过去的堕落
  19. 【转】WPF自定义控件与样式(13)-自定义窗体Window 自适应内容大小消息框MessageBox...
  20. 计算机junit测试类,复利计算器4.0之再遇JUnit

热门文章

  1. 有功功率、无功功率、视在功率 的定义
  2. Android AsyncTask 接口回调
  3. 手机的模拟,有耗电和充电方法, 有电量的属性
  4. 大漠插件问题:解决win10win7win8系统找不到指定的模块,注册不了大漠插件的问题
  5. 营造平安 信息化使公交事业更加人性化
  6. Win10调整各窗口的任务栏位置
  7. a different object with the same identifier value was already associated whith
  8. 技术人生:故事之八 OFFICE是软件打字机?
  9. Android黄油刀插件使用记录
  10. S4HANA 2020输入会计凭证提示需要输入税码的配置