1.5 嵌入式系统常用的C语言基本语法概要
C语言是在20世纪70年代初问世的,1978年美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。由B.W.Kernighan和D.M.Ritchit合著的The C Programming Language一书,简称K&R,也有人称其为K&R标准。但是,在K&R中并没有定义一个完整的标准C语言,后来由美国国家标准学会在此基础上制定了一个C语言标准,并于1983年发表,通常称为ANSI C或标准C。
本节简要介绍C语言的基本知识,特别是和嵌入式系统编程密切相关的基本知识,未学过标准C语言的读者可以通过本节了解C语言,以后通过实例逐步积累相关编程知识;对C语言很熟悉的读者,可以跳过本节。
1.5.1 C语言的运算符与数据类型
1. C语言的运算符
C语言的运算符分为算术、逻辑、关系和位运算及一些特殊的操作符。表1-1所示为C语言的常用运算符及其含义。
表1-1 C语言的常用运算符
2. C语言的数据类型
C语言的数据类型有基本数据类型和构造数据类型两大类。其中,基本数据类型是指字节型、整型及实型,如表1-2所示;构造数据类型有数组、指针、枚举、结构体、共用体和空类型。
枚举是一个被命名为整型常量的集合。结构体和共用体是基本数据类型的组合。空类型字节长度为0,主要有两个用途:一是明确表示一个函数不返回任何值;二是产生一个同一类型指针(可根据需要动态地分配其内存)。
嵌入式中还常用到寄存器类型(Register)变量。例如,通常将内存变量(包括全局变量、静态变量、局部变量)的值存放在内存中,CPU访问内存变量要通过三总线(地址总线、数据总线、控制总线)进行,如果有一些变量使用频繁,则为存取变量的值要花不少时间。为提高执行效率,C语言允许使用关键字“register”声明,将少量局部变量的值放在CPU的内部寄存器中,需要用时直接从寄存器中取出参加运算,不必再到内存中存取。关于register类型变量的使用需注意:①只有局部变量和形式参数可以使用寄存器变量,其他变量(如全局变量、静态变量)不能使用register类型变量;②CPU内部寄存器数目很少,不能定义任意多个寄存器变量。
表1-2 C语言基本数据类型
1.5.2 程序流程控制
在程序设计中主要有顺序结构、选择结构和循环结构3种基本控制结构。
1. 顺序结构
顺序结构就是从前向后依次执行语句。从整体上看,所有程序的基本结构都是顺序结构,中间的某个过程可以是选择结构或循环结构。
2. 选择结构
在大多数程序中都会包含选择结构。其作用是根据所指定的条件是否满足,决定执行哪些语句。在C语言中主要有if和switch两种选择结构。
1)if结构
if(表达式)语句项;
或
if(表达式) 语句项; else 语句项;
如果表达式取值真(除0以外的任何值),则执行if的语句项;否则,如果else存在,就执行else的语句项。每次只会执行if或else中的某一个分支。语句项可以是单独的一条语句,也可以是多条语句组成的语句块(要用一对大括号“{}”括起来)。
if语句可以嵌套,有多个if语句时else与最近的一个配对。对于多分支语句,可以使用if…else if…else if…else…的多重判断结构,也可以使用下面讲到的switch开关语句。
2)switch结构
switch是C语言内部多分支选择语句,它根据某些整型和字符常量对一个表达式进行连续测试,当一常量值与其匹配时,它就执行与该变量有关的一个或多个语句。switch语句的一般形式如下。
根据case语句中所给出的常量值,按顺序对表达式的值进行测试,当常量与表达式的值相等时,就执行这个常量所在的case后的语句块,直到碰到break语句,或者switch的末尾为止。若没有一个常量与表达式的值相符,则执行default后的语句块。default是可选的,如果它不存在,并且所有的常量与表达式的值都不相符,那就不做任何处理。
switch语句与if语句的不同之处在于,switch只能对等式进行测试,而if可以计算关系表达式或逻辑表达式。
break语句在switch语句中是可选的,但是不用break,则从当前满足条件的case语句开始连续执行后续指令,不判断后续case语句的条件,直到碰到break或switch语句的末尾为止。为了避免输出不应有的结果,在每一个case语句之后加break语句,使每一次执行之后均可跳出switch语句。
3. 循环结构
C语言中的循环结构常用for循环、while循环与do…while循环。
1)for循环
格式为:
for(初始化表达式;条件表达式;修正表达式) {循环体}
执行过程为:先求解初始化表达式;再判断条件表达式,若为假(0),则结束循环,转到循环下面的语句;如果其值为真(非0),则执行“循环体”中的语句。然后求解修正表达式;再转到判断条件表达式处根据情况决定是否继续执行“循环体”。
2)while循环
格式为:
while(条件表达式) {循环体}
当表达式的值为真(非0)时执行循环体,其特点是先判断后执行。
3)do…while循环
格式为:
do {循环体} while(条件表达式);
其特点是先执行后判断,即当流程到达do后,立即执行循环体一次,然后才对条件表达式进行计算、判断。若条件表达式的值为真(非0),则重复执行一次循环体。
4. break和continue语句在循环中的应用
在循环中常常使用break和continue语句,这两个语句都会改变循环的执行情况。break语句用来从循环体中强行跳出循环,终止整个循环的执行;continue语句使其后语句不再被执行,进行新的一次循环(可以理解为返回循环开始处执行)。
1.5.3 函数
函数即子程序,也是“语句的集合”,就是把经常使用的语句群定义成函数,供其他程序调用,函数的编写与使用要遵循软件工程的基本规范。
使用函数要注意:函数定义时要同时声明其类型;调用函数前要先声明该函数;传给函数的参数值,其类型要与函数原定义一致;接收函数返回值的变量,其类型也要与函数类型一致等。函数传参有传值与传址之分。
函数的返回值:
return表达式;
return语句用来立即结束函数,并返回一确定值给调用程序。如果函数的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换,即函数类型决定返回值的类型。
1.5.4 数据存储方式
在C语言中,存储与操作方式除基本变量方式外,还有数组、指针、结构体、共用体,简介如下。此外,数据类型还可使用typedef定义别名,方便使用。
1. 数组
在C语言中,数组是一个构造类型的数据,是由基本类型数据按照一定的规则组成的。构造类型还包括结构体类型、共用体类型。数组是有序数据的集合,数组中的每一个元素都属于同一个数据类型,并用一个统一的数组名和下标唯一地确定数组中的元素。
1)一维数组的定义和引用
定义方式为:
类型说明符数组名[常量表达式];
其中,数组名的命名规则和变量相同。定义数组时,需要指定数组中元素的个数,即常量表达式需要明确设定,不可以包含变量。例如:
int a[10]; //定义了一个整型数组,数组名为a,有10个元素,下标0~9
数组必须先定义,然后才能使用。而且只能通过下标一个一个地访问,其表示形式为:数组名[下标]。
2)二维数组的定义和引用
定义方式为:
类型说明符数组名[常量表达式][常量表达式]
例如:
float a[3][4]; //定义3行4列的数组a,下标0~2,0~3
其实,二维数组可以看成两个一维数组。可以把a看作一个一维数组,它有3个元素:a[0],a[1],a[2],而每个元素又是一个包含4个元素的一维数组。二维数组的表示形式为:数组名[下标][下标]。
3)字符数组
用于存放字符数据(char类型)的数组是字符数组。字符数组中的一个元素存放一个字符。例如:
char c[5]; c[0]='t';c[1]= 'a';c[2]= 'b';c[3]= 'l';c[4]= 'e'; //字符数组c[5]中存放的就是字符串"table".
在C语言中,是将字符串作为字符数组来处理的。但是,在实际应用中,关于字符串的实际长度,C语言规定了一个“字符串结束标志”,以字符'\0'作为标志(实际值0x00)。即如果有一个字符串,前面n-1个字符都不是空字符(即'\0'),而第n个字符是'\0',则此字符的有效字符为n-1个。
4)动态数组
动态数组是相对于静态数组而言的。静态数组的长度在整个程序中是预先定义好的,一旦给定大小后就无法改变。而动态数组则不然,它可以随程序需要而重新指定大小。动态数组的内存空间是从堆(heap)上分配(即动态分配)的,是通过执行代码而为其分配存储空间。当程序执行到这些语句时,才为其分配。程序自己负责释放内存。
在C语言中,可以通过malloc、calloc函数进行内存空间的动态分配,从而实现数组的动态化,以满足实际需求。
5)数组如何模拟指针的效果
其实,数组名就是一个地址,一个指向这个数组元素集合的首地址。可以通过数组加位置的方式进行数组元素的引用。例如:
int a[5]; //定义了一个整型数组,数组名为a,有5个元素,下标0~4
访问到数组a的第3个元素方式有两种:方式一为a[2],方式二为*(a+2),关键是数组的名称本身就可以当作地址看待。
2. 指针
指针是C语言中广泛使用的一种数据类型,运用指针是C语言最主要的风格之一。在嵌入式编程中,指针尤为重要。利用指针变量可以表示各种数据结构,很方便地使用数组和字符串,并能像汇编语言一样处理内存地址,从而编出精练而高效的程序。但是使用指针时要特别细心、计算得当,避免指向不适当的区域。
指针是一种特殊的数据类型,在其他语言中一般没有。指针是指向变量的地址,实质上指针就是存储单元的地址。根据所指的变量类型不同,可以是整型指针(int *)、浮点型指针(float*)、字符型指针(char*)、结构指针(struct*)和联合指针(union*)。
1)指针变量的定义
其一般形式为:
类型说明符*变量名;
其中,“*”表示一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。例如:
int*p1; //表示p1是指向整型数的指针变量,p1的值是整型变量的地址
2)指针变量的赋值
指针变量与普通变量一样,使用之前不仅要进行声明,而且必须赋予具体的值。未经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址。例如:
3)指针的运算
(1)取地址运算符&。取地址运算符&是单目运算符,其结合性为自右至左,其功能是取变量的地址。
(2)取内容运算符*。取内容运算符*是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在*运算符之后跟的变量必须是指针变量。例如:
注意:取内容运算符“*”和指针变量声明中的“*”虽然符号相同,但含义不同。在指针变量声明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符,用以表示指针变量所指的变量。
(3)指针的加减算术运算。对于指向数组的指针变量,可以加/减一个整数n(由于指针变量实质是地址,给地址加/减一个非整数就错了)。设pa是指向数组a的指针变量,则pa+n、pa-n、pa++、++pa、pa--、--pa运算都是合法的。指针变量加/减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。
注意:数组指针变量前/后移动一个位置和地址加/减1在概念上是不同的。因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的。如果指针变量加1,即向后移动一个位置,就表示指针变量指向下一个数据元素的首地址,而不是在原地址基础上加1。例如:
注意:指针变量的加/减运算只能对数组指针变量进行,对指向其他类型变量的指针变量做加/减运算是毫无意义的。
4)void指针类型
顾名思义,void *为“无类型指针”,即用来定义指针变量,不指定它是指向哪种类型的数据,但可以把它强制转化为任何类型的指针。
众所周知,如果指针p1和p2的类型相同,那么可以直接在p1和p2之间互相赋值;如果p1和p2指向不同的数据类型,就必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。例如:
而void *则不同,任何类型的指针都可以直接赋值给它,无须进行强制类型转换。例如:
但这并不意味着,“void *”也可以无须强制类型转换地赋给其他类型的指针,也就是说,p2=p1这条语句编译就会出错,而必须将p1强制类型转换为“int *”类型。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。
3. 结构体
结构体是由基本数据类型构成的,并用一个标识符来命名的各种变量的组合。结构体中可以使用不同的数据类型。
1)结构体的说明和结构体变量的定义
例如,定义一个名为student的结构体变量类型:
这样,若声明s1为一个student类型的结构体变量,则使用如下语句:
又如,定义一个名为student的结构体变量类型,同时声明s1为一个student类型的结构体变量:
2)结构体变量的使用
结构体是一个新的数据类型,因此结构体变量也可以像其他类型的变量一样赋值运算,不同的是结构体变量以成员作为基本变量。
结构体成员的表示方式为:
结构体变量.成员名
如果将“结构体变量.成员名”看成一个整体,则这个整体的数据类型与结构体中该成员的数据类型相同,这样就可以像前面所讲的变量那样使用。例如:
s1.age=18; //将数据18赋给s1.age(理解为学生s1的年龄为18)
3)结构体指针
结构体指针是指向结构体的指针。它由一个加在结构体变量名前的“*”操作符来声明。例如,用上面已说明的结构体声明一个结构体指针为:
struct student *Pstudent; //声明Pstudent为一个student类型指针
使用结构体指针对结构体成员的访问,与结构体变量对结构体成员的访问在表达方式上有所不同。结构体指针对结构体成员的访问表示为:
结构体指针名->结构体成员
其中“->”是符号“-”和“>”的组合,好像一个箭头指向结构体成员。例如,要给上面定义结构体中的name和age赋值,可以用下面语句:
strcpy(Pstudent->name,"LiuYuZhang"); Pstudent->age=18;
实际上,Pstudent->name就是(*Pstudent).name的缩写形式。
需要指出的是,结构体指针是指向结构体的一个指针,即结构体中第一个成员的首地址,因此在使用之前应该对结构体指针初始化,即分配整个结构体长度的字节空间。这可用下面函数完成:
Pstudent=(struct student*)malloc(sizeof(struct student));
其中,sizeof(struct student)自动求取student结构体的字节长度,malloc()函数定义了一个大小为结构体长度的内存区域,然后将其地址作为结构体指针返回。
4. 共用体
在进行某些算法的C语言编程时,需要在几种不同类型的变量之间进行切换,可以将它们存放到同一段内存单元中,也就是使用覆盖技术,使几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中被称为“共用体”类型结构,简称共用体,其语法为:
union共用体名 { 成员表列 }变量表列;
有的文献中翻译为“联合体”,似乎不妥,中文使用“共用体”一词更为妥当。
5. 用typedef定义类型
除了可以直接使用C语言提供的标准类型名(如int、char、float、double、long等)和自己定义的结构体、指针、枚举等类型外,还可以用typedef定义新的类型名来代替已有的类型名。例如:
typedef unsigned char uint_8;
指定用uint_8代表unsigned char类型。这样下面的两个语句是等价的:
unsigned char n1;
等价于
uint_8 n1;
用法说明如下。
(1)用typedef可以定义各种类型名,但不能用来定义变量。
(2)用typedef只是对已经存在的类型增加一个类型别名,而没有创造新的类型。
(3)typedef与#define有相似之处。例如:
typedef unsigned int uint_16; #define uint_16 unsigned int;
这两句的作用都是用uint_16代表unsigned int(注意顺序)。但事实上它们又有不同,#define是在预编译时处理,只能做简单的字符串替代,而typedef是在编译时处理。
(4)当不同源文件中用到各种类型数据(尤其是像数组、指针、结构体、共用体等较复杂数据类型)时,常用typedef定义一些数据类型,并把它们单独存放在一个文件中,然后在需要用到它们时,用#include命令把该文件包含进来。
(5)使用typedef有利于程序的通用与移植,特别是用typedef定义结构体类型,在嵌入式程序中经常用到。例如:
以上声明的新类型名STU,代表一个结构体类型。可以用该新类型名来定义结构体变量。例如:
1.5.5 编译预处理
C语言提供编译预处理的功能,“编译预处理”是C编译系统中的一个重要组成部分。C语言允许在程序中使用几种特殊的命令(它们不是一般的C语言语句)。在C编译系统中对程序进行通常的编译(包括语法分析、代码生成、优化等)之前,先对程序中的这些特殊的命令进行“预处理”,然后将预处理的结果和源程序一起进行常规的编译处理,以得到目标代码。C语言提供的预处理功能主要有宏定义、条件编译和文件包含。
1. 宏定义
宏定义的一般形式为:
#define宏名表达式
其中,表达式可以是数字、字符,也可以是若干条语句。在编译时,所有引用该宏的地方,都将自动被替换成宏所代表的表达式。例如:
撤销宏定义的一般形式为:
#undef宏名
2. 条件编译
#if表达式 #else表达式 #endif
如果表达式成立,编译#if下的程序,否则编译#else下的程序,#endif为条件编译的结束标志。
条件编译通常用来调试、保留程序(但不编译),或者在需要对两种状况做不同处理时使用。
3. 文件包含
文件包含是指一个源文件将另一个源文件的全部内容包含进来,其一般形式为:
#include "文件名"