现在的位置: 首页 > 综合 > 正文

面试题浅解

2019年05月18日 ⁄ 综合 ⁄ 共 8039字 ⁄ 字号 评论关闭

        这里是笔试题的链接,大家可以下载下来然后对照着看看~http://pan.baidu.com/s/1kTG99HH(以下使用到的程序均在X86_32位机器下进行的。)
1.题 考查隐式数据类型转换 , C 默认的是将需要自动转换的从低级转换为高级具体情况如下:
                        
那么什么时候会发生隐式数据类型转换呢 , 主要有下列的情况 ~:
a>算数运算式中低类型能转换为高类型.
b>复制表达式,右边表达式的值自动转换为左边变量的类型并赋值给它.
c>函数调用中参数传递时,系统隐式地将实参转换为形参类型,并赋值给形参.
d>函数有返回值时, 系统将隐式地返回值表达式类型转换为返回值类型,复制给调用函数.

注:不同的数据类型的数据进行操作时,应该先将其转换成相同的数据类型,然后进行操作,转换规则是由低级转换为高级.
2. 考查sizeof 计算结构和联合所占的字节数
3.考查位运算
看到题目,开始是让求平均数, 我们可以很简单地进行相加除以2,这里却用 a + (b-a)/2 这样做的好处就是防止相加的时候两个数过大, 上溢出.关于下面的功能(当然,需要给后面整体加上括号,符号优先级问题).关于这个为什么这样写, (a&b) + ((a^b)>>1) , 大家知道右移1位 , 相当于除以二. 但是 a&b和a^b是什么意思呢. 我们先来看一个用位运算时现任何两个数的加法的操作.
 :
        首先一位的加法(不产生进位的情况), 用^处理就好了.  关于处理进位情况,当两个位都为1的时候,产生进位. 那么这时就可以用&操作, 保留所有需要进位的位 , 然后对结果进行一次左移, 我们知道左移就相当于进位了. 然后其它位的处理,就使用^进行, 我们把操作分为了两部分, 一部分用来处理进位情况, 一部分用来处理不进位的那些位. 然后我们进行迭代. 直到不产生进位的时候, 也就是& <<1 操作的值为0 的时候. 代码附一下 :

01 #include <stdio.h>
02 
03 int cal (int a , int  b) {
04 
05     return  b ? cal ((a  ^  b) , (a & b)<<1) : a ;
06 }
07 
08 int main(int argc, char *argv[])
09 {
10     int a  ,  c ;
11 
12     scanf ("%d%d",&a , &c) ;
13 
14     printf ("val = %d\n" , cal (a , c) );
15 
16     return 0;
17 }

这是一个迭代的操作, 所以我们可以使用递归来完成 . 同样的想实现减法, 给b取非 +1, 很简单就完成了.
然后大家再看这个题 前面那个a&b同样的是判断有没有进位, 如果有进位,那么就保留那一位,因为要除以二,所以不用向左平移 , 后面是处理不用进位的部分 , 同样的需要除以二 , 用向右移位完成. 好啦 ~ 就是这个思路. 至于乘法 , 除法 ,我就没有写了.大家可以研究研究.
4.考查sizeof与strlen的区别,大家应该都知道了. sizeof 在编译后就已经没有了, 已经被计算出来的值替换了. 而strlen则是运行时计算的.sizeof计算的是后面对应的数据类型的字节大小,值得一提的是数组是一个构造数据类型,它是一个整体.应该以整体计算.可以看一个例子.

01 #include <stdio.h>
02 
03 int main( )
04 {
05     int a=3 , b  ;
06 
07     b = a + sizeof (a) ;
08 
09     return 0;
10 }

看一下反汇编以后的代码:

080483f0 <main>:
 80483f0:	55                   	push   ebp
 80483f1:	89 e5                	mov    ebp,esp
 80483f3:	83 ec 10             	sub    esp,0x10
 80483f6:	c7 45 fc 03 00 00 00 	mov    DWORD PTR [ebp-0x4],0x3
 80483fd:	8b 45 fc             	mov    eax,DWORD PTR [ebp-0x4]
 8048400:	83 c0 04             	add    eax,0x4
 8048403:	89 45 f8             	mov    DWORD PTR [ebp-0x8],eax
 8048406:	b8 00 00 00 00       	mov    eax,0x0
 804840b:	c9                   	leave  
 804840c:	c3                   	ret    
 804840d:	66 90                	xchg   ax,ax
 804840f:	90                   	nop

