(经典)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))
解答:
实现输入两个参数并返回较小的一个,应使用三目表达式。使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合。
参看:C语言再学习 -- C 预处理器
使用#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) {...}
for (;;){...}
Loop; ... goto Loop
解答:
参看: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;
(5)在for循环中使用数组, 可使用#define来指定数组大小
例如:
#define SIZE 10
for (int n = 0; n < SIZE; n++)
printf ("hello world\n");
5、用变量a给出下面的定义
a)一个整型数
int a;
b)一个指向整型数的指针
int *a;
c)一个指向指针的的指针,它指向的指针是指向一个整型数
int **a;
d)一个有10个整型数的数组
int a[10];
e)一个有10个指针的数组,该指针是指向一个整型数的。
int *a[10];
f)一个指向有10个整型数数组的指针
int (*a)[10];
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数
int (*a)(int);
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
int (*a[10])(int);
解答:
参看:C语言再学习 -- 再论数组和指针
指针数组:首先它是一个数组,数组的元素都是指针,例如: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 修饰函数
解答:
参看:C语言再学习 -- 存储类型关键字
(1)static 修饰的全局变量也叫静态全局变量,该类具有静态存储时期、文件作用域和内部链接,仅在编译时初始化一次。如未明确初始化,它的字节都被设定为0。static全局变量只初使化一次,是为了防止在其他文件单元中被引用;利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。
(2)static 修饰的局部变量也叫静态局部变量,该类具有静态存储时期、代码作用域和空链接,仅在编译时初始化一次。如未明确初始化,它的字节都被设定为0。函数调用结束后存储区空间并不释放,保留其当前值。
(3)static 修饰的函数也叫静态函数,只可以在定义它的文件中使用。
7、关键字const有什么含意?
const 修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的。或者说const意味着 只读。
解答:
参看:C语言再学习 -- 关键字const
(1)在定义该const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为 const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const 类型,以使得其返回值不为“左值”。
作用的话,可以保护被修改的东西,防止意外的修改,增强程序的健壮性。
8、关键字volatile有什么含意?并给出三个不同的例子。
volatile 关键字是一种类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。直接读值是指从内存重新装载内容,而不是直接从寄存器拷贝内容。
volatile 使用:
(1)并行设备的硬件寄存器(如:状态寄存器)
这些寄存器里面的值是随时变化的。如果我们没有将这个地址强制类型转换成 volatile,那么我们在使用GPC1CON 这个寄存器的时候, 会直接从 CPU 的寄存器中取值。因为之前GPC1CON 被访问过,也就是之前就从内存中取出 GPC1CON 的值保存到某个寄存器中。之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问 CPU寄存器比访问 RAM 快的多)。用 volatile 关键字对 0xE0200080 进行强制转换,使得每一次访问 GPC1CON 时,执行部件都会从 0xE0200080 这个内存单元中取出值来赋值给 GPC1CON 。
(2)一个中断服务子程序中会访问到的非自动变量
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。
(3)多线程应用中被几个任务共享的变量
当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。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;
}
解答:
参看:C语言再学习 -- 位操作
10、嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa55。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
int *ptr;
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 不能返回一个值。
(2)ISR 不能传递参数
(3) 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
(4)与第三点一脉相承,printf() 经常有重入和性能上的问题。
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)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
(1)越界,理论上可以申请4G,但超过程序内存的大小就会返回空指针,所以要检查返回的指针是否为空。
(2)用完记得释放,以免内存泄漏。 还有使用的过程中不要指针越界,这样会导致致命错误。
(3)使用 free 或 delete 释放了内存后,没有将指针设置为 NULL。导致产生“野指针”。
解答:
参看:C语言再学习 -- 详解C++/C 面试题 1
函数用法:
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指针。哪种方法更好呢?(如果有的话)为什么?
typedef更好
解答:
参看:C语言再学习 -- 关键字typedef
首先你要了解 typedef 和 define 的区别, 宏定义只是简单的字符串代换,是在预处理完成的 ,而 typedef是在编译时处理的 ,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。
上面两种情况,从形式上看这两者相似,但在实际使用中却不相同。
dPS p1,p2;
在宏代换后变成: struct s* p1, p2; 定义p1为一个指向结构的指针,p2为一个实际的结构。
tPS p3,p4;
而typedef代换后,正确地定义了p3 和p4 两个指针。
总结,typedef和#define的不同之处:
1、与#define不同,typedef 给出的符号名称仅限于对类型,而不是对值。
2、typedef 的解释由编译器,而不是是处理器执行。
3、虽然它的范围有限,但在其受限范围内,typedef 比 #define 更灵活。
16、C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
上面的例子是完全合乎语法的。
解答:
C语言再学习 -- 运算符与表达式
#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;