1.4 嵌入式系统常用的标准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.数据类型

C语言的数据类型有基本类型和构造类型两大类。基本类型如表1-1所示。

表1-1 C语言基本类型

注:常用的嵌入式C语言中的double类型长度为4字节。

构造类型有数组、结构、联合、枚举、指针和空类型。结构和联合是基本数据类型的组合。枚举是一个被命名为整型常量的集合。空类型字节长度为0,主要有两个用途:一是明确地表示一个函数不返回任何值;二是产生一个同一类型指针(可根据需要动态地分配给其内存)。

2.运算符

C语言的运算符分为算术、逻辑、关系和位运算及一些特殊的操作符。表1-2列出了C语言的运算符及使用方法。

表1-2 C语言的运算符及使用方法

注:增量运算符和减量运算符存在运算和取数先后次序,例如,A++是先取变量A的值再对A加1,而++A是先对变量A加1再取A的值。

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语句的一般形式如下:

          switch(表达式)
          {
          case 常数1:
          语句项1
          break;
          case常数2:
          语句项2
          break;
          …
          default:
          语句项
          }

根据case语句中所给出的常量值,按顺序对表达式的值进行测试,当常量与表达式值相等时,就执行这个常量所在的 case 后的语句块,直到碰到 break 语句,或者 switch的末尾为止。若没有一个常量与表达式值相符,则执行default后的语句块。default是可选的,如果它不存在,并且所有的常量与表达式值都不相符,那就不作任何处理。

switch语句与if语句的不同之处在于switch只能对等式进行测试,而if可以计算关系表达式或逻辑表达式。

break语句在switch语句中是可选的,如果不用break,就继续寻找下一个条件满足的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 语句使其后语句不再被执行,进行新的一次循环(可以形象地理解为返回循环开始处执行)。

4.函数

所谓函数,即子程序,也就是“语句的集合”,就是说把经常使用的语句群定义成函数,供其他程序调用,函数的编写与使用要遵循软件工程的基本规范。

使用函数要注意:函数定义时要同时声明其类型;调用函数前要先声明该函数;传给函数的参数值,其类型要与函数原定义一致;接收函数返回值的变量,其类型也要与函数类型一致等。

函数的返回值:return表达式;

return语句用来立即结束函数,并返回一确定值给调用程序。如果函数的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。

5.指针

指针是C语言中广泛使用的一种数据类型,运用指针是C语言最主要的风格之一。在嵌入式编程中,指针尤为重要。利用指针变量可以表示各种数据结构,很方便地使用数组和字符串,并能像汇编语言一样处理内存地址,从而编出精练而高效的程序。

指针是一种特殊的数据类型,在其他语言中一般没有。指针是指向变量的地址,实质上指针就是存储单元的地址。根据所指的变量类型不同,可以是整型指针(int *)、浮点型指针(float *)、字符型指针(char *)、结构指针(struct *)和联合指针(union *)。

1)指针变量的定义

其一般形式为:类型说明符 * 变量名;

其中,*表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。

例如:

          int*p1;   //表示p1是指向整型数的指针变量,p1的值是整型变量的地址

2)指针变量的赋值

指针变量同普通变量一样,使用之前不仅要进行声明,而且必须赋予具体的值。未经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址。

例如:

          int a;          //a为整型数据变量
          int*p1;        //声明p1是整型指针变量
          p1=&a;         //将a的地址作为p1初值

3)指针的运算

(1)取地址运算符&。取地址运算符&是单目运算符,其结合性为自右至左,其功能是取变量的地址。

(2)取内容运算符*。取内容运算符*是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在*运算符之后跟的变量必须是指针变量。

例如:

          int a,b;         //a,b为整型数据变量
          int*p1;        //声明p1是整型指针变量
          p1=&a;        //将a的地址作为p1初值
          a=80;
          b=*p1;        //运行结果:b=80,即为a的值

注意

取内容运算符“*”和指针变量声明中的“*”虽然符号相同,但含义不同。在指针变量声明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量。

(3)指针的加减算术运算。

对于指向数组的指针变量,可以加/减一个整数 n(由于指针变量实质是地址,给地址加/减一个非整数就错了)。设pa是指向数组a的指针变量,则pa+n,pa-n,pa++,++pa, pa--,--pa运算都是合法的。指针变量加/减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。

注意

数组指针变量前/后移动一个位置和地址加/减 1 在概念上是不同的。因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的。如指针变量加 1,即向后移动1个位置,表示指针变量指向下一个数据元素的首地址,而不是在原地址基础上加1。

例如:

          int a[5],*pa;   //声明a为整型数组(下标为0~5),pa为整型指针
          pa=a;       //pa指向数组a,也是指向a[0]。
          pa=pa+2;       //pa指向a[2],即pa的值为&pa[2]

注意

指针变量的加/减运算只能对数组指针变量进行,对指向其他类型变量的指针变量进行加/减运算是毫无意义的。

4)void指针类型

顾名思义,void *为“无类型指针”,即用来定义指针变量,不指定它是指向哪种类型数据,但可以把它强制转化成任何类型的指针。

众所周知,如果指针p1和p2的类型相同,那么可以直接在p1和p2间互相赋值;如果p1和p2指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。

例如:

          float*p1;       //声明p1为浮点型指针
          int*p2;          //声明p2为整型指针
          p1=(float*)p2;   //强制转换整型指针p2为浮点型指针值给p1赋值
          //而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
          void*p1;        //声明p1无类型指针
          int*p2;          //声明p2为整型指针
          p1=p2;          //用整型指针p2的值给p1直接赋值

