您的位置  > 互联网

(知识点)变量与取模的一种替代方法

1.布尔变量和零值的比较

布尔变量不能直接与 TRUE、FALSE 或 1 或 0 进行比较。根据布尔类型的语义,零值是“假”(记为 FALSE),任何非零值都是“真”(记为为真)。

TRUE的值到底是什么,没有统一的标准。 例如,C++ 将 TRUE 定义为 1,而 Basic 将 TRUE 定义为 -1。

假设布尔变量名为flag,则将其与零值进行比较的标准if语句如下:

if (flag) // 表示flag为真
if (!flag) // 表示flag为假

其他用法是不好的风格,例如:

if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)

2. 整型变量和零值的比较

整数变量应使用“==”或“!=”直接与 0 进行比较。 假设整型变量的名称是 value,则将其与零值进行比较的标准 if 语句如下:

if (value == 0)
if (value != 0)

不能模仿布尔变量的风格来编写:

if (value) // 会让人误解 value是布尔变量
if (!value)

3. 浮点变量和零值的比较

浮点变量不能使用“==”或“!=”与任何数字进行比较。 一定要注意,无论是浮点型还是类型变量,都有精度限制。

因此,一定要避免使用“==”或“!=”来比较浮点变量与数字,而尽量将其转换为“">=”或“c可以重写为a>(c*b)。

如果您确定操作数是无符号的,则最好使用无符号除法,因为它比有符号除法更有效。

3.另一种取模方法

我们使用余数运算符来提供算术模数。 但有时您可以使用 if 语句来执行模运算。 考虑以下两个示例:

uint modulo_func1 (uint count){
   return (++count % 60);
}
uint modulo_func2 (uint count){
   if (++count >= 60)
      count = 0;
   return (count);
}

优先使用 if 语句而不是余数运算符,因为 if 语句执行速度更快。 这里请注意,只有当我们知道输入计数余额为 0 到 59 时,该函数的新版本才能正常工作。

4.使用数组下标

如果您想将字符值设置为代表某些内容的变量,您可以这样做:

switch ( queue )
{
case0 :   letter = 'W';
   break;
case1 :   letter = 'S';
   break;
case2 :   letter = 'U';
   break;
}

或者这样做:

if ( queue == 0 )
  letter = 'W';
elseif ( queue == 1 )
  letter = 'S';
else
  letter = 'U';

一种更干净、更快的方法是使用数组下标来获取字符数组的值。 如下:

staticchar *classes="WSU";
letter = classes[queue];

5.使用别名

考虑以下示例:

void func1( int *data ){
    int i;
 
    for(i=0; i<10; i++)
    {
          anyfunc( *data, i);
    }
}

虽然*data的值可能永远不会改变,但编译器不知道该函数不会修改它,因此程序每次使用它时都必须从内存中读取它。 如果我们知道变量的值不会改变,那么我们应该使用以下编码:

void func1( int *data ){
    int i;
    int localdata;
 
    localdata = *data;
    for(i=0; i<10; i++)
    {
          anyfunc ( localdata, i);
    }
}

这为编译器优化代码提供了条件。

6. 局部变量的类型

我们应该尽量不要使用char和short类型的局部变量。 对于 char 和 Short 类型,编译器每次赋值时都需要将局部变量减少到 8 或 16 位。

这称为有符号变量的符号扩展和无符号变量的零扩展。 这些扩展可以通过将寄存器左移 24 位或 16 位,然后根据是否有符号标志右移相同位数来实现,这将消耗两个计算机指令操作(无符号字符类型的零扩展)只需要一条计算机指令)。

使用 int 和 int 类型的局部变量可以避免此类移位操作。 这对于将数据加载到局部变量然后处理局部变量数据值等操作很重要。 无论输入和输出数据是8位还是16位,都值得将它们视为32位。

考虑以下三个函数:

int wordinc (int a){
   return a + 1;
}
short shortinc (short a){
    return a + 1;
}
char charinc (char a){
    return a + 1;
}

虽然结果相同,但第一个程序片段的运行速度比后两个要快。

3. 循环语句 1. 多个循环

