分类 技术志 下的文章

使用C99特性简化代码编写

至今我还记得第一次听说C99标准还是在读大一时,那时同寝一位兄弟手头有一本Herbert Schildt编写的《C: The Complete Reference,Fourth Edition》(中文名:C语言大全),书封皮的右上角上赫然写着"详解C99 ANSI/ISO最新标准",那时离C99标准发布仅仅才一年。

那个时候我们大学授课以及实验用的还是Borland的Turbo C 2.0,C99标准根本无从谈起。转眼间十多年过去了,C99标准逐渐成熟,各大编译器厂商以及开源编译器都完善了自己的产品,对C99有了很好的支持,像Gcc编译器在最新的4.6版本里几乎完全支持所有C99特性。但如果你依旧在用Microsoft的Visual Studio,那么很遗憾你可能依旧无法使用C99的诸多新特性。

工作以来一直使用GCC作为C的编译器,但一直采用的是GCC的默认C标准,即gnu89,也就是C90标准加上一些GCC自行的扩展。直到去年年末与Dreamhead闲聊时,Dreamhead提出可利用C99标准简化代码编写的想法,我这才有意识地去主动了解一些有关C99与上一版标准不同的地方,并在今年的项目中尝试用gnu99(-std=gnu99)替代gnu89。

与上一版标准相比,C99做了几十处修订,可用于简化代码编写的新增特性虽然不多,不过大多都还算很实用,其中一些是GCC在自己的扩展中已经存在了多年的特性,这次也被正式纳入C99标准中了。

下面我就列举一些可以帮助你简化C代码编写的C99特性(也许还不够全面):

* 布尔类型
很多C程序员都很向往Java以及C#等语言中提供的原生bool类型,在C语言没有真正提供bool类型之前,很多C程序中都有这样的代码:

#undef  bool
#undef  true
#undef  false

typedef enum {
        false,
        true
} bool;

C99标准中正式引入了布尔类型_Bool,注意是_Bool而不是bool。虽然不是bool,而是一个对于类型名称而言有些丑陋的名字,但这也给C程序员带来了些许福音。C标准委员会显然也考虑到了大家的质疑,遂又为C99引入了一个标准头文件"stdbool.h",在该文件中我们看到了bool,true和false的定义,只不过它们不是原生的,而是宏:
#define bool    _Bool
#define true    1
#define false   0

即使是这样,我们依旧可以无需编写自己的bool类型了(不过如果考虑在不同版本编译器之间的移植的话,还是需要根据__STDC_VERSION__来选择到底使用内置bool还是自定义bool的)。

#include
bool found = true;
bool empty = false;
bool is_foo();
int xx_hash_create(xx_hash_t **h, bool shared);

或者用_Bool类型关键字:
/* no header needed */
_Bool found = 1;
_Bool empty = 0;
_Bool is_foo();
int xx_hash_create(xx_hash_t **h, _Bool shared);

* 可变参数宏
在不支持可变参数宏的日子里,我们经常这么定义一些宏:

#define compare2(compf, arg1, arg2) \
    compf(arg1, arg2)

#define compare3(compf, arg1, arg2, arg3) \
    compf(arg1, arg2, arg3)

#define compare4(compf, arg1, arg2, arg3, arg4) \
    compf(arg1, arg2, arg3, arg4)
… …

有了可变参数宏后,我们只需一个定义即可:
#define compare(compf, …) \
    compf(__VA_ARGS__)

compare(strcmp, "hello", "world");
compare(triplestrcmp, "hello", "world", "foo");
… …

* Compound Literals
这个特性比较难于译成中文,直译起来就是"复合字面量"。其实它类似一个匿名变量,其语法形式为"(类型){初始值列表}",下面是一些例子可以帮助你理解:

在没有"Compound Literals"特性之前,我们可以这样编写代码:
struct xx_allocator_t allocator;
allocator.af = malloc;
allocator.ff = free;
xx_hash_new(.., &allocator);

