Satan's Syntax

Satan's Syntax
by Steve Goodwin

A lot of people code by example. This example becomes a template. Once the template is known, different variables, different values, and different functions are applied to the template and combined with glue code to implement the required solution. By being more aware of the syntax, much of this glue can be removed. This article illustrates a few quirks of the C syntax, and how it can be used (abused?) to implement more efficient code, without qualifying for the IOCCC (International Obfuscated C Code Contest)!

Remember The Return Types

My first example of this "template programming" will involve the formatting output function sprintf. It is not unusual to write code such as:

   
   
sprintf(str1, "Old v=%d/t",v); /* Some code that plays with v */ sprintf(str2, "New v=%d",v); strcat(str1, str2); printf(str1);

Most instances of sprintf use a temporary string as the first parameter. This is the template that becomes engrained: "sprintf requires a temporary string". Whereas a better template would be a syntactical one, "sprintf requires a pointer to a character [array]". This is a reminder that we could, instead, use a function that returns a char * as our first parameter, saving a temporary buffer. For example:

   
   
sprintf(str, "Old v=%d/t",v); /* Some code that plays with v */ sprintf(strchr(str, '/0'), "New=%d",v); printf(str);

You should be prudent in using pointers directly, to avoid deferencing a NULL. In this case, strchr(str, '/0') is always valid thanks to the first sprintf. There is also a hidden bonus with sprintf - it returns the number of characters written into the buffer - which can save a call to strlen! Check your libraries. You might get lucky!

To take another example, if the reason for getting a string is to determine its length is valid for some operation, you might write:

   
   
len = strlen(GetFileName()); if (len > 0) ; /* File name is not null */

However, if all you're going to do is check len against zero, why not write:

   
   
if (GetFileName()[0] != '/0') ; /* File name is not null */

This time, not only do we save ourselves another temporary buffer (which is quite common, seeing as we use the result directly - it also eliminates the extraneous copy that usually follows), but we also show understanding for the data being returned (that a NULL-length string starts with '/0'), and we also remove the strlen overhead. Note that the same result could be achieved with the equivalent code:

   
   
if (*GetFileName () != '/0') ; /* File name is not null */

As a string is simply a structure (of type char *), you could also use this method with functions that return whole structures, C++ classes or pointers to them, and just isolate the element you want. For example:

   
   
struct POINT GetCurrentPos(void); int y; y = GetCurrentPos().y;

or,

   
   
printf(GetDevice()->pName);

The compiler is likely to create the structures as temporary variables, and pass their pointers as hidden parameters, so you need not worry about the overhead of copying structures to and from the stack.

Just as a side point, remember the lessons that say "you can't return a string from a function"? Well. You can. Of sorts. By creating a structure of type STRING consisting of one character array called str, for instance, you can return STRING from a function, referencing it in the same manner.

   
   
struct STRING { char str[256]; }; struct STRING GetName(void) { . } printf(GetName().str);

In the real world, this method offers little or no benefit to passing the string in as a pointer, but it does save the programmer creating temporary variables.

Expressions

