C语言测试:嵌入式程序员必须知道的16个问题
C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测试能为带面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这个愚蠢的问题吗?如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是“是”的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮住。这些问题都是我这些年实际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。
预处理器(Preprocessor)
1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
?; #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
?; 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
?; 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
?; 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
2 . 写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
?; 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于
嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
?; 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
?; 懂得在宏中小心地把参数用括号括起来
?; 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
3. 预处理器标识#error的目的是什么?
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问
题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。
死循环(Infinite loops)
4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
这个问题用几个解决方案。我首选的方案是:
while(1)
{
?}
一些程序员更喜欢如下方案:
for(;
{
?}
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做
的基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。
第三个方案是用 goto
Loop:
...
goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。
数据声明(Data declarations)
5. 用变量a给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns a
n integer)
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an i
nteger argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下
书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果
不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?
Static
6. 关键字static的作用是什么?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
?; 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
?; 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
?; 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得
本地化数据和代码范围的好处和重要性。
Const
7.关键字const有什么含意?
我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有
用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只
要能说出const意味着“只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读
一下Saks的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
//
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a
是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针
(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺
带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下
的几下理由:
?; 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花
很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
?; 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
?; 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
?; 关于const关键字还有以下测试
===========================================================================
int *const (*p)[2];//指针变量
int *x=NULL;
int *y=NULL;
int *const z[2]={x,y};//定义指针变量指向的数组
p=&z;//指向数组
*(z[0])=0;//编译正确
z[0]=NULL;//编译出错
上面定义一个指针p,它指一个数组,数组的元是int型指针,
并用了const进行修饰,这里到底是指针是只读还是指针所指向地址的值是只读呢?
通过实际测试*(z[0])=0;这句编译是正确的,而z[0]=NULL;则编译出错,
说明const限定的是指针是只读,而不是其指向地址的值是只读
如果将定义成下面的形式
int const *(*p)[2];
则*(z[0])=0;这句会编译错误,则z[0]=NULL则是编译正确的
===========================================================================
Volatile
8. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变
量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
?; 并行设备的硬件寄存器(如:状态寄存器)
?; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
?; 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTO
S等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
?; 一个参数既可以是const还可以是volatile吗?解释为什么。
?; 一个指针可以是volatile 吗?解释为什么。
?; 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
?; 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
volatile修饰符告诉complier变量值可以以任何不被程序明确指明的方式改变,最常见的例子就是外部端口的值,它的变化可以不用程序内的任何赋值语句就有可能改变的,这种变量就可以用volatile来修饰,complier不会优化掉它。
const修饰的变量在程序里面是不能改变的,但是可以被程序外的东西修改,就象上面说的外部端口的值,如果仅仅使用const,有可能complier会优化掉这些变量,加上volatile就万无一失了。
?; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
?; 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
位操作(Bit manipulation)
9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个
操作中,要保持其它位不变。
对这个问题有三种基本的反应
?; 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
?; 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我
最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields
的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
?; 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1 << 3)//说明常数
static int a;
void set_bit3(void) {
a |= BIT3;//BIT3为1,其他位不变
}
void clear_bit3(void) {
a &= ~BIT3;//BIT3为0,其他位不变
}
一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。
访问固定的内存位置(Accessing fixed memory locations)
10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译
器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不
同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
A more obscure approach is:
一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。
中断(Interrupts)
11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __in
terrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}
这个函数有太多的错误了,以至让人不知从何说起了:
?; ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
?; ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
?; 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做
浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
?; 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么
你的被雇用前景越来越光明了。
*****
代码例子(Code examples)
12 . 下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ”>
6”。***原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型****。因此-20变成了一个非常大的正整数,所以该表达式
计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工
作的边缘。
13. 评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/11's complement of zero
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机
程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做
得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付
问题的方法,而不是答案。不管如何,你就当是这个娱乐吧…
动态内存分配(Dynamic memory allocation)
14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的
问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的执行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger,
他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:
下面的代码片段的输出是什么,为什么?
char *ptr;
看《匠人手记》,与匠人同行!北航出版,正在热卖!
上一篇:DSP简介
下一篇:浅谈单片机程序设计中的“分层思想”(zt)
看《匠人手记》,与匠人同行!北航出版,正在热卖!
if ((ptr = (char *)malloc(0)) ==
NULL)
else
puts("Got a null pointer");
puts("Got a valid pointer");
这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,
该代码的输出是“Got a valid pointer”。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重
要,但解决问题的方法和你做决定的基本原理更重要些。
Typedef
:
15 Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
.
上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。
晦涩的语法
16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际
上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风
格,代码的可读性,代码的可修改性的好的话题。
好了,伙计们,你现在已经做完所有的测试了。这就是我出的C语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是认为这
是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个
Re:C语言测试:嵌入式程序员必须知道的16个问题
LLS(游客)发表评论于2009-2-16 2:29:00 个人主页 | 引用 | 返回 | 删除 | 回复
另一个问题:
//=============================
......
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
.......
我也是刚接触C的,我是这样理解的:因为a++是运算后自加,所以c=a+b,然后a再加1.
Re:C语言测试:嵌入式程序员必须知道的16个问题
sinba(游客)发表评论于2008-11-14 16:24:00 个人主页 | 引用 | 返回 | 删除 | 回复
今天看了你的文章,四个字:受益匪浅 !
我是一个简单的单片机应该是应用者,不是一个嵌入式开发者,但我正在向嵌入式这方面学习,感觉很困难。
以前编程中也很少注意这些关键字:static typedef const 同样对指针的用法也没有见过怎么多,今天算是学了很多东西。对我以后
编程我感觉有很大的帮助。不过我有些问题想请教:
在您的这段代码中:
//=============================
下面的代码片段的输出是什么,为什么?
char *ptr;
if ((ptr = (char *)malloc(0)) ==
NULL)
else
puts("Got a null pointer");
puts("Got a valid pointer");
//=============================
为什么输出的是后者,而不是两个都输出?(不知道是不排版问题,因为if 后面也没执行语句)
另一个问题:
//=============================
......
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
.......
//=============================
我实在不确定为什么C还是等于12,:)
以上的问题,只是我个人对C语言程序深入编写不太懂,提出的疑惑,没有任何一点反对攻击的意思,如果那里有不对的地方,请您见谅。我自认为
对硬件比较了解,现在想向高级编程方便学习。
*/
/*
17. C语言中的#error
#error 其实就是一个标记,你可以写一段代码,对他进行判断,如果错误进入else啥的
然后你在里面写上#error程序运行到这里就会中断,给出错误信息,我想这应该是用来调试用的
#error 使您可以从代码中的特定位置生成错误;就是说执行到这一句时自动产生错误,例如:
#if DEBUG
#error DEBUG is defined // 进入这里将产生错误
#endif
*/
/*
18. C语言中的连接符问题
#define VPCAP_REGISTER_PROTO_DEFINE(X, _proto) \
static void X(pcap_buff_t*, packet_info_t*); \
static field_info __##X = {NULL, _proto, X, 0, NULL}; \
field_info* X ## _field_info = &__##X;
这个是一个宏定义 &__##X 这个表达式的作用是什么呢?
回答:
使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起.
static field_info __##X = {NULL, _proto, X, 0, NULL};
这句相当于定义了一个静态变量,名字是在X前面加了两个下划线(也就是__)。
field_info* X ## _field_info = &__##X;
这句中前面是定义了指针,&取地址,__##X表示上一句定义的变量。
也就是去上面定义的变量的地址,赋给一个指针。
*/
/*-------------------------------------------------------------------------------------------------------
关于C语言中头文件与C文件的问题
---------------------------------------------------------------------------------------------------------*/
/*
因为 #include "xx.h" 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。
由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),
所以用 #include "xx.h" 这个宏就简化了许多行代码——让预处理器自己替换好了。
也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),
至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
纯粹用 C 语言语法的角度,你当然可以在 .h 中放任何东西,因为 #include 完全等价 于把 .h 文件 Ctrl-C,然后 Ctrl-V 到 .c 中
.h 中应该都是一些宏定义和变量、函数声明,告诉别人你的程序“能干什么、该怎么用”
.c 中是所有变量和函数的定义,告诉计算机你的程序“该怎么实现”
*/
/*-------------------------------------------------------------------------------------------------------
关于text段,data段及bss段简介
---------------------------------------------------------------------------------------------------------*/
/*
一个程序的3个基本段:text段,data段,bss段。
text段在内存中被映射为只读,但.data和.bss是可写的。
text段:就是放程序代码的,编译时确定,只读;
data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,常量也存放在这个区域;
bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域;
首先,两者都是存储全局变量的,只不过是bss存储那些没有初始化的或者初始化为0的全局变量,bss的特点就是他不占用物理文件尺寸,但,占用内存空间。
数据段主要存储了已经初始化了的全局变量,它占用物理文件,也占用内存空间。
另一个差别,
当程序读取数据段的数据时,这时,系统会出发缺页故障,从而分配相应的物理内存。
当程序读取BSS段的数据时,内核会将其转到一个全零的页面,不会发生缺页故障,也不会为其分配相应的物理内存。
对于全局常量变量,由于其不可以被修改,所以,它被放在只读的代码段。
对于一个函数中的static变量,即使没有初始化也会被放在数据段上,而不是BSS段。
*/
/*-----------------------------------------------------------------------------------------------------
typedef用法简介
//-----------------------------------------------------------------------------------------------------
1.简单的函数指针的应用
形式1:返回类型(*函数名)(参数表)
[cpp] view plaincopy
char (*pFun)(int);
char glFun(int a){ return;}
void main()
{
pFun = glFun;
(*pFun)(2);
}
第一行定义了一个指针变量pFun。首先我们根据前面提到的“形式1”认识到它是一个指向某种函数的指针,这种函数参数是一个int型,返回值是char类型。只有第一句我们还无法使用这个指针,因为我们还未对它进行赋值。
第二行定义了一个函数glFun()。该函数正好是一个以int为参数返回char的函数。我们要从指针的层次上理解函数——函数的函数名实际上就是一个指针,函数名指向该函数的代码在内存中的首地址
然后就是main()函数了,它的第一句您应该看得懂了——它将函数glFun的地址赋值给变量pFun。main()函数的第二句中“*pFun”显然是取pFun所指向地址的内容,当然也就是取出了函数glFun()的内容,然后给定参数为2。
2.使用typedef更直观更方便
形式1:typedef 返回类型(*新类型)(参数表)
[cpp] view plaincopy
typedef char (*PTRFUN)(int);
PTRFUN pFun;
char glFun(int a){ return;}
void main()
{
pFun = glFun;
(*pFun)(2);
}
typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。
第二行的代码便使用这个新类型定义了变量pFun,此时就可以像使用形式1一样使用这个变量了。
3.例子说明
[cpp] view plaincopy
#include <stdio.h>
#include <assert.h>
typedef int (*FP_CALC)(int,int);//定义一个函数指针类型
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return b ? a/b : -1;
}
//定义一个函数,参数为op,返回一个指针,该指针类型为拥有两个int参数、
//返回类型为int的函数指针。它的作用是根据操作符返回相应函数的地址
FP_CALC calc_func(char op)
{
switch( op )
{
case '+':
return add;
case '-':
return sub;
case '*':
return mul;
case '/':
return div;
default:
return NULL;
}
return NULL;
}
//s_calc_func为函数,它的参数是 op,
//返回值为一个拥有两个int参数、返回类型为int的函数指针
int (*s_calc_func(char op)) (int , int)
{
return calc_func(op);
}
//最终用户直接调用的函数,该函数接收两个int整数,
//和一个算术运算符,返回两数的运算结果
int calc(int a, int b, char op)
{
FP_CALC fp = calc_func(op);
int (*s_fp)(int,int) = s_calc_func(op);//用于测试
assert(fp == s_fp);// 可以断言这两个是相等的
if(fp)
return fp(a,b);
else
return -1;
}
void main()
{
int a = 100, b = 20;
printf("calc(%d, %d, %c) = %d\n", a, b, '+', calc(a, b, '+'));
printf("calc(%d, %d, %c) = %d\n", a, b, '-', calc(a, b, '-'));
printf("calc(%d, %d, %c) = %d\n", a, b, '*', calc(a, b, '*'));
printf("calc(%d, %d, %c) = %d\n", a, b, '/', calc(a, b, '/'));
}
*/
/*-----------------------------------------------------------------------------------------------------
MIPS解释
//-----------------------------------------------------------------------------------------------------
MIPS:单字长定点指令平均执行速度,即每秒处理的百万级的机器语言指令数。
这是衡量CPU速度的一个指标。比如一台电脑可以每秒处理3百万到5百万机器语言指令,
我们可以说这个电脑是3到5MIPS的CPU。
单字长定点指令平均执行速度 Million Instructions Per Second的缩写,每秒处理的百万级的机器语言指令数。
这是衡量CPU速度的一个指标.
STM32有三级流水线,指令周期不定的,arm给出的是1.25MIPS/Mhz,一个平均执行速度。
就是1Mhz的频率,每秒钟可以执行1.25M指令。72M,那么就是72*1.25
MIPS是世界上很流行的一种RISC处理器。MIPS的意思“无内部互锁流水级的微处理器”(Microprocessor without interlocked piped stages),
其机制是尽量利用软件办法避免流水线中的数据相关问题。
*/
/*-----------------------------------------------------------------------------------------------------
PWM相关知识
//-----------------------------------------------------------------------------------------------------
1. 什么是PWM?
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,
是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,
广泛应用在从测量、通信到功率控制与变换的许多领域中。
详细情况可参考虑以下网站:http://baike.baidu.com/view/631384.htm?fromtitle=PWM&fromid=3034961&type=syn
2. 什么是PWM的占空比?
PWM,即脉冲宽度调制,一个周期内高电平的所占的比例就是占空比了。这个时间周期你自己定,要的是比值。
*/
/*----------------------------------------------------------------------------------------------------
常见的知名半导体厂家中英文对照
------------------------------------------------------------------------------------------------------
1. Analog Devices, 亚德诺半导体
2. Atmel, 爱特梅尔
3. Cypress, 赛普拉斯
4. Freescale, 飞思卡尔
5. Infineon, 英飞凌
6. Renesas, 瑞萨半导体
7. ST, 意法半导体,著名的STM32系列单片机
8. Texas Instruments, 德州仪器
9. Xilinx, 赛灵思半导体
*/
//========================C语言算法题===============================
/*
题1.
写一个算法,将一个数按高位到低位逆转,例如,用户输入123,则输出321。
第一反应容易忘字符串反转想,其实不用。
void main()
{
int m = 0 , n = 123;
while(n>0)
{
m = m*10 + n%10;
n = n/10;
}
printf("%d",m);
}
//=============题2:最大公约数和最小公倍数==============
输入两个正整数m和n, 求其最大公约数和最小公倍数.
<1> 用辗转相除法求最大公约数 算法描述: m对n求余为a, 若a不等于0 则 m <- n, n <- a, 继续求余 否则 n 为最大公约数
<2> 最小公倍数 = 两个数的积 / 最大公约数
//求自然数m和n的最大公约数 连续整数检测法
int cgcd (int u, int v)
{
int t = u>v ? v:u; //取两个数的较小的一个
do{
if((u%t==0)&&(v%t==0))return t; //如果能同时整除t,则证明找到了最大公约数
else t--; //自减除数
}
while(t>0);
}
//求自然数m和n的最大公约数 欧几里得算法(又称辗转相除法).
int gcd (int u, int v){
do {
int t = u % v; //得到余数
u = v; //利用上一步的除数作为被除数
v = t; //利用上一步的余数作为除数
} while (v); //直到余数为0
return u;
}
辗转相除法是求两数最大公约数的一种方法。
它的依据是"[a除以b所得的余数与b的公约数]等于[a与b的公约数]"以及"a是b的倍数,则b是a和b的最大公约数"。
例如:求125和45的最大公约数
1. 先作除法125÷45得到余数35
2. 再作除法45÷35得到余数10
3. 再做35÷10得到余数5
4. 至此,10÷5 的余数为0,所以125和45的最大公约数为5
要领:
1. 首先以大数除以小数得到余数
2. 然后原来的除数作被除数,前一步的余数作除数,相除得到余数
3. 照上面步骤继续下去,直到余数为0(能整除);
这时除数就是最大公约数。
//===============题3:水仙花数===================
水仙花数实际是一个3位数,其各位数的立方和等于该数。
例如:153就是一个水仙花数,因为153=1的立方+5的立方+3的立方
例如,可用如下的示例代码:
void main() {
int i,j,k,n;
printf("water flower number is:");
for(n=100;n<1000;n++) { //水仙花数是一个3位数
i=n/100;//分解出百位
j=n/10%10;//分解出十位
k=n%10;//分解出个位
if (i*100+j*10+k==i*i*i+j*j*j+k*k*k) {
printf("%-5d", n);//输出所有水仙花数字
}
}
printf("\n");
}
//==============题4:自由落体运动================
一球从100米高度自由落下,每次落地后反弹回高度的一半,再落下
请它在第10次落地时,共经过多少米?第10次反弹多高?
void main() {
float sn=100.0, hn=sn/2;
int n;
for(n=2;n<=10;n++) {
sn=sn+2*hn;//第n次落地时共经过的米数
hn=hn/2;//第n次反弹高度(每次减半)
}
printf("the total of road is %f\n", sn);
printf("the tenth of %f meter\n", hn);
}
//==============题5:合并排序问题===================
合并法排序(将两个有序数组A、B合并成另一个有序的数组C,升序,请注意:A和B都是有序的数组)
基本思路:
1)先在A、B数组中各取第一个元素进行比较,将小的元素放入C数组;
2)取小的元素所在数组的下一个元素与另一数组中上次比较后较大的元素比较,重复上述比较过程,直到某个数组被先排完;
3)将另一个数组剩余元素抄入C数组,合并排序完成。
程序段如下:
void main()
{
int a[10],b[10],c[20],i,ia,ib,ic;
printf("please input the first array:\n");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
for(i=0;i<10;i++)
scanf("%d",&b[i]);
printf("\n");
ia=0;ib=0;ic=0;
while(ia<10&&ib<10)
{
if(a[ia]<b[ib]) { //得到较小的元素并放入C数组
c[ic]=a[ia];
ia++;
}
else {
c[ic]=b[ib];
ib++;
}
ic++;
}
while(ia<=9) { //将另一个数组剩余元素抄入C数组
c[ic]=a[ia];
ia++;
ic++;
}
while(ib<=9) {
c[ic]=b[ib];
b++;
ic++;
}
for(i=0;i<20;i++)
printf("%d\n",c[i]);
}
数组定义字符串:
每次定义数组的时候,系统都会在内存开辟你指定数组大小的空间,并且数组中的内容对于我们是可读可写的,看如下代码:
1 #include<stdio.h> 2 int main() 3 { 4 char str[100] = "hello world"; 5 char str1[100] = "hello world"; 6 str[0] = 'd'; 7 printf("%s\n\n",str); 8 printf("%x\n",&str); 9 printf("%x\n",&str1); 10 return 0; 11 }
再看执行结果:
每次定义的数组的首地址是不相同的。
指针定义字符串:
指针定义的字符串是存储在内存中的静态存储空间中,可读但不可写,并且如果再定义一个相同的字符串,指针的值不会变,还会指向原来的地址,不会开辟新的存储空间,看如下代码:
1 #include<stdio.h> 2 int main() 3 { 4 char *p = "hello world"; 5 char *p1 = "hello world"; 6 //p[0] = 'd'; 7 printf("%s\n\n",p); 8 printf("%x\n",p); 9 printf("%x\n",p1); 10 return 0; 11 }
看执行结果:
当我们试图改变hello world中的h时,运行程序时会自动停止,相同的字符串的指针是指向同一个地方的。