C语言的设计模式

简介: C语言的设计模式

单一职责

单一职责原则:通常的定义是只专注于做一件事和仅有一个引起它变化的原因。对于接口、实现、函数级别往往我们比较容易关注单一职责,大家谈的也比较多,但对于返回值、参数可能不会有太多的人关注。但往往就是这些不符合单一职责原则的设计可能导致一些很难发现的BUG。看看下面这段代码:

pBuf = (byte*)realloc( pBuf, size);
if( pbBuf != NULL )
{
  TODO...
}

可能很多人一眼看上去并没有什么问题,先让我们看看这个库函数的定义:

函数简介

  • 原型
extern void *realloc(void *mem_address, unsigned int newsize);

语法:指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)。


  • 功能


     先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将mem_address返回,如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域,同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。


  • 返回值


     如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。正常情况下pBuf是新空间的地址没有任何问题,但我们考虑下如果分配失败了呢,pBuf会被赋值成NULL,pBuf原指向的地址空间就没有指针指向了,造成了内存泄露。这种问题往往很难定位。熟悉realloc机制的人可能对这个问题很不屑,认为高手不会犯这些错误。但我们可以想下有没有办法设计一个好的接口让菜鸟也写出不会出错的代码呢。假设这个库函数的接口是这样的呢:

函数简介


  • 原型


       extern flag realloc(void **ppMem_address, unsigned int newsize);


  • 语法


     返回值 =(数据类型*)realloc(要改变内存大小的指针名,新的大小)。


  • 返回值

     如果重新分配成功则返回指True(ppMem_address保存新分配空间地址),否则返回False(ppMem_address保存老空间地址)。


相信任何一个使用这个接口的人都会写出下面的代码:

if( True == realloc( &pBuf, size))
{
  TODO...
}
else
{
    TODO...
}

为什么有人会犯pBuf = (byte*)realloc( pBuf, size);这种错误?因为他只关注了realloc返回值是一个地址,没有关注该返回值还有错误识别的功能,换句话来说这个库函数的返回值不具备单一职责,导致了可能的错误使用。如果使用改进后的接口,因为返回值只有一个判断分配成功与否的功能,相信没有人还会用错。


我们再仔细看看我们新的接口,总觉得似乎有什么地方还是不对,看到void **ppMem_address可能要想一下明白,这个参数既是入参又是出参,它承担了原始地址的输入和新地址的输出,这不又违反了单一职责吗?好吧我们再改进一下:

函数简介

  • 原型 extern flag realloc(void *pIn_Mem_address,void **ppOut_Mem_address, unsigned int newsize);
  • 语法 返回值 =(数据类型*)realloc(要改变内存大小的指针名,新的内存指针名,新的大小)。
  • 返回值 如果重新分配成功则返回指True,否则返回False。


现在这个接口就算一个初次看到的人也应该大概知道什么意思,相信也不会写出什么带BUG的代码,因为函数的参数、返回值都具有单一的功能,通过返回值来判断分配成功与否,通过出参来获取地址。一切看起来都很清晰。


在C库中还有很多类似的函数,如果当初的设计人员能多考虑单一职责,也许现在的系统中就会少了很多隐藏的BUG,接口永远是给别人使用的,一定要把使用者当成傻瓜,也许才能设计出好的接口。

面向对象机制的实现

为什么要用C来模拟面向对象的机制,在实际的工作中我们往往在感慨一些面向对象的经典设计模式由于C语言的限制无法使用,其实通过简单的模拟面向对象的行为,在C语言中也可以使用这些模式。

1:类的构建

类描述了所创建的对象共同的属性和方法。我们在一个源文件中通过把数据和操作进行适当的组织来完成类的模拟。

/*类的数据*/
typedef struct SQUARE_S SQUARE_T;
struct SQUARE_S
{
  void (*draw)(void*);
  int sideLen;
};
/*类的方法*/
static void draw(void* pObj)
{
  SQUARE_T* pSqr = (SQUARE_T*)pObj;
  printf("Draw Square len is %d\n",pSqr->sideLen);
}

如上所示,一个正方形的类我们用一个结构体SQUARE_T来表示正方形的属性,draw是其中的一个方法。

2:类的封装性

类的封装一般要求对细节的隐藏并且提供指定的方法供调用者使用,在SQUARE这个类中,sideLen是图形的细节,只需要提供一个draw接口给调用者。因此在提供给外部调用的接口头文件中构建如下的接口。

typedef struct SHAPE_S SHAPE;
struct SHAPE_S
{
  void (*draw)(void*);
};

通过定义不同的数据结构来达到数据隐藏的目的,如下图所示,对外接口中只能看到draw,内部实现中可以看到draw和sideLen。

3:多态的模拟

