分类归档: Programming

编程编程编程。。。

解析C语言复杂声明

C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英文原文是这样说的:

The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.

这段英文的翻译如下:

右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。

笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。

现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:

int (*func)(int *p);

首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是 int。

int (*func)(int *p, int (*f)(int*));

func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。

int (*func[5])(int *p);

func 右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。

int (*(*func)[5])(int *p);

func 被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。

int (*(*func)(int *p))[5];

func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。

要注意有些复杂指针声明是非法的,例如:

int func(void) [5];

func是一个返回值为具有5个int元素的数组的函数。但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组。

int func[5](void);

func是一个具有5个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的。

作为练习,下面列几个复杂指针声明给读者自己来解析,答案放在第十章里。

int (*(*func)[5][6])[7][8];

int (*(*(*func)(int *))[5])(int *);

int (*(*func[7][8][9])(int*))[5];

实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,例如对于声明:

int (*(*func)(int *p))[5];

可以这样分解:

typedef int (*PARA)[5];
typedef PARA (*func)(int *);

这样就容易看得多了。

Read: 599

关于(*(void(*)())0)()

    前段时间看了这本C Traps and Pitfalls,看到里面有这样的一个语句:
   (*(void(*)())0)();

    初看此语句,让我头皮发麻,不寒而栗呀,我看了好半天书,也只是一知半解,后来在下面两个论坛上分别发一个帖子问了一下:
http://bbs.chinaunix.net/viewthread.php?tid=891271&extra=page%3D2
http://www.vckbase.com/bbs/viewtopic2.asp?rid=2869084&sf=92&forumID=1&pg=4

    首先这是一个函数调用,但是一个特殊的函数调用,在我们普通的C语言程序中是没有任何意义的,甚至不能执行的,我在VC6的环境下写了一个测试例子:
int main()
{
     (*(void(*)())0)();
     return 0;
}

    编译是通过了,但是却在执行的过程中出现了错误。在Linux下也进行了测试,也是编译通过,但运行出现了错误。
    然而,这个并不代表它没有用,它在一些特殊的环境中是有用的,比如在地址0处设置了跳转或是中断处理函数之类的,那么这个函数调用就有用了。
    总结一下我自己的理解和网上网友的理解:
   
    这是一个函数调用,它调用的是地址为0的函数。
如果我们令 f =
*(void(*)())0
那么这个上面的语句可以写成 (f)(),也就是f();这很显然是一个函数调用
而 void(*)()是一个指针类型,它是一个参数为空而返回值为void (实际上也是空)的函数指针类型,(void(*)())0就是把0强制转换成为一个函数指针,
我们可以这样理解
typedef void(*pp)();
   (*(pp)0)();

(void(*)())0这个函数指针指向一个地址为0的函数,如果前面再加一个星号*,那么就是对这个函数的引用,即(*(void(*)())0)就代表了这个函数,后面再加一个()就是对该函数的调用。

Read: 758

细说字体 Sans Serif 与 Serif

拜读了 LGJ Font Notes 的 Serif VS. Sans Serif 注1 一文,顿时胸中豁然开朗,
终于明白了什么是Sans Serif字体,什么是Serif字体,什么地方应该使用Sans Serif字体,
什么地方又应该使用Serif字体。这些问题,不管是对Web Designer还是普通用户,
了解它我觉得还是十分必要的,比如在很多浏览器中就涉及相应的设置。

Quote:


在西方国家罗马字母阵营中,字体分为两大种类:Sans Serif和 Serif,打字机体虽然也属于 Sans Serif,但由于是等宽字体,所以另外独立出 Monospace 这一种类,例如在Web中,表示代码时常常要使用等宽字体。

Serif的意思是,在字的笔画开始及結束的地方有额外的装饰,而且笔画的粗细会因直橫的不同而有不同。相反的,Sans Serif则沒有这些额外的装饰,笔画粗细大致差不多。如下图:
Open full image in a new window
可以看出,我们平时所用的Georgia、Times New Roman等就属于Serif字体,而Arial、Tahoma、Verdana等则属于Sans Serif字体。对中文而言,同样存在这两大种类,很明显,宋体、细明体(繁体中常用)等就属于Serif,而黑体、幼圆等则属于Sans Serif。

