那年曾让我哭笑不得抓狂的C语言

  1.关于+=以及-=

  这是两个运算符,但你否有过这种经历:

    int temp;
    char i
    ;i<MAX;i++)
    {
        ...
        temp=+;    //这里本意是每次循环,temp都自增2,但是却将'+='写成了'=+',按照这种写法,每次循环都为temp赋值正数2,与本意相差甚远
    }

  2. 关于意想不到的死循环

  当我们用上述代码想实现一个小循环时,结果却事与愿违,这其实是死循环的另一种写法,因为无符号变量i最大只有255,要命的是,编译器并不会指出这个错误。

与之相类似的代码是:

​   这也是一个死循环,你看出什么原因了吗?无论i如何减,i都是大于等于0的。

  这就告诉我们对于每个变量类型的取值范围要由清醒的认识。值得注意的是相同的变量类型对于不同的CPU构架和不同的编译器会有不同的结果。比如int类型在大多数16位CPU构架中占用两个字节,但在32位CPU中却往往占用4个字节;char类型在绝大多数编译器中都是有符号数,但在keil MDK中却是无符号数,若是要在keil MDK下定义有符号char类型变量,必须用signed显式声明。我曾读过一本书,其中有一句话:“signed关键字也是很宽宏大量,你也可以完全当它不存在,在缺省状态下,编译器默认数据位signed类型”,这句话便是有异议的,我们应该对自己所用的CPU构架以及编译器熟练掌握。

  3. 关于'='和'=='

if(Value=0x01)
{
​      //something
}

  当我们判断一个变量是否等于0x01时,你是否也写过类似上面的代码?C语言的创造者认为赋值运算符"="出现的概率要远远大于等于运算符"==",因此,我们正常逻辑中的"等于"符号(=)在C语言中成了赋值运算符,而C语言的"等于"运算符却被两个等于号(==)所代替。我之所以对这个事件耿耿于怀是因为我在大二的时候参加的C++二级上机考试,当我感觉很轻松的做完最后一道题后,却发现运算的结果却与逻辑相悖,经过调试发现,有一个条件一直为真,我检查了很多遍才发现出问题的逻辑将等于运算符写成了赋值运算符。在if语句中给变量赋一个非零值,也难怪这个逻辑总是为真。编译器同样不对这个问题做出指导性建议,值得一提的是,如果你在Keil的if语句中使用了赋值运算符,编译器会给出警告。

  避免这个问题的一个很好的办法是使用良好编程习惯,比如上面的代码可写为:

  将常量值放到变量的前面,即使将等于运算符写成赋值运算符,编译器也能产生一个语法错误,因为将一个变量赋值给一个常量是非法的。

  4. error: #7: unrecognized token

  我在刚使用C语言以及Keil编译器时,对于这个编译器错误,有很深的印象。出现这个编译错误的典型代表是在敲代码的时候输入了中文标点!!真是让人感慨万分的错误!我们这些与硬件打交道的程序员,为模数电生,为PCB死,为Debug奋斗一辈子,吃需求的亏,上大小写的当,最后死在标点上!!

  5. 关于字母'O'和数字'0',以及字母'l'和数字'1' ,在嵌入式编程中很容易和寄存器打交道,一个CPU如果有两个相同模块时,这些模块寄存器,往往使用数字0和数字1来区分模块0和模块1,比如,NXP的ARM7 串口模块的两个接收缓冲寄存器分别为:U0RBR和U1RBR,要命的是在键盘上字母O和数字0相距的还那么近,你是否也有将上述寄存器写成UORBR和UlRBR的经历,我是曾经在这方面纠结过一次,好在编译器能指出这个未定义的字符串。

  6. sizeof()

  不知道有多少人和我曾经一样,将这个关键字认为是一个库函数。

  既然提到它,不如多说一下,sizeof在计算变量所占空间大小时,括号可以省略,而计算类型大小时,不能省略。什么意思呢?还是上面的变量声明,可以写成j=sizeof(i)也可以写成j=sizeof i,因为这是计算变量所占空间大小;可以写成j=sizeof(int),但不可以写成j=sizeof int,因为这是计算数据类型大小。

  总体来说,关键字sizeof的具有一定的变态基础的,在我还是小白的时候,曾经为下面的一道题伤过脑袋:

下面代码里,假设在32位系统下,个sizeof计算的结果分别是多少?

int *p=NULL;

sizeof(p)的值是:

sizeof(*p)的值是:

int a[100]

sizeof(a)的值是:

sizeof(a[100])的值是:

sizeof(&a)的值是:

sizeof(&a[0])的值是:

int b[100];

void fun(int b[100])

{

sizeof(b);

}

sizeof(b)的值为:

  7 关于数组越界

  这是个典型的数组越界例子,最近我同事的一个程序中便出现了。不知道有多少同学遇到或将要遇到数组越界问题,即便你定义了30个数组a[30],你也不可以为a[30]赋值,因为下标为30的元素已经越界了。所以说数组下标定义的很奇特,它是从0开始的。但当我们还是新手的时候,最容易忽视这一点。幸好现在的有些编译器会对这个越界产生警告信息。

  8. 关于宏

  这个错误编译器会指出的,即便这样,相信很多同学在最初的时候也不会在第一时间发现这句代码的最后多了一个分号。这个分号会导致一些编译器报错,因为宏定义的结尾并不需要分号。

  同样与define有关的是这样一句:#define  "config.h",我便吃过类似暗亏,在编译器的提示之下,看了几遍才发现头文件包含应该是#include "config.h"。

  既然提到#define,还是说说它需要注意的几个点,也是经常在资料上被提及的。

  a.使用#define时,括号一定要足够。比如定义一个宏函数,求x的平方:

 #define SQR(x)  x*x   .............. 1 

或者这样写:

上面两种都是有风险的,对于第一种定义,SQR(10+1)就会得到和我们的设想不一致的结果;第二种SQR(5*3)*SQR(5*3)也会得到和我们设想不一致的结果,因此更安全的定义方法是:

b.使用#define的时候,,意空格的使用。比如下面的例子:

  这已经不是SQR(x)函数了,编译器会把认为定义了一个宏SQR,代表(x)  ((x)*(x)),因为SQR与(x)之间有空格。这点需要注意。

  c.使用'#'在字符串中包含宏参数。比如下面的例子:

#define  SQR(x)  printf("The square of  x  is %d.\n",((x)*(x))")

  如果这样使用宏

SQR()

  则输出为:The square of  x  is 64.

  这个时候引号中的x被当做字符串来处理了,而不是一个可以被宏参数替换的符号.如果你想在字符中的x也被宏参数替换,可以这么来定义宏:

 #define  SQR(x)  printf("The square of "#x" is %d.\n",((x)*(x))")

  这样得到的结果为:The square of 8 is 64.

  上面的这些例子,恐怕是网上随处可见的,但真的会这么用却有待考证。下面给出一个我自己遇到的不加括号产生错误的例子。在嵌入式编程中,遇到读取IO端口某一位的电平状态的场合是在平常不过的了,比如在NXP的ARM7中,读取端口P0.11的电平状态并判断是否为高电平,代码如下:

#define READSDA       IO0PIN&(1<<11)  //定义宏,读IO口p0.11的端口状态,但并未使用足够多的括号

//判断p0.11端口是否为高电平,使用下述语句就是错误的:
<<))
{
     //是高电平,处理高电平的问题
}

  编译器在编译后将宏带入,原if语句变为:

<<) ==(<<))
{
    //是高电平,处理高电平的问题
} 

  这样的话,运算符'=='的优先级是大于'&'的,从而IO0PIN&(1<<11) ==(1<<11))语句等效为IO0PIN&0x00000001,相当于判断P0.1是否为高电平,与原意相差甚远。

  9. 数组和指针

  在32位系统下,

  定义一个数组:

]={,,,,,,,,,};

  定义一个指针:

int *p;

  那么,a、a[0]、&a、&a[0]各表示什么意思?

  那么,sizeof(a)、sizeof(a[0])、sizeof(&a)、sizeof(&a[0])的值各是什么?

  如果,对指针p赋值:

  p=a;

  并且通过编译器仿真,得知现在p等于a等于0x0000 0200,

  那么,a+1=?

&a+1=?

p+1=?

p[2]=?

*(p+2)=?