使用C99特性,我们就可以省掉xx_hash_new之前的那个变量定义和初始化了:
xx_hash_new(.., &(struct xx_allocator_t){.af = malloc, .ff = free});

* Designated initializers(指定初始化器)
在没有这个特性之前,我们在用初始化器初始化一个数组或者一个结构体时,一般要给所有元素都赋值:

struct foo {
    int a;
    char b;
    char s[20];
};

int a[5] = {1, 2, 3, 4, 5};
struct foo f = {1, 'A', "hello"};
struct foo v[3] = {
    {1, 'A', "hello"},
    {2, 'B', "hi"},
    {3, 'C', "hey"}
};

如果我们只想为数组中某一个元素赋值,或者为结构体中某一个字段赋值的话,我们就不能使用初始化器了,只能这样来做:
int a[5];
a[2] = 3;

struct foo f;
strcpy(f.s, "hello");

struct foo v[3];
v[1].a = 2;
v[1].b = 'B';
strccpy(v[1].s, "hi");

C99给我们带来了指定初始化器的特性,我们可以在初始化时指定为哪个结构体字段或数组元素赋值:

int a[5] = {[2] = 3, [4] = 5};

struct foo f = {.s = "hello"};
struct foo v[3] = {
    [1] = {.a = 2, .b= 'B', .s = "hi"}
};

* 为选择与迭代语句引入新的块范围
这个C++程序员定然不陌生,在C++中我们可以这样定义一个循环:
for (int i = 0; i < 100; i++) {
    … …
}

但在老版本C中,我们只能这样做:
int i;
for (i = 0; i < 100; i++) {
    … …
}

不过使用C99后,你就可以和C++程序员同等待遇了。

和近几年涌现的一些新语言相比,古老的C语言中可以用于简化代码编写的语法糖就显得少得有些可怜。C1x标准目前正在制定中,但也不要对C1x期望太高,毕竟C语言的精髓并非旨在改善开发效率。

C程序员驯服Common Lisp – 入门

毫无疑问,Common Lisp是一门庞大且复杂的语言,学习曲线并不平坦。对于一个从未接触过函数式语言、交互式语言以及动态类型语言的C程序员来说,学习Common Lisp显然是一个很大的挑战。

也许有人会问:"C语言已经无所不能了,为何还要学习Common Lisp?"在这里我不想说太多冠冕堂皇的话,至少对我而言,理由有三:
一是好奇,在C语言的世界里待得久了,总想探出头来吸几口新鲜空气,这次我选择了Common Lisp;
二是为了变成一名更好的程序员。为何学习Common Lisp就能成为一名更好的程序员呢?这不是我的观点,而是诸多牛人或大师们(包括Paul GrahamPeter Norvig以及另外一个Peter:Peter Seibel等)的观点。不过不管你们信不信,反正我是信了。这个观点的关键思想就是一门语言可以影响一个程序员的思维方式。我相信Common Lisp可以给我带来一种不同于以往的新的编程思维方式,这样至少比只有一种思维方式要好,不是吗;
最后,Lisp是一门可编程的编程语言,可以很容易扩展自身并且创造一门新的语言。我无法不动心于如此一门强大的语言。

学习总是需要一些付出的。Jolt大奖得主《Practical Common Lisp》的作者Peter Seibel花了一年的时间放下一切潜心学习Common Lisp并终有所成。我们还有工作,有生活压力,无法像Seibel那样潇洒,但我们依旧可以去学习Common Lisp,循序渐进地学,一步一步来"驯服"Common Lisp这个"猛兽"。"猛兽"被驯服后,才能为你所用,发挥出异常的威力,不是吗?我们需要的仅是恒心和足够的耐心罢了。

"驯服"意味着"学会",何为学会一门语言?只是知晓语法,看懂代码还远远不够,那些仅仅叫知道或了解或"纸上谈兵",还谈不上真正地"学会"。古人云:"学以致用",只有在实际中可以灵活自如的使用了,才叫真正的"学会"了。