可以看到,add后面的源操作数直接就是4了.
5.考查位域
C 不允许每个位域成员超过基类型的大小.使用sizeof计算位域的大小,将返回基类型的大小,即字节对其以后的大小.关于位域,这里有很多需要注意的点,我在这一一列举一下:
1>.linux下位域可以横跨两个字节,即一个位域可以超过8bit , 但是不能超过自己的类型的sizeof.
2>.不允许对位域成员进行取地址.
3>.如果相邻字段位域类型相同,且前后加起来的大小不超过sizeof类型的大小 , 则后面字段将紧临前一字段存储.
4>.如果相邻字段类型相同但是前后加起来大于类型的sizeof大小,则另起炉灶.
5>.相邻字段间的类型不同,则不同的编译器处理不同.
6>.整个结构体大小为最宽的类型的大小的整数倍.
当然 , 这里说的这些也不能完全概括,之后的需要大家去补充。 关于上面的字节大小, 大家可以自己去验证。
下面再给大家加一点点东西,比特序(wiki给的定义):
In computingbit
numbering
 (or sometimes bit endianness)
is the convention used to identify the 
bit positions
in a 
binary
number
 or a container for such a value. The bit number starts with zero and is incremented by one for each subsequent bit position.
大概意思和little-edian和big-endian一样, 不过前者是针对于字节为单位的,现在则讨论的是以bit为单位的。先来看看这样的一道题:
 1 #include <iostream>
 2 #include <cstring>
 using namespace std;
 struct A
 5 {
 6     int a:5;
 7     int b:3;
 8 };
 int main(void)
10 {
11     char str[100] = "012345";
12     struct A c;
13     memcpy(&c, str, sizeof(A));
14     cout << c.a << endl;
15     cout << c.b << endl;
16     return 0;
17 }

猜猜结果会是怎么样,我给大家画一下(字符0对应的ASCII 值为48 也就是00110000)

自己敲一下上面的代码,应该可以得到结果 ,然后小小猜一下,也差不多出来了。

这里就涉及到LSB(least signficant bit) , MSB(most signficant bit),小端的CPU 一般采用的都是LSB  0位序 。大端的cpu可能采取LSB也可能采取MSB 。 (关于大端小端,下面会讲)
其实LSB也就是一个“高高低低”的的原则,即数据的最低位放在字节的第0位,从又至左一次递增,所以后面5位是a的数据,前面3位是b的数据,这样就可以解释结果(-16    1)了。所以MSB就和LSB相反喽 。好了,就扯这么多,详细的大家参考这个链接http://en.wikipedia.org/wiki/Bit_numbering和这位前辈的博客http://blog.chinaunix.net/uid-25909722-id-2749575.html(注意自己去考证里面的内容哦!实践是检验真理的唯一标准哦。)
6.考查while , do-while 这个 , 就不用多讲了吧 ~ , 相信大家都狠狠懂。
7.考查逗号表达式和switch语句特点,这道题详细的可以参考C99ISO文档 (135页), 里面有详细的说明,我就不赘述了。简单说说吧。
首先逗号表达式大家应该狠是清楚吧, 整个表达式的值是最后一个子表达式的值。而且switch后面的表达式必须是interger类型。 case标签必须是constant expression , 也会发生整型提升。在switch的域内 ,case标签外部的语句,均为无效语句,若是有定义语句,是允许的 , 如果是数据定义,则无论其初始化与否,其值在未重新赋值之前,都是未定义的。(GCC允许在switch中定义函数,clang则不允许,这个行为应该是未定义吧, 由编译器实现决定。尽量避免这样做。)
8.这个题就不用细讲了吧 , 记住这个就行了:a[1] = *(a+1) = *(1+a) = 1[a] ;数组元素的定位也是通过指针实现的。
9.考查数组和指针的联系。
10.考查float和double的精度问题。

都知道的是float的精度小于double的精度,很简单么,double占得字节多呗! 

默认的float小数点后最多有7位有效数字,但是能保证精度准确的只有前6位, 从第7位开始就是不确定的了。但是double就不一样了,double默认的小数点后可以有16位,但是能保证精度的只有前15位,所以,拿一个float与一个字面值常量进行比较,当然得不到你想要的结果。因为字面值常量在C里面是做为double类型来处理的。这样去比较是肯定会出问题的。所以我们比较两个数的时候最好做到类型统一,减少隐式转换带来的副作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
  
