现在的位置: 首页 > 综合 > 正文

C 语言高效编程的几招——A few action of efficient C language programming

2017年12月16日 ⁄ 综合 ⁄ 共 9475字 ⁄ 字号 评论关闭

   
编写高效简洁的C 语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。

 

1
招:以空间换时间

   
计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决问题的第1
--以空间换时间。

   
例如:字符串的赋值。

方法A,通常的办法:

   
#define LEN 32

   
char string1 [LEN];

   
memset (string1,0,LEN);

   
strcpy (string1,"This is an example!!"

方法B:

   
const char string2[LEN]="This is an example!"

   
char*cp;

   
cp=string2;

   
(
使用的时候可以直接用指针来操作。)

   
从上面的例子可以看出,A
B 的效率是不能比的。在同样的存储空间下,B 直接使用指针就可以操作了,而A
需要调用两个字符函数才能完成。B 的缺点在于灵活性没有A
好。在需要频繁更改一个字符串内容的时候,A 具有更好的灵活性;

   
如果采用方法B,则需要预存许多字符串,虽然占用了 大量的内存,但是获得了程序执行的高效率。如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。

   

   
该招数的边招--使用宏函数而不是函数。举例如下:

方法C:

   
#define bwMCDR2_ADDRESS 4

   
#define bsMCDR2_ADDRESS 17

   
int BIT_MASK (int_bf)

   
{

   
    return ((IU<<(bw##_bf))-1)<<(bs##_bf);

   
}

   
void SET_BITS(int_dst,int_bf,int_val)

   
{

   
    _dst=((_dst) & ~ (BIT_MASK(_bf)))I\

   
    (((_val)<<<(bs##_bf))&(BIT_MASK(_bf)))

   
}

   
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumber);

方法D:

   
#define bwMCDR2_ADDRESS 4

   
#define bsMCDR2_ADDRESS 17

   
#define bmMCDR2_ADDRESS BIT_MASK

   
(MCDR2_ADDRESS)

   
#define BIT_MASK(_bf)(((1U<<(bw##_bf))-1)<<

   
(bs##_bf)

   
#define SET_BITS(_dst,_bf,_val)\

   
((_dst)=((_dst)&~(BIT_MASK(_bf)))I

   
(((_val)<<(bs##_bf))&(BIT_MASK(_bf))))

   
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumber);

   
函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的。

   
栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU
也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU 时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。

   
D
方法是我看到的最好的置位操作函数,是ARM
公司源码的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有的位操作功能。C 方法是其变体,其中滋味还需大家仔细体会。

 

2
招:数学方法解决问题

   
现在我们演绎高效C 语言编写的第二招--采用数学方法来解决问题。

数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序的执行效率有数量级的提高。

   
举例如下,求1~100
的和。

方法E

   
int I,j;

方法F

   
int I;

   
for (I=1; I<=100; I++)

   
{

   
    j+=I;

   
}

   
I=(100*(1+100))/2

   
这个例子是我印象最深的一个数学用例,是我的计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用公式Nx(N+1)/2
来解决这个问题。方法E 循环了100 次才解决问题,也就是说最少用了100
个赋值、100 个判断、200个加法(I
j);而方法F 仅仅用了1
个加法、1 个乘法、1 次除法。效果自然不言而喻。所以,现在我在编程序的时候,更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。

 

3
招:使用位操作

   
实现高效的C 语言编写的第三招--使用位操作,减少除法和取模的运算。在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用位运算来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例台如下:

 

方法G

   
int I,J;

   
I=257/8;

   
J=456%32;

 

方法H

   
int I,J;

   
I=257>>3;

   
J=456-(456>>4<<4);

   
在字面上好象H G
麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法 G 调用了基本的取模函数和除法函数,既有函数调用,还有很多汇编代码和寄存器参与运算;而方法H
则仅仅是几句相关的汇编,代码更简洁、效率更高。当然,由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的MS C,ARM C
来看,效率的差距还是不小。相关汇编代码就不在这里列举了。

   
运用这招需要注意的是,因为CPU
的不同而产生的问题。比如说,在PC 上用这招编写的程序,并在PC
上调试通过,在移植到一个16 位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。

 

4
招:汇编嵌入高效C 语言编程的必杀技

第四招--嵌入汇编。

   
在熟悉汇编语言的人眼里,C
语言编写的程序都是垃圾。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方法--嵌入汇编、混合编程。

   
举例如下,将数组一赋值给数组二,要求每一个字节都相符。char string1[1024], string2[1024];

方法I

   
int I;

   
for (I=0; I<1024; I++)

   
*(string2+I)=*(string1+I)

方法J

   
#int I;

   
for(I=0; I<1024; I++)

   
*(string2+I)=*(string1+I);

   
#else

   
#ifdef_ARM_

   
_asm

   
{

   
    MOV R0,string1

   
    MOV R1,string2

   
    MOV R2,#0

   
loop:

   
    LDMIA R0!,[R3-R11]

   
    STMIA R1!,[R3-R11]

   
    ADD R2,R2,#8

   
    CMP R2, #400

   
    BNE loop

   
}

   
#endif

   
方法I
是最常见的方法使用了1024
次循环方法J
则根据平台不同做了区分ARM
平台下用嵌入汇编仅用128次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为0
的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于LCD 数据的拷贝过程。根据不同的CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。

   
虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在不同平台移植的过程中,卧虎藏龙、险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才可以采用。

   
使用C 语言进行高效率编程,我的体会仅此而已。在此已本文抛砖引玉,还请各位高手共同切磋。希望各位能给出更好的方法,大家一起提高我们的编程技巧。

   

   
这个问题用几个解决方案。我首选的方案是:

   
while(1)

   
{

   
    ?

   
}

一些程序员更喜欢如下方案:

   
for(;;)

   
{

   
    ?

   
}

   
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。

第三个方案是用 goto

Loop:

...

goto Loop;

   
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN
程序员。

 

数据声明(Data declarations

5.
用变量a 给出下面的定义

   
a)
一个整型数(An integer

   
b)
一个指向整型数的指针( A pointer to an integer

   
c)
一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an integer

   
d)
一个有10 个整型数的数组( An array of 10 integers

   
e)
一个有10 个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers

   
f)
一个指向有10 个整型数数组的指针( A pointer to an array of 10 integers

   
g)
一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument

and returns an integer

   
h)
一个有10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions t

hat take an integer argument and return an integer

答案是:

   
a) int a;          
// An integer

   
b) int *a;     // A pointer to an integer

   
c) int **a;         // A pointer to a pointer to an integer

   
d) int a[10];       // An array of 10 integers

   
e) int *a[10]; // An array of 10 pointers to integers

   
f) int (*a)[10];   // A pointer to an array of 10 integers

   
g) int (*a)(int);  // A pointer to a function a that takes an integer argument and returns an integer

   
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

   
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。

   
应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

 

   
Static

