逸珺 · 2020年09月15日

由static来谈谈模块封装

首发:嵌入式客栈
作者:逸珺

导读

static的用法对于很多刚刚开始接触开发的朋友来说,可能没理解其真正的用途,虽说这个是老生常谈的话题,但这也是高频面试要点,所以本文来聊聊。

最近有点小忙,更文慢了些,抱歉。

先谈存储类型

存储类型表示变量的可见性和位置。它告诉可以从代码的哪一部分访问变量。存储类用于描述以下内容:

  • 变量的作用域(scope),作用域指存取变量的代码范围。
  • 变量从哪里分配存储内存。
  • 变量的初始化值。
  • 变量的生命周期(lifetime),生命周期指存取变量的时间范围,从程序运行时角度去考察变量的。

那么有哪些存储类型呢?下面几个词是C语言描述存储类型的关键字:

  • auto : 自动型,为变量的默认存储方式,作用域从定义点到该局部程序块尾部,分配存储在栈内,生命周期程序运行至定义点出生,到程序运行退出该块时消亡。
  • extern:外部型,作用域为整个程序,其分配存储在数据段,生命周期为整个程序运行生命周期。
  • static:静态型,其分配存储在数据段,故其生命周期为整个程序运行生命周期,其作用域分两种情况:
  • 定义在文件中,则作用域为定义点至该文件尾部。
  • 定义在函数中,则作用域为定义点至该函数尾部。
  • register:寄存器型,寄存器存储类用于定义应存储在寄存器而不是RAM中的局部变量。这意味着该变量的最大位长度等于寄存器的位长度(通常是一个字),并且不能对其使用'&'运算符(因为它没有存储在内存中)。 仅应用于需要快速访问的变量(例如计数器)。还应注意定义“register”并不意味着变量将一定会存储在寄存器中。它可能根据不同硬件和实现限制存储在寄存器中。

由static来谈C封装

static用在文件中修饰变量,如下代码:

/*这是某模块文件,比如叫senor.c*/  
#include "sensor.h"  
static float sensor_value;  
static float filter(float in)  
{  
    float out;  
      
    /*这里实现滤波计算*/  
    ......  
          
    return out;  
}  
void update_sensor_exe(void)  
{  
     float temp   = adc_read();  
     sensor_value = fileter(temp);  
}  
  
float get_sensor_value(void)  
{  
    return sensor_value;  
}  

这里定义其头文件,比如sensor.h

#ifdef  __SENSOR_H__  
#define __SENSOR_H__  
  
void update_sensor_exe(void);  
  
#endif  

用一个UML图来描述一下这个模块:

640.png

这样使用,是不是有点模块封装的意思呢,来总结一下:

  • 利用static定义属于模块的变量,可以将属于模块属性隐藏在模块内部,对外部不见,是不是有点类似对象语言中的private变量的赶脚呢?
  • static修饰函数成局部函数是不是也相当于面向对象语言中一个类的私有方法呢?如此,外部程序是无法调用filter函数的。
  • update\_sensor\_exe/get\_sensor\_value 为模块对外接口,这样一来使用者就可以不关心模块内部究竟是怎么做的,可以看成是个黑盒子,只需要知道update\_sensor\_exe更新了传感器采集,而调用get\_sensor\_value则可以返回获取到当前的测量数据。
  • static修饰变量可为广义对象,比如struct。这样可以将相关属性更为紧凑的封装,事实上这种用法是非常好的用法,也被广为使用。
  • C语言在多人协同开发时,利用static的这种用法时,系统设计人员定义模块或子系统接口,可以很好的解决同名变量/函数冲突,有利于协同并行开发。可以隐藏各自的实现细节,各自造各自的轮子,造整车的管造整车,从而较易实现系统集成。

对上述代码稍作总结,对一个使用该模块的程序员而言来看模块,就是下面这样一个视图:

640-1.png

static在函数内

#include   
int fun1()   
{   
   int count = 0;   
   count++;   
   return count;   
}   
  
int fun2()   
{   
   static int count = 0;   
   count++;   
   return count;   
}   
  
int main()   
{   
   printf("fun1=%d\n", fun1());   
   printf("fun1=%d\n", fun1());  
      
   printf("fun2=%d\n", fun2());   
   printf("fun2=%d\n", fun2());     
   return 0;   
}  

由于一个自动型变量,一个是静态变量,存储位置不一样,生命周期也不一样,所以运行结果也不一样。

fun1=1  
fun1=1  
fun2=1  
fun2=2  

对于把函数内部变量定义为static型,个人建议是如某特性只与函数内需求或特性相关,则可以这样使用,如果不是则不建议将过多变量定义成函数内部静态变量。当然事无绝对,这个使用起来还是很灵活的。举个栗子:

  • 比如一个函数内部某些操作,在整个程序运行生命周期,只允许运行一次,这种特性属于私有特性,个人会采用下面这种策略。
void fun()  
{  
     static bool called = false;  
     if(!called)  
     {  
        called = true;  
        /*应用代码A*/  
        .....  
     }  
       
     .....  
}  

这样当该函数在第一次运行时,将会调用应用代码A块,然后将标志设置为true,由于该变量生命周期为整个程序的生命周期,则该函数下次进入时,将不会调用应用代码A块。当然如果把这个标志用模块静态变量或者全局变量标记从功能上是一样的,这样放入内部的好处是这种需求的scope就是该函数内部,所以作用域与待实现的需求比较好的匹配。

总结一下

由于C语言不是对象语言,如能很好利用static关键字的语言特性,也可以实现些封装属性、开放接口的对象思想。当然C语言的对象编程策略绝不仅限于这一点。如能善用一些语言特点将会使代码变得更加紧凑、优雅。本文做了些简单示例总结,当然对于软件大牛而言,则显得颇为粗浅了。

本文辛苦原创总结,如果觉得有价值也请帮忙点赞/转发支持,不胜感激!

推荐阅读

推荐阅读
关注数
2891
内容数
284
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息