但这并不意味着,void *也可以无需强制类型转换地赋给其他类型的指针,也就是说p2=p1这条语句编译就会出错,而必须将p1强制类型转换成“void *”类型。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。

6.结构体

结构体是由基本数据类型构成的,并用一个标识符来命名的各种变量的组合。结构体中可以使用不同的数据类型。

1)结构体的说明和结构体变量的定义

例1:定义一个名为student的结构体变量类型

          struct student         //定义一个名为student的结构体变量类型
          {   char name[8];     //成员变量“name”为字符型数组
          char class[10];       //成员变量“class”为字符型数组
          int age;              //成员变量“age”为整型
          };

这样,若声明s1为一个“student”类型的结构体变量,则使用如下语句:

struct student s1; //声明s1为“student”类型的结构体变量

例2:定义一个名为student的结构体变量类型,同时声明s1为一个“student”类型的结构体变量

          struct student         //定义一个名为student的结构体变量类型
          {   char name[8];     //成员变量“name”为字符型数组
          char class[10];       //成员变量“class”为字符型数组
          int age;              //成员变量“age”为整型
          }s1;                 //声明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()函数定义了一个大小为结构体长度的内存区域,然后将其地址作为结构体指针返回。

7.位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进制位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域(Bit Field)”,也有人叫做“位段”。所谓“位域”,实际上是字节中一些位的组合,可以认为它是“位信息组”。位域将一个字节中的二进制位划分为几个不同的区域,并给每个区域起个域名,允许在程序中按域名进行操作。

1)位域的定义

例2:定义一个名为bs的位域变量类型,同时声明b1为bs类型的变量。

              struct bs
              {
                  unsigned int  a:2;   //第0~1位
                  unsigned int  b:6;   //第2~7位
              }b1;

注意

定义位域必须使用“unsigned int”。上例声明b1为bs变量,共占8位(1字节)。其中位域a占2位(a的范围为0~3),位域b占6位(b的范围为0~63)。对于位域的定义尚有以下几点说明。

(1)一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

          struct bs
              {
                  unsigned int a:4
                  unsigned int:0    //空域(本字节剩余位不用)
                  unsigned int b:4   //从下一单元开始存放
                  unsigned int c:4
                };

在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

(2)由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进制位。

(3)位域可以无位域名,这时它只用来作为填充或调整位置。无名的位域是不能使用的。例如:

          struct k
              {
                  unsigned int a:1      //第0位
                  unsigned int:2       //无域名,2位不能使用
                  unsigned int b:3      //第3~5位
                  unsigned int c:2      //第6~7位
              }b1;

从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进制位分配的。

2)位域的使用

位域的使用和结构成员的使用相同,其一般形式为:

          位域变量名.位域名位

例如在上面定义的位域b1可以这样调用:

          b1.a=1;    //将b1的第0位置1(注意一个字节从最低位开始)
          b1.b=7;    //将b1的第3~5位置111

通过位域定义位变量,是实现单个位操作的重要途径和方法,采用位域定义位变量,产生的代码紧凑、高效。

8.编译预处理

C语言提供编译预处理的功能,“编译预处理”是C编译系统的一个重要组成部分。C语言允许在程序中使用几种特殊的命令(它们不是一般的C语句)。在C编译系统对程序进行通常的编译(包括语法分析、代码生成、优化等)之前,先对程序中的这些特殊的命令进行“预处理”,然后将预处理的结果和源程序一起再进行常规的编译处理,以得到目标代码。C提供的预处理功能主要有宏定义、条件编译和文件包含。

1)宏定义

          #define 宏名 表达式

表达式可以是数字、字符,也可以是若干条语句。在编译时,所有引用该宏的地方,都将自动被替换成宏所代表的表达式。例如:

          #define  PI  3.1415926     //以后程序中用到数字3.1415926就可写为PI
          #define  S(r)  PI*r*r      //以后程序中用到PI*r*r就写S(r)

2)条件编译

          #if  表达式
          #else 表达式
          #endif

如果表达式成立,则编译#if下的程序,否则编译#else下的程序,#endif为条件编译的结束标志。

          #ifdef  宏名           //如果宏名称被定义过,则编译以下程序
          #ifndef  宏名          //如果宏名称未被定义过,则编译以下程序

条件编译通常用来调试、保留程序(但不编译),或者在需要对两种状况做不同处理时使用。

3)“文件包含”处理

所谓“文件包含”是指一个源文件将另一个源文件的全部内容包含进来,其一般形式为:

          #include  “文件名”

9.用typedef定义类型

除了可以直接使用C提供的标准类型名(如int、char、float、double、long等)和自己定义的结构体、指针、枚举等类型外,还可以用typedef定义新的类型名来代替已有的类型名。例如:

          typedef unsigned char  INT8U;

指定用INT8U代表unsigned char类型。这样下面的两个语句是等价的:

          unsigned char i;         等价于         INT8U  i;

用法说明:

(1)用typedef可以定义各种类型名,但不能用来定义变量。

(2)用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。

(3)typedef与#define有相似之处,如:

          typedef  unsigned int  INT16U;
          #define  INT16U  unsigned int;

这两句的作用都是用INT16U代表unsigned int。但事实上它们二者不同,#define是在预编译时处理,它只能做简单的字符串替代,而typedef是在编译时处理。

(4)当不同源文件中用到各种类型数据(尤其是像数组、指针、结构体、共用体等较复杂数据类型)时,常用typedef定义一些数据类型,并把它们单独存放在一个文件中,而后在需要用到它们的文件中用#include命令把它们包含进来。

(5)使用typedef有利于程序的通用与移植。