在多个循环中,如果可能的话,最长的循环应该放在最内层,最短的循环应该放在最外层,以减少CPU跨循环层的次数。 例如,示例 4-4(b) 比示例 4-4(a) 更有效:

2.循环体内的判断

如果循环体中有逻辑判断且循环次数较多,建议将逻辑判断移到循环体之外。

例4-4(c)中的程序比例4-4(d)多执行N-1次逻辑判断。 并且由于前者总是需要进行逻辑判断,因此中断了循环“管道”操作,使得编译器无法对循环进行优化,降低了效率。

如果N很大,最好采用例4-4(d)的写法,这样可以提高效率。 如果N很小,两者的效率差异并不明显。 最好采用例4-4(c)的写法,这样程序更加简洁。

3.for语句的循环控制变量

不能在 for 循环体内修改循环变量,以防止 for 循环失去控制。 建议将for语句的循环控制变量的值写成“半开半闭区间”的方式。

例4-5(a)中的x值属于半开半闭区间“0 =< x < N”,从起点到终点的区间为N,循环次数为N。

例4-5(b)中的x值属于闭区间“0 =< x 10 && b=4)。在Fast计算之类的语句中),因此第二部分可能不需要执行。

6、用()函数代替if…else…

对于涉及if...else...else...的多条件判断,例如:

if( val == 1)
    dostuff1();
elseif (val == 2)
    dostuff2();
elseif (val == 3)
    dostuff3();

使用以下方法可能会更快:

switch( val )
{
    case1: dostuff1(); break;
    case2: dostuff2(); break;
    case3: dostuff3(); break;
}

在if()语句中,如果最后一条语句命中,则需要测试并执行前面的条件一次。 使我们无需进行额外的测试。 如果必须使用 if...else... 语句,请将最有可能执行的语句放在最前面。

功能相关 1、参数必须写完整

参数写入应该完整。 不要只写参数类型而省略参数名称来节省时间。 如果函数没有参数,则用 void 填充。 例如:

voidSetValue(intwidth,intheight); // 良好的风格
voidSetValue(int,int);            // 不良的风格
floatGetValue(void); // 良好的风格
floatGetValue();     // 不良的风格

2、参数命名要恰当,顺序要合理。

例如,编写一个字符串复制函数,它有两个参数。 如果参数名称为str1和str2,例如:

void StringCopy(char*str1,char*str2);

那么我们就很难弄清楚是把str1复制到str2还是反过来。

您可以更有意义地命名参数,例如 and。 这样,您就可以从名称中看出它应该被复制到。

还有一个问题,这两个参数哪个在前面,哪个在后面? 参数的顺序应该遵循程序员的习惯。 一般来说,目标参数应该放在前面,源参数放在最后:

void StringCopy(char*strDestination,char*strSource);

3.参数是指针

如果参数是指针且仅用于输入,则应在类型前添加const,以防止指针在函数体内被意外修改。

例如:

void StringCopy(char*strDestination,constchar*strSource);

4.不要省略返回值的类型

在C语言中,任何没有类型描述的函数都会自动被视为整数。 这不会有任何好处,而且很容易被误解为 void 类型。

5. 函数名和返回值类型在语义上不能冲突

违反此规则的典型示例是 C 标准库函数。 例如:

charc;
c=getchar();
if(c==EOF)
…

顾名思义,很自然地将变量 c 声明为 char 类型。 但不幸的是它不是char类型,而是int类型。 其原型如下:

int getchar(void);

由于c是char类型,所以取值范围为[-128, 127]。 如果宏EOF的值超出了char的取值范围,则if语句将始终失败。 通常人们怎么会想到这种“危险”呢! 用户不对本示例中的错误负责; 这是误导用户的功能。

6. 不要一起返回正常值和错误标志。

正常值是使用输出参数获得的,而错误标志是使用语句返回的。

回顾上面的例子,为什么C标准库函数的设计者要把它声明为令人困惑的int类型呢?

正常情况下确实返回单个字符。 但如果遇到文件结束标志或者发生读取错误,则必须返回标志EOF。 为了与普通字符区别,EOF必须定义为负数(通常是负1)。 因此该函数的类型变为 int。

