![de8d9e5a735bc98a491c70e1c756040a.png](https://i-blog.csdnimg.cn/blog_migrate/b6e00a9c5e5c7ea8d2f9677200ee0394.jpeg)
做为一个CBD(code base design基于代码的设计)开发者,如果你维护的程序是以万行为单位来计算,那么你不得不面对一个问题全局变量global variable。在2018版功能安全标准ISO2626-6 Table6 1e中有如下描述(针对ASILB级以上产品):
Avoid global variables or else justify their usage(避免使用全局变量,如果必须使用,请判断并说明)
而 在《The Art of Designing Embedded Systems》一书中作者也写道“If God didn't want us to use global variables, he wouldn't have invented them. Rather than disappoint God, use as many globals as possible假如上帝不希望我们使用全局变量,就不会创造它,如果想让上帝失望,那就尽可能多的使用全局变量.”
为什么大家对全局变量都是如此又爱又恨呢?让我们先看看全局变量会带给我们那些麻烦:
1、全局变量的使用会破坏信息隐藏原则,让你持续违背功能安全标准(ISO2626-6 Table6 1h 中要求No hidden data flow or cnotrol flow没有隐藏的数据流和控制流)如果你的代码足够长、足够复杂的话,最后真的只有天知道什么地方会更改全局变量。
2、全局变量会导致模块之间的强耦合。很不幸,说到这一条,全局变量将再一次违背功能安全标准,( ISO2626-6 Table3 1e 要求 Loose coupling between software components软件组件之间要低耦合 ),设计再好的架构在全局变量面前都像是一坨面条,没有人知道那条面条会通向哪里。
3、全局变量有可能导致系统奔溃,说到这一条,基本上几乎所有功能安全标准都会违背,已经没有办法去确切的列出,到底那一条标准又被违反了。
4、全局变量会使系统维护困难。全局变量的使用在初期编码阶段可谓是一日千里,可以快速实现功能,但是,到了后期尤其是复杂代码除了当初写代码的人,几乎没有人可以真正搞懂这些代码。因为这些代码已经投入了大量的人力物力来开发,考虑到沉没成本,公司有时候又不希望重写。这时候所有人都会面临客户不停抱怨,开发与项目售后累成狗但是代码是一团乱麻的尴尬情况。
说了这么多缺点,那么,我们不使用全局变量不就没有这些问题了。程序员会有疑问如果不使用全局变量,如何进行跨模块的信息传递?(模块之间的信息流如下图所示)
![2c73feccfeba1d8685346bd803bc4dc1.png](https://i-blog.csdnimg.cn/blog_migrate/29185937907e4f74f2321e54c8030004.png)
这个问题也是我们这篇文章讨论的重点,封装。本文以C语言为例进行解释,最直观的办法是限制变量的作用域,如下文所示:
当我们在模块A中使用变量 我们最好将变量定义在 模块A中同时用Static进行声明:
❌int Module_A data;
✔ static int Module_A data;
涉及跨模块的信息,我们定义好读写函数:
int Get_ModuleA_data(void);
{
return Module_A_data;
}
void Set_ModuleA_data(int var);
{
ModuleA_data = var;
}
上面提到的关于全局变量的诸多问题,主要是出现在对全局变量进行更改的环节,单纯的把全局变量定义在函数中并不能解决问题,我们还需要对涉及变量更改的Set函数进行控制。假如你不希望任何中断打断你的操作,你可以使能中断如下所示
void Set_ModuleA_data(int var);
{
/*Disable Interrupt*/
ModuleA_data = var;
/*Enable Interrupt*/
}
如果你定义的变量需要跨模块传输,你可以基于你的设计设置写入条件或者预编译条件。
void Set_ModuleA_data(int var);
{
/*if (condition)*/
ModuleA_data = var;
}
或者
#if (条件)
void Set_ModuleA_data(int var);
{
/*if (condition)*/
ModuleA_data = var;
}
#endif当然你还可以通过写入计数的方式,来控制写入操作进行统计:
void Set_ModuleA_data(int var);
{
ModuleA_data = var;
/*update counter*/
}为了能够进一步对变量进行控制,并且增加可读性的话,可以进一步封装。
在模块对应的头文件中设置自定义类型,经所有变量约束在一起:
typedef struct/*此处附赠一条小建议 MISRAC2012不建议使用tag即结构体名称 */
{
int ModuleA_data1;
char ModuleA_data2;
.......
}ModuleA_t
在模块函数中声明:
static ModuleA_t A_parameter;/*用static 将变量限制在模块内部*/
当你需要用这些变量时,可以用如下封装:
int Get_ModuleA_data(void);
{
return A_parameter.Module_A_data1;
}
void Set_ModuleA_data(int var);
{
A_parameter.Module_A_data1 = var;
}
然后,结合刚刚提到的控制方法就可以控制变量的使用了。这需要在进一步强调一点,如果你的模块十分复杂,比如一个Module有上万行,那么你需要再认真的考虑一下模块该如何划分了,对于过于复杂的模块,即便是你static了全局变量,但是在你的C文件中依然存在刚刚提到的那些风险。所以此处我们提到的全局变量安全使用是在你的模块与架构本身已经优化,且模块本身规模可控为前提的。本文处理的主要是针对Extern 出来的那些全局变量,本文提到的做法可以做到没有global variable但是,对于模块内部依然要提高警惕,设计过于复杂的module即便是用了static,依然会风险依旧。
在文章即将结尾的地方再补充两点:
1,为了进一步增加函数的可读性,可以参考autosar标准在软件开发阶段,设计一个中间层,将需要跨模块传输的变量全部放在中间层中,然后通过条件编译等手段进行限制。
2,如果你的代码希望符合功能安全,那么就不要用刚刚示例代码这种int,char的类型定义,需要改用自定义类型并且结合你所使用的芯片位宽做如下定义;
typedef signed char Sint8_t