免费智能真题库 > 历年试卷 > 嵌入式系统设计师 > 2021年上半年 嵌入式系统设计师 上午试卷 综合知识
  第21题      
  知识点:   C程序设计基础   C++
  关键词:   编译   源程序        章/节:   嵌入式系统程序设计       

 
以编译方式翻译C/C++源程序的过程中,类型检查在() 阶段处理。
 
 
  A.  词法分析
 
  B.  语义分析
 
  C.  语法分析
 
  D.  目标代码生成
 
 
 

  相关试题:嵌入式系统程序设计          更多>  
 
  第48题    2023年上半年  
   0%
近年来具有健壮分区功能的嵌入式实时操作系统(例如VxWorks653)在嵌入式领域得到了广泛应用,以下不属于分区操作系统特点的是(..
  第19题    2019年下半年  
   52%
将编译器的工作过程划分为词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成时,语法分析阶段的输入是(18)。..
  第27题    2014年下半年  
   46%
任务调度是嵌入式操作系统的一个重要功能,嵌入式操作系统内核一般分为非抢占式和抢占式两种,以下叙述中,不正确的是(27)。
   知识点讲解    
   · C程序设计基础    · C++
 
       C程序设计基础
        C语言是面向过程的结构化程序设计语言,由于它兼具高级语言和低级语言的特点,因此是创建嵌入式系统时最普遍使用的语言,由于C语言不是专门为嵌入式系统应用而设计的,因此国标GB/T 28169-2011作为应用C语言进行嵌入式软件开发的编码规范。
                      C程序基础
                      C程序是由函数组成的,其基本要素有预处理指令、常量、变量、宏、运算符和表达式、流程控制和语句等。
                             预处理指令
                             在C程序中以#开头的行被称为预处理指令,这些指令是ANSI C统一规定的。编程时可使用预处理命令来扩展C语言的表示能力,提高编程效率。对C源程序进行编译之前,首先由预处理器对程序中的预处理指令进行处理。
                             文件包含#include、宏定义#define、条件编译#ifdef、#ifndef是常用的预处理命令,在头文件assert.h中定义宏assert(断言),用于测试表达式的值,若表达式的值为0,则显示错误信息并终止程序的运行。
                                    宏定义
                                    在C程序中用好宏定义可以提高程序的可移植性、可读性,减少出错。对于嵌入式系统而言,为了达到性能要求,也常用宏作为一种代替函数的方法。例如,用宏求解两个数据对象的较小者。
                                    
                                    再如,用#define声明一个常数,忽略闰年情况下表示一年有多少秒。
                                    
                                    通过使用预定义宏可以返回程序的某些状态,以方便交叉编译和调试,每个预定义宏的名称一两个下画线字符开头和结尾,这些预定义宏不能被取消定义(#undef)或由编程人员重新定义。例如,用_FUNC_打印函数名、_LINE_打印行号,以定位程序中打印该信息的函数和位置。常用的几个预定义宏如下。
                                    _DATE_当前源文件的编译日期,格式为“Mmm dd yyyy”的字符串字面量
                                    _TIME_当前源文件的编译时间,格式为“hh:mm:ss”的字符串字面量
                                    _FILE_当前源文件名称,含路径信息
                                    _FUNC_当前函数名称
                                    _LINE_当前程序行的行号,表示为十进制整型常量
                                    _STDC_若当前编译器符合ISO标准,那么该宏的值为1,否则未定义
                                    _STDC_HOSTED_(C99)如果当前是宿主系统,则该宏的值为1,否则为0
                                    条件编译
                                    编写嵌入式应用程序时经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句,这时就需要使用条件编译。条件编译命令最常见的形式为:
                                    
                                    其作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2,其中#else部分也可以没有。
                                    在所有的预处理指令中,#pragma最为复杂,其作用是设定编译器的状态或者是指示编译器完成一些特定的动作,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。#pragma pack有多种形式,#pragma pack(n)表示变量以n字节对齐。
                                    在网络程序中采用#pragma pack(1)(即变量紧缩),可以减少网络流量以及兼容各种系统,避免由于系统对齐方式不同而导致的解包错误。
                             基本数据类型
                             在C程序中,数据都具有类型,通过数据类型定义了数值范围以及可进行的运算。
                             C的数据类型可分为基本数据类型(内置的类型)和复合数据类型(用户定义的类型)。内置的类型是指C语言直接规定的类型,用户定义的类型在使用以前必须先定义,枚举、结构体和共用体类型都是用户定义类型。
                             C的基本数据类型有字符型(char)、整型(int)、浮点型(float、double),如下表所示。
                             
                             C基本数据类型
                             void类型也是一种基本类型,void不对应具体的值,只用于一些特定的场合,例如用于定义函数的参数类型、返回值、函数中指针类型等进行声明,表示没有或暂未确定类型。
                             C程序中的数据以变量、常量(包括字面量和const常量等)表示,它们都具有类型属性。
                                    变量
                                    变量本质上指代存储数据的内存单元,变量的定义(definition)指示编译器为变量分配存储空间,还可以为变量指定初始值。在一个C程序中,一个变量有且仅有一个定义。当C程序文件中需要引用其他程序文件中定义的变量时,就需要进行声明。
                                    变量声明(declaration)用来表明变量的类型和名字,当定义变量时即声明了它的类型和名字。可以通过使用extern关键字声明变量名。
                                    例如,下面是对变量a的定义、b的声明。
                                    
                                    在嵌入式C程序设计中,用volatile修饰变量时,即告知编译器该变量的值无任何持久性,不要对它进行任何优化。因为用volatile定义的变量可能会在其所在程序外被改变,因此需要从其所在的内存位置或设备端口重新读取,而不是使用其寄存器中的缓存值。
                                    字面量
                                    字面量(literal)是指数据在源程序中直接以值的形式呈现,在程序运行中不能被修改,表现为整型、浮点型和字符串类型。
                                    默认情况下,整型字面量以十进制形式表示,前缀0表示是八进制常数,前缀0x或0X表示是十六进制常数。同样,一个整型常数也可以加U或u后缀,指定为是unsigned类型。
                                    以0作为八进制常数的前导符号并不符合人们的习惯,可能造成潜在的程序错误。
                                    例如,
                                    
                                    浮点型字面量总是假定为double型,除非有字母F或f后缀,才被认为是float型;若有后缀L或1,则被处理为long double型。实型常量也可以表示成指数形式,例如0.004可以表示成4.0E-3或4.0e-3,其中E或e代表指数。
                                    字符字面量用一对单引号括起来,例如‘A’。对于不能打印的特殊字符,可以用它们的编码指定。还有一些转义字符,如’\n'表示换行、’\r'表示回车等。
                                    用双引号括起来的零个或多个字符则构成字符串型字面值。例如,
                                    
                                    const常量和宏定义常量
                                    常量修饰符const的含义是其所修饰的对象为常量(immutable)。若一个变量被修饰为const,则该变量的值就不能被其他语句修改。例如:
                                    
                                    C程序中常用宏定义的方式在源程序中为常量命名。例如:
                                    
                                    const常量与宏定义常量有所不同:const常量有数据类型,而宏定义常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,不进行类型安全检查,并且在字符替换可能会产生意料不到的错误。有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中使用const常量。
                                    标识符和名字的作用域
                                    在C程序中使用的变量名、函数名、标号以及用户定义数据类型名等统称为标识符。除库函数的函数名由系统定义外,其余都由用户自定义。
                                    C语言的标识符一般应遵循如下的命名规则:
                                    .标识符必须以字母a~z、A~Z或下画线开头,后面可跟任意个字符,这些字符可以是字母、下画线和数字,其他字符不允许出现在标识符中;
                                    .标识符区分大小写字母;
                                    .标识符的长度在C89标准中规定31个字符以内,在C99标准中规定63个字符以内;
                                    .C语言中的关键字(保留字)有特殊意义,不能作为标识符;
                                    .标识符最好使用具有一定意义的字符串,便于记忆和理解。变量名一般用小写字母,用户自定义类型名的开头字母大写。
                                    通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。同一个名字在不同的作用域可能表示不同的对象。
                                    C程序中的名字有块作用域、函数作用域、函数原型作用域和文件作用域之分,作用域可以是嵌套的。
                                    一般情况下,尽可能将变量定义(声明)在最小的作用域内,并且为其设置初始值。
                             数组、字符数组与字符串
                                    数组
                                    数组是一种集合数据类型,它由多个元素组成,每个元素都有相同的数据类型,占有相同大小的存储单元,且在内存中连续存放。每个数组有一个名字,数组中的每个元素有一个序号(称为下标),表示元素在数组中的位序(位置),数组的维数和大小在定义数组时确定,程序运行时不能改变。
                                    一维数组的定义形式为:
                                    
                                    其中,“类型说明符”指定数组元素的类型;“数组名”的命名规则与变量相同;“常量表达式”的值表示数组元素的个数,必须是一个正整数。例如:
                                    
                                    在C程序中,数组元素的下标总是从0开始的,如果一个数组有n个元素,则第一个元素的下标是0,最后一个元素的下标是n-1。例如,在上面定义的temp数组中,第一个元素是temp[0],第二个元素是temp[1],以此类推,最后一个元素是temp[99]。访问数组元素的方法是通过数组名及数组名后的方括号中的下标。例如:
                                    
                                    程序员需确保访问数组元素时下标的有效性,访问一个不存在的数组元素(例如temp[100]),可能会导致严重的错误。
                                    定义数组时就给出数组元素的初值,称之为初始化,数组的初始化与简单变量的初始化类似。初值放在一对花括号中,各初值之间用逗号隔开,称为初始化表。例如:
                                    
                                    对于没有给出数组元素个数而给出了初始化表的数组定义,编译器会根据初值的个数和类型,为数组分配相应大小的内存空间。初始化表中值的个数必须小于或等于数组元素的个数。
                                    对于“int primes[10]={1,2,3,5,7};”,前5个数组元素的初值分别为1,2,3,5,7,后5个元素的初值都为0。
                                    二维数组可视为是一个矩阵,定义形式为:
                                    
                                    其中,“类型说明符”指定数组元素的类型,“常量表达式1”指定行数,“常量表达式2”指定列数。例如,可以定义一个二维数组:
                                    
                                    这个数组在内存中占用能存放12个double型数据且地址连续的存储单元。
                                    C语言中二维数组在内存中按行顺序存放。
                                    可以用sizeof计算数组空间的大小,即字节数。例如,
                                    
                                    二维数组可以看作元素是一维数组的一维数组,三维数组可看作元素是二维数组的一维数组,以此类推。
                                    字符数组与字符串
                                    当数组中的元素由字符组成时,便称为字符数组。
                                    字符串是一个连续的字符系列,用特殊字符’\0’结尾。字符串常用字符数组来表示。数组的每一个元素保存字符串的一个字符,并附加一个空字符,表示为“\0”,添加在字符串的末尾,以标识字符串结束。如果一个字符串有n个字符,则至少需要长度为n+1的字符数组来保存它。
                                    一个字符串常量用一对双引号括起来,如Welcome,编译系统自动在每一字符串常量的结尾增加’\0’结尾符。字符串可以由任意字符组成,一个长字符串可以占两行或多行,但在最后一行之前的各行需用反斜杠结尾,如A String Can be write on multilines可等价地表示为:
                                    
                                    需要注意的是,"A"与'A’是不同的,"A"是由两个字符(字符'A'与字符’\0’)组成的字符串,而后者只有一个字符。最短的字符串是空字符串"",它仅包含一个结尾符‘\0'。
                             枚举类型
                             枚举就是把一种类型数据可取的值逐一列举出来。枚举类型是一种用户定义的数据类型,其一般定义形式为:
                             
                             其中,“枚举类型名”右边花括号中的内容称为枚举表,枚举表中的每一项称为枚举成员,枚举成员是常量。枚举成员之间用逗号隔开,方括号中的“整型常数”是枚举成员的初值。
                             如果没有为枚举成员赋初值,即省掉了标识符后的“=整型常数”时,编译系统为每一个枚举成员赋予一个不同的整型值,第一个成员为0,第二个成员为1,以此类推。当枚举类型中的某个成员赋值后,其后的成员则按依次加1的规则确定其值。例如:
                             
                             此时,eBLUE=6、Eyellow=7、Eburgundy=41。
                             结构体、位域和共用体
                             结构体、位域和共用体类型在程序中需要用户进行定义,同时用typedef定义数据类型的别名。
                                    结构体
                                    利用结构体类型可以把一个数据元素的各个不同的数据项聚合为一个整体。结构体类型的声明格式为:
                                    
                                    例如,一个复数z=x+yi包含了实部x和虚部y两部分(x和y为实数),可以定义一个表示复数的结构体类型,并用typedef为结构体类型命名为Complex:
                                    
                                    在该定义中,Complex是这个结构体类型的名字,re和im是结构的成员。一般情况下,对结构体变量的运算必须通过对其成员进行运算来完成,成员运算符“.”用来访问结构体变量的成员,方式为:
                                    
                                    例如,定义结构体变量z,将-4和5分别赋值给一个复数z的实部成员变量和虚部成员变量:
                                    
                                    z.re和z.im相当于普通的double型变量。结构体外的变量名和结构体中的成员名相同时不会发生冲突。一个结构体变量的存储空间长度不少于其所有成员所占空间长度之和。
                                    结构体数据的空间中可能产生填充信息,因为对大多数处理器而言,访问按字或者半字对齐的数据速度更快,当定义结构体时,编译器为了性能优化,可能会将它们按照半字或字对齐。
                                    例如,下面两个结构体变量structA和structB的成员相同但排列顺序不同,用sizeof计算其所占用存储空间的字节数,sizeof(structA)的值为8,sizeof(structB)的值为12。其存储空间中的填充处理如下图所示。
                                    
                                    结构体变量的存储空间
                                    位域
                                    有些信息在存储时只需要一个或几个二进制位,而不是完整的字节空间,这时可通过位域的方式来处理,即将一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。
                                    位域的定义格式如下:
                                    
                                    其中,位域列表的形式为:类型说明符位域名:位域长度。
                                    例如,定义了下面的位域结构变量bit后,可以为其位域赋值:
                                    
                                    共用体
                                    共用体类型的声明格式为:
                                    
                                    例如,定义共用体类型DATA及其变量a。
                                    
                                    不能直接引用联合类型的变量,只能引用其成员。用“.”运算符引用共用体变量的成员,引用方式为:
                                    
                                    例如,a.i,a.ch,a.f
                                    一个共用体变量的存储空间的大小等于其占用空间最大的成员的大小,所有成员变量占用同一段内存空间,如下图所示。
                                    
                                    共用体变量a的存储空间
                             运算符与表达式
                             C语言提供了丰富的运算符,包括算术运算符、关系运算符、逻辑运算符、位运算符、条件运算符、赋值运算符、逗号运算符及其他运算符。根据运算符需要的操作数个数,可分为单目运算符(一个操作数)、双目运算符(两个操作数)和三目运算符(三个操作数)。
                             表达式总是由运算符和操作数组成,它规定了数据对象的运算过程。
                                    自增(++)与自减(--)
                                    运算符的作用是将数值变量的值增加1或减少1。自增或自减运算符只能作用于变量而不能作用于常量或表达式。
                                    ++value称为前缀方式,value++称为后缀方式,其区别是:前缀方式先将变量的值增1,然后取变量的新值参与表达式的运算;后缀方式是先取变量的值参与表达式的运算,然后再将变量的值增加1。自减运算同理。
                                    关系运算符
                                    关系运算符用于数值之间的比较,包含等于(==)、不等于(!=)、小于(<)、小于或等于(<=)、大于(>)、大于或等于(>=)这6种,结果的值为1(表示关系成立)或为0(表示关系不成立)。
                                    不能用关系运算符对字符串进行比较,因为被比较的不是字符串的内容本身,而是字符串的地址。例如,“HELLO”<“BYE”是用“HELLO”的地址与“BYE”的地址来比较大小,这没有意义。
                                    逻辑运算符
                                    逻辑与(&&)、逻辑或(||)、逻辑非(!)的运算结果为1(表示true)或为0(表示false)。“逻辑非”是单目运算符,它将操作数的逻辑值取反。“逻辑与”是双目运算符号,其含义是“当且仅当两个操作数的值都为true时,逻辑与运算的结果为true”。“逻辑或”的含义是“当且仅当两个操作数的值都为false时,逻辑或运算的结果为false”。
                                    例如,逻辑表达式!20的结果是0,10&&5的结果是1,10||5.5的结果是1,10&&0的结果是0。
                                    在C程序中,由逻辑运算符“&&”“||”构造的表达式采用短路计算方式求值,即对于“a&&b”,a为1(假)时不需要再计算b的值就可以确定该表达式的结果为0(假),对于“a||b”,a为1(真)时不需要再计算b的值就可以确定该表达式的结果为1(真)。
                                    例如,对于逻辑表达式((year%4==0)&&(year%100!=0)||(year%400==0)),若year%4的结果不是0(例如year的值为2001),就不需要再计算year%100的值了,因为此时“&&”运算的结果已经确定为0(即条件不成立)。
                                    赋值运算与组合赋值
                                    赋值运算符(=)的作用是将一个表达式的值赋给一个变量,可进行组合赋值。例如:
                                    
                                    书写组合表达式时,可能存在的潜在错误是书写错误,例如将“+=”写成了“=+”,这类错误在编译阶段无法识别,只能在程序的运行结果不符合预期时再进行排查。
                                    C程序中,常出现将比较相等的运算符号“==”误用为“=”(赋值运算符)的情况,编程时需要特别注意。
                                    条件运算符和逗号运算符
                                    (1)条件运算符是C中唯一的三目运算符,也称为三元运算符,它有三个操作数:
                                    
                                    (2)多个表达式可以用逗号组合成一个表达式,即逗号表达式。逗号运算符带两个操作数,结果是右操作数。逗号表达式的一般形式是:表达式1,表达式2,……,表达式n,它的值是表达式n的值。逗号运算符的用途仅在于解决只能出现一个表达式的地方却要出现多个表达式的问题。
                                    位运算符
                                    位运算符要求操作数是整型数,并按二进制位的顺序来处理它们。C/C++提供6种位运算符,如下表所示,为简化起见,设整数字长(word)为16位。
                                    
                                    C的位运算
                                    赋值运算符也可与位运算符组合,产生&=、|=、^=、<<=、>>=等组合运算符。
                                    例如,用宏定义通过位运算得到一个字的高位和低位字节。
                                    
                                    sizeof
                                    sizeof用于计算表达式或数据类型的字节数,其运算结果与系统相关。例如,对于下面的数组定义,可用“sizeof(a)/sizeof(int)”计算出数组a的元素个数为7。
                                    
                                    类型转换
                                    在混合数据类型的运算过程中,系统自动进行类型转换。例如,一个int型操作数和一个long型操作数进行运算时,将int类型数据转换为long类型后再运算,结果为long型;一个float型操作数和一个double型操作数的运算结果是double型。这称为类型提升。
                                    在程序中也可以进行数据类型的强制转换(显式类型转换),一般形式为:
                                    
                                    需要注意,(int)(x+y)是将(x+y)转换为int型,而(int)x+y是将x转换为int型后再与y相加。对变量进行显式类型转换只是得到一个所需类型的中间变量,原来变量的类型并不发生变化。
                                    当不得已混合使用类型时,一个比较好的习惯是使用强制类型转换。强制类型转换可以避免编译器隐式转换带来的错误,同时也给维护人员传递一些有用信息。
                             输入/输出
                             C程序中输入/输出操作都由输入/输出标准库函数(在头文件stdio.h中声明)完成,常见的有格式化输出函数printf和格式化输入函数scanf,以及文件操作函数fopen、fprintf和fscanf等。
                             语句
                             语句是构成程序的一种基本单位,用来描述数据定义或声明、运算和控制过程。下面主要介绍描述基本流程控制的分支(选择)、循环结构等语句,包括if、switch、for、while、do-while、break、continue、return等。
                                    选择语句
                                    表示分支(选择)结构的语句有if语句和switch语句。
                                    (1)if语句。if语句用于表达根据一定的条件在两条流程中选择一条执行的情况。if语句的一般形式为:
                                    
                                    其含义是当给定的条件p满足(即表达式p的值不为0)时,执行语句1,否则执行语句2。语句1和语句2中必须且仅能执行其中的一条。在if语句的简单形式中,可以省略else及其子句“语句2”。良好的C编程风格提倡将语句1和语句2用“{”“}”括起来。
                                    if语句能够嵌套使用,即一个if语句能够出现在另一个if语句里。使用if语句的嵌套形式需要注意else的配对情况,C规定:else子句总是与离它最近且没有else相匹配的if语句配对。
                                    例如,下面语句(a)、(b)中,else与if的匹配不同。
                                    
                                    在语句(a)中,else与if(x<5)匹配,该语句的含义是:当x大于0且小于5时,执行y=x+1;,若x大于或等于5,则执行y=x-1;。
                                    在语句(b)中,else与if(x>0)匹配,该语句含义是:当x大于0且小于5时,执行y=x+1;,若x小于或等于0,则执行y=x-1;。
                                    (2)switch语句。switch语句用于表示从多分支的执行流程中选择一个来执行的情况。
                                    switch语句的一般形式如下:
                                    
                                    switch语句的执行过程可以理解为:首先计算表达式p的值,然后自上而下地将其结果值依次与每一个常量表达式的值进行匹配(常量表达式的值的类型必须与“表达式”的类型相同)。如果匹配成功,则执行该常量表达式后的语句系列。当遇到break时,则立即结束switch语句,否则顺序执行到switch中的最后一条语句。default是可选的,如果没有常量表达式的值与“表达式”的值匹配,则执行default后的语句系列。需要注意的是,表达式p的值必须是字符型或整型。
                                    编译时通常根据switch语句中各case后面的常量表达式来构造一个跳转表,从而在确定表达式p的值之后可以快速定位到相应的语句位置开始执行,而不是逐一与各常量表达式的值进行比较。
                                    循环语句
                                    C提供的循环语句有while、do-while和for,循环体部分应使用语句块符号(即大括号)括起来。
                                    (1)while语句。while语句的一般形式为:
                                    
                                    while语句的含义是首先计算表达式p(称之为循环条件)的值,如果其值不为0(即为真),则执行“循环体语句”(称为循环体)。这个过程重复进行,直至“表达式”的值为0(假)时结束循环。
                                    (2)do-while语句。do-while语句的一般形式为:
                                    
                                    do-while语句的含义是先执行循环体语句,再计算表达式p,如果表达式p的值不为0,则继续执行循环体语句,否则循环终止。
                                    (3)for语句。for语句的一般形式为:
                                    
                                    for语句的含义是:
                                    ①计算表达式1(循环初值)。
                                    ②计算表达式2(循环条件),如果其结果不为0,则执行循环体语句(循环体),否则循环终止。
                                    ③计算表达式3(循环增量)。
                                    ④重复②和③。
                                    for语句在形式上比实现相同控制逻辑的while语句更为简洁和紧凑。
                                    break、continue、return
                                    break语句用在switch语句中时,用于跳出switch语句,结束switch语句的执行。
                                    break语句在循环体中时,其作用是终止循环并结束循环语句的执行。在多重(层)循环控制中,break的作用只限于终止(并跳出)一重(层)循环控制结构,其作用不能到达更外层的循环控制。
                                    return语句仅用于从函数返回。
                      函数
                      在编写C程序时,一般都会把一个代码行数多的大程序分为若干个子程序,函数就是C程序中的子程序。因此,函数是一个功能模块,用来完成特定的任务。
                      标准库函数是已经定义并随着编译系统发布的、可供用户调用的函数,例如printf、scanf等;用户自定义函数是根据需要来定义的函数。
                             函数定义
                             函数定义的一般形式如下:
                             
                             函数调用时有可能传递了错误参数,外界的强干扰可能将传递的参数修改掉,因此在执行函数主体前,需要先确定传进来的参数是否合法。
                             函数调用
                             函数调用的格式为:
                             
                             函数调用由函数名和函数调用运算符“(,)”组成,“(,)”内有0个或多个逗号分隔的参数(称为实参)。每个实参是一个变量或表达式,且实参的个数与类型要与被调用函数定义时的参数(称为形参)个数和类型匹配。当被调函数执行时,首先计算实参表达式,并将结果值传送给形参,然后执行函数体,返回值被传送到调用函数。如果函数调用后有返回值,函数调用可以用在表达式中,而无返回值的函数调用常常作为一个单独的语句使用。调用一个函数之前必须对被调用函数进行声明。
                             C程序中的参数传递方式为值传递(地址也是一种值)。函数在被调用以前,形参变量并不占内存单元,当函数被调用时,才为形参变量分配存储单元,并将相应的实参变量的值复制到形参变量单元中。所以,被调用函数在执行过程修改形参变量的值并不影响实参变量的值。
                             当数组作为函数参数时,调用函数中的实参数组只是传送该数组在内存中的首地址,即调用函数通知被调函数在内存中的什么地方找到该数组。数组参数并不指定数组元素的个数,除传送数组名外,调用函数还必须将数组的元素个数通知给被调用函数。所以,有数组参数的函数原型的一般形式为:
                             
                             函数参数的引用传递不同于值传递。值传递是把实参的值复制到形参,实参和形参占用不同的存储单元,形参若改变值,不会影响到实参。而引用传递本质上是将实参的地址传递给形参。以数组作为函数参数传递时,是引用传递方式,即把实参数组在内存中的首地址传给了形参,避免了复制每一个数组元素,从而可以节省内存空间和运行时间。在被调用函数中,如果改变了形参数组中元素的值,那么在调用函数中,实参数组对应元素的值也会发生相应的改变。
                             函数声明
                             如果一个函数调用另一个函数,在调用函数中必须对被调用函数进行声明。函数声明的一般形式如下:
                             
                             C程序中,函数原型用于声明函数,下面是函数声明的例子:
                             
                             可以将一些函数的声明集中放在头文件中,然后再用“#include”将头文件包含在程序文件中,也可以放在程序文件的开头,而把函数的定义放在程序文件后面的某个地方。C程序是从main函数开始执行的,而main函数在程序文件中的位置并没有特别的要求。
                             递归函数
                             递归函数是指函数直接调用自己或通过一系列调用语句间接调用自己,是一种描述问题和解决问题的常用方法。
                             递归过程的特点是“先逐步深入,然后再逐步返回”,它有两个基本要素:边界条件和递归模式,边界条件确定递归何时终止,也称为递归出口;递归模式表示大问题是如何分解为小问题的,也称为递归体。
                      存储管理
                      C程序中经常需要使用各种变量,如全局变量、静态变量、局部变量,编程时需要了解这些变量的作用域及其所占用的存储位置及存储空间大小,包括静态存储和动态存储的概念。若程序中的一个变量在运行时总是不正常地被改变,那么有理由怀疑它临近的数据存在溢出情况,从而改变了这个变量值。要进行跟踪排查,就必须知道该变量被分配的位置及其附近的其他变量。
                      程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是通常所说的全局变量。
                      例如,在下面的程序段中,有全局变量degree、cnt和局部变量times、price。
                      
                             内存布局
                             一个C程序在不同的系统中运行时,虽然对其代码和数据所占用的内存空间会有不同的布局和安排,但是一般都包括正文段(包含代码和只读数据)、数据区、堆和栈等。例如,在Linux系统中进程的内存布局示意图如下图所示。
                             
                             程序的内存映像示意图
                             (1)正文段中主要包括由CPU执行的机器指令,该存储区是只读区域,以防止程序由于意外事件而修改,该段也是可共享的,因此经常执行的程序在存储器中只需要有一个副本。
                             (2)数据区(段)分为初始化部分和未初始化部分,在程序中已初始化的全局变量和静态局部变量的存储单元在该区域。还有程序中未初始化的全局数据所占存储区域,常称为BSS段(来源于早期汇编程序的一个操作,即Block Started by Symbol),在程序开始执行之前,内核将此段初始化为0。
                             (3)栈是局部变量以及每次函数调用时所需保存的信息的存储区域,其空间的分配和释放由操作系统进行管理。每次函数调用时,其返回地址以及调用者的环境信息(例如某些寄存器)都存放在栈中。然后,在栈中为新被调用的函数的自动和临时变量分配存储空间。栈空间向低地址方向增长。
                             (4)堆是一块动态存储区域,由程序员堆分配和释放,若程序员不释放,则程序结束时由操作系统回收。堆空间地址的增长方向是从低地址向高地址。在C程序中,通过调用标准库函数malloc/calloc/realloc等向系统动态地申请堆存储空间来存储相应规模的数据,之后用free函数释放所申请到的存储空间。
                             当程序使用这些函数去获得新的内存空间时,系统首先在堆上进行内存空间的分配,操作系统一般需要维护一个记录空闲内存地址的链表。当系统收到程序的申请时,会遍历该链表,寻找适用于所申请空间大小的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给用户程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free操作才能正确地释放本段内存空间。由于找到的堆结点大小不一定正好等于申请空间的大小,因此涉及到复杂的分配机制,需要进行系统调用,可能产生内存碎片以及用户态与核心态的转换等一系列问题。
                             对于内存受限的系统,应尽量避免使用动态内存分配,多采用静态内存分配,从而在程序编译时就能确定其运行时所需要的存储空间。
                             大端模式和小端模式
                             在计算机系统中是以字节为单位存储信息的,每个地址单元都对应着一个字节(8bit)。但是在C程序中除了8bit的char型数据外,还有16bit的short型、32bit的int型及long型(要看具体的编译器)。另外,对于16位或者32位的处理器,由于寄存器宽度为多个字节,那么必然存在着如何将多个字节安排的问题。因此就导致了大端(Big-endian)存储模式和小端(Little-endian)存储模式。
                             大端模式就是高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
                             小端模式就是低位字节存储在内存的低地址端,高位字节存储在内存的高地址端。
                             常用CPU中的PowerPC、IBM、Sun、KEIL C51采用大端模式,X86、DEC采用小端模式,很多ARM、DSP为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
                             一般操作系统是小端模式,而通信协议是大端模式。另外,Java和所有的网络通信协议都是使用大端模式的编码。
                             例如,对于一个32bit的十六进制整数0x12345678,在Little-endian模式以及Big-endian模式内存中的存储方式(假设从地址0x4000开始存放)如下表所示。
                             
                             大端模式和小端模式存储示例
                      指针
                      简单来说,指针是内存单元的地址,它可能是变量的地址、数组空间的地址,或者是函数的入口地址。存储地址的变量称为指针变量,简称为指针。指针是C语言中最有力的武器,能够为程序员提供极大的编程灵活性。
                             指针的定义
                             指针类型的变量是用来存放内存地址的,下面是两个指针变量的定义:
                             
                             变量ptr1和ptr2都是指针类型的变量,ptr1用于保存一个整型变量的地址(称ptr1指向一个整型变量),ptr2用于保存一个字符型变量的地址(称ptr2指向一个字符变量)。
                             使用指针时需明确两个概念:指针对象和指针指向的对象。指针对象是明确命名的指针变量,如上例中的ptr1、ptr2;指针指向的对象是另一个变量,用“*”和指针变量联合表示,如上例中的整型变量*ptr1和字符变量*ptr2,由于上面的定义中未对ptr1和ptr2进行初始化,它们的初始值是随机的,也就是*ptr1和*ptr2可视为并不存在。
                             借助指针变量可以针对指定的地址进行操作,例如,设置地址为0x1234开始的存储空间存放一个整型变量的值0x5678,代码如下。
                             
                             定义指针变量时需要在每个变量名前加“*”,如下:
                             
                                    指针的加减运算
                                    对指针变量进行加减运算时,是以指针所指向的数据类型存储宽度为单位计算的。
                                    例如,下面的指针p和s在进行加1运算时有不同的结果。
                                    
                                    p+1实际上是按照公式p+1*sizeof(int)来计算的,s+1则是按照s+1*sizeof(char)进行计算。
                                    空指针
                                    标准预处理宏NULL(它的值为0,称为空指针常量)常用来表示指针不指向任何内存单元,可以把NULL赋给任意类型的指针变量,以初始化指针变量。例如:
                                    
                                    需要注意:全局指针变量会被自动初始化为NULL,局部指针变量的初始值是随机的。编程时常见的一个错误是没有给指针变量赋初值。未初始化的指针变量可能表示了一个非法的地址,导致程序运行时出现内存访问错误,从而使程序异常终止。
                                    “&”和“*”
                                    “&”称为地址运算符,其作用是获取变量的地址。“*”称为间接运算符,其作用是获取指针所指向的变量。
                                    例如,下面的语句“pa=&b;”执行后,变量pa就得到了b的地址(称为指针pa指向b),*pa表示pa指向的变量(也就是变量b)。
                                    例如:
                                    
                                    在上面的例子中,通过指针pa修改了变量b的值,本质上是对b的间接访问。在程序中通过指针访问数据对象或函数对象,提供了运算处理上的灵活性。
                                    如果指针变量的值是空指针或者是随机的,通过指针来访问数据就是一种错误(在编译时报错,或者在运行时发生异常),下面的语句会产生一个运行时错误(vp可能表示了一个非法的地址,因此它所指向的对象*vp也是非法的)。
                                    
                                    void*类型可以与任意的数据类型匹配。void指针在被使用之前,必须转换为明确的类型。例如:
                                    
                                    指针与堆内存
                                    在程序运行过程中,堆内存能够被动态地分配和释放,在C程序中通过malloc(或calloc、realloc)和free函数实现该处理要求。
                                    例如:
                                    
                                    在堆中分配的内存块的生存期是由程序员自己控制的,应在程序中显式地释放。例如:
                                    
                                    注意:指针为空(指针值为0或NULL)时表示不指向任何内存单元,因此释放空指针没有意义。
                                    因为内存资源是有限的,所以若申请的内存块不再需要就及时释放。如果程序中存在未被释放(由于丢失其地址在程序中也不能再访问)的内存块,则称为内存泄漏。持续的内存泄漏会导致程序性能降低,甚至崩溃。嵌入式系统存储空间非常有限,一般情况下应尽量采用静态存储分配策略。
                             指针与数组
                                    通过指针访问数组元素
                                    在C程序中,常利用指针访问数组元素,数组名表示数组在内存中的首地址,即数组中第一个元素的地址。可以通过下标访问数组元素,也可以通过指针访问数组元素。
                                    例如:
                                    
                                    数组arr的元素可以用*ptr、*(ptr+1)、*(ptr+2)、*(ptr+3)来引用。
                                    数组名是常量指针,数组名的值不能改变,因此arr++是错误的,而ptr++是允许的。例如,下面的代码通过修改指针ptr来访问数组中的每个元素。
                                    
                                    一般情况下,一个int型变量占用4个字节的内存空间,一个char型变量占用一个字节的空间,所以str是字符指针的话,str++就使str指向下一个字符;而整型指针ptr++则使ptr指向下一个int型整数,即指向数组的第二个元素。
                                    可以用指针访问二维数组元素。例如,对于一个m行、n列的二维整型数组,其定义为
                                    
                                    由于二维数组元素在内存中是以线性序列方式存储的,且按行存放,所以用指针访问二维数组的关键是如何计算出某个二维数组元素在内存中的地址。二维数组a的元素a[i][j](ii][j]之前的元素所占空间形成的偏移量,概念上表示为a+(i×n+j)*sizeof(int),在程序中需要表示为(&a[0][0]+i×n+j)。
                                    通过指针访问字符串常量
                                    可将指针设置为指向字符串常量(存储在只读存储区域),通过指针读取字符串或其中的字符。例如,
                                    
                                    不允许在程序运行过程中修改字符串常量。例如,下面代码试图通过修改字符串的第2个字符将“hello”改为“hallo”,程序运行时该操作会导致异常,原因是str指向的是字符串常量“hello”,该字符串在运行时不能被修改。
                                    
                                    如果用const进行修饰,这个错误在编译阶段就能检查出来,修改如下:
                                    
                                    指针数组
                                    如果数组的元素类型是指针类型,则称之为指针数组。下面的ptrarr是一维数组,数组元素是指向整型变量的指针。
                                    
                                    若需要动态生成二维整型数组,则传统的处理方式是先设置一个指针数组arr2,然后将其每个元素的值(指针)初始化为动态分配的“行”。
                                    
                                    指针运算
                                    在C程序中,对指针变量加一个整数或减一个整数的含义与指针指向的对象有关,也就是与指针所指向的变量所占用存储空间的大小有关。例如:
                                    
                                    常量指针与指针常量
                                    常量指针是指针变量指向的对象是常量,即指针变量可以修改,但是不能通过指针变量来修改其指向的对象。例如,
                                    
                                    指针常量是指针本身是个常量,不能再指向其他对象。
                                    在定义指针时,如果在指针变量前加一个const修饰符,就定义了一个指针常量,即指针值是不能修改的。
                                    
                                    指针常量定义时被初始化为指向整型变量d。p本身不能修改(即p不能再指向其他对象),但它所指向变量的内容却可以修改,例如,*p=5(实际上是将d的值改为5)。
                                    区分常量指针和指针常量的关键是“*”的位置,如果const在“*”的左边,则为常量指针,如果const在“*”的右边则为指针常量。如果将“*”读作“指针”,将const读作“常量”,内容正好符合。对于定义“int const *p;”p是常量指针,而定义“int* const p;”p是指针常量。
                             指针与函数
                             指针可以作为函数的参数或返回值。
                                    指针作为函数参数
                                    函数调用时,用指针作为函数的参数可以借助指针来改变调用函数中实参变量的值。以下面的swap函数为例进行说明,该函数的功能是交换两个整型变量的值。
                                    
                                    若有函数调用swap(&x,&y),则swap函数执行后两个实参x和y的值被交换。函数中参与运算的值不是pa、pb本身,而是它们所指向的变量,也就是实参x、y(*pa与x、*pb与y所表示的对象相同)。在调用函数中,是把实参的地址传送给形参,即传送&x和&y,在swap函数中指针pa和pb并没有被修改。
                                    如果在被调用函数中修改了指针参数的值,则不能实现对实参变量的修改。例如,下面函数get_str中的错误是将指针p指向的目标修改了,从而在main中调用get_str后,ptr的值仍然是NULL。
                                    
                                    将上面的函数定义和调用作如下修改,就可以修改实参ptr的值,使其指向函数中所申请的字符串存储空间。
                                    
                                    函数调用为:get_str(&ptr);
                                    用const修饰函数参数,可以避免在被调用函数中出现不当的修改。例如:
                                    
                                    其中,from是输入参数,to是输出参数,如果在函数strcpy内通过from来修改其指向的字符(如*from='a'),编译时将报错。
                                    若需要使指针参数在函数内不能修改为指向其他对象,则可如下修饰指针参数。
                                    
                                    指针作为函数返回值
                                    函数的返回值也可以是一个指针。返回指针值的函数的一般定义形式是:
                                    
                                    例如,如下进行函数定义和调用,可以降低函数参数的复杂性。
                                    
                                    函数调用为:ptr=get_str();
                                    注意:不能将具有局部作用域的变量的地址作为函数的返回值。这是因为局部变量的内存空间在函数返回后即被释放,而该变量也不再有效。
                                    例如,下面函数被调用后,变量a的生存期结束,其存储空间(地址)不再有效。
                                    
                                    函数指针
                                    在C程序中,可以将函数地址保存在函数指针变量中,然后用该指针间接调用函数。例如:
                                    
                                    该语句定义了一个名称为Compare的函数指针变量,用于保存任何有两个常量字符指针形参、返回整型值的函数的地址(函数的地址通常用函数名表示)。例如,Compare可以指向字符串运算函数库中的函数strcmp。
                                    
                                    函数指针也可以在定义时初始化:
                                    
                                    将函数地址赋给函数指针时,其参数和类型必须匹配。
                                    若有函数定义int strcmp(const char*,const char*);则strcmp能被直接调用,也能通过Compare被间接调用。下面三个函数调用是等价的:
                                    
                             指针与链表
                             指针是C语言的特色和精华所在,链表是指针的重要应用之一,创建、查找、插入和删除结点是链表上的基本运算,需熟练掌握这些运算的实现过程,其关键点是指针变量的初始化和在链表结点间的移动处理。
                             以元素值为整数的单链表为例,需要先定义链表中结点的类型,下面将其命名为Node,而LinkList则是指向Node类型变量的指针类型名。
                             
                             当p指向Node类型的结点时,涉及两个指针变量:p和p->next,p是指向结点的指针,p->next是结点中的指针域,如下图(a)所示;运算“p=p->next;”之后,p指向下一个结点;如下图(b)所示;运算“p->next=p;”之后,结点的指针域指向结点自己,如下图(c)所示。
                             
                             指向结点的指针运算示例
                      栈与队列
                      栈和队列是程序中常用的两种数据结构,其特点在于运算受到了限制:栈按“后进先出”的规则进行修改,队列按“先进先出”的规则进行修改。
                             栈
                             栈是只能通过访问它的一端来实现数据存储和检索的一种线性数据结构。换句话说,栈的修改是按先进后出的原则进行的。因此,栈又称为先进后出(FILO,或后进先出)的线性表。在栈中,进行插入和删除操作的一端称为栈顶(top),相应地,另一端称为栈底(bottom)。不含数据元素的栈称为空栈。
                             栈的基本运算如下。
                             (1)初始化栈initStack(S):创建一个空栈S。
                             (2)判栈空isEmpty(S):当栈S为空栈时返回“真”,否则返回“假”。
                             (3)入栈push(S,x):将元素x加入栈顶,并更新栈顶指针。
                             (4)出栈pop(S):将栈顶元素从栈中删除,并更新栈顶指针。
                             (5)读栈顶元素top(S):返回栈顶元素的值,但不修改栈顶指针。
                             可以用一维数组作为栈的存储空间,同时设置指针top指示栈顶元素的位置。在这种存储方式下,需要预先定义或申请栈的存储空间,也就是说栈空间的容量是有限的。因此一个元素入栈时,需要判断是否栈满,若栈满(即栈空间中没有空闲单元),则元素入栈会发生上溢现象。
                             用链表作为存储结构的栈称为链栈。由于栈中元素的插入和删除仅在栈顶一端进行,因此不必另外设置头指针,链表的头指针就是栈顶指针。链栈的表示如下图所示,其类型定义如下:
                             
                             链栈示意图
                             
                             以栈中元素类型为整型为例,实现栈基本运算的函数定义如下。
                             【函数】创建一个单链表表示的空栈。
                             
                             【函数】元素入栈。
                             
                             【函数】元素出栈。
                             
                             【函数】读取并返回栈顶元素。
                             
                             
                             【函数】判断栈是否空。
                             
                             队列
                             队列常用于需要排队的场合,如操作系统中处理打印任务的打印队列、离散事件的计算机模拟等。
                             队列是一种先进先出(FIFO)的线性表,只允许在队列的一端插入元素,而在另一端删除元素。在队列中,允许插入元素的一端称为队尾(rear),允许删除元素的一端称为队头(front)。
                             队列的基本运算如下。
                             (1)初始化队列initQueue(Q):创建一个空的队列Q。
                             (2)判队空isEmpty(Q):当队列为空时返回“真”值,否则返回“假”值。
                             (3)入队enQueue(Q,x):将元素x加入到队列Q的队尾,并更新队尾指针。
                             (4)出队delQueue(Q):将队头元素从队列Q中删除,并更新队头指针。
                             (5)取队头元素frontQueue(Q):返回队头元素的值,但不更新队头指针。
                             可以用一组地址连续的存储单元存放队列中的元素,称为顺序队列。由于队中元素的插入和删除限定在两端进行,因此设置队头指针和队尾指针,分别指示出当前的队首元素和队尾元素。
                             设队列Q的容量为6,其队头指针为front,队尾指针为rear,头、尾指针和队列中元素之间的关系如下图所示。
                             
                             队列的头、尾指针与队列中元素之间的关系
                             在顺序队列中,为了降低运算的复杂度,元素入队时只修改队尾指针,元素出队时只修改队头指针。由于顺序队列的存储空间容量是提前设定的,所以队尾指针会有一个上限值,当队尾指针达到该上限时,就不能只通过修改队尾指针来实现新元素的入队操作了。若将顺序队列设想为一个环状结构(通过整除取余运算实现),则可维持入队、出队操作运算的简单性,如下图所示,称之为循环队列。
                             
                             循环队列的头、尾指针示意图
                             设循环队列Q的容量为MAXSIZE,初始时队列为空,且Q.rear和Q.front都等于0,如下图(a)所示。元素入队时修改队尾指针,即令Q.rear=(Q.rear+1)% MAXSIZE,如下图(b)所示。元素出队时修改队头指针,即令Q.front=(Q.front+1)% MAXSIZE,如下图(c)所示。
                             
                             循环队列的头、尾指针示意图
                             根据队列操作的定义,当出队操作导致队列变为空时,就有Q.rear==Q.front,如上图(d)所示;若入队列操作导致队列满,则也有Q.rear==Q.front,如上图(e)所示。在队列空和队列满的情况下,循环队列的队头、队尾指针指向的位置是相同的,此时仅根据Q.rear和Q.front之间的关系无法判定队列的状态。为了区分队空和队满,可采用两种处理方式:其一是设置一个标志域,以区别头、尾指针的值相同时队列是空还是满;其二是牺牲一个元素空间,约定以“队列的尾指针所指位置的下一个位置是头指针”表示队列满,如上图(f)所示,而头、尾指针的值相同时表示队列为空。
                             设队列中的元素类型为整型,则循环队列的类型定义为:
                             
                             【函数】创建一个空的循环队列。
                             
                             【函数】判断队列是否为空。
                             
                             【函数】元素入循环队列。
                             
                             【函数】元素出循环队列。
                             
                             队列的链式存储也称为链队列。为了便于操作,可给链队列添加一个头结点,并令头指针指向头结点,如下图所示。在这种情况下,队列为空的判定条件是头指针和尾指针相同,且均指向头结点。
                             
                             链队列示意图
                      C程序内嵌汇编
                      嵌入式程序开发与硬件密切相关,通过汇编语言可以直接读写指定的地址或者将代码放入指定的Flash地址,而使用C语言来读写底层寄存器、存取数据、控制硬件时,C语言和硬件之间由编译器来联系,一些C标准不支持的硬件特性操作可由编译器提供。
                      在某些情况下可能需要在C程序中直接编写内嵌的汇编代码,示例如下。
                      
 
       C++
        C++语言是一种面向对象的强类型语言,由AT&T的Bell实验室于1980年推出。
        C++语言是C语言的一个向上兼容的扩充,而不是一种新语言。C++是一种支持多范型的程序设计语言,它既支持面向对象的程序设计,也支持面向过程的程序设计。
        C++支持基本的面向对象概念,包括对象、类、方法、消息、子类和继承。C++完全支持多继承,并且通过使用try/throw/catch模式提供了一个完整的异常处理机制。它同时支持静态类型和动态类型,也完全支持多继承,不提供自动的无用存储单元收集,这必须通过程序员来实现,或者通过编程环境提供合适的代码库来予以支持。
   题号导航      2021年上半年 嵌入式系统设计师 上午试卷 综合知识   本试卷我的完整做题情况  
1 /
2 /
3 /
4 /
5 /
6 /
7 /
8 /
9 /
10 /
11 /
12 /
13 /
14 /
15 /
 
16 /
17 /
18 /
19 /
20 /
21 /
22 /
23 /
24 /
25 /
26 /
27 /
28 /
29 /
30 /
 
31 /
32 /
33 /
34 /
35 /
36 /
37 /
38 /
39 /
40 /
41 /
42 /
43 /
44 /
45 /
 
46 /
47 /
48 /
49 /
50 /
51 /
52 /
53 /
54 /
55 /
56 /
57 /
58 /
59 /
60 /
 
61 /
62 /
63 /
64 /
65 /
66 /
67 /
68 /
69 /
70 /
71 /
72 /
73 /
74 /
75 /
 
第21题    在手机中做本题