Serif 和 Sans Serif 的一般比较
Serif的字体容易辨认,因此易读性较高。反之Sans Serif 则较醒目,但在行文阅读的情況下,Sans Serif 容易造成字母辨认的困扰,常会有来回重读及上下行错乱的情形。
Serif 强调了字母笔画的开始及结束,因此较易前后连续性的辨识。
Serif 强调一个word,而非单一的字母,反之Sans Serif则强调个别字母。
在小字体的场合,通常Sans Serif比Serif更清晰。
适用用途:
通常文章的內文、正文使用的是易读性较佳的 Serif 字体,这可增加易读性,而且长时间阅读下因为会以word为单位来阅读,较不容易疲倦。而标题、表格內用字则采用较醒目的Sans Serif字体,它需要显著、醒目,但不必长时间盯著这些字来阅读。

像宣传品、海报类,为求醒目,它的短篇的段落也会采用Sans Serif字体。但在书籍、报刊杂志,正文有相当篇幅的情形下,则应采用Serif字体来减轻读者阅读上的负担。在Web设计及浏览器设置中也应遵循此原则为是。


实际应用
在Firefox 中(目前似乎只有Firefox有此功能),可以分别单独指定Sans Serif、Serif及Monospace的中西文字体,然而这个选项并未设置在工具菜单中,不过可以在Addressbar中键入about &: config,然后在Filter中过滤font找到如下Preference Name:

Code:
font.name.monospace.x-western
font.name.monospace.zh-CN
font.name.sans-serif.x-western
font.name.sans-serif.zh-CN
font.name.serif.x-western
font.name.serif.zh-CN

你可以依照上述Sans Serif、Serif及Monospace的原则来分别指定一种对应字体,按照W3C的CSS规则 注2 ,在font(或者font-family)的最后都要求指定一个Serif这样的Generic-family,避免客户端实在没有指定字体时使用本机上的Serif默认字体。

由于Mozilla系的浏览器的这种特性,我们在CSS的font设定中,可以分别设定中西文字体来丰富版面,例如我们指定一组小字体(类似此文章标题下的日期、作者、类别):

Code:
font-family: Arial, PMingLiU, Sans-Serif;
/*按照文章前面所讲,在小字体时Sans-serif更为清晰,所以随便指定一个Arial,
而作为Serif的PMingLiU(明体)在小字体显示时也有不俗的外观。
最后不忘指定一个Sans-Serif类。*/

参考资料:

Code:
注1: Serif VS. Sans Serif
注2: Font family: the ‘font-family’ property

Read: 650

C语言全局变量和局部变量

引子—
         变量可以在程序中三个地方说明: 函数内部、函数的参数定义中或所有的函数外部。根据所定义位置的不同, 变量可分为局部变量、形式参数和全程变量。从空间角度来看,变量可以分为全局变量和局部变量,而从时间角度来分的 可以有静态存储变量和动态存储变量之分。

一。全局变量和局部变量

1。局部变量

他是 指在函数内部定义的变量 作用域为定义局部变量的函数 也就是说他只能在定义他的函数中使用

最值得注意的是 只有在程序执行到定义他的模块时才能生成,一旦执行退出该模块则起变量消失

eg.

func ()

{
    int x;    局部变量x的作用域 很明确
    ……
}

2。全局变量

在程序执行的过程中一直有效

eg.

int x=1;

func ()

{
    x=x+1;

}

func1 ()

{
   x=x-1;

}

main ()

{

}
由此不难看出整型x的作用范围

对于全局变量 如果在定义的时候不做初始化 则系统将自动为起赋值 数值型为0

字符型为空”

全局变量的弊端 增加内存开销 降低函数的通用性

定义全局变量时 理想的位置是在文件的开头 当这些函数以及同一个程序中的其他源程序文件中的某些函数需要使用该全局变量时 在函数内部对该变量使用extern 加以说明 说明他是外部的

(这里还要做详细的讲解)

eg.

main ()

{
extern int a,b;
printf ("mIn is %dn",min(a,b));
}

int a=1,b=2;
int min (x,y)
int x,y;
{
int z;
z=x<y?x:y;
return(z);
}
我还要说明的是 对外部变量的说明和对全局变量的定义不是一回事

对外部变量的说明 只是声明该变量是在外部定义过的一个全局变量 在这里引用 而对全局变量的定义则是要对起分配存储单元的