Function calls are just expressions. If they return a type (excluding void, which isn't really a type) then you can use them anywhere you would use a normal expression, like a while loop. This allows entire loops and the like to be short-circuited quickly, taking advantage of C's lazy evaluator (see sidebar).

   
   
while (1 && GetNextLine(&str)) ;

Changing the 1 to a 0 causes the whole expression to evaluate to 0: The GetNextLine function does not get called, and the loop never executes. I often use this method to remove long winded if statements from experimental code:

   
   
if (1 && ComplicatedExpression1 && ComplicatedExpression2 && ComplicatedExpression3 ) ;

By making the 1 a global, or static, variable, I can then remove the code during program execution with the debugger, or change it with a special menu option, allowing me to test different section on a single compile.

End

I hope there's a few new ideas for you there. I neglected to mention code could also be removed with

   
   
/* . code here . //*/

and re-inserted by adding a single / to the first line. But that's just too nasty!

Sidebar

C uses a lazy evaluator. This means it will only evaluate the necessary expressions to prove the expression is definitely TRUE, or definitely FALSE. So a line like,

   
   
if (fn1() && fn2() && fn3()) .

Would evaluate fn1() first, but would only continue onto fn2() if it had too. Namely, if fn1() returned TRUE. If it returned FALSE, there is nothing the other functions could do to make the final expression TRUE. Most people know this instinctively from code snippets like:

   
   
if (ptr && ptr->Name) printf(ptr->Name);

This is different from Pascal.

转贴翻译:

  很多人写代码是照猫画虎,这些“猫”最终就变成了教条(注1)。一旦教条被人熟知,不同的变量,数值,功能就被按照教条使用,然后用一些“胶水”代码组合起来,实现需要的方案。通过对语法的深入了解,我们可以消除很多的“胶水”。这篇文章举了几个怪异的C语法的例子,以及如何在不导致歧义(注2)的情况下,利用(滥用?)他们实现更高效的代码。

记得返回值

我的第一个关于“教条编程”的例子将讨论格式化输出函数sprintf。下面这段代码的写法并不鲜见:
    sprintf(str1, "Old v=%d/t",v);
    /* Some code that plays with v */
    sprintf(str2, "New v=%d",v);
    strcat(str1, str2);
    printf(str1);


大部分的sprintf实例使用一个临时的字符串变量作为它的第一个参数。这就是那个根深蒂固的教条:“sprintf需要一个临时字符串”。然而更好的教条是符合语法的,“sprintf需要一个指向字符数组的指针”。这提醒我们,可以用一个返回char *的函数替代它作为第一个参数,从而节省一个临时缓冲区的空间。例如:
   
sprintf(str, "Old v=%d/t",v);
   
/* Some code that plays with v */
   
sprintf(strchr(str, '/0'), "New=%d",v);
    printf(str);

当直接使用指针的时候,你必须谨慎的避免NULL指针。在这个例子里,str(str,'/0')的有效得益于第一个sprintf。sprintf还隐藏了一个好处-它返回已经写入缓冲区的字符数量-这个数量可以节省一次对strlen的调用!检查你的代码吧。祝你好运!

另一个例子,如果得到一个字符串的长度,是为了在某些操作时判定它是否有效,你也许会这样写:
   
len = strlen(GetFileName());
    if (len > 0)
        ; /* File name is not null */


然而,如果你只是打算用它和0比较的话,为什么不这样写:
    if (GetFileName()[0] != '/0')
        ; /* File name is not null */


这次,我们不仅节省了一个缓冲区(这很普通,我们直接使用返回值(注3)-这也避免了一次额外的拷贝),而且还在正确的检查了返回值的同时,消除了前面strlen的调用开销。别忘了,相同的结果可以用等价的方法得到。
   
if (*GetFileName () != '/0')
        ; /* File name is not null */

因为字符串只是一个(可以用char * 指向的)结构,上面的方法同样可以用于返回结构体,C++类或其指针的函数,直接提取你需要的元素。例如:
   
struct POINT GetCurrentPos(void);

   
int y;

    y = GetCurrentPos().y;


或者,

    printf(GetDevice()->pName);

编译器可能会把结构创建为临时对象,然后把他们的指针作为隐含参数,所以你不必顾虑把结构体拷贝进栈或者出栈的开销。

从另一个角度,你还记得“函数无法返回一个字符串”的课程吗?其实你可以。方法很多。例如,创建一个名为STRING的结构体,里面包含一个字符串数组,你可以在函数中返回这样一个STRING,然后像上面那样引用其中的字符串。
   
struct STRING { char str[256]; };

    struct STRING GetName(void)
    {
        .
    }

    printf(GetName().str);


实际开发中,这和传递字符串指针相比,用处很少甚至完全无益,但是它的确减少了程序员创建临时变量的需求。

表达式

  函数调用只是表达式。如果他们返回一个类型(不包括void,那并不是一种真正的类型),那么你就可以把它作为一个普通的表达式用在任何需要的地方,例如一个while循环。这使得循环或者类似的流程被快速短路成为可能,从C的懒惰表达式计算中获益。(看补充)
    while (1 && GetNextLine(&str))
        ;

把表达式中的1改成0,导致整个表达式的结果变成0,GetNextLine函数不会被调用,循环永远都不会运行。我经常使用这种方法在测试代码中取消复杂的if语句块。
   
if (1
        && ComplicatedExpression1
        && ComplicatedExpression2
        && ComplicatedExpression3
    )
        ;


通过把1改成全局,静态,变量,我可以在调试时,取消这些代码的运行,或者通过菜单选项改变它,从而在一次编译中测试多个项目。

结束

我希望还有一些新的想法告诉你。我忘了讲,代码也可以使用这种方式移除:
   
/*
    . code here .
    //*/

然后用在首行添加一个单独的/字符来重新插入这段代码。

补充

C使用一个懒惰表达式计算器。这就是说,它只计算需要的表达式来推导最终的结果是TRUE还是FALSE,所以一行类似
    if (fn1() && fn2() && fn3())
.
的代码,会先计算fn1(),只有在必要时才继续计算fn2(),也就是说,当fn1()返回TRUE的时候。如果它返回FALSE,无论其他表达式如何,都无法使得最终结果是TRUE。大部分人从如下的代码段中凭直觉了解了这一点:
   
if (ptr && ptr->Name)
        printf(ptr->Name);


这和Pascal是不同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值