现在只是开始!这里我会按照C程序员学习C语言的逻辑展开,为了更加贴近C程序员的思维模式,我选择了这种相对平滑的学习方式。也许最初的几篇会让你觉得Common Lisp很像一门命令式语言^_^!

言归正传!学习一门编程语言之前,最好先弄清楚该语言在当前众多语言中的位置,了解一下它的前世今生,这有助于你对这门语言的认知。不过关于Common Lisp的详细历史这里就不赘述了,在进行下面内容之前,请先阅读一下维基百科,或是读读几本经典Common Lisp书籍(如《ANSI Common Lisp》、《On Lisp》以及《Practical Common Lisp》等)中对Common Lisp历史的介绍。

Common Lisp是Lisp语言大家族中的一分子,和Scheme等一样,它也是一门Lisp方言(Dialect)。与C语言相比,Lisp更加古老,是史上第二古老的编程语言,仅次于Fortran。但Common Lisp比C年轻,它是在上世纪80年代诞生的。与C语言普遍采用的"编辑->编译->调试/执行"的工作方式不同,Common Lisp更多采用的是类似于Python、Ruby那样的交互式的解释器工作模式。你在Common Lisp交互环境中就可以完成上述C语言的所有步骤。这种方式目前看来更易于语言的学习(虽然C语言目前也有解释器的实现,如Ch,但C程序员似乎更喜欢传统方式)。

目前市面上Common Lisp的实现有很多种,既有商业收费的,也有开源免费的。商业软件这里就不提了,常用的免费开源的主流Common Lisp解释器包括CLISPSBCL(Steel Bank Common Lisp)和Clozure CL。我个人更喜欢使用CLISP,所以后续有关解释器方面的内容更多以CLISP为主。

CLISP支持诸多平台,你可以很容易得到安装包并顺利的完成安装,关于这方面内容这里就不赘述了。打开一个终端(Windows下打开一个命令行窗口),敲入"clisp",回车,你就进入到CLISP提供的Common Lisp顶层环境(Top-Level)当中了(若要进入SBCL,敲入sbcl;若要进入Clozure CL,敲入ccl,以上的前提是这些包的可执行程序路径已经加入到你的PATH环境变量中了),就像这样:

$ clisp
… …
Welcome to GNU CLISP 2.44.1 (2008-02-23) <http://clisp.cons.org/>

Copyright (c) Bruno Haible, Michael Stoll 1992, 1993
Copyright (c) Bruno Haible, Marcus Daniels 1994-1997
Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible, Sam Steingold 1999-2000
Copyright (c) Sam Steingold, Bruno Haible 2001-2008

Type :h and hit Enter for context help.

[1]> _

对于所谓的"顶层环境",熟悉Python和Ruby等解释型语言的朋友并不陌生。它就是一个已经加载了标准Common Lisp包的REPL环境。其中REPL是Read-Eval-Print-Loop的缩写。说白了,这就是一个Common Lisp代码的执行环境,你在里面可以输入Common Lisp代码,这些代码可以被直接执行,执行结果也会立刻展现在你的眼前,或如果遇到错误/异常时,你还可以在里面直接进行代码调试。当然了"顶层"还有一个范围(Scope)的概念在里面,用于区分不同变量和函数的作用域。

我们在CLISP中输入一些字符串、字符以及数字以及简单表达式:

[1]> "hello lisp"
"hello lisp"
[2]> #\c
#\c
[3]> 1
1
[4]> (+ 1 2)
3