在我们的实际工作中,经常会遇到以上尴尬的问题。 为了避免误解,我们应该将正常值和错误标志分开。 即:使用输出参数获取正常值,使用语句返回错误标志。

该函数可以重写为BOOL(char*c);。

7.附加返回值,增强函数的灵活性

有时函数本来不需要返回值,但为了增加灵活性,例如支持链式表达式,可以附加返回值。 例如字符串复制函数的原型:

char *strcpy(char *strDest,const char *strSrc);

该函数将被复制到输出参数,并且该函数的返回值将是。 这样做并不多余,并且提供了以下灵活性:

char str[20];
int length=strlen(strcpy(str,“HelloWorld”));

循环展开

可以展开简单循环以获得更好的性能,但代价是增加代码大小。 循环展开后,循环计数应该变得越来越小,以便执行更少的代码分支。

如果循环迭代次数很少,则可以完全展开循环以消除循环负担。 例如:

for(i=0; i<3; i++)
{
    something(i);
}

扩展到:

something(0);
something(1);
something(2);

这可以节省相当大的性能,因为代码不需要在每个循环中检查并递增 i 的值。

if判断条件的顺序

if判断条件中概率最大的情况应该放在最前面。 例子:

if (1 == condition)
{
}
else if (2 == condition)
{
}
else
{
}

这里,如果1的概率较大,则将if(1==)放在前面。

如果2的概率较大,则将if(2==)放在前面,如:

if (2 == condition)
{
}
else if (1 == condition)
{
}
else
{
}

这里有一个小细节:使用if判断变量是否等于常量时,可以将常量写在前面,将变量写在后面,如:

if (2 == condition)

2 放在前面,放在后面。 这样做的好处是,当你错过=符号时,编译器会指出你的错误。

尽早退出循环

通常,循环不需要完整执行。 例如,如果我们正在数组中查找特定值,那么一旦找到,我们应该尽早打破循环。

例如:以下循环从 10,000 个整数中搜索 -99。

found = FALSE;
for(i=0;i<10000;i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
    }
}
if( found ) 
{
    printf("Yes, there is a -99. Hooray!\n");
}

无论我们是否找到这段代码,循环都会被完整执行。 更好的方法是一旦找到我们要查找的号码就终止查询。

将程序修改为:

found = FALSE;
for(i=0;i<10000;i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
        break;
    }
}
if( found ) 
{
    printf("Yes, there is a -99. Hooray!\n");
}

如果要检查的数据位于第23位,则程序将执行23次,从而节省9977个周期。

使用按位运算代替四种算术运算

在许多较旧的微处理器上,按位运算比加法和减法稍快,并且通常按位运算比乘法和除法快得多。

在现代架构中,位运算往往与加法运算的速度相同,但仍然比乘法运算快。所以通常在乘或除2n时,可以使用位运算来代替四种算术运算,例如

a = a * 8;
a = a / 8;
a = a % 8;

变成:

a = a << 3;
a = a >> 3;
a = a & 7;

以空间换时间

当内存足够的时候,可以用空间来换取时间。 例如,使用查表的方法,将一些可能的结果预先保存到表中。 例如,求阶乘的常用方法是:

long factorial(int i){
    if (i == 0)
        return 1;
    else
        return i * factorial(i - 1);
}

如果有足够的空间并且可以列出所需的结果,则代码可以修改为:

static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720  /* etc */ };
long factorial(int i){
    return factorial_table[i];
}

使用复合赋值语句

有两种方法可以增加变量的值,例如:a = a + 5 和 a += 5。有两种方法可以增加变量的值有什么意义呢?

K&R C 设计者认为复合赋值运算符可以让程序员更清晰地编写代码。 此外,编译器可以生成更紧凑的代码。

现在,a = a + 5 和 a += 5 之间的差异不再那么重要,现代编译器可以毫无问题地为这两个表达式生成优化代码。

但是,请考虑以下语句:

a[2*(y-6*f(x))] = a[2*(y-6*f(x))] + 5;
a[2*(y-6*f(x))] += 5;

这里a是一个数组。 在第一种形式中,由于编译器无法知道函数 f 是否有副作用,因此它必须对数组 a 的下标表达式求值两次。

