(经典)C语言测试:想成为嵌入式程序员应知道的0x10个基本问题。
参看:嵌入式程序员面试问题集锦
1、用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SENCONDS_PER_YEAR (60*60*24*365)UL
解答:
#define 声明一个常量,使用计算常量表达式的值来表明一年中有多少秒,显得就更加直观了。再有这个表达式的值为无符号长整形,因此应使用符号 UL。
2、写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
解答:
实现输入两个参数并返回较小的一个,应使用三目表达式。使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合。
使用#define需要注意下面几点:
(1)宏的名字中不能有空格,但是在替代字符串中可以使用空格。ANSI C 允许在参数列表中使用空格。
(2)用圆括号括住每个参数,并括住宏的整体定义。
(3)用大写字母表示宏函数名,便于与变量区分。
(4)有些编译器限制宏只能定义一行。即使你的编译器没有这个限制,也应遵守这个限制。
(5)宏的一个优点是它不检查其中的变量类型,这是因为宏处理字符型字符串,而不是实际值。
(6)在宏中不要使用增量或减量运算符。
3、预处理器标识#error的目的是什么?
#error 字符串 => 表示产生一个错误信息
解答:
#error 字符串 => 表示产生一个错误信息
#warning 字符串 => 表示产生一个警告信息
//#error和#warning的使用
#include <stdio.h>
#define VERSION 4
#define VERSION 2
#define VERSION 3
#if(VERSION < 3)
#error "版本过低"
#elif(VERSION > 3)
#warning "版本过高"
#endif
int main(void)
{
printf("程序正常运行\n");
return 0;
}
输出结果:
警告: #warning "版本过高"
//错误: #error "版本过低"
//程序正常运行
4、嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
while(逻辑表达式)
{
反复执行的语句
}
只要逻辑表达式结果为真就反复不停执行大括号里的语句,直到逻辑表达式结果为假循环结束,只要把逻辑表达式写成1则循环成为死循环。
while 很好理解,下面讲讲 for 循环。
例如:for(num=1; num <10; num++);
在关键字for之后的圆括号中包含了由两个分号分开的三个表达式:
第一个表达式进行初始化,它在for循环开始的时候执行一次,可以使用逗号为多个变量进行初始化。
第二个表达式是判断条件,在每次执行之前都要对它进行求值。当表达式为假时,循环结束。
第三个表达式进行改变或者称为更新,它在每次循环结束时进行计算。
for循环的灵活性
(1)可以让一个或多个表达式为空(但是不要遗漏分号)。只须确保在循环中包含一些能是循环最终结束的语句。(2)顺便说一句,中间的那个控制表达式为空会被认为是真,所以下面的循环会永远执行:
for (; ;)
printf ("hello world\n");
(3)第一个表达式不必初始化一个变量,它也可是某种类型的 printf() 语句,要记住第一个表达式只在执行循环的其他部分之前被求值或执行一次。
(4)for循环可使用逗号运算符把两个表达式链接为一个表达式,并保证最左边的表达式最先计算。
例如:
for (n = 2, m = 0; m < 1000; n *=2)
m +=n;
#define SIZE 10
for (int n = 0; n < SIZE; n++)
printf ("hello world\n");
5、用变量a给出下面的定义
a)一个整型数
d)一个有10个整型数的数组
e)一个有10个指针的数组,该指针是指向一个整型数的。
f)一个指向有10个整型数数组的指针
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
指针数组:首先它是一个数组,数组的元素都是指针,例如:int *p1[10];
数组指针:首先它是一个指针,它指向一个数组,例如:int (*p2)[10];
函数指针:首先它是一个指针,它指向一个函数,例如:int (*p3)(int);
这里需要明白一个符号之间优先级的问题,"[ ]"的优先级比"*"要高。p1 先与“ []”结合,构成一个数组的定义,数组名为 p1, int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个指向 int 类型数据的指针,即指针数组。
至于 p2 就更好理解了,在这里"( )"的优先级比"[ ]"高,"*"号和 p2 构成一个指针的定义,指针变量名为 p2, int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指 针,它指向一个包含 10 个 int 类型数据的数组,即数组指针。
再至于p3也不难理解,在这里"( )"的结合方向是 从左到右,也就是说首先它是一个指针,它指向一个函数,即函数指针。
6、关键字static的作用是什么?
static 修饰全局变量
static 修饰局部变量
static 修饰函数
解答:
(1)static 修饰的全局变量也叫静态全局变量,该类具有静态存储时期、文件作用域和内部链接,仅在编译时初始化一次。如未明确初始化,它的字节都被设定为0。static全局变量只初使化一次,是为了防止在其他文件单元中被引用;利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。
(2)static 修饰的局部变量也叫静态局部变量,该类具有静态存储时期、代码作用域和空链接,仅在编译时初始化一次。如未明确初始化,它的字节都被设定为0。函数调用结束后存储区空间并不释放,保留其当前值。
(3)static 修饰的函数也叫静态函数,只可以在定义它的文件中使用。
7、关键字const有什么含意?
const 修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的。或者说const意味着 只读。
解答:
(1)在定义该const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为 const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const 类型,以使得其返回值不为“左值”。
作用的话,可以保护被修改的东西,防止意外的修改,增强程序的健壮性。
8、关键字volatile有什么含意?并给出三个不同的例子。
解答: 参看:C语言再学习 -- 关键字volatile
9、嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
#define BIT3 (0x1 << 3)
static int a;
void set_bit3 (void)
{
a |= BIT3;
}
void clear_bit3 (void)
{
a &= ~BIT3;
}
解答:
10、嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa55。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
ptr = (int *)0x67a9;
*ptr = 0xaa55;
11、中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}
(1)ISR 不能返回一个值。
12、下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main (void)
{
int a = 10;
printf ("sizeof ((a > 5) ? 4 : 8.0) = %d\n", sizeof ((a > 5) ? 4 : 8.0));
return 0;
}
输出结果:
sizeof ((a > 5) ? 4 : 8.0) = 8
13、评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下: unsigned int compzero = ~0;
14、尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
函数用法:
type *p;
p = (type*)malloc(n * sizeof(type));
if(NULL == p)
/*请使用if来判断,这是有必要的*/
{
perror("error...");
exit(1);
}
.../*其它代码*/
free(p);
p = NULL;/*请加上这句*/
函数使用需要注意的地方:
1、malloc 函数返回的是 void * 类型,必须通过 (type *) 来将强制类型转换。
2、malloc 函数的实参为 sizeof(type),用于指明一个整型数据需要的大小。
3、申请内存空间后,必须检查是否分配成功
4、当不需要再使用申请的内存时,记得释放,而且只能释放一次。如果把指针作为参数调用free函数释放,则函数结束后指针成为野指针(如果一个指针既没有捆绑过也没有记录空地址则称为野指针),所以释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
5、要求malloc和free符合一夫一妻制,如果申请后不释放就是内存泄漏,如果无故释放那就是什么也没做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
15、Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
tPS p3,p4;
总结,typedef和#define的不同之处:
1、与#define不同,typedef 给出的符号名称仅限于对类型,而不是对值。
2、typedef 的解释由编译器,而不是是处理器执行。
3、虽然它的范围有限,但在其受限范围内,typedef 比 #define 更灵活。
16、C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
#include <stdio.h>
int main (void)
{
int a = 5,b = 7,c;
c = a+++b;
printf ("c = %d\n", c);
return 0;
}
输出结果:
c = 12
c = a+++b;
c = a++ + b;
c = a++ + b;