CLISP对于我们的输入给予了回应:对于字符串、字符(注意Common Lisp的字符表示法很特别,以#\作为前缀,#\c即C语言中的'c')以及数字,CLISP进行了回显(实际上是对输入求值后的结果),对于"(+ 1 2)"这个计算1和2之和的表达式,CLISP给出了求值后的结果。

我们继续输入一个a:

[5]> a

*** – EVAL: variable A has no value
The following restarts are available:
USE-VALUE      :R1      You may input a value to be used instead of A.
STORE-VALUE    :R2      You may input a new value for A.
ABORT          :R3      Abort main loop
Break 1 [6]>

与前面不同的是,这次CLISP给出了错误提示,求值器(evaluator)无法找到a绑定的值,CLISP进入异常处理模式,或称作调试模式。CLISP给出了三种选择:我们选择输入:R3,可以回到top-level主循环;选择输入:R2,则可以为a赋值。

Break 1 [6]> :R2
New A: 5
5
[7]> a
5

SBCL和Clozure CL与CLISP类似,都会有类似的调试模式,退出调试模式的方法参见各自的提示说明即可。

如果要退出CLISP解释器,我们可以输入"(quit)",注意quit两边的括号也是命令的一部分;在SBCL中,我们可以输入(SB-EXT:QUIT)退出;Clozure CL的退出方法与CLISP相同。

Common Lisp源代码是由一组S-expressions(symbolic expression)构成的。什么是S-expression呢?这个在Common Lisp书籍中很难找到答案,因为S-expression是一种组织数据的结构,并不是Lisp独有的,只是Lisp恰好也采用了这种结构来组织存储Lisp的代码和数据罢了。在维基百科上,S-expression有一个递归的定义:"S-expression要么是一个被成为原子(atoms)的单一的数据对象(data object),要么是一个S-expressions列表(list)。数字、数组、字符串以及符号都是原子",比如:

[1]> 13
13
[2]> #(1 2 3)
#(1 2 3)
[3]> "hello"
"hello"
[4]> #'length
#

数字'13'、数组'#(1 2 3)'、字符串"hello"以及符号'length'都是原子。

Lisp将代码和数据都存储于S-expressions当中,这是Lisp与其他主流语言的最大区别之一。我们在编写Common Lisp源码时,需要遵循正确的S-expression格式。前面说过Common Lisp解释器就是一个READ-EVAL-PRINT-LOOP环境,这个环境主要由一个Reader和一个Evaluator构成。Reader负责读取源文件中的文本或者我们在提示符后面输入的文本,检查文本格式是否符合S-Expression要求,直到所有文本都符合格式要求,这样解释器就得到了正确的S-expression:

[1]> (+ 1 2))
3
[2]>
*** – READ from … >: an object
      cannot start with #\)

通过上面例子可以看出,Reader识别出了不符合S-expression格式的源码文本。

Reader将文本转换为S-expressions后,Evaluator就开始对S-expression进行校验,校验其是否符合Lisp Code的规范形式(Lisp Form)。

下面的例子说明了Evaluator的作用:

[1]> (foo 1 2)
*** – EVAL: undefined function FOO

毫无疑问,(foo 1 2)是一个有效的S-expression,其通过Reader这关是没有问题的。但是当Evaluator对S-expression"(foo 1 2)"进行验证求值时,却发现无法找到函数foo的定义,这行源码不合法。

简单总结Reader和Evaluator的工作流程就是:"源码文本"通过Reader转换为有效的"S-expressions",后者则由Evaluator转换成有效"Lisp Form"并求值得出结果。

Common Lisp初学者常常被那满眼的括号所吓住,不过事实上括号并没有那么"可怕"。括号其实主要是给Common Lisp解释器(Reader和Evaluator)用的,而不是给程序员看的。现今的代码编辑器都很智能,基本上可以消除括号在编程过程中给你带来的影响(要说一点影响没有也不太可能)。

Common Lisp支持多种注释形式。在C语言中我们用'//'进行单行注释(C99标准引入),而Common Lisp的单行注释符号为';'。C语言采用'/*…*/'进行多行注释,Common Lisp使用的是'#|…|#'。Common Lisp还提供了一种大多语言都不具备的注释方式,那就是将注释直接写到紧邻函数定义的参数列表后面的位置上,这样通过Common Lisp提供的工具,我们可以轻松地提取出该函数的注释,并生成代码文档,比如:

[1]> (defun foo (x) "test comments" (+ x 1))
FOO
[2]> (documentation #'foo t)
"test comments"

由于Common Lisp括号众多,一个风格良好的Lisp程序需要通过良好风格的代码缩进来保证,这方面我推荐AI领域大师Peter Norvig若干年前编写的一篇有关优秀Lisp编程风格的文章《Tutorial on Good Lisp Programming Style》。

很多C程序员可能还是习惯于将代码写到文件中。Common Lisp解释器提供了将你的源文件加载到顶层环境并直接使用其中的定义的方法:
;; foo.lisp
(defun foo (x) "test foo"
   (+ x 1))

[1]> (load "foo.lisp")
;; Loading file foo.lisp …
;; Loaded file foo.lisp
T
[2]> (foo 5)
6

利用load函数我们可将你的源文件加载到顶层环境中,并在顶层环境里使用该源文件中定义的函数。

编程语言初学者总喜欢在终端控制台上看到自己编写的程序的输出结果,那样会产生一种奇妙的成就感,程序员们多陶醉于其中。C程序员最常用的就是printf函数了,Common Lisp中也有与printf等价的函数,它就是format。这里不是专门讲解format函数的,下面仅仅列举一些常见的例子,这些例子应该可以满足你在学习语言初期的需求了:

* 输出整型数
(format t "~d" 1000000) ==> 1000000
(format t "~x" 1000000) ==> f4240
(format t "~o" 1000000) ==> 3641100
(format t "~b" 1000000) ==> 11110100001001000000

上面依次是按十进制、16进制、八进制和二进制输出。

* 输出浮点数
(format t "~f" 3.1415) ==> 3.1415

* 输出字符串
(format t "~a" "hello lisp") ==> hello lisp

* 输出字符
(format t "~c" #\c) ==> c

* 输出换行符
以下借用《ANSI Common Lisp》书中的一个例子:
(format nil "Dear ~a, ~% Our records indicate…" "Mr. Malatesta")
==> "Dear Mr. Malatesta,
 Our records indicate…"

format函数的第一个参数表示是否输出到"标准输出(*STANDARD-OUTPUT*",如果传入t,则表示输出到标准输出设备上。第二个参数与C中的printf函数的第一个参数类似,是一个格式串,不同的是格式串中的指示符(directive)由printf中的'%'变成了'~'。

为了让大家更加直观地了解Common Lisp源代码到底是什么样子的,下面将给出一个Common Lisp的例子程序,这个程序用来计算参数字符串中大写字母的总个数:

我们先给出一个命令式风格的实现版本:
;; upper-char-counter.lisp
(defun upper-char-counter (str)
  (let ((len (length str)) (result 0))
      (do ((i 0 (+ i 1)))
          ((>= i len) result)
        (if (upper-case-p (char str i)) (setf result (1+ result))))))

即使你不懂Common Lisp语法,你也能大致猜测处理这段代码的逻辑,基本上与下面C代码是等价的:
int upper_char_counter(const char *str) {
    int result = 0;
    int len = strlen(str);

    int i = 0;
    while (i < len) {
        if (str[i] >= ‘A’ && str[i] <= 'Z') {
            result++;
        }
        i++;
    }

    return result;
}
 
下面是一个函数式风格的实现版本:

;; upper-char-counter.lisp
(defun upper-char-counter (str)
   (count-if #'upper-case-p str))

[1]> (load "upper-char-counter.lisp")
;; Loading file upper-char-counter.lisp …
;; Loaded file upper-char-counter.lisp
T
[2]> (upper-char-counter "a5B6CD!")
3

这个版本的代码显然更加简洁,但理解起来有些难度。函数count-if接受一个函数和一个字符串作为参数,count-if将函数upper-case-p应用于str中的各个字符上,并将返回true(t)的结果个数累加得到最终返回值。

走到这里,我想大家应该对Common Lisp有了一个感性的认识了,至少可以编写一些命令式风格的简单代码或复制一些现存的代码放到顶层环境中执行了。如果真的是这样,那我的目的就达到了^_^。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats