[计算机]C面试宝典.doc
1、C中的malloc与C+中的new有什么区别?(1) new、delete是操作符,可以重载,只能在C+中使用;(2) malloc、free是函数,可以覆盖,C、C+中都可以使用;(3) new可以调用对象的构造函数,对应的delete调用相应的析构函数;(4) malloc仅仅分配内存,free仅仅回收内存,并不执行构造和析构函数;(5) new、delete返回的是某种数据类型指针,malloc、free返回的是void指针。2、delete与delete区别delete只会调用一次析构函数,而delete会调用每一个成员的析构函数。在More Effective C+中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用delete来释放内存。”delete与new配套,delete与new配套。MemTest *mTest1 = new MemTest10;MemTest *mTest2 = new MemTest;int *pInt1 = new int10;int *pInt2 = new int;delete pInt1; /-1-delete pInt2; /-2-delete mTest1;/-3-delete mTest2;/-4-在-4-处报错。这就说明:对于内建简单数据类型,delete和delete功能是相同的。对于自定义的复杂数据类型,delete和delete不能互用。delete删除一个数组,delete删除一个指针。简单来说,用new分配的内存用delete删除,用new分配的内存用delete删除。delete会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。3、多态,虚函数,纯虚函数,抽象类多态简单而言,一个接口,多种实现。也可以这么理解,同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。多态性分为两种,一种是编译时的多态性,另一种是运行时的多态性。编译时的多态性是通过重载来实现的,编译器在编译阶段根据函数的参数个数、参数类型决定实现何种操作。运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。虚函数虚函数是指一个在类中希望被重写的成员函数,当用一个基类指针或引用指向一个派生类对象的时候,调用一个虚函数,实际调用的是派生类的版本。虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数。虚函数是成员函数,而且是非static的成员函数。如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。在派生类中,若要重写该虚函数,其原型必须满足如下条件:(1) 与基类的虚函数参数个数相同;(2) 其参数的类型与基类的虚函数的对应参数类型相同;(3) 如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的,但这种例外只适用于返回值,而不适用于参数)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。纯虚函数纯虚函数是一种特殊的虚函数,是在基类中声明的但在基类中没有定义,要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。它的一般格式如下:class <类名>virtual <类型><函数名>(<参数表>)=0;在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。抽象类包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。抽象类的主要作用是将有关的类组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类,而是一个可以建立对象的具体类。4、指针找错题分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。试题1 以下是引用片段:void test1() / 数组越界char string10;char *str1 = "0123456789"strcpy(string, str1);试题2 以下是引用片段:void test2()char string10, str110;int i;for(i=0; i<10; i+)str1= 'a'strcpy(string, str1);试题3 以下是引用片段:void test3(char *str1)char string10;if(strlen(str1) <= 10)strcpy(string, str1);解答:对试题1:字符串str1需要11个字节才能存放下(包括末尾的'0'),而string只有10个字节的空间,strcpy会导致数组越界。对试题2:如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string,str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分。对试题3:if(strlen(str1) <= 10)应改为if(strlen(str1) <10),因为strlen的结果未统计'0'所占用的1个字节。剖析:考查对基本功的掌握(1) 字符串以'0'结尾;(2) 对数组越界把握的敏感度;(3) 库函数strcpy的工作方式;(4) 对strlen的掌握,它没有包括字符串末尾的'0'。试题4 以下是引用片段:void GetMemory(char *p)p = (char *)malloc(100);void Test(void)char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);试题5 以下是引用片段:char* GetMemory(void)char p = "hello world"return p;void Test(void)char *str = NULL;str = GetMemory();printf( str );试题6 以下是引用片段:void GetMemory(char *p, int num)*p = (char *)malloc(num);void Test(void)char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);试题7 以下是引用片段:void Test(void)char *str = (char *)malloc(100);strcpy(str, "hello");free(str);. /省略的其它语句解答:试题4传入中GetMemory(char *p)函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完后的str仍然为NULL。试题5中char p = "hello world"return p;p数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句*p = (char *)malloc(num);后未判断内存是否申请成功,应加上:if (*p = NULL)./进行申请内存失败处理试题7存在与试题6同样的问题,在执行char *str = (char *) malloc(100);后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上str = NULL;。试题6的Test函数中也未对malloc的内存进行释放。剖析:试题47考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中5060的错误。但是要完全解答正确,却也绝非易事。对内存操作的考查主要集中在:(1) 指针的理解;(2) 变量的生存期及作用范围;(3) 良好的动态内存申请和释放习惯。再看看下面的一段程序有什么错误,以下是引用片段:swap(int *p1, int *p2)int *p;*p = *p1;*p1 = *p2;*p2 = *p;在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC+中DEBUG运行时提示错误"Access Violation"。该程序应该改为:以下是引用片段:swap(int *p1, int *p2)int p;p = *p1;*p1 = *p2;*p2 = p;5、如何编写一个标准strcpy函数?总分值为10,下面给出几个不同得分的答案:2分 以下是引用片段:void strcpy(char *strDest, char *strSrc)while(*strDest+ = * strSrc+) != '0');4分 以下是引用片段:void strcpy(char *strDest, const char *strSrc)/ 将源字符串加const,表明其为输入参数,加2分while( (*strDest+ = * strSrc+) != '0' );7分 以下是引用片段:void strcpy(char *strDest, const char *strSrc)/ 对源地址和目的地址加非0断言,加3分assert(strDest != NULL) && (strSrc != NULL);while(*strDest+ = * strSrc+) != '0');10分 以下是引用片段:/ 为了实现链式操作,将目的地址返回,加3分!char * strcpy( char *strDest, const char *strSrc )assert(strDest != NULL) && (strSrc != NULL);char *address = strDest;while(*strDest+ = * strSrc+) != '0');return address;从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为:以下是引用片段:int strlen(const char *str) / 输入参数constassert(strt != NULL); / 断言字符串地址非0int len=0; / 一定要初始化while(*str+) != '0')len+;return len;6、 请你分别画出OSI的七层网络结构图和TCP/IP的五层结构图并简述OSI体系结构七层模型。如下图所示:应用层:为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。表示层:确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通用格式来实现多种数据格式之间的转换。会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据);UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。数据链路层:定义了如何让格式化数据进行传输,以及如何控制对物理介质的访问。这一层通常还提供错误检测和纠正机制,以确保数据的可靠传输。物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。7、8086是多少位的系统?在数据总线上是怎么实现的?8086微处理器共有4个16位的段寄存器,在寻址内存单元时,用它们直接或间接地存放段地址。代码段寄存器CS:存放当前执行的程序的段地址。数据段寄存器DS:存放当前执行的程序所用操作数的段地址。堆栈段寄存器SS:存放当前执行的程序所用堆栈的段地址。附加段寄存器ES:存放当前执行程序中一个辅助数据段的段地址。由CS:IP构成指令地址,SS:SP构成堆栈的栈顶地址。DS和ES用作数据段和附加段的段地址(段起始地址或段值)8086/8088微处理器的存储器管理(1) 地址线(码)与寻址范围:N条地址线,寻址范围=2N。(2) 8086有20条地址线,寻址范围为1MB由00000HFFFFFH定义。(3) 8086微处理器是一个16位结构,用户可用的寄存器均为16位:寻址64KB。(4) 8086/8088采用分段的方法对存储器进行管理。具体做法是:把1MB的存储器空间分成若干段,每段容量为64KB,每段存储器的起始地址必须是一个能被16整除的地址码,即在20位的二进制地址码中最低4位必须是“0”。每个段首地址的高16位二进制代码就是该段的段号(称段基地址)或简称段地址,段号保存在段寄存器中。我们可对段寄存器设置不同的值来使微处理器的存储器访问指向不同的段。(5) 段内的某个存储单元相对于该段段首地址的差值,称为段内偏移地址(也叫偏移量)用16位二进制代码表示。(6) 物理地址是由8086/8088芯片地址引线送出的20位地址码,它用来参加存储器的地址译码,最终读/写所访问的一个特定的存储单元。(7) 逻辑地址由某段的段地址和段内偏移地址(也叫偏移量)两部分所组成。写成:段地址:偏移地址(例如,1234H:0088H)。(8) 在硬件上起作用的是物理地址,物理地址 段基址 × 10H + 偏移地址。8、论述含参数的宏与函数的优缺点。l 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。l 函数调用是在程序运行时处理的,分配临时的内存单元。而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。l 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换。而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。l 调用函数只可得到一个返回值。而用宏可以设法得到几个结果。l 使用宏次数多时,宏展开后源程序长,因为每展开一次都使程序增长。而函数调用不使源程序变长。l 宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。一般来说,用宏来代表简短的表达式比较合适。内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样。内联函数是不能为虚函数的,但样子上写成了内联的,即隐含的内联方式。在某种情况下,虽然有些函数我们声明为所谓“内联”方式,但有时系统也会把它当作普通的函数来处理,这里的虚函数也一样,虽然同样被声明为所谓“内联”方式,但系统会把它当作非内联的方式来处理。9、CPU在上电后,进入操作系统的main()之前必须做什么工作?过程如下:(1) BIOS自举,检查硬件等;(2) 读取MBR;(3) 转到MBR执行它的代码,它会检测活动分区;(4) 把活动分区的引导扇区的引导代码装入内存;(5) 运行引导代码;(6) 引导代码装入该分区的操作系统,也就是进入main()(当然不一定叫main,如linux下叫start_kernel)执行一系列的初始化,然后最终启动登录界面,实现启动过程。附:什么是MBR?MBR是英文Master Boot Record的缩写,中文意为主引导记录。硬盘的0磁道的第一个扇区称为MBR,它的大小是512字节,而这个区域可以分为两个部分。第一部分为pre-boot区(预启动区),占446字节;第二部分是Partition table区(分区表),占66个字节,该区相当于一个小程序,作用是判断哪个分区被标记为活动分区,然后去读取那个分区的启动区,并运行该区中的代码。10、堆栈溢出一般是由什么原因导致的?答:(1) 没有回收垃圾资源;(2) 层次太深的递归调用。11、用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写出C程序。答:典型的约瑟夫环问题(可用循环链表处理)。以下为利用循环链表的核心代码:void CountPrint(LNode *head, LNode *tail, int m) LNode *cur = head; LNode *pre = tail; int cnt = m-1; while (cur && cur != cur->next) if (cnt != 0) cnt-; pre = cur; cur = cur->next; else pre->next = cur->next; printf("%d ", cur->data); free(cur); cur = pre->next; cnt = m - 1; if (cur != NULL) printf("%d ", cur->data); free(cur); cur = NULL; head = tail = NULL; printf("n");12、全局变量可不可以定义在可被多个C文件包含的头文件中?为什么?答:可以。在不同的C文件中以static形式来声明同名全局变量。可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。13、static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?答:全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。static函数与普通函数作用域不同。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。14、-1,2,7,28, ,126请问28和126中间那个数是什么?为什么?答:应该是43-1=63规律是n3-1(当n为偶数0,2,4)n3+1(当n为奇数1,3,5)15、如何用两个栈实现一个队列的功能?要求给出算法和思路!答:设2个栈为A,B,一开始均为空。入队:将新元素push入栈A。出队:(1) 判断栈B是否为空;(2) 如果为空,则将栈A中所有元素依次pop出并push到栈B;(3) 将栈B的栈顶元素pop出。这样实现的队列入队和出队的平均复杂度都还是O(1)。16、用预处理指令#define声明一个常数,用以表明1年中有多少秒。(忽略闰年问题)#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL我在这想看到几件事情:(1) #define语法的基本知识(例如:不能以分号结束,括号的使用,等等);(2) 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的;(3) 意识到这个表达式将使一个16位机的整型数溢出,因此要用到长整型符号L,告诉编译器这个常数是的长整型数;(4) 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。17、写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。#define MIN(A,B) (A) <= (B) ? (A) : (B)这个测试是为下面的目的而设的:(1) 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法;(2) 三目条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的;(3) 懂得在宏中小心地把参数用括号括起来;(4) 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?least = MIN(*p+, b);18、嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?这个问题用几个解决方案。我首选的方案是:while (1)一些程序员更喜欢如下方案:for(;)这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。第三个方案是用gotoLoop:.goto Loop;应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。19、用变量a给出下面的定义(a) 一个整型数(An integer)(b) 一个指向整型数的指针(A pointer to an integer)(c) 一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)(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 an integer)(h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(An array of ten pointers to functions that take an integer 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 a10; / An array of 10 integers(e) int *a10; / 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 (*a10)(int); / An array of 10 pointers to functions that take an integer argument and return an integer人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么做准备呢?20、关键字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 int * const a;前两个的作用是一样,a是一个常整型数。第三个和第四个的作用一样,意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第五个意味着a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我有如下的几点理由:(1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下垃圾让别人来清理的)(2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。(3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。21、关键字volatile有什么含意?并给出三个不同的例子。一个定义为volatile的变量是说这变量可能会被意想不到地改变。这样,编译器就不会去假设这个变量的值。精确地说就是,编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:(1) 并行设备的硬件寄存器(如:状态寄存器);(2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables);(3) 多线程应用中被几个任务共享的变量。回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。(1) 一个参数既可以是const还可以是volatile吗?解释为什么。(2) 一个指针可以是volatile吗?解释为什么。(3) 下面的函数有什么错误:int square(volatile int *ptr)return *ptr * *ptr;下面是答案:(1) 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。(2) 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。(3) 这段代码的确有个恶作剧。这段代码的目的是用来返指针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;22、下面的代码输出是什么,为什么?void foo(void)unsigned int a = 6;int b = -20;(a+b > 6) puts("> 6") : puts("<= 6");这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是“>6”。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是非常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。23、C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是,它做些什么?int a = 5, b = 7, c;c = a+b;这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最优处理原则,编译器应当能处理所有合法的用法。因此,上面的代码被处理成:c = a+ + b;因此,这段代码执行后a = 6, b = 7, c = 12。如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处:这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。24、用递归算法判断数组aN是否为一个递增数组。答:递归的方法如下:bool fun(int a, int n)if (n = 1)return true;if (n = 2)return an-1 >= an-2;return fun(a, n-1) && (an-1 >= an