多态无疑是面向对象语言的很重要的一个机制,很多面向对象的设计模式都是以多态为基础,C语言并不支持多态,导致很多设计模式都无法直接使用。


一个典型的多态例子,通过声明一个SHAPE接口,根据实例化对象类型的不同,pShape在运行时动态的表现不同的行为。

SHAPE* pShape = NULL; //一个形状接口
pShape = (SHAPE*)Ins(SQUARE,2); //实例化为一个正方形
pShape->draw(pShape); //pShape表现为正方形的行为

多态机制的实现依赖函数指针,在每个类的构造函数中把相关接口用具体的函数地址填充,这样在实例化一个对象的时候我们才绑定了其具体的操作,也就是所谓的动态绑定。

/*每个类的构造函数*/
static void* Constructor(void* pObj,va_list* pData)
{
    SQUARE_T* pSquare = (SQUARE_T*)pObj;
    pSquare->draw = draw; //具体行为的填充
    pSquare->sideLen = va_arg(*pData,int);
    return pObj;
}

4:对象的创建

有了类,我们需要实例化为可以运行的对象,实例化主要的工作是分配内存、动态绑定、数据初始化等工作。

void* Ins(const void* pClass,...)
{
  CLASS* pCls = NULL;
  void* pObj = NULL;
  va_list vaList = NULL;
  pCls = (CLASS* )pClass;
  pObj = malloc(pCls->classSize);
  memset(pObj,0,pCls->classSize);
  va_start(vaList,pClass);
  pObj = pCls->Constructor(pObj,&vaList);
  return pObj;
}

接口隔离

定义为客户端不应该依赖它不需用的接口,在C语言中我们可以把头文件看成一个模块的接口,根据接口隔离原则也就是说这个头文件中只能包含外部需要的接口,但在实际的项目中往往头文件都不符合接口隔离原则。

1:内、外部接口的隔离

头文件中通常包含了模块内部接口(内部类型定义、内部接口声明)和外部接口(外部接口声明)


假设moudle模块对外提供一个fun1接口,模块内部实现需要定义一个结构类型,一般的实现如下:

/*moudle.h*/
typedef struct str_s str_t;
struct str_s
{
  int a;
  int b;
};
void fun1();
/*moudle.c*/
#include "moudle.h"
void fun1()
{
  str_t s = {0};
  TODO...
}

客户端在使用接口的时候需要包含moudle.h文件,而该接口并不符合接口的隔离,其内部包含了客户并不需要的一些定义。为了解决这个问题我们可以通过定义不同的头文件来隔离接口,moudle.h定义外部的接口,moudle.inc定义内部接口

/*moudle.h*/
void fun1();
/*moudle.inc*/
typedef struct str_s str_t;
struct str_s
{
  int a;
  int b;
};
/*moudle.c*/
#include "moudle.inc"
void fun1()
{
  str_t s = {0};
  TODO...
};

moudle.h包含外部模块需要的接口,外部模块包含moudle.h,moudle.inc包含内部模块需要的接口,在模块内部包含moudle.inc。通过查看模块的.inc和.h文件,我们就可以清晰的理解模块对外和对内提供了什么接口。

2:避免万能头文件的使用

在实际项目中我们经常可以看到一些头文件包含了所有模块的接口声明,客户端只需要包含这个头文件就可以使用任何接口了。

/*global.h*/
#inlcude "moudle1.h"
#inlcude "moudle2.h"
#inlcude "moudle3.h"
....
#inlcude "moudlen.h"

可能带来如下问题:


会显著的增加编译时间,如果项目大,可能大部分的编译时间都花在展开头文件(笔者一个项目测试80%左右的时间)。


不利于代码的框架的理解,客户端无法从包含的头文件中清晰的看到依赖什么外部模块。

3:如果没有隔离接口可能会导致一些误操作:

一个数据获取模块提供两个接口分别从网络和本地缓存获取数据,后台管理模块使用网络接口定时获取数据更新缓存,前台模块使用缓存接口快速获取数据显示,由于没有对接口隔离,后期的维护人员可能并不清楚开始的设计,在前台模块中直接使用网络接口来获取数据显示,导致界面延迟严重。如果一开始就把接口分离,给前台模块提供本地缓存接口,给后台模块提供网络接口,就不会导致问题的出现。

往期精彩

嵌入式系统软件架构设计(长篇深度好文)


分享一个非常有用且简单C语言测试框架


分享一个自己量产项目上的集成测试软件MTTEST


使您的软件运行起来: 防止缓冲区溢出(C语言精华帖)

目录
相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
10天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
27 6
|
30天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
36 10
|
24天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
29天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
58 7
|
29天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
30 4
|
1月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
1月前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
33 2
|
1月前
|
Java 编译器 C语言
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
【一步一步了解Java系列】:Java中的方法对标C语言中的函数
22 3