老代码如何改进?正确的函数式编程最重要!

近段时间开始看《代码大全2》,一节中提到“很多好的编程做法都能减轻大脑灰质(指脑力)的负担”,于是我便去搜寻了一下《*公司C语言编程规范》拿来阅读,一下午便收获不少,其中提到几点使我写下此记录。


文中对函数有以下部分的要求

  • 可重入函数应避免使用共享变量,若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。
  • 函数应避免使用全局变量、静态局部变量和IO操作,不可避免的地方应集中使用。
  • 函数的参数个数不超过5个。
  • 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字。

文中对头文件的部分要求

  • 头文件中适合放置接口的声明,不合适放置实现。
  • 禁止头文件循环依赖。
  • 头文件应当自包含。

以上这几点我希望拿出来提示自己在以后编写时要多加注意

  • 全局变量封装成函数
  1. 因为在工程里经常使用构造类型(结构体)来定义全局变量供不同函数使用,我会在头文件定义自己构造类型并在文件开头出定义全局变量,诸如
    //protocol_data.h头文件中定义
    typedef struct my_protocol
    {
    	uint8_t frame_len;
    	uint8_t frame_data[20];
    } MY_Protocol_t;
    //protocol_data.c文件中定义
    MY_Protocol_Typedef Pro_Info;
    
  2. 很多函数都需要读取或修改此全局变量的成员内容,则在头文件中又会加上。
    extern MY_Protocol_Typedef Pro_Info;
    
  3. 于是乎,在其他.c文件函数里很happy地到处调用&Pro_Info,或者Pro_Info.frame_len来修改内容。而这在程序设计上是非常不好的行为,在protocol_data.c里大部分都是相关内容的数据处理函数,Pro_Info可以看作私有成员变量在不断交换着信息,此时外部一个test.c的函数调用它并修改了,对protocol_data.c其他函数的影响可能是巨大的 (尤其在多线程场景下)
  4. 我认为可以采用的规范做法是:
    //在protocol_data.c文件中定义
    static MY_Protocol_Typedef Pro_Info;
    
    MY_Protocol_Typedef *Pro_info_f(void)
    {
    	return &Pro_Info;
    }
    //在protocol_data.h头文件中声明
    MY_Protocol_Typedef *Pro_info_f(void);
    
    • 此时在test.c函数各个地方使用局部指针变量来接受Pro_info_f()的返回结果,再做下一步操作。
  • 通常我们可能会有很多构造类型的变量在各个函数间使用,使用函数封装成接口对这些变量访问会清晰并安全很多。
  • 因为我们的文件可能要提供给第三方,必然把c文件封装成库的形式留出头文件给用户,这时如果有很多全局变量,用户访问起来也变得方便并可控不少了。

  • 模块间函数解耦
    • 以前写的代码,由于模块间互相要使用到很多全局变量,不知不觉间耦合性就大大增加了,常常是一个变量的指针从最外层函数进入一直传到4、5层的内部去使用。其实很多设计方式有问题,根本不需要这样处理,看示例代码就明白了。
    //A模块中有两个函数
    uint8_t Mate_Respond_process(Network_Protocol_TypDef* pNet, Device_Info* pSwitch, Device_TypDef *p_device)
    {
    	...
    	if(PICK_Switch(p_device, &self_num, 1))
    		return 0;
    	...
    	if(...)
    		p_buf[VAL] = p_device->val & 0X0F;
    	else
    		...
    		p_buf[VAL + 1] = p_device->val & 0X0F;
    	...
    }
    
    uint8_t PICK_Switch(Device_TypDef *p_device, uint8_t* self_num, uint8_t Mode)
    {
    	...
    	if(...)
    		which_node = p_device->MateFlag & 0x0f;
    	else
    		which_node = p_device->ChangeFlag & 0x0f;
    	....
    }
    
    • 可以很明显看出,p_device被最外层函数Mate_Respond_process作为形参传入,到了PICK_Switch时又一次被传入。
    • 其实PICK_Switch仅仅读取了一个成员的值,却完整传入了整个Device_TypDef类型的指针p_device而恰恰这个构造类型在其他模块头文件中被定义PICK_Switch也被声明为外部函数;因此还要在A模块的头文件引入另一个B模块的头文件来标识Device_TypDef类型定义的位置。如果B模块有所变动会导致A模块也被重新编译,如此的操作在大工程里一多,编译时间的增加是非常巨大的。
      • 完全可以用一个确切的值传入PICK_Switch作为参数,清爽地定义一个标准类型的uint8_t which_node_val,就不需要引入B模块的头文件了。
    • Mate_Respond_processp_device的后面两次使用也都是简单的读取了成员的值,用上面同样方法处理就可以少一个传参,这就减少了函数使用的复杂度。很多类似的函数都可以这样来简化解耦,对于一些大型工程的维护是非常友好且必要的。

先记录到这里,吃饭去啦…

发布了22 篇原创文章 · 获赞 29 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Emmy_kanly/article/details/98502468