专栏首页嵌入式大杂烩C语言代码优化的一些经验及小技巧(四)

C语言代码优化的一些经验及小技巧(四)

无限循环优先选用for(;;),而不是while(1)

在C语言中,最常用的无限循环语句主要有两种:while(1)for(;;)。从功能上讲, 这两种语句的效果完全一样。那么,我们究竟该选择哪一种呢?

其实,for(;;)语句运行速度要快一些。按照for的 语法规则,两个分号分开的是3个表达式。现在表达式为空,很自然地被编译成无条件的跳转(即无条件循环,不用判断条件)。如代码for(;;)Microsoft Visual Studio 2010 集成开发环境VC++的Debug模式下将生成如下汇编代码:

for(;;)
00931451 jmp main+41h (931451h)

相比之下,while语句就不一样了。按照while的语法规则,while()语句中必须有一个 表达式(这里是1 )判断条件,生成的代码用它进行条件跳转。即while语句()属于有条件循环,有条件就要判断条件是否成立,所以其相对于for(;;)语句需要多几条指令。如代码 while (1)Microsoft Visual Studio 2010集成开发环境VC++的Debug模式下将生成如下汇 编代码:

while(1)
011A1451 mov   eax,1
011A1456 test  eax,eax
011A1458 je    main+55h (11A1465h)
011A1463 jmp   main+41h (11A1451h)

根据上面的分析结果,很显然,for(;;)语句指令少,不占用寄存器,而且没有判断、 跳转指令。当然,如果从实际的编译结果来看,两者的效果常常是一样的,因为大部分编译 器都会对while (1)语句做一定的优化。但是,这还需要取决于编译器。因此,我们还是应该优先选用for(;;)语句。

没有参数的函数必须用void填充

在C语言中,void的作用主要有两个:

1、对函数返回值的限定。
2、对函数参数的限定。

看一个示例函数:

int f()
{
    return 100;
}

从表面看,函数f()没有参数,也就是说,它不允许接受参数。但事实并非如此,我们来验证一下:

#include <stdio.h>
int f()
{
    return 100;
}

int main(void)
{
    printf("%d\n", f(666));
    return 0;
}

编译、运行结果为:

可见,使用GCC可正常通过编译,这说明可以向无参数的函数传递参数。但是,需要注意的是,在一些IDE中不能通过编译。

所以,为了提高程序的统一性、安全性与可读性。我们对没有参数的函数必须使用void进行填充。我们使用void填充上面的f函数之后,编译就不通过了,报错如下:

尽可能为简单功能编写函数

有时候,我们需要用函数去封装仅用一两行代码就可完成的功能。对于这样的函数,单 从代码最上看,好像没有什么封装的必要。但是,用函数可使其功能明确化、具体化,从而 增加程序可读性,并且也方便代码的维护与测试。示例代码如下:

int Max(int x,int y)
{
    return (x>y? x : y);
}
int Min(int x,int y)
{
    return (x<y?x:y);
}

当然,也可以使用宏来代替上面的函数,代码如下:

#define MAX(x,y)  (((x) > (y)) ? (x) : (y))
#define MIN(x,y)  (((x) < (y)) ? (x) : (y))

在C程序中,我们可以适当地用宏代码来提高执行效率。宏代码本身不是函数,但使用起来与函数相似。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了运行速度。但是,使用宏代码最大的缺点就是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。因此, 尽管看起来宏要比函数简单得多,但还是建议使用函数的形式来封装这些简单功能的代码。

函数地抽象级别应在同一个层次

先来看下面一段示例代码:

void Init(void)
{
    /* 本地初始化 */
    ......
    /* 远程初始化 */
    InitRemote();
}

void InitRemote(void)
{
    /* 远程初始化 */
    ......
}

上面地Init函数主要完成本地初始化与远程初始化工作,在其功能上没有什么不妥之处。但从设计观点看,却存在这一定缺陷。因为本地初始化与远程初始化层次相当,本地初始化也应当作为独立的函数存在。应改为:

void Init(void)
{
    /* 本地初始化 */
    InitLocal();
    /* 远程初始化 */
    InitRemote();
}

void InitLocal(void)
{
    /* 本地初始化 */
    ......
}

