0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C语言12个浅显易懂的知识点

j4AI_wujianying 来源:未知 作者:胡薇 2018-06-25 09:50 次阅读

1.关于+=以及-=

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

[cpp]view plaincopy

inttemp;

chari

for(i=0;i

{

...

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

}

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

[cpp]view plaincopy

unsignedchari;

for(i=0;i<256;i++)  

{

//something

}

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

与之相类似的代码是:

[cpp]view plaincopy

unsignedchari;

for(i=10;i>=0;i--)

{

//something

}

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

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

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

[cpp]view plaincopy

if(Value=0x01)

{

//something

}

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

编译器同样不对这个问题做出指导性建议,值得一提的是,如果你在Keil的if语句中使用了赋值运算符,编译器会给出警告。

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

[cpp]view plaincopy

if(0x01==Value)

{

//something

}

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

4.error:#7:unrecognizedtoken

我在刚使用C语言以及Keil编译器时,对于这个编译器错误,有很深的印象。出现这个编译错误的典型代表是在敲代码的时候输入了中文标点!!

真是让人感慨万分的错误!我们这些与硬件打交道的程序员,为模数电生,为PCB死,为Debug奋斗一辈子,吃需求的亏,上大小写的当,最后死在标点上!!

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

6.sizeof()

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

[cpp]view plaincopy

inti,j;

j=sizeof(i);//对于这一句,当初压根没把它往关键字上想,这家伙伪装的实在够好。

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

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

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

int*p=NULL;

sizeof(p)的值是:

sizeof(*p)的值是:

inta[100]

sizeof(a)的值是:

sizeof(a[100])的值是:

sizeof(&a)的值是:

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

intb[100];

voidfun(intb[100])

{

sizeof(b);

}

sizeof(b)的值为:

7 关于数组越界

[cpp]view plaincopy

inta[30];

for(i=30;i>0;i--)

{

a[i]=something;

}

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

8. 关于宏

[cpp]view plaincopy

#defineMAX_TAST4;

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

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

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

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

[cpp]view plaincopy

#defineSQR(x)x*x..............1

或者这样写:

[cpp]view plaincopy

#defineSQR(x)(x)*(x)...............2

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

[cpp]view plaincopy

#defineSQR(x)((x)*(x))

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

[cpp]view plaincopy

#defineSQR(x)((x)*(x))

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

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

[cpp]view plaincopy

#defineSQR(x)printf("Thesquareofxis%d.\n",((x)*(x))")

[cpp]view plaincopy

如果这样使用宏:

[cpp]view plaincopy

SQR(8)

则输出为:

Thesquareofxis64.

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

[cpp]view plaincopy

#defineSQR(x)printf("Thesquareof"#x"is%d.\n",((x)*(x))")

这样得到的结果为:

Thesquareof8is64.

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

[cpp]view plaincopy

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

//判断p0.11端口是否为高电平,使用下述语句就是错误的:

if(READSDA==(1<<11))  

{

//是高电平,处理高电平的问题

}

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

[cpp]view plaincopy

if(IO0PIN&(1<<11) ==(1<<11))  

{

//是高电平,处理高电平的问题

}

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

9.数组和指针

在32位系统下,

定义一个数组:

[cpp]view plaincopy

inta[10]={1,2,3,4,5,6,7,8,9,0};

定义一个指针:

[cpp]view plaincopy

int*p;

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

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

如果,对指针p赋值:

p=a;

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

那么,a+1=?

&a+1=?

p+1=?

p[2]=?

*(p+2)=?

*(a+2)=?

再如果

[cpp]view plaincopy

int*ptr=(int*)(&a+1);

那,*(ptr-1)=?

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

当我们定义一个数组inta[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=0x00000200,那么p++运算之后的p值应该为0x00000204。有多少同学,曾经把它计算成0x00000201!

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

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

10.3/(-3)=?

3%(-2)=?

(-3)%2=?

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

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

11.指针数组与数组指针

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

[cpp]view plaincopy

int*p1[10];

int(*p2)[10];

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

对于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为例:

[cpp]view plaincopy

int(*p1[5])(void);

分析如下:

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

[cpp]view plaincopy

int(*p1)(void);

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

12.运算符的优先级

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

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

[cpp]view plaincopy

*string++;

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

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

[cpp]view plaincopy

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

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

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

[cpp]view plaincopy

if(IO0SET&(1<<17)==0)   

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

按照原意,应该这样写:

[cpp]view plaincopy

if((IO0SET&(1<<17)))==0)  

其实,运算符的优先级是有一定的规律可循的,下面给出优先级口诀(注:口诀来源于互联网)

优先级口诀

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

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

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

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

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

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

"三分天下"八九十;

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

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

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

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

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C语言
    +关注

    关注

    180

    文章

    7533

    浏览量

    128782
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48621

原文标题:曾让我哭笑不得抓狂的C语言

文章出处:【微信号:wujianying_danpianji,微信公众号:单片机精讲吴鉴鹰】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    单片机C语言知识点精华集锦(三)

    好资料分享!! 电子发烧友网讯:继《单片机学习知识点全攻略》得到广大读者好评,根据有网友提出美中不足的是所用单片机编程语言为汇编,基于此,电子发烧友网再接再厉再次为读者诚挚奉上非常详尽的《单片机C
    发表于 07-06 10:23

    经典中的经典!《C51单片机及C语言知识点必备秘籍》

    ,电子发烧友网再接再厉再次为读者诚挚奉上非常详尽的《单片机C语言知识点全攻略》系列单片机C语言学习教程,本教程共分为四部分,主要
    发表于 07-30 16:06

    嵌入式C/单片机C/标准C知识点

    、算法  说明:  学习单片机C一般只需要前9知识点即可进行产品开发,但要学习嵌入式C还需要要掌握:指针、结构体、链表、宏定义等知识点。 
    发表于 10-24 14:42

    工程师总结了一些C语言知识点

    :结构体知识点11:链表和文件(LINUX)知识点12:宏定义和编程思想、算法说明:学习单片机C一般只需要前9
    发表于 07-25 10:23

    【信盈达】C语言知识点的总结

    、算法说明:学习单片机C一般只需要前9知识点即可进行产品开发,但要学习嵌入式C还需要要掌握:指针、结构体、链表、宏定义等知识点。二、单片机
    发表于 10-08 14:41

    关于STM32中重要的C语言知识点看完你就懂了

    关于STM32中重要的C语言知识点看完你就懂了
    发表于 10-13 07:47

    C语言程序小知识点总结

    C语言总结(stm32嵌入式开发)文章目录C语言总结(stm32嵌入式开发)c程序小知识点总结1
    发表于 11-05 07:45

    浅谈几个C语言基础知识点

    文章目录前言一、位操作二、使用步骤前言这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础
    发表于 12-09 07:14

    嵌入式C语言必学知识点汇总

    导读:怎么做好嵌入式?相信这个问题无论问谁你都会得到一句学好C语言!今天推荐一篇大佬写的嵌入式C语言知识点总结,非常值得一读。从语法上来说
    发表于 12-15 07:43

    浅显易懂的方式带你敲开Linux驱动开发的大门

    # 前言开发过单片机的小伙伴可以看下我之前的一篇文章从单片机开发到linux内核驱动,以浅显易懂的方式带你敲开Linux驱动开发的大门。# 正文用户空间的每个函数(用于使用设备或者文件的),在内
    发表于 01-19 08:31

    嵌入式C语言知识点汇总,绝对实用

    了一些这方面的经验和思考,就希望总结下来,系统的阐述嵌入式C语言的重要知识点,就是这篇文章的由来。本文以自己在嵌入式上的实践为基础,在结合相关资料, 阐述嵌入式需要了解的C
    发表于 02-17 06:10

    RT-Thread嵌入式实时多线程操作系统介绍

    的自主知识产权。经过近12年头的沉淀,伴随着物联网的兴起,它正演变成一功能强大、组件丰富的物联网操作系统。RT-Thread的官网。读者可以在官网上看到许多RT-Thread的相关
    发表于 02-17 07:13

    如何看懂时序图

    关于如何看懂时序图的技术文档, 精辟,浅显易懂弄噢。
    发表于 10-29 15:01 0次下载

    解析甄别人工智能泡沫浅显易懂的方法

    不过,这套方法论对用户可能并不适用,毕竟并无这样的专业知识和经验积累。所以,面对市面上林林总总的人工智能产品,要不要掏钱包,还得有浅显易懂的甄别人工智能泡沫的方法。
    的头像 发表于 01-09 10:43 2145次阅读

    电容器是如何工作的概述

    通过本文浅显易懂的了解电容器是如何工作的?
    的头像 发表于 03-29 10:46 7644次阅读