6.
关键字static 的作用是什么?

   
这个简单的问题很少有人能回答完全。在C
语言中,关键字static 有三个明显的作用:

   
•;
在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。

   
•;
在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函
数访问。它是一个本地的全局变量。

   
•;
在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

   
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

 

   
Const

7.关键字const
有什么含意?

   
我只要一听到被面试者说:"const
意味着常数",我就知道我正在和一个业余者打交道。去年Dan Saks
已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const
能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const
意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks
的文章吧。)

   
如果应试者能正确回答这个问题,我将问他一个附加的问题:

   
下面的声明都是什么意思?

   
const int a;

   
int const a;

   
const int *a;

   
int * const a;

   
int const * a const;       /******/

   
前两个的作用是一样,a
是一个常整型数。第三个意味着a 是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。

   
第四个意思a 是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a
是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const
呢?我也如下的几下理由:

   
•;
关键字const
的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const
的程序员很少会留下的垃圾让别人来清

理的。)

   
•;
通过给优化器一些附加的信息,使用关键字const
也许能产生更紧凑的代码。

   
•;
合理地使用关键字const
可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

 

   
Volatile

8.
关键字volatile 有什么含意?并给出三个不同的例子。

   
一个定义为volatile
的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile
变量的几个例子:

   
•;
并行设备的硬件寄存器(如:状态寄存器)

   
•;
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

   
•;
多线程应用中被几个任务共享的变量

   
回答不出这个问题的人是不会被雇佣的。我认为这是区分C
程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS 等等打交道,所有这些都要求用到volatile
变量。不懂得volatile 的内容将会带来灾难。

   
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile
完全的重要性。

   
•;
一个参数既可以是const
还可以是volatile 吗?解释为什么。

   
•;
一个指针可以是volatile
吗?解释为什么。

   
•;
下面的函数有什么错误:

I  
nt square(volatile int *ptr)

   
{

   
    return *ptr * *ptr;

   
}

下面是答案:

   
•;
是的。一个例子是只读的状态寄存器。它是volatile
因为它可能被意想不到地改变。它是const 因为程序不应该试图去修改它。

   
•;
是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer
的指针时。

   
•;
这段代码有点变态。这段代码的目的是用来返指针*ptr
指向值的平方,但是,由于*ptr 指向一个volatile
型参数,编译器将产生类似下面的代码:

   
int square(volatile int *ptr)

   
{

   
    int a,b;

   
    a = *ptr;

   
    b = *ptr;

   
    return a * b;

   
}

   
由于*ptr 的值可能被意想不到地该变,因此a
b 可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

   
long square(volatile int *ptr)

   
{

   
    int a;

   
    a = *ptr;

   
    return a * a;

   
}

 

 

位操作Bit manipulation

9.
嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a
bit 3,第二个清除a bit 3。在以上两个操作中,要保持其它位不变。

   
printf("\nArea = %f", area);

   
return area;

   
}

   
这个函数有太多的错误了,以至让人不知从何说起了:

   
•; ISR
不能返回一个值。如果你不懂这个,那么你不会被雇用的。

   
•; ISR
不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。

   
•;
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR
中做浮点运算。此外,ISR 应该是短而有效率的,在ISR
中做浮点运算是不明智的。

   
•;
与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。*****

 

代码例子(Code examples

12 .
下面的代码输出是什么,为什么?

void foo(void)

{

   
unsigned int a = 6;

   
int b = -20;

   
(a+b > 6) ? puts("> 6") : puts("<= 6");

}

   
这个问题测试你是否懂得C
语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。 因此-20
变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

 

13.
评价下面的代码片断:

   
unsigned int zero = 0;

   
unsigned int compzero = 0xFFFF;

   
    /*1's complement of zero */

   
对于一个int 型不是16
位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

   
这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而P

   
C
机程序往往把硬件作为一个无法避免的烦恼。到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧...

   
动态内存分配(Dynamic memory allocation

 

14.
尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?

   
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP
杂志中被广泛地讨论过了(主要是 P.J.Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:

   
下面的代码片段的输出是什么,为什么?

char *ptr;

if ((ptr = (char *)malloc(0)) ==NULL)

else

   
puts("Got a null pointer");

   
puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0
值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

 

Typedef:

15 Typedef
C 语言中频繁用以声明一个已经存在的数据类型的同义字。

   
也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *

typedef struct s * tPS;

   
以上两种情况的意图都是要定义dPS
tPS 作为一个指向结构s 指针。哪种方法更好呢?(如果有的话)为什么?

   
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef
更好。思考下面的例子:

   
dPS p1,p2;

   
tPS p3,p4;

第一个扩展为

   
struct s * p1, p2;

   
上面的代码定义p1 为一个指向结构的指,p2
为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 p4
两个指针。

晦涩的语法

 

16 . C
语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

   
int a = 5, b = 7, c;

   
c = a+++b;

   
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

   
因此, 这段代码持行后a = 6, b = 7, c = 12

   
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。

抱歉!评论已关闭.