void InitRemote(void)
{
    /* 远程初始化 */
    ......
}

尽量避免在非调度函数中使用控制参数

在函数设计中,我们可以将函数简单地分为两大类:调度函数与非调度函数(非调度函数一般也称为功能函数或实现函数)。

所谓的调度函数是指根据输入的消息类型或控制命令来启动相应的功能实体(即函数或过程)的函数。调度函数本身不能提供功能实现,相反,它必须委托给实现函数来完成具体的功能。也就是说,调度型函数永远只关注“what to do”,而“how to do”则是由实现函数来关心的,调度函数不需要关心“how to do”。这种调度函数与实现函数的分离设计也满足了单一职责的原则,即调度的不实现,实现的不调度。

对调度函数来讲,控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。然而,如果在非调度函数中也使用控制参数来决定具体怎样工作,那么这样做无疑会增加函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一, 违背了函数功能的单一原则。示例代码如下:

int Calculate( int a, int b, const int calculate_flag )
{
    int sum = 0;
    switch(calculate_flag)
    {
        case 1: 
            sum = a + b; 
            break;
        case 2:
            sum = a - b;
        case 3: 
            sum = a * b; 
            break;
        case 4: 
            sum = a / b; 
            break;
        default: 
            printf("error\n");
            break;
    }
    return sum;
}

上面的函数虽然看起来很简洁,实际上这种设计是不合理的。由于控制参数calculate_flag的原因,使函数间的耦合度增大,也违背了函数的功能单一原则。因此,不如分为如下4个函数清晰,示例代码如下:

int Add(int a,int b)
{
    return a + b;
}
int Sub(int a,int b)
{
    return a - b;
}
int Mul(int a,int b)
{
    return a * b;
}
int Div(int a,int b)
{
    return a / b;
}

以上就是本次的分享,如有错误,欢迎指出!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【C语言笔记】函数参数压栈的顺序?

    按照日常习惯来看,C语言的函数参数压栈顺序是从左到右吧?但是事实却是相反的,C语言函数参数压栈顺序是从右到左的。下面看一个程序:

    正念君
  • 【C语言笔记】指针函数与函数指针?

    由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。即:int *(pfun(int, int));

    正念君
  • 【C语言笔记】函数指针作为函数的参数

    函数指针有两种常用的用法,一种是作为结构体成员,关于函数指针作为结构体成员的用法可移步至上一篇【C语言笔记】函数指针作为结构体成员进行查看。另一种是函数指针作为...

    正念君
  • 《Go语言程序设计》读书笔记(二)函数

    《Go 语言程序设计》在线阅读地址:https://yar999.gitbooks.io/gopl-zh/content/

    KevinYan
  • cf666E. Forensic Examination(广义后缀自动机 线段树合并)

    考虑把询问离线,然后把\(S\)拿到自动机上跑,同时维护一下最长能匹配的位置,对于每个以\(i\)位置为右端点的询问我们需要找到\(len\)最小的状态满足\(...

    attack
  • 栈(stack)

    栈是一个先入后出的有序列表,栈中的元素插入和删除只能在线性表的同一端进行。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底。...

    桑鱼
  • go 学习笔记之学习函数式编程前不要忘了函数基础 原

    在编程世界中向来就没有一家独大的编程风格,至少目前还是百家争鸣的春秋战国,除了众所周知的面向对象编程还有日渐流行的函数式编程,当然这也是本系列文章的重点.

    雪之梦技术驿站
  • BZOJ3413: 匹配(后缀自动机 线段树合并)

    首先可以转化一下模型(想不到qwq):问题可以转化为统计\(B\)中每个前缀在\(A\)中出现的次数。(画一画就出来了)

    attack
  • 线性查找

      线性查找也叫顺序查找,这是最基本的一种查找方法,从给定的值中进行搜索,从一端开始逐一检查每个元素,直到找到所需元素的过程。   如果元素个数为 N,那么线性...

    IT可乐
  • LeetCode77. 组合

     常规dfs题,首先在dfs函数中判断边界条件,因为只要取到k个数就够了,所以边界条件当k用完,也就是k==0就应该return了。然后再看dfs函数内部的实现...

    mathor

扫码关注云+社区

领取腾讯云代金券