Linux工程师面试题C部分

 

1、"匈牙利命名法"有什么优缺点?(2分)


2、下面x, y, *p的值是多少,有什么问题?(2分)
int x, y, z = 2;
int *p=&z;
x=sizeof*p;
y=x/*p; // x=?, *p=?, y=?, 有什么问题?


3、下面的语句是什么意思?如何声明或定义才使它们更易懂?(10分)
int (*foo())();
int (*foo())[];
int (*foo[])();
(*(void(*)())0)();
void (*signal(int,void(*)(int)))(int);


4、本题(2分)。一般使用malloc时,需要进行强制类型转换,如:
char *s; s = (char *)malloc(31);
下面中???该如何填写,才可以正确执行强制类型转换?
int (*monthp)[31]; monthp = (???)malloc(31);


5、关于C语言运算符优先级的记忆技巧是什么?(2分)

// 下面r的值是多少

int hi, low, r;
hi=7;low=3;
r=hi<<4+low;
 
6、指针和数组的区别是什么?用一个简单的声明把它们区分开。(2分)
指针和数组的声明在什么情况下是相同的?(2分)
 
7、C语言的左值(lvalue)和右值(rvalue)的含义是什么?(2分)
 
8、为什么C语言可以实现printf(char *format, ...)这样可变参数的调用形式?这样有什么缺点?(2分)
 
9、说明C语言中术语"声明""定义""原型"的含义?(2分)
 
10、举一个例子,说明使用assert和防错代码的区别。(5分)
 
11、对语句 if else 与操作符 ? : 使用场合的比较。(2分)
 
12、编写一个函数,输入一个的整型数字,可以选择按照8/10/16进制输出字符串。
注意边界值。(5分)
 
13、本题(2分)。下面是一个16x16的黑白图标:
static unsigned short stopwatch[] = {
0x07c6,
0x1ff7,
0x383b,
0x600c,
0x600c,
0xc006,
0xc006,
0xdf06,
0xc106,
0xc106,
0x610c,
0x610c,
0x3838,
0x1ff0,
0x07c0,
0x0000,
};
如何修改声明,可以使之在源代码中形象地表现出图形的模样。
 
14、说出可以使用calendar[11][30]变量的四种类型定义。(5分)
如:int calendar[12][31]; // 二维数组 


15、使用strcmp,当字符串相同时会返回'\0'。但'\0'一般作为逻辑假,
因此下面的语句不容易理解:
if (!strcmp(s, "string")) return EQUATION;
如何经过简单修改,使之更易懂?(2分)
 
16、编写一个自己的完全C语言版本的memset函数,并且评价这个实现的性能和可移植性。(5分)
 
17、在树和图这些数据结构中,通常使用指针来组织数据。如果我们要把这些数据保存到文件中,指针是没有意义的。我们该如何解决这个问题。(2分)
 
18、用2种不同的方法计算long变量的"1"bit的个数。(2分)
 
19、任意给出一个C的基本数据类型,如何编码判断这个数据类型是有符号还是无符号的?(2分)
不得上机实验,写出下面代码的输出。解释这个行为是标准定义的,还是依赖实现的。(2分)
int i;
for (i = 0; i < 10; i++) {
    int j = i;
    printf ("%d\n", j);
}
 
20、列出5种以上你所看过的C编程的书籍,并写简要书评。(5分)
对C的评价。如果要你改造一把菜刀,使之更加安全,你是否会使用这样的菜刀,为什么?(5分)

-----华丽的分割线-----
 

 
1、"匈牙利命名法"有什么优缺点?(2分)
    首先,我们要知道有这么个东西,因为这可能是目前公认的程序员的良好素质。匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述,其中每一对象的名称都要求有明确含义,可以取对象名字全称或名字的一部分。匈牙利命名法是一位叫 Charles Simonyi 的匈牙利程序员发明的,后来他在微软呆了几年,于是这种命名法就通过微软的各种产品和文档资料向世界传播开了。
    然后,我们来讨论一下优点。虽然我很少涉足OOP,但是对这个东西多少知道一点。匈牙利命名法非常便于记忆,而且使变量名非常清晰易懂,这样,增强了代码的可读性,方便各程序员之间相互交流代码。而且由于是从微软出来的东西,现在又变成了世界范围内的编码规范,所以使在熟悉这一命名法的程序员之间交流代码变得轻松。我们一直强调要有良好的程序风格,而这就需要从命名开始。
    最后,我们再来讨论一下缺点。其实很少人说名压力命名法的缺点,所以我认为这道题的目的并不是让你骨头里挑刺,而是要看你有没有“怀疑一切,否定一切”的态度,是要看你是不是喜欢逆来顺受,要看你是不是能对一件看似既定的事情说“NO!”。那么好,给你一个机会,但也不能太离谱。凡是都有两面性,在匈牙利命名法给我们带来清晰的命名规范的同时,我们不得不承认,它的命名成本是很高的。比如一个变量,可能在程序里会出现100次,在debug阶段的时候忽然觉得它应该是另一个类型,那么好,噩梦开始,你要修改100遍啊100遍!还有匈牙利命名法的收益,有一些简单的不能再简单,清楚的不能再清楚的变量,非要加上一串帽子,即便是一个简单的函数和变量,我们是不是也要停下来仔细琢磨一下它想传递一个什么值呢?在类型越来越多,越来越复杂的情况下,匈牙利命名法可能会画蛇添足,火上浇油,比如一个名为ppp的帽子,想说明什么?是Point to Point Protocol么?显然不是,它的意思是指向指针的指针的指针,晕吧。
    好了,大致是这样,每个人都有心中的不满,导致这个题目没有标准答案,就像我说的,这道题的目的在于你是不是能够说出“不”,而不是走大众路线。引用AMD创始人Jerry Sanders的一句名言:只有偏执狂才能生存。我们虽然不偏执,但是总要有自己的想法和愤怒,不是么。
    给出一篇参考文献,值得仔细品味:
http://baike.baidu.com/view/419474.htm

 

2、下面x, y, *p的值是多少,有什么问题?(2分)
int x, y, z = 2;
int *p=&z;
x=sizeof*p;
y=x/*p; // x=?, *p=?, y=?, 有什么问题?

    有什么问题……我想说的是,如果你按这个写出来,肯定编译不过去。为什么?最明显的:y=x/*p;,这是什么东西?为p;做个注释?不至于懒得加一个括号吧,y=x/(*p);。

    类似的问题,x=sizeof(*p);是不是更好看,这是编码素质的问题。好了,解决了这个问题,可以编译了,但是我们需要一个前提:你的宿主机是多少位的?下面我们用32bits举例,因为嵌入式linux都是跑在32bits的ARM上的。一句一句来:
    int x, y, z = 2;  //x=?,y=?,z=2
    int *p=&z;   //等价于int *p;p=&z; 所以p为指向z的数据的地址,*p=z=2
    x=sizeof(*p); //考点来了,32bits的CPU运行完这句,x=4,同理,如果换成16bits的,x=2
    y=x/(*p);  //y=4/2=2
    那么最后的结果就是:x=2,y=2,*p=2。很简单吧。
 
3、下面的语句是什么意思?如何声明或定义才使它们更易懂?(10分)
int (*foo())();
int (*foo())[];
int (*foo[])();
(*(void(*)())0)();
void (*signal(int,void(*)(int)))(int);

第一个:int (*foo())();
    先一步一步的分析:首先是被左边的()括起来的*foo()。
    (1) 里面()的优先级高于*,所以foo()先是一个函数;
    (2) *foo()说明这个函数返回一个指针;
    (3) (*foo())()这个函数返回的指针指向一个函数;
    (4) int (*foo())();
    (5) 最后这个函数返回一个整型数。
    好了,我们来一遍全的:这个函数的含义是foo()函数返回的指针指向一个函数,该函数返回一个整型数。(我汗啊……)

 

第二个:int (*foo())[];
    有上面的打基础了,这回好多了。大致还是那个意思,但是就是由函数变成了数组,所以还更简单些,具体的含义是:foo()函数返回的指针指向一个具有整型数的数组。

 

第三个:int (*foo[])();
    这个就是上两个的翻版:一个存有指针的数组*foo[],该指针指向一个函数,该函数返回一个整型数。

 

第四个:(*(void(*)())0)();
    明显比上面的复杂,但是不要怕,还是那样,一层一层的分析:
    (1) 最里面的(*)()意思是一个指向函数的指针;
    (2) 加个前缀void(*)(),表示一个指向返回类型为void的函数的指针;
    (3) 套个马甲(void(*)())的意思就是类型强制转换;
    (4) (void(*)())0就是把0强制转换成前面说的那个类型的指针;
    (5) 那么好,现在(void(*)())0是一个指针了,这个指针指向了一个函数,我们用宏定义简化一下,看着太麻烦:
    (6) #define ptr (void (*)())0 ,现在(*(void(*)())0)();这个怪物就可以简化成:(*ptr)();
    (7) 别忘了ptr是一个指向函数的指针,既然这样*ptr就是函数了,那么上面的结果再次被简化成:ptr();,这不就是函数的调用么!
    至此这个怪物函数的含义和简化形式就都给出了,我就不再总结了。不过我还想再给出一个简化形式,这会对下面的问题有启示:
    使用typedef,typedef void (*ptr)(); ,然后就可以用(*(ptr)0)();来表示这个怪物函数了,其灵活性比使用#define要高。

 

第五个:void (*signal(int,void(*)(int)))(int);
    这是著名的signal函数的原型,可以如此声明:
    typedef void (*HANDLER)(int);
    HANDLER signal(int,HANDLER);
    或者:
    typedef void HANDLER(int);
    HANDLER *signal(int,HANDLER *);
    随你喜欢,反正用起来没什么区别。这个函数简单的表述就是发出一个信号(int型),然后跳转到指向函数的指针HANDLER指向的信号处理函数处。

 

    好了,都答完了,前三个我认为没有必要简化,所以只给出了后两个的简化。不知道做的对不对,八九不离十吧。
 
4、一般使用malloc时,需要进行强制类型转换,如:
char *s; s = (char *)malloc(31);
下面中???该如何填写,才可以正确执行强制类型转换? (2分)
int (*monthp)[31]; monthp = (???)malloc(31);

    说实在的,我认为题干有一点点问题。我先来说一下我的思路:malloc的用法在上一部分说过了,不清楚的去翻书,或者翻我的文章。在这里int (*monthp)[31];是一个套,代表定义一个指向有31个整型数的数组的指针,我们如果吧这个数组看成一个连续的内存区域,那么(*monthp)[31]原则上和*s没有什么区别,区别只是类型,如果只是强制类型转换,monthp = (int *)malloc(31); 就可以了。
    但是这个题干本可以出的更精彩点,比如不给提示,直接int (*monthp)[31]; monthp = (???)malloc(???);,那么我们就要考虑到开辟空间的大小了,所以 monthp = (int *)malloc(sizeof(int)*31);应该是最完美的回答。

 

5、关于C语言运算符优先级的记忆技巧是什么?(2分)
//下面r的值是多少
int hi, low, r;
hi=7;low=3;
r=hi<<4+low;

    技巧……我不知道有什么技巧,摘一篇别人总结的:(1)先(括号)内层,后(括号)外层。(2)先函数,后运算。(3)先算术,后关系,再逻辑。(4)先乘除,后加减。(5)先左,后右。(6)搞不清,加括号。
    我认为凡事都是一个熟能生巧的过程,技巧固然重要,但是以为追求技巧而忽略了原则,可谓得不偿失,早晚要栽跟头的。所以多用多练是最好的技巧。实在不行……就用括号!
    回到题目中,位运算的级别是低于算术运算的,所以r=hi<<4+low;就变成了r=hi<<(4+low);,把数带进去就是r=7<<7;意思是把7左移7位,不用我再解释了吧,r=896。

 

6、指针和数组的区别是什么?用一个简单的声明把它们区分开。(2分)
指针和数组的声明在什么情况下是相同的?(2分)

    这个问题可能在我们熟练使用指针和数组后已经很少去思考了。

 

第一个问题:
    其实数组是一个地址,指针则是指向地址的地址。举个例子:
    char array[10];
    char *pt;
    pt=array;
    char array[10];的含义是,在内存里开辟一个10个字节的空间用来存放数据,其中array是这个空间的头地址,正如刚才所说,数组是一个地址。
    char *pt;的含义是,只是定义一个指针pt,这个指针可以指向任意char型的地址,而指向的地址则存放在地址*pt中,也就是刚才说的指针是指向地址的地址。
    那么好,pt=array;的意思就是我们把*pt中存放的地址(指针)指向了数组array[10]的头地址array,这个时候pt和array辩证的统一了,区别用通俗的话说就是数组是地名,指针是路牌。但是别忘了,在这个例子中,数组实实在在的占用了10个字节的空间,而指针只占用了4个字节用来存放地址而已(假设是32bits系统)。
    最后我们总结一下区别:
    数组:保存数据;直接访问数据;用于存储数目固定且类型相同的数据;由编译器自动分配和删除;自身即为数据名。
    指针:保存地址;间接访问数据(先取得指针的内容,然后以它为地址,取得数据);通常用于动态数据结构;动态的分配和删除;通常指向隐式数据。

 

第二个问题:
    在第一个问题中,虽然指针和数组的本质定义永远不可能是一样的,但是在某种情况下也可以辩证的统一。
    char array[10]={'0','1','2','3','4','5','6','7','8','9'};
    char *pt="0123456789";
    这样一来,它们在初始化阶段都占用了同样大小的内存空间,如此情况下,也就可以认为是相同的了。
 
7、C语言的左值(lvalue)和右值(rvalue)的含义是什么?(2分)

    这个问题挺汗的,从来没仔细琢磨过其深层次的含义。阅览了一下文献,大致可以这么理解,当然也可能不对,我尽可能说的准确些:
    左值就是一个可被存储的单元,右值就是一个可被读取的数据。
    如此说笼统了一些,详细一些,就是左值必须是一个被明确了的内存存储单元,可以用来被赋值;右值必须是一个能被读出来的确确实实的值,这个值可以是数据,可以是指针,可以是结构,反正只要能被读出来的,都可以定义为右值。
    大致我就理解这么多,可能不太准确,只是作为思考的参考,我也想知道最精准的答案。
 
8、为什么C语言可以实现printf(char *format, ...)这样可变参数的调用形式?这样有什么缺点?(2分)

    关于这个问题,恐怕要上升到理论的高度,说实话,我并不清楚为什么,所以更谈不上缺点,经过阅览文献,我还是找出了一些蛛丝马迹,解释的比较笼统,希望有高人能用通俗的语言解答一下!
    可变参数的函数,即函数的参数是不确定的。为了支持可变参数函数,C语言引入新的调用协议,即C语言调用约定 __cdecl。采用C语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明。__cdecl 最大好处在于由于是调用者清理栈,它可以处理可变参数,缺点则在于它增加了程序的大小,因为在每个调用返回的时候,需要多执行一条清理栈的指令。
    好了,我能理解的就这么多,希望真正回答这个问题的时候不要再被深入提及,否则就露馅了。在这个别和书签,有时间要好好琢磨一下。

 

9、说明C语言中术语"声明""定义""原型"的含义?(2分)

    用函数举个例子,因为变量仿佛不存在原型的说法。
    函数声明和函数原型其实很相似,函数声明是把函数的名字,函数类型以及形参的类型,个数和顺序通知编译系统,以便在调用该函数时系统进行对照检查,但是函数声明并不包括函数的功能。而函数的定义则是具体指出了函数要完成的功能。
    函数的定义只能有一次,函数的原型只能有一个,而声明可以有多次,例如前面题目提到的著名的signal函数,原型是void (*signal(int,void(*)(int)))(int); ,但是可以声明成:
    typedef void (*HANDLER)(int);
    HANDLER signal(int,HANDLER);
    当然也可以声明成其他的,声明后仍然只是个名字,而且已经和原型看似不同了,但是实际上将声明翻译回去,应该与原型不冲突。
    我总结了一下,原型应该是函数最原始的形态,声明是将函数按照原型的形式声明成方便使用的形态,定义是为声明的函数添加具体的工作。
    有可能不准确,但是我认为大致是这个意思。


10、举一个例子,说明使用assert和防错代码的区别。(5分)

    我恐怕这5分得不到了,由此做出抛砖引玉吧。
    说到防错代码,我第一联想到的是#ifdef、#ifndef、#else、#endif之类的在预处理阶段的一些宏和一些条件判断。而assert函数则是在程序中使用的宏(注意,其实assert是一个宏)。在使用防错代码时,一般判断为假的时候可以使用一些语句继续调试,而使用assert后,当判断为假貌似就直接结束程序了。所以我认为这是一个区别。还有就是在debug版的程序中可以允许assert,但是在release版中不应该出现assert,而防错代码应该是可以出现的,扩展的说,就是assert不能代替条件过滤。
    还有什么区别?应该有吧,只是我实在是不知道了。多学多问,不耻下问,请高人指点啊!!!

 

11、对语句 if else 与操作符 ? : 使用场合的比较。(2分)

    这个……其实?:就是if else的简化版,并没有太多的区别,如果说场合上有所不同的话,我各说一下它们为对方所不能的地方。
    if else语句可以用在复杂的程序里,判断的结果为真时要执行N多程序,这个时候如果用?:来写恐怕写出来的东西连自己亲妈都不认识了。所以?:操作符一般都是用在简单的条件判断中,虽然也可以嵌套和干一些别的,但是程序的可读性大大降低了。
    但是操作符?:可以用在一个宏里,比如一个经典的面试题,定义一个宏MIN,比较两个数,输出小者,就可以这么写:#define MIN(A,B) ((A) <= (B) ? (A) : (B))  ,恐怕if else做不到吧。
    具体还有什么场合上要注意的,我再想想看。

 

12、编写一个函数,输入一个的整型数字,可以选择按照8/10/16进制输出字符串。注意边界值。(5分)

char* transfer(int kind,unsigned int num)
{
  char *pt;
  switch(kind)
  {
    case 8:
    {sprintf(pt,"%o",num);break;}
    case 10:
    {sprintf(pt,"%d",num);break;}
    case 16:
    {sprintf(pt,"%x",num);break;}
    default:
    {sprintf(pt,"%d",num);break;}
  }
  return pt;
}
    随手写了一个,没有具体验证,大致意思就是函数transfer的两个输入参数为kind和num,kind是一个整型数,告诉函数transfer要转换成多少进制的字符串,可以输入8、10、16,默认是10进制;num是一个无符号整型数,是被转换数。函数transfer将指针pt指向转换后的字符串地址,并将其返回。
    需要说明的是,我不太清楚题目中是要输出到屏幕还是哪里,所以就做了一个返回值,如果要输出的屏幕也是很简单的事情。还有就是边界值的问题,我们首先要分清楚系统是多少bits的,其实这个边界值的判断应该在函数外完成。还有就是题目要注意,也没说怎么注意,我只是做了一下unsigned,算是注意了吧,其实如果超出范围了,注意了也没用。

 

13、本题(2分)。下面是一个16x16的黑白图标:
static unsigned short stopwatch[] = {
0x07c6,
0x1ff7,
0x383b,
0x600c,
0x600c,
0xc006,
0xc006,
0xdf06,
0xc106,
0xc106,
0x610c,
0x610c,
0x3838,
0x1ff0,
0x07c0,
0x0000,
};
如何修改声明,可以使之在源代码中形象地表现出图形的模样。

    我们需要明白一点,在做字模或者图形的时候,在黑白状态下,0代表白,1代表黑,那么好了,我们把字模变成二进制不就可以了。十六进制前缀是0x,二进制就是0b。以上面为例子,看看结果吧,是不是很有意思:
static unsigned short stopwatch[] = {
0b0000011111000110,
0b0001111111110111,
0b0011100000111011,
0b0110000000001100,
0b0110000000001100,
0b1100000000000110,
0b1100000000000110,
0b1101111100000110,
0b1100000100000110,
0b1100000100000110,
0b0110000100001100,
0b0110000100001100,
0b0011100000111000,
0b0001111111110000,
0b0000011111000000,
0b0000000000000000,
};

 

14、说出可以使用calendar[11][30]变量的四种类型定义。(5分)
如:int calendar[12][31]; // 二维数组 

    首先,我们的脑子里在对待数组和指针的时候,应该将两者紧紧的联系起来!我们把二者结合起来通俗的讲,数组名的意思就是指向具有N个连续字节空间的头地址;指针的意思就是指向的地址是具有N个连续字节空间的头地址,可以认为是一个数组名。不知道我说的够不够通俗,反正说完我反而晕了。
既然这样,如果一个一维数组可以用一个指针代替,那么二维数组就可以用一个指向指针的指针代替了。这也就是第一个答案:
    int **calendar;
    如果处于这个思考,我们完全可以再扩展出两种方式:一种是一个指针指向一个一维数组;另一个是一个一维数组中有若干个指针。形象的定义出来就是我们要的第二种和第三种答案:
    int (*calendar)[31];
    int *calendar[12];
    好了,结合给出的int calendar[12][31];,不多不少正好四种,要是让我想第五种我都想不出来了。
最后想说一下,用指针方式变相定义数组的时候,一定要在定义后使用前分配空间,否则有可能空间是乱的,因为只告诉了编译器脑袋在哪里,并没告诉编译器身子有多长。
 
15、使用strcmp,当字符串相同时会返回'\0'。但'\0'一般作为逻辑假,因此下面的语句不容易理解:
if (!strcmp(s, "string")) return EQUATION;
如何经过简单修改,使之更易懂?(2分)

    我不知道题目中说的'\0'是哪来的,但是我明白题目的意思了,我来以我的理解翻译一下:
    strcmp这个函数是用来比较字符串的,当两个字符串相同的时候,返回值为0。但是一般情况下我们认为0是逻辑假,所以语句:if (!strcmp(s, "string")) return EQUATION; 让不懂strcmp的人比较纳闷:怎么要加“!”呢?因为加上!后,if才认为两个字符串是相同的。对于这个问题,理解了之后,再简单不过了:if (strcmp(s, "string")==0) return EQUATION; 。
 
16、编写一个自己的完全C语言版本的memset函数,并且评价这个实现的性能和可移植性。(5分)

    memset这个函数的作用大致可以理解为将从指针指向的地址开始的指定字节长度的空间全部替换为指定的值。那么我们可以很轻松的用C来自己实现这个函数:
void *memset(void *position,void value,unsigned int count)
{
  void *head=position;
  while(count--)
  {
     *(char *)position=(char)value;
     position=(char *)position+1;
  }
  return head;
}
    相信大家都能写出来,只是方式不同罢了。在这个函数中,没有调用任何别的函数,完全靠C最基本的语句实现的,所以移植性应该很好,另外,在为memory的一个字节空间的set上,可以看到,除了while的递减外,只用到了两句话,可以说没法再精简了,所以性能应该说很好。至少我是这么觉得,嘿嘿。

 

17、在树和图这些数据结构中,通常使用指针来组织数据。如果我们要把这些数据保存到文件中,指针是没有意义的。我们该如何解决这个问题。(2分)

    首先,这是一个面向对象的概念了。不写代码了,也没法写,就说说我的想法吧,想法最重要。
    我记得我在上学的时候,C语言课上做过这么一个题目,大概意思是把一个数组中的数据写到文件中,然后再读回来恢复到数组中。实现的方法是用一个给定的分隔符将数组中各个数据分隔开,连分隔符一起写入文件,然后再读回来的时候识别出分隔符,将数据依次写入数组。具体实现的方法很多。
    其实无论是树还是图,都是一种数据结构,只是多了些*lchild,*rchild,*next之类的指针,每一个struct其实都是一片连续的内存区域,也就是一串连续的数据,我们可以把这一串数据看做一个数据元素,如果为这些数据做一个索引,写入文件的时候我们就可以不去考虑指针的问题了,同样,从文件恢复数据的时候,按照这个索引重新建立指针就可以了。
    这样的做法确实可行,但是如果自己去组织文件和结构显得很繁琐,所以我们可以利用数据库。有了数据库,我们就可以用数据库的列对应struct中的各个元素,如果struct中还有struct,那么就用关系型数据库,而且数据库有现成的ID索引(行),我们就不用自己造索引了,写数据库的时候按照数据的结构,写入数据库不同表的不同行中,读的时候也一样,只是最后要重新建立指针罢了。
    不知道我的想法是否成熟,但是我是这么做的,虽然实际应用的次数很少。关于对象序列化我真的没什么研究,因为一直在用C,没有怎么研究过C++,这类的问题在《Thinking in C++》中应该有阐述。还有,如果有更好的办法一定告诉我!!
 
18、用2种不同的方法计算long变量的"1"bit的个数。(2分)

    时间有限,代码我就不写全了,只写关键代码,说一下我最先想到的思路:


第一种方法:
for(i=sizeof(long)*8;i>=0;i--)
{if(num>>i&1) count++;}
    这是用了位操作,先判断一个long型变量有多少位,然后不断位移进行与操作,如果位移后最低位是1,if为真,计数加一。如此循环,最后得到结果。

 

第二种方法:
i=sizeof(long)*8;
while(i)
{
 if(num%2) count++;
 num=num/2;
 i--;
}
    其实与前一种同出一辙,但是运用了除法和求余,原理差不多,不解释了。

 

    其他的方法,我还想到了用typedef的位定义,虽然也是方法,不过挺麻烦的。至于别的,具体实现的方法太多了,原理其实都是差不多的。
 
19、任意给出一个C的基本数据类型,如何编码判断这个数据类型是有符号还是无符号的?(2分)
不得上机实验,写出下面代码的输出。解释这个行为是标准定义的,还是依赖实现的。(2分)
int i;
for (i = 0; i < 10; i++) {
    int j = i;
    printf ("%d\n", j);
}

第一个问题:
    这仿佛是一个微软的面试题,用宏的办法判断比较简单,代码如下:
    #define ISUNSIGNED(val) ((val)*0-1>0)
    其实就是用到了有符号数可以小于0,但无符号数不可能小于0这个特性,不过这个宏对char或者其他类型的数据判断就无效了,当然谁也不会白痴到去判断一个char型的数据有没有符号。

 

第二个问题:
    输出很简单:
0
1
2
3
4
5
6
7
8
9
    至于标准定义还是依赖实现,我认为是ANSI C的标准定义,无论是for还是printf都是标准的ANSI C函数,因为当你运行这段代码的时候无需依赖任何头文件。

 

20、列出5种以上你所看过的C编程的书籍,并写简要书评。(5分)
对C的评价。如果要你改造一把菜刀,使之更加安全,你是否会使用这样的菜刀,为什么?(5分)

    这个题目应该是最后放松的送分题了,没有标准答案,看的是个人的学习经历和对C的感悟。
    我看过的C语言的书籍并不多,不到5本,看来为了凑数也要多看两本啊,现在让我深刻感受到了“书到用时方恨少”真正的含义!
    我看过最多的,对我来说最重要的两本C语言的书是:老谭的《C程序设计》和两个米国人写的《新编C语言大全》。前者是“学电脑要从娃娃抓起”般的普及教材,同时也是各个中学、大学的C语言标准教材,内容相对比较少,而且没有涉及太深的知识,非常适合初学者;后者是我很久以前花了大价钱买的一本貌似很专业的书,确实比老谭写的详细的多,因为厚度就是老谭那本的两倍,很多东西都可以在那里找到。如果数据结构也能算的话,也算一本吧,感觉就是囫囵吞枣似的书籍,应付考试用的。要是再来两本C++的书就完美了,可惜我真没看过,或者看过也忘了名字了,惭愧……

    对于C的评价,我简单的说一下自己的感觉,C语言之所以30多年来长盛不衰,因为其灵活且紧凑的代码结构,丰富的运算符和数据结构的表达,加上无与伦比的硬件操作能力,使其完美的兼顾了高级语言和低级语言的特点,同时也极大扩展了应用范围。一个优秀的编译器可以使其代码的执行率直逼汇编语言,而且又具备汇编语言不可比拟的可移植性。但是其数据的封装和语法的限制不够严格,在这点上不如其他高级语言,如C++就改善了许多。虽然指针的引入是C语言划时代的进步,但是其不安全性也逐渐显现出来,虽然C++做了改进,将指针保留了下来,但是高级语言发展到JAVA和.NET架构的时代,已经彻底取消了指针,这样做虽然提高了安全性,但是我们永远不要忘记指针给我们带来的快捷与便利。

    最后一个菜刀问题,我不是很明白题目想说什么,如果菜刀是C,改造的菜刀是C++,那么对于安全性的改进我们没有理由不接受,因为事实也是如此。在这个时刻讲究安全性的时代,改进是必然了的,但是我们不要忘了,任何所谓的安全性都是基于严谨的思维方式,即便是更安全的C++,也是C写出来的。菜刀只是一个工具,可以切菜也可以切手指头,关键是你的刀功和安全意识好不好,而不是一味强调工具是否安全。

阅读更多
个人分类: 收集
想对作者说点什么? 我来说一句

linux c经典面试笔试题

2016年12月11日 335KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