在第二种形式中,下标表达式只需要计算一次,因此第二种形式更加高效。 而且,从书写的角度来看,第一种形式的下标表达需要写两次,而第二种形式只需要写一次。

尽量减少循环体内的工作量

循环过程中,随着循环次数的增加,系统资源的消耗也会增加。 我们应该确认一些操作是否必须放在循环体内。 示例代码:

for (i = 0; i < n; i++)
{
    tmp += i;
    sum = tmp;
}

这是一个求和运算,但是每次循环时,都会生成一个 sum = tmp; 执行操作。 这种写法是一种资源的浪费。 该语句可以移到循环之外:

for (i = 0; i < n; i++)
{
    tmp += i;
}
sum = tmp;

这样,sum = tmp; 语句只执行一次,这样不仅提高了程序效率,而且提高了可读性。 同时我们也可以考虑是否有必要将这样的代码封装成一个函数,以便多处调用。

对于无限循环,最好使用 for(;;) 而不是 while(1)

在C语言中,最常用的无限循环语句主要有两种:while(1)和for(;;)。 从功能上来说,这两种说法具有完全相同的效果。 那么,我们应该选择哪一个呢?

事实上,for(;;) 语句运行得更快。 根据for的语法规则,两个分号分隔三个表达式。 既然表达式为空,自然就编译成无条件跳转(即无条件的无条件循环)。 例如,for(;;)的代码在2010集成开发环境VC++的Debug模式下会生成如下汇编代码:

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

相比之下,while 语句则不同。 根据while的语法规则,while()语句中必须有一个表达式(这里是1)来确定条件,生成的代码使用它来执行条件跳转。 即while语句()是一个条件循环。 如果有条件,就要判断条件是否为真,所以比for(;;)语句需要多几条指令。 例如,代码while(1)在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 
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

当然,你也可以使用宏来代替上述函数。 代码如下:

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

在C程序中,我们可以适当使用宏代码来提高执行效率。 宏代码本身不是函数,但它的使用方式就像函数一样。 预处理器通过复制宏代码来代替函数调用,无需将参数压入堆栈,无需生成汇编语言的CALL调用,返回参数,执行进程,从而提高了运行速度。 然而,使用宏代码的最大缺点是容易出错。 复制宏代码时,预处理器通常会产生意想不到的副作用。 带参数的宏注释:【C语言笔记】学习使用带参数的宏(一)、【C语言笔记】学习使用带参数的宏(二)

因此,虽然看起来宏比函数简单很多,但仍然建议使用函数的形式来封装这些简单函数的代码。

函数的抽象级别应该处于同一级别

我们先看下面的示例代码:

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

上面的Init函数主要完成本地初始化和远程初始化工作,其功能并没有什么问题。 但从设计的角度来看,存在一定的缺陷。 由于本地初始化与远程初始化处于同一级别,因此本地初始化也应该作为独立函数存在。 应改为:

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

尽量避免在非预定函数中使用控制参数

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

所谓调度功能是指根据输入的消息类型或控制命令启动相应的功能实体(即功能或进程)的功能。 调度功能本身不能提供功能实现。 相反,必须委托给实现函数来完成特定的功能。 换句话说,调度函数总是只关注“做什么”,而“怎么做”是实现函数关心的,调度函数不需要关心“怎么做”。 这种调度函数和实现函数的分离设计也满足了单一职责的原则,即调度不实现,实现不调度。

对于调度函数来说,控制参数是指改变函数功能行为的参数,即函数根据这些参数决定如何工作。 但是,如果在非调度函数中也使用控制参数来决定如何工作,那么这样做无疑会增加函数之间的控制耦合度,可能会增加函数之间的耦合度,使函数的功能不唯一,违反了功能功能单一原则。 示例代码如下:

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;
}

虽然上面的功能看起来很简单,但实际上这样的设计是不合理的。 由于控制参数的原因,函数之间的耦合度增大,这也违反了函数的单一函数原则。 因此,还不如划分为以下四个功能来得清晰。 示例代码如下:

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;
}

本站涵盖的内容、图片、视频等数据,部分未能与原作者取得联系。若涉及版权问题,请及时通知我们并提供相关证明材料,我们将及时予以删除!谢谢大家的理解与支持!

Copyright © 2023