一个全局变量只能定义一次 可是却可以多次引用

*** 在同一源文件中,全局变量和局部变量同名时,在局部变量的作用域内,全局变量不起作用的。

二。静态存储变量和动态存储变量

对于程序运行期间根据需要进行临时动态分配存储空间的变量 为动态存储变量

对于那些程序运行期间永久占用固定内存的变量 称为静态存储变量

还要说明的是 程序的指令代码是存放在程序代码区的 静态存储变量是存放在静态数据区的 包括全局变量等 而程序中的动态存储变量存放在动态数据区 如函数的形参以及函数调用时的返回地址等

三。C语言中的变量存储分类指定
    
     auto
     auto称为自动变量 如果函数不做其他说明的话 均为自动变量

     static
     static称为静态变量。根据变量的类型可以分为静态局部变量和静态全程变量。
     1. 静态局部变量
    

      它与局部变量的区别在于: 在函数退出时, 这个变量始终存在, 但不能被其它函数使用, 当再次进入该函数时, 将保存上次的结果。其它与局部变量一样。
    

     2. 静态全程变量
    

      Turbo C2.0允许将大型程序分成若干独立模块文件分别编译, 然后将所有模块的目标文件连接在一起, 从而提高编译速度, 同时也便于软件的管理和维护。静态全程变量就是指只在定义它的源文件中可见而在其它源文件中不可见的变量。它与全程变量的区别是: 全程变量可以再说明为外部变量(extern), 被其它源文件使用, 而静态全程变量却不能再被说明为外部的, 即只能被所在的源文件使用。
   
   
     extern
     extern称为外部变量。为了使变量除了在定义它的源文件中可以使用外, 还要被其它文件使用。因此,    必须将全程变量通知每一个程序模块文件,    此时可用extern来说明。

eg.
         文件1为file1.c                   文件2为file2.c
     int i, j;/*定义全程变量*/         extern int i, j;/*说明将i, j从
                                                      文件1中复制过来*/
     char c;                           extern char c; /*将c复制过来*/
     void func1(int k);                func2()         /*用户定义函数*/
                                      {
     main()                               static float k;/*定义静态变量*/
     {                                    i=j*5/100;
           func1(20);/*调用函数*/         k=i/1.5;
           func2();                            .
           .                                   .
           .                                   .
           .                             }
      }
      func1(int k) /*用户定义函数*/
      {
           j=k*100;
      }

     对于以上两个文件file1.c和file2.c, 用Turbo C2.0的集成开发环境进行编译
连接时, 首先应建立一个.prj的文件。例如file.prj, 该文件内容如下:
      file1.c
      file2.c
     然后将file.prj的文件名写入主菜单Project中的Project Name项中。 再用F9
编译连接, 就可产生一个文件名为fioe.exe的可执行文件。
     
  

     register
     register称为寄存器变量。它只能用于整型和字符型变量。定义符register说明的变量被Turbo C2.0存储在CPU的寄存器中,   而不是象普通的变量那样存储在内存中, 这样可以提高运算速度。但是Turbo C2.0只允许同时定义两个寄存器变量,一旦超过两个, 编译程序会自动地将超过限制数目的寄存器变量当作非寄存器变量来处理。因此, 寄存器变量常用在同一变量名频繁出现的地方。
     另外, 寄存器变量只适用于局部变量和函数的形式参数, 它属于auto型变量,
因此, 不能用作全程变量。定义一个整型寄存器变量可写成:
       register int a;
     对于以上所介绍的变量类型和变量存储类型将会在以后的学习中, 通过例行程序中的定义、使用来逐渐加深理解。

Read: 16

实现C语言高效编程的四大秘技

第1招:以空间换时间

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

例如:字符串的赋值。

方法A,通常的办法:

#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,“This is a example!!”);

方法B:

const char string2[LEN] =“This is a 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 ((1U << (bw ## __bf)) – 1) << (bs ## __bf);
}
void SET_BITS(int __dst, int __bf, int __val)
{
__dst = ((__dst) & ~(BIT_MASK(__bf))) | (((__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))) |
(((__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;
for (I = 1 ;I<=100; I ++){
j += I;
}

方法F

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

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

  在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例如下:

方法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

#ifdef _PC_
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,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。

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

Read: 587