int main () {
  
        float PI = 3.14000000000000000;
          
        printf ("PI =  %.10f \n" , PI) ;
        printf ("double PI = %.10lf \n" , 3.14) ;
  
        if (PI == 3.14) {
                printf ("That's impossible !\n") ;
        }
  
        return 0 ;
}

[tutu@localhost 面试]$ ./a.out PI = 3.1400001049 double PI = 3.1400000000 [tutu@localhost 面试]$

结果很明显了

11题考查系统栈的一些知识, 首先我们应该知道,局部变量的值是存储在栈里面的。而且栈是向下增长的。看一下反汇编以后的代码~ , (推荐大家去读一下刘欢学长的浅谈缓冲区溢出之栈溢出) :

然后内存中的占空间图如下:

 

关于i为什么分配在数组之前,我测试过放在数组后定义,但是汇编代码是一样的.在崔娇娇学姐那里却不对了,i分配的时候分在了数组的下面.呵呵, 这个就有些让人费解了,找原因,最后看看gcc版本不同. 还有,在64位gcc编译的时候,不会出现这样的溢出.详细的有兴趣大家去研究研究吧~~~ .

12题考查的还是缓冲区问题,但是这个是输入缓冲区
gets是有缓冲的函数,意思是输入会先保存到一个buffer中,当buffer满或者强制刷新的时候(如:换行,fclose操作),buffer会刷新.
缓冲分为:全缓冲,行缓冲,无缓冲 .分别举例说一下吧.
全缓冲:指的是系统在填满标准IO缓冲区之后才进行实际的IO操作;注意,对于驻留在磁盘上的文件来说通常是由标准IO库实施全缓冲。
行缓冲:
标准IO在输入和输出中遇到换行符时执行IO操作;注意,当流涉及终端的时候,通常使用的是行缓冲。
无缓冲:
无缓冲指的是标准IO库不对字符进行缓冲存储;注意,标准出错流stderr通常是无缓冲的。
我们可以分别实验一下:(摘UNIX环境高级编程8章)

    #include <stdio.h>  
    #include <string.h>  
    #include <unistd.h>  

    char buf[] = "Let's start !\n";  
       
    int main()  
    {    
             pid_t pid;  
      
             if(write(STDOUT_FILENO, buf, strlen(buf)) != strlen(buf))  
             {  
                     fprintf(stderr, "write error");  
                     return 0;  
             }  
       
             printf("before fork()...\n");  
       
             if((pid = fork()) == -1)  
             {  
                     fprintf(stderr, "fork error");  
                     return 0;  
              }      
           
             sleep(2);  
     
             printf("parent process id:%d ", getppid());  
             printf("process id:%d\n", getpid());  
        
       
             return 0;  
    } 

直接运行的结果和运行时重定向,结果是有区别的, 这个例子让你很好理解行缓冲和全缓冲.当我们的程序有交互的时候,e.g.和终端交互,缓冲类型为行缓冲.来看一下下输出:

tutu@localhost 面试]$ ./a.out
Let's start !
before fork()...
parent process id:2004 process id:2991
parent process id:2991 process id:2992
[tutu@localhost 面试]$ 

我们再用重定向到文件看看:

[tutu@localhost 面试]$ ./a.out > test.txt
[tutu@localhost 面试]$ vim test.txt
Let's start !
before fork()...
parent process id:2004 process id:2996
before fork()...
parent process id:2996 process id:2997
[tutu@localhost 面试]$

这样就多了一次before fork () ... ,  没有和终端交互,那么就是全缓冲. 也看到了write 是无缓冲的(就暂时这样去理解),它直接写到对应的流中,如输出流.
还有要注意的时gets函数,绝对不推荐大家使用,因为这货不对输入长度检查,输入多少就往栈里尽可能扔多少,完全不估计溢出的情况,所以就是这货引发了1988年一次规模比较大的蠕虫病毒.原因什么的,大家有兴趣可以自己研究,欢学长的博客建议大家一定要看看.以后就赶快扔了gets吧,全部使用fgets (char *s , int size , FILE *stream) .

13.就不用讲了, 大家都会的 . 排序重组么.
14.考查static 的作用, 首先应该知道的是static 变量保存在全局静态区 , 是全局静态区 , 生存期整个程序 , 全局静态变量作用域本文件.局部的那就只能在局部.需要注意的是static 变量初始化的时候必须以常量或者常量表达式.不初始化时,编译器给你自动初始化为0 .
       题目中的初始化方式不对, 由于逗号表达式的值是不定的,并非常量或者常量的表达式.