*(a+2)=?

  再如果

);

  那,*(ptr-1)=?

  世上最暧昧、最纠缠不清的,莫过于数组名和指针。这一方面源于大学的教材并没有重视这一块,也源于教学时硬生生的将C语言和硬件分开。一方面,教材和教这一门的老师在开始时便向我们灌输了“数组名和指针很像,可以等同”的思想;另一方面,在学C语言的时候,并没有系统的学过计算机硬件(寻址、存储、汇编),C语言是一个很接近硬件的高级语言,如果没有处理器(包括单片机等微处理器)的基础知识,会导致非常多的同学怎么都理解不透C语言的指针和数组。

  当我们定义一个数组int a[10]时,编译器会分配一块内存,这块内存的名字命名为a,这个a只是一个名字,只是方便编译器和编程者使用,编译器并不为这个名字分配空间来存储它。我们可以用a[0]、a[1]来访问数组内的元素。a作为右值(位于等号的右边)时,表示的是数组第一个元素的地址(意义与&a[0]一样,但&a[0]是一个指针变量,编译器会为他分配空间,a却不一样,编译器并不为它分配什么空间),而并非数组首地址,&a才表示数组的首地址。

  所以,第一个问题,a是这个数组所在的内存的名字,当它为右值时,表示数组首元素的地址,a[0]是数组的第一个元素,其值等于1,&a是整个数组的首地址,它是一个指针;&a[0]是数组首元素的地址,它的值和a做右值时一样,但意义不同,因为&a[0]是一个指针,编译器要为它分配存储空间,但a却不会被分配存储空间,a也不是指针型变量。

  明白了上面那些,关于sizeof的计算也就不会困难了:

  sizeof(a)=4*10=40,因为a代表的是整块数组内存;

  sizeof(a[0])=4,这相当于计算int的大小,在32位系统下,int占4个字节。

  sizeof(&a)和sizeof(&a[0])都是计算指针变量的大小,在32位系统下,指针变量占4个字节。

  对于最后一个问题,涉及到指针的加减。

  指针的加减中有一个重要的原则就是它的加减跟我们普通意义上的加减不是一个概念,它是按指针所指类型的内存大小来进行加减的。当我还是一个新手的时候,对于p++、p+1这类指针运算的含义超出了我的意料之外,在上例中,若是p=0x0000 0200,那么p++运算之后的p值应该为0x0000 0204。有多少同学,曾经把它计算成0x0000 0201!

  数组名a在做计算的时候表示数组首元素的地址,这时候a等于0x0000 0200,所以a+1等于0x0000 0200+4=0x0000 0204,因为一个int型在32位系统下占用4个字节。&a是整个数组的首地址,&a+1=0x0000 0200+4*10=0x0000 0228。

  其它的也都比较好理解,p+1=0x0000 0200+04=0x0000 0204、 p[2]=3、 *(p+2)=3、*(ptr-1)=0.

  10.  3/(-3)=?

  3%(-2)=?

  (-3)%2=?

  抛开它是否有实际的意义,这个看似简单的语句,不知道有多少同学不确定结果到底是什么。

  其实大多数的编译器遵循这样一个规定:余数与被除数的正负号相同,被除数等于结果乘以除数加上余数。所以,以上的三个结果分别为-1、1、-1。

  11. 指针数组与数组指针

  有一段时间,我怎么都不能区分指针数组和数组指针,就像下面的声明:

];
];

  首先,要来解释一下什么是指针数组,什么是数组指针:指针数组首先是一个数组,它的成员都是指针型变量;数组指针首先是一个指针,这个指针指向一个数组(它的值和数组名表示的值一样,只是数组指针是一个变量,编译器要为它分配存储空间,但数组名类似于一个常亮,是编译器在编译阶段就确定好的一个值,编译器不会为它分配存储空间)。

  对于p1,由于中括号的优先级(关于优先级,后面会专门提起)是大于*的,所以p1首先与'[]'相结合,构成一个数组,在这个数组之前又有一个'*'运算符,说明这是定义一个指针数组(int *a:定义一个指针a,这里可以将p1[10]替换成a,就不难理解了),数组的元素都是指向int型的指针。

  对于p2,'()'虽然与'[]'为同一优先级,但却是表达式结合方向从左到右结合的,所以编译器会先处理(*p2),这是典型的定义一个指针,只不过这个指针指向一个包含10个int型数据的内存块。为了加强理解,这里给出两个相同原理的函数声明:

  void * p1(void);  ---------------------- 声明1,定义一个返回值是void类型指针的函数p1

  void (*p2)(void); ----------------------- 声明2,定义一个函数指针,该函数不返回任何值

  有了上面的铺垫,现在定义一个高级C语言编程技巧中常用的函数指针数组应该很容易了吧!首先这是一个数组,数组的元素是指向一个函数的指针,以定义一个参数为空,返回值为int类型的函数指针数组p1为例:

])(void);

  分析如下:定义一个返回值为int类型的函数指针p1应该是:

int (*p1)(void);  

  那么将这类指针放到一个数组中不正是我们需要的定义吗,套用指针数组的定义方法,返回值为int类型函数指针数组定义为:int (*p1[5])(void);

  12 .运算符的优先级

  C语言有32个关键字却有44个运算符!运算符之间有固定的优先级,虽然它们可以分成15类优先级,但如果让一个程序员完全记住这些运算符之间的优先级关系,怕是老手也是不容易的吧。如果你的程序只是语法错误,这类错误是最容易解决的,编译器就会帮你检测出来;如果是你自己的逻辑出现错误,那么根据运行结果仔细检查一下代码可能也不难发现;但若是你的逻辑正确却记错了运算符的优先级关系,导致程序运行结果和你设想的不同,这种错误就很难查出了,因为在你的潜意识里,已经把这种错误点当成理所当然不用关注的。

  请看下面一句代码代表什么意思:

*string ++;  

  由于*和++但是单目运算符,优先级相同,但结合方向却是自右向左,那么*string++应该就是*(string++),取出当前字符后将指针后移。不知道有没有人把它认为是(*string)++,即取指针string所指向的对象,然后将该对象增1.

  我曾经在代码中不止一次的出现过因为优先级问题而导致的程序逻辑错误,那个时候我并没有完整的记过优先级,二十使用了一种“偷巧”的方法:只是简单记住前几级优先级,其它自己没把握的一律使用括号。这种方法我现在是不推荐的,一是因为大量的括号影响代码阅读和程序的简洁,二是总有时候我们稍微一松懈,就忘记了加括号,而后一种情况,正是很多人可能会遇到的。比如下面一句代码,无符号8位变量ucTimeValue 中存放十进制编码的数据23,我想将十进制编码转成16进制编码,代码为:

temp8=(ucTimeValue>>)*+ucTimeValue&0x0F;     //十进制转化为16进制,但忽略了运算符'+'的优先级是大于运算符'&'的

  像这类代码编译肯定可以通过,但运行的结果却出乎我的意料,而且由于我先入为主的错误思想,要在一大段代码中发现这个错误着实要花费一番功夫。

再例如,如果我想判断一个寄存器的某一位是否为零,假如是判断寄存器IO0SET的bit17是否为零,但代码却写成了这样:

<<)==) 

  这样写其实是得不到正确的结果的,因为我忽略了"=="的优先级是大于"&"的.按照上面的代码分析:因为"=="的优先级大于"&",所以程序先判断(1<<17)是否等于0?发现这是不相等的,所以(1<<17)==0表达式的值为假,即为0,0与(&)上任何一个数都是0,所以IO0SET&(1<<17))==0整个表达式的值永远为0,这与原意相差甚远。

  按照原意,应该这样写:

<<)))==)

  其实,运算符的优先级是有一定的规律可循的,下面给出优先级口诀

括号成员第一;    括号运算符[]() 成员运算符.  ->

全体单目第二;    所有的单目运算符比如++ -- +(正) -(负) 指针运算*&

乘除余三,加减四;             这个"余"是指取余运算即%

移位五,关系六;              移位运算符:<< >> ,关系:> < >= <= 等

等于(与)不等排第七;       即== !=

位与异或和位或;             这几个都是位运算: 位与(&)异或(^)位或(|)

"三分天下"八九十;

逻辑或跟与;                     逻辑运算符:|| 和 &&

十二和十一;                     注意顺序:优先级(||)  底于 优先级(&&)

条件高于赋值,                    三目运算符优先级排到 13 位只比赋值运算符和","高

逗号运算级最低!              逗号运算符优先级最低

上一篇:[入门]bower安装和使用


下一篇:前端安全 -- XSS攻击