15. 考查大端小端,实际上就是数据在内存中的存放顺序. 不同的处理器生产商有自己不同的设计. 可以参见这位大神的ce123的博客,很经典的说.我在这里也是retell.
一般小端的机器有:x86 DEC
大端机器有:POWER PC , IBM , SUN
可能是大端也可能是小端:ARM MAC
现代的CPU都可以大端小端共存的 , 通过跳线来对不同的情况作出处理.  现在我们来大概看一下自己的pc是哪种endian .

#include <stdio.h>

int main(int argc, char *argv[])
{
	int a = 0x12345678 ; 
	char * p = (char *) &a ;

	printf ("%x\n" , *p) ;
	printf ("%x\n" , *(p+1)) ;
	printf ("%x\n" , *(p+2)) ;
	printf ("%x\n" , *(p+3)) ;


	return 0;
}

方法很多,这样只是简便一点吧 . 看一下结果 :

[tutu@localhost 面试]$ ./a.out 
78
56
34
12
[tutu@localhost 面试]$ 

简单画一下 :

可以看到高位自己放在了高地址, 低位字节放在了低地址.这是典型的高高低低.我们这里的单位是字节即8bit为atomic elements.
对应大端呢, 就是倒过来了.

JAVA和网络传输都使用的大端, 所以,我们在做网络相关的开发时, 总是需要这几个函数:htonl(), htons(), ntohs(),ntohl() , 就是用来转换字节序的.详细的大家自己去google吧.

16题考查C预处理都做了什么即 , 宏替换在先还是去除注释在先 .这个在C99文档里(147页左右)有相关说明.这里我大概说一下这里有前辈的链接C预处理的步骤
1.三连符替换成相应的单字符
2.把用\字符续行的多行代码接成一行比如

#define STR "hello, "\
		"world"

替换成一行
3.把注释(不管是单行注释还是多行注释)都替换成一个空格。
4.处理Token(记号)和空白字符 (即分割)
5.宏展开,包含源文件
...
后面的大家去研究吧,更细的东西在链接里.大家有兴趣去看看吧.

17题考查枚举常量,这个相信大家都会了 , 就不多讲了.
18题又考查sizeof , 这个上面已经给大家将了,可以参照上面的方法.做一下,一目了然.
19.纯粹时考查你的细心程度. 当然这也告诉大家 , 注意自己的代码风格,格式.别到最后自己都看不懂自己的代码了.
21题 我相信大家一定会的.
20题23题24题25题都是编程题目, 这个我觉得要是我讲的画, 你的思路就会被我的想法占据. 我也相信大家都会的. 这里只简单说一下.

int my_strcmp (const char * dest ,const char * source ) {

    if ( NULL == dest || NULL == source ) {
        printf ("error\n") ;
        exit (-1) ;
    }

    while (( 0 != *dest ) && ( 0 != *source )) {
        if (tolower(*dest) == tolower(*source)) {
            dest++ ;
            source++ ;
        }
        else 
              break ;
    }
        
    return *dest - *source ;

}

思路比较简单.实现的也很简单.
23题  用两个变量(max , less_max)分别保存最大和次大,假定第一个节点最大,赋值给max , 并初始化less_max为INT_MIN , 每次向前比较, 发现比当前最大值大 , 将max更新,并同时更新less_max为max之前值, 当发现当前值比max小,这时也要比较是否比less_max大,如果大,则更新less_max.嗯 , 思路就是这样.
24.字符串匹配问题,有库函数可以调的 char * strstr(const char * , const char *),返回第一次出现子串的地址. 这里让大家手动实现,有两种方法, BF , KMP , 这是两种不同的思想.BF 是暴力去匹配, 也是我们平时用的多的. KMP则是使用一定的算法(呜呜, 有点难理解) . 这个有小组张续学长的博客在,大家上去可以仔细研究.KMP详细过程

25.实现一个范型的swap , 方法很多, 我这里直接调的库函数

void 	swap (void * dest , void * source , int size ) {
	void * p = malloc (size) ;
	memcpy (p , dest , size) ;
	memcpy (dest , source , size) ;
	memcpy (source , p , size) ;

	free (p) ;
}

抱歉!评论已关闭.